@planu/cli 4.10.12 → 4.11.0

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/config/project-knowledge-graph.json +42 -5
  3. package/dist/engine/core-bridge-project-graph.d.ts +13 -0
  4. package/dist/engine/core-bridge-project-graph.js +499 -0
  5. package/dist/engine/core-bridge.d.ts +7 -2
  6. package/dist/engine/core-bridge.js +55 -0
  7. package/dist/engine/frontmatter-parser.js +73 -23
  8. package/dist/engine/model-tier-resolver.d.ts +8 -7
  9. package/dist/engine/model-tier-resolver.js +70 -73
  10. package/dist/engine/next-spec-resolver/orchestration-planner.d.ts +5 -0
  11. package/dist/engine/next-spec-resolver/orchestration-planner.js +34 -5
  12. package/dist/engine/project-graph/builder.js +271 -36
  13. package/dist/engine/project-graph/cache.d.ts +22 -4
  14. package/dist/engine/project-graph/cache.js +412 -33
  15. package/dist/engine/project-graph/index.d.ts +1 -0
  16. package/dist/engine/project-graph/index.js +1 -0
  17. package/dist/engine/project-graph/native.d.ts +3 -0
  18. package/dist/engine/project-graph/native.js +36 -0
  19. package/dist/engine/project-graph/query.js +34 -2
  20. package/dist/engine/provider-adapters/adapters/claude.js +38 -14
  21. package/dist/engine/scan-project/index.js +88 -15
  22. package/dist/engine/spec-format/lean-spec-generator.d.ts +2 -2
  23. package/dist/engine/spec-format/lean-spec-generator.js +65 -50
  24. package/dist/engine/spec-format/metadata-value-policy.d.ts +161 -0
  25. package/dist/engine/spec-format/metadata-value-policy.js +87 -0
  26. package/dist/engine/spec-format/value-only-spec-serializer.d.ts +12 -0
  27. package/dist/engine/spec-format/value-only-spec-serializer.js +18 -0
  28. package/dist/engine/spec-generator/fallback-generator.js +4 -2
  29. package/dist/engine/spec-generator/opus-generator.js +5 -2
  30. package/dist/engine/spec-migrator/lean-migration.js +26 -13
  31. package/dist/storage/spec-store.js +6 -6
  32. package/dist/tools/create-spec.js +1027 -739
  33. package/dist/tools/render-spec-for-provider.js +4 -3
  34. package/dist/tools/reverse-engineer/handler.js +76 -43
  35. package/dist/tools/spec-split-handler.js +36 -75
  36. package/dist/types/conventions.d.ts +9 -0
  37. package/dist/types/core-bridge.d.ts +72 -0
  38. package/dist/types/next-spec.d.ts +2 -1
  39. package/dist/types/project-knowledge-graph.d.ts +58 -0
  40. package/dist/types/spec/core.d.ts +7 -4
  41. package/dist/types/spec-format.d.ts +2 -1
  42. package/dist/types/spec-generator.d.ts +8 -2
  43. package/package.json +11 -9
  44. package/planu-native.json +1 -1
  45. package/planu-plugin.json +1 -1
  46. package/dist/engine/spec-format/model-budget-deriver.d.ts +0 -5
  47. package/dist/engine/spec-format/model-budget-deriver.js +0 -7
@@ -3,6 +3,7 @@ import * as nodeFs from 'node:fs';
3
3
  import { existsSync, readFileSync, statSync, readdirSync } from 'node:fs';
4
4
  import { join, extname, basename, dirname, sep } from 'node:path';
5
5
  import { createHash, createHmac } from 'node:crypto';
6
+ import { tsExtractTsRelations, tsQueryAffectedNodes, tsQueryShortestPath, resolveProjectGraphLimits, tsScanProjectGraphSources, tsSelectCompactGraphSlice, } from './core-bridge-project-graph.js';
6
7
  const require = createRequire(import.meta.url);
7
8
  function detectMusl() {
8
9
  if (process.platform !== 'linux') {
@@ -671,6 +672,60 @@ export function fastHmacVerify(secret, data, signature) {
671
672
  const n = getNative();
672
673
  return n ? n.fastHmacVerify(secret, data, signature) : tsHmacVerify(secret, data, signature);
673
674
  }
675
+ export function fastScanProjectGraphSources(rootPath, policy, overrides = {}) {
676
+ const limits = resolveProjectGraphLimits(policy, overrides);
677
+ const n = getNative();
678
+ if (typeof n?.scanProjectGraphSources === 'function') {
679
+ return n.scanProjectGraphSources(rootPath, limits);
680
+ }
681
+ return tsScanProjectGraphSources(rootPath, limits);
682
+ }
683
+ export function fastExtractTsRelations(rootPath, paths, policy, overrides = {}) {
684
+ const limits = resolveProjectGraphLimits(policy, overrides);
685
+ const n = getNative();
686
+ if (typeof n?.extractTsRelations === 'function') {
687
+ return n.extractTsRelations(rootPath, paths, limits);
688
+ }
689
+ return tsExtractTsRelations(rootPath, paths, limits);
690
+ }
691
+ function compactEdges(edges) {
692
+ return {
693
+ ids: edges.map((edge) => edge.id),
694
+ from: edges.map((edge) => edge.from),
695
+ to: edges.map((edge) => edge.to),
696
+ relations: edges.map((edge) => edge.relation),
697
+ };
698
+ }
699
+ export function fastQueryAffectedNodes(seed, edges, relationFilter, policy, overrides = {}) {
700
+ const limits = resolveProjectGraphLimits(policy, overrides);
701
+ const n = getNative();
702
+ if (typeof n?.queryAffectedNodesCompact === 'function') {
703
+ return n.queryAffectedNodesCompact(seed, compactEdges(edges), relationFilter, limits);
704
+ }
705
+ if (typeof n?.queryAffectedNodes === 'function') {
706
+ return n.queryAffectedNodes(seed, edges, relationFilter, limits);
707
+ }
708
+ return tsQueryAffectedNodes(seed, edges, relationFilter, limits);
709
+ }
710
+ export function fastQueryShortestPath(from, to, edges, relationFilter, policy, overrides = {}) {
711
+ const limits = resolveProjectGraphLimits(policy, overrides);
712
+ const n = getNative();
713
+ if (typeof n?.queryShortestPath === 'function') {
714
+ return n.queryShortestPath(from, to, edges, relationFilter, limits);
715
+ }
716
+ return tsQueryShortestPath(from, to, edges, relationFilter, limits);
717
+ }
718
+ export function fastSelectCompactGraphSlice(seeds, nodes, edges, policy, overrides = {}) {
719
+ const limits = resolveProjectGraphLimits(policy, overrides);
720
+ const n = getNative();
721
+ if (typeof n?.selectCompactGraphSliceCompact === 'function') {
722
+ return n.selectCompactGraphSliceCompact(seeds, nodes.map((node) => node.id), compactEdges(edges), limits);
723
+ }
724
+ if (typeof n?.selectCompactGraphSlice === 'function') {
725
+ return n.selectCompactGraphSlice(seeds, nodes, edges, limits);
726
+ }
727
+ return tsSelectCompactGraphSlice(seeds, nodes, edges, limits);
728
+ }
674
729
  export function startNativeWatcher(rootPath, callback) {
675
730
  const n = getNative();
676
731
  if (n && typeof n.startProjectWatcher === 'function') {
@@ -13,37 +13,87 @@ export function parseFrontmatter(content) {
13
13
  return { metadata: {}, body: content };
14
14
  }
15
15
  const metadata = {};
16
- for (const line of match[1].split('\n')) {
17
- const kv = KV_LINE_RE.exec(line.trim());
16
+ const frontmatter = match[1];
17
+ for (const line of frontmatter.split('\n')) {
18
+ if (/^\s/.test(line)) {
19
+ continue;
20
+ }
21
+ const kv = KV_LINE_RE.exec(line);
18
22
  if (!kv?.[1] || kv[2] === undefined) {
19
23
  continue;
20
24
  }
21
25
  const key = kv[1];
22
26
  const val = kv[2].trim();
23
- if (val.startsWith('[') && val.endsWith(']')) {
24
- metadata[key] = val
25
- .slice(1, -1)
26
- .split(',')
27
- .map((s) => s.trim().replace(/^["']|["']$/g, ''));
28
- }
29
- else if (val === 'null') {
30
- metadata[key] = null;
31
- }
32
- else if (/^\d+$/.test(val)) {
33
- metadata[key] = parseInt(val, 10);
34
- }
35
- else if (val === 'true') {
36
- metadata[key] = true;
37
- }
38
- else if (val === 'false') {
39
- metadata[key] = false;
40
- }
41
- else {
42
- metadata[key] = val.replace(/^["']|["']$/g, '');
43
- }
27
+ metadata[key] = parseScalar(val);
28
+ }
29
+ const generation = parseGenerationProvenance(frontmatter);
30
+ if (generation !== undefined) {
31
+ metadata.generation = generation;
32
+ }
33
+ const estimation = parseKnownMappingBlock(frontmatter, 'estimation');
34
+ if (estimation !== undefined) {
35
+ metadata.estimation = estimation;
44
36
  }
45
37
  return { metadata, body: match[2] };
46
38
  }
39
+ function parseScalar(value) {
40
+ if (value.startsWith('[') && value.endsWith(']')) {
41
+ return value
42
+ .slice(1, -1)
43
+ .split(',')
44
+ .map((item) => item.trim().replace(/^["']|["']$/g, ''));
45
+ }
46
+ if (value === 'null') {
47
+ return null;
48
+ }
49
+ if (/^\d+$/.test(value)) {
50
+ return parseInt(value, 10);
51
+ }
52
+ if (value === 'true') {
53
+ return true;
54
+ }
55
+ if (value === 'false') {
56
+ return false;
57
+ }
58
+ return value.replace(/^["']|["']$/g, '');
59
+ }
60
+ function parseGenerationProvenance(frontmatter) {
61
+ const values = parseKnownMappingBlock(frontmatter, 'generation');
62
+ if (!values) {
63
+ return undefined;
64
+ }
65
+ const method = values.method;
66
+ const generatedAt = values.generatedAt;
67
+ if (!isGenerationMethod(method) || typeof generatedAt !== 'string' || !generatedAt) {
68
+ return undefined;
69
+ }
70
+ const host = typeof values.host === 'string' ? values.host : undefined;
71
+ const modelId = typeof values.modelId === 'string' ? values.modelId : undefined;
72
+ return {
73
+ method,
74
+ generatedAt,
75
+ ...(host ? { host } : {}),
76
+ ...(modelId && method !== 'deterministic' ? { modelId } : {}),
77
+ };
78
+ }
79
+ function parseKnownMappingBlock(frontmatter, key) {
80
+ const blockPattern = new RegExp(`^${key}:\\s*$\\n((?:^[ \\t]+.*(?:\\n|$))*)`, 'm');
81
+ const block = blockPattern.exec(frontmatter)?.[1];
82
+ if (!block) {
83
+ return undefined;
84
+ }
85
+ const values = {};
86
+ for (const line of block.split('\n')) {
87
+ const match = /^ {2}([A-Za-z][A-Za-z0-9_-]*):\s*(.+)$/.exec(line);
88
+ if (match?.[1] && match[2] !== undefined) {
89
+ values[match[1]] = parseScalar(match[2].trim());
90
+ }
91
+ }
92
+ return Object.keys(values).length > 0 ? values : undefined;
93
+ }
94
+ function isGenerationMethod(value) {
95
+ return value === 'deterministic' || value === 'internal-model' || value === 'host';
96
+ }
47
97
  /**
48
98
  * Strip YAML frontmatter from markdown content, returning only the body.
49
99
  * If no frontmatter is present, returns the original content unchanged.
@@ -1,8 +1,9 @@
1
1
  import type { ModelMappingCache, ModelTier, ModelTierMapping } from '../types/index.js';
2
2
  export declare function detectAvailableProviders(): string[];
3
+ /** @deprecated Static model defaults are intentionally empty; use provider catalog evidence. */
3
4
  export declare const STATIC_TIER_MAPPING: Record<string, ModelTierMapping>;
4
- /** Classify a model name into a canonical tier based on name patterns. */
5
- export declare function classifyModelTier(modelId: string): ModelTier;
5
+ /** Classify a model name only when its observed ID carries a recognized tier signal. */
6
+ export declare function classifyModelTier(modelId: string): ModelTier | undefined;
6
7
  /**
7
8
  * Build a ModelTierMapping from a list of model IDs.
8
9
  * Prefers the highest-capability model per tier (last one wins when sorted alphabetically desc).
@@ -10,8 +11,8 @@ export declare function classifyModelTier(modelId: string): ModelTier;
10
11
  export declare function buildTierMapping(modelIds: string[]): ModelTierMapping;
11
12
  /**
12
13
  * SPEC-644 AC1+AC4: Fetch model tier mapping from detected provider APIs.
13
- * Runs at init_project. Returns the built ModelMappingCache.
14
- * Silent failure always returns a valid cache (uses static fallback).
14
+ * Runs at init_project. Returns only mappings observed from configured provider
15
+ * APIs. Silent failure leaves that provider absent from the cache.
15
16
  */
16
17
  export declare function fetchAndCacheModelMapping(projectPath: string): Promise<ModelMappingCache>;
17
18
  /**
@@ -21,8 +22,8 @@ export declare function fetchAndCacheModelMapping(projectPath: string): Promise<
21
22
  */
22
23
  export declare function triggerModelMappingRefresh(projectPath: string): void;
23
24
  /**
24
- * SPEC-644 AC5: Resolve the canonical tier to a provider-specific model ID.
25
- * Uses cached mapping if available; falls back to static Anthropic mapping.
25
+ * Resolve a canonical tier only when a fresh catalog for the exact provider
26
+ * contains that tier. The absence of evidence is represented by `undefined`.
26
27
  */
27
- export declare function resolveModelId(projectPath: string, tier: ModelTier, preferredProvider?: string): Promise<string>;
28
+ export declare function resolveModelId(projectPath: string, tier: ModelTier, preferredProvider?: string): Promise<string | undefined>;
28
29
  //# sourceMappingURL=model-tier-resolver.d.ts.map
@@ -1,11 +1,12 @@
1
1
  // @crash-shield-ignore-file — config/cache reader for Planu-controlled JSON; writer is this codebase, shape guaranteed by build/seed.
2
2
  // engine/model-tier-resolver.ts — SPEC-644: Dynamic model tier mapping from provider APIs
3
3
  // Fetches model lists from Anthropic/OpenAI/Google/Mistral and classifies into canonical tiers.
4
- // Caches in conventions.json with 7-day TTL. Never hardcodes model IDs.
4
+ // Caches provider-reported model IDs in conventions.json with a 7-day TTL.
5
5
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
6
6
  import { join } from 'node:path';
7
7
  const TTL_MS = 7 * 24 * 60 * 60 * 1000;
8
8
  const FETCH_TIMEOUT_MS = 5_000;
9
+ const MODEL_MAPPING_CACHE_VERSION = 1;
9
10
  // ---------------------------------------------------------------------------
10
11
  // Provider detection (AC4: env-var based)
11
12
  // ---------------------------------------------------------------------------
@@ -20,35 +21,12 @@ export function detectAvailableProviders() {
20
21
  .filter(([, envVar]) => !!process.env[envVar])
21
22
  .map(([provider]) => provider);
22
23
  }
23
- // ---------------------------------------------------------------------------
24
- // Static fallback mapping (used when API unreachable or no provider configured)
25
- // ---------------------------------------------------------------------------
26
- export const STATIC_TIER_MAPPING = {
27
- anthropic: {
28
- haiku: 'claude-haiku-4-5',
29
- sonnet: 'claude-sonnet-4-5',
30
- opus: 'claude-opus-4-5',
31
- },
32
- openai: {
33
- haiku: 'gpt-4o-mini',
34
- sonnet: 'gpt-4o',
35
- opus: 'o1',
36
- },
37
- google: {
38
- haiku: 'gemini-2.0-flash',
39
- sonnet: 'gemini-1.5-pro',
40
- opus: 'gemini-1.5-ultra',
41
- },
42
- mistral: {
43
- haiku: 'mistral-small-latest',
44
- sonnet: 'mistral-medium-latest',
45
- opus: 'mistral-large-latest',
46
- },
47
- };
24
+ /** @deprecated Static model defaults are intentionally empty; use provider catalog evidence. */
25
+ export const STATIC_TIER_MAPPING = {};
48
26
  // ---------------------------------------------------------------------------
49
27
  // Tier classification heuristics (name-based, no API response parsing)
50
28
  // ---------------------------------------------------------------------------
51
- /** Classify a model name into a canonical tier based on name patterns. */
29
+ /** Classify a model name only when its observed ID carries a recognized tier signal. */
52
30
  export function classifyModelTier(modelId) {
53
31
  const id = modelId.toLowerCase();
54
32
  // opus / frontier tier keywords
@@ -59,8 +37,10 @@ export function classifyModelTier(modelId) {
59
37
  if (/haiku|\bmini\b|small|flash|\bnano\b|lite|quick/.test(id)) {
60
38
  return 'haiku';
61
39
  }
62
- // default to balanced
63
- return 'sonnet';
40
+ if (/sonnet|medium|\bpro\b|gpt-4o(?!-mini)/.test(id)) {
41
+ return 'sonnet';
42
+ }
43
+ return undefined;
64
44
  }
65
45
  /**
66
46
  * Build a ModelTierMapping from a list of model IDs.
@@ -69,7 +49,10 @@ export function classifyModelTier(modelId) {
69
49
  export function buildTierMapping(modelIds) {
70
50
  const byTier = { haiku: [], sonnet: [], opus: [] };
71
51
  for (const id of modelIds) {
72
- byTier[classifyModelTier(id)].push(id);
52
+ const tier = classifyModelTier(id);
53
+ if (tier) {
54
+ byTier[tier].push(id);
55
+ }
73
56
  }
74
57
  // Pick one per tier — prefer the most recent (sort desc, take first)
75
58
  const pick = (ids) => ids.sort().reverse()[0] ?? '';
@@ -137,36 +120,23 @@ const PROVIDER_FETCHERS = {
137
120
  mistral: fetchMistralModels,
138
121
  };
139
122
  /**
140
- * Fetch model IDs for a single provider. Returns static fallback on any error.
141
- * SPEC-644 AC3: silent degradation when API unavailable.
123
+ * Fetch model IDs for a single provider. Missing/unreachable catalogs produce
124
+ * no routing evidence rather than a guessed model ID.
142
125
  */
143
126
  async function fetchProviderModels(provider, apiKey) {
144
127
  const fetcher = PROVIDER_FETCHERS[provider];
145
128
  if (!fetcher) {
146
- return (STATIC_TIER_MAPPING[provider] ??
147
- STATIC_TIER_MAPPING.anthropic ?? { haiku: '', sonnet: '', opus: '' });
129
+ return undefined;
148
130
  }
149
131
  try {
150
132
  const models = await withTimeout(fetcher(apiKey), FETCH_TIMEOUT_MS);
151
133
  if (!models || models.length === 0) {
152
- return (STATIC_TIER_MAPPING[provider] ??
153
- STATIC_TIER_MAPPING.anthropic ?? { haiku: '', sonnet: '', opus: '' });
134
+ return undefined;
154
135
  }
155
- const mapping = buildTierMapping(models);
156
- // Fill any empty tier from static fallback
157
- const staticFallback = STATIC_TIER_MAPPING[provider];
158
- if (staticFallback) {
159
- return {
160
- haiku: mapping.haiku || staticFallback.haiku,
161
- sonnet: mapping.sonnet || staticFallback.sonnet,
162
- opus: mapping.opus || staticFallback.opus,
163
- };
164
- }
165
- return mapping;
136
+ return buildTierMapping(models);
166
137
  }
167
138
  catch {
168
- return (STATIC_TIER_MAPPING[provider] ??
169
- STATIC_TIER_MAPPING.anthropic ?? { haiku: '', sonnet: '', opus: '' });
139
+ return undefined;
170
140
  }
171
141
  }
172
142
  // ---------------------------------------------------------------------------
@@ -219,28 +189,48 @@ async function patchConventions(projectPath, modelMapping) {
219
189
  // TTL check
220
190
  // ---------------------------------------------------------------------------
221
191
  function isModelMappingStale(cache) {
222
- const age = Date.now() - new Date(cache.resolvedAt).getTime();
192
+ const resolvedAt = new Date(cache.resolvedAt).getTime();
193
+ if (!Number.isFinite(resolvedAt)) {
194
+ return true;
195
+ }
196
+ const age = Date.now() - resolvedAt;
223
197
  return age > TTL_MS;
224
198
  }
199
+ function isVerifiedModelMappingCache(cache) {
200
+ const providers = cache.providers;
201
+ if (cache.version !== MODEL_MAPPING_CACHE_VERSION ||
202
+ cache.provenance?.source !== 'provider-api' ||
203
+ !Array.isArray(cache.provenance.verifiedProviders) ||
204
+ typeof providers !== 'object' ||
205
+ providers === null) {
206
+ return false;
207
+ }
208
+ return cache.provenance.verifiedProviders.every((provider) => typeof provider === 'string' && Object.hasOwn(providers, provider));
209
+ }
210
+ function createVerifiedModelMappingCache(providers, resolvedAt) {
211
+ return {
212
+ version: MODEL_MAPPING_CACHE_VERSION,
213
+ provenance: {
214
+ source: 'provider-api',
215
+ verifiedProviders: Object.keys(providers).sort(),
216
+ },
217
+ providers,
218
+ resolvedAt,
219
+ };
220
+ }
225
221
  // ---------------------------------------------------------------------------
226
222
  // Public API
227
223
  // ---------------------------------------------------------------------------
228
224
  /**
229
225
  * SPEC-644 AC1+AC4: Fetch model tier mapping from detected provider APIs.
230
- * Runs at init_project. Returns the built ModelMappingCache.
231
- * Silent failure always returns a valid cache (uses static fallback).
226
+ * Runs at init_project. Returns only mappings observed from configured provider
227
+ * APIs. Silent failure leaves that provider absent from the cache.
232
228
  */
233
229
  export async function fetchAndCacheModelMapping(projectPath) {
234
230
  const providers = detectAvailableProviders();
235
231
  const resolvedAt = new Date().toISOString();
236
232
  if (providers.length === 0) {
237
- // No providers configured — use Anthropic static fallback
238
- const cache = {
239
- providers: {
240
- anthropic: STATIC_TIER_MAPPING.anthropic ?? { haiku: '', sonnet: '', opus: '' },
241
- },
242
- resolvedAt,
243
- };
233
+ const cache = createVerifiedModelMappingCache({}, resolvedAt);
244
234
  await patchConventions(projectPath, cache).catch(() => {
245
235
  /* best-effort */
246
236
  });
@@ -249,9 +239,12 @@ export async function fetchAndCacheModelMapping(projectPath) {
249
239
  const providerMappings = {};
250
240
  await Promise.allSettled(providers.map(async (provider) => {
251
241
  const apiKey = process.env[PROVIDER_ENV_VARS[provider] ?? ''] ?? '';
252
- providerMappings[provider] = await fetchProviderModels(provider, apiKey);
242
+ const mapping = await fetchProviderModels(provider, apiKey);
243
+ if (mapping) {
244
+ providerMappings[provider] = mapping;
245
+ }
253
246
  }));
254
- const cache = { providers: providerMappings, resolvedAt };
247
+ const cache = createVerifiedModelMappingCache(providerMappings, resolvedAt);
255
248
  await patchConventions(projectPath, cache).catch(() => {
256
249
  /* best-effort */
257
250
  });
@@ -266,7 +259,11 @@ export function triggerModelMappingRefresh(projectPath) {
266
259
  void (async () => {
267
260
  try {
268
261
  const conventions = await readConventions(projectPath);
269
- if (conventions?.modelMapping && !isModelMappingStale(conventions.modelMapping)) {
262
+ const configuredProviders = detectAvailableProviders();
263
+ if (conventions?.modelMapping &&
264
+ isVerifiedModelMappingCache(conventions.modelMapping) &&
265
+ !isModelMappingStale(conventions.modelMapping) &&
266
+ configuredProviders.every((provider) => conventions.modelMapping?.provenance?.verifiedProviders.includes(provider))) {
270
267
  return;
271
268
  }
272
269
  await fetchAndCacheModelMapping(projectPath);
@@ -277,24 +274,24 @@ export function triggerModelMappingRefresh(projectPath) {
277
274
  })();
278
275
  }
279
276
  /**
280
- * SPEC-644 AC5: Resolve the canonical tier to a provider-specific model ID.
281
- * Uses cached mapping if available; falls back to static Anthropic mapping.
277
+ * Resolve a canonical tier only when a fresh catalog for the exact provider
278
+ * contains that tier. The absence of evidence is represented by `undefined`.
282
279
  */
283
280
  export async function resolveModelId(projectPath, tier, preferredProvider = 'anthropic') {
284
281
  try {
285
282
  const conventions = await readConventions(projectPath);
286
- const providers = conventions?.modelMapping?.providers;
287
- if (providers) {
288
- const firstKey = Object.keys(providers)[0] ?? '';
289
- const mapping = providers[preferredProvider] ?? providers[firstKey];
290
- if (mapping?.[tier]) {
291
- return mapping[tier];
292
- }
283
+ const cache = conventions?.modelMapping;
284
+ if (!cache || !isVerifiedModelMappingCache(cache) || isModelMappingStale(cache)) {
285
+ return undefined;
286
+ }
287
+ if (!cache.provenance?.verifiedProviders.includes(preferredProvider)) {
288
+ return undefined;
293
289
  }
290
+ const modelId = cache.providers[preferredProvider]?.[tier];
291
+ return modelId === '' ? undefined : modelId;
294
292
  }
295
293
  catch {
296
- /* fall through to static */
294
+ return undefined;
297
295
  }
298
- return (STATIC_TIER_MAPPING[preferredProvider]?.[tier] ?? STATIC_TIER_MAPPING.anthropic?.[tier] ?? tier);
299
296
  }
300
297
  //# sourceMappingURL=model-tier-resolver.js.map
@@ -1,4 +1,5 @@
1
1
  import type { Spec } from '../../types/spec/core.js';
2
+ import type { ModelTier } from '../../types/multi-agent.js';
2
3
  import type { Spec686Plan } from '../../types/next-spec.js';
3
4
  export type { Spec686WaveEntry, Spec686Plan } from '../../types/next-spec.js';
4
5
  export declare const MAX_WAVES = 10;
@@ -17,4 +18,8 @@ export declare const ORCHESTRATION_TRIGGER_THRESHOLD = 2;
17
18
  * 5. Increment planVersion from previous session.json value.
18
19
  */
19
20
  export declare function resolveOrchestrationPlan(projectPath: string, allSpecs: Spec[]): Promise<Spec686Plan | null>;
21
+ /** Resolve current execution routing without requiring durable provider metadata. */
22
+ export declare function resolveExecutionTier(spec: Partial<Pick<Spec, 'difficulty' | 'scope' | 'model'>>): ModelTier | undefined;
23
+ /** Preserve only an explicit legacy budget; never invent a token limit from a tier. */
24
+ export declare function resolveExecutionBudget(spec: Partial<Pick<Spec, 'difficulty' | 'scope' | 'model' | 'budget'>>): 800 | 2000 | 4000 | undefined;
20
25
  //# sourceMappingURL=orchestration-planner.d.ts.map
@@ -109,10 +109,14 @@ function buildTopologicalLevels(unblocked, inDegree, dependents) {
109
109
  return levels;
110
110
  }
111
111
  function buildWaveEntry(spec, group, resolvedIds) {
112
+ // The wave planner has no provider capability/catalog context. Preserve an
113
+ // explicit legacy constraint, but do not manufacture a provider-specific tier.
114
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
115
+ const legacyModel = normalizeLegacyTier(spec.model);
112
116
  return {
113
117
  specId: spec.id,
114
118
  title: spec.title,
115
- model: normalizeModel(spec.model),
119
+ ...(legacyModel ? { model: legacyModel } : {}),
116
120
  parallelSafe: group.length > 1,
117
121
  dependsOn: (Array.isArray(spec.dependencies) ? spec.dependencies : []).filter((d) => resolvedIds.has(d)),
118
122
  };
@@ -151,11 +155,36 @@ function buildSummary(approvedCount, waves) {
151
155
  });
152
156
  return `${String(approvedCount)} specs approved. Plan: ${waveParts.join(', ')}`;
153
157
  }
154
- function normalizeModel(model) {
155
- if (model === 'haiku' || model === 'sonnet' || model === 'opus') {
156
- return model;
158
+ /** Resolve current execution routing without requiring durable provider metadata. */
159
+ export function resolveExecutionTier(spec) {
160
+ const { difficulty, scope } = spec;
161
+ if (scope === 'cross-module' ||
162
+ scope === 'architectural' ||
163
+ difficulty === 4 ||
164
+ difficulty === 5) {
165
+ return 'opus';
157
166
  }
158
- return 'sonnet';
167
+ if (scope === 'trivial' || difficulty === 1 || difficulty === 2) {
168
+ return 'haiku';
169
+ }
170
+ if (scope === 'feature' || difficulty === 3) {
171
+ return 'sonnet';
172
+ }
173
+ // Compatibility for legacy records that predate required difficulty/scope metadata.
174
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
175
+ return normalizeLegacyTier(spec.model);
176
+ }
177
+ /** Preserve only an explicit legacy budget; never invent a token limit from a tier. */
178
+ export function resolveExecutionBudget(spec) {
179
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
180
+ const legacyBudget = spec.budget;
181
+ if (legacyBudget === 800 || legacyBudget === 2000 || legacyBudget === 4000) {
182
+ return legacyBudget;
183
+ }
184
+ return undefined;
185
+ }
186
+ function normalizeLegacyTier(value) {
187
+ return value === 'haiku' || value === 'sonnet' || value === 'opus' ? value : undefined;
159
188
  }
160
189
  async function readPlanVersion(projectPath) {
161
190
  try {