* test(#3531): failing-first suite for routing_tier_defaults manifest merge * fix(#3531): merge routing_tier_defaults over manifest tier defaults * docs(#3531): document routing_tier_defaults merge-over-built-ins semantics * fix(#3531): correct test helper scope, update folded #443 expectations, guard merge keys * test(#3531): pin tiers in effort-sync and surface-axis fixtures post-merge * chore(#3531): backfill changeset pr number * fix(#3531): correct rebase resolution — keep both 3531 and 3533 test blocks intact * test(#3531): pin inherit/effort fixtures to the layer that reaches tiered agents --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -23,6 +23,9 @@ import os from 'node:os';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- model-resolver.cjs is an export= CommonJS module
|
||||
import modelResolver = require('./model-resolver.cjs');
|
||||
const { EFFORT_SET: GSD_EFFORT_SET } = modelResolver as { EFFORT_SET: Set<string> };
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- model-catalog.cjs is an export= CommonJS module
|
||||
import modelCatalog = require('./model-catalog.cjs');
|
||||
const { mergeEffortTierDefaults } = modelCatalog as { mergeEffortTierDefaults: typeof import('./model-catalog.cjs').mergeEffortTierDefaults };
|
||||
|
||||
interface EffortConfig {
|
||||
agent_overrides?: Record<string, unknown>;
|
||||
@@ -161,6 +164,11 @@ function readGsdEffectiveEffortConfig(targetDir: string | null = null): EffortCo
|
||||
// Per-project wins on conflict within each sub-field. Merge field-by-field so
|
||||
// a project config that only sets agent_overrides still inherits global
|
||||
// routing_tier_defaults and default.
|
||||
// #3531 (10c): routing_tier_defaults is deep-merged per-tier like
|
||||
// agent_overrides — a project block naming only `heavy` must not discard the
|
||||
// home block's `light`/`standard` entries (the top-level spread would
|
||||
// otherwise replace the whole block, the same defect class as the
|
||||
// manifest-replacement this change fixes).
|
||||
return {
|
||||
...(homeEffort || {}),
|
||||
...(projectEffort || {}),
|
||||
@@ -169,6 +177,11 @@ function readGsdEffectiveEffortConfig(targetDir: string | null = null): EffortCo
|
||||
...((homeEffort && homeEffort.agent_overrides) || {}),
|
||||
...((projectEffort && projectEffort.agent_overrides) || {}),
|
||||
},
|
||||
// Deep-merge routing_tier_defaults (project wins per-tier)
|
||||
routing_tier_defaults: {
|
||||
...((homeEffort && homeEffort.routing_tier_defaults) || {}),
|
||||
...((projectEffort && projectEffort.routing_tier_defaults) || {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -179,8 +192,10 @@ function readGsdEffectiveEffortConfig(targetDir: string | null = null): EffortCo
|
||||
*
|
||||
* Precedence (mirrors resolveEffortInternal):
|
||||
* 1. effortCfg.agent_overrides[agentName]
|
||||
* 2. effortCfg.routing_tier_defaults[agentTier] (if effortCfg present)
|
||||
* — OR manifest tier defaults when effortCfg is null
|
||||
* 2. routing_tier_defaults merged over the manifest tier defaults
|
||||
* (#3531/10c: a config block — present or partial — no longer disables
|
||||
* the built-in tier ladder; invalid config values are dropped by the
|
||||
* merge so the manifest value for the tier surfaces)
|
||||
* 3. effortCfg.default
|
||||
* 4. 'high' (hardcoded fallback)
|
||||
*
|
||||
@@ -207,17 +222,14 @@ function resolveInstallTimeEffort(effortCfg: EffortConfig | null, agentName: str
|
||||
const { AGENT_DEFAULT_TIERS, EFFORT_MANIFEST_TIER_DEFAULTS, EFFORT_MANIFEST_DEFAULT } = _getGsdEffortCatalog();
|
||||
const agentTier = AGENT_DEFAULT_TIERS[agentName];
|
||||
if (agentTier) {
|
||||
if (effortCfg && effortCfg.routing_tier_defaults &&
|
||||
typeof effortCfg.routing_tier_defaults === 'object' &&
|
||||
!Array.isArray(effortCfg.routing_tier_defaults)) {
|
||||
const v = effortCfg.routing_tier_defaults[agentTier];
|
||||
if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v;
|
||||
} else if (!effortCfg) {
|
||||
// No effort config — use manifest tier defaults
|
||||
const v = EFFORT_MANIFEST_TIER_DEFAULTS[agentTier];
|
||||
if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v;
|
||||
}
|
||||
// effortCfg exists but has no routing_tier_defaults — fall through
|
||||
const isValidEffort = (v: unknown): v is string => typeof v === 'string' && GSD_EFFORT_SET.has(v);
|
||||
const merged = mergeEffortTierDefaults(
|
||||
EFFORT_MANIFEST_TIER_DEFAULTS,
|
||||
effortCfg ? effortCfg.routing_tier_defaults : undefined,
|
||||
isValidEffort,
|
||||
);
|
||||
const v = merged[agentTier];
|
||||
if (isValidEffort(v)) return v;
|
||||
}
|
||||
|
||||
// Step 3: effort.default
|
||||
|
||||
@@ -343,5 +343,36 @@ export function renderEffortForRuntime(runtime: string, universalEffort: string)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* #3531 (10c) — Merge a config `effort.routing_tier_defaults` block over the
|
||||
* manifest tier defaults instead of replacing them. A partial config must not
|
||||
* discard built-ins: per tier, a valid override value wins and an invalid one
|
||||
* is ignored so the manifest value for that tier surfaces (ADR-443 D1's
|
||||
* "invalid values fall through" holds within the merged layer).
|
||||
*
|
||||
* Pure: returns a new object and never mutates either input — the manifest
|
||||
* constants (`CANONICAL_CONFIG_DEFAULTS`, the catalog cache) stay frozen. The
|
||||
* validator is injected because `EFFORT_SET` lives in model-resolver, which
|
||||
* imports this leaf (a reverse import would be a cycle); both effort
|
||||
* resolvers pass their own `(v) => typeof v === 'string' && EFFORT_SET.has(v)`.
|
||||
*/
|
||||
export function mergeEffortTierDefaults(
|
||||
manifest: Record<string, string> | null | undefined,
|
||||
override: unknown,
|
||||
isValid: (v: unknown) => boolean,
|
||||
): Record<string, string> {
|
||||
const merged: Record<string, string> = { ...(manifest || {}) };
|
||||
if (override && typeof override === 'object' && !Array.isArray(override)) {
|
||||
for (const [tier, value] of Object.entries(override as Record<string, unknown>)) {
|
||||
// House pollution guard (mirrors _deepMergeConfig in config-loader): the
|
||||
// string-only validator already makes these inert, but an explicit skip
|
||||
// keeps this merge safe even if a caller's validator is ever relaxed.
|
||||
if (tier === '__proto__' || tier === 'constructor' || tier === 'prototype') continue;
|
||||
if (isValid(value)) merged[tier] = value as string;
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
// ─── Fast mode propagation ───────────────────────────────────────────────────
|
||||
export const RUNTIMES_WITH_FAST_MODE: Set<string> = new Set(['api']);
|
||||
|
||||
@@ -31,7 +31,7 @@ import { CONFIG_DEFAULTS as CANONICAL_CONFIG_DEFAULTS } from './configuration.cj
|
||||
import modelProfiles = require('./model-profiles.cjs');
|
||||
const { MODEL_PROFILES, AGENT_TO_PHASE_TYPE, AGENT_DEFAULT_TIERS, VALID_AGENT_TIERS, nextTier } = modelProfiles;
|
||||
|
||||
import { MODEL_ALIAS_MAP, RUNTIME_PROFILE_MAP, PROVIDER_PRESETS, VALID_TIERS, CLAUDE_AGENT_ALIASES } from './model-catalog.cjs';
|
||||
import { MODEL_ALIAS_MAP, RUNTIME_PROFILE_MAP, PROVIDER_PRESETS, VALID_TIERS, CLAUDE_AGENT_ALIASES, mergeEffortTierDefaults } from './model-catalog.cjs';
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
@@ -801,23 +801,26 @@ function resolveEffortInternal(cwd: string, agentType: string, opts?: EffortOpts
|
||||
}
|
||||
|
||||
// Step 3: routing_tier_defaults by agent's default tier.
|
||||
// #3531 (10c): the config block merges OVER the manifest tier defaults
|
||||
// rather than replacing them — an effort block without
|
||||
// routing_tier_defaults (or missing this agent's tier) falls back to the
|
||||
// manifest built-in for that tier instead of skipping to effort.default.
|
||||
// Invalid config values are dropped by the merge, so the manifest value for
|
||||
// the tier surfaces (the same "invalid falls through" rule every layer has).
|
||||
const agentTier = (AGENT_DEFAULT_TIERS)[agentType];
|
||||
if (agentTier) {
|
||||
if (effortCfg && effortCfg['routing_tier_defaults'] &&
|
||||
typeof effortCfg['routing_tier_defaults'] === 'object' &&
|
||||
!Array.isArray(effortCfg['routing_tier_defaults'])) {
|
||||
const v = (effortCfg['routing_tier_defaults'] as Record<string, unknown>)[agentTier];
|
||||
if (typeof v === 'string' && EFFORT_SET.has(v)) return v;
|
||||
} else if (!effortCfg) {
|
||||
const canonicalEffort = (CANONICAL_CONFIG_DEFAULTS)['effort'];
|
||||
const manifestDefaults = canonicalEffort && typeof canonicalEffort === 'object'
|
||||
? (canonicalEffort as Record<string, unknown>)['routing_tier_defaults']
|
||||
: undefined;
|
||||
if (manifestDefaults && typeof manifestDefaults === 'object') {
|
||||
const v = (manifestDefaults as Record<string, unknown>)[agentTier];
|
||||
if (typeof v === 'string' && EFFORT_SET.has(v)) return v;
|
||||
}
|
||||
}
|
||||
const canonicalEffort = (CANONICAL_CONFIG_DEFAULTS)['effort'];
|
||||
const manifestDefaults = canonicalEffort && typeof canonicalEffort === 'object'
|
||||
? (canonicalEffort as Record<string, unknown>)['routing_tier_defaults'] as Record<string, string> | undefined
|
||||
: undefined;
|
||||
const isValidEffort = (v: unknown): v is string => typeof v === 'string' && EFFORT_SET.has(v);
|
||||
const merged = mergeEffortTierDefaults(
|
||||
manifestDefaults,
|
||||
effortCfg ? effortCfg['routing_tier_defaults'] : undefined,
|
||||
isValidEffort,
|
||||
);
|
||||
const v = merged[agentTier];
|
||||
if (isValidEffort(v)) return v;
|
||||
}
|
||||
|
||||
// Step 4: effort.default
|
||||
|
||||
Reference in New Issue
Block a user