TypeScript 55.4%
Python 43.2%
SQL 1.2%
1/**2 * Advanced search syntax (spec §30). Browser-safe.3 *4 * openai pricing entity:org_openai type:pricing_change,terms_change after:2026-09-01 before:2026-09-105 * silent:true first_party:true confirmed:true importance:>70 confidence:>=50 signal:>806 * source:openai category:ai country:CA lang:fr class:pricing group:security cluster:clu_…7 *8 * Quoted values are supported: source:"bank of canada". Unknown keys stay in the free-text part.9 */10export interface ParsedSearch {11 text: string;12 filters: {13 entity?: string;14 source?: string;15 domain?: string;16 event_type?: string;17 group?: string;18 category?: string;19 country?: string;20 language?: string;21 after?: string;22 before?: string;23 silent_change?: boolean;24 first_party?: boolean;25 confirmed?: boolean;26 importance_min?: number;27 confidence_min?: number;28 signal_min?: number;29 change_class?: string;30 cluster?: string;31 sensor?: string;32 url?: string;33 };34 tokens: { key: string; value: string }[];35}3637const KEYS = new Set(["entity", "source", "domain", "type", "event_type", "group", "category", "cat", "country", "lang", "language", "after", "since", "before", "until", "silent", "first_party", "firstparty", "confirmed", "importance", "imp", "confidence", "conf", "signal", "class", "cluster", "sensor", "url", "is"]);3839function bool(v: string): boolean | undefined {40 const s = v.toLowerCase();41 if (["true", "1", "yes", "y"].includes(s)) return true;42 if (["false", "0", "no", "n"].includes(s)) return false;43 return undefined;44}4546function num(v: string): number | undefined {47 const m = v.match(/^(?:>=|>|=)?\s*(\d+(?:\.\d+)?)$/);48 return m ? Number(m[1]) : undefined;49}5051/** Relative dates: 1h, 6h, 24h, 7d, 30d → ISO timestamp. */52function date(v: string): string | undefined {53 const rel = v.match(/^(\d+)\s*([hdwm])$/i);54 if (rel) {55 const n = Number(rel[1]);56 const unit = rel[2]!.toLowerCase();57 const ms = unit === "h" ? 3600e3 : unit === "d" ? 86400e3 : unit === "w" ? 7 * 86400e3 : 30 * 86400e3;58 return new Date(Date.now() - n * ms).toISOString();59 }60 const d = new Date(v.length === 10 ? `${v}T00:00:00Z` : v);61 return Number.isNaN(d.getTime()) ? undefined : d.toISOString();62}6364export function parseSearch(input: string): ParsedSearch {65 const tokens: { key: string; value: string }[] = [];66 const free: string[] = [];67 const re = /(\w+):("([^"]*)"|'([^']*)'|(\S+))|("([^"]*)")|(\S+)/g;68 let m: RegExpExecArray | null;69 while ((m = re.exec(input)) !== null) {70 if (m[1] && KEYS.has(m[1].toLowerCase())) tokens.push({ key: m[1].toLowerCase(), value: (m[3] ?? m[4] ?? m[5] ?? "").trim() });71 else free.push(m[7] ?? m[8] ?? m[0]);72 }73 const f: ParsedSearch["filters"] = {};74 for (const { key, value } of tokens) {75 switch (key) {76 case "entity":77 f.entity = /^(org|prd|ent)_/.test(value) ? value : `org_${value.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;78 break;79 case "source":80 f.source = value.toLowerCase().replace(/[^a-z0-9.-]+/g, "-");81 break;82 case "domain":83 f.domain = value.toLowerCase();84 break;85 case "type":86 case "event_type":87 f.event_type = value;88 break;89 case "group":90 f.group = value.toLowerCase();91 break;92 case "category":93 case "cat":94 f.category = value.toLowerCase();95 break;96 case "country":97 f.country = value.toUpperCase();98 break;99 case "lang":100 case "language":101 f.language = value.toLowerCase();102 break;103 case "after":104 case "since":105 f.after = date(value);106 break;107 case "before":108 case "until":109 f.before = date(value);110 break;111 case "silent":112 f.silent_change = bool(value);113 break;114 case "first_party":115 case "firstparty":116 f.first_party = bool(value);117 break;118 case "confirmed":119 f.confirmed = bool(value);120 break;121 case "importance":122 case "imp":123 f.importance_min = num(value);124 break;125 case "confidence":126 case "conf":127 f.confidence_min = num(value);128 break;129 case "signal":130 f.signal_min = num(value);131 break;132 case "class":133 f.change_class = value.toLowerCase();134 break;135 case "cluster":136 f.cluster = value;137 break;138 case "sensor":139 f.sensor = value;140 break;141 case "url":142 f.url = value;143 break;144 case "is":145 if (value === "silent") f.silent_change = true;146 else if (value === "breaking") f.importance_min = Math.max(f.importance_min ?? 0, 80);147 else if (value === "confirmed") f.confirmed = true;148 else if (value === "first-party" || value === "firstparty") f.first_party = true;149 break;150 }151 }152 return { text: free.join(" ").trim(), filters: f, tokens };153}154155/** Inverse: build a query string from filters (for saved views / URL sync). */156export function stringifySearch(p: { text?: string; filters: ParsedSearch["filters"] }): string {157 const parts: string[] = [];158 if (p.text) parts.push(p.text);159 const f = p.filters;160 const q = (v: string): string => (/\s/.test(v) ? `"${v}"` : v);161 if (f.entity) parts.push(`entity:${q(f.entity)}`);162 if (f.source) parts.push(`source:${q(f.source)}`);163 if (f.domain) parts.push(`domain:${f.domain}`);164 if (f.event_type) parts.push(`type:${f.event_type}`);165 if (f.group) parts.push(`group:${f.group}`);166 if (f.category) parts.push(`category:${f.category}`);167 if (f.country) parts.push(`country:${f.country}`);168 if (f.language) parts.push(`lang:${f.language}`);169 if (f.after) parts.push(`after:${f.after.slice(0, 10)}`);170 if (f.before) parts.push(`before:${f.before.slice(0, 10)}`);171 if (f.silent_change !== undefined) parts.push(`silent:${f.silent_change}`);172 if (f.first_party !== undefined) parts.push(`first_party:${f.first_party}`);173 if (f.confirmed !== undefined) parts.push(`confirmed:${f.confirmed}`);174 if (f.importance_min !== undefined) parts.push(`importance:>${f.importance_min}`);175 if (f.confidence_min !== undefined) parts.push(`confidence:>${f.confidence_min}`);176 if (f.signal_min !== undefined) parts.push(`signal:>${f.signal_min}`);177 if (f.change_class) parts.push(`class:${f.change_class}`);178 if (f.cluster) parts.push(`cluster:${f.cluster}`);179 return parts.join(" ");180}181