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:
+3
-2
@@ -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
@@ -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"
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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. |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user