Skip to main content

a3s_code_core/
meta_harness.rs

1//! Non-bypassable Meta Harness kernel policy and ordered assemble.
2//!
3//! Hosts mount ordered Moore components on one fact log (`components: [...]`).
4//! Entries are stock parts (`system` / `tools` / `budget` / `compact` / `infer`)
5//! or host registry mounts (`host:<id>`). Permission projection and the
6//! completion gate stay Core-owned: a composed graph cannot disable them.
7//!
8//! Full Rust arbitrary trees use [`HostHarnessAssembler`] or
9//! [`admit_component_tree`]. SDKs pass the declarative recipe; host Moore
10//! factories stay Rust-side (no second imperative loop, no Effect-TS embed).
11
12use a3s_effect::{
13    budget, coding_scheduler, compact, component, compose_coding_actor, system, tools,
14    CodingServices, ErasedComponent, HarnessConfig, HarnessGraph, HarnessView, MetaHarnessSpec,
15    ToolSpec,
16};
17
18pub use a3s_effect::HarnessPartId;
19
20/// One entry in an ordered `components: [...]` assemble list.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum HarnessComponentRef {
23    Stock(HarnessPartId),
24    /// Registry id without the `host:` prefix.
25    Host(String),
26}
27
28/// Host-facing compose recipe for SessionOptions / SDKs.
29#[derive(Debug, Clone, Default, PartialEq, Eq)]
30pub struct HarnessComposeOptions {
31    /// Tool-call budget for the stock scheduler (`budget` part).
32    pub tool_budget: Option<u32>,
33    /// Compaction character threshold (`compact` part).
34    pub compact_after_chars: Option<usize>,
35    /// Extra system prompts merged into the `system` part.
36    pub system: Vec<String>,
37    /// Ordered stock parts. Used when [`Self::components`] is empty.
38    pub parts: Vec<HarnessPartId>,
39    /// Ordered assemble list. When non-empty, takes precedence over
40    /// [`Self::parts`]. Entries are stock names or `host:<id>`.
41    pub components: Vec<String>,
42}
43
44impl HarnessComposeOptions {
45    pub fn to_spec(&self, tools: Vec<ToolSpec>, defaults: &HarnessConfig) -> MetaHarnessSpec {
46        MetaHarnessSpec {
47            name: "a3s-code",
48            budget: self.tool_budget.unwrap_or_else(|| defaults.budget()),
49            compact_after_chars: self
50                .compact_after_chars
51                .unwrap_or_else(|| defaults.compact_after_chars()),
52            step_limit: defaults.step_limit(),
53            model_attempts: defaults.model_attempts(),
54            system: if self.system.is_empty() {
55                defaults.system().to_vec()
56            } else {
57                self.system.clone()
58            },
59            tools,
60            tool_round_cap: defaults.tool_round_cap(),
61            parts: self.parts.clone(),
62        }
63    }
64
65    /// Resolve the ordered component list (stock + host mounts).
66    pub fn resolved_components(&self) -> anyhow::Result<Vec<HarnessComponentRef>> {
67        if !self.components.is_empty() {
68            return parse_harness_components(&self.components);
69        }
70        if !self.parts.is_empty() {
71            return Ok(self
72                .parts
73                .iter()
74                .copied()
75                .map(HarnessComponentRef::Stock)
76                .collect());
77        }
78        Ok(default_stock_components())
79    }
80
81    /// Build from an ordered `components: [...]` list.
82    ///
83    /// Stock names: `system`, `tools`, `budget`, `compact`, `infer`.
84    /// Host mounts: `host:<id>` (resolved at admit time via
85    /// [`HostHarnessRegistry`]).
86    pub fn compose(
87        components: Vec<String>,
88        tool_budget: Option<u32>,
89        compact_after_chars: Option<usize>,
90        system: Vec<String>,
91    ) -> anyhow::Result<Self> {
92        let refs = parse_harness_components(&components)?;
93        let (stock_parts, host_ids) = partition_components(&refs);
94        // Host mounts force the components-list admit path; stock-only keeps
95        // MetaHarnessSpec / cause-key stability via `parts`.
96        let parts = if host_ids.is_empty() {
97            stock_parts
98        } else {
99            Vec::new()
100        };
101        Ok(Self {
102            tool_budget,
103            compact_after_chars,
104            system,
105            parts,
106            components,
107        })
108    }
109}
110
111/// Default stock order when neither `components` nor `parts` is set.
112fn default_stock_components() -> Vec<HarnessComponentRef> {
113    vec![
114        HarnessComponentRef::Stock(HarnessPartId::System),
115        HarnessComponentRef::Stock(HarnessPartId::Tools),
116        HarnessComponentRef::Stock(HarnessPartId::Budget),
117        HarnessComponentRef::Stock(HarnessPartId::Compact),
118        HarnessComponentRef::Stock(HarnessPartId::Infer),
119    ]
120}
121
122/// Split an assemble list into stock parts and host mount ids (order preserved
123/// within each side). Used by compose admission so both arms are real.
124fn partition_components(refs: &[HarnessComponentRef]) -> (Vec<HarnessPartId>, Vec<String>) {
125    let mut stock = Vec::new();
126    let mut hosts = Vec::new();
127    for entry in refs {
128        match entry {
129            HarnessComponentRef::Stock(part) => stock.push(*part),
130            HarnessComponentRef::Host(id) => hosts.push(id.clone()),
131        }
132    }
133    (stock, hosts)
134}
135
136/// Parse a single stock Meta Harness part id.
137pub fn parse_harness_part(name: &str) -> anyhow::Result<HarnessPartId> {
138    match name.trim().to_ascii_lowercase().as_str() {
139        "system" => Ok(HarnessPartId::System),
140        "tools" => Ok(HarnessPartId::Tools),
141        "budget" => Ok(HarnessPartId::Budget),
142        "compact" => Ok(HarnessPartId::Compact),
143        "infer" => Ok(HarnessPartId::Infer),
144        other => anyhow::bail!(
145            "unknown harness part '{other}'; expected system|tools|budget|compact|infer"
146        ),
147    }
148}
149
150/// Parse ordered stock part names into [`HarnessPartId`] values.
151pub fn parse_harness_parts(parts: &[String]) -> anyhow::Result<Vec<HarnessPartId>> {
152    parts.iter().map(|part| parse_harness_part(part)).collect()
153}
154
155/// Parse a stock name or `host:<id>` assemble entry.
156pub fn parse_harness_component(name: &str) -> anyhow::Result<HarnessComponentRef> {
157    let trimmed = name.trim();
158    if let Some(id) = trimmed
159        .strip_prefix("host:")
160        .or_else(|| trimmed.strip_prefix("HOST:"))
161    {
162        let id = id.trim();
163        if id.is_empty() {
164            anyhow::bail!("host harness mount requires a non-empty id after 'host:'");
165        }
166        if id.contains(|ch: char| !(ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')) {
167            anyhow::bail!("invalid host harness id '{id}': use ascii alphanumeric, '_' or '-'");
168        }
169        return Ok(HarnessComponentRef::Host(id.to_ascii_lowercase()));
170    }
171    Ok(HarnessComponentRef::Stock(parse_harness_part(trimmed)?))
172}
173
174/// Parse an ordered `components: [...]` list.
175pub fn parse_harness_components(components: &[String]) -> anyhow::Result<Vec<HarnessComponentRef>> {
176    components
177        .iter()
178        .map(|entry| parse_harness_component(entry))
179        .collect()
180}
181
182/// Format a host mount id as a `components: [...]` entry.
183pub fn host_component_id(id: impl AsRef<str>) -> String {
184    format!("host:{}", id.as_ref().trim().to_ascii_lowercase())
185}
186
187/// Kernel flags that host components cannot clear.
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub struct KernelPolicy {
190    /// Permission overlay strips tool definitions before the model sees them.
191    pub permission_overlay: bool,
192    /// Mutating runs require verification / host waiver bound to an effect digest.
193    pub completion_gate: bool,
194}
195
196impl Default for KernelPolicy {
197    fn default() -> Self {
198        Self {
199            permission_overlay: true,
200            completion_gate: true,
201        }
202    }
203}
204
205impl KernelPolicy {
206    /// Hosts may not construct a policy that disables governance.
207    pub fn admit(self) -> Self {
208        Self {
209            permission_overlay: true,
210            completion_gate: true,
211        }
212    }
213}
214
215/// Host-authored Moore component factory (Rust-side; not model-grantable).
216pub trait HostHarnessRegistry: Send + Sync {
217    /// Build one host component for `id` (without the `host:` prefix).
218    fn mount(
219        &self,
220        id: &str,
221        config: &HarnessConfig,
222    ) -> anyhow::Result<ErasedComponent<CodingServices, HarnessView>>;
223}
224
225/// Full custom graph builder for Rust embedders (arbitrary `compose_coding_actor`).
226pub trait HostHarnessAssembler: Send + Sync {
227    fn assemble(&self, config: HarnessConfig) -> anyhow::Result<HarnessGraph>;
228}
229
230/// Built-in registry with the `intent_stamp` host component.
231///
232/// Injects a stable system line so compose trees can prove host mounts without
233/// embedding a second loop. Used by hermetic and Layer C Meta Harness suites.
234#[derive(Debug, Default, Clone, Copy)]
235pub struct BuiltinHostHarnessRegistry;
236
237/// Stable marker injected by [`BuiltinHostHarnessRegistry`] `intent_stamp`.
238pub const INTENT_STAMP_MARKER: &str = "a3s.meta_harness.intent_stamp.v1";
239
240/// Moore output for the builtin `intent_stamp` host mount.
241fn intent_stamp_view() -> a3s_effect::CodingView {
242    a3s_effect::CodingView {
243        system: vec![INTENT_STAMP_MARKER.into()],
244        ..a3s_effect::CodingView::empty()
245    }
246}
247
248impl HostHarnessRegistry for BuiltinHostHarnessRegistry {
249    fn mount(
250        &self,
251        id: &str,
252        _config: &HarnessConfig,
253    ) -> anyhow::Result<ErasedComponent<CodingServices, HarnessView>> {
254        match id {
255            "intent_stamp" => Ok(component(
256                || (),
257                |state, _fact| state,
258                |_state| (intent_stamp_view(), Vec::new()),
259            )),
260            other => anyhow::bail!(
261                "unknown builtin host harness component '{other}'; known: intent_stamp"
262            ),
263        }
264    }
265}
266
267/// Build the default graph under kernel policy.
268pub fn admit_default_graph(config: HarnessConfig) -> (HarnessGraph, KernelPolicy) {
269    (
270        HarnessGraph::coding(config),
271        KernelPolicy::default().admit(),
272    )
273}
274
275/// Build a graph from a Meta Harness stock-only spec under kernel policy.
276pub fn admit_spec_graph(spec: MetaHarnessSpec) -> anyhow::Result<(HarnessGraph, KernelPolicy)> {
277    let config = spec
278        .clone()
279        .into_config()
280        .map_err(|error| anyhow::anyhow!(error))?;
281    let graph = HarnessGraph::from_spec(spec, || coding_scheduler(config.clone()));
282    Ok((graph, KernelPolicy::default().admit()))
283}
284
285/// Admit an explicit component tree under kernel policy.
286pub fn admit_component_tree(
287    name: &'static str,
288    components: Vec<ErasedComponent<CodingServices, HarnessView>>,
289) -> (HarnessGraph, KernelPolicy) {
290    (
291        HarnessGraph::from_actor(compose_coding_actor(name, components)),
292        KernelPolicy::default().admit(),
293    )
294}
295
296/// Resolve SessionOptions harness compose into an admitted graph.
297pub fn admit_from_compose(
298    compose: Option<&HarnessComposeOptions>,
299    config: HarnessConfig,
300) -> anyhow::Result<(HarnessGraph, KernelPolicy)> {
301    admit_from_compose_with_registry(compose, None, config)
302}
303
304/// Resolve compose + optional host registry into an admitted graph.
305pub fn admit_from_compose_with_registry(
306    compose: Option<&HarnessComposeOptions>,
307    registry: Option<&dyn HostHarnessRegistry>,
308    config: HarnessConfig,
309) -> anyhow::Result<(HarnessGraph, KernelPolicy)> {
310    let Some(options) = compose else {
311        return Ok(admit_default_graph(config));
312    };
313    let refs = options.resolved_components()?;
314    let needs_host = refs
315        .iter()
316        .any(|entry| matches!(entry, HarnessComponentRef::Host(_)));
317    if !needs_host {
318        // Stock-only path keeps MetaHarnessSpec / cause-key stability.
319        let tools = config.tools().to_vec();
320        let mut spec = options.to_spec(tools, &config);
321        if !options.components.is_empty() {
322            let (stock_parts, _) = partition_components(&refs);
323            spec.parts = stock_parts;
324        }
325        return admit_spec_graph(spec);
326    }
327    let registry = registry.ok_or_else(|| {
328        anyhow::anyhow!(
329            "harness components include host:* mounts but no HostHarnessRegistry was installed"
330        )
331    })?;
332    admit_mixed_tree(options, refs, registry, config)
333}
334
335fn admit_mixed_tree(
336    options: &HarnessComposeOptions,
337    refs: Vec<HarnessComponentRef>,
338    registry: &dyn HostHarnessRegistry,
339    config: HarnessConfig,
340) -> anyhow::Result<(HarnessGraph, KernelPolicy)> {
341    let system_prompts = if options.system.is_empty() {
342        config.system().to_vec()
343    } else {
344        options.system.clone()
345    };
346    let tool_specs = config.tools().to_vec();
347    let budget_limit = options.tool_budget.unwrap_or_else(|| config.budget());
348    let compact_after = options
349        .compact_after_chars
350        .unwrap_or_else(|| config.compact_after_chars());
351
352    let mut components = Vec::with_capacity(refs.len());
353    for entry in refs {
354        match entry {
355            HarnessComponentRef::Stock(HarnessPartId::System) => {
356                components.push(system(system_prompts.clone()));
357            }
358            HarnessComponentRef::Stock(HarnessPartId::Tools) => {
359                components.push(tools(tool_specs.clone()));
360            }
361            HarnessComponentRef::Stock(HarnessPartId::Budget) => {
362                components.push(budget(budget_limit));
363            }
364            HarnessComponentRef::Stock(HarnessPartId::Compact) => {
365                components.push(compact(compact_after));
366            }
367            HarnessComponentRef::Stock(HarnessPartId::Infer) => {
368                components.push(coding_scheduler(config.clone()));
369            }
370            HarnessComponentRef::Host(id) => {
371                components.push(registry.mount(&id, &config)?);
372            }
373        }
374    }
375    Ok(admit_component_tree("a3s-code", components))
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381
382    #[test]
383    fn kernel_policy_cannot_disable_governance() {
384        let cleared = KernelPolicy {
385            permission_overlay: false,
386            completion_gate: false,
387        };
388        let admitted = cleared.admit();
389        assert!(admitted.permission_overlay);
390        assert!(admitted.completion_gate);
391    }
392
393    #[test]
394    fn default_admission_builds_stock_graph() {
395        let config = HarnessConfig::new(4, 1_000, 8, 1, vec!["s".into()], vec![]).expect("config");
396        let (graph, policy) = admit_default_graph(config);
397        assert_eq!(graph.actor().name, "a3s-code");
398        assert!(policy.permission_overlay);
399        assert!(policy.completion_gate);
400    }
401
402    #[test]
403    fn compose_options_override_budget() {
404        let defaults = HarnessConfig::new(8, 1_000, 8, 1, vec![], vec![]).expect("config");
405        let options = HarnessComposeOptions {
406            tool_budget: Some(3),
407            compact_after_chars: Some(50),
408            system: vec!["compose".into()],
409            parts: vec![HarnessPartId::System, HarnessPartId::Infer],
410            components: Vec::new(),
411        };
412        let spec = options.to_spec(Vec::new(), &defaults);
413        assert_eq!(spec.budget, 3);
414        assert_eq!(spec.compact_after_chars, 50);
415        assert_eq!(spec.system, vec!["compose".to_string()]);
416        assert_eq!(
417            spec.parts,
418            vec![HarnessPartId::System, HarnessPartId::Infer]
419        );
420    }
421
422    #[test]
423    fn compose_parses_stock_part_names() {
424        let options = HarnessComposeOptions::compose(
425            vec![
426                "system".into(),
427                "tools".into(),
428                "budget".into(),
429                "compact".into(),
430                "infer".into(),
431            ],
432            Some(2),
433            Some(10),
434            vec!["s".into()],
435        )
436        .expect("compose");
437        assert_eq!(
438            options.parts,
439            vec![
440                HarnessPartId::System,
441                HarnessPartId::Tools,
442                HarnessPartId::Budget,
443                HarnessPartId::Compact,
444                HarnessPartId::Infer,
445            ]
446        );
447        assert!(parse_harness_part("parallel_task").is_err());
448    }
449
450    #[test]
451    fn components_list_accepts_host_mount_and_reorder() {
452        let options = HarnessComposeOptions::compose(
453            vec![
454                "system".into(),
455                host_component_id("intent_stamp"),
456                "tools".into(),
457                "budget".into(),
458                "infer".into(),
459            ],
460            Some(2),
461            None,
462            vec!["base".into()],
463        )
464        .expect("compose");
465        assert!(
466            options.parts.is_empty(),
467            "host mounts force components path"
468        );
469        let refs = options.resolved_components().expect("refs");
470        assert_eq!(
471            refs,
472            vec![
473                HarnessComponentRef::Stock(HarnessPartId::System),
474                HarnessComponentRef::Host("intent_stamp".into()),
475                HarnessComponentRef::Stock(HarnessPartId::Tools),
476                HarnessComponentRef::Stock(HarnessPartId::Budget),
477                HarnessComponentRef::Stock(HarnessPartId::Infer),
478            ]
479        );
480    }
481
482    #[test]
483    fn host_mount_without_registry_fails_closed() {
484        let config = HarnessConfig::new(2, 100, 8, 1, vec![], vec![]).expect("config");
485        let options = HarnessComposeOptions::compose(
486            vec![
487                "system".into(),
488                host_component_id("intent_stamp"),
489                "infer".into(),
490            ],
491            None,
492            None,
493            vec![],
494        )
495        .expect("compose");
496        let err = match admit_from_compose_with_registry(Some(&options), None, config) {
497            Ok(_) => panic!("missing registry must fail"),
498            Err(error) => error,
499        };
500        assert!(err.to_string().contains("HostHarnessRegistry"));
501    }
502
503    #[test]
504    fn builtin_registry_admits_mixed_tree() {
505        let config = HarnessConfig::new(2, 100, 8, 1, vec!["sys".into()], vec![]).expect("config");
506        let options = HarnessComposeOptions::compose(
507            vec![
508                "system".into(),
509                "tools".into(),
510                host_component_id("intent_stamp"),
511                "budget".into(),
512                "infer".into(),
513            ],
514            Some(2),
515            None,
516            vec!["sys".into()],
517        )
518        .expect("compose");
519        let (graph, policy) = admit_from_compose_with_registry(
520            Some(&options),
521            Some(&BuiltinHostHarnessRegistry),
522            config,
523        )
524        .expect("admit");
525        assert_eq!(graph.actor().name, "a3s-code");
526        assert!(policy.permission_overlay && policy.completion_gate);
527    }
528
529    #[test]
530    fn unknown_host_id_fails_closed() {
531        let config = HarnessConfig::new(2, 100, 8, 1, vec![], vec![]).expect("config");
532        let options = HarnessComposeOptions::compose(
533            vec!["system".into(), host_component_id("nope"), "infer".into()],
534            None,
535            None,
536            vec![],
537        )
538        .expect("compose");
539        let err = match admit_from_compose_with_registry(
540            Some(&options),
541            Some(&BuiltinHostHarnessRegistry),
542            config,
543        ) {
544            Ok(_) => panic!("unknown host must fail"),
545            Err(error) => error,
546        };
547        assert!(err.to_string().contains("unknown builtin"));
548    }
549
550    #[test]
551    fn reject_empty_and_invalid_host_ids() {
552        assert!(parse_harness_component("host:").is_err());
553        assert!(parse_harness_component("host:bad.id").is_err());
554        assert!(parse_harness_component("parallel_task").is_err());
555    }
556
557    #[test]
558    fn admit_component_tree_preserves_kernel() {
559        let (_graph, policy) = admit_component_tree(
560            "custom",
561            vec![system(vec!["x".into()]), budget(1), compact(8)],
562        );
563        assert!(policy.permission_overlay);
564        assert!(policy.completion_gate);
565    }
566
567    #[test]
568    fn stock_only_components_list_uses_spec_path() {
569        let config = HarnessConfig::new(3, 50, 8, 1, vec![], vec![]).expect("config");
570        let options = HarnessComposeOptions::compose(
571            vec![
572                "system".into(),
573                "tools".into(),
574                "budget".into(),
575                "infer".into(),
576            ],
577            Some(3),
578            None,
579            vec![],
580        )
581        .expect("compose");
582        // No compact part — intentional subset assemble.
583        assert!(!options
584            .resolved_components()
585            .unwrap()
586            .iter()
587            .any(|entry| matches!(entry, HarnessComponentRef::Stock(HarnessPartId::Compact))));
588        let (graph, _) =
589            admit_from_compose_with_registry(Some(&options), None, config).expect("admit");
590        assert_eq!(graph.actor().name, "a3s-code");
591    }
592
593    #[test]
594    fn resolved_components_falls_back_to_parts_then_default_stock() {
595        let from_parts = HarnessComposeOptions {
596            parts: vec![HarnessPartId::System, HarnessPartId::Infer],
597            ..HarnessComposeOptions::default()
598        };
599        assert_eq!(
600            from_parts.resolved_components().expect("parts"),
601            vec![
602                HarnessComponentRef::Stock(HarnessPartId::System),
603                HarnessComponentRef::Stock(HarnessPartId::Infer),
604            ]
605        );
606
607        let defaults = HarnessComposeOptions::default()
608            .resolved_components()
609            .expect("default stock");
610        assert_eq!(defaults, default_stock_components());
611    }
612
613    #[test]
614    fn parse_harness_parts_accepts_stock_names_and_rejects_unknown() {
615        let parts = parse_harness_parts(&[
616            "System".into(),
617            "TOOLS".into(),
618            "budget".into(),
619            "compact".into(),
620            "infer".into(),
621        ])
622        .expect("stock names");
623        assert_eq!(
624            parts,
625            vec![
626                HarnessPartId::System,
627                HarnessPartId::Tools,
628                HarnessPartId::Budget,
629                HarnessPartId::Compact,
630                HarnessPartId::Infer,
631            ]
632        );
633        assert!(parse_harness_parts(&["nope".into()]).is_err());
634    }
635
636    #[test]
637    fn admit_from_compose_wrapper_defaults_and_admits_stock() {
638        let config = HarnessConfig::new(2, 100, 8, 1, vec![], vec![]).expect("config");
639        let (default_graph, policy) = admit_from_compose(None, config.clone()).expect("default");
640        assert_eq!(default_graph.actor().name, "a3s-code");
641        assert!(policy.permission_overlay && policy.completion_gate);
642
643        let options = HarnessComposeOptions {
644            parts: vec![
645                HarnessPartId::System,
646                HarnessPartId::Budget,
647                HarnessPartId::Infer,
648            ],
649            tool_budget: Some(2),
650            ..HarnessComposeOptions::default()
651        };
652        let (graph, _) = admit_from_compose(Some(&options), config).expect("stock");
653        assert_eq!(graph.actor().name, "a3s-code");
654    }
655
656    #[test]
657    fn mixed_tree_includes_compact_and_empty_system_fallback() {
658        let config =
659            HarnessConfig::new(2, 40, 8, 1, vec!["cfg-system".into()], vec![]).expect("config");
660        let options = HarnessComposeOptions::compose(
661            vec![
662                "system".into(),
663                "tools".into(),
664                "budget".into(),
665                "compact".into(),
666                host_component_id("intent_stamp"),
667                "infer".into(),
668            ],
669            Some(2),
670            Some(40),
671            vec![], // fall back to config.system()
672        )
673        .expect("compose");
674        let (graph, policy) = admit_from_compose_with_registry(
675            Some(&options),
676            Some(&BuiltinHostHarnessRegistry),
677            config,
678        )
679        .expect("admit mixed with compact");
680        assert_eq!(graph.actor().name, "a3s-code");
681        assert!(policy.permission_overlay && policy.completion_gate);
682    }
683
684    #[test]
685    fn partition_and_intent_stamp_view_are_behavior_oracles() {
686        let refs = vec![
687            HarnessComponentRef::Stock(HarnessPartId::System),
688            HarnessComponentRef::Host("intent_stamp".into()),
689            HarnessComponentRef::Stock(HarnessPartId::Infer),
690        ];
691        let (stock, hosts) = partition_components(&refs);
692        assert_eq!(stock, vec![HarnessPartId::System, HarnessPartId::Infer]);
693        assert_eq!(hosts, vec!["intent_stamp".to_string()]);
694
695        let view = intent_stamp_view();
696        assert_eq!(view.system, vec![INTENT_STAMP_MARKER.to_string()]);
697    }
698
699    #[test]
700    fn host_prefix_is_case_insensitive_and_normalizes_id() {
701        let upper = parse_harness_component("HOST:Intent_Stamp").expect("HOST:");
702        assert_eq!(upper, HarnessComponentRef::Host("intent_stamp".into()));
703        assert_eq!(
704            host_component_id(" Intent_Stamp "),
705            "host:intent_stamp".to_string()
706        );
707    }
708
709    #[test]
710    fn to_spec_keeps_default_system_when_compose_system_empty() {
711        let defaults =
712            HarnessConfig::new(4, 10, 8, 1, vec!["default-sys".into()], vec![]).expect("config");
713        let options = HarnessComposeOptions {
714            tool_budget: None,
715            compact_after_chars: None,
716            system: vec![],
717            parts: vec![HarnessPartId::System],
718            components: Vec::new(),
719        };
720        let spec = options.to_spec(Vec::new(), &defaults);
721        assert_eq!(spec.budget, 4);
722        assert_eq!(spec.compact_after_chars, 10);
723        assert_eq!(spec.system, vec!["default-sys".to_string()]);
724    }
725}