1use serde_json::Value;
6use std::io::IsTerminal;
7
8pub(crate) const DESCRIPTION_TRUNCATE_LEN: usize = 80;
13
14pub(crate) fn resolve_format_inner(explicit_format: Option<&str>, is_tty: bool) -> &'static str {
20 if let Some(fmt) = explicit_format {
21 return match fmt {
22 "json" => "json",
23 "table" => "table",
24 "csv" => "csv",
25 "yaml" => "yaml",
26 "jsonl" => "jsonl",
27 other => {
28 tracing::warn!("Unknown format '{}', defaulting to 'json'.", other);
31 "json"
32 }
33 };
34 }
35 if is_tty {
36 "table"
37 } else {
38 "json"
39 }
40}
41
42pub fn resolve_format(explicit_format: Option<&str>) -> &'static str {
49 let is_tty = std::io::stdout().is_terminal();
50 resolve_format_inner(explicit_format, is_tty)
51}
52
53pub(crate) fn truncate(text: &str, max_length: usize) -> String {
64 if text.len() <= max_length {
65 return text.to_string();
66 }
67 let cutoff = max_length.saturating_sub(3);
68 let mut end = cutoff;
70 while end > 0 && !text.is_char_boundary(end) {
71 end -= 1;
72 }
73 format!("{}...", &text[..end])
74}
75
76fn extract_str<'a>(v: &'a Value, keys: &[&str]) -> &'a str {
82 for key in keys {
83 if let Some(s) = v.get(key).and_then(|s| s.as_str()) {
84 return s;
85 }
86 }
87 ""
88}
89
90fn extract_tags(v: &Value) -> Vec<String> {
92 v.get("tags")
93 .and_then(|t| t.as_array())
94 .map(|arr| {
95 arr.iter()
96 .filter_map(|s| s.as_str().map(|s| s.to_string()))
97 .collect()
98 })
99 .unwrap_or_default()
100}
101
102pub fn format_module_list(modules: &[Value], format: &str, filter_tags: &[&str]) -> String {
115 format_module_list_with_deps(modules, format, filter_tags, false)
116}
117
118pub fn format_module_list_with_deps(
120 modules: &[Value],
121 format: &str,
122 filter_tags: &[&str],
123 show_deps: bool,
124) -> String {
125 use comfy_table::{ContentArrangement, Table};
126
127 match format {
128 "table" => {
129 if modules.is_empty() {
130 if !filter_tags.is_empty() {
131 return format!(
132 "No modules found matching tags: {}.",
133 filter_tags.join(", ")
134 );
135 }
136 return "No modules found.".to_string();
137 }
138
139 let mut table = Table::new();
140 table.set_content_arrangement(ContentArrangement::Dynamic);
141 if show_deps {
142 table.set_header(vec!["ID", "Description", "Tags", "Deps"]);
143 } else {
144 table.set_header(vec!["ID", "Description", "Tags"]);
145 }
146
147 for m in modules {
148 let id = extract_str(m, &["module_id", "id", "canonical_id", "name"]);
149 let desc_raw = extract_str(m, &["description"]);
150 let desc = truncate(desc_raw, DESCRIPTION_TRUNCATE_LEN);
151 let tags = extract_tags(m).join(", ");
152 if show_deps {
153 let dep_count = m
154 .get("dependencies")
155 .and_then(|v| v.as_array())
156 .map_or(0, |a| a.len());
157 table.add_row(vec![id.to_string(), desc, tags, dep_count.to_string()]);
158 } else {
159 table.add_row(vec![id.to_string(), desc, tags]);
160 }
161 }
162
163 table.to_string()
164 }
165 "json" => {
166 let result: Vec<serde_json::Value> = modules
167 .iter()
168 .map(|m| {
169 let id = extract_str(m, &["module_id", "id", "canonical_id", "name"]);
170 let desc = extract_str(m, &["description"]);
171 let tags: Vec<serde_json::Value> = extract_tags(m)
172 .into_iter()
173 .map(serde_json::Value::String)
174 .collect();
175 serde_json::json!({
176 "id": id,
177 "description": desc,
178 "tags": tags,
179 })
180 })
181 .collect();
182
183 serde_json::to_string_pretty(&result).unwrap_or_else(|_| "[]".to_string())
184 }
185 unknown => {
186 tracing::warn!(
187 "Unknown format '{}' in format_module_list, using json.",
188 unknown
189 );
190 format_module_list(modules, "json", filter_tags)
191 }
192 }
193}
194
195fn render_panel(title: &str) -> String {
201 use comfy_table::Table;
202 let mut table = Table::new();
203 table.load_preset(comfy_table::presets::UTF8_FULL);
204 table.add_row(vec![title]);
205 table.to_string()
206}
207
208fn render_section(title: &str, content: &str) -> Option<String> {
211 if content.is_empty() {
212 return None;
213 }
214 Some(format!("\n{}:\n{}", title, content))
215}
216
217pub fn format_module_detail(module: &Value, format: &str) -> String {
223 let id = extract_str(module, &["module_id", "id", "canonical_id", "name"]);
224 let description = extract_str(module, &["description"]);
225
226 match format {
227 "table" => {
228 let mut parts: Vec<String> = Vec::new();
229
230 parts.push(render_panel(&format!("Module: {}", id)));
232
233 parts.push(format!("\nDescription:\n {}", description));
235
236 if let Some(input_schema) = module.get("input_schema").filter(|v| !v.is_null()) {
238 let content =
239 serde_json::to_string_pretty(input_schema).unwrap_or_else(|_| "{}".to_string());
240 if let Some(section) = render_section("Input Schema", &content) {
241 parts.push(section);
242 }
243 }
244
245 if let Some(output_schema) = module.get("output_schema").filter(|v| !v.is_null()) {
247 let content = serde_json::to_string_pretty(output_schema)
248 .unwrap_or_else(|_| "{}".to_string());
249 if let Some(section) = render_section("Output Schema", &content) {
250 parts.push(section);
251 }
252 }
253
254 if let Some(ann) = module.get("annotations").and_then(|v| v.as_object()) {
256 if !ann.is_empty() {
257 let content: String = ann
258 .iter()
259 .map(|(k, v)| {
260 let val = v.as_str().unwrap_or(&v.to_string()).to_string();
261 format!(" {}: {}", k, val)
262 })
263 .collect::<Vec<_>>()
264 .join("\n");
265 if let Some(section) = render_section("Annotations", &content) {
266 parts.push(section);
267 }
268 }
269 }
270
271 let x_fields: Vec<(String, String)> = module
273 .as_object()
274 .map(|obj| {
275 obj.iter()
276 .filter(|(k, _)| k.starts_with("x-") || k.starts_with("x_"))
277 .map(|(k, v)| {
278 let val = v.as_str().unwrap_or(&v.to_string()).to_string();
279 (k.clone(), val)
280 })
281 .collect()
282 })
283 .unwrap_or_default();
284 if !x_fields.is_empty() {
285 let content: String = x_fields
286 .iter()
287 .map(|(k, v)| format!(" {}: {}", k, v))
288 .collect::<Vec<_>>()
289 .join("\n");
290 if let Some(section) = render_section("Extension Metadata", &content) {
291 parts.push(section);
292 }
293 }
294
295 let tags = extract_tags(module);
297 if !tags.is_empty() {
298 if let Some(section) = render_section("Tags", &format!(" {}", tags.join(", "))) {
299 parts.push(section);
300 }
301 }
302
303 parts.join("\n")
304 }
305 "json" => {
306 let mut result = serde_json::Map::new();
307 result.insert("id".to_string(), serde_json::Value::String(id.to_string()));
308 result.insert(
309 "description".to_string(),
310 serde_json::Value::String(description.to_string()),
311 );
312
313 for key in &["input_schema", "output_schema"] {
315 if let Some(v) = module.get(*key).filter(|v| !v.is_null()) {
316 result.insert(key.to_string(), v.clone());
317 }
318 }
319
320 if let Some(ann) = module
321 .get("annotations")
322 .filter(|v| !v.is_null() && v.as_object().is_some_and(|o| !o.is_empty()))
323 {
324 result.insert("annotations".to_string(), ann.clone());
325 }
326
327 let tags = extract_tags(module);
328 if !tags.is_empty() {
329 result.insert(
330 "tags".to_string(),
331 serde_json::Value::Array(
332 tags.into_iter().map(serde_json::Value::String).collect(),
333 ),
334 );
335 }
336
337 if let Some(obj) = module.as_object() {
339 for (k, v) in obj {
340 if k.starts_with("x-") || k.starts_with("x_") {
341 result.insert(k.clone(), v.clone());
342 }
343 }
344 }
345
346 serde_json::to_string_pretty(&serde_json::Value::Object(result))
347 .unwrap_or_else(|_| "{}".to_string())
348 }
349 unknown => {
350 tracing::warn!(
351 "Unknown format '{}' in format_module_detail, using json.",
352 unknown
353 );
354 format_module_detail(module, "json")
355 }
356 }
357}
358
359fn apply_field_selection(result: &Value, fields: &str) -> Value {
368 if let Some(obj) = result.as_object() {
369 let mut selected = serde_json::Map::new();
370 for field in fields.split(',') {
371 let field = field.trim();
372 if field.is_empty() {
373 continue;
374 }
375 let mut val: &Value = &Value::Object(obj.clone());
376 for part in field.split('.') {
377 if let Some(next) = val.get(part) {
378 val = next;
379 } else {
380 val = &Value::Null;
381 break;
382 }
383 }
384 selected.insert(field.to_string(), val.clone());
385 }
386 Value::Object(selected)
387 } else {
388 result.clone()
389 }
390}
391
392pub fn format_exec_result(result: &Value, format: &str, fields: Option<&str>) -> String {
399 use comfy_table::{ContentArrangement, Table};
400
401 let result = if let Some(f) = fields {
402 apply_field_selection(result, f)
403 } else {
404 result.clone()
405 };
406
407 match &result {
408 Value::Null => String::new(),
409
410 Value::String(s) => s.clone(),
411
412 Value::Object(_) if format == "csv" => {
413 let obj = result.as_object().unwrap();
414 let keys: Vec<&String> = obj.keys().collect();
415 let header = keys
416 .iter()
417 .map(|k| k.as_str())
418 .collect::<Vec<_>>()
419 .join(",");
420 let values = keys
421 .iter()
422 .map(|k| {
423 let v = obj.get(*k).unwrap();
424 match v {
425 Value::String(s) => s.clone(),
426 other => other.to_string(),
427 }
428 })
429 .collect::<Vec<_>>()
430 .join(",");
431 format!("{header}\n{values}")
432 }
433
434 Value::Array(arr) if format == "csv" => {
435 if arr.is_empty() {
436 return String::new();
437 }
438 if let Some(first_obj) = arr[0].as_object() {
439 let keys: Vec<&String> = first_obj.keys().collect();
440 let header = keys
441 .iter()
442 .map(|k| k.as_str())
443 .collect::<Vec<_>>()
444 .join(",");
445 let mut rows = vec![header];
446 for item in arr {
447 if let Some(obj) = item.as_object() {
448 let row = keys
449 .iter()
450 .map(|k| {
451 let v = obj.get(*k).unwrap_or(&Value::Null);
452 match v {
453 Value::String(s) => s.clone(),
454 other => other.to_string(),
455 }
456 })
457 .collect::<Vec<_>>()
458 .join(",");
459 rows.push(row);
460 }
461 }
462 rows.join("\n")
463 } else {
464 serde_json::to_string(&result).unwrap_or_default()
465 }
466 }
467
468 _ if format == "yaml" => serde_yaml::to_string(&result)
469 .map(|s| s.trim_end().to_string())
470 .unwrap_or_else(|_| {
471 serde_json::to_string_pretty(&result).unwrap_or_else(|_| "null".to_string())
472 }),
473
474 Value::Array(arr) if format == "jsonl" => arr
475 .iter()
476 .map(|item| serde_json::to_string(item).unwrap_or_default())
477 .collect::<Vec<_>>()
478 .join("\n"),
479
480 _ if format == "jsonl" => serde_json::to_string(&result).unwrap_or_default(),
481
482 Value::Object(_) if format == "table" => {
483 let obj = result.as_object().unwrap();
484 let mut table = Table::new();
485 table.set_content_arrangement(ContentArrangement::Dynamic);
486 table.set_header(vec!["Key", "Value"]);
487 for (k, v) in obj {
488 let val_str = match v {
489 Value::String(s) => s.clone(),
490 other => other.to_string(),
491 };
492 table.add_row(vec![k.clone(), val_str]);
493 }
494 table.to_string()
495 }
496
497 Value::Object(_) | Value::Array(_) => {
498 serde_json::to_string_pretty(&result).unwrap_or_else(|_| "null".to_string())
499 }
500
501 other => other.to_string(),
503 }
504}
505
506#[cfg(test)]
511mod tests {
512 use super::*;
513 use serde_json::json;
514
515 #[test]
518 fn test_resolve_format_explicit_json_tty() {
519 assert_eq!(resolve_format_inner(Some("json"), true), "json");
521 }
522
523 #[test]
524 fn test_resolve_format_explicit_table_non_tty() {
525 assert_eq!(resolve_format_inner(Some("table"), false), "table");
527 }
528
529 #[test]
530 fn test_resolve_format_none_tty() {
531 assert_eq!(resolve_format_inner(None, true), "table");
533 }
534
535 #[test]
536 fn test_resolve_format_none_non_tty() {
537 assert_eq!(resolve_format_inner(None, false), "json");
539 }
540
541 #[test]
544 fn test_truncate_short_string() {
545 let s = "hello";
546 assert_eq!(truncate(s, 80), "hello");
547 }
548
549 #[test]
550 fn test_truncate_exact_length() {
551 let s = "a".repeat(80);
552 assert_eq!(truncate(&s, 80), s);
553 }
554
555 #[test]
556 fn test_truncate_over_limit() {
557 let s = "a".repeat(100);
558 let result = truncate(&s, 80);
559 assert_eq!(result.len(), 80);
560 assert!(result.ends_with("..."));
561 assert_eq!(&result[..77], &"a".repeat(77));
562 }
563
564 #[test]
565 fn test_truncate_exactly_81_chars() {
566 let s = "b".repeat(81);
567 let result = truncate(&s, 80);
568 assert_eq!(result.len(), 80);
569 assert!(result.ends_with("..."));
570 }
571
572 #[test]
575 fn test_format_module_list_json_two_modules() {
576 let modules = vec![
577 json!({"module_id": "math.add", "description": "Add numbers", "tags": ["math"]}),
578 json!({"module_id": "text.upper", "description": "Uppercase", "tags": []}),
579 ];
580 let output = format_module_list(&modules, "json", &[]);
581 let parsed: serde_json::Value = serde_json::from_str(&output).expect("must be valid JSON");
582 let arr = parsed.as_array().expect("must be array");
583 assert_eq!(arr.len(), 2);
584 assert_eq!(arr[0]["id"], "math.add");
585 assert_eq!(arr[1]["id"], "text.upper");
586 }
587
588 #[test]
589 fn test_format_module_list_json_empty() {
590 let output = format_module_list(&[], "json", &[]);
591 assert_eq!(output.trim(), "[]");
592 }
593
594 #[test]
595 fn test_format_module_list_table_two_modules() {
596 let modules =
597 vec![json!({"module_id": "math.add", "description": "Add numbers", "tags": ["math"]})];
598 let output = format_module_list(&modules, "table", &[]);
599 assert!(output.contains("math.add"), "table must contain module ID");
600 assert!(
601 output.contains("Add numbers"),
602 "table must contain description"
603 );
604 }
605
606 #[test]
607 fn test_format_module_list_table_columns() {
608 let modules =
609 vec![json!({"module_id": "math.add", "description": "Add numbers", "tags": []})];
610 let output = format_module_list(&modules, "table", &[]);
611 assert!(output.contains("ID"), "table must have ID column");
612 assert!(
613 output.contains("Description"),
614 "table must have Description column"
615 );
616 assert!(output.contains("Tags"), "table must have Tags column");
617 }
618
619 #[test]
620 fn test_format_module_list_table_empty_no_tags() {
621 let output = format_module_list(&[], "table", &[]);
622 assert_eq!(output.trim(), "No modules found.");
623 }
624
625 #[test]
626 fn test_format_module_list_table_empty_with_filter_tags() {
627 let output = format_module_list(&[], "table", &["math", "text"]);
628 assert!(
629 output.contains("No modules found matching tags:"),
630 "must contain tag-filter message"
631 );
632 assert!(output.contains("math"), "must contain tag name");
633 assert!(output.contains("text"), "must contain tag name");
634 }
635
636 #[test]
637 fn test_format_module_list_table_description_truncated() {
638 let long_desc = "a".repeat(100);
639 let modules = vec![json!({"module_id": "x.y", "description": long_desc, "tags": []})];
640 let output = format_module_list(&modules, "table", &[]);
641 assert!(
642 output.contains("..."),
643 "long description must be truncated with '...'"
644 );
645 assert!(
646 !output.contains(&"a".repeat(100)),
647 "full description must not appear"
648 );
649 }
650
651 #[test]
652 fn test_format_module_list_json_tags_present() {
653 let modules = vec![json!({"module_id": "a.b", "description": "desc", "tags": ["x", "y"]})];
654 let output = format_module_list(&modules, "json", &[]);
655 let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
656 let tags = parsed[0]["tags"].as_array().unwrap();
657 assert_eq!(tags.len(), 2);
658 assert_eq!(tags[0], "x");
659 }
660
661 #[test]
664 fn test_format_exec_result_null_returns_empty() {
665 let output = format_exec_result(&Value::Null, "json", None);
666 assert_eq!(output, "", "Null result must produce empty string");
667 }
668
669 #[test]
670 fn test_format_exec_result_string_plain() {
671 let result = json!("hello world");
672 let output = format_exec_result(&result, "json", None);
673 assert_eq!(output, "hello world");
674 }
675
676 #[test]
677 fn test_format_exec_result_string_table_mode_also_plain() {
678 let result = json!("hello");
680 let output = format_exec_result(&result, "table", None);
681 assert_eq!(output, "hello");
682 }
683
684 #[test]
685 fn test_format_exec_result_object_json_mode() {
686 let result = json!({"sum": 42, "status": "ok"});
687 let output = format_exec_result(&result, "json", None);
688 let parsed: serde_json::Value = serde_json::from_str(&output).expect("must be valid JSON");
689 assert_eq!(parsed["sum"], 42);
690 assert_eq!(parsed["status"], "ok");
691 }
692
693 #[test]
694 fn test_format_exec_result_object_table_mode() {
695 let result = json!({"key": "value", "count": 3});
696 let output = format_exec_result(&result, "table", None);
697 assert!(output.contains("key"), "table must contain 'key'");
699 assert!(output.contains("value"), "table must contain 'value'");
700 assert!(output.contains("count"), "table must contain 'count'");
701 assert!(output.contains('3'), "table must contain '3'");
702 }
703
704 #[test]
705 fn test_format_exec_result_array_is_json() {
706 let result = json!([1, 2, 3]);
707 let output = format_exec_result(&result, "json", None);
708 let parsed: serde_json::Value = serde_json::from_str(&output).expect("must be valid JSON");
709 assert!(parsed.is_array());
710 assert_eq!(parsed.as_array().unwrap().len(), 3);
711 }
712
713 #[test]
714 fn test_format_exec_result_array_table_mode_is_json() {
715 let result = json!([{"a": 1}, {"b": 2}]);
717 let output = format_exec_result(&result, "table", None);
718 let parsed: serde_json::Value =
719 serde_json::from_str(&output).expect("array must produce JSON");
720 assert!(parsed.is_array());
721 }
722
723 #[test]
724 fn test_format_exec_result_number_scalar() {
725 let result = json!(42);
726 let output = format_exec_result(&result, "json", None);
727 assert_eq!(output, "42");
728 }
729
730 #[test]
731 fn test_format_exec_result_bool_scalar() {
732 let result = json!(true);
733 let output = format_exec_result(&result, "json", None);
734 assert_eq!(output, "true");
735 }
736
737 #[test]
738 fn test_format_exec_result_float_scalar() {
739 let result = json!(3.15);
740 let output = format_exec_result(&result, "json", None);
741 assert!(output.starts_with("3.15"), "float must stringify correctly");
742 }
743
744 #[test]
747 fn test_format_module_detail_json_full() {
748 let module = json!({
749 "module_id": "math.add",
750 "description": "Add two numbers",
751 "input_schema": {"type": "object", "properties": {"a": {"type": "integer"}}},
752 "output_schema": {"type": "object", "properties": {"result": {"type": "integer"}}},
753 "tags": ["math"],
754 "annotations": {"author": "test"}
755 });
756 let output = format_module_detail(&module, "json");
757 let parsed: serde_json::Value = serde_json::from_str(&output).expect("must be valid JSON");
758 assert_eq!(parsed["id"], "math.add");
759 assert_eq!(parsed["description"], "Add two numbers");
760 assert!(
761 parsed.get("input_schema").is_some(),
762 "input_schema must be present"
763 );
764 assert!(
765 parsed.get("output_schema").is_some(),
766 "output_schema must be present"
767 );
768 let tags = parsed["tags"].as_array().unwrap();
769 assert_eq!(tags[0], "math");
770 }
771
772 #[test]
773 fn test_format_module_detail_json_no_output_schema() {
774 let module = json!({
775 "module_id": "text.upper",
776 "description": "Uppercase",
777 });
778 let output = format_module_detail(&module, "json");
779 let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
780 assert!(
781 parsed.get("output_schema").is_none(),
782 "output_schema must be absent when not set"
783 );
784 }
785
786 #[test]
787 fn test_format_module_detail_json_no_none_fields() {
788 let module = json!({
789 "module_id": "a.b",
790 "description": "desc",
791 "input_schema": null,
792 "output_schema": null,
793 "tags": null,
794 });
795 let output = format_module_detail(&module, "json");
796 let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
797 assert!(
798 parsed.get("input_schema").is_none(),
799 "null input_schema must be absent"
800 );
801 assert!(parsed.get("tags").is_none(), "null tags must be absent");
802 }
803
804 #[test]
805 fn test_format_module_detail_table_contains_description() {
806 let module = json!({
807 "module_id": "math.add",
808 "description": "Add two numbers",
809 });
810 let output = format_module_detail(&module, "table");
811 assert!(
812 output.contains("Add two numbers"),
813 "table must contain description"
814 );
815 }
816
817 #[test]
818 fn test_format_module_detail_table_contains_module_id() {
819 let module = json!({
820 "module_id": "math.add",
821 "description": "desc",
822 });
823 let output = format_module_detail(&module, "table");
824 assert!(output.contains("math.add"), "table must contain module ID");
825 }
826
827 #[test]
828 fn test_format_module_detail_table_input_schema_section() {
829 let module = json!({
830 "module_id": "math.add",
831 "description": "desc",
832 "input_schema": {"type": "object"}
833 });
834 let output = format_module_detail(&module, "table");
835 assert!(
836 output.contains("Input Schema"),
837 "table must contain Input Schema section"
838 );
839 }
840
841 #[test]
842 fn test_format_module_detail_table_no_output_schema_section_when_absent() {
843 let module = json!({
844 "module_id": "text.upper",
845 "description": "desc",
846 });
847 let output = format_module_detail(&module, "table");
848 assert!(
849 !output.contains("Output Schema"),
850 "Output Schema section must be absent when not set"
851 );
852 }
853
854 #[test]
855 fn test_format_module_detail_table_tags_section() {
856 let module = json!({
857 "module_id": "math.add",
858 "description": "desc",
859 "tags": ["math", "arithmetic"]
860 });
861 let output = format_module_detail(&module, "table");
862 assert!(output.contains("Tags"), "table must contain Tags section");
863 assert!(output.contains("math"), "table must contain tag value");
864 }
865
866 #[test]
867 fn test_format_module_detail_table_annotations_section() {
868 let module = json!({
869 "module_id": "a.b",
870 "description": "desc",
871 "annotations": {"author": "alice", "version": "1.0"}
872 });
873 let output = format_module_detail(&module, "table");
874 assert!(
875 output.contains("Annotations"),
876 "table must contain Annotations section"
877 );
878 assert!(
879 output.contains("author"),
880 "table must contain annotation key"
881 );
882 assert!(
883 output.contains("alice"),
884 "table must contain annotation value"
885 );
886 }
887
888 #[test]
889 fn test_format_module_detail_table_extension_metadata() {
890 let module = json!({
891 "module_id": "a.b",
892 "description": "desc",
893 "x-category": "utility"
894 });
895 let output = format_module_detail(&module, "table");
896 assert!(
897 output.contains("Extension Metadata"),
898 "must contain Extension Metadata section"
899 );
900 assert!(output.contains("x-category"), "must contain x- key");
901 assert!(output.contains("utility"), "must contain x- value");
902 }
903}