feat(config): add dormant harness profile resolver

Add a pure HarnessProfile resolver for provider/model routes while keeping runtime provider/model routing, prompts, tools, auth, context, and persisted config unchanged.\n\nVerification:\n- cargo test -p codewhale-config harness_profile --locked\n- cargo fmt --all --check\n- git diff --check\n- cmp -s CHANGELOG.md crates/tui/CHANGELOG.md\n- ./scripts/release/check-versions.sh\n- ./scripts/release/check-ohos-deps.sh
This commit is contained in:
Hunter Bown
2026-06-06 01:58:17 -07:00
committed by GitHub
parent 35cd09a9f7
commit e60eeb8162
6 changed files with 180 additions and 15 deletions
+3 -2
View File
@@ -305,8 +305,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The config crate now carries the v0.9 HarnessPosture data model:
`HarnessPosture`, `HarnessProfile`, and typed posture/compaction/tool/safety
enums. The schema rejects misspelled posture names or unknown profile keys
instead of silently falling back to `custom`; runtime provider/model posture
selection remains a follow-up (#2693, #2741).
instead of silently falling back to `custom`; a pure resolver can match
provider/model routes for tests and future status plumbing, while runtime
provider/model posture selection remains a follow-up (#2693, #2741, #2728).
### Fixed
+5 -5
View File
@@ -606,12 +606,12 @@ deepseek_v4_flash_prior = 4.2
fallback_default_prior = 3.8
# ─────────────────────────────────────────────────────────────────────────────────
# Harness Profiles (preview schema; runtime resolver follows in later v0.9 slices)
# Harness Profiles (preview schema; runtime consumption follows later)
# ─────────────────────────────────────────────────────────────────────────────────
# Harness profiles let future CodeWhale runtime slices select model-specific prompt,
# context, tool, and subagent posture. v0.9 currently parses and validates this
# schema, but normal Agent and WhaleFlow runs do not silently promote or mutate
# behavior from these profiles yet.
# Harness profiles let future CodeWhale runtime slices select model-specific
# prompt, context, tool, and subagent posture. v0.9 parses, validates, and can
# resolve profiles for tests/status plumbing, but normal Agent and WhaleFlow
# runs do not silently promote or mutate behavior from these profiles yet.
#
# [[harness_profiles]]
# provider_route = "deepseek"
+161
View File
@@ -475,6 +475,18 @@ pub struct HarnessProfile {
pub posture: HarnessPosture,
}
impl HarnessProfile {
/// Return true when this profile applies to the provider/model route.
///
/// This is a pure config helper: matching a profile must not mutate runtime
/// provider selection, prompts, auth, tools, context, or persisted config.
#[must_use]
pub fn matches_route(&self, provider_route: &str, model: &str) -> bool {
provider_routes_equal(&self.provider_route, provider_route)
&& wildcard_pattern_matches(&self.model_pattern, model)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ConfigToml {
/// TUI-compatible DeepSeek API key. Kept at the root so both `deepseek`
@@ -535,6 +547,65 @@ pub struct ConfigToml {
pub extras: BTreeMap<String, toml::Value>,
}
impl ConfigToml {
/// Resolve the first configured harness profile for a provider/model route.
///
/// This helper is deliberately dormant for v0.9: callers may display or
/// test the resolved profile, but runtime provider/model routing and prompt
/// shaping remain unchanged until a later, explicit integration slice.
#[must_use]
pub fn resolve_harness_profile(
&self,
provider_route: &str,
model: &str,
) -> Option<&HarnessProfile> {
self.harness_profiles
.iter()
.find(|profile| profile.matches_route(provider_route, model))
}
}
fn provider_routes_equal(expected: &str, actual: &str) -> bool {
match (ProviderKind::parse(expected), ProviderKind::parse(actual)) {
(Some(expected), Some(actual)) => expected == actual,
_ => expected.trim().eq_ignore_ascii_case(actual.trim()),
}
}
fn wildcard_pattern_matches(pattern: &str, value: &str) -> bool {
wildcard_chars_match(
&pattern.chars().collect::<Vec<_>>(),
&value.chars().collect::<Vec<_>>(),
)
}
fn wildcard_chars_match(pattern: &[char], value: &[char]) -> bool {
let (mut pattern_idx, mut value_idx) = (0, 0);
let mut star_idx: Option<usize> = None;
let mut star_value_idx = 0;
while value_idx < value.len() {
if pattern_idx < pattern.len()
&& (pattern[pattern_idx] == '?' || pattern[pattern_idx] == value[value_idx])
{
pattern_idx += 1;
value_idx += 1;
} else if pattern_idx < pattern.len() && pattern[pattern_idx] == '*' {
star_idx = Some(pattern_idx);
pattern_idx += 1;
star_value_idx = value_idx;
} else if let Some(star) = star_idx {
pattern_idx = star + 1;
star_value_idx += 1;
value_idx = star_value_idx;
} else {
return false;
}
}
pattern[pattern_idx..].iter().all(|ch| *ch == '*')
}
/// Ordered primary-plus-fallback provider list for future provider routing.
///
/// The helper is intentionally dormant: constructing or parsing a chain does
@@ -5909,6 +5980,96 @@ safety_posture = "strict"
);
}
#[test]
fn harness_profile_matches_provider_alias_and_model_wildcard() {
let profile = HarnessProfile {
provider_route: "xiaomi-mimo".to_string(),
model_pattern: "mimo-v2.?-pro".to_string(),
posture: HarnessPosture::cache_heavy(),
};
assert!(profile.matches_route("mimo", "mimo-v2.5-pro"));
assert!(!profile.matches_route("mimo", "mimo-v2.50-pro"));
assert!(!profile.matches_route("deepseek", "mimo-v2.5-pro"));
}
#[test]
fn resolve_harness_profile_returns_first_matching_profile() {
let config = ConfigToml {
harness_profiles: vec![
HarnessProfile {
provider_route: "deepseek".to_string(),
model_pattern: "deepseek-v4-flash".to_string(),
posture: HarnessPosture::lean(),
},
HarnessProfile {
provider_route: "deepseek".to_string(),
model_pattern: "deepseek-v4-*".to_string(),
posture: HarnessPosture::cache_heavy(),
},
],
..ConfigToml::default()
};
let flash = config
.resolve_harness_profile("deepseek-cn", "deepseek-v4-flash")
.expect("exact profile should match first");
assert_eq!(flash.posture.kind, HarnessPostureKind::Lean);
let pro = config
.resolve_harness_profile("deepseek", "deepseek-v4-pro")
.expect("wildcard profile should match pro model");
assert_eq!(pro.posture.kind, HarnessPostureKind::CacheHeavy);
}
#[test]
fn resolve_harness_profile_returns_none_when_route_or_model_misses() {
let config = ConfigToml {
harness_profiles: vec![HarnessProfile {
provider_route: "huggingface".to_string(),
model_pattern: "deepseek-ai/*".to_string(),
posture: HarnessPosture::lean(),
}],
..ConfigToml::default()
};
assert!(
config
.resolve_harness_profile("openrouter", "deepseek-ai/DeepSeek-V4-Pro")
.is_none()
);
assert!(
config
.resolve_harness_profile("hf", "Qwen/Qwen3.6-Coder")
.is_none()
);
}
#[test]
fn resolving_harness_profile_does_not_change_runtime_options() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
let config = ConfigToml {
provider: ProviderKind::Deepseek,
model: Some("deepseek-v4-pro".to_string()),
harness_profiles: vec![HarnessProfile {
provider_route: "deepseek".to_string(),
model_pattern: "deepseek-v4-*".to_string(),
posture: HarnessPosture::lean(),
}],
..ConfigToml::default()
};
let profile = config
.resolve_harness_profile("deepseek", "deepseek-v4-pro")
.expect("profile should resolve for display/future runtime");
assert_eq!(profile.posture.kind, HarnessPostureKind::Lean);
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::Deepseek);
assert_eq!(resolved.model, "deepseek-v4-pro");
}
#[test]
fn harness_posture_kind_rejects_unknown_values() {
let err = toml::from_str::<ConfigToml>(
+3 -2
View File
@@ -305,8 +305,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The config crate now carries the v0.9 HarnessPosture data model:
`HarnessPosture`, `HarnessProfile`, and typed posture/compaction/tool/safety
enums. The schema rejects misspelled posture names or unknown profile keys
instead of silently falling back to `custom`; runtime provider/model posture
selection remains a follow-up (#2693, #2741).
instead of silently falling back to `custom`; a pure resolver can match
provider/model routes for tests and future status plumbing, while runtime
provider/model posture selection remains a follow-up (#2693, #2741, #2728).
### Fixed
+7 -5
View File
@@ -59,10 +59,10 @@ inspiration only after the text distinguishes these stages:
No v0.9.0 harness profile should be silently promoted, mutated, or written to a
cached-main overlay by the schema/resolver/display lane.
## Smoke Evidence Needed Before Release
## Smoke Evidence
Before v0.9.0 ships with HarnessProfile behavior beyond schema parsing, the
acceptance matrix should record evidence for:
Before v0.9.0 ships with HarnessProfile runtime behavior beyond schema parsing
and pure resolver checks, the acceptance matrix should record evidence for:
- DeepSeek V4 resolving to a cache-heavy profile;
- Xiaomi MiMo resolving to a cache-heavy profile without sharing DeepSeek auth;
@@ -73,5 +73,7 @@ acceptance matrix should record evidence for:
constitution separately;
- no automatic profile mutation during normal Agent or WhaleFlow runs.
Until that evidence exists, release notes should call HarnessProfile a typed
schema/config foundation rather than an automatic harness creator.
For v0.9.0, pure resolver tests may satisfy the profile-selection evidence, but
status display and runtime use remain deferred until separate PRs wire those
surfaces deliberately. Release notes should still call HarnessProfile a typed
schema/resolver foundation rather than an automatic harness creator.
+1 -1
View File
@@ -62,7 +62,7 @@ config source, result, and follow-up issue or PR.
| WhaleFlow typed IR, mock executor, replay, TeacherReview, StudentReplay, and cutline docs are tested | WhaleFlow steward | ship | #2821/#2824/#2831/#2833/#2839/#2840/#2841 plus focused local `cargo test -p codewhale-whaleflow --locked`; #2670 closed after `cargo test -p codewhale-whaleflow starlark --locked` passed 7/7 on current stewardship head. |
| Live `workflow_run`, worktree application, provider calls, and TraceStore writes are deferred until cancellation/replay/atomicity semantics pass | WhaleFlow steward | defer | #2669 and #2679 remain open for live runtime execution, provider calls, TraceStore writes, Arcee/student replay, and CLI/TUI workflow mode; current v0.9 branch ships mock executor/replay foundations only. |
| Model Lab / Hugging Face MVP is included or deferred with release-note wording | model-lab steward | decide | |
| HarnessProfile runtime MVP is deferred; schema/config foundation ships with release-note wording | harness steward | ship foundation / defer runtime | #2844 (`efbcc681a`) documents the cutline; `HarnessPosture` / `HarnessProfile` config schema and strict validation are present; resolver, seed-profile runtime selection, telemetry, and status display remain follow-up work. |
| HarnessProfile runtime MVP is deferred; schema/resolver foundation ships with release-note wording | harness steward | ship foundation / defer runtime | #2844 (`efbcc681a`) documents the cutline; `HarnessPosture` / `HarnessProfile` config schema and strict validation are present; a pure resolver matches provider/model routes without changing runtime behavior; seed-profile runtime selection, telemetry, and status display remain follow-up work. |
| `codebase_search` MVP is included or deferred with release-note wording | search steward | decide | |
| External memory remains explicit/optional per `WHALEFLOW_EXTERNAL_MEMORY.md` | memory steward | ship | #2842 (`a7052751e`) added the external-memory cutline: optional/explicit workflow node/plugin only, visible state/owner/storage/scope, and no hidden default context substrate. |