its-magic 0.1.3-0 → 0.1.3-1
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 +1 -1
- package/scripts/check_intake_template_parity.py +62 -0
- package/template/.cursor/agents/curator.mdc +1 -0
- package/template/.cursor/agents/po.mdc +1 -0
- package/template/.cursor/agents/release.mdc +1 -0
- package/template/.cursor/model-catalog.local.example.cursor-only.json +9 -0
- package/template/.cursor/model-catalog.local.example.json +9 -0
- package/template/.cursor/model-catalog.local.example.level-1-easy.json +9 -0
- package/template/.cursor/model-catalog.local.example.level-2-complex.json +9 -0
- package/template/.cursor/model-catalog.local.example.level-3-mega.json +9 -0
- package/template/.cursor/model-catalog.local.example.level-4-super.json +9 -0
- package/template/.cursor/model-catalog.local.example.role-based-balanced.json +18 -0
- package/template/.cursor/model-catalog.local.example.role-based-highend.json +18 -0
- package/template/.cursor/scratchpad.local.example.md +55 -0
- package/template/.cursor/scratchpad.md +62 -0
- package/template/docs/engineering/runbook.md +197 -0
- package/template/scripts/check_intake_template_parity.py +62 -0
- package/template/scripts/model_tier_lib.py +680 -0
- package/template/scripts/model_tier_validate.py +368 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Model tier resolver library (US-0101 / DEC-0086; US-0102 / DEC-0087).
|
|
4
|
+
|
|
5
|
+
Provides:
|
|
6
|
+
- Tier→alias resolution (cheap→fast, balanced→inherit, strong→omit)
|
|
7
|
+
- Local catalog schema validation (v1 + v2)
|
|
8
|
+
- Unified 5-step precedence resolver (US-0102)
|
|
9
|
+
- Resolver algorithm with fail-closed reason codes
|
|
10
|
+
|
|
11
|
+
Reason codes (DEC-0086):
|
|
12
|
+
- MODEL_TIER_INVALID: unknown tier value
|
|
13
|
+
- MODEL_CATALOG_INVALID: malformed catalog JSON (v1)
|
|
14
|
+
- MODEL_SLUG_UNKNOWN: tier key missing from catalog
|
|
15
|
+
- MODEL_RESOLVE_FALLBACK: catalog lookup failed, using fallback
|
|
16
|
+
|
|
17
|
+
Reason codes (DEC-0087):
|
|
18
|
+
- MODEL_OVERRIDE_SLUG_UNKNOWN: direct slug validation failure
|
|
19
|
+
- MODEL_ROLE_SLUG_UNKNOWN: role catalog lookup miss (fall through)
|
|
20
|
+
- MODEL_CATALOG_SCHEMA_V2_INVALID: v2 schema validation failure
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import sys
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from enum import Enum
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Dict, Optional
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Tier(str, Enum):
|
|
32
|
+
"""Three operator-facing tiers for LLM model strength."""
|
|
33
|
+
CHEAP = "cheap"
|
|
34
|
+
BALANCED = "balanced"
|
|
35
|
+
STRONG = "strong"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ReasonCode(str, Enum):
|
|
39
|
+
"""Fail-closed reason codes for model tier resolution."""
|
|
40
|
+
MODEL_TIER_INVALID = "MODEL_TIER_INVALID"
|
|
41
|
+
MODEL_CATALOG_INVALID = "MODEL_CATALOG_INVALID"
|
|
42
|
+
MODEL_SLUG_UNKNOWN = "MODEL_SLUG_UNKNOWN"
|
|
43
|
+
MODEL_RESOLVE_FALLBACK = "MODEL_RESOLVE_FALLBACK"
|
|
44
|
+
MODEL_OVERRIDE_SLUG_UNKNOWN = "MODEL_OVERRIDE_SLUG_UNKNOWN"
|
|
45
|
+
MODEL_ROLE_SLUG_UNKNOWN = "MODEL_ROLE_SLUG_UNKNOWN"
|
|
46
|
+
MODEL_CATALOG_SCHEMA_V2_INVALID = "MODEL_CATALOG_SCHEMA_V2_INVALID"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Tier→Cursor alias mapping (DEC-0086 §2)
|
|
50
|
+
TIER_ALIAS_MAP = {
|
|
51
|
+
Tier.CHEAP: "fast",
|
|
52
|
+
Tier.BALANCED: "inherit",
|
|
53
|
+
Tier.STRONG: None, # omit model: field
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Default phase→tier matrix (DEC-0086 §4)
|
|
57
|
+
DEFAULT_PHASE_TIER_MATRIX = {
|
|
58
|
+
# cheap tier
|
|
59
|
+
"ask": Tier.CHEAP,
|
|
60
|
+
"refresh-context": Tier.CHEAP,
|
|
61
|
+
"memory-audit": Tier.CHEAP,
|
|
62
|
+
"status-reconcile": Tier.CHEAP,
|
|
63
|
+
"pause": Tier.CHEAP,
|
|
64
|
+
# balanced tier
|
|
65
|
+
"intake": Tier.BALANCED,
|
|
66
|
+
"discovery": Tier.BALANCED,
|
|
67
|
+
"research": Tier.BALANCED,
|
|
68
|
+
"release": Tier.BALANCED,
|
|
69
|
+
"plan-verify": Tier.BALANCED,
|
|
70
|
+
# strong tier
|
|
71
|
+
"architecture": Tier.STRONG,
|
|
72
|
+
"execute": Tier.STRONG,
|
|
73
|
+
"quick": Tier.STRONG,
|
|
74
|
+
"qa": Tier.STRONG,
|
|
75
|
+
"verify-work": Tier.STRONG,
|
|
76
|
+
"security-review": Tier.STRONG,
|
|
77
|
+
# auto inherits parent (no tier override)
|
|
78
|
+
"auto": None,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Canonical phase ids for MODEL_<PHASE> keys (DEC-0086 + DEC-0087)
|
|
82
|
+
CANONICAL_PHASE_IDS = frozenset(DEFAULT_PHASE_TIER_MATRIX.keys())
|
|
83
|
+
|
|
84
|
+
# Catalog v2 role keys (DEC-0087 §5)
|
|
85
|
+
CATALOG_ROLE_KEYS = frozenset({
|
|
86
|
+
"po", "sa", "dev", "dev_difficult", "qa", "security", "release",
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
# Phase→logical role (DEC-0051 + DEC-0087 §6)
|
|
90
|
+
PHASE_LOGICAL_ROLE: Dict[str, Optional[str]] = {
|
|
91
|
+
"intake": "po",
|
|
92
|
+
"discovery": "po",
|
|
93
|
+
"research": "tech-lead",
|
|
94
|
+
"architecture": "tech-lead",
|
|
95
|
+
"plan-verify": "qa",
|
|
96
|
+
"execute": "dev",
|
|
97
|
+
"quick": "dev",
|
|
98
|
+
"qa": "qa",
|
|
99
|
+
"verify-work": "qa",
|
|
100
|
+
"security-review": "security",
|
|
101
|
+
"release": "release",
|
|
102
|
+
"refresh-context": "curator",
|
|
103
|
+
"ask": "dev",
|
|
104
|
+
"memory-audit": "dev",
|
|
105
|
+
"status-reconcile": "dev",
|
|
106
|
+
"pause": "dev",
|
|
107
|
+
"auto": None,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Logical role → catalog roles key (DEC-0087 §6)
|
|
111
|
+
LOGICAL_ROLE_TO_CATALOG_KEY = {
|
|
112
|
+
"po": "po",
|
|
113
|
+
"tech-lead": "sa",
|
|
114
|
+
"dev": "dev",
|
|
115
|
+
"qa": "qa",
|
|
116
|
+
"security": "security",
|
|
117
|
+
"release": "release",
|
|
118
|
+
"curator": "dev",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# 5-step precedence chain labels (DEC-0087 §2) — for contract tests / docs
|
|
122
|
+
PRECEDENCE_CHAIN_STEPS = (
|
|
123
|
+
"MODEL_<PHASE>",
|
|
124
|
+
"MODEL_TIER_<PHASE>",
|
|
125
|
+
"role_catalog_lookup",
|
|
126
|
+
"MODEL_TIER_DEFAULT",
|
|
127
|
+
"cursor_alias",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def phase_to_model_key(phase_id: str) -> str:
|
|
132
|
+
"""Scratchpad key for direct slug override: MODEL_<PHASE>."""
|
|
133
|
+
return f"MODEL_{phase_id.upper()}"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def phase_to_tier_key(phase_id: str) -> str:
|
|
137
|
+
"""Scratchpad key for tier override: MODEL_TIER_<PHASE>."""
|
|
138
|
+
return f"MODEL_TIER_{phase_id.upper()}"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class ResolveResult:
|
|
143
|
+
"""Result of model tier resolution."""
|
|
144
|
+
success: bool
|
|
145
|
+
alias: Optional[str] # "fast", "inherit", or None (omit)
|
|
146
|
+
reason_code: Optional[ReasonCode]
|
|
147
|
+
reason_message: Optional[str]
|
|
148
|
+
tier: Optional[Tier] = None
|
|
149
|
+
slug: Optional[str] = None # vendor-specific slug from catalog or direct override
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def success_alias(cls, tier: Tier, alias: Optional[str]) -> "ResolveResult":
|
|
153
|
+
"""Successful resolution with alias."""
|
|
154
|
+
return cls(
|
|
155
|
+
success=True,
|
|
156
|
+
alias=alias,
|
|
157
|
+
reason_code=None,
|
|
158
|
+
reason_message=None,
|
|
159
|
+
tier=tier,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
@classmethod
|
|
163
|
+
def success_slug(cls, tier: Optional[Tier], slug: str) -> "ResolveResult":
|
|
164
|
+
"""Successful resolution with vendor slug."""
|
|
165
|
+
return cls(
|
|
166
|
+
success=True,
|
|
167
|
+
alias=None,
|
|
168
|
+
reason_code=None,
|
|
169
|
+
reason_message=None,
|
|
170
|
+
tier=tier,
|
|
171
|
+
slug=slug,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
def failure(cls, reason_code: ReasonCode, message: str) -> "ResolveResult":
|
|
176
|
+
"""Failed resolution with reason code."""
|
|
177
|
+
return cls(
|
|
178
|
+
success=False,
|
|
179
|
+
alias=None,
|
|
180
|
+
reason_code=reason_code,
|
|
181
|
+
reason_message=message,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
@classmethod
|
|
185
|
+
def fallthrough(
|
|
186
|
+
cls,
|
|
187
|
+
reason_code: ReasonCode,
|
|
188
|
+
message: str,
|
|
189
|
+
) -> "ResolveResult":
|
|
190
|
+
"""Emit reason code but signal fall-through (role catalog miss)."""
|
|
191
|
+
return cls(
|
|
192
|
+
success=False,
|
|
193
|
+
alias=None,
|
|
194
|
+
reason_code=reason_code,
|
|
195
|
+
reason_message=message,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _validate_tiers_object(tiers: object) -> Optional[str]:
|
|
200
|
+
"""Validate tiers object; return error message or None."""
|
|
201
|
+
if not isinstance(tiers, dict):
|
|
202
|
+
return "Field 'tiers' must be an object"
|
|
203
|
+
required_tiers = {t.value for t in Tier}
|
|
204
|
+
actual_tiers = set(tiers.keys())
|
|
205
|
+
missing = required_tiers - actual_tiers
|
|
206
|
+
if missing:
|
|
207
|
+
return f"Missing required tier keys: {', '.join(sorted(missing))}"
|
|
208
|
+
for tier_name, slug in tiers.items():
|
|
209
|
+
if not isinstance(slug, str) or not slug.strip():
|
|
210
|
+
return f"Tier '{tier_name}' must have a non-empty string slug"
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _validate_roles_object(roles: object) -> Optional[str]:
|
|
215
|
+
"""Validate v2 roles object; return error message or None."""
|
|
216
|
+
if not isinstance(roles, dict):
|
|
217
|
+
return "Field 'roles' must be an object"
|
|
218
|
+
actual_keys = set(roles.keys())
|
|
219
|
+
missing = CATALOG_ROLE_KEYS - actual_keys
|
|
220
|
+
extra = actual_keys - CATALOG_ROLE_KEYS
|
|
221
|
+
if missing:
|
|
222
|
+
return f"Missing required role keys: {', '.join(sorted(missing))}"
|
|
223
|
+
if extra:
|
|
224
|
+
return f"Unknown role keys: {', '.join(sorted(extra))}"
|
|
225
|
+
for role_name, slug in roles.items():
|
|
226
|
+
if not isinstance(slug, str) or not slug.strip():
|
|
227
|
+
return f"Role '{role_name}' must have a non-empty string slug"
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def validate_catalog_schema(catalog_path: Path) -> tuple[bool, Optional[str]]:
|
|
232
|
+
"""
|
|
233
|
+
Validate local catalog schema (DEC-0086 v1 + DEC-0087 v2).
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
(is_valid, error_message)
|
|
237
|
+
"""
|
|
238
|
+
if not catalog_path.exists():
|
|
239
|
+
return False, f"Catalog file not found: {catalog_path}"
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
with open(catalog_path, "r", encoding="utf-8") as f:
|
|
243
|
+
catalog = json.load(f)
|
|
244
|
+
except json.JSONDecodeError as e:
|
|
245
|
+
return False, f"Malformed JSON: {e}"
|
|
246
|
+
|
|
247
|
+
if "schema_version" not in catalog:
|
|
248
|
+
return False, "Missing required field: schema_version"
|
|
249
|
+
|
|
250
|
+
schema_version = catalog["schema_version"]
|
|
251
|
+
if schema_version not in (1, 2):
|
|
252
|
+
return False, f"Unsupported schema_version: {schema_version} (expected 1 or 2)"
|
|
253
|
+
|
|
254
|
+
if "tiers" not in catalog:
|
|
255
|
+
return False, "Missing required field: tiers"
|
|
256
|
+
|
|
257
|
+
tiers_error = _validate_tiers_object(catalog["tiers"])
|
|
258
|
+
if tiers_error:
|
|
259
|
+
if schema_version == 2:
|
|
260
|
+
return False, f"v2 schema error: {tiers_error}"
|
|
261
|
+
return False, tiers_error
|
|
262
|
+
|
|
263
|
+
if schema_version == 2 and "roles" in catalog:
|
|
264
|
+
roles_error = _validate_roles_object(catalog["roles"])
|
|
265
|
+
if roles_error:
|
|
266
|
+
return False, f"v2 schema error: {roles_error}"
|
|
267
|
+
|
|
268
|
+
return True, None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def catalog_validation_reason_code(error_message: Optional[str], catalog: Optional[dict]) -> ReasonCode:
|
|
272
|
+
"""Pick fail-closed reason code for catalog validation failure."""
|
|
273
|
+
if catalog and catalog.get("schema_version") == 2:
|
|
274
|
+
return ReasonCode.MODEL_CATALOG_SCHEMA_V2_INVALID
|
|
275
|
+
if error_message and "v2 schema error" in error_message:
|
|
276
|
+
return ReasonCode.MODEL_CATALOG_SCHEMA_V2_INVALID
|
|
277
|
+
return ReasonCode.MODEL_CATALOG_INVALID
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def load_catalog(catalog_path: Path) -> tuple[Optional[dict], Optional[str]]:
|
|
281
|
+
"""
|
|
282
|
+
Load and validate catalog.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
(catalog_dict, error_message)
|
|
286
|
+
"""
|
|
287
|
+
is_valid, error = validate_catalog_schema(catalog_path)
|
|
288
|
+
if not is_valid:
|
|
289
|
+
return None, error
|
|
290
|
+
|
|
291
|
+
with open(catalog_path, "r", encoding="utf-8") as f:
|
|
292
|
+
return json.load(f), None
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _catalog_contains_slug(catalog: dict, slug: str) -> bool:
|
|
296
|
+
"""Check whether slug appears in catalog tiers or roles values."""
|
|
297
|
+
tiers = catalog.get("tiers", {})
|
|
298
|
+
if slug in tiers.values():
|
|
299
|
+
return True
|
|
300
|
+
roles = catalog.get("roles", {})
|
|
301
|
+
if isinstance(roles, dict) and slug in roles.values():
|
|
302
|
+
return True
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def validate_direct_slug(
|
|
307
|
+
slug: str,
|
|
308
|
+
model_resolve: str,
|
|
309
|
+
catalog: Optional[dict],
|
|
310
|
+
) -> tuple[bool, Optional[str]]:
|
|
311
|
+
"""
|
|
312
|
+
Validate direct MODEL_<PHASE> slug per DEC-0087 §4.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
(is_valid, error_message)
|
|
316
|
+
"""
|
|
317
|
+
if not slug or not slug.strip():
|
|
318
|
+
return False, "Direct slug override must be a non-empty string"
|
|
319
|
+
|
|
320
|
+
if model_resolve == "alias_only":
|
|
321
|
+
return True, None
|
|
322
|
+
|
|
323
|
+
if model_resolve in ("local_catalog", "role_catalog"):
|
|
324
|
+
if not catalog:
|
|
325
|
+
return False, "Direct slug validation requires catalog when MODEL_RESOLVE is local_catalog or role_catalog"
|
|
326
|
+
if not _catalog_contains_slug(catalog, slug):
|
|
327
|
+
return False, f"Slug '{slug}' not found in catalog tiers or roles"
|
|
328
|
+
return True, None
|
|
329
|
+
|
|
330
|
+
return False, f"Unknown MODEL_RESOLVE value: {model_resolve}"
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def resolve_logical_role(phase_id: str, scratchpad: Dict[str, str]) -> Optional[str]:
|
|
334
|
+
"""Resolve phase to logical role with AUTO_ROLE_* policy overrides."""
|
|
335
|
+
if phase_id == "auto":
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
base_role = PHASE_LOGICAL_ROLE.get(phase_id)
|
|
339
|
+
if base_role is None:
|
|
340
|
+
return "dev"
|
|
341
|
+
|
|
342
|
+
if phase_id == "research":
|
|
343
|
+
override = scratchpad.get("AUTO_ROLE_RESEARCH", "").strip()
|
|
344
|
+
if override in ("po", "tech-lead"):
|
|
345
|
+
return override
|
|
346
|
+
return base_role
|
|
347
|
+
|
|
348
|
+
if phase_id == "plan-verify":
|
|
349
|
+
override = scratchpad.get("AUTO_ROLE_PLAN_VERIFY", "").strip()
|
|
350
|
+
if override in ("qa", "tech-lead"):
|
|
351
|
+
return override
|
|
352
|
+
return base_role
|
|
353
|
+
|
|
354
|
+
if phase_id == "refresh-context":
|
|
355
|
+
override = scratchpad.get("AUTO_ROLE_REFRESH_CONTEXT", "").strip()
|
|
356
|
+
if override in ("curator", "po"):
|
|
357
|
+
return override
|
|
358
|
+
return base_role
|
|
359
|
+
|
|
360
|
+
return base_role
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _resolve_tier_chain(
|
|
364
|
+
phase_id: str,
|
|
365
|
+
tier: Optional[Tier],
|
|
366
|
+
model_resolve: str,
|
|
367
|
+
model_fallback: str,
|
|
368
|
+
catalog: Optional[dict],
|
|
369
|
+
) -> ResolveResult:
|
|
370
|
+
"""DEC-0086 tier→alias / local_catalog chain for a resolved tier."""
|
|
371
|
+
if tier is None:
|
|
372
|
+
return ResolveResult.success_alias(None, None)
|
|
373
|
+
|
|
374
|
+
if model_resolve == "alias_only":
|
|
375
|
+
alias = TIER_ALIAS_MAP.get(tier)
|
|
376
|
+
return ResolveResult.success_alias(tier, alias)
|
|
377
|
+
|
|
378
|
+
if model_resolve == "local_catalog" or model_resolve == "role_catalog":
|
|
379
|
+
if not catalog:
|
|
380
|
+
return ResolveResult.failure(
|
|
381
|
+
ReasonCode.MODEL_CATALOG_INVALID,
|
|
382
|
+
"MODEL_RESOLVE=local_catalog requires catalog",
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
tier_key = tier.value
|
|
386
|
+
tiers = catalog.get("tiers", {})
|
|
387
|
+
if tier_key not in tiers:
|
|
388
|
+
if model_fallback == "inherit":
|
|
389
|
+
return ResolveResult(
|
|
390
|
+
success=True,
|
|
391
|
+
alias="inherit",
|
|
392
|
+
reason_code=ReasonCode.MODEL_RESOLVE_FALLBACK,
|
|
393
|
+
reason_message=f"Tier '{tier_key}' not in catalog, using fallback 'inherit'",
|
|
394
|
+
tier=tier,
|
|
395
|
+
)
|
|
396
|
+
return ResolveResult.failure(
|
|
397
|
+
ReasonCode.MODEL_SLUG_UNKNOWN,
|
|
398
|
+
f"Tier '{tier_key}' not found in catalog",
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
slug = tiers[tier_key]
|
|
402
|
+
return ResolveResult.success_slug(tier, slug)
|
|
403
|
+
|
|
404
|
+
return ResolveResult.failure(
|
|
405
|
+
ReasonCode.MODEL_TIER_INVALID,
|
|
406
|
+
f"Unknown MODEL_RESOLVE value: {model_resolve} (expected: alias_only|local_catalog|role_catalog)",
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _resolve_tier_for_phase(
|
|
411
|
+
phase_id: str,
|
|
412
|
+
scratchpad: Dict[str, str],
|
|
413
|
+
use_default_only: bool = False,
|
|
414
|
+
) -> Optional[Tier]:
|
|
415
|
+
"""Resolve tier from MODEL_TIER_<PHASE> or phase matrix / default."""
|
|
416
|
+
if use_default_only:
|
|
417
|
+
default = scratchpad.get("MODEL_TIER_DEFAULT", "balanced").strip()
|
|
418
|
+
try:
|
|
419
|
+
return Tier(default)
|
|
420
|
+
except ValueError:
|
|
421
|
+
return Tier.BALANCED
|
|
422
|
+
|
|
423
|
+
tier_key = phase_to_tier_key(phase_id)
|
|
424
|
+
if tier_key in scratchpad and scratchpad[tier_key].strip():
|
|
425
|
+
try:
|
|
426
|
+
return Tier(scratchpad[tier_key].strip())
|
|
427
|
+
except ValueError:
|
|
428
|
+
return None
|
|
429
|
+
|
|
430
|
+
tier = DEFAULT_PHASE_TIER_MATRIX.get(phase_id)
|
|
431
|
+
if tier is not None:
|
|
432
|
+
return tier
|
|
433
|
+
|
|
434
|
+
if phase_id == "auto":
|
|
435
|
+
return None
|
|
436
|
+
|
|
437
|
+
default = scratchpad.get("MODEL_TIER_DEFAULT", "balanced").strip()
|
|
438
|
+
try:
|
|
439
|
+
return Tier(default)
|
|
440
|
+
except ValueError:
|
|
441
|
+
return Tier.BALANCED
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def resolve_model_for_phase(
|
|
445
|
+
phase_id: str,
|
|
446
|
+
scratchpad: Optional[Dict[str, str]] = None,
|
|
447
|
+
catalog: Optional[dict] = None,
|
|
448
|
+
catalog_path: Optional[Path] = None,
|
|
449
|
+
) -> ResolveResult:
|
|
450
|
+
"""
|
|
451
|
+
Unified 5-step precedence resolver (DEC-0087 §2).
|
|
452
|
+
|
|
453
|
+
Precedence:
|
|
454
|
+
1. MODEL_<PHASE> direct slug
|
|
455
|
+
2. MODEL_TIER_<PHASE> → DEC-0086 tier chain
|
|
456
|
+
3. MODEL_RESOLVE=role_catalog → phase→role→catalog slug (fall through on miss)
|
|
457
|
+
4. MODEL_TIER_DEFAULT → DEC-0086 tier chain
|
|
458
|
+
5. Cursor stable alias
|
|
459
|
+
"""
|
|
460
|
+
pad = scratchpad or {}
|
|
461
|
+
model_resolve = pad.get("MODEL_RESOLVE", "alias_only").strip() or "alias_only"
|
|
462
|
+
model_fallback = pad.get("MODEL_FALLBACK", "inherit").strip() or "inherit"
|
|
463
|
+
|
|
464
|
+
if catalog is None and catalog_path is not None:
|
|
465
|
+
catalog, catalog_error = load_catalog(catalog_path)
|
|
466
|
+
if catalog_error:
|
|
467
|
+
code = catalog_validation_reason_code(catalog_error, None)
|
|
468
|
+
return ResolveResult.failure(code, catalog_error)
|
|
469
|
+
|
|
470
|
+
# Step 1: MODEL_<PHASE> direct slug override
|
|
471
|
+
model_key = phase_to_model_key(phase_id)
|
|
472
|
+
if model_key in pad and pad[model_key].strip():
|
|
473
|
+
slug = pad[model_key].strip()
|
|
474
|
+
is_valid, error = validate_direct_slug(slug, model_resolve, catalog)
|
|
475
|
+
if not is_valid:
|
|
476
|
+
return ResolveResult.failure(
|
|
477
|
+
ReasonCode.MODEL_OVERRIDE_SLUG_UNKNOWN,
|
|
478
|
+
error or "Direct slug validation failed",
|
|
479
|
+
)
|
|
480
|
+
return ResolveResult.success_slug(None, slug)
|
|
481
|
+
|
|
482
|
+
# Step 2: MODEL_TIER_<PHASE> tier chain (skipped when role_catalog — step 3 handles slug)
|
|
483
|
+
if model_resolve in ("alias_only", "local_catalog"):
|
|
484
|
+
tier = _resolve_tier_for_phase(phase_id, pad, use_default_only=False)
|
|
485
|
+
if tier is not None:
|
|
486
|
+
result = _resolve_tier_chain(phase_id, tier, model_resolve, model_fallback, catalog)
|
|
487
|
+
if result.success:
|
|
488
|
+
return result
|
|
489
|
+
if result.reason_code != ReasonCode.MODEL_RESOLVE_FALLBACK:
|
|
490
|
+
return result
|
|
491
|
+
|
|
492
|
+
# Step 3: role catalog lookup (opt-in)
|
|
493
|
+
if model_resolve == "role_catalog" and phase_id != "auto":
|
|
494
|
+
logical_role = resolve_logical_role(phase_id, pad)
|
|
495
|
+
if logical_role:
|
|
496
|
+
catalog_role_key = LOGICAL_ROLE_TO_CATALOG_KEY.get(logical_role)
|
|
497
|
+
if catalog and catalog_role_key:
|
|
498
|
+
roles = catalog.get("roles")
|
|
499
|
+
if isinstance(roles, dict) and catalog_role_key in roles:
|
|
500
|
+
slug = roles[catalog_role_key]
|
|
501
|
+
if slug and slug.strip():
|
|
502
|
+
return ResolveResult.success_slug(None, slug.strip())
|
|
503
|
+
# Miss → fall through with reason (not hard stop)
|
|
504
|
+
# Continue to step 4; reason emitted via optional metadata on result path
|
|
505
|
+
|
|
506
|
+
# Step 4: MODEL_TIER_DEFAULT tier chain
|
|
507
|
+
default_tier = _resolve_tier_for_phase(phase_id, pad, use_default_only=True)
|
|
508
|
+
if default_tier is not None:
|
|
509
|
+
result = _resolve_tier_chain(phase_id, default_tier, model_resolve, model_fallback, catalog)
|
|
510
|
+
if result.success:
|
|
511
|
+
return result
|
|
512
|
+
|
|
513
|
+
# Step 5: Cursor alias from phase matrix tier or balanced default
|
|
514
|
+
fallback_tier = DEFAULT_PHASE_TIER_MATRIX.get(phase_id, Tier.BALANCED)
|
|
515
|
+
if fallback_tier is None:
|
|
516
|
+
fallback_tier = Tier.BALANCED
|
|
517
|
+
alias = TIER_ALIAS_MAP.get(fallback_tier)
|
|
518
|
+
return ResolveResult.success_alias(fallback_tier, alias)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def resolve_model_tier(
|
|
522
|
+
phase: str,
|
|
523
|
+
model_resolve: str = "alias_only",
|
|
524
|
+
model_fallback: str = "inherit",
|
|
525
|
+
catalog_path: Optional[Path] = None,
|
|
526
|
+
tier_override: Optional[str] = None,
|
|
527
|
+
) -> ResolveResult:
|
|
528
|
+
"""
|
|
529
|
+
Resolve model tier for a given phase (DEC-0086 §3 resolver algorithm).
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
phase: Canonical phase ID (e.g., "execute", "qa")
|
|
533
|
+
model_resolve: Resolution strategy ("alias_only" | "local_catalog" | "role_catalog")
|
|
534
|
+
model_fallback: Fallback when catalog lookup fails ("inherit")
|
|
535
|
+
catalog_path: Path to local catalog (required when model_resolve="local_catalog")
|
|
536
|
+
tier_override: Explicit tier override (bypasses phase matrix lookup)
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
ResolveResult with success/failure, alias/slug, and reason code
|
|
540
|
+
"""
|
|
541
|
+
# Step 1: Determine tier value
|
|
542
|
+
tier = None
|
|
543
|
+
if tier_override:
|
|
544
|
+
try:
|
|
545
|
+
tier = Tier(tier_override)
|
|
546
|
+
except ValueError:
|
|
547
|
+
return ResolveResult.failure(
|
|
548
|
+
ReasonCode.MODEL_TIER_INVALID,
|
|
549
|
+
f"Unknown tier value: {tier_override} (expected: cheap|balanced|strong)",
|
|
550
|
+
)
|
|
551
|
+
else:
|
|
552
|
+
tier = DEFAULT_PHASE_TIER_MATRIX.get(phase)
|
|
553
|
+
if tier is None and phase != "auto":
|
|
554
|
+
tier = Tier.BALANCED
|
|
555
|
+
|
|
556
|
+
catalog = None
|
|
557
|
+
if catalog_path and model_resolve in ("local_catalog", "role_catalog"):
|
|
558
|
+
catalog, error = load_catalog(catalog_path)
|
|
559
|
+
if error:
|
|
560
|
+
code = catalog_validation_reason_code(error, None)
|
|
561
|
+
return ResolveResult.failure(code, error)
|
|
562
|
+
|
|
563
|
+
return _resolve_tier_chain(phase, tier, model_resolve, model_fallback, catalog)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def get_tier_for_phase(phase: str) -> Optional[Tier]:
|
|
567
|
+
"""Get default tier for a phase (None for 'auto')."""
|
|
568
|
+
return DEFAULT_PHASE_TIER_MATRIX.get(phase)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def get_alias_for_tier(tier: Tier) -> Optional[str]:
|
|
572
|
+
"""Get Cursor alias for a tier (None means omit model: field)."""
|
|
573
|
+
return TIER_ALIAS_MAP.get(tier)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def self_test() -> bool:
|
|
577
|
+
"""Run self-test to validate library contract (US-0101 / DEC-0086; US-0102 / DEC-0087)."""
|
|
578
|
+
print("[SELF-TEST] Validating model_tier_lib contract...")
|
|
579
|
+
errors = []
|
|
580
|
+
|
|
581
|
+
# Test 1: Tier enum values
|
|
582
|
+
expected_tiers = {"cheap", "balanced", "strong"}
|
|
583
|
+
actual_tiers = {t.value for t in Tier}
|
|
584
|
+
if actual_tiers != expected_tiers:
|
|
585
|
+
errors.append(f"Tier enum mismatch: expected {expected_tiers}, got {actual_tiers}")
|
|
586
|
+
|
|
587
|
+
# Test 2: Phase matrix has all expected phases
|
|
588
|
+
expected_phases = {
|
|
589
|
+
"ask", "refresh-context", "memory-audit", "status-reconcile", "pause",
|
|
590
|
+
"intake", "discovery", "research", "release", "plan-verify",
|
|
591
|
+
"architecture", "execute", "quick", "qa", "verify-work", "security-review",
|
|
592
|
+
"auto"
|
|
593
|
+
}
|
|
594
|
+
actual_phases = set(DEFAULT_PHASE_TIER_MATRIX.keys())
|
|
595
|
+
if actual_phases != expected_phases:
|
|
596
|
+
errors.append(f"Phase matrix mismatch: expected {expected_phases}, got {actual_phases}")
|
|
597
|
+
|
|
598
|
+
# Test 3: Tier→alias mapping
|
|
599
|
+
if TIER_ALIAS_MAP[Tier.CHEAP] != "fast":
|
|
600
|
+
errors.append(f"CHEAP alias mismatch: expected 'fast', got '{TIER_ALIAS_MAP[Tier.CHEAP]}'")
|
|
601
|
+
if TIER_ALIAS_MAP[Tier.BALANCED] != "inherit":
|
|
602
|
+
errors.append(f"BALANCED alias mismatch: expected 'inherit', got '{TIER_ALIAS_MAP[Tier.BALANCED]}'")
|
|
603
|
+
if TIER_ALIAS_MAP[Tier.STRONG] is not None:
|
|
604
|
+
errors.append(f"STRONG alias mismatch: expected None, got '{TIER_ALIAS_MAP[Tier.STRONG]}'")
|
|
605
|
+
|
|
606
|
+
# Test 4: Reason codes
|
|
607
|
+
expected_codes = {
|
|
608
|
+
"MODEL_TIER_INVALID", "MODEL_CATALOG_INVALID", "MODEL_SLUG_UNKNOWN",
|
|
609
|
+
"MODEL_RESOLVE_FALLBACK", "MODEL_OVERRIDE_SLUG_UNKNOWN",
|
|
610
|
+
"MODEL_ROLE_SLUG_UNKNOWN", "MODEL_CATALOG_SCHEMA_V2_INVALID",
|
|
611
|
+
}
|
|
612
|
+
actual_codes = {c.value for c in ReasonCode}
|
|
613
|
+
if actual_codes != expected_codes:
|
|
614
|
+
errors.append(f"ReasonCode mismatch: expected {expected_codes}, got {actual_codes}")
|
|
615
|
+
|
|
616
|
+
# Test 5: Precedence chain
|
|
617
|
+
if len(PRECEDENCE_CHAIN_STEPS) != 5:
|
|
618
|
+
errors.append(f"Precedence chain must have 5 steps, got {len(PRECEDENCE_CHAIN_STEPS)}")
|
|
619
|
+
|
|
620
|
+
# Test 6: resolve_model_for_phase exported
|
|
621
|
+
if not callable(resolve_model_for_phase):
|
|
622
|
+
errors.append("resolve_model_for_phase is not callable")
|
|
623
|
+
|
|
624
|
+
if errors:
|
|
625
|
+
print("[SELF_TEST_FAILED]")
|
|
626
|
+
for error in errors:
|
|
627
|
+
print(f" {error}", file=sys.stderr)
|
|
628
|
+
return False
|
|
629
|
+
else:
|
|
630
|
+
print("[MODEL_TIER_SELF_TEST_OK]")
|
|
631
|
+
return True
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
if __name__ == "__main__":
|
|
635
|
+
import argparse
|
|
636
|
+
|
|
637
|
+
parser = argparse.ArgumentParser(description="Model tier resolver (US-0101 / US-0102)")
|
|
638
|
+
parser.add_argument("--phase", help="Phase ID (e.g., execute, qa)")
|
|
639
|
+
parser.add_argument(
|
|
640
|
+
"--resolve",
|
|
641
|
+
default="alias_only",
|
|
642
|
+
choices=["alias_only", "local_catalog", "role_catalog"],
|
|
643
|
+
)
|
|
644
|
+
parser.add_argument("--fallback", default="inherit", help="Fallback strategy")
|
|
645
|
+
parser.add_argument("--catalog", type=Path, help="Path to local catalog")
|
|
646
|
+
parser.add_argument("--tier-override", help="Explicit tier (cheap|balanced|strong)")
|
|
647
|
+
parser.add_argument("--self-test", action="store_true", help="Run self-test")
|
|
648
|
+
|
|
649
|
+
args = parser.parse_args()
|
|
650
|
+
|
|
651
|
+
if args.self_test:
|
|
652
|
+
success = self_test()
|
|
653
|
+
sys.exit(0 if success else 1)
|
|
654
|
+
|
|
655
|
+
if not args.phase:
|
|
656
|
+
parser.error("--phase is required unless --self-test is specified")
|
|
657
|
+
|
|
658
|
+
result = resolve_model_tier(
|
|
659
|
+
phase=args.phase,
|
|
660
|
+
model_resolve=args.resolve,
|
|
661
|
+
model_fallback=args.fallback,
|
|
662
|
+
catalog_path=args.catalog,
|
|
663
|
+
tier_override=args.tier_override,
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
if result.success:
|
|
667
|
+
print(f"[OK] phase={args.phase} tier={result.tier.value if result.tier else 'n/a'}", end="")
|
|
668
|
+
if result.alias:
|
|
669
|
+
print(f" alias={result.alias}")
|
|
670
|
+
elif result.slug:
|
|
671
|
+
print(f" slug={result.slug}")
|
|
672
|
+
else:
|
|
673
|
+
print(" (omit model: field)")
|
|
674
|
+
if result.reason_code:
|
|
675
|
+
print(f" reason={result.reason_code.value}: {result.reason_message}")
|
|
676
|
+
sys.exit(0)
|
|
677
|
+
else:
|
|
678
|
+
print(f"[FAIL] phase={args.phase} reason={result.reason_code.value}")
|
|
679
|
+
print(f" {result.reason_message}")
|
|
680
|
+
sys.exit(1)
|