induscode 0.1.0__py3-none-any.whl
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.
- induscode/__init__.py +56 -0
- induscode/addons/__init__.py +176 -0
- induscode/addons/contract.py +923 -0
- induscode/addons/dispatch/__init__.py +43 -0
- induscode/addons/dispatch/event_dispatcher.py +348 -0
- induscode/addons/dispatch/tool_interceptor.py +349 -0
- induscode/addons/host.py +469 -0
- induscode/addons/loader.py +314 -0
- induscode/addons/manifest.py +232 -0
- induscode/addons/surface.py +199 -0
- induscode/boot/__init__.py +108 -0
- induscode/boot/auth_vault.py +323 -0
- induscode/boot/boot.py +210 -0
- induscode/boot/contract.py +223 -0
- induscode/boot/invocation.py +117 -0
- induscode/boot/runners/__init__.py +42 -0
- induscode/boot/runners/link_runner.py +82 -0
- induscode/boot/runners/oneshot_runner.py +85 -0
- induscode/boot/runners/registry.py +46 -0
- induscode/boot/runners/repl_runner.py +340 -0
- induscode/boot/runners/session.py +549 -0
- induscode/boot/stages.py +198 -0
- induscode/boot/upgrade/__init__.py +36 -0
- induscode/boot/upgrade/apply.py +125 -0
- induscode/boot/upgrade/upgrades.py +136 -0
- induscode/briefing/__init__.py +115 -0
- induscode/briefing/compose.py +414 -0
- induscode/briefing/contract.py +528 -0
- induscode/briefing/macros.py +721 -0
- induscode/briefing/skills.py +417 -0
- induscode/capability_deck/__init__.py +233 -0
- induscode/capability_deck/bridge_ledger/__init__.py +66 -0
- induscode/capability_deck/bridge_ledger/key.py +181 -0
- induscode/capability_deck/bridge_ledger/ledger.py +276 -0
- induscode/capability_deck/bridge_ledger/network.py +336 -0
- induscode/capability_deck/builtin_bridge.py +358 -0
- induscode/capability_deck/cards/__init__.py +116 -0
- induscode/capability_deck/cards/bg_process.py +482 -0
- induscode/capability_deck/cards/memory.py +226 -0
- induscode/capability_deck/cards/saas.py +280 -0
- induscode/capability_deck/cards/task.py +256 -0
- induscode/capability_deck/cards/todo.py +312 -0
- induscode/capability_deck/contract.py +450 -0
- induscode/capability_deck/manifest.py +126 -0
- induscode/capability_deck/provision.py +217 -0
- induscode/channels/__init__.py +146 -0
- induscode/channels/contract.py +585 -0
- induscode/channels/framer.py +132 -0
- induscode/channels/link/__init__.py +50 -0
- induscode/channels/link/dialog.py +246 -0
- induscode/channels/link/driver.py +308 -0
- induscode/channels/link/server.py +217 -0
- induscode/channels/oneshot.py +178 -0
- induscode/channels/ops.py +140 -0
- induscode/channels/session_ops.py +172 -0
- induscode/conductor/__init__.py +240 -0
- induscode/conductor/catalog.py +309 -0
- induscode/conductor/conductor.py +1084 -0
- induscode/conductor/contract.py +1035 -0
- induscode/conductor/matcher.py +291 -0
- induscode/conductor/serialize.py +575 -0
- induscode/conductor/signal_hub.py +382 -0
- induscode/conductor/skill_parse.py +294 -0
- induscode/conductor/transcript_store.py +449 -0
- induscode/console/__init__.py +236 -0
- induscode/console/app.py +1677 -0
- induscode/console/components/__init__.py +62 -0
- induscode/console/components/banner.py +499 -0
- induscode/console/components/banner_sweep.py +188 -0
- induscode/console/components/emblem.py +181 -0
- induscode/console/components/status_bar.py +102 -0
- induscode/console/contract.py +836 -0
- induscode/console/input/__init__.py +107 -0
- induscode/console/input/chord.py +197 -0
- induscode/console/input/dir_reader.py +113 -0
- induscode/console/input/intents.py +258 -0
- induscode/console/input/providers.py +469 -0
- induscode/console/mount.py +137 -0
- induscode/console/overlays/__init__.py +94 -0
- induscode/console/overlays/auth.py +503 -0
- induscode/console/overlays/pickers.py +526 -0
- induscode/console/overlays/router.py +129 -0
- induscode/console/overlays/sessions.py +232 -0
- induscode/console/reducer.py +145 -0
- induscode/console/resume_picker.py +156 -0
- induscode/console/slash_commands/__init__.py +78 -0
- induscode/console/slash_commands/builtins.py +254 -0
- induscode/console/slash_commands/dynamic.py +217 -0
- induscode/console/slash_commands/integrations.py +949 -0
- induscode/console/slash_commands/transcript.py +404 -0
- induscode/console/slash_commands/workbench.py +430 -0
- induscode/console/startup.py +434 -0
- induscode/console/theme/__init__.py +44 -0
- induscode/console/theme/adapter.py +168 -0
- induscode/console/theme/palette.py +128 -0
- induscode/console/theme/resolve.py +123 -0
- induscode/console/theme/tokens.py +185 -0
- induscode/console_slash/__init__.py +111 -0
- induscode/console_slash/contract.py +185 -0
- induscode/console_slash/registry.py +140 -0
- induscode/console_slash/resolve.py +194 -0
- induscode/console_slash/shared.py +172 -0
- induscode/entry.py +108 -0
- induscode/insight/__init__.py +153 -0
- induscode/insight/collector.py +73 -0
- induscode/insight/replay.py +305 -0
- induscode/insight/wrapper.py +1115 -0
- induscode/kit/__init__.py +82 -0
- induscode/kit/clipboard_image.py +215 -0
- induscode/kit/external_editor.py +120 -0
- induscode/kit/image.py +188 -0
- induscode/kit/shell.py +89 -0
- induscode/kit/tool_fetch.py +288 -0
- induscode/launch/__init__.py +224 -0
- induscode/launch/catalog.py +310 -0
- induscode/launch/contract.py +569 -0
- induscode/launch/credentials.py +852 -0
- induscode/launch/invocation/__init__.py +39 -0
- induscode/launch/invocation/attachments.py +281 -0
- induscode/launch/invocation/flags.py +210 -0
- induscode/launch/invocation/read.py +369 -0
- induscode/launch/invocation/usage.py +110 -0
- induscode/launch/oauth.py +808 -0
- induscode/launch/packages.py +299 -0
- induscode/launch/pickers.py +291 -0
- induscode/py.typed +0 -0
- induscode/runtime_bridge/__init__.py +166 -0
- induscode/runtime_bridge/bridges/__init__.py +66 -0
- induscode/runtime_bridge/bridges/_drive.py +268 -0
- induscode/runtime_bridge/bridges/builtins.py +177 -0
- induscode/runtime_bridge/bridges/claude_cli.py +198 -0
- induscode/runtime_bridge/bridges/codex_cli.py +203 -0
- induscode/runtime_bridge/bridges/indusagi_cli.py +217 -0
- induscode/runtime_bridge/broker.py +397 -0
- induscode/runtime_bridge/contract.py +734 -0
- induscode/runtime_bridge/sink.py +351 -0
- induscode/sessions/__init__.py +25 -0
- induscode/sessions/contract.py +119 -0
- induscode/sessions/library.py +350 -0
- induscode/settings/__init__.py +47 -0
- induscode/settings/contract.py +313 -0
- induscode/settings/manager.py +268 -0
- induscode/transcript_export/__init__.py +109 -0
- induscode/transcript_export/contract.py +522 -0
- induscode/transcript_export/publish.py +455 -0
- induscode/transcript_export/sgr.py +566 -0
- induscode/transcript_export/template.py +319 -0
- induscode/transcript_export/theme_bridge.py +325 -0
- induscode/window_budget/__init__.py +76 -0
- induscode/window_budget/budget/__init__.py +26 -0
- induscode/window_budget/budget/estimate.py +273 -0
- induscode/window_budget/budget/gate.py +60 -0
- induscode/window_budget/budget/slice.py +145 -0
- induscode/window_budget/condenser.py +170 -0
- induscode/window_budget/contract.py +329 -0
- induscode/window_budget/summarize/__init__.py +33 -0
- induscode/window_budget/summarize/condense.py +212 -0
- induscode/window_budget/summarize/prompt.py +241 -0
- induscode/workspace/__init__.py +30 -0
- induscode/workspace/brand.py +96 -0
- induscode/workspace/locator.py +269 -0
- induscode-0.1.0.dist-info/METADATA +97 -0
- induscode-0.1.0.dist-info/RECORD +167 -0
- induscode-0.1.0.dist-info/WHEEL +4 -0
- induscode-0.1.0.dist-info/entry_points.txt +3 -0
- induscode-0.1.0.dist-info/licenses/CREDITS.md +22 -0
- induscode-0.1.0.dist-info/licenses/NOTICE +7 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Model matcher — :class:`ModelMatcher` (port of TS ``src/conductor/catalog/matcher.ts``).
|
|
2
|
+
|
|
3
|
+
A scored-candidate resolver over a :class:`~induscode.conductor.catalog.ModelCatalog`.
|
|
4
|
+
Given a free-form query (a string, or a structured
|
|
5
|
+
:class:`~induscode.conductor.contract.MatchQuery`), it produces the single
|
|
6
|
+
best-matching :class:`~induscode.conductor.contract.ModelCardRef`, or ``None``
|
|
7
|
+
when nothing clears the bar.
|
|
8
|
+
|
|
9
|
+
Resolution is a *prioritized scoring pipeline*, not a chain of single-purpose
|
|
10
|
+
match helpers: each candidate card is scored against the query by a set of
|
|
11
|
+
independent strategy probes (exact-canonical, exact-id, provider-prefix,
|
|
12
|
+
alias-substring, glob), the capability filters gate the field, and the highest
|
|
13
|
+
scorer (with deterministic tie-breaks) wins.
|
|
14
|
+
|
|
15
|
+
Port notes
|
|
16
|
+
----------
|
|
17
|
+
- The TS glob compiler is ported character-by-character (an index loop, the
|
|
18
|
+
same ``[^/]`` classes, the same escape set) and memoized via
|
|
19
|
+
:func:`functools.lru_cache` in place of the TS module-level ``Map`` cache.
|
|
20
|
+
- Scoring weights and the tie bonus are VERBATIM:
|
|
21
|
+
pin 1000 / exact-canonical 900 / exact-id 800 / provider 600 / glob 400 /
|
|
22
|
+
substring 200, plus ``max(0, 40 - min(|len(id) - len(needle)|, 40))`` and a
|
|
23
|
+
``+3`` reasoning nudge.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import re
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from functools import lru_cache
|
|
31
|
+
from typing import Union
|
|
32
|
+
|
|
33
|
+
from induscode.conductor.contract import MatchQuery, ModelCardRef
|
|
34
|
+
|
|
35
|
+
from induscode.conductor.catalog import CatalogCard, ModelCatalog, to_card_ref
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"ModelMatcher",
|
|
39
|
+
"ResolveInput",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Query shape
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
#: Either a raw selector string or the structured ``MatchQuery``.
|
|
48
|
+
ResolveInput = Union[str, MatchQuery]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _lift_query(input: ResolveInput) -> MatchQuery:
|
|
52
|
+
"""Lift a bare selector string into a ``MatchQuery``."""
|
|
53
|
+
return MatchQuery(pattern=input) if isinstance(input, str) else input
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Scoring scale
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
# Strategy weights, highest-confidence first. A candidate's score is the single
|
|
61
|
+
# best strategy that fires (strategies do not stack), so the order here defines
|
|
62
|
+
# the resolution priority. The gap between tiers is wide enough that a weaker
|
|
63
|
+
# probe never out-ranks a stronger one regardless of secondary bonuses.
|
|
64
|
+
_SCALE_PINNED_PAIR = 1000 # explicit provider + modelId pin
|
|
65
|
+
_SCALE_EXACT_CANONICAL = 900 # pattern == "provider/modelId"
|
|
66
|
+
_SCALE_EXACT_MODEL_ID = 800 # pattern == modelId (provider-scoped id)
|
|
67
|
+
_SCALE_PROVIDER_SCOPED = 600 # pattern == "provider/" or provider name
|
|
68
|
+
_SCALE_GLOB_PATTERN = 400 # pattern is a glob that matches the id
|
|
69
|
+
_SCALE_ALIAS_SUBSTRING = 200 # pattern is a case-folded substring of id/name
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True, slots=True)
|
|
73
|
+
class _ScoredCard:
|
|
74
|
+
"""A scored candidate awaiting tie-break (TS ``ScoredCard``)."""
|
|
75
|
+
|
|
76
|
+
card: CatalogCard
|
|
77
|
+
score: int
|
|
78
|
+
bonus: int
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Capability gate
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _clears_constraints(card: CatalogCard, q: MatchQuery) -> bool:
|
|
87
|
+
"""Reject a card that fails a hard capability or provider constraint.
|
|
88
|
+
|
|
89
|
+
These are gates, not scores: a card that fails any constraint is removed
|
|
90
|
+
from the field entirely rather than merely down-ranked.
|
|
91
|
+
"""
|
|
92
|
+
if q.provider is not None and card.provider != str(q.provider):
|
|
93
|
+
return False
|
|
94
|
+
if q.modelId is not None and card.modelId != q.modelId:
|
|
95
|
+
return False
|
|
96
|
+
if q.reasoning is True and not card.reasoning:
|
|
97
|
+
return False
|
|
98
|
+
if q.supportsImageInput is True and not card.acceptsImages:
|
|
99
|
+
return False
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# Glob compiler (standard technique — no fnmatch dependency, ported verbatim)
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
_GLOBBY = re.compile(r"[*?\[\]]")
|
|
108
|
+
|
|
109
|
+
# The TS literal-character escape set: /[.+^${}()|\\]/
|
|
110
|
+
_NEEDS_ESCAPE = frozenset(".+^${}()|\\")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _looks_globby(pattern: str) -> bool:
|
|
114
|
+
"""True when a selector contains glob metacharacters worth compiling."""
|
|
115
|
+
return _GLOBBY.search(pattern) is not None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@lru_cache(maxsize=None)
|
|
119
|
+
def _compile_glob(pattern: str) -> re.Pattern[str] | None:
|
|
120
|
+
"""Compile a shell-style glob (``*``, ``?``, ``[set]``) to an anchored,
|
|
121
|
+
case-folded regex. Compilation is memoized. A ``/`` in the pattern is
|
|
122
|
+
matched literally, so ``anthropic/*`` scopes to a provider. Returns
|
|
123
|
+
``None`` for an uncompilable pattern.
|
|
124
|
+
|
|
125
|
+
Ported char-by-char from the TS compiler (index loop, same classes).
|
|
126
|
+
"""
|
|
127
|
+
body = ""
|
|
128
|
+
i = 0
|
|
129
|
+
while i < len(pattern):
|
|
130
|
+
ch = pattern[i]
|
|
131
|
+
if ch == "*":
|
|
132
|
+
body += "[^/]*"
|
|
133
|
+
elif ch == "?":
|
|
134
|
+
body += "[^/]"
|
|
135
|
+
elif ch == "[":
|
|
136
|
+
close = pattern.find("]", i + 1)
|
|
137
|
+
if close == -1:
|
|
138
|
+
body += "\\["
|
|
139
|
+
else:
|
|
140
|
+
body += "[" + pattern[i + 1 : close].replace("\\", "\\\\") + "]"
|
|
141
|
+
i = close
|
|
142
|
+
else:
|
|
143
|
+
body += "\\" + ch if ch in _NEEDS_ESCAPE else ch
|
|
144
|
+
i += 1
|
|
145
|
+
try:
|
|
146
|
+
return re.compile(f"^{body}$", re.IGNORECASE)
|
|
147
|
+
except re.error:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
# Strategy probes (independent; one fires per card, the strongest wins)
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _probe_pattern(card: CatalogCard, pattern: str) -> int:
|
|
157
|
+
"""Probe a single card against the selector pattern, returning the best
|
|
158
|
+
strategy score (or 0 if no probe fires). The probes are tried
|
|
159
|
+
strongest-first and the first hit short-circuits."""
|
|
160
|
+
needle = pattern.strip()
|
|
161
|
+
if needle == "":
|
|
162
|
+
return 0
|
|
163
|
+
folded = needle.lower()
|
|
164
|
+
|
|
165
|
+
if card.id == needle:
|
|
166
|
+
return _SCALE_EXACT_CANONICAL
|
|
167
|
+
if card.modelId == needle:
|
|
168
|
+
return _SCALE_EXACT_MODEL_ID
|
|
169
|
+
|
|
170
|
+
# provider-scoped: "anthropic/" or the bare provider slug
|
|
171
|
+
provider_prefix = f"{card.provider}/"
|
|
172
|
+
if needle == provider_prefix or folded == card.provider.lower():
|
|
173
|
+
return _SCALE_PROVIDER_SCOPED
|
|
174
|
+
|
|
175
|
+
if _looks_globby(needle):
|
|
176
|
+
compiled = _compile_glob(needle)
|
|
177
|
+
if compiled is not None and (
|
|
178
|
+
compiled.match(card.id) is not None or compiled.match(card.modelId) is not None
|
|
179
|
+
):
|
|
180
|
+
return _SCALE_GLOB_PATTERN
|
|
181
|
+
|
|
182
|
+
if folded in card.id.lower() or folded in card.name.lower():
|
|
183
|
+
return _SCALE_ALIAS_SUBSTRING
|
|
184
|
+
|
|
185
|
+
return 0
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _tie_bonus(card: CatalogCard, pattern: str) -> int:
|
|
189
|
+
"""Secondary bonus for tie-breaking within a strategy tier. Rewards a card
|
|
190
|
+
whose id is *closer* to the pattern length (a tighter alias match), and
|
|
191
|
+
gently prefers reasoning-capable cards. Always small relative to a tier
|
|
192
|
+
gap."""
|
|
193
|
+
needle = pattern.strip().lower()
|
|
194
|
+
bonus = 0
|
|
195
|
+
if len(needle) > 0:
|
|
196
|
+
# tighter substring matches (less surrounding noise) rank higher
|
|
197
|
+
slack = abs(len(card.id) - len(needle))
|
|
198
|
+
bonus += max(0, 40 - min(slack, 40))
|
|
199
|
+
if card.reasoning:
|
|
200
|
+
bonus += 3
|
|
201
|
+
return bonus
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _rank_key(scored: _ScoredCard) -> tuple[int, int, str]:
|
|
205
|
+
"""Deterministic candidate ordering: higher strategy tier first, then
|
|
206
|
+
higher tie-bonus, then canonical id lexical order so equal scores never
|
|
207
|
+
depend on catalog iteration order (TS ``rankCandidates``)."""
|
|
208
|
+
return (-scored.score, -scored.bonus, scored.card.id)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
# ModelMatcher
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class ModelMatcher:
|
|
217
|
+
"""Resolves model queries against a ``ModelCatalog`` by scored candidates.
|
|
218
|
+
|
|
219
|
+
Construct once with a catalog, then call :meth:`resolve` per query. The
|
|
220
|
+
matcher holds no mutable state; it is safe to share.
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
__slots__ = ("_catalog",)
|
|
224
|
+
|
|
225
|
+
def __init__(self, catalog: ModelCatalog) -> None:
|
|
226
|
+
self._catalog = catalog
|
|
227
|
+
|
|
228
|
+
def resolve(self, input: ResolveInput) -> ModelCardRef | None:
|
|
229
|
+
"""Resolve a query to the single best ``ModelCardRef``, or ``None``.
|
|
230
|
+
|
|
231
|
+
Pipeline:
|
|
232
|
+
1. lift the input to a ``MatchQuery``;
|
|
233
|
+
2. honor an explicit ``provider``+``modelId`` *pin* (highest tier)
|
|
234
|
+
up front;
|
|
235
|
+
3. gate every card through the constraint filter;
|
|
236
|
+
4. with no pattern, return the leading surviving candidate (default
|
|
237
|
+
pick);
|
|
238
|
+
5. otherwise score each survivor (probe + tie bonus) and return the
|
|
239
|
+
top scorer (deterministic tie-break by tier, bonus, id).
|
|
240
|
+
"""
|
|
241
|
+
q = _lift_query(input)
|
|
242
|
+
card = self.resolve_card(q)
|
|
243
|
+
return to_card_ref(card) if card is not None else None
|
|
244
|
+
|
|
245
|
+
def resolve_card(self, input: ResolveInput) -> CatalogCard | None:
|
|
246
|
+
"""As :meth:`resolve`, but returns the full :class:`CatalogCard`."""
|
|
247
|
+
q = _lift_query(input)
|
|
248
|
+
|
|
249
|
+
# Tier 0: an explicit provider+modelId pin resolves directly.
|
|
250
|
+
if q.provider is not None and q.modelId is not None:
|
|
251
|
+
pinned = self._catalog.get(f"{q.provider}/{q.modelId}")
|
|
252
|
+
if pinned is not None and _clears_constraints(pinned, q):
|
|
253
|
+
return pinned
|
|
254
|
+
|
|
255
|
+
field = [c for c in self._catalog.all() if _clears_constraints(c, q)]
|
|
256
|
+
if len(field) == 0:
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
pattern = q.pattern.strip() if q.pattern is not None else None
|
|
260
|
+
if not pattern:
|
|
261
|
+
# No selector: the default candidate is the leading survivor,
|
|
262
|
+
# nudged toward a reasoning model when the field offers one.
|
|
263
|
+
return next((c for c in field if c.reasoning), field[0])
|
|
264
|
+
|
|
265
|
+
scored = [
|
|
266
|
+
_ScoredCard(card=c, score=score, bonus=_tie_bonus(c, pattern))
|
|
267
|
+
for c in field
|
|
268
|
+
if (score := _probe_pattern(c, pattern)) > 0
|
|
269
|
+
]
|
|
270
|
+
if len(scored) == 0:
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
scored.sort(key=_rank_key)
|
|
274
|
+
return scored[0].card
|
|
275
|
+
|
|
276
|
+
def resolve_all(self, input: ResolveInput) -> list[ModelCardRef]:
|
|
277
|
+
"""Resolve, returning *all* surviving candidates ranked best-first
|
|
278
|
+
(the field a "did you mean" picker would show). Useful for ambiguous
|
|
279
|
+
selectors."""
|
|
280
|
+
q = _lift_query(input)
|
|
281
|
+
field = [c for c in self._catalog.all() if _clears_constraints(c, q)]
|
|
282
|
+
pattern = q.pattern.strip() if q.pattern is not None else None
|
|
283
|
+
if not pattern:
|
|
284
|
+
return [to_card_ref(c) for c in field]
|
|
285
|
+
scored = [
|
|
286
|
+
_ScoredCard(card=c, score=score, bonus=_tie_bonus(c, pattern))
|
|
287
|
+
for c in field
|
|
288
|
+
if (score := _probe_pattern(c, pattern)) > 0
|
|
289
|
+
]
|
|
290
|
+
scored.sort(key=_rank_key)
|
|
291
|
+
return [to_card_ref(s.card) for s in scored]
|