Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
One-time setup that gathers your project's design context and saves it to CLAUDE.md for future sessions.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/live/event-validation.mjs
1/**2* Shared event validation for the live helper server.3* Extracted for unit testing (insert mode rules).4*/56import { canCreateInsert } from './insert-ui.mjs';78// The accepted visual action values come from the canonical vocabulary so the9// validator, the picker UI, and the marketing demo never drift. Imported (not10// just re-exported) so it is also in scope for the validators below.11import { VISUAL_ACTIONS } from './vocabulary.mjs';12export { VISUAL_ACTIONS };1314const ID_PATTERN = /^[0-9a-f]{8}$/;15const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/;16const INSERT_POSITIONS = new Set(['before', 'after']);17const FORBIDDEN_MANUAL_EDIT_TEXT_CHARS = ['<', '{', '}', '`'];1819function isValidId(v) { return typeof v === 'string' && ID_PATTERN.test(v); }20function isValidVariantId(v) { return typeof v === 'string' && VARIANT_ID_PATTERN.test(v); }2122function validateManualEditText(newText) {23if (typeof newText !== 'string') return null;24const hits = FORBIDDEN_MANUAL_EDIT_TEXT_CHARS.filter((char) => newText.includes(char));25return hits.length > 0 ? hits : null;26}2728function validateAnnotationFields(msg) {29if (msg.screenshotPath !== undefined && typeof msg.screenshotPath !== 'string') {30return 'generate: screenshotPath must be string';31}32if (msg.comments !== undefined && !Array.isArray(msg.comments)) {33return 'generate: comments must be array';34}35if (msg.strokes !== undefined && !Array.isArray(msg.strokes)) {36return 'generate: strokes must be array';37}38return null;39}4041function validateInsertGenerate(msg) {42if (!msg.insert || typeof msg.insert !== 'object') return 'generate: insert mode requires insert object';43if (!INSERT_POSITIONS.has(msg.insert.position)) return 'generate: insert.position must be before or after';44const anchor = msg.insert.anchor;45if (!anchor || typeof anchor !== 'object') return 'generate: insert.anchor required';46if (!anchor.tagName && !anchor.outerHTML && !(Array.isArray(anchor.classes) && anchor.classes.length)) {47return 'generate: insert.anchor needs tagName, classes, or outerHTML';48}49if (!msg.placeholder || typeof msg.placeholder !== 'object') return 'generate: insert mode requires placeholder dimensions';50if (!Number.isFinite(msg.placeholder.width) || !Number.isFinite(msg.placeholder.height)) {51return 'generate: placeholder width and height must be numbers';52}53if (!canCreateInsert({54prompt: msg.freeformPrompt,55comments: msg.comments,56strokes: msg.strokes,57})) {58return 'generate: insert requires freeformPrompt or annotations';59}60return validateAnnotationFields(msg);61}6263function validateReplaceGenerate(msg) {64if (!msg.action || !VISUAL_ACTIONS.includes(msg.action)) return 'generate: invalid action';65if (!msg.element || !msg.element.outerHTML) return 'generate: missing element context';66return validateAnnotationFields(msg);67}6869function validateManualEditEvent(msg, label) {70if (!isValidId(msg.id)) return label + ': missing or malformed id';71if (!msg.pageUrl || typeof msg.pageUrl !== 'string') return label + ': missing pageUrl';72if (!msg.element || typeof msg.element !== 'object') return label + ': missing element';73if (!Array.isArray(msg.ops) || msg.ops.length === 0) return label + ': ops must be non-empty array';74if (msg.ops.length > 100) return label + ': too many ops (max 100)';75for (const op of msg.ops) {76if (typeof op.ref !== 'string') return label + ': op.ref required';77if (typeof op.tag !== 'string') return label + ': op.tag required';78if (typeof op.originalText !== 'string') return label + ': op.originalText required';79if (op.deleted !== true && typeof op.newText !== 'string') {80return label + ': text op requires newText';81}82if (typeof op.newText === 'string') {83if (op.deleted !== true && op.newText.trim().length === 0) {84return label + ': newText cannot be empty';85}86const forbidden = validateManualEditText(op.newText);87if (forbidden) {88return label + ': newText cannot contain ' + forbidden.join(' ') + ' (plain text only; ask the AI to insert markup)';89}90}91}92return null;93}9495export function validateEvent(msg) {96if (!msg || typeof msg !== 'object' || !msg.type) return 'Missing or invalid message';97switch (msg.type) {98case 'generate':99if (!isValidId(msg.id)) return 'generate: missing or malformed id';100if (!Number.isInteger(msg.count) || msg.count < 1 || msg.count > 8) return 'generate: count must be 1-8';101if (msg.mode === 'insert') return validateInsertGenerate(msg);102return validateReplaceGenerate(msg);103case 'accept':104if (!isValidId(msg.id)) return 'accept: missing or malformed id';105if (!isValidVariantId(msg.variantId)) return 'accept: missing or malformed variantId';106if (msg.paramValues !== undefined) {107if (typeof msg.paramValues !== 'object' || msg.paramValues === null || Array.isArray(msg.paramValues)) {108return 'accept: paramValues must be an object';109}110}111return null;112case 'discard':113return isValidId(msg.id) ? null : 'discard: missing or malformed id';114case 'checkpoint':115if (!isValidId(msg.id)) return 'checkpoint: missing or malformed id';116if (!Number.isInteger(msg.revision) || msg.revision < 0) return 'checkpoint: revision must be a non-negative integer';117if (msg.paramValues !== undefined && (typeof msg.paramValues !== 'object' || msg.paramValues === null || Array.isArray(msg.paramValues))) {118return 'checkpoint: paramValues must be an object';119}120return null;121case 'exit':122return null;123case 'prefetch':124if (!msg.pageUrl || typeof msg.pageUrl !== 'string') return 'prefetch: missing pageUrl';125return null;126case 'manual_edits':127return validateManualEditEvent(msg, 'manual_edits');128case 'steer':129if (!isValidId(msg.id)) return 'steer: missing or malformed id';130if (typeof msg.message !== 'string' || !msg.message.trim()) return 'steer: message required';131if (msg.message.length > 4000) return 'steer: message too long';132if (msg.pageUrl !== undefined && typeof msg.pageUrl !== 'string') return 'steer: pageUrl must be string';133return null;134default:135return 'Unknown event type: ' + msg.type;136}137}138