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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ would look authoritative while being wrong.
| `t27_check` | Parses and typechecks `.t27` source with the real compiler front end (`wasm/explorer.wasm`, no network) and returns each diagnostic with its line, the source line and a short fix hint for foreign syntax (`in` as a name, `as u3`, `x++`, `a ? b : c`, `match`). The hint rules live in `specs/t27mcp/check_hints.t27`, run as `wasm/check_hints.wasm` (see `wasm/BUILD`); a message the spec does not know gets no hint. `ok: true` means it parses and typechecks, not that it is right: the type checker does not yet resolve names or unknown types (gHashTag/t27#8087), and tests are not run |
| `t27_examples` | Short worked examples of `.t27` idioms for a topic (`clocked`, `comb`, `fn`, `test`, `reset` with async, sync or none): 1-3 slices of real specs, at most 40 lines each, cut from the corpus at a pinned commit, with one line on what each shows. Every slice parses and typechecks and its tests pass on the lab. The topic matcher and the pointer table live in `specs/t27mcp/examples.t27`, run as `wasm/examples.wasm` (see `wasm/BUILD`); an unknown topic returns the list of topics and no example |
| `t27f_view` | Draws a still of the T27F demo reel as an SVG file and returns its path. By default the still is a presentation slide (`specs/t27f/deck.t27`); `look: 0` draws the plain map (`specs/t27f/reel.t27`). A scene still shows the whole glass card (`STILL_CARD_ALPHA`); an exact `frame` shows the card as the video would (`card_alpha`). Every position, size and colour comes from the specs, run as `wasm/t27f.wasm` (see `wasm/BUILD`) |
| `t27f_graph` | Draws the T27F graph view (`specs/t27f/spec_graph_view.t27`) as an SVG file and returns its path: a box per node and a line per edge, placed, sized and coloured by the spec's layout functions, each box labelled with its node kind, inference label (exact, derived, suggested, unknown) and source lines. Until t27c exports graphs, only the `spec_graph` fixtures can be drawn: `fixture: 0` is tutorial 08, `fixture: 1` adds a suggestion |
| `t27f_graph` | Draws the T27F graph view (`specs/t27f/spec_graph_view.t27`) as an SVG file and returns its path: a box per node and a line per edge, placed, sized and coloured by the spec's layout functions, each box labelled with its node kind, inference label (exact, derived, suggested, unknown) and source lines. Pass `path` (a published `.t27` spec, e.g. `specs/tutorial/08_modules_and_arrays.t27`) to draw that spec's exported graph, or `fixture` for the built-in samples (`fixture: 0` is tutorial 08, `fixture: 1` adds a suggestion). A path graph is exact only (nodes module, const, fn, test, invariant, bench, use; edges calls, uses, constrains; no derived edges) and holds at most 192 nodes and 254 edges: a larger spec is refused with that verdict, never drawn truncated |
| `t27f_outline` | Reads a `.t27` spec through its T27F graph instead of the whole file. `path` is a published spec, e.g. `specs/tutorial/08_modules_and_arrays.t27`. Returns compact text: one line per node (kind, name, source lines a-b, label), one per edge (kind, from -> to, line) and a short section "fn X is tested by: ..." (derived from the exact calls edges, and labelled so). The wording, which facts print, how a span prints, which refusal text a verdict takes and the tested-by view are decided in `specs/t27f/spec_graph_outline.t27`, run as `wasm/t27f.wasm` (see `wasm/BUILD`); the host only moves bytes and fills the spec's `{}` templates. The graph is exact only (the exporter, `specs/t27f/spec_graph_export.t27`, run as `wasm/t27f.wasm`, emits no derived, suggested or unknown edges; top-level items only) and capped at 192 nodes and 254 edges. A spec over the cap (e.g. `specs/numeric/gf16.t27`, 436 nodes) returns the refusal verdict with the counts, never a truncated graph |

Every tool takes `fresh: true` to bypass the five-minute cache. That parameter
is not decoration: the publish pipeline is slower than a conversation, so a
Expand Down
57 changes: 57 additions & 0 deletions graph.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Host I/O of t27f_outline and t27f_graph {path}. The exporter, the layout and every decision about the
// outline run in wasm/t27f.wasm: specs/t27f/spec_graph_export.t27 (nodes and edges),
// spec_graph_view.t27 (layout) and spec_graph_outline.t27 (the wording, which facts print, how a span
// prints, which refusal text a verdict takes, the tests of a fn). See wasm/BUILD. This file moves bytes in,
// reads values out through the gx_* exports and fills the `{}` marks of the spec's templates, left to
// right. It chooses no template, filters no fact and computes no span.
import * as G from './wasm/spec_graph_view.mjs';
import * as O from './wasm/spec_graph_outline.mjs';

// Name of constant `v` among the G constants that start with `prefix` (NODE_FN -> 'fn').
const nm = (prefix, v) => Object.keys(G).find((c) => c.startsWith(prefix) && !c.endsWith('KINDS') && G[c] === v)?.slice(prefix.length).toLowerCase() ?? String(v);

// Fill the `{}` marks of a spec template with `vals`, in order.
const fill = (tpl, ...vals) => { let i = 0; return tpl.replace(/\{\}/g, () => String(vals[i++])); };

// Write the spec into the wasm input buffer and run the exporter; returns the exporter's verdict.
export function load(R, path, src) {
const body = Buffer.from(src, 'utf8'), p = Buffer.from(path, 'utf8');
if (!R.gx_set_len(body.length)) throw new Error(`${path} is ${body.length} bytes; the exporter's input buffer holds ${R.gx_src_cap()}`);
if (!R.gx_set_path_len(p.length)) throw new Error(`path is ${p.length} bytes; the limit is 512`);
const mem = new Uint8Array(R.memory.buffer);
mem.set(body, R.gx_src_ptr());
mem.set(p, R.gx_path_ptr());
return R.gx_load();
}

export const nodeName = (R, i) => Buffer.from(R.memory.buffer, R.gx_node_name_ptr(i), R.gx_node_name_len(i)).toString('utf8');

// The text of a refused graph: the spec picks the template, the host fills in the numbers.
export function refusal(R, path, v) {
const t = O.REFUSAL_TEXT[R.refusal_index(v)];
if (v === G.ERR_TOO_MANY_NODES) return fill(t, path, R.gx_node_count(), G.MAX_NODES);
if (v === G.ERR_TOO_MANY_EDGES) return fill(t, path, R.gx_edge_count(), G.MAX_EDGES);
return fill(t, path, nm('ERR_', v).replace(/_/g, ' '), v);
}

export function outline(R, path, src) {
const v = load(R, path, src);
if (v !== G.GRAPH_OK) return refusal(R, path, v);
const T = O.OUTLINE_TEXT, n = R.gx_node_count(), m = R.gx_edge_count(), out = [];
const span = (i) => { const a = R.gx_line_start(i), b = R.gx_line_end(i); return R.span_is_range(a, b) ? `${a}-${b}` : String(a); };
out.push(fill(T[0], path, R.gx_line_count(), n, m, G.MAX_NODES, G.MAX_EDGES), T[1]);
for (let i = 0; i < n; i++) if (R.shows_in_outline(R.gx_node_label(i))) out.push(fill(T[4], i, nm('NODE_', R.gx_node_kind(i)), nodeName(R, i), span(i), nm('LABEL_', R.gx_node_label(i))));
out.push(T[2]);
for (let j = 0; j < m; j++) {
const f = R.gx_edge_from(j), t = R.gx_edge_to(j);
if (R.shows_in_outline(R.gx_edge_label(j))) out.push(fill(T[5], nm('EDGE_', R.gx_edge_kind(j)), f, nodeName(R, f), t, nodeName(R, t), R.gx_edge_line(j)));
}
out.push(T[3]);
for (let i = 0; i < n; i++) {
if (R.gx_node_kind(i) !== G.NODE_FN) continue;
const names = [];
for (let k = 0; k < R.gx_o_tested_by_count(i); k++) names.push(nodeName(R, R.gx_o_tested_by_of(i, k)));
out.push(names.length ? fill(T[6], nodeName(R, i), names.join('; ')) : fill(T[7], nodeName(R, i)));
}
return out.join('\n');
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"bin": { "t27-mcp": "./server.mjs" },
"main": "server.mjs",
"files": ["server.mjs", "check.mjs", "examples.mjs", "brief.mjs", "deck-view.mjs", "wasm/explorer.wasm", "wasm/check_hints.wasm", "wasm/check_hints.mjs", "wasm/examples.wasm", "wasm/examples_table.mjs", "wasm/spec_brief.wasm", "wasm/spec_brief.mjs", "wasm/t27f.wasm", "wasm/reel.mjs", "wasm/deck.mjs", "README.md"],
"files": ["server.mjs", "graph.mjs", "check.mjs", "examples.mjs", "brief.mjs", "deck-view.mjs", "wasm/explorer.wasm", "wasm/check_hints.wasm", "wasm/check_hints.mjs", "wasm/examples.wasm", "wasm/examples_table.mjs", "wasm/spec_brief.wasm", "wasm/spec_brief.mjs", "wasm/t27f.wasm", "wasm/spec_graph_view.mjs", "wasm/spec_graph_export.mjs", "wasm/spec_graph_outline.mjs", "wasm/reel.mjs", "wasm/deck.mjs", "README.md"],
"engines": { "node": ">=18" },
"scripts": {
"start": "node server.mjs",
Expand Down
62 changes: 45 additions & 17 deletions server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { slide } from './deck-view.mjs';
import { check } from './check.mjs';
import { examples } from './examples.mjs';
import { brief, one, failureReasons } from './brief.mjs';
import * as GX from './graph.mjs';

const VERSION = '1.1.0';
const SITE = process.env.T27_SITE || 'https://t27.ai';
Expand Down Expand Up @@ -178,9 +179,15 @@ const SPEC_FILTERS = {
// deck look, specs/t27f/deck.t27 (gHashTag/t27); their bodies run as wasm/t27f.wasm and their constants
// are wasm/reel.mjs and wasm/deck.mjs. This glue writes SVG only; the deck look is deck-view.mjs.
// t27f_graph draws specs/t27f/spec_graph_view.t27 the same way; its constants are wasm/spec_graph_view.mjs.
// t27f_outline and t27f_graph {path} read a spec's bytes through specs/t27f/spec_graph_export.t27 (graph.mjs is the I/O).
const R = new WebAssembly.Instance(new WebAssembly.Module(readFileSync(new URL('./wasm/t27f.wasm', import.meta.url)))).exports;
const PAINT = ['none', '#3a3f4b', '#3b82f6', '#8a7a3a', '#ffd45a', '#22c55e', '#1f2229']; // host palette by STYLE_*
const esc = (v) => String(v).replace(/[<>&"]/g, (c) => `&#${c.charCodeAt(0)};`);
// A corpus-relative .t27 path for the exporter tools; the fetch itself is specSource's.
const specPath = (p) => {
if (typeof p !== 'string' || !/^[A-Za-z0-9_\-./]+\.t27$/.test(p) || p.startsWith('/') || p.split('/').includes('..')) throw new Error('path must be a corpus path to a .t27 file, e.g. specs/tutorial/08_modules_and_arrays.t27');
return p;
};

const tools = [
{
Expand Down Expand Up @@ -667,29 +674,50 @@ const tools = [
return `${path}\nT27F demo reel, ${look === deck.LOOK_DECK ? `${['cover', 'stage', 'split'][R.layout_for(R.demo_kind(s))]} slide` : 'map'}, scene ${s} (${name}), frame ${f} of ${n}, ${W}x${H}, focus cell ${focus}; channel for host ${host}: ${ch}.`;
},
},
{
name: 't27f_outline',
description:
"A spec's structure from its T27F graph instead of its whole source: one line per declaration (kind, name, lines a-b, label) and per edge (calls, uses, constrains: from -> to, line of first use), plus a short derived list of which tests call which fn. Read by specs/t27f/spec_graph_export.t27, a deterministic byte scanner: every node and edge is EXACT, no derived or suggested edge is invented; the tested-by list is marked derived and is only an inversion of the exact test -> fn calls. The graph holds at most 192 nodes and 254 edges (top-level declarations and the edges between them); a larger spec is refused with that verdict, never truncated. Only the top level is read: nothing inside a struct or enum body, names are matched as text, not resolved.",
inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Exact corpus path, e.g. specs/tutorial/08_modules_and_arrays.t27' }, fresh: { type: 'boolean' } }, required: ['path'] },
handler: async ({ path, fresh }) => GX.outline(R, specPath(path), await specSource(specPath(path), fresh)),
},
{
name: 't27f_graph',
description:
'Draw the T27F graph view (specs/t27f/spec_graph_view.t27) of a spec graph as an SVG file and return its path: a box per node, a line per edge, placed and styled by the spec, labelled by kind, inference label and source lines. Only the spec_graph fixtures exist until t27c exports graphs: 0 tutorial 08, 1 tutorial 08 with a suggestion.',
inputSchema: { type: 'object', properties: { fixture: { type: 'integer', description: '0 tutorial 08 (default), 1 with a suggestion' }, out: { type: 'string', description: 'Output file (default: the temp directory)' } } },
handler: async ({ fixture = 0, out }) => {
if (fixture !== 0 && fixture !== 1) throw new Error('fixture must be 0 or 1');
const v = R.fx_verdict(fixture), hex = (c) => `#${c.toString(16).padStart(6, '0')}`;
if (v !== G.VIEW_OK) throw new Error(`the view refuses fixture ${fixture}: verdict ${v}`);
"Draw the T27F graph view (specs/t27f/spec_graph_view.t27) of a spec graph as an SVG file and return its path: a box per node, a line per edge, placed and styled by the spec, labelled by kind, name, inference label and source lines. With path, the graph is the exact-only export of that corpus spec (specs/t27f/spec_graph_export.t27: no derived edges; at most 192 nodes and 254 edges, a larger spec is refused, never truncated). With fixture, the hand-read tutorial 08 graph: 0 as is, 1 with a suggestion. path wins when both are given.",
inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Exact corpus path, e.g. specs/tutorial/08_modules_and_arrays.t27' }, fixture: { type: 'integer', description: '0 tutorial 08 (default), 1 with a suggestion' }, out: { type: 'string', description: 'Output file (default: the temp directory)' }, fresh: { type: 'boolean' } } },
handler: async ({ path, fixture = 0, out, fresh }) => {
// q reads the same quantities from the exported graph (gx_*) or from a fixture (fx_*).
let q, tag;
if (path !== undefined) {
const p = specPath(path), v = GX.load(R, p, await specSource(p, fresh));
if (v !== G.GRAPH_OK) throw new Error(GX.refusal(R, p, v));
const g = { width: 'view_width', height: 'view_height', verdict: 'view_verdict', node_x: 'view_node_x', node_y: 'view_node_y', node_w: 'view_node_w', node_h: 'view_node_h', node_fill: 'view_node_fill', node_stroke: 'view_node_stroke', node_dash: 'view_node_dash', edge_x1: 'view_edge_x1', edge_y1: 'view_edge_y1', edge_x2: 'view_edge_x2', edge_y2: 'view_edge_y2', edge_stroke: 'view_edge_stroke', edge_dash: 'view_edge_dash', node_line_start: 'line_start', node_line_end: 'line_end' };
q = (k, ...a) => R[`gx_${g[k] ?? k}`](...a);
tag = p;
} else {
if (fixture !== 0 && fixture !== 1) throw new Error('fixture must be 0 or 1');
q = (k, ...a) => R[`fx_${k}`](fixture, ...a);
tag = `fixture ${fixture}`;
}
const v = q('verdict'), hex = (c) => `#${c.toString(16).padStart(6, '0')}`;
if (v !== G.VIEW_OK) throw new Error(`the view refuses ${tag}: verdict ${v}`);
const nm = (p, k) => Object.keys(G).find((c) => c.startsWith(p) && !c.endsWith('KINDS') && G[c] === k)?.slice(p.length).toLowerCase() ?? k;
const W = R.fx_width(fixture), H = R.fx_height(fixture), svg = [`<rect width="${W}" height="${H}" fill="${hex(G.VIEW_BACKDROP)}"/>`];
for (let j = 0; j < R.fx_edge_count(fixture); j++) {
const d = R.fx_edge_dash(fixture, j);
svg.push(`<line x1="${R.fx_edge_x1(fixture, j)}" y1="${R.fx_edge_y1(fixture, j)}" x2="${R.fx_edge_x2(fixture, j)}" y2="${R.fx_edge_y2(fixture, j)}" stroke="${hex(R.fx_edge_stroke(fixture, j))}" stroke-width="1.5"${d ? ` stroke-dasharray="${d}"` : ''}><title>${nm('EDGE_', R.fx_edge_kind(fixture, j))}, line ${R.fx_edge_line(fixture, j)}</title></line>`);
const W = q('width'), H = q('height'), svg = [`<rect width="${W}" height="${H}" fill="${hex(G.VIEW_BACKDROP)}"/>`];
for (let j = 0; j < q('edge_count'); j++) {
const d = q('edge_dash', j);
svg.push(`<line x1="${q('edge_x1', j)}" y1="${q('edge_y1', j)}" x2="${q('edge_x2', j)}" y2="${q('edge_y2', j)}" stroke="${hex(q('edge_stroke', j))}" stroke-width="1.5"${d ? ` stroke-dasharray="${d}"` : ''}><title>${nm('EDGE_', q('edge_kind', j))}, line ${q('edge_line', j)}</title></line>`);
}
for (let i = 0; i < R.fx_node_count(fixture); i++) {
const x = R.fx_node_x(fixture, i), y = R.fx_node_y(fixture, i), d = R.fx_node_dash(fixture, i), a = R.fx_node_line_start(fixture, i), b = R.fx_node_line_end(fixture, i);
svg.push(`<rect x="${x}" y="${y}" width="${R.fx_node_w(fixture, i)}" height="${R.fx_node_h(fixture, i)}" rx="6" fill="${hex(R.fx_node_fill(fixture, i))}" stroke="${hex(R.fx_node_stroke(fixture, i))}" stroke-width="1.5"${d ? ` stroke-dasharray="${d}"` : ''}/>`,
`<text x="${x + 8}" y="${y + G.VIEW_FONT_PX + 6}" font-size="${G.VIEW_FONT_PX}" fill="${hex(G.VIEW_TEXT)}">${nm('NODE_', R.fx_node_kind(fixture, i))} ${i} - ${nm('LABEL_', R.fx_node_label(fixture, i))} - L${a}${b > a ? `-${b}` : ''}</text>`);
for (let i = 0; i < q('node_count'); i++) {
const x = q('node_x', i), y = q('node_y', i), d = q('node_dash', i), a = q('node_line_start', i), b = q('node_line_end', i);
const who = path !== undefined ? GX.nodeName(R, i) : String(i), w = q('node_w', i);
const full = `${nm('NODE_', q('node_kind', i))} ${who} - ${nm('LABEL_', q('node_label', i))} - L${a}${b > a ? `-${b}` : ''}`, fit = Math.floor((w - 16) / (G.VIEW_FONT_PX * 0.6)); // host text fitting; the box is the spec's
svg.push(`<rect x="${x}" y="${y}" width="${w}" height="${q('node_h', i)}" rx="6" fill="${hex(q('node_fill', i))}" stroke="${hex(q('node_stroke', i))}" stroke-width="1.5"${d ? ` stroke-dasharray="${d}"` : ''}/>`,
`<text x="${x + 8}" y="${y + G.VIEW_FONT_PX + 6}" font-size="${G.VIEW_FONT_PX}" fill="${hex(G.VIEW_TEXT)}"><title>${esc(full)}</title>${esc(full.length > fit ? `${full.slice(0, fit - 1)}...` : full)}</text>`);
}
const path = out || join(tmpdir(), `t27f-graph-fx${fixture}.svg`);
writeFileSync(path, `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" font-family="system-ui,sans-serif">${svg.join('')}</svg>\n`);
return `${path}\nT27F graph view, fixture ${fixture}: ${R.fx_node_count(fixture)} nodes, ${R.fx_edge_count(fixture)} edges, ${W}x${H}.`;
const file = out || join(tmpdir(), path !== undefined ? `t27f-graph-${path.replace(/[^A-Za-z0-9]+/g, '_')}.svg` : `t27f-graph-fx${fixture}.svg`);
writeFileSync(file, `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" font-family="system-ui,sans-serif">${svg.join('')}</svg>\n`);
return `${file}\nT27F graph view, ${tag}: ${q('node_count')} nodes, ${q('edge_count')} edges, ${W}x${H}${path !== undefined ? ', exact only' : ''}.`;
},
},
];
Expand Down
Loading
Loading