1use zenith_core::schema as core_schema;
8use zenith_tx::schema as tx_schema;
9
10use crate::commands::serialize_pretty;
11use crate::json_types::{
12 SchemaAttr, SchemaBrandChildNode, SchemaBrandDiagCode, SchemaBrandOutput, SchemaDiagnosticCode,
13 SchemaDiagnosticsOutput, SchemaNodeContent, SchemaNodeDetail, SchemaNodeEntry,
14 SchemaNodeOutput, SchemaNodesOutput, SchemaOpDetail, SchemaOpEntry, SchemaOpFieldEntry,
15 SchemaOpOutput, SchemaOpsOutput, SchemaOverridePropEntry, SchemaOverviewOutput,
16 SchemaSurfaceOutput, SchemaTokenDetail, SchemaTokenEntry, SchemaTokenOutput,
17 SchemaTokensOutput, SchemaVariantOutput,
18};
19
20const DIAGNOSTICS_PRECEDENCE: &str = "Resolution today is the in-file `diagnostics { … }` \
22block (last-wins per code); CLI flags and config-file overrides resolve in a later unit.";
23
24pub fn overview(json: bool) -> (String, u8) {
30 let node_count = core_schema::node_kinds().len();
31 let op_count = tx_schema::op_names().len();
32 let token_type_count = core_schema::token_types().len();
33
34 if json {
35 let out = SchemaOverviewOutput {
36 schema: "zenith-schema-v1",
37 node_kinds: node_count,
38 tx_ops: op_count,
39 token_types: token_type_count,
40 };
41 (serialize_pretty(&out), 0)
42 } else {
43 let diag_count = core_schema::diagnostic_codes().len();
44 let text = format!(
45 "Zenith schema — {node_count} node kinds, {op_count} tx ops, \
46 {token_type_count} token types, 7 non-node surfaces, \
47 {diag_count} diagnostic codes\n\n\
48 Drill in:\n \
49 zenith schema nodes # list all node kinds\n \
50 zenith schema node <kind> # attributes for one kind\n \
51 zenith schema ops # list all tx ops\n \
52 zenith schema op <name> # fields + example for one op\n \
53 zenith schema tokens # list all token types\n \
54 zenith schema token <type> # value form + children + example for one type\n \
55 zenith schema page # page declaration attributes\n \
56 zenith schema asset # asset declaration attributes\n \
57 zenith schema document # document root attributes\n \
58 zenith schema variant # variants block + override entry structure\n \
59 zenith schema diagnostics # diagnostic-policy verbs + codes\n \
60 zenith schema brand # brand-contract block (allowed colors/fonts/weights)\n \
61 zenith schema block # block role declaration: vocab, props, scopes\n\n\
62 Attribute types, required-ness, and valid values are enforced by \
63 `zenith validate`."
64 );
65 (text, 0)
66 }
67}
68
69pub fn nodes(json: bool) -> (String, u8) {
73 let kinds = core_schema::node_kinds();
74
75 if json {
76 let entries: Vec<SchemaNodeEntry> = kinds
77 .iter()
78 .map(|&kind| SchemaNodeEntry {
79 kind: kind.to_owned(),
80 summary: core_schema::node_summary(kind).unwrap_or("").to_owned(),
82 })
83 .collect();
84 let out = SchemaNodesOutput {
85 schema: "zenith-schema-v1",
86 nodes: entries,
87 };
88 (serialize_pretty(&out), 0)
89 } else {
90 let mut text = String::from("node kinds:\n");
91 for &kind in kinds {
92 let summary = core_schema::node_summary(kind).unwrap_or("");
93 text.push_str(&format!(" {kind:<12} {summary}\n"));
94 }
95 (text.trim_end().to_owned(), 0)
96 }
97}
98
99pub fn node_detail(kind: &str, json: bool) -> (String, u8) {
105 let summary = match core_schema::node_summary(kind) {
106 Some(s) => s,
107 None => {
108 let valid = core_schema::node_kinds().join(", ");
109 let hint = match kind {
111 "override" | "variant" => {
112 "\nhint: 'override' and 'variant' are not node kinds — \
113 see `zenith schema variant` for the variants block and override entry."
114 }
115 _ => "",
116 };
117 let msg = format!("error: unknown node kind '{kind}'\nvalid kinds: {valid}{hint}");
118 return (msg, 1);
119 }
120 };
121
122 let attrs: Vec<SchemaAttr> = core_schema::node_attributes(kind)
123 .iter()
124 .map(|&a| SchemaAttr {
125 name: a.to_owned(),
126 ty: core_schema::attribute_type_for_kind(kind, a).to_owned(),
127 })
128 .collect();
129
130 let content_desc = core_schema::node_content(kind);
131
132 if json {
133 let content = content_desc.map(|d| SchemaNodeContent {
134 description: d.description.to_owned(),
135 example: d.example.to_owned(),
136 });
137 let out = SchemaNodeOutput {
138 schema: "zenith-schema-v1",
139 node: SchemaNodeDetail {
140 kind: kind.to_owned(),
141 summary: summary.to_owned(),
142 attributes: attrs,
143 content,
144 },
145 };
146 (serialize_pretty(&out), 0)
147 } else {
148 let mut text = format!("{kind}: {summary}\n");
149 if attrs.is_empty() {
150 text.push_str(" (no fixed attribute list)\n");
151 } else {
152 text.push_str("Attributes:\n");
153 text.push_str(&format_attr_table(&attrs));
154 }
155 if let Some(d) = content_desc {
156 text.push_str("\nContent:\n");
157 text.push_str(&format!(" {}\n", d.description));
158 text.push_str(" Example:\n");
159 for line in d.example.lines() {
160 text.push_str(&format!(" {line}\n"));
161 }
162 }
163 text.push_str(
164 "\nNote: required-ness and full valid values are enforced by\n\
165 `zenith validate` (the authoritative diagnostic loop).",
166 );
167 (text.trim_end().to_owned(), 0)
168 }
169}
170
171pub fn ops(json: bool) -> (String, u8) {
175 let names = tx_schema::op_names();
176
177 if json {
178 let entries: Vec<SchemaOpEntry> = names
179 .iter()
180 .map(|&name| SchemaOpEntry {
181 op: name.to_owned(),
182 summary: tx_schema::op_summary(name).unwrap_or("").to_owned(),
183 })
184 .collect();
185 let out = SchemaOpsOutput {
186 schema: "zenith-schema-v1",
187 ops: entries,
188 };
189 (serialize_pretty(&out), 0)
190 } else {
191 let mut text = String::from("tx ops:\n");
192 for &name in names {
193 let summary = tx_schema::op_summary(name).unwrap_or("");
194 text.push_str(&format!(" {name:<24} {summary}\n"));
195 }
196 (text.trim_end().to_owned(), 0)
197 }
198}
199
200pub fn op_detail(name: &str, json: bool) -> (String, u8) {
204 let summary = match tx_schema::op_summary(name) {
205 Some(s) => s,
206 None => {
207 let valid = tx_schema::op_names().join(", ");
208 let msg = format!("error: unknown op '{name}'\nvalid ops: {valid}");
209 return (msg, 1);
210 }
211 };
212
213 let fields = tx_schema::op_fields(name).unwrap_or(&[]);
216 let example = tx_schema::op_example(name).unwrap_or("");
217
218 if json {
219 let field_entries: Vec<SchemaOpFieldEntry> = fields
220 .iter()
221 .map(|f| SchemaOpFieldEntry {
222 name: f.name.to_owned(),
223 ty: f.ty.to_owned(),
224 required: f.required,
225 })
226 .collect();
227 let out = SchemaOpOutput {
228 schema: "zenith-schema-v1",
229 op: SchemaOpDetail {
230 op: name.to_owned(),
231 summary: summary.to_owned(),
232 fields: field_entries,
233 example: example.to_owned(),
234 },
235 };
236 (serialize_pretty(&out), 0)
237 } else {
238 let mut text = format!("{name}: {summary}\n");
239 if fields.is_empty() {
240 text.push_str("\nFields: (none — this op carries no fields beyond the \"op\" tag)\n");
241 } else {
242 text.push_str("\nFields:\n");
243 for f in fields {
244 let req = if f.required { ", required" } else { "" };
245 text.push_str(&format!(" {:<20} ({}{req})\n", f.name, f.ty));
246 }
247 }
248 text.push_str(&format!("\nExample:\n {example}"));
249 (text, 0)
250 }
251}
252
253pub fn tokens(json: bool) -> (String, u8) {
259 let types = core_schema::token_types();
260
261 if json {
262 let entries: Vec<SchemaTokenEntry> = types
263 .iter()
264 .map(|&ty| SchemaTokenEntry {
265 ty: ty.to_owned(),
266 summary: core_schema::token_type_summary(ty).unwrap_or("").to_owned(),
268 })
269 .collect();
270 let out = SchemaTokensOutput {
271 schema: "zenith-schema-v1",
272 token_types: entries,
273 };
274 (serialize_pretty(&out), 0)
275 } else {
276 let mut text = String::from("token types:\n");
277 for &ty in types {
278 let summary = core_schema::token_type_summary(ty).unwrap_or("");
279 text.push_str(&format!(" {ty:<12} {summary}\n"));
280 }
281 (text.trim_end().to_owned(), 0)
282 }
283}
284
285pub fn token_detail(ty: &str, json: bool) -> (String, u8) {
290 let desc = match core_schema::token_type_descriptor(ty) {
291 Some(d) => d,
292 None => {
293 let valid = core_schema::token_types().join(", ");
294 let msg = format!("error: unknown token type '{ty}'\nvalid types: {valid}");
295 return (msg, 1);
296 }
297 };
298
299 if json {
300 let out = SchemaTokenOutput {
301 schema: "zenith-schema-v1",
302 token: SchemaTokenDetail {
303 ty: desc.type_name.to_owned(),
304 summary: desc.summary.to_owned(),
305 value_form: desc.value_form.to_owned(),
306 child_nodes: desc.child_nodes.to_owned(),
307 example: desc.example.to_owned(),
308 },
309 };
310 (serialize_pretty(&out), 0)
311 } else {
312 let mut text = format!("{}: {}\n", desc.type_name, desc.summary);
313 if !desc.value_form.is_empty() {
314 text.push_str(&format!("\nValue form:\n {}\n", desc.value_form));
315 }
316 if !desc.child_nodes.is_empty() {
317 text.push_str(&format!("\nChild nodes:\n {}\n", desc.child_nodes));
318 }
319 text.push_str(&format!(
320 "\nExample:\n {}",
321 desc.example.replace('\n', "\n ")
322 ));
323 (text, 0)
324 }
325}
326
327pub fn page(json: bool) -> (String, u8) {
333 surface_detail(
334 "page",
335 core_schema::page_summary(),
336 core_schema::page_attributes(),
337 json,
338 )
339}
340
341pub fn asset(json: bool) -> (String, u8) {
345 surface_detail(
346 "asset",
347 core_schema::asset_summary(),
348 core_schema::asset_attributes(),
349 json,
350 )
351}
352
353pub fn document(json: bool) -> (String, u8) {
357 surface_detail(
358 "document",
359 core_schema::document_summary(),
360 core_schema::document_attributes(),
361 json,
362 )
363}
364
365fn surface_detail(
367 surface: &'static str,
368 summary: &'static str,
369 raw_attrs: Vec<&'static str>,
370 json: bool,
371) -> (String, u8) {
372 let attrs: Vec<SchemaAttr> = raw_attrs
373 .iter()
374 .map(|&a| SchemaAttr {
375 name: a.to_owned(),
376 ty: core_schema::attribute_type(a).to_owned(),
377 })
378 .collect();
379
380 if json {
381 let out = SchemaSurfaceOutput {
382 schema: "zenith-schema-v1",
383 surface,
384 summary: summary.to_owned(),
385 attributes: attrs,
386 };
387 (serialize_pretty(&out), 0)
388 } else {
389 let mut text = format!("{surface}: {summary}\n");
390 if attrs.is_empty() {
391 text.push_str(" (no fixed attribute list)\n");
392 } else {
393 text.push_str("Attributes:\n");
394 text.push_str(&format_attr_table(&attrs));
395 }
396 text.push_str(
397 "\nNote: required-ness and full valid values are enforced by\n\
398 `zenith validate` (the authoritative diagnostic loop).",
399 );
400 (text.trim_end().to_owned(), 0)
401 }
402}
403
404pub fn variant(json: bool) -> (String, u8) {
408 let desc = core_schema::variant_descriptor();
409
410 if json {
411 let props: Vec<SchemaOverridePropEntry> = desc
412 .override_props
413 .iter()
414 .map(|&(name, ty, required)| SchemaOverridePropEntry {
415 name: name.to_owned(),
416 ty: ty.to_owned(),
417 required,
418 })
419 .collect();
420 let out = SchemaVariantOutput {
421 schema: "zenith-schema-v1",
422 summary: desc.summary.to_owned(),
423 block_structure: desc.block_structure.to_owned(),
424 variant_node: desc.variant_node.to_owned(),
425 override_entry: desc.override_entry.to_owned(),
426 override_props: props,
427 example: desc.example.to_owned(),
428 };
429 (serialize_pretty(&out), 0)
430 } else {
431 let mut text = format!("variant: {}\n", desc.summary);
432
433 text.push_str(&format!("\nBlock structure:\n {}\n", desc.block_structure));
434 text.push_str(&format!(
435 "\nvariant node:\n {}\n",
436 desc.variant_node.replace('\n', "\n ")
437 ));
438 text.push_str(&format!(
439 "\noverride entry:\n {}\n",
440 desc.override_entry.replace('\n', "\n ")
441 ));
442
443 text.push_str("\nOverride properties:\n");
444 let col_width = desc
445 .override_props
446 .iter()
447 .map(|(n, _, _)| n.len())
448 .max()
449 .unwrap_or(0);
450 for &(name, ty, required) in desc.override_props {
451 let req = if required { ", required" } else { "" };
452 text.push_str(&format!(
453 " {:<col_width$} — ({ty}{req})\n",
454 name,
455 col_width = col_width,
456 ));
457 }
458
459 text.push_str(&format!(
460 "\nExample:\n {}",
461 desc.example.replace('\n', "\n ")
462 ));
463 (text, 0)
464 }
465}
466
467pub fn diagnostics(json: bool) -> (String, u8) {
472 let summary = core_schema::diagnostics_summary();
473 let verbs = core_schema::diagnostics_verbs();
474 let catalog = core_schema::diagnostic_codes();
475
476 if json {
477 let codes: Vec<SchemaDiagnosticCode> = catalog
478 .iter()
479 .map(|info| SchemaDiagnosticCode {
480 code: info.code.to_owned(),
481 severity: crate::json_types::severity_str(&info.severity).to_owned(),
482 summary: info.summary.to_owned(),
483 governable: info.is_governable(),
484 })
485 .collect();
486 let out = SchemaDiagnosticsOutput {
487 schema: "zenith-schema-v1",
488 summary: summary.to_owned(),
489 verbs: verbs.iter().map(|&v| v.to_owned()).collect(),
490 precedence: DIAGNOSTICS_PRECEDENCE,
491 codes,
492 };
493 (serialize_pretty(&out), 0)
494 } else {
495 let mut text = format!("diagnostics: {summary}\n\n");
496
497 text.push_str("Policy verbs (in a root `diagnostics { … }` block):\n");
498 text.push_str(" allow \"<code>\" — suppress this advisory/warning\n");
499 text.push_str(" deny \"<code>\" — elevate to a blocking Error (CI gate)\n");
500 text.push_str(" warn \"<code>\" — force to a Warning\n\n");
501
502 text.push_str("Precedence: ");
503 text.push_str(DIAGNOSTICS_PRECEDENCE);
504 text.push_str("\n\n");
505
506 text.push_str("Governable codes (code · severity · summary):\n");
508 let governable: Vec<&_> = catalog.iter().filter(|i| i.is_governable()).collect();
509 let col_width = governable.iter().map(|i| i.code.len()).max().unwrap_or(0);
510 for info in &governable {
511 text.push_str(&format!(
512 " {:<col_width$} {:<9} {}\n",
513 info.code,
514 crate::json_types::severity_str(&info.severity),
515 info.summary,
516 col_width = col_width,
517 ));
518 }
519
520 text.push_str(
521 "\nNote: integrity Errors cannot be suppressed or weakened — `allow`/`warn` on an \
522 Error code is reported as `policy.ineffective_on_error`. Only governable \
523 (Warning/Advisory) codes are listed above; for the COMPLETE catalog including the \
524 always-enforced Error codes (e.g. `token.raw_visual_literal`), run \
525 `zenith schema diagnostics --json`.",
526 );
527 (text.trim_end().to_owned(), 0)
528 }
529}
530
531pub fn brand(json: bool) -> (String, u8) {
535 const SUMMARY: &str = "Declare the allowed palette, fonts, and weights for this document; \
536 resolved token values outside the contract emit Warnings that can be elevated to \
537 blocking Errors for a CI gate.";
538
539 const PLACEMENT: &str = "Top-level child of the root `zenith version=1 { … }` node, \
540 sibling of `tokens`, `assets`, and `document`. At most one `brand { … }` block \
541 per document.";
542
543 const ABSENT_MEANS: &str = "An absent child node means that category is UNCONSTRAINED — \
544 omitting `colors` allows any color; omitting `fonts` allows any font family; \
545 omitting `weights` allows any weight. A completely empty `brand {}` block constrains \
546 nothing.";
547
548 const CHILD_NODES: &[SchemaBrandChildNode] = &[
549 SchemaBrandChildNode {
550 node: "colors",
551 syntax: r##"colors "#rrggbb" "#rrggbb" …"##,
552 description: "Allowed sRGB hex colors (case-insensitive). Color tokens and the \
553 sRGB-equivalent of CMYK tokens are compared against this list. Any resolved \
554 color token whose value is absent from this set emits `brand.color_off_palette`.",
555 },
556 SchemaBrandChildNode {
557 node: "fonts",
558 syntax: r#"fonts "Family Name" "Another Family" …"#,
559 description: "Allowed font family names. Any resolved fontFamily token whose value \
560 is not in this set emits `brand.font_not_allowed`.",
561 },
562 SchemaBrandChildNode {
563 node: "weights",
564 syntax: "weights 400 700 …",
565 description: "Allowed font weights as bare integers (100–900 in multiples of 100). \
566 Any resolved fontWeight token whose value is not in this set emits \
567 `brand.weight_not_allowed`.",
568 },
569 ];
570
571 const DIAG_CODES: &[SchemaBrandDiagCode] = &[
572 SchemaBrandDiagCode {
573 code: "brand.color_off_palette",
574 severity: "warning",
575 summary: "Resolved color token value is not in the declared brand palette.",
576 },
577 SchemaBrandDiagCode {
578 code: "brand.font_not_allowed",
579 severity: "warning",
580 summary: "Resolved fontFamily token value is not in the declared brand font list.",
581 },
582 SchemaBrandDiagCode {
583 code: "brand.weight_not_allowed",
584 severity: "warning",
585 summary: "Resolved fontWeight token value is not in the declared brand weight list.",
586 },
587 ];
588
589 const EXAMPLE: &str = concat!(
590 "zenith version=1 {\n",
591 " brand {\n",
592 " colors \"#0b1f33\" \"#1b6cf0\" \"#ffffff\"\n",
593 " fonts \"Noto Sans\"\n",
594 " weights 400 700\n",
595 " }\n",
596 " tokens format=\"zenith-token-v1\" {\n",
597 " token id=\"color.primary\" type=\"color\" value=\"#1b6cf0\"\n",
598 " token id=\"color.bg\" type=\"color\" value=\"#ffffff\"\n",
599 " token id=\"font.body\" type=\"fontFamily\" value=\"Noto Sans\"\n",
600 " token id=\"weight.bold\" type=\"fontWeight\" value=700\n",
601 " }\n",
602 " document id=\"doc\" title=\"Brand demo\" {}\n",
603 "}\n",
604 "\n",
605 "# CI gate — make off-contract values block the build:\n",
606 "# zenith validate doc.zen --deny brand.color_off_palette\n",
607 "#\n",
608 "# Or declare the policy in-file:\n",
609 "# diagnostics { deny \"brand.color_off_palette\" }"
610 );
611
612 if json {
613 let child_nodes: Vec<SchemaBrandChildNode> = CHILD_NODES
614 .iter()
615 .map(|n| SchemaBrandChildNode {
616 node: n.node,
617 syntax: n.syntax,
618 description: n.description,
619 })
620 .collect();
621 let diag_codes: Vec<SchemaBrandDiagCode> = DIAG_CODES
622 .iter()
623 .map(|d| SchemaBrandDiagCode {
624 code: d.code,
625 severity: d.severity,
626 summary: d.summary,
627 })
628 .collect();
629 let out = SchemaBrandOutput {
630 schema: "zenith-schema-v1",
631 summary: SUMMARY.to_owned(),
632 placement: PLACEMENT,
633 child_nodes,
634 absent_means: ABSENT_MEANS,
635 diagnostic_codes: diag_codes,
636 example: EXAMPLE,
637 };
638 (serialize_pretty(&out), 0)
639 } else {
640 let mut text = format!("brand: {SUMMARY}\n");
641
642 text.push_str(&format!("\nPlacement:\n {PLACEMENT}\n"));
643
644 text.push_str("\nChild nodes (all optional):\n");
645 for node in CHILD_NODES {
646 text.push_str(&format!(
647 " {:<8} syntax: {}\n {}\n",
648 node.node, node.syntax, node.description
649 ));
650 }
651
652 text.push_str(&format!("\nAbsent-child rule:\n {ABSENT_MEANS}\n"));
653
654 text.push_str("\nDiagnostic codes (Warning by default):\n");
655 let col = DIAG_CODES.iter().map(|d| d.code.len()).max().unwrap_or(0);
656 for d in DIAG_CODES {
657 text.push_str(&format!(
658 " {:<col$} — {}\n",
659 d.code,
660 d.summary,
661 col = col,
662 ));
663 }
664
665 text.push_str(
666 "\nCI gate:\n \
667 Elevate to blocking Errors with `--deny <code>` on the CLI:\n \
668 zenith validate doc.zen --deny brand.color_off_palette\n \
669 Or declare the policy in-file (cross-reference `zenith schema diagnostics`):\n \
670 diagnostics { deny \"brand.color_off_palette\" }\n",
671 );
672
673 text.push_str(&format!("\nExample:\n {}", EXAMPLE.replace('\n', "\n ")));
674 (text, 0)
675 }
676}
677
678pub fn block(json: bool) -> (String, u8) {
683 let role_vocab = zenith_core::BLOCK_ROLE_VOCAB;
685
686 const PROPS: &[(&str, &str)] = &[
687 (
688 "role",
689 "string — required; the markdown block role to target (see vocab above)",
690 ),
691 (
692 "font-family",
693 "token ref or literal string — override font family for this role",
694 ),
695 (
696 "font-size",
697 "token ref, (px) literal, or dimension — override font size",
698 ),
699 (
700 "font-weight",
701 "token ref or literal — override font weight (100–900)",
702 ),
703 (
704 "fill",
705 "token ref or color literal — override text fill color",
706 ),
707 (
708 "align",
709 r#"string — text alignment: "left", "center", "right", "justify""#,
710 ),
711 ("italic", "#true / #false — override italic rendering"),
712 (
713 "space-before",
714 "(px) or other dimension — extra space above the block",
715 ),
716 (
717 "space-after",
718 "(px) or other dimension — extra space below the block",
719 ),
720 ];
721
722 const SCOPES: &[(&str, &str)] = &[
723 (
724 "document",
725 "Declared as a direct child of the `document id=… { … }` block. \
726 Lowest cascade precedence — applies when neither the page nor the text node \
727 declares a matching role.",
728 ),
729 (
730 "page",
731 "Declared as a child of a `page id=… { … }` block (alongside `safe-zone` and `fold`). \
732 Middle cascade precedence — overrides the document scope for this page's text nodes.",
733 ),
734 (
735 "text",
736 "Declared as a child of a `text id=… { … }` block (before `span` children). \
737 Highest cascade precedence — overrides both document and page scope for this node.",
738 ),
739 ];
740
741 const CASCADE_NOTE: &str = "Cascade precedence: text > page > document. \
742 When the same `role` is declared at multiple scopes, the most-specific scope wins \
743 property-by-property (fine-grained merging is a later unit; in this unit the whole \
744 `BlockStyle` struct is stored per scope and the layout engine merges at consume time). \
745 Block decls are consumed ONLY on text nodes with `format=\"markdown\"`; they have no \
746 effect on plain-text or non-markdown nodes.";
747
748 const SOURCE_SYNTAX: &[(&str, &str)] = &[
751 (
752 "h1..h6",
753 r##"# H1 ## H2 ### H3 #### H4 ##### H5 ###### H6 (ATX headings)"##,
754 ),
755 ("p", "blank line between paragraphs"),
756 ("blockquote", "> text on its own line"),
757 (
758 "li",
759 "- item or * item or + item (unordered); 1. item (ordered)",
760 ),
761 (
762 "code-block",
763 "``` (optional lang)\ncode lines\n``` (fenced; lang after opening fence is optional)",
764 ),
765 ("hr", "--- or *** or ___ on its own line"),
766 ];
767 const INLINE_SYNTAX: &str =
770 "**bold** *italic* ~~strike~~ ==highlight== ++underline++ `code` [label](url)";
771
772 const V1_LIMITS: &str = "v1 limitation: in a chain flow, code-block backgrounds and --- rules are not drawn \
773 and blockquote/list indent is not applied. These render fully only in a single \
774 non-chained text box.";
775
776 const EXAMPLE: &str = concat!(
777 "document id=\"doc.main\" {\n",
778 " block role=\"h1\" font-size=(token)\"size.h1\" font-weight=(token)\"weight.bold\" space-after=(px)16\n",
779 " block role=\"p\" space-after=(px)8\n",
780 " page id=\"pg.cover\" w=(px)1280 h=(px)720 {\n",
781 " block role=\"h1\" fill=(token)\"color.accent\"\n",
782 " text id=\"body\" format=\"markdown\" src=\"article.md\" x=(px)80 y=(px)80 w=(px)1120 h=(px)560 {\n",
783 " block role=\"p\" space-after=(px)4\n",
784 " }\n",
785 " }\n",
786 "}",
787 );
788
789 if json {
790 use serde_json::{json, to_string_pretty};
791 let roles: Vec<&str> = role_vocab.to_vec();
792 let props: Vec<serde_json::Value> = PROPS
793 .iter()
794 .map(|(name, desc)| json!({ "name": name, "description": desc }))
795 .collect();
796 let scopes: Vec<serde_json::Value> = SCOPES
797 .iter()
798 .map(|(name, desc)| json!({ "scope": name, "description": desc }))
799 .collect();
800 let source_syntax: Vec<serde_json::Value> = SOURCE_SYNTAX
801 .iter()
802 .map(|(role, syntax)| json!({ "role": role, "source_syntax": syntax }))
803 .collect();
804 let out = json!({
805 "schema": "zenith-schema-v1",
806 "surface": "block",
807 "role_vocabulary": roles,
808 "markdown_source_syntax": source_syntax,
809 "markdown_inline_syntax": INLINE_SYNTAX,
810 "v1_limitations": V1_LIMITS,
811 "properties": props,
812 "scopes": scopes,
813 "cascade": CASCADE_NOTE,
814 "example": EXAMPLE,
815 });
816 (to_string_pretty(&out).unwrap_or_else(|e| e.to_string()), 0)
817 } else {
818 let mut text = String::new();
819 text.push_str("block role=\"…\" — per-role markdown block style declaration\n");
820 text.push_str("\nRole vocabulary and markdown source syntax:\n");
821 let col = SOURCE_SYNTAX
822 .iter()
823 .map(|(r, _)| r.len())
824 .max()
825 .unwrap_or(0);
826 for (role, syntax) in SOURCE_SYNTAX {
827 text.push_str(&format!(" {role:<col$} {syntax}\n", col = col));
828 }
829 text.push_str(&format!(
830 "\nInline marks (format=\"markdown\"):\n {INLINE_SYNTAX}\n"
831 ));
832 text.push_str(&format!("\nv1 limitations:\n {V1_LIMITS}\n"));
833 text.push_str("\nProperties (on block role=\"…\" declarations):\n");
834 for (name, desc) in PROPS {
835 text.push_str(&format!(" {name:<16} {desc}\n"));
836 }
837 text.push_str("\nScopes:\n");
838 for (scope, desc) in SCOPES {
839 text.push_str(&format!(" {scope:<12} {desc}\n"));
840 }
841 text.push_str(&format!("\nCascade:\n {CASCADE_NOTE}\n"));
842 text.push_str(&format!("\nExample:\n {}", EXAMPLE.replace('\n', "\n ")));
843 (text, 0)
844 }
845}
846
847fn format_attr_table(attrs: &[SchemaAttr]) -> String {
854 let col_width = attrs.iter().map(|a| a.name.len()).max().unwrap_or(0);
855
856 let mut out = String::new();
857 for attr in attrs {
858 out.push_str(&format!(
859 " {:<col_width$} — {}\n",
860 attr.name,
861 attr.ty,
862 col_width = col_width,
863 ));
864 }
865 out
866}
867
868#[cfg(test)]
871mod tests {
872 use super::*;
873
874 #[test]
875 fn overview_human_contains_counts() {
876 let (text, code) = overview(false);
877 assert_eq!(code, 0);
878 assert!(text.contains("node kinds"), "must mention node kinds");
879 assert!(text.contains("tx ops"), "must mention tx ops");
880 }
881
882 #[test]
883 fn overview_json_schema_field() {
884 let (text, code) = overview(true);
885 assert_eq!(code, 0);
886 assert!(
887 text.contains("zenith-schema-v1"),
888 "JSON must carry schema field"
889 );
890 assert!(
891 text.contains("node_kinds"),
892 "JSON must carry node_kinds count"
893 );
894 }
895
896 #[test]
897 fn nodes_human_contains_rect() {
898 let (text, code) = nodes(false);
899 assert_eq!(code, 0);
900 assert!(text.contains("rect"), "must list rect kind");
901 assert!(text.contains("Rectangle"), "must include rect summary");
902 }
903
904 #[test]
905 fn nodes_json_schema_field() {
906 let (text, code) = nodes(true);
907 assert_eq!(code, 0);
908 assert!(text.contains("zenith-schema-v1"));
909 assert!(text.contains("\"kind\""));
910 }
911
912 #[test]
913 fn node_detail_known_kind() {
914 let (text, code) = node_detail("rect", false);
915 assert_eq!(code, 0);
916 assert!(text.contains("rect"), "must name the kind");
917 assert!(text.contains("Attributes:"), "must list attributes");
918 assert!(text.contains("fill"), "rect must have a fill attribute");
919 assert!(
920 text.contains("token ref"),
921 "fill must show its type hint (token ref)"
922 );
923 assert!(text.contains("—"), "attributes must use — separator");
924 assert!(
925 text.contains("zenith validate"),
926 "must mention zenith validate for types"
927 );
928 }
929
930 #[test]
931 fn node_detail_human_shows_name_and_type() {
932 let (text, code) = node_detail("rect", false);
934 assert_eq!(code, 0);
935 assert!(text.contains("x "), "must list x attribute; got:\n{text}");
937 assert!(
938 text.contains("px literal"),
939 "x must show px literal type; got:\n{text}"
940 );
941 assert!(
942 text.contains("token ref: color/gradient"),
943 "fill must show token ref type; got:\n{text}"
944 );
945 }
946
947 #[test]
948 fn node_detail_json_known_kind() {
949 let (text, code) = node_detail("pattern", true);
950 assert_eq!(code, 0);
951 assert!(text.contains("zenith-schema-v1"));
952 assert!(text.contains("\"kind\""));
953 assert!(text.contains("\"attributes\""));
954 assert!(
956 text.contains("\"name\""),
957 "attribute objects must have name field"
958 );
959 assert!(
960 text.contains("\"ty\""),
961 "attribute objects must have ty field"
962 );
963 }
964
965 #[test]
966 fn node_detail_json_attr_has_type_hint() {
967 let (text, code) = node_detail("rect", true);
968 assert_eq!(code, 0);
969 assert!(
971 text.contains("\"fill\""),
972 "fill attribute must appear; got:\n{text}"
973 );
974 assert!(
975 text.contains("token ref"),
976 "fill type must be a token ref; got:\n{text}"
977 );
978 assert!(
980 text.contains("px literal"),
981 "x must have px literal type; got:\n{text}"
982 );
983 }
984
985 #[test]
986 fn node_detail_unknown_kind_returns_error() {
987 let (text, code) = node_detail("not-a-kind", false);
988 assert_eq!(code, 1);
989 assert!(
990 text.contains("unknown node kind"),
991 "must report unknown kind"
992 );
993 assert!(text.contains("valid kinds"), "must list valid kinds");
994 }
995
996 #[test]
997 fn ops_human_contains_set_fill() {
998 let (text, code) = ops(false);
999 assert_eq!(code, 0);
1000 assert!(text.contains("set_fill"), "must list set_fill op");
1001 }
1002
1003 #[test]
1004 fn ops_json_schema_field() {
1005 let (text, code) = ops(true);
1006 assert_eq!(code, 0);
1007 assert!(text.contains("zenith-schema-v1"));
1008 assert!(text.contains("\"op\""));
1009 }
1010
1011 #[test]
1012 fn op_detail_known_op() {
1013 let (text, code) = op_detail("set_fill", false);
1014 assert_eq!(code, 0);
1015 assert!(text.contains("set_fill"), "must name the op");
1016 assert!(text.contains("fill"), "must mention the fill field");
1017 assert!(text.contains("Fields:"), "must include Fields section");
1018 assert!(text.contains("Example:"), "must include Example section");
1019 }
1020
1021 #[test]
1022 fn op_detail_json_known_op() {
1023 let (text, code) = op_detail("add_node", true);
1024 assert_eq!(code, 0);
1025 assert!(text.contains("zenith-schema-v1"));
1026 assert!(text.contains("\"op\""));
1027 assert!(
1028 text.contains("\"fields\""),
1029 "JSON must include fields array"
1030 );
1031 assert!(
1032 text.contains("\"example\""),
1033 "JSON must include example string"
1034 );
1035 }
1036
1037 #[test]
1038 fn op_detail_detach_pattern_human() {
1039 let (text, code) = op_detail("detach_pattern", false);
1040 assert_eq!(code, 0);
1041 assert!(text.contains("detach_pattern"));
1042 assert!(text.contains("Fields:"));
1043 assert!(text.contains("node"));
1044 assert!(text.contains("Example:"));
1045 }
1046
1047 #[test]
1048 fn op_detail_set_fill_json_has_node_and_fill_fields() {
1049 let (text, code) = op_detail("set_fill", true);
1050 assert_eq!(code, 0);
1051 assert!(text.contains("\"node\""), "fields must include node");
1052 assert!(text.contains("\"fill\""), "fields must include fill");
1053 assert!(text.contains("token ref"), "fill type must be token ref");
1054 assert!(
1055 text.contains("color.brand"),
1056 "example must use realistic value"
1057 );
1058 }
1059
1060 #[test]
1061 fn op_detail_unknown_op_returns_error() {
1062 let (text, code) = op_detail("not_an_op", false);
1063 assert_eq!(code, 1);
1064 assert!(text.contains("unknown op"), "must report unknown op");
1065 assert!(text.contains("valid ops"), "must list valid ops");
1066 }
1067
1068 #[test]
1069 fn overview_mentions_new_surfaces() {
1070 let (text, code) = overview(false);
1071 assert_eq!(code, 0);
1072 assert!(text.contains("page"), "overview must mention page surface");
1073 assert!(
1074 text.contains("asset"),
1075 "overview must mention asset surface"
1076 );
1077 assert!(
1078 text.contains("document"),
1079 "overview must mention document surface"
1080 );
1081 }
1082
1083 #[test]
1084 fn page_human_contains_geometry_attrs() {
1085 let (text, code) = page(false);
1086 assert_eq!(code, 0);
1087 assert!(text.contains("page"), "must name the surface");
1088 assert!(text.contains("Attributes:"), "must list attributes");
1089 assert!(text.contains("w"), "page must have w attribute");
1090 assert!(text.contains("h"), "page must have h attribute");
1091 assert!(text.contains("—"), "attributes must use — separator");
1092 assert!(
1093 text.contains("px literal"),
1094 "w/h must show px literal type hint"
1095 );
1096 assert!(
1097 text.contains("zenith validate"),
1098 "must mention zenith validate"
1099 );
1100 }
1101
1102 #[test]
1103 fn page_json_schema_field() {
1104 let (text, code) = page(true);
1105 assert_eq!(code, 0);
1106 assert!(text.contains("zenith-schema-v1"));
1107 assert!(text.contains("\"surface\""));
1108 assert!(text.contains("\"attributes\""));
1109 assert!(text.contains("\"page\""));
1110 assert!(
1112 text.contains("\"name\""),
1113 "attribute objects must have name field"
1114 );
1115 assert!(
1116 text.contains("\"ty\""),
1117 "attribute objects must have ty field"
1118 );
1119 }
1120
1121 #[test]
1122 fn asset_human_contains_provenance_attrs() {
1123 let (text, code) = asset(false);
1124 assert_eq!(code, 0);
1125 assert!(text.contains("asset"), "must name the surface");
1126 assert!(text.contains("sha256"), "asset must include sha256");
1127 assert!(text.contains("ai-prompt"), "asset must include ai-prompt");
1128 }
1129
1130 #[test]
1131 fn asset_json_schema_field() {
1132 let (text, code) = asset(true);
1133 assert_eq!(code, 0);
1134 assert!(text.contains("zenith-schema-v1"));
1135 assert!(text.contains("\"asset\""));
1136 }
1137
1138 #[test]
1139 fn document_human_contains_root_attrs() {
1140 let (text, code) = document(false);
1141 assert_eq!(code, 0);
1142 assert!(text.contains("document"), "must name the surface");
1143 assert!(
1144 text.contains("colorspace"),
1145 "document must include colorspace"
1146 );
1147 assert!(text.contains("doc-id"), "document must include doc-id");
1148 }
1149
1150 #[test]
1151 fn document_json_schema_field() {
1152 let (text, code) = document(true);
1153 assert_eq!(code, 0);
1154 assert!(text.contains("zenith-schema-v1"));
1155 assert!(text.contains("\"document\""));
1156 }
1157
1158 #[test]
1159 fn overview_mentions_token_types() {
1160 let (text, code) = overview(false);
1161 assert_eq!(code, 0);
1162 assert!(
1163 text.contains("token types"),
1164 "overview must mention token types; got:\n{text}"
1165 );
1166 assert!(
1167 text.contains("zenith schema tokens"),
1168 "overview must mention 'zenith schema tokens'; got:\n{text}"
1169 );
1170 assert!(
1171 text.contains("zenith schema token"),
1172 "overview must mention 'zenith schema token <type>'; got:\n{text}"
1173 );
1174 }
1175
1176 #[test]
1177 fn overview_json_has_token_types_count() {
1178 let (text, code) = overview(true);
1179 assert_eq!(code, 0);
1180 assert!(
1181 text.contains("token_types"),
1182 "JSON overview must carry token_types count; got:\n{text}"
1183 );
1184 }
1185
1186 #[test]
1187 fn tokens_human_lists_all_types() {
1188 let (text, code) = tokens(false);
1189 assert_eq!(code, 0);
1190 assert!(text.contains("color"), "must list color type");
1191 assert!(text.contains("gradient"), "must list gradient type");
1192 assert!(text.contains("shadow"), "must list shadow type");
1193 assert!(text.contains("filter"), "must list filter type");
1194 assert!(text.contains("mask"), "must list mask type");
1195 assert!(text.contains("dimension"), "must list dimension type");
1196 assert!(text.contains("number"), "must list number type");
1197 assert!(text.contains("fontFamily"), "must list fontFamily type");
1198 assert!(text.contains("fontWeight"), "must list fontWeight type");
1199 }
1200
1201 #[test]
1202 fn tokens_json_schema_field() {
1203 let (text, code) = tokens(true);
1204 assert_eq!(code, 0);
1205 assert!(text.contains("zenith-schema-v1"));
1206 assert!(text.contains("\"token_types\""));
1207 assert!(text.contains("\"ty\""));
1208 assert!(text.contains("\"summary\""));
1209 }
1210
1211 #[test]
1212 fn token_detail_color_human() {
1213 let (text, code) = token_detail("color", false);
1214 assert_eq!(code, 0);
1215 assert!(text.contains("color"), "must name the type");
1216 assert!(
1217 text.contains("Value form:"),
1218 "must include Value form section"
1219 );
1220 assert!(text.contains("#rrggbb"), "must describe hex color form");
1221 assert!(text.contains("Example:"), "must include Example section");
1222 }
1223
1224 #[test]
1225 fn token_detail_gradient_human() {
1226 let (text, code) = token_detail("gradient", false);
1227 assert_eq!(code, 0);
1228 assert!(text.contains("gradient"), "must name the type");
1229 assert!(
1230 text.contains("Child nodes:"),
1231 "gradient must include Child nodes section"
1232 );
1233 assert!(text.contains("stop"), "gradient must describe stop child");
1234 assert!(text.contains("Example:"), "must include Example section");
1235 }
1236
1237 #[test]
1238 fn token_detail_shadow_human() {
1239 let (text, code) = token_detail("shadow", false);
1240 assert_eq!(code, 0);
1241 assert!(text.contains("shadow"), "must name the type");
1242 assert!(
1243 text.contains("Child nodes:"),
1244 "shadow must include Child nodes section"
1245 );
1246 assert!(text.contains("layer"), "shadow must describe layer child");
1247 }
1248
1249 #[test]
1250 fn token_detail_json_has_all_fields() {
1251 let (text, code) = token_detail("gradient", true);
1252 assert_eq!(code, 0);
1253 assert!(text.contains("zenith-schema-v1"));
1254 assert!(text.contains("\"token\""));
1255 assert!(text.contains("\"ty\""));
1256 assert!(text.contains("\"summary\""));
1257 assert!(text.contains("\"value_form\""));
1258 assert!(text.contains("\"child_nodes\""));
1259 assert!(text.contains("\"example\""));
1260 }
1261
1262 #[test]
1263 fn token_detail_unknown_type_returns_error() {
1264 let (text, code) = token_detail("bogus", false);
1265 assert_eq!(code, 1);
1266 assert!(
1267 text.contains("unknown token type"),
1268 "must report unknown type"
1269 );
1270 assert!(text.contains("valid types"), "must list valid types");
1271 }
1272
1273 #[test]
1274 fn node_detail_override_kind_hints_variant_surface() {
1275 let (text, code) = node_detail("override", false);
1277 assert_eq!(code, 1);
1278 assert!(
1279 text.contains("unknown node kind"),
1280 "must report unknown kind"
1281 );
1282 assert!(
1283 text.contains("zenith schema variant"),
1284 "error for 'override' must hint at `zenith schema variant`; got:\n{text}"
1285 );
1286 }
1287
1288 #[test]
1289 fn node_detail_variant_kind_hints_variant_surface() {
1290 let (text, code) = node_detail("variant", false);
1292 assert_eq!(code, 1);
1293 assert!(
1294 text.contains("unknown node kind"),
1295 "must report unknown kind"
1296 );
1297 assert!(
1298 text.contains("zenith schema variant"),
1299 "error for 'variant' must hint at `zenith schema variant`; got:\n{text}"
1300 );
1301 }
1302
1303 #[test]
1304 fn node_detail_other_unknown_no_variant_hint() {
1305 let (text, code) = node_detail("frobnicate", false);
1307 assert_eq!(code, 1);
1308 assert!(
1309 text.contains("unknown node kind"),
1310 "must report unknown kind"
1311 );
1312 assert!(
1313 !text.contains("zenith schema variant"),
1314 "generic unknown kind must not mention variant surface; got:\n{text}"
1315 );
1316 }
1317
1318 #[test]
1319 fn variant_human_contains_key_sections() {
1320 let (text, code) = variant(false);
1321 assert_eq!(code, 0);
1322 assert!(text.contains("variant"), "must name the surface");
1323 assert!(
1324 text.contains("Override properties:"),
1325 "must list override properties"
1326 );
1327 assert!(
1328 text.contains("node"),
1329 "override properties must include 'node' selector"
1330 );
1331 assert!(
1332 text.contains("visible"),
1333 "override properties must include 'visible'"
1334 );
1335 assert!(
1336 text.contains("x") && text.contains("y") && text.contains("w") && text.contains("h"),
1337 "override properties must include geometry keys x/y/w/h; got:\n{text}"
1338 );
1339 assert!(
1340 text.contains("Example:"),
1341 "must include a worked example section"
1342 );
1343 assert!(
1344 text.contains("source="),
1345 "example must show the source= attribute on a variant node"
1346 );
1347 }
1348
1349 #[test]
1350 fn variant_human_override_node_selector_note() {
1351 let (text, code) = variant(false);
1352 assert_eq!(code, 0);
1353 assert!(
1355 text.contains("node"),
1356 "override entry must describe the 'node' selector key; got:\n{text}"
1357 );
1358 assert!(
1360 text.to_lowercase().contains("not") || text.contains("NOT"),
1361 "override entry should warn that 'id' is the wrong key; got:\n{text}"
1362 );
1363 }
1364
1365 #[test]
1366 fn variant_json_schema_field() {
1367 let (text, code) = variant(true);
1368 assert_eq!(code, 0);
1369 assert!(
1370 text.contains("zenith-schema-v1"),
1371 "JSON must carry schema field"
1372 );
1373 assert!(
1374 text.contains("\"summary\""),
1375 "JSON must carry summary field"
1376 );
1377 assert!(
1378 text.contains("\"override_props\""),
1379 "JSON must carry override_props array"
1380 );
1381 assert!(
1382 text.contains("\"example\""),
1383 "JSON must carry example field"
1384 );
1385 }
1386
1387 #[test]
1388 fn variant_json_override_props_have_geometry() {
1389 let (text, code) = variant(true);
1390 assert_eq!(code, 0);
1391 for key in &["\"x\"", "\"y\"", "\"w\"", "\"h\""] {
1393 assert!(
1394 text.contains(key),
1395 "variant JSON override_props must include {key}; got:\n{text}"
1396 );
1397 }
1398 assert!(
1400 text.contains("\"node\""),
1401 "variant JSON override_props must include node; got:\n{text}"
1402 );
1403 }
1404
1405 #[test]
1406 fn op_detail_add_node_position_describes_id_field() {
1407 let (text, code) = op_detail("add_node", false);
1409 assert_eq!(code, 0);
1410 assert!(
1411 text.contains("id"),
1412 "add_node position description must mention the 'id' field; got:\n{text}"
1413 );
1414 assert!(
1415 text.contains("before") && text.contains("after"),
1416 "add_node position description must mention before/after variants; got:\n{text}"
1417 );
1418 assert!(
1419 text.contains("index"),
1420 "add_node position description must mention index variant; got:\n{text}"
1421 );
1422 }
1423
1424 #[test]
1425 fn op_detail_add_node_position_json_has_correct_shape() {
1426 let (text, code) = op_detail("add_node", true);
1427 assert_eq!(code, 0);
1428 assert!(
1430 text.contains("sibling-id") || text.contains("\"id\""),
1431 "add_node position field ty must describe the sibling id key; got:\n{text}"
1432 );
1433 }
1434
1435 #[test]
1436 fn token_detail_fontweight_no_value_form_confusion() {
1437 let (text, code) = token_detail("fontWeight", false);
1439 assert_eq!(code, 0);
1440 assert!(
1441 text.contains("700"),
1442 "fontWeight example must use a bare integer"
1443 );
1444 assert!(
1446 !text.contains("\"700\""),
1447 "fontWeight must not suggest string form"
1448 );
1449 }
1450
1451 #[test]
1454 fn node_detail_shape_human_shows_content_section() {
1455 let (text, code) = node_detail("shape", false);
1456 assert_eq!(code, 0);
1457 assert!(
1458 text.contains("Content:"),
1459 "shape detail must include Content section; got:\n{text}"
1460 );
1461 assert!(
1462 text.contains("span"),
1463 "shape Content section must mention span children; got:\n{text}"
1464 );
1465 assert!(
1466 text.contains("label") || text.contains("centered"),
1467 "shape Content section must describe the owned label behaviour; got:\n{text}"
1468 );
1469 assert!(
1470 text.contains("Example:"),
1471 "shape Content section must include an example; got:\n{text}"
1472 );
1473 }
1474
1475 #[test]
1476 fn node_detail_shape_json_has_content_field() {
1477 let (text, code) = node_detail("shape", true);
1478 assert_eq!(code, 0);
1479 assert!(
1480 text.contains("\"content\""),
1481 "shape JSON must carry a content field; got:\n{text}"
1482 );
1483 assert!(
1484 text.contains("\"description\""),
1485 "shape JSON content must carry a description; got:\n{text}"
1486 );
1487 assert!(
1488 text.contains("\"example\""),
1489 "shape JSON content must carry an example; got:\n{text}"
1490 );
1491 assert!(
1493 !text.contains("\"content\": null"),
1494 "shape JSON content must be non-null; got:\n{text}"
1495 );
1496 }
1497
1498 #[test]
1499 fn node_detail_polygon_human_shows_content_section() {
1500 let (text, code) = node_detail("polygon", false);
1501 assert_eq!(code, 0);
1502 assert!(
1503 text.contains("Content:"),
1504 "polygon detail must include Content section; got:\n{text}"
1505 );
1506 assert!(
1507 text.contains("point"),
1508 "polygon Content section must mention point children; got:\n{text}"
1509 );
1510 }
1511
1512 #[test]
1513 fn node_detail_text_human_shows_content_section() {
1514 let (text, code) = node_detail("text", false);
1515 assert_eq!(code, 0);
1516 assert!(
1517 text.contains("Content:"),
1518 "text detail must include Content section; got:\n{text}"
1519 );
1520 assert!(
1521 text.contains("span"),
1522 "text Content section must mention span children; got:\n{text}"
1523 );
1524 }
1525
1526 #[test]
1527 fn node_detail_rect_human_no_content_section() {
1528 let (text, code) = node_detail("rect", false);
1530 assert_eq!(code, 0);
1531 assert!(
1532 !text.contains("Content:"),
1533 "rect detail must NOT include a Content section; got:\n{text}"
1534 );
1535 }
1536
1537 #[test]
1538 fn node_detail_rect_json_content_is_absent() {
1539 let (text, code) = node_detail("rect", true);
1540 assert_eq!(code, 0);
1541 assert!(
1543 !text.contains("\"content\""),
1544 "rect JSON must not carry a content field (skip_serializing_if = None); got:\n{text}"
1545 );
1546 }
1547
1548 #[test]
1551 fn brand_human_contains_key_sections() {
1552 let (text, code) = brand(false);
1553 assert_eq!(code, 0);
1554 assert!(
1555 text.contains("brand {"),
1556 "human output must include worked example with 'brand {{'; got:\n{text}"
1557 );
1558 assert!(
1559 text.contains("colors"),
1560 "human output must describe the colors child node; got:\n{text}"
1561 );
1562 assert!(
1563 text.contains("fonts"),
1564 "human output must describe the fonts child node; got:\n{text}"
1565 );
1566 assert!(
1567 text.contains("weights"),
1568 "human output must describe the weights child node; got:\n{text}"
1569 );
1570 assert!(
1571 text.contains("brand.color_off_palette"),
1572 "human output must list brand.color_off_palette diagnostic code; got:\n{text}"
1573 );
1574 assert!(
1575 text.contains("brand.font_not_allowed"),
1576 "human output must list brand.font_not_allowed diagnostic code; got:\n{text}"
1577 );
1578 assert!(
1579 text.contains("brand.weight_not_allowed"),
1580 "human output must list brand.weight_not_allowed diagnostic code; got:\n{text}"
1581 );
1582 assert!(
1583 text.contains("--deny"),
1584 "human output must mention --deny for CI gate; got:\n{text}"
1585 );
1586 }
1587
1588 #[test]
1589 fn brand_json_schema_field() {
1590 let (text, code) = brand(true);
1591 assert_eq!(code, 0);
1592 assert!(
1593 text.contains("zenith-schema-v1"),
1594 "JSON must carry schema field; got:\n{text}"
1595 );
1596 assert!(
1597 text.contains("\"summary\""),
1598 "JSON must carry summary field; got:\n{text}"
1599 );
1600 assert!(
1601 text.contains("\"child_nodes\""),
1602 "JSON must carry child_nodes array; got:\n{text}"
1603 );
1604 assert!(
1605 text.contains("\"diagnostic_codes\""),
1606 "JSON must carry diagnostic_codes array; got:\n{text}"
1607 );
1608 }
1609
1610 #[test]
1611 fn overview_mentions_brand_surface() {
1612 let (text, code) = overview(false);
1613 assert_eq!(code, 0);
1614 assert!(
1615 text.contains("zenith schema brand"),
1616 "overview must mention 'zenith schema brand'; got:\n{text}"
1617 );
1618 assert!(
1619 text.contains("zenith schema block"),
1620 "overview must mention 'zenith schema block'; got:\n{text}"
1621 );
1622 assert!(
1623 text.contains("7 non-node surfaces"),
1624 "overview must count 5 non-node surfaces after adding block; got:\n{text}"
1625 );
1626 }
1627}