@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.
- package/CHANGELOG.md +9 -0
- package/dist/config/project-knowledge-graph.json +42 -5
- package/dist/engine/core-bridge-project-graph.d.ts +13 -0
- package/dist/engine/core-bridge-project-graph.js +499 -0
- package/dist/engine/core-bridge.d.ts +7 -2
- package/dist/engine/core-bridge.js +55 -0
- package/dist/engine/frontmatter-parser.js +73 -23
- package/dist/engine/model-tier-resolver.d.ts +8 -7
- package/dist/engine/model-tier-resolver.js +70 -73
- package/dist/engine/next-spec-resolver/orchestration-planner.d.ts +5 -0
- package/dist/engine/next-spec-resolver/orchestration-planner.js +34 -5
- package/dist/engine/project-graph/builder.js +271 -36
- package/dist/engine/project-graph/cache.d.ts +22 -4
- package/dist/engine/project-graph/cache.js +412 -33
- package/dist/engine/project-graph/index.d.ts +1 -0
- package/dist/engine/project-graph/index.js +1 -0
- package/dist/engine/project-graph/native.d.ts +3 -0
- package/dist/engine/project-graph/native.js +36 -0
- package/dist/engine/project-graph/query.js +34 -2
- package/dist/engine/provider-adapters/adapters/claude.js +38 -14
- package/dist/engine/scan-project/index.js +88 -15
- package/dist/engine/spec-format/lean-spec-generator.d.ts +2 -2
- package/dist/engine/spec-format/lean-spec-generator.js +65 -50
- package/dist/engine/spec-format/metadata-value-policy.d.ts +161 -0
- package/dist/engine/spec-format/metadata-value-policy.js +87 -0
- package/dist/engine/spec-format/value-only-spec-serializer.d.ts +12 -0
- package/dist/engine/spec-format/value-only-spec-serializer.js +18 -0
- package/dist/engine/spec-generator/fallback-generator.js +4 -2
- package/dist/engine/spec-generator/opus-generator.js +5 -2
- package/dist/engine/spec-migrator/lean-migration.js +26 -13
- package/dist/storage/spec-store.js +6 -6
- package/dist/tools/create-spec.js +1027 -739
- package/dist/tools/render-spec-for-provider.js +4 -3
- package/dist/tools/reverse-engineer/handler.js +76 -43
- package/dist/tools/spec-split-handler.js +36 -75
- package/dist/types/conventions.d.ts +9 -0
- package/dist/types/core-bridge.d.ts +72 -0
- package/dist/types/next-spec.d.ts +2 -1
- package/dist/types/project-knowledge-graph.d.ts +58 -0
- package/dist/types/spec/core.d.ts +7 -4
- package/dist/types/spec-format.d.ts +2 -1
- package/dist/types/spec-generator.d.ts +8 -2
- package/package.json +11 -9
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
- package/dist/engine/spec-format/model-budget-deriver.d.ts +0 -5
- 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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
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
|
|
14
|
-
* Silent failure
|
|
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
|
-
*
|
|
25
|
-
*
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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.
|
|
141
|
-
*
|
|
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
|
|
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
|
|
153
|
-
STATIC_TIER_MAPPING.anthropic ?? { haiku: '', sonnet: '', opus: '' });
|
|
134
|
+
return undefined;
|
|
154
135
|
}
|
|
155
|
-
|
|
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
|
|
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
|
|
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
|
|
231
|
-
* Silent failure
|
|
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
|
-
|
|
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
|
-
|
|
242
|
+
const mapping = await fetchProviderModels(provider, apiKey);
|
|
243
|
+
if (mapping) {
|
|
244
|
+
providerMappings[provider] = mapping;
|
|
245
|
+
}
|
|
253
246
|
}));
|
|
254
|
-
const cache =
|
|
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
|
-
|
|
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
|
-
*
|
|
281
|
-
*
|
|
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
|
|
287
|
-
if (
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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 {
|