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