From cfa46e43d5413923e4cc5d2745d64fe8dcc9d638 Mon Sep 17 00:00:00 2001 From: Doug Rennehan Date: Sat, 18 Jul 2026 12:38:01 -0400 Subject: [PATCH 1/2] feat(show): surface boundary language in read view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `hydrate show` rendered only a node's kind, path, ports, and edges — a boundary's codegen language (set with `--language`) was invisible, so the only way to confirm it was the web UI. Surface it in both output modes, carrying the same information: - JSON: a `language` field on nodes that have one, omitted when unset. - Human: annotate the boundary line, e.g. `Core [boundary] (python)`, only when set. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/cmd/show.rs | 63 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/src/cmd/show.rs b/src/cmd/show.rs index e954ede..8fefd9a 100644 --- a/src/cmd/show.rs +++ b/src/cmd/show.rs @@ -122,6 +122,10 @@ impl ShowPort { struct ShowNode { path: String, kind: String, + /// Codegen language for a boundary node (`--language`). Omitted when unset so a + /// languageless node never emits a bogus or null value. + #[serde(skip_serializing_if = "Option::is_none")] + language: Option, #[serde(skip_serializing_if = "Vec::is_empty")] inputs: Vec, #[serde(skip_serializing_if = "Vec::is_empty")] @@ -211,6 +215,9 @@ fn build_view(graph: &GraphResponse, filter: Option<&str>) -> Result String { .rsplit('.') .next() .expect("a node path always has at least one segment"); - out.push_str(&format!("\n{indent}{leaf} [{}]", node.kind)); + let language = node + .language + .as_deref() + .map(|l| format!(" ({l})")) + .unwrap_or_default(); + out.push_str(&format!("\n{indent}{leaf} [{}]{language}", node.kind)); let ports = " ".repeat(depth + 2); if !node.inputs.is_empty() { out.push_str(&format!("\n{ports}in: {}", join_ports(&node.inputs))); @@ -550,6 +562,55 @@ mod tests { assert_eq!(edges[0]["to"], "Api.Rater.raw"); } + #[test] + fn boundary_language_is_shown_in_both_modes() { + // A boundary with a codegen language surfaces it in show — otherwise the + // web UI is the only place to confirm what `--language` set. It rides on + // the boundary node line (human) and as a `language` field (JSON). + let mut g = sample_graph(); + // Api is the boundary node (index 0 in sample_graph). + g.nodes[0].data.language = Some(Some("python".to_string())); + + let human = render(&g, "proj", "main", None, OutputMode::Human).unwrap(); + assert!( + human.contains("Api [boundary] (python)"), + "human view must annotate the boundary's language: {human}" + ); + + let json = render(&g, "proj", "main", None, OutputMode::Json).unwrap(); + let v: serde_json::Value = serde_json::from_str(&json).unwrap(); + let api = v["nodes"] + .as_array() + .unwrap() + .iter() + .find(|n| n["path"] == "Api") + .unwrap(); + assert_eq!(api["language"], "python", "{json}"); + } + + #[test] + fn node_without_language_emits_no_language_value() { + // A node with no language must not emit a bogus or "null"-string value in + // either mode. The sample graph carries no language on any node. + let g = sample_graph(); + + let json = render(&g, "proj", "main", None, OutputMode::Json).unwrap(); + let v: serde_json::Value = serde_json::from_str(&json).unwrap(); + for n in v["nodes"].as_array().unwrap() { + assert!( + n.get("language").is_none(), + "a languageless node must omit the field: {n}" + ); + } + assert!(!json.contains("language"), "{json}"); + + let human = render(&g, "proj", "main", None, OutputMode::Human).unwrap(); + assert!( + !human.contains("("), + "no language annotation expected: {human}" + ); + } + #[test] fn position_field_is_omitted() { // The graph endpoint's placeholder position must never surface in show. From 70a8b0c1c7bad047693911b20e9faa568771c520 Mon Sep 17 00:00:00 2001 From: Doug Rennehan Date: Sat, 18 Jul 2026 13:03:10 -0400 Subject: [PATCH 2/2] refactor(show): tighten language nits from review - reword the ShowNode.language doc to reflect it surfaces on whichever node the server reports it on (not boundary-gated in code) - tighten the no-annotation test to assert the exact `] (` signature is absent rather than any stray `(`, so unrelated output can't false-trip Co-Authored-By: Claude Opus 4.8 (1M context) --- src/cmd/show.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/cmd/show.rs b/src/cmd/show.rs index 8fefd9a..b265b87 100644 --- a/src/cmd/show.rs +++ b/src/cmd/show.rs @@ -122,8 +122,9 @@ impl ShowPort { struct ShowNode { path: String, kind: String, - /// Codegen language for a boundary node (`--language`). Omitted when unset so a - /// languageless node never emits a bogus or null value. + /// Codegen language carried by a node — set via `--language`, in practice on a + /// boundary. Surfaced for whichever node the server reports it on; omitted when + /// unset so a languageless node never emits a bogus or null value. #[serde(skip_serializing_if = "Option::is_none")] language: Option, #[serde(skip_serializing_if = "Vec::is_empty")] @@ -605,8 +606,11 @@ mod tests { assert!(!json.contains("language"), "{json}"); let human = render(&g, "proj", "main", None, OutputMode::Human).unwrap(); + // The language annotation is the only `] (` sequence show emits (it rides + // right after a node's `[kind]`); assert that exact signature is absent + // rather than any stray `(`, so unrelated future output can't false-trip. assert!( - !human.contains("("), + !human.contains("] ("), "no language annotation expected: {human}" ); }