UNPKG

@fusedashlabs/ui9000-workspace

Version:

UI9000 workspace MCP. One tool, show_workspace, draws a chart from your table. Set TYPESAFE_API_KEY to let Jev choose the chart.

246 lines (245 loc) • 7.36 kB
import { DATA_PROFILE_KEYS } from '../spec/data-profile.js'; const PROFILE_KEY_SET = new Set(DATA_PROFILE_KEYS); const CMP_OPS = ['===', '!==', '<=', '>=', '<', '>']; function isDataProfileKey(key) { return PROFILE_KEY_SET.has(key); } function tokenize(when) { const tokens = []; let i = 0; const src = when; const skipWs = () => { while (i < src.length && /\s/.test(src[i])) i += 1; }; while (i < src.length) { skipWs(); if (i >= src.length) break; if (src.startsWith('profile.', i)) { i += 'profile.'.length; const start = i; if (!/[A-Za-z]/.test(src[i] ?? '')) { throw new Error(`when: expected DataProfile key after profile. at ${start}`); } i += 1; while (i < src.length && /[A-Za-z0-9]/.test(src[i])) i += 1; const key = src.slice(start, i); if (!isDataProfileKey(key)) { throw new Error(`when references unknown DataProfile key "${key}"`); } skipWs(); if (src[i] === '(') { throw new Error('when does not allow calls'); } tokens.push({ kind: 'profile', key }); continue; } if (src.startsWith('&&', i)) { tokens.push({ kind: 'op', op: '&&' }); i += 2; continue; } if (src.startsWith('||', i)) { tokens.push({ kind: 'op', op: '||' }); i += 2; continue; } if (src.startsWith('===', i)) { tokens.push({ kind: 'op', op: '===' }); i += 3; continue; } if (src.startsWith('!==', i)) { tokens.push({ kind: 'op', op: '!==' }); i += 3; continue; } if (src.startsWith('<=', i)) { tokens.push({ kind: 'op', op: '<=' }); i += 2; continue; } if (src.startsWith('>=', i)) { tokens.push({ kind: 'op', op: '>=' }); i += 2; continue; } if (src[i] === '<' || src[i] === '>') { tokens.push({ kind: 'op', op: src[i] }); i += 1; continue; } if (src[i] === '!') { tokens.push({ kind: 'op', op: '!' }); i += 1; continue; } if (src[i] === '(') { tokens.push({ kind: 'lparen' }); i += 1; continue; } if (src[i] === ')') { tokens.push({ kind: 'rparen' }); i += 1; continue; } if (src[i] === '-' || /[0-9]/.test(src[i])) { const start = i; if (src[i] === '-') i += 1; if (!/[0-9]/.test(src[i] ?? '')) { throw new Error(`when uses disallowed characters near "${src.slice(start)}"`); } while (i < src.length && /[0-9]/.test(src[i])) i += 1; if (src[i] === '.') { i += 1; if (!/[0-9]/.test(src[i] ?? '')) { throw new Error('when: number has a trailing dot'); } while (i < src.length && /[0-9]/.test(src[i])) i += 1; } tokens.push({ kind: 'number', value: Number(src.slice(start, i)) }); continue; } throw new Error(`when uses disallowed characters: ${src.slice(i)}`); } return tokens; } class Parser { tokens; profile; pos = 0; constructor(tokens, profile) { this.tokens = tokens; this.profile = profile; } peek() { return this.tokens[this.pos]; } peekOp(op) { const token = this.peek(); return token?.kind === 'op' && token.op === op; } take() { const token = this.tokens[this.pos]; if (!token) throw new Error('when: unexpected end of expression'); this.pos += 1; return token; } expectEnd() { if (this.pos !== this.tokens.length) { throw new Error('when: unexpected trailing tokens'); } } parseExpr() { return this.parseOr(); } parseOr() { let left = this.parseAnd(); while (this.peekOp('||')) { this.take(); const right = this.parseAnd(); left = left || right; } return left; } parseAnd() { let left = this.parseComparison(); while (this.peekOp('&&')) { this.take(); const right = this.parseComparison(); left = left && right; } return left; } parseComparison() { const left = this.parseUnary(); const next = this.peek(); if (next?.kind === 'op' && CMP_OPS.includes(next.op)) { const op = next.op; this.take(); const right = this.parseUnary(); return compare(op, left, right); } return left; } parseUnary() { const next = this.peek(); if (next?.kind === 'op' && next.op === '!') { this.take(); return !this.parseUnary(); } return this.parsePrimary(); } parsePrimary() { const token = this.take(); if (token.kind === 'profile') { return this.profile[token.key]; } if (token.kind === 'number') { return token.value; } if (token.kind === 'lparen') { const inner = this.parseExpr(); const close = this.take(); if (close.kind !== 'rparen') { throw new Error('when: expected closing parenthesis'); } return inner; } throw new Error('when: expected profile key, number, or grouped expression'); } } function compare(op, left, right) { switch (op) { case '===': return left === right; case '!==': return left !== right; case '<=': return left <= right; case '>=': return left >= right; case '<': return left < right; case '>': return left > right; } } /** Pull `profile.<key>` identifiers out of a `when` string. */ export function profileKeysInWhen(when) { return tokenize(when) .filter((token) => token.kind === 'profile') .map((token) => token.key); } export function assertWhenUsesClosedProfile(when) { if (!when.trim()) { throw new Error('empty when expression'); } tokenize(when); } export function assertProfileUsesClosedKeys(profile) { for (const key of Object.keys(profile)) { if (!PROFILE_KEY_SET.has(key)) { throw new Error(`evalCases.profile has unknown key "${key}"`); } } } /** * Evaluate a catalog `when` string against a profile. * Only `profile.<DataProfileKey>`, numbers, && || ! comparisons, and grouping. */ export function evalWhen(when, profile) { assertWhenUsesClosedProfile(when); const parser = new Parser(tokenize(when), profile); const value = parser.parseExpr(); parser.expectEnd(); return Boolean(value); } export { isDataProfileKey };