@tasksai/install 0.1.30 → 0.1.32

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tasksai/install",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "description": "Shared TasksAI MCP installer CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,7 +11,7 @@ import unicodedata
11
11
  from collections.abc import Iterable, Mapping, Sequence
12
12
 
13
13
 
14
- POLICY_VERSION = "tasksai-trigger-matcher/2.0"
14
+ POLICY_VERSION = "tasksai-trigger-matcher/2.1"
15
15
 
16
16
  DEFAULT_STOP_WORDS = {
17
17
  "a", "an", "and", "are", "as", "at", "be", "by", "can", "create",
@@ -57,11 +57,48 @@ DEFAULT_CONTEXT_WORDS = {
57
57
  DEFAULT_THRESHOLDS = {"high": 90, "medium": 45}
58
58
  DEFAULT_RESULT_COUNT = 3
59
59
  MAXIMUM_RESULT_COUNT = 10
60
+ DEFAULT_RESULT_COUNTS = {"realtor": 5}
61
+ REALTOR_ACTION_WORDS = {
62
+ "assemble", "build", "create", "develop", "document", "draft",
63
+ "generate", "make", "prepare", "produce", "write", "writing",
64
+ }
65
+ REALTOR_DISCOVERY_WORDS = {
66
+ "browse", "display", "find", "get", "give", "look", "pull",
67
+ "search", "searching", "see", "show", "up", "view",
68
+ }
69
+ DEFAULT_TOKEN_ALIASES = {
70
+ "realtor": {
71
+ "assemble": "create",
72
+ "build": "create",
73
+ "develop": "create",
74
+ "draft": "create",
75
+ "dwelling": "property",
76
+ "generate": "create",
77
+ "home": "property",
78
+ "house": "property",
79
+ "listed": "listing",
80
+ "listings": "listing",
81
+ "list": "listing",
82
+ "make": "create",
83
+ "prepare": "create",
84
+ "prepared": "create",
85
+ "preparing": "create",
86
+ "preparation": "create",
87
+ "produce": "create",
88
+ "residence": "property",
89
+ "residential": "property",
90
+ "write": "create",
91
+ "writing": "create",
92
+ },
93
+ }
60
94
  TRIGGER_EVIDENCE_RANK = {
61
- "trigger_exact": 4,
95
+ "trigger_exact": 5,
96
+ "trigger_equivalent": 4,
62
97
  "trigger_complete": 3,
63
98
  "trigger_query_complete": 2,
64
- "trigger_partial": 1,
99
+ "trigger_partial": 2,
100
+ "trigger_broad": 1,
101
+ "trigger_context": 1,
65
102
  }
66
103
 
67
104
 
@@ -149,9 +186,95 @@ def _normalize_scoring_tokens(
149
186
  value: object,
150
187
  stop_words: set[str],
151
188
  abbreviations: Mapping[str, object],
189
+ token_aliases: Mapping[str, str],
152
190
  ) -> tuple[str, ...]:
153
191
  expanded = _expand_abbreviations(_tokens(value), abbreviations)
154
- return tuple(token for token in expanded if token not in stop_words)
192
+ canonical = tuple(token_aliases.get(token, token) for token in expanded)
193
+ return tuple(token for token in canonical if token not in stop_words)
194
+
195
+
196
+ def _same_token_bag(left: tuple[str, ...], right: tuple[str, ...]) -> bool:
197
+ return bool(left) and len(left) == len(right) and sorted(left) == sorted(right)
198
+
199
+
200
+ def _within_one_edit(left: str, right: str) -> bool:
201
+ """Return True for one insertion, deletion, substitution, or transposition."""
202
+ if left == right or abs(len(left) - len(right)) > 1:
203
+ return False
204
+ if len(left) == len(right):
205
+ differences = [index for index, pair in enumerate(zip(left, right)) if pair[0] != pair[1]]
206
+ if len(differences) == 1:
207
+ return True
208
+ return (
209
+ len(differences) == 2
210
+ and differences[1] == differences[0] + 1
211
+ and left[differences[0]] == right[differences[1]]
212
+ and left[differences[1]] == right[differences[0]]
213
+ )
214
+ shorter, longer = (left, right) if len(left) < len(right) else (right, left)
215
+ short_index = long_index = differences = 0
216
+ while short_index < len(shorter) and long_index < len(longer):
217
+ if shorter[short_index] == longer[long_index]:
218
+ short_index += 1
219
+ long_index += 1
220
+ continue
221
+ differences += 1
222
+ long_index += 1
223
+ if differences > 1:
224
+ return False
225
+ return True
226
+
227
+
228
+ def _build_token_index(
229
+ skills: Sequence[Mapping[str, object]],
230
+ triggers: Mapping[str, object],
231
+ product_id: str,
232
+ policy: Mapping[str, object],
233
+ ) -> tuple[dict[tuple[int, str], set[str]], set[str]]:
234
+ """Index public discovery tokens by length and first character for bounded typo repair."""
235
+ stop_words = policy["stop_words"]
236
+ abbreviations = policy["abbreviations"]
237
+ token_aliases = policy["token_aliases"]
238
+ vocabulary = set(token_aliases) | set(token_aliases.values())
239
+ for skill in skills:
240
+ for value in (skill.get("name", ""), _category_text(skill)):
241
+ vocabulary.update(
242
+ _normalize_scoring_tokens(value, stop_words, abbreviations, token_aliases)
243
+ )
244
+ skill_id = str(skill.get("id", ""))
245
+ for trigger in _trigger_values(triggers, skill_id, product_id):
246
+ vocabulary.update(
247
+ _normalize_scoring_tokens(trigger, stop_words, abbreviations, token_aliases)
248
+ )
249
+ index: dict[tuple[int, str], set[str]] = {}
250
+ for token in vocabulary:
251
+ if len(token) < 5:
252
+ continue
253
+ index.setdefault((len(token), token[0]), set()).add(token)
254
+ return index, vocabulary
255
+
256
+
257
+ def _repair_query_tokens(
258
+ tokens: tuple[str, ...],
259
+ token_index: Mapping[tuple[int, str], set[str]],
260
+ vocabulary: set[str],
261
+ ) -> tuple[tuple[str, ...], list[str]]:
262
+ repaired: list[str] = []
263
+ diagnostics: list[str] = []
264
+ for token in tokens:
265
+ if len(token) < 5 or token in vocabulary:
266
+ repaired.append(token)
267
+ continue
268
+ candidates: set[str] = set()
269
+ for length in range(len(token) - 1, len(token) + 2):
270
+ candidates.update(token_index.get((length, token[0]), set()))
271
+ matches = sorted(candidate for candidate in candidates if _within_one_edit(token, candidate))
272
+ if len(matches) == 1:
273
+ repaired.append(matches[0])
274
+ diagnostics.append(f"fuzzy_corrected:{token}->{matches[0]}")
275
+ else:
276
+ repaired.append(token)
277
+ return tuple(repaired), diagnostics
155
278
 
156
279
 
157
280
  def _contains_tokens(container: tuple[str, ...], candidate: tuple[str, ...]) -> bool:
@@ -222,6 +345,9 @@ def _resolve_config(product_id: str, config: Mapping[str, object] | None) -> tup
222
345
  for value in supplied_stop_words
223
346
  for token in _tokens(value)
224
347
  }
348
+ if _canonical_product_id(product_id) == "realtor":
349
+ stop_words.difference_update(REALTOR_ACTION_WORDS)
350
+ stop_words.update(REALTOR_DISCOVERY_WORDS)
225
351
  context_default = DEFAULT_CONTEXT_WORDS.get(_canonical_product_id(product_id), set())
226
352
  supplied_context_words = source.get("context_words", context_default)
227
353
  if not isinstance(supplied_context_words, Sequence) or isinstance(supplied_context_words, (str, bytes)):
@@ -242,10 +368,13 @@ def _resolve_config(product_id: str, config: Mapping[str, object] | None) -> tup
242
368
  diagnostics.append("bundled_thresholds:invalid_order")
243
369
  thresholds = dict(DEFAULT_THRESHOLDS)
244
370
 
245
- default_count = source.get("default_result_count", DEFAULT_RESULT_COUNT)
371
+ product_default_count = DEFAULT_RESULT_COUNTS.get(
372
+ _canonical_product_id(product_id), DEFAULT_RESULT_COUNT
373
+ )
374
+ default_count = source.get("default_result_count", product_default_count)
246
375
  maximum_count = source.get("maximum_result_count", MAXIMUM_RESULT_COUNT)
247
376
  if not isinstance(default_count, int) or isinstance(default_count, bool) or default_count < 1:
248
- default_count = DEFAULT_RESULT_COUNT
377
+ default_count = product_default_count
249
378
  if not isinstance(maximum_count, int) or isinstance(maximum_count, bool) or maximum_count < 1:
250
379
  maximum_count = MAXIMUM_RESULT_COUNT
251
380
  maximum_count = min(maximum_count, MAXIMUM_RESULT_COUNT)
@@ -262,6 +391,8 @@ def _resolve_config(product_id: str, config: Mapping[str, object] | None) -> tup
262
391
  "default_result_count": default_count,
263
392
  "maximum_result_count": maximum_count,
264
393
  "abbreviations": configured_abbreviations,
394
+ "token_aliases": DEFAULT_TOKEN_ALIASES.get(_canonical_product_id(product_id), {}),
395
+ "broad_matching": _canonical_product_id(product_id) == "realtor",
265
396
  }, diagnostics
266
397
 
267
398
 
@@ -271,8 +402,12 @@ def _score_trigger(
271
402
  stop_words: set[str],
272
403
  context_words: set[str],
273
404
  abbreviations: Mapping[str, object],
405
+ token_aliases: Mapping[str, str],
406
+ broad_matching: bool,
274
407
  ) -> tuple[int, int, set[str], str]:
275
- trigger_tokens = _normalize_scoring_tokens(trigger, stop_words, abbreviations)
408
+ trigger_tokens = _normalize_scoring_tokens(
409
+ trigger, stop_words, abbreviations, token_aliases
410
+ )
276
411
  if not trigger_tokens:
277
412
  return 0, 0, set(), ""
278
413
 
@@ -284,10 +419,14 @@ def _score_trigger(
284
419
 
285
420
  if query_tokens == trigger_tokens:
286
421
  return 120, specificity, matched, "trigger_exact"
422
+ if broad_matching and _same_token_bag(query_tokens, trigger_tokens):
423
+ return 110, specificity, matched, "trigger_equivalent"
287
424
  if _contains_tokens(query_tokens, trigger_tokens):
288
425
  bonus = min(15, max(0, specificity - 1) * 3)
289
426
  return 90 + bonus, specificity, matched, "trigger_complete"
290
427
  query_specific = query_set - context_words
428
+ if broad_matching and not query_specific and query_set and query_set <= trigger_set:
429
+ return 50, specificity, matched, "trigger_context"
291
430
  if len(query_specific) >= 2 and _contains_tokens(trigger_tokens, query_tokens):
292
431
  return 70, specificity, matched, "trigger_query_complete"
293
432
 
@@ -296,6 +435,10 @@ def _score_trigger(
296
435
  coverage = len(covered) / len(coverage_tokens)
297
436
  if len(covered) >= 2 and coverage >= 0.67:
298
437
  return round(40 * coverage), specificity, matched, "trigger_partial"
438
+ matched_specific_query = matched & (query_set - context_words)
439
+ matched_context = matched & context_words
440
+ if broad_matching and matched_specific_query and matched_context:
441
+ return 35, specificity, matched, "trigger_broad"
299
442
  return 0, specificity, set(), ""
300
443
 
301
444
 
@@ -309,6 +452,8 @@ def _score_skill(
309
452
  stop_words = policy["stop_words"]
310
453
  context_words = policy["context_words"]
311
454
  abbreviations = policy["abbreviations"]
455
+ token_aliases = policy["token_aliases"]
456
+ broad_matching = policy["broad_matching"]
312
457
  query_set = set(query_tokens)
313
458
  specific_query = query_set - context_words
314
459
  context_query = query_set & context_words
@@ -317,7 +462,8 @@ def _score_skill(
317
462
  strongest_trigger = (0, 0, set(), "", "")
318
463
  for trigger in _trigger_values(triggers, skill_id, product_id):
319
464
  score, specificity, matched, evidence = _score_trigger(
320
- query_tokens, trigger, stop_words, context_words, abbreviations
465
+ query_tokens, trigger, stop_words, context_words, abbreviations,
466
+ token_aliases, broad_matching
321
467
  )
322
468
  candidate = (score, specificity, matched, evidence, normalize_text(trigger))
323
469
  candidate_key = (score, specificity, len(matched), normalize_text(trigger))
@@ -335,7 +481,7 @@ def _score_skill(
335
481
  matched_tokens = set(trigger_matched)
336
482
 
337
483
  name_tokens = _normalize_scoring_tokens(
338
- skill.get("name", ""), stop_words, abbreviations
484
+ skill.get("name", ""), stop_words, abbreviations, token_aliases
339
485
  )
340
486
  name_set = set(name_tokens)
341
487
  identity_score = 0
@@ -352,7 +498,9 @@ def _score_skill(
352
498
  evidence.append(f"name_context:{token}")
353
499
 
354
500
  category_set = set(
355
- _normalize_scoring_tokens(_category_text(skill), stop_words, abbreviations)
501
+ _normalize_scoring_tokens(
502
+ _category_text(skill), stop_words, abbreviations, token_aliases
503
+ )
356
504
  )
357
505
  category_score = 0
358
506
  for token in sorted(specific_query & category_set):
@@ -365,7 +513,9 @@ def _score_skill(
365
513
  evidence.append(f"category_context:{token}")
366
514
 
367
515
  description_set = set(
368
- _normalize_scoring_tokens(skill.get("description", ""), stop_words, abbreviations)
516
+ _normalize_scoring_tokens(
517
+ skill.get("description", ""), stop_words, abbreviations, token_aliases
518
+ )
369
519
  )
370
520
  description_matches = sorted(specific_query & description_set)
371
521
  description_score = len(description_matches)
@@ -457,7 +607,28 @@ def match_skills(
457
607
 
458
608
  raw_tokens = _tokens(query)
459
609
  expanded_tokens = _expand_abbreviations(raw_tokens, configured_abbreviations)
460
- query_tokens = tuple(token for token in expanded_tokens if token not in policy["stop_words"])
610
+ aliased_tokens = tuple(
611
+ policy["token_aliases"].get(token, token) for token in expanded_tokens
612
+ )
613
+ initial_query_tokens = tuple(
614
+ token for token in aliased_tokens if token not in policy["stop_words"]
615
+ )
616
+
617
+ scoped_skills = [skill for skill in skills if _belongs_to_product(skill, product_id)]
618
+ if policy["broad_matching"]:
619
+ token_index, vocabulary = _build_token_index(
620
+ scoped_skills, triggers, product_id, policy
621
+ )
622
+ repaired_tokens, fuzzy_diagnostics = _repair_query_tokens(
623
+ initial_query_tokens, token_index, vocabulary
624
+ )
625
+ else:
626
+ repaired_tokens, fuzzy_diagnostics = initial_query_tokens, []
627
+ query_tokens = tuple(
628
+ policy["token_aliases"].get(token, token) for token in repaired_tokens
629
+ if policy["token_aliases"].get(token, token) not in policy["stop_words"]
630
+ )
631
+ diagnostics.extend(fuzzy_diagnostics)
461
632
  query_normalized = " ".join(query_tokens)
462
633
 
463
634
  requested_count = policy["default_result_count"] if result_count is None else result_count
@@ -465,7 +636,6 @@ def match_skills(
465
636
  requested_count = policy["default_result_count"]
466
637
  requested_count = max(1, min(requested_count, policy["maximum_result_count"]))
467
638
 
468
- scoped_skills = [skill for skill in skills if _belongs_to_product(skill, product_id)]
469
639
  if len(scoped_skills) != len(skills):
470
640
  diagnostics.append("product_mismatches_filtered")
471
641