1use 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
37pub 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 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
115pub 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 #[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}