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}
35
36impl TranscriptParser {
37 /// Parse the captured events file into ordered tool invocations.
38 pub(crate) fn parse(self, path: &Path) -> io::Result<Vec<ToolInvocation>> {
39 match self {
40 TranscriptParser::ClaudeStreamJson => {
41 super::claude_code::stream_json::parse_claude_stream_json(path)
42 }
43 TranscriptParser::CodexItems => super::codex::transcript::parse_codex_events(path),
44 }
45 }
46
47 /// The full-summary counterpart of [`parse`](Self::parse).
48 pub(crate) fn parse_full(self, path: &Path) -> io::Result<TranscriptSummary> {
49 match self {
50 TranscriptParser::ClaudeStreamJson => {
51 super::claude_code::stream_json::parse_claude_stream_json_full(path)
52 }
53 TranscriptParser::CodexItems => super::codex::transcript::parse_codex_events_full(path),
54 }
55 }
56}
57
58/// Staged-slug generators, for harnesses whose naming rules need
59/// sanitization/truncation beyond a format string.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
61#[serde(rename_all = "kebab-case")]
62pub enum SlugCapability {
63 /// OpenCode's lowercase-alphanumeric-single-hyphen names, length-capped.
64 Opencode,
65}
66
67impl SlugCapability {
68 /// Generate the staged slug for one `(iteration, condition, skill)` cell.
69 pub(crate) fn staged_slug(
70 self,
71 prefix: &str,
72 iteration: u32,
73 condition: &str,
74 skill_name: &str,
75 ) -> String {
76 match self {
77 SlugCapability::Opencode => {
78 super::opencode::opencode_slug(prefix, iteration, condition, skill_name)
79 }
80 }
81 }
82}
83
84/// Shadow preflights: detect installed skills that shadow a staged slug.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
86#[serde(rename_all = "kebab-case")]
87pub enum ShadowPreflight {
88 /// Claude Code plugin/skill scan rooted at the user config dir.
89 ClaudePlugins,
90}
91
92impl ShadowPreflight {
93 /// Detect staged skill names that are also discoverable from the
94 /// operator's live environment. `None` when nothing is shadowed.
95 pub(crate) fn detect(
96 self,
97 scan_root: &Path,
98 staged_skill_names: &[&str],
99 ) -> Option<PluginShadowReport> {
100 match self {
101 ShadowPreflight::ClaudePlugins => super::claude_code::plugin_shadow::shadow_preflight(
102 &super::claude_code::plugin_shadow::config_dir_from_env(),
103 scan_root,
104 staged_skill_names,
105 ),
106 }
107 }
108}