@supericons/mcp 0.4.7 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -46,17 +46,109 @@ const GENERIC_SLOT_WORDS = new Set([
46
46
  'view',
47
47
  ]);
48
48
 
49
- const VARIANT_PENALTIES = Object.freeze([
50
- { pattern: /circle/i, penalty: 5 },
51
- { pattern: /square/i, penalty: 4 },
52
- { pattern: /dash/i, penalty: 5 },
53
- { pattern: /badge/i, penalty: 4 },
54
- { pattern: /off/i, penalty: 6 },
55
- { pattern: /slash/i, penalty: 6 },
56
- { pattern: /warning/i, penalty: 4 },
57
- ]);
49
+ const VARIANT_PENALTIES = Object.freeze([
50
+ { token: 'circle', pattern: /circle/i, penalty: 12 },
51
+ { token: 'square', pattern: /square/i, penalty: 12 },
52
+ { token: 'dash', pattern: /dash/i, penalty: 5 },
53
+ { token: 'badge', pattern: /badge/i, penalty: 4 },
54
+ { token: 'brand', pattern: /\bbrand\b/i, penalty: 30 },
55
+ { token: 'off', pattern: /\boff\b/i, penalty: 18 },
56
+ { token: 'slash', pattern: /slash/i, penalty: 8 },
57
+ { token: 'warning', pattern: /warning/i, penalty: 5 },
58
+ { token: 'ai', pattern: /\bai\b/i, penalty: 18 },
59
+ { token: 'add', pattern: /\badd\b/i, penalty: 12 },
60
+ { token: 'plus', pattern: /\bplus\b/i, penalty: 18 },
61
+ { token: 'edit', pattern: /\bedit\b/i, penalty: 12 },
62
+ { token: 'remove', pattern: /\bremove\b/i, penalty: 12 },
63
+ { token: 'delete', pattern: /\bdelete\b/i, penalty: 12 },
64
+ { token: 'minus', pattern: /\bminus\b/i, penalty: 24 },
65
+ { token: 'cancel', pattern: /\bcancel\b/i, penalty: 30 },
66
+ { token: 'x', pattern: /\bx\b/i, penalty: 30 },
67
+ { token: 'exclamation', pattern: /\bexclamation\b/i, penalty: 24 },
68
+ { token: 'discount', pattern: /\bdiscount\b/i, penalty: 24 },
69
+ { token: 'heart', pattern: /\bheart\b/i, penalty: 18 },
70
+ { token: 'zap', pattern: /\bzap\b/i, penalty: 30 },
71
+ { token: 'bolt', pattern: /\bbolt\b/i, penalty: 18 },
72
+ { token: 'wifi', pattern: /\bwifi\b/i, penalty: 12 },
73
+ { token: 'align', pattern: /\balign\b/i, penalty: 12 },
74
+ { token: 'fruit', pattern: /\bfruit\b/i, penalty: 12 },
75
+ { token: 'open', pattern: /\bopen\b/i, penalty: 28 },
76
+ { token: 'unlock', pattern: /\bunlock(?:ed)?\b/i, penalty: 28 },
77
+ { token: 'ban', pattern: /\bban\b/i, penalty: 24 },
78
+ { token: 'blocked', pattern: /\bblocked\b/i, penalty: 24 },
79
+ { token: 'rupee', pattern: /\brupee\b/i, penalty: 18 },
80
+ { token: 'ruble', pattern: /\bruble\b/i, penalty: 18 },
81
+ { token: 'franc', pattern: /\bfranc\b/i, penalty: 18 },
82
+ { token: 'lira', pattern: /\blira\b/i, penalty: 18 },
83
+ { token: 'bitcoin', pattern: /\bbitcoin\b/i, penalty: 18 },
84
+ { token: 'dollar', pattern: /\bdollar\b/i, penalty: 18 },
85
+ { token: 'cent', pattern: /\bcent\b/i, penalty: 18 },
86
+ { token: 'yen', pattern: /\byen\b/i, penalty: 18 },
87
+ { token: 'yuan', pattern: /\byuan\b/i, penalty: 18 },
88
+ { token: 'euro', pattern: /\beuro\b/i, penalty: 18 },
89
+ { token: 'pound', pattern: /\bpound\b/i, penalty: 18 },
90
+ { token: 'down', pattern: /\bdown\b/i, penalty: 8 },
91
+ { token: 'left', pattern: /\bleft\b/i, penalty: 8 },
92
+ { token: 'up', pattern: /\bup\b/i, penalty: 8 },
93
+ { token: 'corner', pattern: /\bcorner\b/i, penalty: 12 },
94
+ { token: 'break', pattern: /\bbreak\b/i, penalty: 18 },
95
+ { token: 'broken', pattern: /\bbroken\b/i, penalty: 18 },
96
+ { token: 'locked', pattern: /\blocked\b/i, penalty: 18 },
97
+ { token: 'orange', pattern: /\borange\b/i, penalty: 12 },
98
+ ]);
99
+
100
+ const VARIANT_TOKENS = new Set(VARIANT_PENALTIES.map((rule) => rule.token));
101
+
102
+ const REQUESTED_VARIANT_ALIASES = Object.freeze({
103
+ off: ['disabled', 'disable', 'muted', 'mute', 'off', 'broken'],
104
+ brand: ['brand', 'logo'],
105
+ open: ['open', 'unlock', 'unlocked'],
106
+ unlock: ['open', 'unlock', 'unlocked'],
107
+ ban: ['ban', 'banned', 'block', 'blocked'],
108
+ blocked: ['ban', 'banned', 'block', 'blocked'],
109
+ add: ['add', 'create', 'plus'],
110
+ plus: ['add', 'create', 'plus'],
111
+ edit: ['edit', 'editing', 'modify', 'pencil'],
112
+ remove: ['remove', 'removed', 'delete', 'minus'],
113
+ delete: ['delete', 'remove', 'trash'],
114
+ minus: ['minus', 'remove', 'removed', 'delete'],
115
+ cancel: ['cancel', 'canceled', 'cancelled', 'disabled', 'remove'],
116
+ x: ['x', 'close', 'remove', 'delete', 'blocked', 'broken', 'off'],
117
+ exclamation: ['alert', 'warning', 'exclamation'],
118
+ discount: ['discount', 'coupon', 'coupons', 'promo', 'promotion', 'deal'],
119
+ heart: ['heart', 'favorite', 'favourite', 'liked', 'wishlist'],
120
+ ai: ['ai', 'smart', 'assistant', 'automation'],
121
+ break: ['break', 'broken'],
122
+ broken: ['break', 'broken'],
123
+ locked: ['lock', 'locked', 'secure', 'security'],
124
+ ruble: ['ruble', 'rouble', 'rub'],
125
+ franc: ['franc', 'chf'],
126
+ lira: ['lira'],
127
+ bitcoin: ['bitcoin', 'btc'],
128
+ dollar: ['dollar', 'usd'],
129
+ yuan: ['yuan', 'cny'],
130
+ });
131
+
132
+ const DIRECT_LOCALIZED_INTENT_RULES = Object.freeze([
133
+ {
134
+ pattern: /通知|お知らせ|알림|notificaciones?|benachrichtigungen?|notifica(?:ç|c)[aã]o|notificações?/iu,
135
+ terms: ['notification', 'notifications'],
136
+ },
137
+ {
138
+ pattern: /关闭|關閉|オフ|꺼짐|끄기|desactivad[ao]s?|apagad[ao]s?|aus\b|deaktiviert|disabled|muted|mute|off/iu,
139
+ terms: ['off', 'disabled'],
140
+ },
141
+ ]);
58
142
 
59
143
  const COMMON_SLOT_PREFERENCE_RULES = Object.freeze([
144
+ {
145
+ slotPatterns: [/\busers\b/i, /team/i, /members?/i],
146
+ queryVariants: ['users', 'team', 'user group', 'people'],
147
+ iconPreferences: [
148
+ { pattern: /^users(?:_|-|$)|(?:_|-)users(?:_|-|$)|users-group|user-group|user_circle|user-circle/i, bonus: 66 },
149
+ { pattern: /^user(?:_|-|$)|(?:_|-)user(?:_|-|$)/i, bonus: 18 },
150
+ ],
151
+ },
60
152
  {
61
153
  slotPatterns: [
62
154
  /profile/i,
@@ -120,6 +212,15 @@ const COMMON_SLOT_PREFERENCE_RULES = Object.freeze([
120
212
  { pattern: /alarm|alert/i, bonus: 18 },
121
213
  ],
122
214
  },
215
+ {
216
+ priority: 90,
217
+ slotPatterns: [/notifications?\s+off/i, /disabled notifications?/i, /muted notifications?/i, /notification\s+off/i],
218
+ queryVariants: ['bell slash', 'bell off', 'notification off', 'muted bell'],
219
+ iconPreferences: [
220
+ { pattern: /^bell[_-]?(off|slash)$|^bell[_-]?simple[_-]?slash$|notification[_-]?off|notifications?[_-]?off/i, bonus: 180 },
221
+ { pattern: /bell|notification/i, bonus: 22 },
222
+ ],
223
+ },
123
224
  {
124
225
  slotPatterns: [
125
226
  /privacy/i,
@@ -140,8 +241,30 @@ const COMMON_SLOT_PREFERENCE_RULES = Object.freeze([
140
241
  ],
141
242
  queryVariants: ['shield lock', 'lock', 'shield', 'privacy security', 'security'],
142
243
  iconPreferences: [
143
- { pattern: /shield.*lock|lock.*shield|shield-check|shield-alert|shield/i, bonus: 58 },
144
- { pattern: /^lock$|(?:_|-)lock(?:_|-|$)|key|fingerprint/i, bonus: 28 },
244
+ { pattern: /^shield$|^shield[_-]?check$|shield-check|shield_check/i, bonus: 82 },
245
+ { pattern: /shield.*lock|lock.*shield|shield/i, bonus: 58 },
246
+ { pattern: /^lock$|^lock[_-]?keyhole$|(?:_|-)lock(?:_|-|$)|key|fingerprint/i, bonus: 34 },
247
+ { pattern: /open|unlock|ban|minus|off|slash/i, bonus: -54 },
248
+ ],
249
+ },
250
+ {
251
+ priority: 120,
252
+ slotPatterns: [/unlock/i, /open account/i, /unlocked account/i],
253
+ queryVariants: ['lock open', 'unlock', 'lock keyhole open'],
254
+ iconPreferences: [
255
+ { pattern: /^lock[_-]?open$|^lock[_-]?keyhole[_-]?open$|^unlock(?:[_-]?keyhole)?$/i, bonus: 160 },
256
+ { pattern: /lock.*open|open.*lock|unlock/i, bonus: 80 },
257
+ { pattern: /^user(?:_|-|$)|(?:_|-)user(?:_|-|$)|file-user/i, bonus: -70 },
258
+ ],
259
+ },
260
+ {
261
+ priority: 120,
262
+ slotPatterns: [/blocked user/i, /banned user/i, /user blocked/i, /user banned/i],
263
+ queryVariants: ['user x', 'user minus', 'ban user', 'blocked user'],
264
+ iconPreferences: [
265
+ { pattern: /^user[_-]?x$|^user[_-]?minus$|user-round-x|user-round-minus|shield-ban|ban/i, bonus: 150 },
266
+ { pattern: /^user(?:_|-|$)|(?:_|-)user(?:_|-|$)/i, bonus: 24 },
267
+ { pattern: /^file-user$|^user-2$/i, bonus: -80 },
145
268
  ],
146
269
  },
147
270
  {
@@ -252,14 +375,33 @@ const COMMON_SLOT_PREFERENCE_RULES = Object.freeze([
252
375
  { pattern: /person|contact/i, bonus: 10 },
253
376
  ],
254
377
  },
255
- {
256
- slotPatterns: [/model/i, /\bai\b/i, /\bml\b/i, /machine learning/i],
257
- queryVariants: ['brain circuit', 'brain cog', 'neural network', 'model'],
378
+ {
379
+ slotPatterns: [/model/i, /\bai\b/i, /\bml\b/i, /machine learning/i],
380
+ queryVariants: ['brain circuit', 'brain cog', 'neural network', 'model'],
258
381
  iconPreferences: [
259
382
  { pattern: /brain-circuit|brain_circuit/i, bonus: 44 },
260
- { pattern: /brain|circuit|nodes/i, bonus: 24 },
261
- ],
262
- },
383
+ { pattern: /brain|circuit|nodes/i, bonus: 24 },
384
+ ],
385
+ },
386
+ {
387
+ priority: 120,
388
+ slotPatterns: [/\bai search\b/i, /smart search/i, /semantic search/i, /assistant search/i],
389
+ queryVariants: ['search ai', 'ai search', 'smart search'],
390
+ iconPreferences: [
391
+ { pattern: /^search.*ai|ai.*search|search[_-]?[23]?[_-]?ai/i, bonus: 150 },
392
+ { pattern: /^search(?:_|-|$)|(?:_|-)search(?:_|-|$)/i, bonus: 34 },
393
+ { pattern: /brain|robot|spark/i, bonus: 24 },
394
+ ],
395
+ },
396
+ {
397
+ priority: 90,
398
+ slotPatterns: [/automation/i, /workflow/i, /automate/i, /smart action/i],
399
+ queryVariants: ['automation', 'workflow', 'robot', 'refresh', 'sparkles'],
400
+ iconPreferences: [
401
+ { pattern: /workflow|automation|robot|sparkles?|refresh|settings|adjustments/i, bonus: 90 },
402
+ { pattern: /hand|finger|train/i, bonus: -90 },
403
+ ],
404
+ },
263
405
  {
264
406
  slotPatterns: [/prompt/i],
265
407
  queryVariants: ['message text', 'text input', 'terminal', 'text cursor'],
@@ -300,14 +442,87 @@ const COMMON_SLOT_PREFERENCE_RULES = Object.freeze([
300
442
  { pattern: /chart|signal|radar/i, bonus: 16 },
301
443
  ],
302
444
  },
303
- {
304
- slotPatterns: [/billing/i, /payment/i, /invoice/i, /subscription/i],
305
- queryVariants: ['credit card', 'receipt', 'invoice', 'payment', 'wallet'],
306
- iconPreferences: [
307
- { pattern: /credit-card|receipt|wallet|invoice/i, bonus: 44 },
308
- { pattern: /card|banknote|currency|dollar/i, bonus: 18 },
309
- ],
310
- },
445
+ {
446
+ slotPatterns: [/billing/i, /payment/i, /invoice/i, /subscription/i],
447
+ queryVariants: ['credit card', 'receipt', 'invoice', 'payment', 'wallet'],
448
+ iconPreferences: [
449
+ { pattern: /credit-card|receipt|wallet|invoice/i, bonus: 44 },
450
+ { pattern: /card|banknote|currency|dollar/i, bonus: 18 },
451
+ { pattern: /ruble|franc|lira|bitcoin|yuan/i, bonus: -70 },
452
+ ],
453
+ },
454
+ {
455
+ priority: 90,
456
+ slotPatterns: [/storefront/i, /\bstore\b/i, /\bshop\b/i],
457
+ queryVariants: ['store', 'shop', 'building store', 'shopping bag'],
458
+ iconPreferences: [
459
+ { pattern: /^store$|^storefront$|building-store|shop[_-]?line|store[_-]?\d?[_-]?line|shopping-bag/i, bonus: 120 },
460
+ { pattern: /brand-appstore|restore|cancel|\bx\b|off|discount|heart|exclamation|minus|plus|search|share|star|question|bolt|code|copy|dollar|down|up|pin|pause/i, bonus: -120 },
461
+ ],
462
+ },
463
+ {
464
+ priority: 130,
465
+ slotPatterns: [/(store|shop|storefront)\s+(off|disabled|closed|cancelled|canceled)/i, /(off|disabled|closed|cancelled|canceled)\s+(store|shop|storefront)/i],
466
+ queryVariants: ['store off', 'shopping bag x', 'shopping cart off', 'store disabled'],
467
+ iconPreferences: [
468
+ { pattern: /(store|shop|shopping|bag|cart).*(off|\bx\b|cancel|disabled)|(off|\bx\b|cancel|disabled).*(store|shop|shopping|bag|cart)/i, bonus: 220 },
469
+ { pattern: /^building-store$|^store$|^shopping-bag$/i, bonus: -70 },
470
+ ],
471
+ },
472
+ {
473
+ priority: 90,
474
+ slotPatterns: [/checkout/i],
475
+ queryVariants: ['shopping cart', 'credit card', 'payment', 'receipt checkout'],
476
+ iconPreferences: [
477
+ { pattern: /^shopping[_-]?cart$|shopping-cart$|credit-card|card-pay|payment|receipt/i, bonus: 140 },
478
+ { pattern: /fork|knife|git|branch|merge|forklift|grill|cancel|\bx\b|off|discount|heart|exclamation|minus|plus|search|share|star|question|bolt|code|copy|dollar|down|up|pin|pause/i, bonus: -140 },
479
+ ],
480
+ },
481
+ {
482
+ priority: 90,
483
+ slotPatterns: [/customers?/i, /shoppers?/i, /buyers?/i],
484
+ queryVariants: ['users', 'customers', 'user group'],
485
+ iconPreferences: [
486
+ { pattern: /^users(?:_|-|$)|(?:_|-)users(?:_|-|$)|users-group|user-group|user-circle|user_circle|^user(?:_|-|$)/i, bonus: 110 },
487
+ { pattern: /ticket|plane|caret|cancel|\bx\b|off|discount|heart|exclamation|minus|plus|search|share|star|question|bolt|code|copy|dollar|down|up|pin|pause/i, bonus: -120 },
488
+ ],
489
+ },
490
+ {
491
+ priority: 90,
492
+ slotPatterns: [/coupons?/i, /discounts?/i, /promo/i, /promotion/i],
493
+ queryVariants: ['coupon', 'tag percent', 'discount', 'percentage'],
494
+ iconPreferences: [
495
+ { pattern: /coupon|ticket-percent|badge-percent|percent|percentage|tag|shopping-cart-discount|shopping-bag-discount|seal-percent/i, bonus: 120 },
496
+ { pattern: /^percentage-\d+$|bean|candy|cannabis|off|slash|disabled|alert/i, bonus: -180 },
497
+ ],
498
+ },
499
+ {
500
+ priority: 130,
501
+ slotPatterns: [/(cancel|cancelled|canceled|remove|delete)\s+orders?/i, /orders?\s+(cancel|cancelled|canceled|remove|delete)/i],
502
+ queryVariants: ['shopping cart cancel', 'basket cancel', 'cancel order', 'order x'],
503
+ iconPreferences: [
504
+ { pattern: /cancel|\bx\b|remove|delete|minus|trash/i, bonus: 220 },
505
+ { pattern: /shopping|cart|basket|package|receipt|clipboard|list/i, bonus: 20 },
506
+ ],
507
+ },
508
+ {
509
+ priority: 90,
510
+ slotPatterns: [/orders?/i, /purchases?/i],
511
+ queryVariants: ['package', 'receipt', 'clipboard list', 'shopping bag', 'ordered list'],
512
+ iconPreferences: [
513
+ { pattern: /^package$|packages|receipt|shopping-bag$|clipboard|list-ordered|file-invoice|file-text/i, bonus: 115 },
514
+ { pattern: /border|sort|align|cancel|\bx\b|off|discount|heart|exclamation|minus|plus|search|share|star|question|bolt|code|copy|dollar|down|up|pin|pause/i, bonus: -130 },
515
+ ],
516
+ },
517
+ {
518
+ priority: 90,
519
+ slotPatterns: [/products?/i, /catalog/i, /inventory/i],
520
+ queryVariants: ['package', 'box', 'tag', 'shopping bag', 'products'],
521
+ iconPreferences: [
522
+ { pattern: /^package$|packages|package[_-]?\d?|^box$|boxes|tag$|shopping-bag$|warehouse|building-warehouse/i, bonus: 115 },
523
+ { pattern: /brand-producthunt|brand-stocktwits|border|sort|cancel|\bx\b|off|discount|heart|exclamation|minus|plus|search|share|star|question|bolt|code|copy|dollar|down|up|pin|pause/i, bonus: -130 },
524
+ ],
525
+ },
311
526
  {
312
527
  slotPatterns: [/reports?/i, /analytics/i, /insights?/i],
313
528
  queryVariants: ['bar chart', 'file chart', 'analytics chart', 'report document'],
@@ -316,33 +531,301 @@ const COMMON_SLOT_PREFERENCE_RULES = Object.freeze([
316
531
  { pattern: /chart|report|analytics/i, bonus: 16 },
317
532
  ],
318
533
  },
319
- {
320
- slotPatterns: [/settings?/i, /preferences?/i, /configure/i],
321
- queryVariants: ['settings', 'cog', 'sliders'],
534
+ {
535
+ slotPatterns: [/settings?/i, /preferences?/i, /configure/i],
536
+ queryVariants: ['settings', 'cog', 'sliders'],
322
537
  iconPreferences: [
323
538
  { pattern: /^settings$|^cog$|settings-2|sliders/i, bonus: 34 },
324
- { pattern: /settings|cog|adjustments/i, bonus: 16 },
325
- ],
326
- },
327
- {
328
- slotPatterns: [/database/i, /storage/i],
329
- queryVariants: ['database', 'server database', 'data storage'],
330
- iconPreferences: [
331
- { pattern: /^database$|database-stack/i, bonus: 36 },
332
- { pattern: /database|server/i, bonus: 16 },
333
- ],
334
- },
335
- ]);
539
+ { pattern: /settings|cog|adjustments/i, bonus: 16 },
540
+ ],
541
+ },
542
+ {
543
+ priority: 90,
544
+ slotPatterns: [/permissions?/i, /access control/i, /roles?/i],
545
+ queryVariants: ['user key', 'shield lock', 'key', 'settings permissions'],
546
+ iconPreferences: [
547
+ { pattern: /user-key|user-lock|user-check|key|lock|shield|adjustments|settings/i, bonus: 120 },
548
+ { pattern: /free-rights|premium-rights|icons$/i, bonus: -100 },
549
+ ],
550
+ },
551
+ {
552
+ slotPatterns: [/database/i, /storage/i],
553
+ queryVariants: ['database', 'server database', 'data storage'],
554
+ iconPreferences: [
555
+ { pattern: /^database$|database-stack/i, bonus: 36 },
556
+ { pattern: /database|server/i, bonus: 16 },
557
+ ],
558
+ },
559
+ {
560
+ slotPatterns: [/search/i, /find/i, /lookup/i],
561
+ queryVariants: ['search', 'find', 'magnifier', 'magnifying glass'],
562
+ iconPreferences: [
563
+ { pattern: /^search$/i, bonus: 140 },
564
+ { pattern: /^search(?:_|-|$)|(?:_|-)search(?:_|-|$)/i, bonus: 64 },
565
+ { pattern: /magnifier|magnifying/i, bonus: 36 },
566
+ { pattern: /file-search|folder-search|scan-search|mail-search|calendar-search/i, bonus: -90 },
567
+ ],
568
+ },
569
+ {
570
+ priority: 115,
571
+ slotPatterns: [/\b(add|new|create)\s+bookmark\b/i, /\bbookmark\s+(add|new|create)\b/i],
572
+ queryVariants: ['bookmark add', 'add bookmark', 'bookmark plus'],
573
+ iconPreferences: [
574
+ { pattern: /^bookmark[_-]?(add|plus)(?:_|-|$)|(?:_|-)bookmark[_-]?(add|plus)(?:_|-|$)/i, bonus: 220 },
575
+ { pattern: /^bookmarks?(?:_|-|$)|(?:_|-)bookmarks?(?:_|-|$)/i, bonus: 46 },
576
+ { pattern: /^(add|plus)(?:_|-|$)/i, bonus: -80 },
577
+ ],
578
+ },
579
+ {
580
+ priority: 115,
581
+ slotPatterns: [/\bedit\s+bookmark\b/i, /\bbookmark\s+edit\b/i],
582
+ queryVariants: ['bookmark edit', 'edit bookmark', 'bookmark pencil'],
583
+ iconPreferences: [
584
+ { pattern: /^bookmark[_-]?edit(?:_|-|$)|(?:_|-)bookmark[_-]?edit(?:_|-|$)/i, bonus: 190 },
585
+ { pattern: /^bookmarks?(?:_|-|$)|(?:_|-)bookmarks?(?:_|-|$)/i, bonus: 46 },
586
+ { pattern: /^edit(?:_|-|$)|pencil/i, bonus: -70 },
587
+ ],
588
+ },
589
+ {
590
+ priority: 115,
591
+ slotPatterns: [/\b(remove|delete)\s+bookmark\b/i, /\bbookmark\s+(remove|delete)\b/i],
592
+ queryVariants: ['bookmark remove', 'remove bookmark', 'bookmark minus'],
593
+ iconPreferences: [
594
+ { pattern: /^bookmark[_-]?(remove|minus|x)(?:_|-|$)|(?:_|-)bookmark[_-]?(remove|minus|x)(?:_|-|$)/i, bonus: 190 },
595
+ { pattern: /^bookmarks?(?:_|-|$)|(?:_|-)bookmarks?(?:_|-|$)/i, bonus: 46 },
596
+ { pattern: /^(remove|delete|minus)(?:_|-|$)/i, bonus: -70 },
597
+ ],
598
+ },
599
+ {
600
+ slotPatterns: [/bookmark/i, /saved?/i, /save article/i],
601
+ queryVariants: ['bookmark', 'saved', 'save'],
602
+ iconPreferences: [
603
+ { pattern: /^bookmarks?(?:_|-|$)|(?:_|-)bookmarks?(?:_|-|$)/i, bonus: 66 },
604
+ { pattern: /save|favorite|star/i, bonus: 14 },
605
+ ],
606
+ },
607
+ {
608
+ slotPatterns: [/share/i, /send article/i, /forward/i],
609
+ queryVariants: ['share', 'send', 'forward'],
610
+ iconPreferences: [
611
+ { pattern: /^share(?:_|-|$)|(?:_|-)share(?:_|-|$)/i, bonus: 58 },
612
+ { pattern: /send|forward/i, bonus: 18 },
613
+ ],
614
+ },
615
+ {
616
+ priority: 110,
617
+ slotPatterns: [/previous page/i, /previous/i, /\bback\b/i, /go back/i],
618
+ queryVariants: ['arrow left', 'chevron left', 'back arrow', 'previous'],
619
+ iconPreferences: [
620
+ { pattern: /^arrow[_-]?left$|^chevron[_-]?left$|^caret[_-]?left$|arrow[_-]?back$|back[_-]?line|arrow[_-]?to[_-]?left|left(?:_|-|$)/i, bonus: 140 },
621
+ { pattern: /skip-back|step-back/i, bonus: 48 },
622
+ { pattern: /send-to-back|file|archive|audio|floppy|cash|banknote|brand|copy/i, bonus: -140 },
623
+ ],
624
+ },
625
+ {
626
+ priority: 90,
627
+ slotPatterns: [/read more/i, /more link/i, /continue/i, /open article/i, /next page/i, /^next$/i],
628
+ queryVariants: ['arrow right', 'move right', 'chevron right', 'read more', 'next'],
629
+ iconPreferences: [
630
+ { pattern: /^arrow[_-]?right$|^move[_-]?right$|arrow[_-]?to[_-]?right|chevron[_-]?right|right(?:_|-|$)/i, bonus: 90 },
631
+ { pattern: /square|circle|corner|up|down|left|banknote|archive/i, bonus: -70 },
632
+ ],
633
+ },
634
+ {
635
+ slotPatterns: [/categor(?:y|ies)/i, /chips?/i, /filter/i, /topics?/i, /tags?/i],
636
+ queryVariants: ['filter', 'category', 'tag', 'grid'],
637
+ iconPreferences: [
638
+ { pattern: /^filter(?:_|-|$)|(?:_|-)filter(?:_|-|$)/i, bonus: 56 },
639
+ { pattern: /^tag(?:_|-|$)|(?:_|-)tag(?:_|-|$)|category|grid/i, bonus: 26 },
640
+ ],
641
+ },
642
+ {
643
+ slotPatterns: [/trending/i, /popular/i, /top stories/i, /hot/i],
644
+ queryVariants: ['trending up', 'chart up', 'fire', 'popular'],
645
+ iconPreferences: [
646
+ { pattern: /^trending[_-]?up(?:_|-|$)|chart.*up|up.*chart/i, bonus: 62 },
647
+ { pattern: /^fire(?:_|-|$)|flame|hot/i, bonus: 22 },
648
+ ],
649
+ },
650
+ {
651
+ slotPatterns: [/news/i, /article/i, /headline/i, /story/i, /publisher/i, /logo/i, /title/i],
652
+ queryVariants: ['news', 'article', 'newspaper', 'headline'],
653
+ iconPreferences: [
654
+ { pattern: /^news(?:_|-|$)|(?:_|-)news(?:_|-|$)|newspaper|article/i, bonus: 66 },
655
+ { pattern: /file|document|paper/i, bonus: 16 },
656
+ ],
657
+ },
658
+ {
659
+ slotPatterns: [/dashboard/i],
660
+ queryVariants: ['dashboard', 'layout dashboard', 'grid dashboard'],
661
+ iconPreferences: [
662
+ { pattern: /^dashboard$|layout-dashboard|dashboard/i, bonus: 50 },
663
+ { pattern: /grid|layout/i, bonus: 12 },
664
+ ],
665
+ },
666
+ {
667
+ slotPatterns: [/projects?/i],
668
+ queryVariants: ['folder', 'folders', 'project folder'],
669
+ iconPreferences: [
670
+ { pattern: /^folders?$|(?:_|-)folders?(?:_|-|$)/i, bonus: 56 },
671
+ { pattern: /briefcase|project/i, bonus: 12 },
672
+ ],
673
+ },
674
+ {
675
+ slotPatterns: [/tasks?/i, /todo/i, /to do/i, /checklist/i],
676
+ queryVariants: ['list check', 'checklist', 'checkbox', 'task'],
677
+ iconPreferences: [
678
+ { pattern: /list-check|list_check|checkbox|checklist|clipboard-check/i, bonus: 56 },
679
+ { pattern: /check|task/i, bonus: 16 },
680
+ ],
681
+ },
682
+ {
683
+ slotPatterns: [/team/i, /\busers\b/i, /members?/i],
684
+ queryVariants: ['users', 'team', 'user group'],
685
+ iconPreferences: [
686
+ { pattern: /^users(?:_|-|$)|(?:_|-)users(?:_|-|$)|user-group|user_circle|user-circle/i, bonus: 64 },
687
+ { pattern: /^user(?:_|-|$)|(?:_|-)user(?:_|-|$)/i, bonus: 18 },
688
+ ],
689
+ },
690
+ {
691
+ slotPatterns: [/calendar/i, /schedule/i, /events?/i],
692
+ queryVariants: ['calendar', 'calendar event', 'schedule'],
693
+ iconPreferences: [
694
+ { pattern: /^calendar(?:_|-|$)|(?:_|-)calendar(?:_|-|$)/i, bonus: 54 },
695
+ { pattern: /event|schedule/i, bonus: 18 },
696
+ ],
697
+ },
698
+ {
699
+ slotPatterns: [/\bbold\b/i],
700
+ queryVariants: ['text b', 'bold', 'text bold'],
701
+ iconPreferences: [
702
+ { pattern: /^text[_-]?b$|text-bold|bold/i, bonus: 66 },
703
+ ],
704
+ },
705
+ {
706
+ slotPatterns: [/italic/i],
707
+ queryVariants: ['text italic', 'italic'],
708
+ iconPreferences: [
709
+ { pattern: /^text[_-]?italic$|italic/i, bonus: 66 },
710
+ ],
711
+ },
712
+ {
713
+ priority: 130,
714
+ slotPatterns: [/broken\s+link/i, /link\s+(broken|break|disabled|off)/i],
715
+ queryVariants: ['broken link', 'link break', 'link slash'],
716
+ iconPreferences: [
717
+ { pattern: /link.*(break|broken|slash|off)|(break|broken|slash|off).*link/i, bonus: 180 },
718
+ { pattern: /^link(?:_|-|$)|(?:_|-)link(?:_|-|$)/i, bonus: 10 },
719
+ ],
720
+ },
721
+ {
722
+ priority: 130,
723
+ slotPatterns: [/(broken|disabled|off)\s+(image|photo|picture)/i, /(image|photo|picture)\s+(broken|disabled|off)/i],
724
+ queryVariants: ['photo off', 'image off', 'broken image', 'image broken'],
725
+ iconPreferences: [
726
+ { pattern: /(image|photo|picture).*(broken|off|slash)|(broken|off|slash).*(image|photo|picture)/i, bonus: 240 },
727
+ { pattern: /^image(?:_|-|$)|(?:_|-)image(?:_|-|$)|picture|photo/i, bonus: 10 },
728
+ ],
729
+ },
730
+ {
731
+ priority: 130,
732
+ slotPatterns: [/(comments?|chat|discussion)\s+(off|disabled|muted|slash)/i, /(off|disabled|muted|slash)\s+(comments?|chat|discussion)/i],
733
+ queryVariants: ['chat slash', 'comment off', 'comments off', 'message slash'],
734
+ iconPreferences: [
735
+ { pattern: /(chat|comment|message).*(slash|off|x)|(slash|off|x).*(chat|comment|message)/i, bonus: 180 },
736
+ { pattern: /chat|comment|message/i, bonus: 10 },
737
+ ],
738
+ },
739
+ {
740
+ slotPatterns: [/\blink\b/i, /hyperlink/i],
741
+ queryVariants: ['link', 'link simple', 'hyperlink'],
742
+ iconPreferences: [
743
+ { pattern: /^link(?:_|-|$)|(?:_|-)link(?:_|-|$)/i, bonus: 56 },
744
+ { pattern: /chain/i, bonus: 14 },
745
+ { pattern: /break|broken|slash|unlink|brand/i, bonus: -120 },
746
+ ],
747
+ },
748
+ {
749
+ slotPatterns: [/image/i, /photo/i, /picture/i],
750
+ queryVariants: ['image', 'picture', 'photo'],
751
+ iconPreferences: [
752
+ { pattern: /^image(?:_|-|$)|(?:_|-)image(?:_|-|$)|picture|photo/i, bonus: 56 },
753
+ { pattern: /broken|off|slash|brand/i, bonus: -120 },
754
+ ],
755
+ },
756
+ {
757
+ priority: 80,
758
+ slotPatterns: [/comments?/i, /chat/i, /discussion/i],
759
+ queryVariants: ['chat text', 'comments', 'message dots'],
760
+ iconPreferences: [
761
+ { pattern: /chat|comment|message/i, bonus: 76 },
762
+ { pattern: /slash|off|x$|brand/i, bonus: -120 },
763
+ ],
764
+ },
765
+ {
766
+ slotPatterns: [/undo/i],
767
+ queryVariants: ['undo', 'arrow counter clockwise', 'rotate left'],
768
+ iconPreferences: [
769
+ { pattern: /^undo$|arrow-counter-clockwise|arrow_counter_clockwise|rotate.*left/i, bonus: 66 },
770
+ { pattern: /^redo$|^arrows?[_-]clockwise$|clock[_-]clockwise|arrow_clockwise|rotate.*right/i, bonus: -120 },
771
+ ],
772
+ },
773
+ {
774
+ slotPatterns: [/redo/i],
775
+ queryVariants: ['redo', 'arrow clockwise', 'rotate right'],
776
+ iconPreferences: [
777
+ { pattern: /^redo$|arrow-clockwise|arrow_clockwise|rotate.*right/i, bonus: 66 },
778
+ { pattern: /^undo$|^arrows?[_-]counter[_-]clockwise$|clock[_-]counter[_-]clockwise|arrow_counter_clockwise|rotate.*left/i, bonus: -120 },
779
+ ],
780
+ },
781
+ ]);
336
782
 
337
- const SLOT_PREFERENCE_RULES = Object.freeze({
338
- mingcute: [
783
+ const SLOT_PREFERENCE_RULES = Object.freeze({
784
+ lucide: [
785
+ {
786
+ slotPatterns: [/\b(add|new|create)\s+bookmark\b/i, /\bbookmark\s+(add|new|create)\b/i],
787
+ iconPreferences: [
788
+ { pattern: /^bookmark-plus$/i, bonus: 500 },
789
+ { pattern: /^bookmark$/i, bonus: 40 },
790
+ { pattern: /waves-ladder|map-pin/i, bonus: -220 },
791
+ ],
792
+ },
793
+ {
794
+ slotPatterns: [/users/i, /team/i],
795
+ iconPreferences: [
796
+ { pattern: /^users$/i, bonus: 28 },
797
+ { pattern: /^users-2$/i, bonus: 20 },
798
+ { pattern: /^user-2$/i, bonus: -12 },
799
+ ],
800
+ },
801
+ {
802
+ slotPatterns: [/database/i, /storage/i],
803
+ iconPreferences: [
804
+ { pattern: /^database$/i, bonus: 48 },
805
+ { pattern: /^database-(backup|search)$/i, bonus: 28 },
806
+ { pattern: /^database-zap$/i, bonus: -34 },
807
+ ],
808
+ },
809
+ {
810
+ slotPatterns: [/security/i, /privacy/i, /safe/i, /protection/i],
811
+ iconPreferences: [
812
+ { pattern: /^shield$/i, bonus: 80 },
813
+ { pattern: /^shield-check$/i, bonus: 76 },
814
+ { pattern: /^lock$/i, bonus: 52 },
815
+ { pattern: /^lock-keyhole$/i, bonus: 48 },
816
+ { pattern: /open|unlock|ban|minus|off|slash/i, bonus: -80 },
817
+ ],
818
+ },
819
+ ],
820
+ mingcute: [
339
821
  {
340
- slotPatterns: [/home/i],
341
- iconPreferences: [
342
- { pattern: /^home_3_line$/i, bonus: 14 },
343
- { pattern: /^home_2_line$/i, bonus: 8 },
344
- { pattern: /^home_1_line$/i, bonus: 4 },
345
- ],
822
+ slotPatterns: [/home/i],
823
+ iconPreferences: [
824
+ { pattern: /^home_3_line$/i, bonus: 160 },
825
+ { pattern: /^home_2_line$/i, bonus: 8 },
826
+ { pattern: /^home_1_line$/i, bonus: 4 },
827
+ { pattern: /^home_wifi_line$/i, bonus: -24 },
828
+ ],
346
829
  },
347
830
  {
348
831
  slotPatterns: [/create/i, /add/i, /plus/i, /compose/i],
@@ -352,36 +835,132 @@ const SLOT_PREFERENCE_RULES = Object.freeze({
352
835
  { pattern: /^add_circle_line$/i, bonus: 6 },
353
836
  ],
354
837
  },
355
- {
356
- slotPatterns: [/alerts?/i, /notification/i],
357
- iconPreferences: [{ pattern: /^notification_line$/i, bonus: 12 }],
358
- },
359
- {
360
- slotPatterns: [/profile/i, /user/i, /account/i],
361
- iconPreferences: [{ pattern: /^user_1_line$/i, bonus: 12 }],
362
- },
363
- ],
364
- });
838
+ {
839
+ slotPatterns: [/alerts?/i, /notification/i],
840
+ iconPreferences: [
841
+ { pattern: /^notification_line$/i, bonus: 36 },
842
+ { pattern: /^notification_off_line$/i, bonus: -28 },
843
+ ],
844
+ },
845
+ {
846
+ slotPatterns: [/profile/i, /user/i, /account/i],
847
+ iconPreferences: [
848
+ { pattern: /^user_1_line$/i, bonus: 44 },
849
+ { pattern: /^user_4_line$/i, bonus: -16 },
850
+ ],
851
+ },
852
+ {
853
+ slotPatterns: [/search/i],
854
+ iconPreferences: [
855
+ { pattern: /^search_line$/i, bonus: 70 },
856
+ { pattern: /^search_[23]_line$/i, bonus: 18 },
857
+ { pattern: /^search_.*_ai_line$/i, bonus: -70 },
858
+ ],
859
+ },
860
+ {
861
+ slotPatterns: [/bookmark/i, /saved?/i],
862
+ iconPreferences: [
863
+ { pattern: /^bookmark_line$/i, bonus: 34 },
864
+ { pattern: /^bookmarks_line$/i, bonus: 28 },
865
+ { pattern: /^bookmark_(add|edit|remove)_line$/i, bonus: -20 },
866
+ ],
867
+ },
868
+ {
869
+ slotPatterns: [/trending/i, /popular/i, /top stories/i],
870
+ iconPreferences: [
871
+ { pattern: /^trending_up_line$/i, bonus: 150 },
872
+ { pattern: /^trending_down_line$/i, bonus: -30 },
873
+ ],
874
+ },
875
+ {
876
+ slotPatterns: [/read more/i, /continue/i, /open article/i],
877
+ iconPreferences: [
878
+ { pattern: /^arrow_right_line$/i, bonus: 72 },
879
+ { pattern: /^arrow_to_right_line$/i, bonus: 40 },
880
+ { pattern: /^align_arrow_right_line$/i, bonus: -30 },
881
+ ],
882
+ },
883
+ {
884
+ slotPatterns: [/categor(?:y|ies)/i, /chips?/i, /filter/i, /topics?/i],
885
+ iconPreferences: [
886
+ { pattern: /^filter_line$/i, bonus: 34 },
887
+ { pattern: /^filter_[23]_line$/i, bonus: 22 },
888
+ { pattern: /^tag_line$/i, bonus: 16 },
889
+ ],
890
+ },
891
+ {
892
+ slotPatterns: [/news/i, /article/i, /headline/i, /logo/i, /title/i],
893
+ iconPreferences: [
894
+ { pattern: /^news_line$/i, bonus: 76 },
895
+ { pattern: /^news_2_line$/i, bonus: 46 },
896
+ { pattern: /^appstore_line$/i, bonus: -44 },
897
+ { pattern: /^apple_fruit_line$/i, bonus: -44 },
898
+ ],
899
+ },
900
+ {
901
+ slotPatterns: [/projects?/i],
902
+ iconPreferences: [
903
+ { pattern: /^folder_locked_line$/i, bonus: -70 },
904
+ ],
905
+ },
906
+ ],
907
+ });
365
908
 
366
- function normalizeText(value) {
367
- return String(value || '')
368
- .toLowerCase()
909
+ function normalizeText(value) {
910
+ return String(value || '')
911
+ .toLowerCase()
369
912
  .replace(/[_:]+/g, ' ')
370
913
  .replace(/[^a-z0-9\s-]/g, ' ')
371
914
  .replace(/-/g, ' ')
372
915
  .replace(/\s+/g, ' ')
373
- .trim();
374
- }
375
-
376
- function tokenizeText(value) {
377
- const normalized = normalizeText(value);
378
- return normalized ? normalized.split(' ') : [];
379
- }
380
-
916
+ .trim();
917
+ }
918
+
919
+ function normalizeToken(token) {
920
+ const value = String(token || '').toLowerCase();
921
+ if (value.length > 4 && value.endsWith('ies')) return `${value.slice(0, -3)}y`;
922
+ if (value.length > 3 && value.endsWith('es')) return value.slice(0, -2);
923
+ if (value.length > 3 && value.endsWith('s')) return value.slice(0, -1);
924
+ return value;
925
+ }
926
+
927
+ function tokenizeText(value) {
928
+ const normalized = normalizeText(value);
929
+ if (!normalized) return [];
930
+ const tokens = normalized.split(' ');
931
+ return dedupe([...tokens, ...tokens.map(normalizeToken)]);
932
+ }
933
+
381
934
  function dedupe(values) {
382
935
  return [...new Set(values.filter(Boolean))];
383
936
  }
384
937
 
938
+ function buildDirectLocalizedIntentTerms(value) {
939
+ const text = String(value || '');
940
+ return DIRECT_LOCALIZED_INTENT_RULES
941
+ .filter((rule) => rule.pattern.test(text))
942
+ .flatMap((rule) => rule.terms);
943
+ }
944
+
945
+ function buildRequestedTermSet(intentTerms = []) {
946
+ return new Set(intentTerms.map(normalizeToken).filter(Boolean));
947
+ }
948
+
949
+ function isVariantTokenRequested(token, requestedTerms) {
950
+ const normalizedToken = normalizeToken(token);
951
+ if (requestedTerms.has(normalizedToken)) return true;
952
+ const aliases = REQUESTED_VARIANT_ALIASES[normalizedToken] || [];
953
+ return aliases.some((alias) => requestedTerms.has(normalizeToken(alias)));
954
+ }
955
+
956
+ function isIconVariantExplicitlyRequested(icon, intentTerms = []) {
957
+ const requestedTerms = buildRequestedTermSet(intentTerms);
958
+ return tokenizeText(icon.id).some((token) => (
959
+ VARIANT_TOKENS.has(normalizeToken(token)) &&
960
+ isVariantTokenRequested(token, requestedTerms)
961
+ ));
962
+ }
963
+
385
964
  function buildLocalizedVariants(value, locale) {
386
965
  if (!locale) return [];
387
966
  const expanded = expandCjkQuery(value, {
@@ -433,13 +1012,17 @@ function buildSlotIntentTerms(task, slot, locale = null) {
433
1012
  const taskTokens = tokenizeText(task);
434
1013
  const slotTokens = tokenizeText(slot);
435
1014
  const usefulSlotTokens = slotTokens.filter((token) => !GENERIC_SLOT_WORDS.has(token));
1015
+ const localizedSlotTokens = buildLocalizedVariants(slot, locale).flatMap(tokenizeText);
1016
+ const localizedTaskTokens = buildLocalizedVariants(task, locale).flatMap(tokenizeText);
1017
+ const directSlotTokens = buildDirectLocalizedIntentTerms(slot);
436
1018
 
437
1019
  const expanded = [...usefulSlotTokens];
438
1020
 
439
1021
  const usefulTaskTokens = taskTokens.filter((token) => !GENERIC_SLOT_WORDS.has(token));
440
1022
  expanded.push(...usefulTaskTokens);
441
- expanded.push(...buildLocalizedVariants(slot, locale).flatMap(tokenizeText));
442
- expanded.push(...buildLocalizedVariants(task, locale).flatMap(tokenizeText));
1023
+ expanded.push(...localizedSlotTokens);
1024
+ expanded.push(...localizedTaskTokens);
1025
+ expanded.push(...directSlotTokens);
443
1026
 
444
1027
  const variants = buildIntentQueryVariants(`${slot} ${task}`, {
445
1028
  baseQuery: slot,
@@ -449,18 +1032,20 @@ function buildSlotIntentTerms(task, slot, locale = null) {
449
1032
  expanded.push(...tokenizeText(variant));
450
1033
  }
451
1034
 
452
- for (const rule of getMatchingSlotRules(slot, expanded)) {
453
- for (const variant of rule.queryVariants || []) {
454
- expanded.push(...tokenizeText(variant));
455
- }
1035
+ const slotRuleTerms = dedupe([...usefulSlotTokens, ...localizedSlotTokens, ...directSlotTokens]);
1036
+ for (const rule of getMatchingSlotRules(slot, slotRuleTerms)) {
1037
+ for (const variant of rule.queryVariants || []) {
1038
+ expanded.push(...tokenizeText(variant));
1039
+ }
456
1040
  }
457
1041
 
458
1042
  return dedupe(expanded);
459
1043
  }
460
1044
 
461
1045
  function buildSlotQueryVariants(task, slot, locale = null) {
1046
+ const localizedSlotVariants = buildLocalizedVariants(slot, locale);
462
1047
  const localizedVariants = [
463
- ...buildLocalizedVariants(slot, locale),
1048
+ ...localizedSlotVariants,
464
1049
  ...buildLocalizedVariants(`${slot} ${task}`, locale),
465
1050
  ];
466
1051
  const variants = buildIntentQueryVariants(`${slot} ${task}`, {
@@ -470,82 +1055,115 @@ function buildSlotQueryVariants(task, slot, locale = null) {
470
1055
  variants.unshift(...localizedVariants);
471
1056
  const usefulSlotTokens = tokenizeText(slot).filter((token) => !GENERIC_SLOT_WORDS.has(token));
472
1057
  variants.push(...usefulSlotTokens);
473
- const intentTerms = tokenizeText(`${slot} ${task} ${variants.join(' ')}`);
474
- const ruleVariants = getMatchingSlotRules(slot, intentTerms)
1058
+ const slotRuleTerms = [
1059
+ ...tokenizeText(`${slot} ${localizedSlotVariants.join(' ')}`),
1060
+ ...buildDirectLocalizedIntentTerms(slot),
1061
+ ]
1062
+ .filter((token) => !GENERIC_SLOT_WORDS.has(token));
1063
+ const ruleVariants = getMatchingSlotRules(slot, slotRuleTerms)
475
1064
  .flatMap((rule) => rule.queryVariants || []);
476
1065
  variants.unshift(...ruleVariants);
477
1066
  return dedupe(variants).slice(0, 12);
478
1067
  }
479
1068
 
480
- function scoreLexicalFit(icon, intentTerms, slotLabel) {
481
- const tokens = new Set([
482
- ...tokenizeText(icon.id),
483
- ...tokenizeText(icon.name),
484
- ...tokenizeText(`${icon.lib}:${icon.id}`),
485
- ]);
486
- const normalizedId = normalizeText(icon.id);
487
- const normalizedName = normalizeText(icon.name);
488
- const normalizedSlot = normalizeText(slotLabel);
489
-
490
- let score = 0;
491
- for (const term of intentTerms) {
492
- if (tokens.has(term)) score += 14;
493
- else if (normalizedId.includes(term) || normalizedName.includes(term)) score += 8;
494
-
495
- if (normalizedId === term || normalizedName === term) {
496
- score += 20;
497
- }
498
- }
499
-
500
- if (normalizedSlot && (normalizedId === normalizedSlot || normalizedName === normalizedSlot)) {
501
- score += 20;
502
- }
503
-
504
- return score;
505
- }
506
-
507
- function getVariantPenalty(icon) {
508
- const normalizedId = normalizeText(icon.id);
509
- let penalty = 0;
510
- for (const rule of VARIANT_PENALTIES) {
511
- if (rule.pattern.test(normalizedId)) {
512
- penalty += rule.penalty;
513
- }
514
- }
515
- return penalty;
516
- }
517
-
1069
+ function scoreLexicalFit(icon, intentTerms, slotLabel, taskLabel = '') {
1070
+ const tokens = new Set([
1071
+ ...tokenizeText(icon.id),
1072
+ ...tokenizeText(icon.name),
1073
+ ...tokenizeText(`${icon.lib}:${icon.id}`),
1074
+ ]);
1075
+ const normalizedId = normalizeText(icon.id);
1076
+ const normalizedName = normalizeText(icon.name);
1077
+ const normalizedSlot = normalizeText(slotLabel);
1078
+ const slotTerms = tokenizeText(slotLabel).filter((token) => !GENERIC_SLOT_WORDS.has(token));
1079
+ const taskTerms = tokenizeText(taskLabel).filter((token) => !GENERIC_SLOT_WORDS.has(token));
1080
+
1081
+ let score = 0;
1082
+ for (const term of slotTerms) {
1083
+ if (tokens.has(term)) score += 22;
1084
+ else if (normalizedId.includes(term) || normalizedName.includes(term)) score += 14;
1085
+
1086
+ if (normalizedId === term || normalizedName === term) {
1087
+ score += 24;
1088
+ }
1089
+ }
1090
+
1091
+ for (const term of intentTerms) {
1092
+ if (tokens.has(term)) score += 12;
1093
+ else if (normalizedId.includes(term) || normalizedName.includes(term)) score += 7;
1094
+
1095
+ if (normalizedId === term || normalizedName === term) {
1096
+ score += 14;
1097
+ }
1098
+ }
1099
+
1100
+ for (const term of taskTerms) {
1101
+ if (tokens.has(term)) score += 3;
1102
+ }
1103
+
1104
+ if (normalizedSlot && (normalizedId === normalizedSlot || normalizedName === normalizedSlot)) {
1105
+ score += 26;
1106
+ }
1107
+
1108
+ return score;
1109
+ }
1110
+
1111
+ function getVariantPenalty(icon, intentTerms = []) {
1112
+ const normalizedId = normalizeText(icon.id);
1113
+ const requestedTerms = buildRequestedTermSet(intentTerms);
1114
+ let penalty = 0;
1115
+ for (const rule of VARIANT_PENALTIES) {
1116
+ if (!rule.pattern.test(normalizedId)) continue;
1117
+ if (isVariantTokenRequested(rule.token, requestedTerms)) continue;
1118
+ penalty += rule.penalty;
1119
+ }
1120
+ return penalty;
1121
+ }
1122
+
1123
+ function getBrandPenalty(icon, intentTerms = []) {
1124
+ const requestedTerms = buildRequestedTermSet(intentTerms);
1125
+ if (isVariantTokenRequested('brand', requestedTerms)) return 0;
1126
+ return icon.lib === 'simpleicons' ? 80 : 0;
1127
+ }
1128
+
518
1129
  function getMatchingSlotRules(slotLabel, intentTerms = []) {
519
1130
  const rawSlotText = String(slotLabel || '');
520
1131
  const slotText = normalizeText(slotLabel);
521
- return COMMON_SLOT_PREFERENCE_RULES.filter((rule) => rule.slotPatterns.some((pattern) => (
522
- pattern.test(slotText) || pattern.test(rawSlotText)
523
- )));
1132
+ const intentText = normalizeText(intentTerms.join(' '));
1133
+ return COMMON_SLOT_PREFERENCE_RULES
1134
+ .filter((rule) => rule.slotPatterns.some((pattern) => (
1135
+ pattern.test(slotText) ||
1136
+ pattern.test(rawSlotText) ||
1137
+ pattern.test(intentText)
1138
+ )))
1139
+ .sort((left, right) => (right.priority || 0) - (left.priority || 0));
524
1140
  }
525
1141
 
526
- function scoreSlotPreferenceRules(icon, rules = [], slotText = '') {
527
- let bonus = 0;
528
-
529
- for (const rule of rules) {
530
- for (const preference of rule.iconPreferences) {
531
- if (preference.pattern.test(icon.id)) {
532
- bonus += preference.bonus;
533
- }
534
- }
1142
+ function scoreSlotPreferenceRules(icon, rules = [], intentTerms = []) {
1143
+ let bonus = 0;
1144
+ const explicitlyRequestedVariant = isIconVariantExplicitlyRequested(icon, intentTerms);
1145
+
1146
+ for (const rule of rules) {
1147
+ for (const preference of rule.iconPreferences) {
1148
+ if (preference.pattern.test(icon.id)) {
1149
+ if (preference.bonus < 0 && explicitlyRequestedVariant) continue;
1150
+ bonus += preference.bonus;
1151
+ }
1152
+ }
535
1153
  }
536
1154
 
537
1155
  return bonus;
538
1156
  }
539
1157
 
540
- function getSlotPreferenceBonus(icon, slotLabel, intentTerms, library) {
541
- const slotText = `${slotLabel} ${intentTerms.join(' ')}`;
542
- const commonRules = getMatchingSlotRules(slotLabel, intentTerms);
543
- const libraryRules = (SLOT_PREFERENCE_RULES[library] || [])
544
- .filter((rule) => rule.slotPatterns.some((pattern) => pattern.test(slotText)));
545
-
546
- return scoreSlotPreferenceRules(icon, commonRules, slotText) +
547
- scoreSlotPreferenceRules(icon, libraryRules, slotText);
548
- }
1158
+ function getSlotPreferenceBonus(icon, slotLabel, intentTerms, library, requestedVariantTerms = intentTerms) {
1159
+ const slotText = `${slotLabel} ${requestedVariantTerms.join(' ')}`;
1160
+ const commonRules = getMatchingSlotRules(slotLabel, requestedVariantTerms);
1161
+ const libraryRules = (SLOT_PREFERENCE_RULES[library] || [])
1162
+ .filter((rule) => rule.slotPatterns.some((pattern) => pattern.test(slotText)));
1163
+
1164
+ return scoreSlotPreferenceRules(icon, commonRules, requestedVariantTerms) +
1165
+ scoreSlotPreferenceRules(icon, libraryRules, requestedVariantTerms);
1166
+ }
549
1167
 
550
1168
  function summarizeSemanticFit(slotLabel, semanticRecord, intentTerms) {
551
1169
  if (semanticRecord?.depicts && semanticRecord?.use_when) {
@@ -563,7 +1181,7 @@ function summarizeSemanticFit(slotLabel, semanticRecord, intentTerms) {
563
1181
  return `Best available match for ${slotLabel}.`;
564
1182
  }
565
1183
 
566
- function buildWhySelected(slotLabel, semanticRecord, iconResult) {
1184
+ function buildWhySelected(slotLabel, semanticRecord, iconResult) {
567
1185
  const label = semanticRecord?.label || iconResult.name;
568
1186
  if (semanticRecord?.depicts) {
569
1187
  return `${label} matches ${slotLabel} and visually reads as ${String(semanticRecord.depicts).toLowerCase()}.`;
@@ -571,24 +1189,43 @@ function buildWhySelected(slotLabel, semanticRecord, iconResult) {
571
1189
  if (semanticRecord?.use_when) {
572
1190
  return `${label} matches ${slotLabel}. ${semanticRecord.use_when}`;
573
1191
  }
574
- return `${label} is the clearest match for ${slotLabel} from the current library.`;
575
- }
576
-
577
- function buildCandidatePayload(slotLabel, iconResult, semanticRecord, intentTerms) {
578
- return {
579
- id: iconResult.id,
580
- library: iconResult.library,
581
- name: iconResult.name,
582
- style: iconResult.style || 'outline',
583
- label: semanticRecord?.label || iconResult.semantic?.label || iconResult.name,
584
- semantic_fit: summarizeSemanticFit(slotLabel, semanticRecord, intentTerms),
585
- why_selected: buildWhySelected(slotLabel, semanticRecord, iconResult),
586
- svg: iconResult.svg,
587
- semantic: buildPublicSemanticPayload(semanticRecord) || iconResult.semantic || null,
588
- };
589
- }
1192
+ return `${label} is the clearest match for ${slotLabel} from the current library.`;
1193
+ }
1194
+
1195
+ function buildCandidatePayload(
1196
+ slotLabel,
1197
+ iconResult,
1198
+ semanticRecord,
1199
+ intentTerms,
1200
+ responseMode = 'plan',
1201
+ includeSvg = false,
1202
+ includeReason = true
1203
+ ) {
1204
+ const payload = {
1205
+ id: iconResult.id,
1206
+ library: iconResult.library,
1207
+ name: iconResult.name,
1208
+ style: iconResult.style || 'outline',
1209
+ label: semanticRecord?.label || iconResult.semantic?.label || iconResult.name,
1210
+ };
1211
+
1212
+ if (includeReason) {
1213
+ payload.semantic_fit = summarizeSemanticFit(slotLabel, semanticRecord, intentTerms);
1214
+ payload.why_selected = buildWhySelected(slotLabel, semanticRecord, iconResult);
1215
+ }
1216
+
1217
+ if (includeSvg) {
1218
+ payload.svg = iconResult.svg;
1219
+ }
1220
+
1221
+ if (responseMode === 'full') {
1222
+ payload.semantic = buildPublicSemanticPayload(semanticRecord) || iconResult.semantic || null;
1223
+ }
1224
+
1225
+ return payload;
1226
+ }
590
1227
 
591
- async function mapWithConcurrency(items, limit, mapper) {
1228
+ async function mapWithConcurrency(items, limit, mapper) {
592
1229
  const results = new Array(items.length);
593
1230
  let nextIndex = 0;
594
1231
 
@@ -601,27 +1238,57 @@ async function mapWithConcurrency(items, limit, mapper) {
601
1238
  }
602
1239
 
603
1240
  const workerCount = Math.min(Math.max(1, limit), items.length);
604
- await Promise.all(Array.from({ length: workerCount }, () => worker()));
605
- return results;
606
- }
607
-
608
- export async function recommendIconsForTask({
609
- task,
610
- library,
1241
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
1242
+ return results;
1243
+ }
1244
+
1245
+ function getConfidence(topScore, nextScore = 0) {
1246
+ if (topScore >= 90 && topScore - nextScore >= 20) {
1247
+ return { level: 'high', score: topScore };
1248
+ }
1249
+ if (topScore >= 45) {
1250
+ return { level: 'medium', score: topScore };
1251
+ }
1252
+ return { level: 'low', score: topScore };
1253
+ }
1254
+
1255
+ function buildLowConfidenceHint(slotLabel, queriesUsed) {
1256
+ return `Low confidence for ${slotLabel}. Try search_icons with: ${queriesUsed.slice(0, 3).join(', ')}.`;
1257
+ }
1258
+
1259
+ function normalizeResponseMode(responseMode) {
1260
+ if (responseMode === 'assets' || responseMode === 'full') return responseMode;
1261
+ return 'plan';
1262
+ }
1263
+
1264
+ function isNoisyAlternative(entry) {
1265
+ return entry.variantPenalty >= 12 || entry.brandPenalty >= 12 || entry.slotPreferenceBonus < 0;
1266
+ }
1267
+
1268
+ export async function recommendIconsForTask({
1269
+ task,
1270
+ library,
611
1271
  style = 'any',
612
1272
  locale = null,
613
1273
  slots,
614
1274
  limitPerSlot = 3,
615
- searchIconsForQuery,
616
- buildIconResult,
617
- semanticMap,
618
- }) {
619
- const slotResults = await mapWithConcurrency(slots, 6, async (slotLabel) => {
1275
+ responseMode = 'plan',
1276
+ searchIconsForQuery,
1277
+ buildIconResult,
1278
+ semanticMap,
1279
+ }) {
1280
+ const normalizedResponseMode = normalizeResponseMode(responseMode);
1281
+ const scoredSlotResults = await mapWithConcurrency(slots, 6, async (slotLabel) => {
620
1282
  const intentTerms = buildSlotIntentTerms(task, slotLabel, locale);
621
- const queryVariants = buildSlotQueryVariants(task, slotLabel, locale).slice(0, locale ? 8 : 2);
1283
+ const requestedVariantTerms = dedupe([
1284
+ ...tokenizeText(slotLabel),
1285
+ ...buildLocalizedVariants(slotLabel, locale).flatMap(tokenizeText),
1286
+ ...buildDirectLocalizedIntentTerms(slotLabel),
1287
+ ]);
1288
+ const queryVariants = buildSlotQueryVariants(task, slotLabel, locale).slice(0, locale ? 8 : 4);
622
1289
  const pooledIcons = [];
623
- const seen = new Set();
624
-
1290
+ const seen = new Set();
1291
+
625
1292
  const resultGroups = await mapWithConcurrency(queryVariants, 2, async (queryVariant) => {
626
1293
  try {
627
1294
  return await searchIconsForQuery({
@@ -646,62 +1313,137 @@ export async function recommendIconsForTask({
646
1313
  }
647
1314
 
648
1315
  const scored = pooledIcons
649
- .map((icon, index) => {
650
- const semanticRecord = getSemanticRecordForIcon(semanticMap, icon);
651
- const semanticQuery = queryVariants.join(' ');
652
- const semanticScore = semanticRecord ? scoreSemanticAlignment(semanticQuery, semanticRecord) * 3 : 0;
653
- const lexicalScore = scoreLexicalFit(icon, intentTerms, slotLabel);
654
- const semanticBonus = semanticRecord ? 6 : 0;
655
- const variantPenalty = getVariantPenalty(icon);
656
- const slotPreferenceBonus = getSlotPreferenceBonus(icon, slotLabel, intentTerms, library);
657
- const intentProfile = buildSearchIntentProfile(`${slotLabel} ${task}`);
658
- const intentAdjustment = getIntentCandidateAdjustment(icon, intentProfile);
659
-
1316
+ .map((icon, index) => {
1317
+ const semanticRecord = getSemanticRecordForIcon(semanticMap, icon);
1318
+ const semanticQuery = queryVariants.join(' ');
1319
+ const semanticScore = semanticRecord ? scoreSemanticAlignment(semanticQuery, semanticRecord) * 3 : 0;
1320
+ const lexicalScore = scoreLexicalFit(icon, intentTerms, slotLabel, task);
1321
+ const semanticBonus = semanticRecord ? 6 : 0;
1322
+ const variantPenalty = getVariantPenalty(icon, requestedVariantTerms);
1323
+ const brandPenalty = getBrandPenalty(icon, requestedVariantTerms);
1324
+ const slotPreferenceBonus = getSlotPreferenceBonus(icon, slotLabel, intentTerms, library, requestedVariantTerms);
1325
+ const intentProfile = buildSearchIntentProfile(`${slotLabel} ${task}`);
1326
+ const intentAdjustment = getIntentCandidateAdjustment(icon, intentProfile);
1327
+
660
1328
  return {
661
- icon,
662
- index,
663
- semanticRecord,
664
- slotPreferenceBonus,
665
- totalScore:
1329
+ icon,
1330
+ index,
1331
+ semanticRecord,
1332
+ variantPenalty,
1333
+ brandPenalty,
1334
+ slotPreferenceBonus,
1335
+ totalScore:
666
1336
  semanticScore +
667
1337
  lexicalScore +
668
1338
  semanticBonus +
669
- slotPreferenceBonus +
670
- intentAdjustment.boost -
671
- intentAdjustment.penalty -
672
- variantPenalty,
673
- };
674
- })
675
- .filter((entry) => entry.totalScore > 0)
676
- .sort((left, right) => {
677
- if (right.slotPreferenceBonus !== left.slotPreferenceBonus) {
678
- return right.slotPreferenceBonus - left.slotPreferenceBonus;
679
- }
680
- if (right.totalScore !== left.totalScore) return right.totalScore - left.totalScore;
681
- return left.index - right.index;
682
- })
683
- .slice(0, limitPerSlot);
684
-
685
- const preparedCandidates = [];
686
- for (const entry of scored) {
687
- const iconResult = await buildIconResult(entry.icon, { style });
688
- if (!iconResult?.svg) continue;
689
- preparedCandidates.push(buildCandidatePayload(slotLabel, iconResult, entry.semanticRecord, intentTerms));
690
- }
691
-
692
- return {
693
- slot: slotLabel,
694
- queries_used: queryVariants,
695
- recommended: preparedCandidates[0] || null,
696
- alternatives: preparedCandidates.slice(1),
697
- };
698
- });
699
-
700
- return {
701
- task,
702
- library: library || 'all',
703
- style,
704
- slot_count: slots.length,
705
- results: slotResults,
706
- };
707
- }
1339
+ slotPreferenceBonus +
1340
+ intentAdjustment.boost -
1341
+ intentAdjustment.penalty -
1342
+ variantPenalty -
1343
+ brandPenalty,
1344
+ };
1345
+ })
1346
+ .filter((entry) => entry.totalScore > 0)
1347
+ .sort((left, right) => {
1348
+ if (right.totalScore !== left.totalScore) return right.totalScore - left.totalScore;
1349
+ if (right.slotPreferenceBonus !== left.slotPreferenceBonus) {
1350
+ return right.slotPreferenceBonus - left.slotPreferenceBonus;
1351
+ }
1352
+ return left.index - right.index;
1353
+ })
1354
+ .slice(0, Math.max(limitPerSlot * 3, 8));
1355
+
1356
+ return {
1357
+ slot: slotLabel,
1358
+ queries_used: queryVariants,
1359
+ intentTerms,
1360
+ requestedVariantTerms,
1361
+ scored,
1362
+ };
1363
+ });
1364
+
1365
+ const usedIconKeys = new Set();
1366
+ const slotResults = [];
1367
+ for (const slotResult of scoredSlotResults) {
1368
+ const sorted = [...slotResult.scored].sort((left, right) => {
1369
+ const leftKey = `${left.icon.lib}:${left.icon.id}`;
1370
+ const rightKey = `${right.icon.lib}:${right.icon.id}`;
1371
+ const leftDuplicatePenalty = usedIconKeys.has(leftKey) ? 80 : 0;
1372
+ const rightDuplicatePenalty = usedIconKeys.has(rightKey) ? 80 : 0;
1373
+ const leftScore = left.totalScore - leftDuplicatePenalty;
1374
+ const rightScore = right.totalScore - rightDuplicatePenalty;
1375
+
1376
+ if (rightScore !== leftScore) return rightScore - leftScore;
1377
+ return left.index - right.index;
1378
+ });
1379
+ const selectedEntries = [];
1380
+ const primaryEntry = sorted.find((entry) => (
1381
+ !isNoisyAlternative(entry) &&
1382
+ !usedIconKeys.has(`${entry.icon.lib}:${entry.icon.id}`)
1383
+ )) || sorted.find((entry) => !isNoisyAlternative(entry)) || sorted[0];
1384
+ if (primaryEntry) {
1385
+ selectedEntries.push(primaryEntry);
1386
+ }
1387
+ for (const entry of sorted) {
1388
+ if (selectedEntries.length >= limitPerSlot) break;
1389
+ if (selectedEntries.includes(entry)) continue;
1390
+ if (isNoisyAlternative(entry)) continue;
1391
+ selectedEntries.push(entry);
1392
+ }
1393
+ const preparedCandidates = [];
1394
+ const preparedEntries = [];
1395
+ for (const [candidateIndex, entry] of selectedEntries.entries()) {
1396
+ const iconResult = await buildIconResult(entry.icon, { style });
1397
+ if (!iconResult?.svg) continue;
1398
+ const includeSvg = normalizedResponseMode === 'full' || (normalizedResponseMode === 'assets' && candidateIndex === 0);
1399
+ preparedCandidates.push(buildCandidatePayload(
1400
+ slotResult.slot,
1401
+ iconResult,
1402
+ entry.semanticRecord,
1403
+ slotResult.intentTerms,
1404
+ normalizedResponseMode,
1405
+ includeSvg,
1406
+ normalizedResponseMode !== 'plan' || candidateIndex === 0
1407
+ ));
1408
+ preparedEntries.push(entry);
1409
+ }
1410
+ const chosen = preparedEntries[0] || primaryEntry || null;
1411
+ if (chosen) {
1412
+ usedIconKeys.add(`${chosen.icon.lib}:${chosen.icon.id}`);
1413
+ }
1414
+ const confidence = chosen
1415
+ ? getConfidence(chosen.totalScore, sorted[1]?.totalScore || 0)
1416
+ : { level: 'low', score: 0 };
1417
+
1418
+ const slotPayload = {
1419
+ slot: slotResult.slot,
1420
+ confidence,
1421
+ recommended: preparedCandidates[0] || null,
1422
+ alternatives: preparedCandidates.slice(1),
1423
+ };
1424
+ if (confidence.level === 'low') {
1425
+ slotPayload.guidance = buildLowConfidenceHint(slotResult.slot, slotResult.queries_used);
1426
+ }
1427
+ if (normalizedResponseMode !== 'plan') {
1428
+ slotPayload.queries_used = slotResult.queries_used;
1429
+ }
1430
+ slotResults.push(slotPayload);
1431
+ }
1432
+
1433
+ const lowConfidenceSlots = slotResults
1434
+ .filter((slot) => !slot.recommended || slot.confidence?.level === 'low')
1435
+ .map((slot) => slot.slot);
1436
+ const allSlotsResolved = slotResults.every((slot) => Boolean(slot.recommended));
1437
+
1438
+ return {
1439
+ task,
1440
+ library: library || 'all',
1441
+ style,
1442
+ response_mode: normalizedResponseMode,
1443
+ slot_count: slots.length,
1444
+ all_slots_resolved: allSlotsResolved,
1445
+ low_confidence_slots: lowConfidenceSlots,
1446
+ fallback_recommended: !allSlotsResolved || lowConfidenceSlots.length > 0,
1447
+ results: slotResults,
1448
+ };
1449
+ }