diff --git a/specs/OWNERS.md b/specs/OWNERS.md index d52c15b084..75f3f1bb29 100644 --- a/specs/OWNERS.md +++ b/specs/OWNERS.md @@ -29,6 +29,7 @@ | `ui/` | **T-Queen** | The site's viewport contract as a `.t27` spec (`ui/viewport.t27`, `KIND = "viewport"`): tiers by CSS viewport width, panes per tier, the 44 px touch minimum, the six-size QA matrix and the Queen rail capacity read from `Queen.css`; the site (gHashTag/trinity) generates `viewport.generated.ts` / `.css` from a vendored copy and evaluates the spec's own test blocks; `ui/queen_evidence.t27` (`module ui_queen_evidence`): the evidence constants of the Queen's FFN and Ternary Check panel, from which the site generates `queenEvidence.generated.ts`; canonical here since S10, byte-identical to the copy the site carried alone | | `catalog/` | **T-Queen** | How the site's spec catalog and the Queen map find the repositories that write `.t27` (`catalog/discovery.t27`, `KIND = "discovery"`): the GitHub owners scanned, the exclusions, what makes a candidate qualify (a declaration in the compiler's AST, not a file extension) and the limits; the site (gHashTag/trinity) vendors a byte-identical copy and `scripts/discover-t27-worlds.mjs` reads it through the compiler wasm before touching GitHub | | `t27f/` | **T-Queen** | T27F, the visual spec of a .t27 file (gHashTag/trinity#1648, RFC #1649; definition in `.claude/skills/t27f/SKILL.md`): `t27f/spec_graph.t27` (module `t27f_spec_graph`): the core graph IR a spec becomes -- node and edge kinds, inference labels, source spans, `validate_graph`, source mapping both ways and the node -> ui_plan widget binding, with one hand-read fixture; `t27f/spec_graph_view.t27` (module `t27f_spec_graph_view`): the graph view, a deterministic layout of a spec graph -- one column per node kind, style and row order from the inference label, UNKNOWN nodes drawn alone, every edge with a source line; numbers only, a host draws them; `t27f/ui_plan.t27` (module `t27f_ui_plan`): the renderer-neutral UI plan IR and the trusted widget catalog. Applications built on it: the topic-to-value map of gHashTag/trinity#1677 (`t27f/topic_engine.t27`, module `T27fTopicEngine`): the 3 x 9 = 27 lesson shape, intake and gap rules, keyword routing to one domain, the Bee dispatch cap, the evidence and approval gates, and progression that is never money; driven by the Claude Code skill `.claude/skills/t27f/SKILL.md` through the t27 MCP server; `t27f/reel.t27` (module `T27fReel`): the video format of a topic map and the still views of a Claude Code answer -- honeycomb geometry, focus+context camera, scene catalog, per-target limits, gap styling, the posting gate and the channel per host; numbers only, a host draws them; `t27f/deck.t27` (module `T27fDeck`): the default look of a reel, every scene a presentation slide -- layouts per scene, regions, type sizes, glass card, palette kept off the status colours, contrast and card fades | +| `t27mcp/` | **T-Queen** | The decisions of the t27 MCP server (gHashTag/t27-mcp) as `.t27` specs, compiled to wasm that the server runs: `t27mcp/check_hints.t27` (module `T27mcpCheckHints`): which short "write this instead" hint a compiler message gets for foreign syntax (`in` as a name, `as u3`, `x++`, `a ? b : c`, `match`), and the line number a message names; the server's JavaScript is I/O glue and holds no rule | | `trinity/` | **T-Queen** + **A-Architect** | The project manifest of gHashTag/trinity as `.t27` specs (`trinity/project.t27`, `KIND = "project"`; `trinity/capabilities/.t27`, `KIND = "capability"`): the pinned consumer revision, profiles, dispositions and evidence tags, dialect and build counts, dependency pins and the work packages of gHashTag/trinity#988; each card names its owner, canonical spec, implementation and generated paths, build targets, acceptance command and evidence; `tools/trinity_manifest.py` derives the inventory from a clean pinned checkout and holds the cards to it; `trinity/compiler_matrix.t27` (`KIND = "compiler-matrix"`) names the pinned compiler, the backends, the stages and the features of the executable subset with their fixtures under `bootstrap/tests/fixtures/trinity_matrix/`, and `tools/trinity_compiler_matrix.py` measures them; `trinity/build_graph.t27` (`KIND = "build-graph"`) declares the consumer's toolchain, pins, profiles, generated-file boundaries and receipt policy, and `tools/trinity_build_receipt.py` derives the graph, writes the receipts and judges drift; the consumer vendors a byte-identical copy and generates its manifest from it (S12). Shared contracts stay in their own subtrees (`numeric/`, `vsa/`, `queen/`, `tools/`, `docs/`, `ui/`) and are cited, never copied | Each subtree with substantial churn should keep a local **`OWNERS.md`** (see below). diff --git a/specs/t27mcp/check_hints.t27 b/specs/t27mcp/check_hints.t27 new file mode 100644 index 0000000000..7520b782a0 --- /dev/null +++ b/specs/t27mcp/check_hints.t27 @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/t27mcp/check_hints.t27 -- t27_check of the t27 MCP server (gHashTag/t27-mcp): what to tell a model +; that wrote foreign syntax. The compiler's messages are exact but written for compiler authors; this spec +; maps the ones a model really triggers to a short "write this instead", and reads the line number out of a +; message. See #10073 (epic #10066). +; -- +; Compiled to wasm (`t27c gen-c`, then `zig cc -target wasm32-wasi`), the server calls hint_code() and line_of() +; with the compiler's message and nothing else; `t27c gen-js` supplies HINTS. The server's JavaScript holds no +; rule. What is NOT restated here: the parser and the typechecker (bindings/wasm-explorer runs them as wasm +; and produces the messages), and the worked examples (t27mcp/examples.t27). +; -- +; The rules: +; - A message is matched by the text the compiler really emits (each test quotes one), in a fixed order; +; the first rule that matches wins. A message no rule knows gets H_NONE: the server prints the compiler's +; own words and invents nothing. A clean run has no message, so it gets H_NONE too. +; - Constructs that parse and typecheck (`let`, `&&`, `||`, Zig-style signatures) get no hint: the compiler +; accepts them, and a hint would be a claim about taste, not about an error. +; - line_of() is the number after the first `line ` in a message; failing that, the number after a final `:` +; (the typechecker's warnings end `... is declared:2`); failing that, 0. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module T27mcpCheckHints; + +/// hint_code() answers; HINTS[code] is the text to show. +pub const H_NONE : u32 = 0; +pub const H_KEYWORD_IN : u32 = 1; +pub const H_CAST : u32 = 2; +pub const H_MATCH : u32 = 3; +pub const H_INCREMENT : u32 = 4; +pub const H_TERNARY : u32 = 5; +pub const H_TOP_LEVEL : u32 = 6; +pub const H_BRACE : u32 = 7; +pub const H_WIDTH : u32 = 8; +pub const H_UNKNOWN_TYPE : u32 = 9; +pub const H_COUNT : u32 = 10; + +pub const HINTS : [10]str = [ + "", + "`in` is a reserved keyword, so it cannot name a port, parameter or variable. Rename it (din, data_in) everywhere it appears.", + "Casts reach only bool u8 i8 u16 i16 u32 i32 u64 i64 usize f32 f64: there is no `as u3`. Keep a wider type and mask the value, e.g. `x & 7`.", + "t27 has no `match`. Write `return switch (x) { 0 => a, else => b };`: the arms are expressions and `else =>` closes the switch.", + "An expression ended early at `;`. t27 has no `x++` or `x--`: write `x = x + 1;`. Otherwise an operand is missing before the `;`.", + "t27 has no `c ? a : b`. Declare `var r : T = b;`, then `if (c) { r = a; }`, and use r.", + "A top-level item was not recognised. `impl`, `class`, `trait` and `#include` do not exist: write `pub fn name(args) -> T { .. }` and pass the value as a parameter.", + "A `{` where an expression was expected. A switch arm is `pattern => expr,` with no block, and the switch is returned: `return switch (x) { 0 => a, else => b };`.", + "Types of different widths do not convert on their own. Convert with `as u8` (or another type from the cast list: bool u8 i8 u16 i16 u32 i32 u64 i64 usize f32 f64), or declare the wider type.", + "Unknown type. Scalars are bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64; text is `string` (`str` in a const table); an array is `[N]T`." +]; + +/// find() answers when the needle does not occur. +pub const NO_POS : u32 = 4294967295; + +/// Byte offset of the first occurrence of `needle` in `hay`, or NO_POS. +pub fn find(hay: string, needle: string) -> u32 { + var hn : u32 = (hay.len as u32); + var nn : u32 = (needle.len as u32); + if (nn > hn) { return NO_POS; } + var i : u32 = 0; + while (i <= hn - nn) { + var j : u32 = 0; + var same : u32 = 1; + while (j < nn) { + if ((hay[i + j] as u32) != (needle[j] as u32)) { same = 0; } + j = j + 1; + } + if (same == 1) { return i; } + i = i + 1; + } + return NO_POS; +} + +/// 1 when `needle` occurs in `hay`. +pub fn has(hay: string, needle: string) -> u32 { + if (find(hay, needle) == NO_POS) { return 0; } + return 1; +} + +/// The hint code for one compiler message (parse error, typecheck error or warning). +pub fn hint_code(msg: string) -> u32 { + if (has(msg, "KwIn (") == 1) { return H_KEYWORD_IN; } + if (has(msg, "unknown cast target type") == 1) { return H_CAST; } + if (has(msg, "`match` block") == 1) { return H_MATCH; } + if (has(msg, "Unexpected token in expression: Semicolon") == 1) { return H_INCREMENT; } + if (has(msg, "after expression statement: Colon") == 1) { return H_TERNARY; } + if (has(msg, "at module level") == 1) { + if (has(msg, "after expression statement") == 1) { return H_TOP_LEVEL; } + } + if (has(msg, "Unexpected token in expression: LBrace") == 1) { return H_BRACE; } + if (has(msg, "where") == 1) { + if (has(msg, "is declared") == 1) { return H_WIDTH; } + } + if (has(msg, "unknown type `") == 1) { return H_UNKNOWN_TYPE; } + return H_NONE; +} + +/// The typechecker's warnings end in `:N` (`... is declared:2`): N, or 0 when the message does not. +pub fn trailing_line(msg: string) -> u32 { + var len : u32 = (msg.len as u32); + var k : u32 = len; + var n : u32 = 0; + var scale : u32 = 1; + while (k > 0) { + var c : u32 = (msg[k - 1] as u32); + if (c < 48) { break; } + if (c > 57) { break; } + n = n + (c - 48) * scale; + scale = scale * 10; + k = k - 1; + } + if (k == len) { return 0; } + if (k == 0) { return 0; } + if ((msg[k - 1] as u32) != 58) { return 0; } + return n; +} + +/// The line number a message names (`near line 2`, `at line 3`), or 0. +pub fn line_of(msg: string) -> u32 { + var p : u32 = find(msg, "line "); + if (p == NO_POS) { return trailing_line(msg); } + var k : u32 = p + 5; + var n : u32 = 0; + var len : u32 = (msg.len as u32); + while (k < len) { + var c : u32 = (msg[k] as u32); + if (c < 48) { return n; } + if (c > 57) { return n; } + n = n * 10 + (c - 48); + k = k + 1; + } + return n; +} + +test "find: first offset, and NO_POS when absent" { + assert find("abcabc", "ca") == 2; + assert find("abc", "abcd") == NO_POS; + assert find("abc", "x") == NO_POS; +} + +test "line_of: the number after the first `line `" { + assert line_of("parse error in fn 'f' near line 2: unknown cast target type `u3`") == 2; + assert line_of("function 'g' expects 1 args, got 2 at line 13") == 13; + assert line_of("warning: returns U16 where U8 is declared:2") == 2; + assert line_of("warning: returns U16 where U8 is declared:117") == 117; + assert line_of("warning: unknown type `String` in parameter `a` of `f`") == 0; + assert line_of("12345") == 0; + assert line_of("") == 0; +} + +test "keyword in: the message the parser really emits for an `in` port" { + assert hint_code("parse error in fn 'f' near line 2: Unexpected token in expression: KwIn ('in') at line 2:33") == H_KEYWORD_IN; +} + +test "cast: `as u3` and `as u12` are not on the whitelist" { + assert hint_code("parse error in fn 'f' near line 2: unknown cast target type `u3`; expected one of [bool, u8]") == H_CAST; + assert hint_code("parse error in fn 'f' near line 2: unknown cast target type `u12`; expected one of [bool, u8]") == H_CAST; +} + +test "match: a typecheck error, not a parse error" { + assert hint_code("`match` block at line 2 in fn `f` is Rust; t27 has no `match`") == H_MATCH; +} + +test "increment: `x++;` ends the expression at the semicolon" { + assert hint_code("parse error in fn 'f' near line 2: Unexpected token in expression: Semicolon (';') at line 2:38") == H_INCREMENT; +} + +test "ternary: a C conditional leaves a Colon behind" { + assert hint_code("parse error in fn 'f' near line 2: unexpected token after expression statement: Colon") == H_TERNARY; +} + +test "top level: impl and friends" { + assert hint_code("parse error at module level near line 3: unexpected token after expression statement: Ident") == H_TOP_LEVEL; +} + +test "brace: a Zig-style switch arm block" { + assert hint_code("parse error in fn 'f' near line 3: Unexpected token in expression: LBrace ('{') at line 3:23") == H_BRACE; +} + +test "width: the typechecker's warning" { + assert hint_code("warning: returns U16 where U8 is declared:2") == H_WIDTH; +} + +test "unknown type: String is not a t27 type" { + assert hint_code("warning: unknown type `String` in parameter `a` of `f`") == H_UNKNOWN_TYPE; +} + +test "negative control: a clean run and unrelated messages get no hint" { + assert hint_code("") == H_NONE; + assert hint_code("function 'g' expects 1 args, got 2 at line 3") == H_NONE; + assert hint_code("unterminated string literal near line 4") == H_NONE; + assert hint_code("Unexpected token in expression: Plus") == H_NONE; +} + +test "every hint code has the text its rule promises, and H_NONE has none" { + assert HINTS[H_NONE] == ""; + assert has(HINTS[H_KEYWORD_IN], "Rename") == 1; + assert has(HINTS[H_CAST], "x & 7") == 1; + assert has(HINTS[H_MATCH], "switch") == 1; + assert has(HINTS[H_INCREMENT], "x = x + 1") == 1; + assert has(HINTS[H_TERNARY], "if (c)") == 1; + assert has(HINTS[H_TOP_LEVEL], "pub fn") == 1; + assert has(HINTS[H_BRACE], "return switch") == 1; + assert has(HINTS[H_WIDTH], "as u8") == 1; + assert has(HINTS[H_UNKNOWN_TYPE], "string") == 1; + assert has(HINTS[H_UNKNOWN_TYPE], "no such text") == 0; +}