Public: anyone can read this; the crew can change it.
One line of text is one line: src/words.ts, so a title cannot hide, reverse itself or lie about its length #62
Sign in to join this conversation.
Public: anyone can read this; the crew can change it.
Intent. Free text gets one gate in this app, the way a time gets one vocabulary — a single module every action calls, so the next piece of player-written text cannot be validated by hand a second time and differently.
Measured on
maintoday:POST /api/tables {"title":"a\u200bb"}is accepted and renders asab, so two tables can carry the same visible name; a right-to-left override (U+202E) is accepted and reverses everything printed after it, on the page and in the terminal; a non-breaking space passes the "one line of text" check that ASCII control characters fail; and the 60-character limit counts UTF-16 units, so a title of 31 emoji is refused while 60 combining marks stacked on one letter is fine.openTablecarries three hand-written rules for this, and #22 (talk) is about to write a fourth set for 280 characters.Done looks like
src/words.ts(new):oneLine(raw, { max })→Result<string>— normalize to NFC, trim, refuse C0/C1 controls and DEL, refuse the bidi controls (U+202A–U+202E, U+2066–U+2069) and the invisible set (U+200B–U+200D, U+2060, U+FEFF), turn any other Unicode separator into a plain space, collapse runs of spaces, count length in code points, and refuse what is empty after all of it. One named failure and one sentence per rule, in the app's voice ("A title cannot contain invisible characters.").openTablecalls it for the title and its three inline rules are deleted, not duplicated; the stored title is the normalized one, so what is in the database is what a reader sees.src/words.test.ts(new), a case per rule that fails without it: zero-width space, RTL override, NBSP, a title of emoji counted as one each, combining marks counted, an already-clean title passing through byte-identical, and NFD and NFC spellings of the same word storing the same bytes.src/actions.test.tskeeps one test at the action level proving the title path uses the gate, and<script>in a title is still stored as text and still comes back escaped from the page (this module is not an escape;escape()insrc/http.tsstill does that job).AGENTS.mdgains the paragraph that makes it the pattern, beside the one aboutsrc/when.ts: free text has one gate, and an action that validates a string by hand is a drift.Files
src/words.ts(new),src/words.test.ts(new),src/actions.ts,src/actions.test.ts,AGENTS.md.Risks Do not strip what people legitimately write: accents, every non-Latin script and emoji all pass, and only the invisible and the direction-changing are refused — with a refusal that says which, so a player can fix it. Keep it dependency-free:
String.prototype.normalizeand[...text]are enough, and no regex may be built from anything a caller sent.Brief that fits @mason to build, @juniper to review the words of the refusals. (A person assigns.)