@tasksai/install 0.1.30 → 0.1.31

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.31",
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,44 @@ 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
+ DEFAULT_TOKEN_ALIASES = {
66
+ "realtor": {
67
+ "assemble": "create",
68
+ "build": "create",
69
+ "develop": "create",
70
+ "draft": "create",
71
+ "dwelling": "property",
72
+ "generate": "create",
73
+ "home": "property",
74
+ "house": "property",
75
+ "listed": "listing",
76
+ "listings": "listing",
77
+ "list": "listing",
78
+ "make": "create",
79
+ "prepare": "create",
80
+ "prepared": "create",
81
+ "preparing": "create",
82
+ "preparation": "create",
83
+ "produce": "create",
84
+ "residence": "property",
85
+ "residential": "property",
86
+ "write": "create",
87
+ "writing": "create",
88
+ },
89
+ }
60
90
  TRIGGER_EVIDENCE_RANK = {
61
- "trigger_exact": 4,
91
+ "trigger_exact": 5,
92
+ "trigger_equivalent": 4,
62
93
  "trigger_complete": 3,
63
94
  "trigger_query_complete": 2,
64
- "trigger_partial": 1,
95
+ "trigger_partial": 2,
96
+ "trigger_broad": 1,
97
+ "trigger_context": 1,
65
98
  }
66
99
 
67
100
 
@@ -149,9 +182,95 @@ def _normalize_scoring_tokens(
149
182
  value: object,
150
183
  stop_words: set[str],
151
184
  abbreviations: Mapping[str, object],
185
+ token_aliases: Mapping[str, str],
152
186
  ) -> tuple[str, ...]:
153
187
  expanded = _expand_abbreviations(_tokens(value), abbreviations)
154
- return tuple(token for token in expanded if token not in stop_words)
188
+ canonical = tuple(token_aliases.get(token, token) for token in expanded)
189
+ return tuple(token for token in canonical if token not in stop_words)
190
+
191
+
192
+ def _same_token_bag(left: tuple[str, ...], right: tuple[str, ...]) -> bool:
193
+ return bool(left) and len(left) == len(right) and sorted(left) == sorted(right)
194
+
195
+
196
+ def _within_one_edit(left: str, right: str) -> bool:
197
+ """Return True for one insertion, deletion, substitution, or transposition."""
198
+ if left == right or abs(len(left) - len(right)) > 1:
199
+ return False
200
+ if len(left) == len(right):
201
+ differences = [index for index, pair in enumerate(zip(left, right)) if pair[0] != pair[1]]
202
+ if len(differences) == 1:
203
+ return True
204
+ return (
205
+ len(differences) == 2
206
+ and differences[1] == differences[0] + 1
207
+ and left[differences[0]] == right[differences[1]]
208
+ and left[differences[1]] == right[differences[0]]
209
+ )
210
+ shorter, longer = (left, right) if len(left) < len(right) else (right, left)
211
+ short_index = long_index = differences = 0
212
+ while short_index < len(shorter) and long_index < len(longer):
213
+ if shorter[short_index] == longer[long_index]:
214
+ short_index += 1
215
+ long_index += 1
216
+ continue
217
+ differences += 1
218
+ long_index += 1
219
+ if differences > 1:
220
+ return False
221
+ return True
222
+
223
+
224
+ def _build_token_index(
225
+ skills: Sequence[Mapping[str, object]],
226
+ triggers: Mapping[str, object],
227
+ product_id: str,
228
+ policy: Mapping[str, object],
229
+ ) -> tuple[dict[tuple[int, str], set[str]], set[str]]:
230
+ """Index public discovery tokens by length and first character for bounded typo repair."""
231
+ stop_words = policy["stop_words"]
232
+ abbreviations = policy["abbreviations"]
233
+ token_aliases = policy["token_aliases"]
234
+ vocabulary = set(token_aliases) | set(token_aliases.values())
235
+ for skill in skills:
236
+ for value in (skill.get("name", ""), _category_text(skill)):
237
+ vocabulary.update(
238
+ _normalize_scoring_tokens(value, stop_words, abbreviations, token_aliases)
239
+ )
240
+ skill_id = str(skill.get("id", ""))
241
+ for trigger in _trigger_values(triggers, skill_id, product_id):
242
+ vocabulary.update(
243
+ _normalize_scoring_tokens(trigger, stop_words, abbreviations, token_aliases)
244
+ )
245
+ index: dict[tuple[int, str], set[str]] = {}
246
+ for token in vocabulary:
247
+ if len(token) < 5:
248
+ continue
249
+ index.setdefault((len(token), token[0]), set()).add(token)
250
+ return index, vocabulary
251
+
252
+
253
+ def _repair_query_tokens(
254
+ tokens: tuple[str, ...],
255
+ token_index: Mapping[tuple[int, str], set[str]],
256
+ vocabulary: set[str],
257
+ ) -> tuple[tuple[str, ...], list[str]]:
258
+ repaired: list[str] = []
259
+ diagnostics: list[str] = []
260
+ for token in tokens:
261
+ if len(token) < 5 or token in vocabulary:
262
+ repaired.append(token)
263
+ continue
264
+ candidates: set[str] = set()
265
+ for length in range(len(token) - 1, len(token) + 2):
266
+ candidates.update(token_index.get((length, token[0]), set()))
267
+ matches = sorted(candidate for candidate in candidates if _within_one_edit(token, candidate))
268
+ if len(matches) == 1:
269
+ repaired.append(matches[0])
270
+ diagnostics.append(f"fuzzy_corrected:{token}->{matches[0]}")
271
+ else:
272
+ repaired.append(token)
273
+ return tuple(repaired), diagnostics
155
274
 
156
275
 
157
276
  def _contains_tokens(container: tuple[str, ...], candidate: tuple[str, ...]) -> bool:
@@ -222,6 +341,8 @@ def _resolve_config(product_id: str, config: Mapping[str, object] | None) -> tup
222
341
  for value in supplied_stop_words
223
342
  for token in _tokens(value)
224
343
  }
344
+ if _canonical_product_id(product_id) == "realtor":
345
+ stop_words.difference_update(REALTOR_ACTION_WORDS)
225
346
  context_default = DEFAULT_CONTEXT_WORDS.get(_canonical_product_id(product_id), set())
226
347
  supplied_context_words = source.get("context_words", context_default)
227
348
  if not isinstance(supplied_context_words, Sequence) or isinstance(supplied_context_words, (str, bytes)):
@@ -242,10 +363,13 @@ def _resolve_config(product_id: str, config: Mapping[str, object] | None) -> tup
242
363
  diagnostics.append("bundled_thresholds:invalid_order")
243
364
  thresholds = dict(DEFAULT_THRESHOLDS)
244
365
 
245
- default_count = source.get("default_result_count", DEFAULT_RESULT_COUNT)
366
+ product_default_count = DEFAULT_RESULT_COUNTS.get(
367
+ _canonical_product_id(product_id), DEFAULT_RESULT_COUNT
368
+ )
369
+ default_count = source.get("default_result_count", product_default_count)
246
370
  maximum_count = source.get("maximum_result_count", MAXIMUM_RESULT_COUNT)
247
371
  if not isinstance(default_count, int) or isinstance(default_count, bool) or default_count < 1:
248
- default_count = DEFAULT_RESULT_COUNT
372
+ default_count = product_default_count
249
373
  if not isinstance(maximum_count, int) or isinstance(maximum_count, bool) or maximum_count < 1:
250
374
  maximum_count = MAXIMUM_RESULT_COUNT
251
375
  maximum_count = min(maximum_count, MAXIMUM_RESULT_COUNT)
@@ -262,6 +386,8 @@ def _resolve_config(product_id: str, config: Mapping[str, object] | None) -> tup
262
386
  "default_result_count": default_count,
263
387
  "maximum_result_count": maximum_count,
264
388
  "abbreviations": configured_abbreviations,
389
+ "token_aliases": DEFAULT_TOKEN_ALIASES.get(_canonical_product_id(product_id), {}),
390
+ "broad_matching": _canonical_product_id(product_id) == "realtor",
265
391
  }, diagnostics
266
392
 
267
393
 
@@ -271,8 +397,12 @@ def _score_trigger(
271
397
  stop_words: set[str],
272
398
  context_words: set[str],
273
399
  abbreviations: Mapping[str, object],
400
+ token_aliases: Mapping[str, str],
401
+ broad_matching: bool,
274
402
  ) -> tuple[int, int, set[str], str]:
275
- trigger_tokens = _normalize_scoring_tokens(trigger, stop_words, abbreviations)
403
+ trigger_tokens = _normalize_scoring_tokens(
404
+ trigger, stop_words, abbreviations, token_aliases
405
+ )
276
406
  if not trigger_tokens:
277
407
  return 0, 0, set(), ""
278
408
 
@@ -284,10 +414,14 @@ def _score_trigger(
284
414
 
285
415
  if query_tokens == trigger_tokens:
286
416
  return 120, specificity, matched, "trigger_exact"
417
+ if broad_matching and _same_token_bag(query_tokens, trigger_tokens):
418
+ return 110, specificity, matched, "trigger_equivalent"
287
419
  if _contains_tokens(query_tokens, trigger_tokens):
288
420
  bonus = min(15, max(0, specificity - 1) * 3)
289
421
  return 90 + bonus, specificity, matched, "trigger_complete"
290
422
  query_specific = query_set - context_words
423
+ if broad_matching and not query_specific and query_set and query_set <= trigger_set:
424
+ return 50, specificity, matched, "trigger_context"
291
425
  if len(query_specific) >= 2 and _contains_tokens(trigger_tokens, query_tokens):
292
426
  return 70, specificity, matched, "trigger_query_complete"
293
427
 
@@ -296,6 +430,10 @@ def _score_trigger(
296
430
  coverage = len(covered) / len(coverage_tokens)
297
431
  if len(covered) >= 2 and coverage >= 0.67:
298
432
  return round(40 * coverage), specificity, matched, "trigger_partial"
433
+ matched_specific_query = matched & (query_set - context_words)
434
+ matched_context = matched & context_words
435
+ if broad_matching and matched_specific_query and matched_context:
436
+ return 35, specificity, matched, "trigger_broad"
299
437
  return 0, specificity, set(), ""
300
438
 
301
439
 
@@ -309,6 +447,8 @@ def _score_skill(
309
447
  stop_words = policy["stop_words"]
310
448
  context_words = policy["context_words"]
311
449
  abbreviations = policy["abbreviations"]
450
+ token_aliases = policy["token_aliases"]
451
+ broad_matching = policy["broad_matching"]
312
452
  query_set = set(query_tokens)
313
453
  specific_query = query_set - context_words
314
454
  context_query = query_set & context_words
@@ -317,7 +457,8 @@ def _score_skill(
317
457
  strongest_trigger = (0, 0, set(), "", "")
318
458
  for trigger in _trigger_values(triggers, skill_id, product_id):
319
459
  score, specificity, matched, evidence = _score_trigger(
320
- query_tokens, trigger, stop_words, context_words, abbreviations
460
+ query_tokens, trigger, stop_words, context_words, abbreviations,
461
+ token_aliases, broad_matching
321
462
  )
322
463
  candidate = (score, specificity, matched, evidence, normalize_text(trigger))
323
464
  candidate_key = (score, specificity, len(matched), normalize_text(trigger))
@@ -335,7 +476,7 @@ def _score_skill(
335
476
  matched_tokens = set(trigger_matched)
336
477
 
337
478
  name_tokens = _normalize_scoring_tokens(
338
- skill.get("name", ""), stop_words, abbreviations
479
+ skill.get("name", ""), stop_words, abbreviations, token_aliases
339
480
  )
340
481
  name_set = set(name_tokens)
341
482
  identity_score = 0
@@ -352,7 +493,9 @@ def _score_skill(
352
493
  evidence.append(f"name_context:{token}")
353
494
 
354
495
  category_set = set(
355
- _normalize_scoring_tokens(_category_text(skill), stop_words, abbreviations)
496
+ _normalize_scoring_tokens(
497
+ _category_text(skill), stop_words, abbreviations, token_aliases
498
+ )
356
499
  )
357
500
  category_score = 0
358
501
  for token in sorted(specific_query & category_set):
@@ -365,7 +508,9 @@ def _score_skill(
365
508
  evidence.append(f"category_context:{token}")
366
509
 
367
510
  description_set = set(
368
- _normalize_scoring_tokens(skill.get("description", ""), stop_words, abbreviations)
511
+ _normalize_scoring_tokens(
512
+ skill.get("description", ""), stop_words, abbreviations, token_aliases
513
+ )
369
514
  )
370
515
  description_matches = sorted(specific_query & description_set)
371
516
  description_score = len(description_matches)
@@ -457,7 +602,28 @@ def match_skills(
457
602
 
458
603
  raw_tokens = _tokens(query)
459
604
  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"])
605
+ aliased_tokens = tuple(
606
+ policy["token_aliases"].get(token, token) for token in expanded_tokens
607
+ )
608
+ initial_query_tokens = tuple(
609
+ token for token in aliased_tokens if token not in policy["stop_words"]
610
+ )
611
+
612
+ scoped_skills = [skill for skill in skills if _belongs_to_product(skill, product_id)]
613
+ if policy["broad_matching"]:
614
+ token_index, vocabulary = _build_token_index(
615
+ scoped_skills, triggers, product_id, policy
616
+ )
617
+ repaired_tokens, fuzzy_diagnostics = _repair_query_tokens(
618
+ initial_query_tokens, token_index, vocabulary
619
+ )
620
+ else:
621
+ repaired_tokens, fuzzy_diagnostics = initial_query_tokens, []
622
+ query_tokens = tuple(
623
+ policy["token_aliases"].get(token, token) for token in repaired_tokens
624
+ if policy["token_aliases"].get(token, token) not in policy["stop_words"]
625
+ )
626
+ diagnostics.extend(fuzzy_diagnostics)
461
627
  query_normalized = " ".join(query_tokens)
462
628
 
463
629
  requested_count = policy["default_result_count"] if result_count is None else result_count
@@ -465,7 +631,6 @@ def match_skills(
465
631
  requested_count = policy["default_result_count"]
466
632
  requested_count = max(1, min(requested_count, policy["maximum_result_count"]))
467
633
 
468
- scoped_skills = [skill for skill in skills if _belongs_to_product(skill, product_id)]
469
634
  if len(scoped_skills) != len(skills):
470
635
  diagnostics.append("product_mismatches_filtered")
471
636