Skip to main content

majordomus_cli/capability/builtin/
economics.rs

1//! The `economics` module: token economics, measured, projected. Every capability here is a
2//! read of [`crate::economics`] — the one calculator — so the command line, HTTP, the
3//! OpenAPI document, MCP and the Cockpit show the same metric, value, class and sample
4//! count, because none of them computes one.
5//!
6//! A repository that declares no benchmark methodology is answered with `present: false` and
7//! the statement that no claim is available, never with an error and never with a number.
8//!
9//! ```
10//! use majordomus_cli::capability::builtin::economics::module;
11//! let m = module();
12//! let c = m.capabilities.iter().find(|c| c.capability.id.as_str() == "economics.summary").unwrap();
13//! assert_eq!(c.capability.exposure.http.as_ref().unwrap().path, "/api/v1/economics");
14//! assert_eq!(
15//!     c.capability.exposure.cli.as_ref().unwrap().path,
16//!     vec!["economics".to_string(), "summary".to_string()]
17//! );
18//! ```
19
20use std::path::Path;
21
22use crate::capability::benchmark::{BenchmarkCases, CaseContext, NamedCase};
23use crate::capability::handler::{CapabilityError, Context};
24use crate::capability::model::{
25    CachePolicy, CliExposure, Exposure, McpExposure, McpResource, Stability,
26};
27use crate::capability::module::ModuleDescriptor;
28use crate::economics::claims::{EconomicsCheckInput, EconomicsCheckReport};
29use crate::economics::model::{
30    EconomicsExplainInput, EconomicsExplanation, EconomicsQuery, EconomicsRunList,
31    EconomicsRunsQuery, EconomicsSummary,
32};
33use crate::{capability, module};
34
35use super::{get, mcp};
36
37/// The URI under which the summary is read as an MCP resource.
38pub const ECONOMICS_URI: &str = "majordomus://economics";
39
40impl BenchmarkCases for EconomicsQuery {
41    fn benchmark_cases(_: &CaseContext<'_>) -> Vec<NamedCase<Self>> {
42        vec![
43            NamedCase::new("default", EconomicsQuery::default()),
44            // every parameter carries a value so that the OpenAPI document has an example
45            // for each; the values name the shipped pilot suite and one of its tasks
46            NamedCase::new(
47                "narrowed",
48                EconomicsQuery {
49                    suite: Some("pilot".into()),
50                    category: Some("bug-fix".into()),
51                    task: Some("vat-rounding".into()),
52                    model: Some("claude-sonnet-5".into()),
53                },
54            ),
55        ]
56    }
57}
58
59impl BenchmarkCases for EconomicsExplainInput {
60    fn benchmark_cases(_: &CaseContext<'_>) -> Vec<NamedCase<Self>> {
61        vec![NamedCase::new(
62            "primary",
63            EconomicsExplainInput {
64                metric: crate::economics::EFFECTIVE_TOKEN_REDUCTION.into(),
65            },
66        )]
67    }
68}
69
70impl BenchmarkCases for EconomicsRunsQuery {
71    fn benchmark_cases(_: &CaseContext<'_>) -> Vec<NamedCase<Self>> {
72        vec![
73            NamedCase::new("all", EconomicsRunsQuery::default()),
74            NamedCase::new(
75                "narrowed",
76                EconomicsRunsQuery {
77                    suite: Some("pilot".into()),
78                    task: Some("vat-rounding".into()),
79                    variant: Some("baseline".into()),
80                },
81            ),
82        ]
83    }
84}
85
86impl BenchmarkCases for EconomicsCheckInput {
87    fn benchmark_cases(_: &CaseContext<'_>) -> Vec<NamedCase<Self>> {
88        vec![NamedCase::new("default", EconomicsCheckInput::default())]
89    }
90}
91
92fn root(ctx: &Context) -> &Path {
93    Path::new(&ctx.index.repository.root)
94}
95
96fn summary(ctx: &Context, q: EconomicsQuery) -> Result<EconomicsSummary, CapabilityError> {
97    Ok(crate::economics::summarize(root(ctx), &q))
98}
99
100fn explain(
101    ctx: &Context,
102    input: EconomicsExplainInput,
103) -> Result<EconomicsExplanation, CapabilityError> {
104    crate::economics::explain(root(ctx), &input.metric).map_err(CapabilityError::NotFound)
105}
106
107fn runs(ctx: &Context, q: EconomicsRunsQuery) -> Result<EconomicsRunList, CapabilityError> {
108    Ok(crate::economics::runs(root(ctx), &q))
109}
110
111fn check(ctx: &Context, _: EconomicsCheckInput) -> Result<EconomicsCheckReport, CapabilityError> {
112    Ok(crate::economics::claims::check(root(ctx)))
113}
114
115/// The `economics` module: four read-only capabilities, each a thin call into
116/// [`crate::economics`], so no surface computes a metric of its own. The summary, the
117/// explanation and the run list are cached per process for five seconds; the claim check is
118/// never cached, because a gate that answered from memory could pass prose edited a moment ago.
119///
120/// ```
121/// use majordomus_cli::capability::builtin::economics::module;
122/// use majordomus_cli::capability::{CachePolicy, Effect};
123/// let m = module();
124/// let ids: Vec<&str> = m.capabilities.iter().map(|e| e.capability.id.as_str()).collect();
125/// assert_eq!(
126///     ids,
127///     ["economics.summary", "economics.explain", "economics.runs", "economics.check"]
128/// );
129/// // every capability reads; none of them changes the repository
130/// assert!(m.capabilities.iter().all(|e| e.capability.kind.is_read_only()));
131/// assert!(m.capabilities.iter().all(|e| e.capability.execution.effect == Effect::Read));
132/// assert_eq!(m.capabilities[3].capability.cache, CachePolicy::Disabled);
133/// ```
134pub fn module() -> ModuleDescriptor {
135    module! {
136        id: "economics",
137        title: "Token economics",
138        description: "What a coding session consumes with Majordomus installed and without it, from recorded runs: matched control and treatment sessions judged by the same hidden acceptance tests, the provider's usage as it reported it, and every number labelled with its measurement class (observed, counted, derived, estimated, counterfactual). One calculator; every surface is a projection of it, and the verdict states only what the methodology's publication rule allows.",
139        stability: Stability::BehaviorallyVerified,
140        capabilities: [
141            capability! {
142                id: "economics.summary",
143                title: "Token economics summary",
144                description: "Every metric of the economics benchmark with its value, measurement class, sample size, distribution and bootstrap interval; every pair of runs with its status (valid, control failed, treatment failed, incomparable, ...); results by segment; the state of each suite's evidence (current, stale, incompatible); and the one statement the publication rule allows. Context reduction and total-token reduction are separate metrics and are never merged.",
145                input: EconomicsQuery,
146                output: EconomicsSummary,
147                stability: Stability::BehaviorallyVerified,
148                exposure: Exposure {
149                    mcp: Some(McpExposure {
150                        tool: Some("majordomus_economics".into()),
151                        resource: Some(McpResource { uri: ECONOMICS_URI.into(), name: "economics".into() }),
152                    }),
153                    http: get("/api/v1/economics"),
154                    cli: Some(CliExposure { path: vec!["economics".into(), "summary".into()] }),
155                },
156                tags: ["economics", "evidence", "benchmark"],
157                cache: CachePolicy::Process { max_entries: 8, ttl_seconds: Some(5) },
158                handler: summary,
159            },
160            capability! {
161                id: "economics.explain",
162                title: "Explain one economics metric",
163                description: "One metric and everything it rests on: its formula, its measurement class and what that class means, the pairs and runs behind it (including the invalid ones), excluded runs, the suites and revisions, and the commands that reproduce it. A repository that declares no methodology has no metric, and any id is answered with `present: false` and the statement why, not refused.",
164                input: EconomicsExplainInput,
165                output: EconomicsExplanation,
166                stability: Stability::BehaviorallyVerified,
167                exposure: Exposure {
168                    mcp: mcp("majordomus_economics_explain"),
169                    http: get("/api/v1/economics/explain"),
170                    cli: Some(CliExposure { path: vec!["economics".into(), "explain".into()] }),
171                },
172                tags: ["economics", "evidence"],
173                cache: CachePolicy::Process { max_entries: 32, ttl_seconds: Some(5) },
174                handler: explain,
175            },
176            capability! {
177                id: "economics.runs",
178                title: "Recorded benchmark runs",
179                description: "The raw facts every economics metric is computed from: each recorded run with its task, variant, repetition, model, revision, success-gate verdicts and the totals derived from its provider usage, and the path of its record.",
180                input: EconomicsRunsQuery,
181                output: EconomicsRunList,
182                stability: Stability::BehaviorallyVerified,
183                exposure: Exposure {
184                    mcp: mcp("majordomus_economics_runs"),
185                    http: get("/api/v1/economics/runs"),
186                    cli: Some(CliExposure { path: vec!["economics".into(), "runs".into()] }),
187                },
188                tags: ["economics", "evidence"],
189                cache: CachePolicy::Process { max_entries: 16, ttl_seconds: Some(5) },
190                handler: runs,
191            },
192            capability! {
193                id: "economics.check",
194                title: "Refuse unsupported economics claims",
195                description: "Scan the repository's hand-written prose and claim sentences for a quantity (a percentage, 'N times fewer') stated next to the economics vocabulary, and check every claim the methodology binds to a metric: a bound claim may be guaranteed only while its metric's evidence meets the binding and is current. `ok: false` names every finding.",
196                input: EconomicsCheckInput,
197                output: EconomicsCheckReport,
198                stability: Stability::BehaviorallyVerified,
199                exposure: Exposure {
200                    mcp: mcp("majordomus_economics_check"),
201                    http: get("/api/v1/economics/check"),
202                    cli: Some(CliExposure { path: vec!["economics".into(), "check".into()] }),
203                },
204                tags: ["economics", "claims", "gate"],
205                handler: check,
206            },
207        ],
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    /// The declaration is the only place the ids and projection names exist; a refactor
216    /// that dropped one would still compile.
217    #[test]
218    fn the_declaration_yields_the_identity_and_the_projections_it_claims() {
219        let m = module();
220        assert_eq!(m.id.as_str(), "economics");
221        let ids: Vec<&str> = m
222            .capabilities
223            .iter()
224            .map(|e| e.capability.id.as_str())
225            .collect();
226        assert_eq!(
227            ids,
228            [
229                "economics.summary",
230                "economics.explain",
231                "economics.runs",
232                "economics.check"
233            ]
234        );
235        for (e, route, tool) in [
236            (
237                &m.capabilities[0],
238                "/api/v1/economics",
239                "majordomus_economics",
240            ),
241            (
242                &m.capabilities[1],
243                "/api/v1/economics/explain",
244                "majordomus_economics_explain",
245            ),
246            (
247                &m.capabilities[2],
248                "/api/v1/economics/runs",
249                "majordomus_economics_runs",
250            ),
251            (
252                &m.capabilities[3],
253                "/api/v1/economics/check",
254                "majordomus_economics_check",
255            ),
256        ] {
257            let c = &e.capability;
258            assert_eq!(c.exposure.http.as_ref().unwrap().path, route);
259            assert_eq!(c.exposure.mcp.as_ref().unwrap().tool.as_deref(), Some(tool));
260            assert!(
261                c.kind.is_read_only() && c.kind.is_executable(),
262                "{} must only read",
263                c.id.as_str()
264            );
265        }
266        assert_eq!(
267            m.capabilities[0]
268                .capability
269                .exposure
270                .mcp
271                .as_ref()
272                .unwrap()
273                .resource
274                .as_ref()
275                .unwrap()
276                .uri,
277            ECONOMICS_URI
278        );
279    }
280}