Skip to main content

eval_magic/adapters/
capabilities.rs

1//! Named code capabilities a harness descriptor references.
2//!
3//! Everything a descriptor cannot express as data — transcript stitching,
4//! slug sanitization, plugin-shadow scanning — lives behind one of these
5//! closed enums. A descriptor opts in by naming the capability
6//! (`parser = "codex-items"`); a harness whose stream is compatible with an
7//! existing capability gets the full feature from configuration alone. (The
8//! write guard needs no named capability: its install and verdict render from
9//! the descriptor's `[guard]` data via [`super::guard`].)
10//!
11//! The enums deserialize from the kebab-case capability names the
12//! `harness-descriptor` schema also enumerates, so an unknown name fails the
13//! schema gate with a listed-allowed-values message before ever reaching Rust.
14
15use std::io;
16use std::path::Path;
17
18use serde::{Deserialize, Serialize};
19
20use crate::core::ToolInvocation;
21
22use super::TranscriptSummary;
23use super::skill_shadow::PluginShadowReport;
24
25/// Transcript parsers: turn a captured CLI events file into tool invocations
26/// and a [`super::TranscriptSummary`].
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
28#[serde(rename_all = "kebab-case")]
29pub enum TranscriptParser {
30    /// `claude -p --output-format stream-json` events.
31    ClaudeStreamJson,
32    /// `codex exec --json` `item.completed` events.
33    CodexItems,
34    /// `opencode run --format json` `tool_use`/`text`/`step_finish` events.
35    OpencodeEvents,
36}
37
38impl TranscriptParser {
39    /// Parse the captured events file into ordered tool invocations.
40    pub(crate) fn parse(self, path: &Path) -> io::Result<Vec<ToolInvocation>> {
41        match self {
42            TranscriptParser::ClaudeStreamJson => {
43                super::claude_code::stream_json::parse_claude_stream_json(path)
44            }
45            TranscriptParser::CodexItems => super::codex::transcript::parse_codex_events(path),
46            TranscriptParser::OpencodeEvents => {
47                super::opencode::transcript::parse_opencode_events(path)
48            }
49        }
50    }
51
52    /// The full-summary counterpart of [`parse`](Self::parse).
53    pub(crate) fn parse_full(self, path: &Path) -> io::Result<TranscriptSummary> {
54        match self {
55            TranscriptParser::ClaudeStreamJson => {
56                super::claude_code::stream_json::parse_claude_stream_json_full(path)
57            }
58            TranscriptParser::CodexItems => super::codex::transcript::parse_codex_events_full(path),
59            TranscriptParser::OpencodeEvents => {
60                super::opencode::transcript::parse_opencode_events_full(path)
61            }
62        }
63    }
64}
65
66/// Staged-slug generators, for harnesses whose naming rules need
67/// sanitization/truncation beyond a format string.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
69#[serde(rename_all = "kebab-case")]
70pub enum SlugCapability {
71    /// OpenCode's lowercase-alphanumeric-single-hyphen names, length-capped.
72    Opencode,
73}
74
75impl SlugCapability {
76    /// Generate the staged slug for one `(iteration, condition, skill)` cell.
77    pub(crate) fn staged_slug(
78        self,
79        prefix: &str,
80        iteration: u32,
81        condition: &str,
82        skill_name: &str,
83    ) -> String {
84        match self {
85            SlugCapability::Opencode => {
86                super::opencode::opencode_slug(prefix, iteration, condition, skill_name)
87            }
88        }
89    }
90}
91
92/// Shadow preflights: detect live skills that shadow a logical staged skill.
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
94#[serde(rename_all = "kebab-case")]
95pub enum ShadowPreflight {
96    /// Claude Code plugin/skill scan rooted at the user config dir.
97    ClaudePlugins,
98    /// Codex repo/user/admin/plugin skill scan.
99    CodexSkills,
100    /// OpenCode project/global `.opencode`/`.claude`/`.agents` skill scan.
101    OpencodeSkills,
102}
103
104impl ShadowPreflight {
105    /// Detect staged skill names that are also discoverable from the
106    /// operator's live environment. `None` when nothing is shadowed.
107    pub(crate) fn detect(
108        self,
109        scan_root: &Path,
110        staged_skill_names: &[&str],
111    ) -> Option<PluginShadowReport> {
112        match self {
113            ShadowPreflight::ClaudePlugins => super::claude_code::plugin_shadow::shadow_preflight(
114                &super::claude_code::plugin_shadow::config_dir_from_env(),
115                scan_root,
116                staged_skill_names,
117            ),
118            ShadowPreflight::CodexSkills => {
119                super::codex::skill_shadow::shadow_preflight(scan_root, staged_skill_names)
120            }
121            ShadowPreflight::OpencodeSkills => {
122                super::opencode::skill_shadow::shadow_preflight(scan_root, staged_skill_names)
123            }
124        }
125    }
126
127    /// Render the harness-specific build-time warning for a shadow report.
128    pub(crate) fn format_banner(self, report: &PluginShadowReport) -> String {
129        match self {
130            ShadowPreflight::ClaudePlugins => super::skill_shadow::format_shadow_banner(report),
131            ShadowPreflight::CodexSkills => {
132                super::codex::skill_shadow::format_shadow_banner(report)
133            }
134            ShadowPreflight::OpencodeSkills => {
135                super::opencode::skill_shadow::format_shadow_banner(report)
136            }
137        }
138    }
139
140    /// Render harness-specific aggregate validity warnings for a report.
141    pub(crate) fn validity_warnings(self, report: &PluginShadowReport) -> Vec<String> {
142        match self {
143            ShadowPreflight::ClaudePlugins => super::skill_shadow::shadow_validity_warnings(report),
144            ShadowPreflight::CodexSkills => {
145                super::codex::skill_shadow::shadow_validity_warnings(report)
146            }
147            ShadowPreflight::OpencodeSkills => {
148                super::opencode::skill_shadow::shadow_validity_warnings(report)
149            }
150        }
151    }
152}