Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .trinity/seals/t27mcp_T27mcpVisualOutput.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"built_by": "t27c-bootstrap@0.5.2+9448ab4be",
"closure": [],
"config": "gen=c,rust,verilog,zig;tests=zig",
"gen_hash_c": "sha256:076a0683dcc4e8487922bd6ff561422a1dfa707069e639fffaafea9d70ad8e2d",
"gen_hash_rust": "sha256:6267998500edaecfe3378c6ff7ed7482d1b0c239e1b8480fa697a8fd05b0d7b8",
"gen_hash_verilog": "sha256:241e62e3030919b4f540ff38480f8d726a4cc8301fbc30443369e7978adbbb1b",
"gen_hash_zig": "sha256:e7446390463513c4496f484f375ef59ae2f260172c8e46326f21008d84860411",
"module": "T27mcpVisualOutput",
"ring": 12,
"seal_schema": 2,
"sealed_at": "2026-10-18T15:31:37Z",
"sealed_by": "t27c-bootstrap@0.5.2",
"spec_hash": "sha256:06c1c5e73142e2f79fa5d7be037eace63c1f20597a6e33274ebf8e1b428b7584",
"spec_path": "specs/t27mcp/visual_output.t27",
"tests": {
"failed": 0,
"failing": [],
"forced": false,
"passed": 9,
"total": 9,
"vacuous": 0
},
"toolchain": {
"test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d",
"zig": "0.16.0"
}
}
243 changes: 243 additions & 0 deletions specs/t27mcp/visual_output.t27
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
// SPDX-License-Identifier: Apache-2.0
; specs/t27mcp/visual_output.t27 -- every answer about a spec, a flow, numbers or a piece of work carries
; one visual, and the visual is the T27F way: drawn from tool rows, labelled, with the file it came from.
; Where it is drawn depends on the host. The rule is said to Claude Code through the MCP server's
; `instructions` (gHashTag/t27-mcp) and to TRI CLAW's chat agent through its prompt
; (gHashTag/999-multibots-telegraf, apps/vibee-editor/render): both read RULE_* from this spec, so the rule
; has one home. See the epic #10066 and T27F, gHashTag/trinity#1648.
; --
; What was measured (2026-10-18, t27-mcp db98153):
; - t27f_graph of the reel fixture: an SVG 1860 wide, text at 13 px, 5923 bytes. Shown inline at 680 px it
; scales to 0.37, so its text is about 4.75 px: unreadable. t27f_view deck-evidence-wide: 1280 wide, body
; text 26 px (13.8 px inline, readable), a caption at 15 px (8 px inline, not readable).
; - The Claude Code desktop app draws an inline SVG or HTML visual with its visual tool (show_widget), at a
; container 680 wide, text at least 11 px. The CLI draws no image; it renders markdown tables.
; - An MCP server's `instructions` (the initialize result) reach the client model's system prompt; a tool
; result does not reach the user as an image on these hosts (reel.t27 header, 2026-10-17).
; - TRI CLAW's chat shows an image for an https URL ending in .svg on its own line or for ![title](url)
; (player/src/components/Chat/ChatAssets.tsx IMAGE_RE). The t27f tools write their SVG to the server's
; temporary directory, so TRI CLAW serves that directory by URL.
; --
; Hosts 0..3 carry the numbers of reel.t27's HOST_*, and channels 0..3 its CHANNEL_*; this spec adds TRI
; CLAW, the inline widget, the image URL and the markdown table. reel.t27 channel_for() stays the channel
; of a reel still; answer_channel() here is the channel of an answer's visual.
; Readability: a drawing of width W shown at INLINE_W shows a font of F px at F * INLINE_W / W px, so it is
; readable when F * INLINE_W >= MIN_FONT_PX * W for its smallest text. A tool SVG is embedded as it is only
; when readable and at most INLINE_MAX_BYTES (the model re-emits it, so its bytes are output); otherwise it
; is redrawn at INLINE_W from t27f_outline rows.
; phi^2 + 1/phi^2 = 3 | TRINITY

module T27mcpVisualOutput;

/// Hosts. 0..3 are reel.t27's numbers.
pub const HOST_CLAUDE_CODE_CLI : u32 = 0;
pub const HOST_CLAUDE_CODE_DESKTOP : u32 = 1;
pub const HOST_MCP_APPS : u32 = 2;
pub const HOST_SITE : u32 = 3;
pub const HOST_TRI_CLAW : u32 = 4;
pub const HOST_COUNT : u32 = 5;

/// Channels. 0..3 are reel.t27's numbers.
pub const CH_FILE_PATH : u32 = 0;
pub const CH_FILE_AND_PANE : u32 = 1;
pub const CH_UI_RESOURCE : u32 = 2;
pub const CH_LIVE_PLAYER : u32 = 3;
pub const CH_INLINE_WIDGET : u32 = 4;
pub const CH_IMAGE_URL : u32 = 5;
pub const CH_MARKDOWN_TABLE : u32 = 6;
pub const CHANNEL_NAME : [7]str = [
"file path",
"file and pane",
"ui resource",
"live player",
"inline widget",
"image url",
"markdown table"
];

/// Answer kinds. The first four carry a visual.
pub const K_SPEC : u32 = 0;
pub const K_FLOW : u32 = 1;
pub const K_NUMBERS : u32 = 2;
pub const K_REPORT : u32 = 3;
pub const K_ONE_SENTENCE : u32 = 4;
pub const K_QUESTION : u32 = 5;

/// The inline container and the smallest text it may show.
pub const INLINE_W : u32 = 680;
pub const MIN_FONT_PX : u32 = 11;
pub const INLINE_MAX_BYTES : u32 = 16384;

/// Where a tool SVG's drawing comes from.
pub const SRC_EMBED : u32 = 0;
pub const SRC_REDRAW : u32 = 1;

/// The T27F labels, as t27f_outline gives them.
pub const LABEL_NAMES : [4]str = ["exact", "derived", "suggested", "unknown"];

/// answer_verdict() answers.
pub const V_OK : u32 = 0;
pub const V_NO_VISUAL : u32 = 1;
pub const V_INVENTED : u32 = 2;
pub const V_NO_LEGEND : u32 = 3;
pub const V_NO_SOURCE : u32 = 4;
pub const V_UNREADABLE : u32 = 5;
pub const VERDICT_TEXT : [6]str = [
"ok",
"no visual: this answer needs one",
"invented: a node no tool row backs",
"no legend: labels drawn without their key",
"no source: the file or URL the drawing came from is not given",
"unreadable: its smallest text is under 11 px where it is shown"
];

/// The rule as Claude Code reads it, from the t27 MCP server's `instructions`. ASCII, one line each.
pub const RULE_CLAUDE_CODE : [7]str = [
"Visual output (specs/t27mcp/visual_output.t27):",
"- Every answer that explains a spec, a flow, numbers or a piece of work carries one visual. Only a one-sentence reply or a question back goes without.",
"- In the Claude Code desktop app, draw it inline with your visual tool (show_widget) when you have one; else give the SVG path, which the app opens. In the CLI, give the path, or a markdown table when there is no SVG.",
"- Draw from tool rows, never from memory: t27f_outline, t27f_graph, t27f_view, t27_spec. Nothing is drawn that a row does not back.",
"- Label every node exact, derived, suggested or unknown as its row says, and show the legend.",
"- Embed a tool SVG as it is only when its smallest font_px * 680 >= 11 * its width and it is at most 16384 bytes; else redraw it 680 wide from t27f_outline rows.",
"- Always name the file the drawing came from: the file is the record, the drawing is a view of it."
];

/// The rule as TRI CLAW's chat agent reads it, from its prompt. ASCII, one line each.
pub const RULE_TRI_CLAW : [6]str = [
"Visual output (specs/t27mcp/visual_output.t27):",
"- Every answer that explains a spec, a flow, numbers or a piece of work carries one visual. Only a one-sentence reply or a question back goes without.",
"- When a t27f tool answered, show its drawing as an image: its https URL ending in .svg on a line of its own, or ![title](url). Never paste SVG source into the chat.",
"- With no drawing, show the rows as a markdown table, not as prose.",
"- Draw from tool rows, never from memory. Label every item exact, derived, suggested or unknown as its row says, and give the legend.",
"- Name where the drawing came from: the tool and its file or URL."
];
pub const RULE_CLAUDE_CODE_LINES : u32 = 7;
pub const RULE_TRI_CLAW_LINES : u32 = 6;

/// Whether an answer of kind `kind` carries a visual.
pub fn needs_visual(kind: u32) -> bool {
return kind <= K_REPORT;
}

/// How an answer's visual reaches the user on `host`. `inline_tool`: the client has a visual tool
/// (show_widget). `svg`: a t27f tool produced a drawing for this answer.
pub fn answer_channel(host: u32, inline_tool: bool, svg: bool) -> u32 {
if (host == HOST_CLAUDE_CODE_DESKTOP) {
if (inline_tool) { return CH_INLINE_WIDGET; }
if (svg) { return CH_FILE_AND_PANE; }
return CH_MARKDOWN_TABLE;
}
if (host == HOST_MCP_APPS) { return CH_UI_RESOURCE; }
if (host == HOST_SITE) { return CH_LIVE_PLAYER; }
if (host == HOST_TRI_CLAW) {
if (svg) { return CH_IMAGE_URL; }
return CH_MARKDOWN_TABLE;
}
if (svg) { return CH_FILE_PATH; }
return CH_MARKDOWN_TABLE;
}

/// Whether text of `font_px` in a drawing `view_w` wide is at least MIN_FONT_PX when shown INLINE_W wide.
pub fn inline_readable(view_w: u32, font_px: u32) -> bool {
if (view_w == 0) { return false; }
return font_px * INLINE_W >= MIN_FONT_PX * view_w;
}

/// Whether a tool SVG goes inline as it is or is redrawn from t27f_outline rows. `min_font_px` is its
/// smallest text.
pub fn inline_source(view_w: u32, min_font_px: u32, bytes: u32) -> u32 {
if (inline_readable(view_w, min_font_px) == false) { return SRC_REDRAW; }
if (bytes > INLINE_MAX_BYTES) { return SRC_REDRAW; }
return SRC_EMBED;
}

/// The verdict on an answer. `drawn`: nodes in its visual; `backed`: those a tool row backs.
/// `label_kinds`: how many of the four labels it draws. `view_w` / `min_font_px`: the drawing's width
/// and its smallest text, in its own units (0 width when it is not shown inline).
pub fn answer_verdict(kind: u32, has_visual: bool, drawn: u32, backed: u32, label_kinds: u32, legend: bool, source: bool, view_w: u32, min_font_px: u32) -> u32 {
if (needs_visual(kind) == false) { return V_OK; }
if (has_visual == false) { return V_NO_VISUAL; }
if (drawn > backed) { return V_INVENTED; }
if (label_kinds > 0) {
if (legend == false) { return V_NO_LEGEND; }
}
if (source == false) { return V_NO_SOURCE; }
if (view_w > 0) {
if (inline_readable(view_w, min_font_px) == false) { return V_UNREADABLE; }
}
return V_OK;
}

test "the tables are the size they say" {
assert CHANNEL_NAME[CH_INLINE_WIDGET] == "inline widget";
assert CHANNEL_NAME[CH_MARKDOWN_TABLE] == "markdown table";
assert LABEL_NAMES[3] == "unknown";
assert VERDICT_TEXT[V_OK] == "ok";
assert RULE_CLAUDE_CODE[0] == "Visual output (specs/t27mcp/visual_output.t27):";
assert RULE_TRI_CLAW[0] == "Visual output (specs/t27mcp/visual_output.t27):";
assert RULE_CLAUDE_CODE[1] == "- Every answer that explains a spec, a flow, numbers or a piece of work carries one visual. Only a one-sentence reply or a question back goes without.";
assert RULE_TRI_CLAW[1] == "- Every answer that explains a spec, a flow, numbers or a piece of work carries one visual. Only a one-sentence reply or a question back goes without.";
}

test "needs_visual: spec, flow, numbers and a report carry one; one sentence and a question do not" {
assert needs_visual(K_SPEC);
assert needs_visual(K_FLOW);
assert needs_visual(K_NUMBERS);
assert needs_visual(K_REPORT);
assert needs_visual(K_ONE_SENTENCE) == false;
assert needs_visual(K_QUESTION) == false;
}

test "answer_channel: the desktop app draws inline, TRI CLAW shows an image URL" {
assert answer_channel(HOST_CLAUDE_CODE_DESKTOP, true, true) == CH_INLINE_WIDGET;
assert answer_channel(HOST_CLAUDE_CODE_DESKTOP, true, false) == CH_INLINE_WIDGET;
assert answer_channel(HOST_TRI_CLAW, false, true) == CH_IMAGE_URL;
assert answer_channel(HOST_TRI_CLAW, true, true) == CH_IMAGE_URL;
}

test "answer_channel: with no inline tool or no drawing, a path or a markdown table" {
assert answer_channel(HOST_CLAUDE_CODE_DESKTOP, false, true) == CH_FILE_AND_PANE;
assert answer_channel(HOST_CLAUDE_CODE_DESKTOP, false, false) == CH_MARKDOWN_TABLE;
assert answer_channel(HOST_CLAUDE_CODE_CLI, false, true) == CH_FILE_PATH;
assert answer_channel(HOST_CLAUDE_CODE_CLI, false, false) == CH_MARKDOWN_TABLE;
assert answer_channel(HOST_TRI_CLAW, false, false) == CH_MARKDOWN_TABLE;
assert answer_channel(HOST_MCP_APPS, false, true) == CH_UI_RESOURCE;
assert answer_channel(HOST_SITE, false, true) == CH_LIVE_PLAYER;
assert answer_channel(HOST_COUNT, false, true) == CH_FILE_PATH;
}

test "inline_readable: the measured drawings" {
assert inline_readable(1860, 13) == false;
assert inline_readable(1280, 26);
assert inline_readable(1280, 15) == false;
assert inline_readable(680, 11);
assert inline_readable(680, 10) == false;
assert inline_readable(340, 6);
}

test "inline_source: embed a small readable SVG, redraw the rest" {
assert inline_source(680, 12, 5923) == SRC_EMBED;
assert inline_source(1860, 13, 5923) == SRC_REDRAW;
assert inline_source(680, 12, INLINE_MAX_BYTES) == SRC_EMBED;
assert inline_source(680, 12, INLINE_MAX_BYTES + 1) == SRC_REDRAW;
}

test "answer_verdict: a backed, labelled, sourced, readable drawing is ok" {
assert answer_verdict(K_SPEC, true, 12, 12, 3, true, true, 680, 12) == V_OK;
assert answer_verdict(K_REPORT, true, 4, 4, 0, false, true, 680, 11) == V_OK;
assert answer_verdict(K_ONE_SENTENCE, false, 0, 0, 0, false, false, 0, 0) == V_OK;
assert answer_verdict(K_NUMBERS, true, 5, 5, 1, true, true, 0, 0) == V_OK;
}

test "negative control: a spec answer with no visual, or with a node no row backs" {
assert answer_verdict(K_SPEC, false, 0, 0, 0, false, false, 0, 0) == V_NO_VISUAL;
assert answer_verdict(K_FLOW, false, 0, 0, 0, false, true, 0, 0) == V_NO_VISUAL;
assert answer_verdict(K_SPEC, true, 13, 12, 3, true, true, 680, 12) == V_INVENTED;
}

test "negative control: labels without a legend, a drawing without its source, unreadable text" {
assert answer_verdict(K_SPEC, true, 12, 12, 2, false, true, 680, 12) == V_NO_LEGEND;
assert answer_verdict(K_SPEC, true, 12, 12, 2, true, false, 680, 12) == V_NO_SOURCE;
assert answer_verdict(K_SPEC, true, 12, 12, 2, true, true, 1860, 13) == V_UNREADABLE;
assert inline_readable(0, 13) == false;
}
Loading