Skip to main content

lean_ctx/core/
product_capabilities.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Canonical v4 product-capability registry.
4//!
5//! Product access decisions are made from the embedded, validated registry.
6//! Price, source visibility and commercial rights are independent axes.
7//! Legacy IDs remain stable, but never determine price or source visibility.
8
9use std::collections::{BTreeMap, BTreeSet};
10use std::sync::OnceLock;
11
12use serde::{Deserialize, Serialize};
13
14use crate::core::ocla::OclaCapabilityKind;
15
16/// The plan ladder a capability's minimum is ranked on. Account plans live
17/// in LeanCTX | Hive; the engine only orders them.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum Plan {
21    #[serde(alias = "free", alias = "supporter", alias = "sponsor")]
22    Community,
23    Pro,
24    #[serde(alias = "business", alias = "biz")]
25    Team,
26    #[serde(alias = "ent")]
27    Enterprise,
28}
29
30impl Plan {
31    pub const ALL: [Self; 4] = [Self::Community, Self::Pro, Self::Team, Self::Enterprise];
32
33    #[must_use]
34    pub const fn all() -> &'static [Self] {
35        &Self::ALL
36    }
37
38    #[must_use]
39    pub const fn rank(self) -> usize {
40        match self {
41            Self::Community => 0,
42            Self::Pro => 1,
43            Self::Team => 2,
44            Self::Enterprise => 3,
45        }
46    }
47
48    #[must_use]
49    pub const fn as_str(self) -> &'static str {
50        match self {
51            Self::Community => "community",
52            Self::Pro => "pro",
53            Self::Team => "team",
54            Self::Enterprise => "enterprise",
55        }
56    }
57}
58
59/// Package-local mirror of the repository source of truth. CI requires this
60/// byte-for-byte copy so source-package builds remain hermetic.
61pub const REGISTRY_SOURCE: &str = include_str!("../../data/product-capabilities.toml");
62
63#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
64#[serde(rename_all = "snake_case")]
65pub enum UserPriceClass {
66    Free,
67    /// Paid right granted by a LeanCTX | Hive plan; the record's
68    /// [`ProductCapability::hive_plan`] names the plan.
69    Hive,
70    /// Withdrawn capability: still a known lookup name, never granted.
71    Retired,
72    /// Legacy (schema 2) class. Kept so the frozen hosted services and old
73    /// envelopes keep decoding; no schema-3 record uses it.
74    CloudPaid,
75    /// Legacy (schema 2) Pro class of the signed private Intelligence Runtime;
76    /// no schema-3 record uses it.
77    ProPaid,
78    /// Legacy (schema 2) Team class; no schema-3 record uses it.
79    TeamPaid,
80    /// Legacy (schema 2) Enterprise class; no schema-3 record uses it.
81    EnterprisePaid,
82    SdkOemCommercial,
83}
84
85/// The Hive plan that grants an `hive` capability (Enterprise ⊇ Team ⊇ Personal).
86#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
87#[serde(rename_all = "snake_case")]
88pub enum HivePlan {
89    Personal,
90    Team,
91    Enterprise,
92}
93
94impl HivePlan {
95    #[must_use]
96    pub const fn plan(self) -> Plan {
97        match self {
98            Self::Personal => Plan::Pro,
99            Self::Team => Plan::Team,
100            Self::Enterprise => Plan::Enterprise,
101        }
102    }
103}
104
105/// Registry schema this Engine reads (3: `hive` and `retired` classes).
106pub const REGISTRY_SCHEMA_VERSION: u16 = 3;
107
108#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
109#[serde(rename_all = "snake_case")]
110pub enum SourceVisibility {
111    PublicApache,
112    PublicEnterpriseSourceAvailable,
113    PrivateSource,
114}
115
116#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
117#[serde(rename_all = "snake_case")]
118pub enum RuntimeDelivery {
119    SourceBuild,
120    SignedBinary,
121    LocalService,
122    ManagedCloudService,
123    EnterprisePrivateImage,
124    /// Delivered and controlled by the LeanCTX | Hive service (filter packs,
125    /// shared team state, governance), consumed by the local Engine.
126    HiveService,
127}
128
129#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
130#[serde(rename_all = "snake_case")]
131pub enum AhaCriticality {
132    None,
133    Supporting,
134    IndividualAha,
135    TeamAha,
136    /// Withheld by the public projection; see [`public_projection`].
137    Undisclosed,
138}
139
140#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
141#[serde(rename_all = "snake_case")]
142pub enum MoatSensitivity {
143    Low,
144    Medium,
145    CrownJewel,
146    /// Withheld by the public projection; see [`public_projection`].
147    Undisclosed,
148}
149
150/// Marker value of the registry's top-level `projection` key.
151pub const PUBLIC_PROJECTION: &str = "public";
152/// `source_path` of a capability whose implementation is not public.
153pub const UNDISCLOSED_SOURCE: &str = "undisclosed";
154
155#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
156#[serde(rename_all = "snake_case")]
157pub enum CommercialUse {
158    ApacheRights,
159    FreeSmallTeamInternal,
160    ServiceAgreement,
161    EnterpriseAgreement,
162    SdkOemAgreement,
163    ThirdPartyTerms,
164}
165
166#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
167#[serde(rename_all = "snake_case")]
168pub enum RedistributionRights {
169    ApacheRights,
170    SeparateAgreementRequired,
171    ThirdPartyTerms,
172}
173
174#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
175#[serde(rename_all = "snake_case")]
176pub enum TelemetryEventClass {
177    None,
178    ProductUsage,
179    BillingUsage,
180    OperationalAggregate,
181    Audit,
182}
183
184#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
185#[serde(rename_all = "snake_case")]
186pub enum PublicStatus {
187    Available,
188    Preview,
189    Internal,
190    Deprecated,
191    Research,
192    Private,
193}
194
195#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
196#[serde(rename_all = "snake_case")]
197pub enum LicenseClass {
198    ApacheTrustCore,
199    CommercialSource,
200    PrivateService,
201    FreeRuntime,
202    SdkOem,
203    ThirdParty,
204}
205
206#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
207#[serde(deny_unknown_fields)]
208pub struct ProductCapability {
209    pub id: String,
210    pub name: String,
211    pub price_class: UserPriceClass,
212    /// Present exactly on `hive` records: the Hive plan that grants it.
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub hive_plan: Option<HivePlan>,
215    pub source_visibility: SourceVisibility,
216    pub runtime_delivery: RuntimeDelivery,
217    pub aha_criticality: AhaCriticality,
218    pub moat_sensitivity: MoatSensitivity,
219    pub license_class: LicenseClass,
220    pub self_host_available: bool,
221    pub managed_cloud_available: bool,
222    /// Canonical governance key, or the explicit value `none`.
223    pub enterprise_entitlement: String,
224    pub account_required: bool,
225    pub allowed_commercial_use: CommercialUse,
226    pub redistribution_oem_rights: RedistributionRights,
227    pub data_boundary: String,
228    pub telemetry_event_class: TelemetryEventClass,
229    pub public_maturity: PublicStatus,
230    pub source_path: String,
231}
232
233impl ProductCapability {
234    /// Compatibility projection for legacy clients; not entitlement authority.
235    #[must_use]
236    pub const fn minimum_plan(&self) -> Plan {
237        match self.price_class {
238            UserPriceClass::Free => Plan::Community,
239            UserPriceClass::Hive => match self.hive_plan {
240                Some(plan) => plan.plan(),
241                // Unreachable for a validated record; fail toward the top plan.
242                None => Plan::Enterprise,
243            },
244            UserPriceClass::CloudPaid | UserPriceClass::ProPaid => Plan::Pro,
245            UserPriceClass::TeamPaid => Plan::Team,
246            // A retired id is never granted (`allows` refuses it); it never
247            // reads as Community either.
248            UserPriceClass::EnterprisePaid
249            | UserPriceClass::SdkOemCommercial
250            | UserPriceClass::Retired => Plan::Enterprise,
251        }
252    }
253
254    /// True for a capability whose entitlement is Enterprise authority: it
255    /// must name itself as its canonical `enterprise_entitlement`.
256    #[must_use]
257    pub const fn is_enterprise(&self) -> bool {
258        matches!(self.price_class, UserPriceClass::EnterprisePaid)
259            || matches!(
260                (self.price_class, self.hive_plan),
261                (UserPriceClass::Hive, Some(HivePlan::Enterprise))
262            )
263    }
264}
265
266#[derive(Clone, Debug, Deserialize)]
267#[serde(deny_unknown_fields)]
268struct RegistryDocument {
269    schema_version: u16,
270    /// Absent in the canonical registry; [`PUBLIC_PROJECTION`] in the public
271    /// export, whose non-public records carry no strategic classification.
272    #[serde(default)]
273    projection: Option<String>,
274    capability: Vec<ProductCapability>,
275    /// Legacy wire/config lookup names, separate from the 18-field records.
276    aliases: BTreeMap<String, String>,
277}
278
279#[derive(Debug)]
280pub struct ProductCapabilityRegistry {
281    entries: Vec<ProductCapability>,
282    lookup: BTreeMap<String, usize>,
283    projected: bool,
284}
285
286impl ProductCapabilityRegistry {
287    pub fn parse(source: &str) -> Result<Self, String> {
288        let document: RegistryDocument = toml::from_str(source)
289            .map_err(|error| format!("invalid capability registry: {error}"))?;
290        if document.schema_version != REGISTRY_SCHEMA_VERSION {
291            return Err(format!(
292                "unsupported capability registry schema {}",
293                document.schema_version
294            ));
295        }
296        if document.capability.is_empty() {
297            return Err("capability registry is empty".to_string());
298        }
299
300        let projected = match document.projection.as_deref() {
301            None => false,
302            Some(PUBLIC_PROJECTION) => true,
303            Some(other) => return Err(format!("unknown capability registry projection '{other}'")),
304        };
305
306        let mut lookup = BTreeMap::new();
307        let mut ids = BTreeSet::new();
308        for (index, capability) in document.capability.iter().enumerate() {
309            validate_record(capability)?;
310            validate_projection(capability, projected)?;
311            if !ids.insert(capability.id.as_str()) {
312                return Err(format!("duplicate capability id '{}'", capability.id));
313            }
314            lookup.insert(capability.id.clone(), index);
315        }
316        for (alias, target) in document.aliases {
317            if alias.trim().is_empty() || !ids.contains(target.as_str()) {
318                return Err(format!("invalid capability alias '{alias}' -> '{target}'"));
319            }
320            let index = lookup[&target];
321            if lookup.insert(alias.clone(), index).is_some() {
322                return Err(format!("duplicate capability lookup key '{alias}'"));
323            }
324        }
325        Ok(Self {
326            entries: document.capability,
327            lookup,
328            projected,
329        })
330    }
331
332    /// True for the public export's registry, whose non-public records
333    /// withhold their strategic classification.
334    #[must_use]
335    pub const fn is_public_projection(&self) -> bool {
336        self.projected
337    }
338
339    #[must_use]
340    pub fn entries(&self) -> &[ProductCapability] {
341        &self.entries
342    }
343
344    #[must_use]
345    pub fn find(&self, id_or_key: &str) -> Option<&ProductCapability> {
346        self.lookup
347            .get(id_or_key)
348            .and_then(|index| self.entries.get(*index))
349    }
350
351    #[must_use]
352    pub fn allows(&self, plan: Plan, id_or_key: &str) -> bool {
353        self.find(id_or_key).is_some_and(|capability| {
354            // A legacy plan can never grant separate SDK/OEM rights, and no
355            // plan grants a retired capability.
356            !matches!(
357                capability.price_class,
358                UserPriceClass::SdkOemCommercial | UserPriceClass::Retired
359            ) && plan.rank() >= capability.minimum_plan().rank()
360        })
361    }
362
363    #[must_use]
364    pub fn render_markdown(&self) -> String {
365        use std::fmt::Write as _;
366
367        let mut entries: Vec<&ProductCapability> = self.entries.iter().collect();
368        entries.sort_unstable_by_key(|capability| capability.id.as_str());
369        let mut output = String::from(
370            "<!-- Generated from product/capabilities.toml; do not edit by hand. -->\n\n\
371             # Product capabilities\n\n\
372             | ID | Name | Price | Hive plan | Source visibility | Delivery | Aha | Moat | License | Self-host | Managed cloud | Enterprise entitlement | Account | Commercial use | Redistribution/OEM | Data boundary | Telemetry | Maturity | Source path |\n\
373             |---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|\n",
374        );
375        for capability in entries {
376            writeln!(
377                output,
378                "| {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |",
379                capability.id,
380                capability.name.replace('|', "\\|"),
381                enum_name(&capability.price_class),
382                capability
383                    .hive_plan
384                    .as_ref()
385                    .map_or_else(|| "-".to_string(), enum_name),
386                enum_name(&capability.source_visibility),
387                enum_name(&capability.runtime_delivery),
388                enum_name(&capability.aha_criticality),
389                enum_name(&capability.moat_sensitivity),
390                enum_name(&capability.license_class),
391                capability.self_host_available,
392                capability.managed_cloud_available,
393                capability.enterprise_entitlement,
394                capability.account_required,
395                enum_name(&capability.allowed_commercial_use),
396                enum_name(&capability.redistribution_oem_rights),
397                capability.data_boundary.replace('|', "\\|"),
398                enum_name(&capability.telemetry_event_class),
399                enum_name(&capability.public_maturity),
400                capability.source_path.replace('|', "\\|"),
401            )
402            .expect("writing to String cannot fail");
403        }
404        output
405    }
406}
407
408fn enum_name<T: Serialize>(value: &T) -> String {
409    serde_json::to_value(value)
410        .expect("serializable registry enum")
411        .as_str()
412        .expect("registry enum serializes as string")
413        .to_string()
414}
415
416fn validate_record(capability: &ProductCapability) -> Result<(), String> {
417    for (field, value) in [
418        ("id", capability.id.as_str()),
419        ("name", capability.name.as_str()),
420        (
421            "enterprise_entitlement",
422            capability.enterprise_entitlement.as_str(),
423        ),
424        ("source_path", capability.source_path.as_str()),
425        ("data_boundary", capability.data_boundary.as_str()),
426    ] {
427        if value.trim().is_empty() {
428            return Err(format!("capability '{}' has empty {field}", capability.id));
429        }
430    }
431    let invalid = if matches!(
432        capability.aha_criticality,
433        AhaCriticality::IndividualAha | AhaCriticality::TeamAha
434    ) && capability.price_class != UserPriceClass::Free
435    {
436        Some("Aha-critical capability has no free delivery path")
437    } else if capability.moat_sensitivity == MoatSensitivity::CrownJewel
438        && capability.source_visibility != SourceVisibility::PrivateSource
439    {
440        Some("crown-jewel implementation must not enter public source")
441    } else if capability.moat_sensitivity == MoatSensitivity::CrownJewel
442        && capability.redistribution_oem_rights != RedistributionRights::SeparateAgreementRequired
443    {
444        Some("crown-jewel redistribution requires a separate agreement")
445    } else if capability.source_visibility == SourceVisibility::PublicApache
446        && (capability.license_class != LicenseClass::ApacheTrustCore
447            || capability.allowed_commercial_use != CommercialUse::ApacheRights
448            || capability.redistribution_oem_rights != RedistributionRights::ApacheRights
449            || capability.price_class != UserPriceClass::Free
450            || capability.account_required)
451    {
452        Some("public Apache reference rights must remain free and accountless")
453    } else if capability.license_class == LicenseClass::ApacheTrustCore
454        && capability.source_visibility != SourceVisibility::PublicApache
455    {
456        Some("Apache source classification mismatch")
457    } else if capability.source_visibility != SourceVisibility::PublicApache
458        && (capability.allowed_commercial_use == CommercialUse::ApacheRights
459            || capability.redistribution_oem_rights == RedistributionRights::ApacheRights)
460    {
461        Some("non-Apache implementations cannot grant Apache redistribution rights")
462    } else if capability.price_class == UserPriceClass::SdkOemCommercial
463        && (capability.license_class != LicenseClass::SdkOem
464            || capability.allowed_commercial_use != CommercialUse::SdkOemAgreement)
465    {
466        Some("SDK/OEM capability requires a separate commercial license")
467    } else if capability.runtime_delivery == RuntimeDelivery::SourceBuild
468        && (!capability.self_host_available
469            || capability.source_visibility == SourceVisibility::PrivateSource)
470    {
471        Some("source builds require available source and a self-host path")
472    } else if capability.runtime_delivery == RuntimeDelivery::ManagedCloudService
473        && !capability.managed_cloud_available
474    {
475        Some("managed service requires managed-cloud availability")
476    } else if matches!(
477        capability.runtime_delivery,
478        RuntimeDelivery::SignedBinary
479            | RuntimeDelivery::LocalService
480            | RuntimeDelivery::EnterprisePrivateImage
481    ) && !capability.self_host_available
482    {
483        Some("local artifact requires a self-host path")
484    } else if capability.aha_criticality == AhaCriticality::IndividualAha
485        && (capability.account_required || !capability.self_host_available)
486    {
487        Some("individual Aha must work locally without an account")
488    } else if capability.source_visibility == SourceVisibility::PublicEnterpriseSourceAvailable
489        && capability.license_class != LicenseClass::CommercialSource
490    {
491        Some("source-available Enterprise requires its own license")
492    } else if capability.license_class == LicenseClass::FreeRuntime
493        && (capability.source_visibility != SourceVisibility::PrivateSource
494            || capability.price_class != UserPriceClass::Free
495            || capability.allowed_commercial_use != CommercialUse::FreeSmallTeamInternal
496            || capability.redistribution_oem_rights
497                != RedistributionRights::SeparateAgreementRequired)
498    {
499        Some("Free Runtime rights must not grant source/OEM/redistribution rights")
500    } else if capability.is_enterprise() != (capability.enterprise_entitlement == capability.id) {
501        Some("Enterprise capability requires its explicit canonical entitlement")
502    } else if !capability.is_enterprise() && capability.enterprise_entitlement != "none" {
503        Some("non-Enterprise capability must not require governance entitlement")
504    } else if !capability.self_host_available && !capability.managed_cloud_available {
505        Some("capability has no delivery path")
506    } else if (capability.price_class == UserPriceClass::Hive) != capability.hive_plan.is_some() {
507        Some("Hive plan is required exactly on hive capabilities")
508    } else if capability.price_class == UserPriceClass::Hive
509        && (capability.source_visibility != SourceVisibility::PrivateSource
510            || !capability.account_required
511            || !matches!(
512                capability.runtime_delivery,
513                RuntimeDelivery::HiveService | RuntimeDelivery::SignedBinary
514            ))
515    {
516        Some("Hive capability needs private source, an account and the Hive or signed path")
517    } else if capability.runtime_delivery == RuntimeDelivery::HiveService
518        && (capability.price_class != UserPriceClass::Hive || !capability.managed_cloud_available)
519    {
520        Some("Hive service delivery requires a Hive plan and the managed service")
521    } else if matches!(
522        capability.price_class,
523        UserPriceClass::CloudPaid
524            | UserPriceClass::ProPaid
525            | UserPriceClass::TeamPaid
526            | UserPriceClass::EnterprisePaid
527    ) {
528        Some("legacy price class is not valid in schema 3; use free, hive or retired")
529    } else {
530        None
531    };
532    if let Some(reason) = invalid {
533        return Err(format!("capability '{}': {reason}", capability.id));
534    }
535    Ok(())
536}
537
538/// Fields the public projection withholds for capabilities whose
539/// implementation is not public: they describe product strategy, not rights.
540/// Access decisions use only price, source and rights, which stay intact.
541const WITHHELD_FIELDS: [(&str, &str); 4] = [
542    ("aha_criticality", "undisclosed"),
543    ("moat_sensitivity", "undisclosed"),
544    ("public_maturity", "private"),
545    ("source_path", UNDISCLOSED_SOURCE),
546];
547
548/// The canonical registry discloses everything; the public projection
549/// withholds all strategic fields of every non-public record and nothing of
550/// a public one. Anything in between fails closed.
551fn validate_projection(capability: &ProductCapability, projected: bool) -> Result<(), String> {
552    let withheld = [
553        capability.aha_criticality == AhaCriticality::Undisclosed,
554        capability.moat_sensitivity == MoatSensitivity::Undisclosed,
555        capability.public_maturity == PublicStatus::Private,
556        capability.source_path == UNDISCLOSED_SOURCE,
557    ];
558    let undisclosed = withheld[0] || withheld[1] || withheld[3];
559    let public = capability.source_visibility == SourceVisibility::PublicApache;
560    let problem = if !projected && undisclosed {
561        Some("undisclosed classification outside the public projection")
562    } else if projected && public && undisclosed {
563        Some("public capability must stay fully classified")
564    } else if projected && !public && !withheld.iter().all(|field| *field) {
565        Some("public projection discloses a strategic classification")
566    } else {
567        None
568    };
569    match problem {
570        Some(reason) => Err(format!("capability '{}': {reason}", capability.id)),
571        None => Ok(()),
572    }
573}
574
575/// Render the public projection of a canonical registry source: the
576/// `projection` marker plus the withheld strategy fields (Aha criticality,
577/// moat sensitivity, maturity, source path) rewritten for every
578/// non-public record. Layout and all other bytes are preserved; the result
579/// is re-parsed, so a rewrite that misses a field fails closed. Projecting a
580/// projection returns it unchanged.
581pub fn public_projection(source: &str) -> Result<String, String> {
582    use std::fmt::Write as _;
583
584    let registry = ProductCapabilityRegistry::parse(source)?;
585    if registry.is_public_projection() {
586        return Ok(source.to_string());
587    }
588    let hidden: BTreeSet<&str> = registry
589        .entries()
590        .iter()
591        .filter(|capability| capability.source_visibility != SourceVisibility::PublicApache)
592        .map(|capability| capability.id.as_str())
593        .collect();
594
595    let mut output = String::with_capacity(source.len());
596    let mut current_id: Option<String> = None;
597    for line in source.split_inclusive('\n') {
598        let content = line.trim_end_matches(['\n', '\r']);
599        let newline = &line[content.len()..];
600        if content.trim() == "[[capability]]" {
601            current_id = None;
602        }
603        let Some((key, value)) = content.split_once(" = ") else {
604            output.push_str(line);
605            continue;
606        };
607        if key == "id" {
608            current_id = Some(value.trim_matches('"').to_string());
609        }
610        let withheld = current_id
611            .as_deref()
612            .filter(|id| hidden.contains(id))
613            .and_then(|_| WITHHELD_FIELDS.iter().find(|(field, _)| *field == key));
614        match withheld {
615            Some((field, replacement)) => write!(output, "{field} = \"{replacement}\"{newline}")
616                .expect("writing to String cannot fail"),
617            None => output.push_str(line),
618        }
619        if key == "schema_version" {
620            write!(output, "projection = \"{PUBLIC_PROJECTION}\"{newline}")
621                .expect("writing to String cannot fail");
622        }
623    }
624
625    let projected = ProductCapabilityRegistry::parse(&output)?;
626    if !projected.is_public_projection() {
627        return Err("capability registry has no schema_version line to mark".to_string());
628    }
629    Ok(output)
630}
631
632static REGISTRY: OnceLock<ProductCapabilityRegistry> = OnceLock::new();
633
634#[must_use]
635pub fn registry() -> &'static ProductCapabilityRegistry {
636    REGISTRY.get_or_init(|| {
637        ProductCapabilityRegistry::parse(REGISTRY_SOURCE)
638            .unwrap_or_else(|error| panic!("embedded product capability registry failed: {error}"))
639    })
640}
641
642/// Stable product record for each OCLA capability kind. This exhaustive match
643/// makes a newly added OCLA kind fail compilation until it is classified.
644#[must_use]
645pub const fn ocla_product_capability_id(kind: OclaCapabilityKind) -> &'static str {
646    match kind {
647        OclaCapabilityKind::ObservationHook => "trust.ocla.observation_hook",
648        OclaCapabilityKind::UsageSink => "enterprise.ocla.usage_sink",
649        OclaCapabilityKind::MetricsExporter => "team.ocla.metrics_exporter",
650        OclaCapabilityKind::SavingsLedger => "trust.ocla.savings_ledger",
651        OclaCapabilityKind::IntentClassifier => "pro.ocla.intent_classifier",
652        OclaCapabilityKind::OutcomeTracker => "pro.ocla.outcome_tracker",
653        OclaCapabilityKind::CompressionProvider => "trust.ocla.compression_provider",
654        OclaCapabilityKind::ResponseOptimizer => "pro.ocla.response_optimizer",
655        OclaCapabilityKind::EfficiencyAnalyzer => "pro.ocla.efficiency_analyzer",
656        OclaCapabilityKind::ConfigTuner => "pro.ocla.config_tuner",
657        OclaCapabilityKind::ExperimentRunner => "pro.ocla.experiment_runner",
658        OclaCapabilityKind::ConnectorScheduler => "pro.ocla.connector_scheduler",
659        OclaCapabilityKind::AgentGateway => "pro.ocla.agent_gateway",
660        OclaCapabilityKind::DeliveryRegistry => "pro.ocla.delivery_registry",
661    }
662}
663
664pub const EXPERIMENT_EXECUTOR_CAPABILITY_ID: &str = "pro.ocla.experiment_executor";
665
666#[cfg(test)]
667mod tests {
668    use super::*;
669
670    #[test]
671    fn embedded_registry_is_valid() {
672        assert!(registry().entries().len() >= 40);
673        let source = include_str!("../../../product/capabilities.toml");
674        if REGISTRY_SOURCE == source {
675            return;
676        }
677        // A checkout with the canonical private registry embeds its public
678        // projection (scripts/project-capabilities.py; byte equality is
679        // checked by scripts/check-capabilities.py): the same capabilities in
680        // the same order, with no private source path.
681        assert!(
682            REGISTRY_SOURCE.contains("\nprojection = \"public\"\n"),
683            "packaged capability registry drifted from repository source"
684        );
685        let ids = |text: &str| -> Vec<String> {
686            ProductCapabilityRegistry::parse(text)
687                .expect("registry parses")
688                .entries()
689                .iter()
690                .map(|entry| entry.id.clone())
691                .collect()
692        };
693        assert_eq!(ids(REGISTRY_SOURCE), ids(source));
694        assert!(!REGISTRY_SOURCE.contains("\"ee/"));
695    }
696
697    #[test]
698    fn malformed_or_duplicate_records_are_rejected() {
699        assert!(ProductCapabilityRegistry::parse("schema_version = 99").is_err());
700        let first = &registry().entries()[0].id;
701        let second = &registry().entries()[1].id;
702        let duplicate = REGISTRY_SOURCE.replacen(
703            &format!("id = \"{second}\""),
704            &format!("id = \"{first}\""),
705            1,
706        );
707        assert!(ProductCapabilityRegistry::parse(&duplicate).is_err());
708    }
709
710    #[test]
711    fn unknown_capabilities_fail_closed() {
712        for plan in Plan::all() {
713            assert!(!registry().allows(*plan, "unclassified.future.feature"));
714        }
715    }
716
717    fn mutated_first(field: &str, value: toml::Value) -> String {
718        let mut document: toml::Value = toml::from_str(REGISTRY_SOURCE).unwrap();
719        document["capability"][0][field] = value;
720        toml::to_string(&document).unwrap()
721    }
722
723    #[test]
724    fn all_eighteen_classifications_are_required_without_defaults() {
725        let document: toml::Value = toml::from_str(REGISTRY_SOURCE).unwrap();
726        let records = document["capability"].as_array().unwrap();
727        for record in records {
728            let hive = record["price_class"].as_str() == Some("hive");
729            assert_eq!(
730                record.as_table().unwrap().len(),
731                18 + usize::from(hive),
732                "{record}"
733            );
734            for field in record.as_table().unwrap().keys() {
735                let mut incomplete = record.clone();
736                incomplete.as_table_mut().unwrap().remove(field);
737                if field == "hive_plan" {
738                    // Optional in the record format, required by validation.
739                    let parsed: ProductCapability = incomplete.try_into().unwrap();
740                    assert!(validate_record(&parsed).is_err(), "missing {field}");
741                    continue;
742                }
743                assert!(
744                    incomplete.try_into::<ProductCapability>().is_err(),
745                    "missing {field}"
746                );
747            }
748        }
749    }
750
751    /// The registry is the open-core boundary: every public capability points at
752    /// source that exists in this repository, and no private or paid capability
753    /// points into it. Moving paid code into the public tree fails here.
754    #[test]
755    fn registry_source_paths_enforce_the_open_core_boundary() {
756        let repository = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
757        for entry in registry().entries() {
758            for source in entry.source_path.split(';').map(str::trim) {
759                let in_repository = source.starts_with("rust/");
760                if entry.source_visibility == SourceVisibility::PublicApache {
761                    assert!(in_repository, "{}: public source outside rust/", entry.id);
762                    assert!(
763                        repository.join(source).exists(),
764                        "{}: public source {source} is missing",
765                        entry.id
766                    );
767                } else {
768                    assert!(
769                        !in_repository,
770                        "{}: private capability points into the public tree",
771                        entry.id
772                    );
773                }
774                if entry.price_class != UserPriceClass::Free {
775                    assert_ne!(entry.source_visibility, SourceVisibility::PublicApache);
776                }
777            }
778        }
779    }
780
781    #[test]
782    fn shared_team_capabilities_need_team_and_enterprise_self_hosts_them() {
783        let registry = registry();
784        let entry = registry.find("team.context.shared").unwrap();
785        assert_eq!(entry.price_class, UserPriceClass::Hive);
786        assert_eq!(entry.hive_plan, Some(HivePlan::Team));
787        assert!(entry.managed_cloud_available && entry.self_host_available);
788        assert!(!registry.allows(Plan::Pro, &entry.id));
789        assert!(registry.allows(Plan::Team, &entry.id));
790        assert!(registry.allows(Plan::Enterprise, &entry.id));
791        let mut public = entry.clone();
792        public.source_visibility = SourceVisibility::PublicApache;
793        assert!(validate_record(&public).is_err());
794    }
795
796    #[test]
797    fn team_runtime_capabilities_ship_in_the_signed_self_hosted_runtime() {
798        let registry = registry();
799        for id in [
800            "team.runtime.artifact_reader",
801            "team.runtime.artifact_author",
802        ] {
803            let entry = registry.find(id).unwrap();
804            assert_eq!(entry.price_class, UserPriceClass::Hive);
805            assert_eq!(entry.hive_plan, Some(HivePlan::Team));
806            assert_eq!(entry.runtime_delivery, RuntimeDelivery::SignedBinary);
807            assert!(entry.self_host_available && !entry.managed_cloud_available);
808            assert!(!registry.allows(Plan::Pro, id));
809            assert!(registry.allows(Plan::Team, id));
810            // Each condition of the Hive rule refuses on its own.
811            let breaks: [fn(&mut ProductCapability); 3] = [
812                |record| {
813                    record.source_visibility = SourceVisibility::PublicEnterpriseSourceAvailable;
814                },
815                |record| record.account_required = false,
816                |record| record.runtime_delivery = RuntimeDelivery::LocalService,
817            ];
818            for change in breaks {
819                let mut broken = entry.clone();
820                change(&mut broken);
821                let refusal = validate_record(&broken).unwrap_err();
822                assert!(refusal.contains("Hive capability"), "{refusal}");
823            }
824        }
825    }
826
827    #[test]
828    fn hosted_seat_authority_is_classified_for_team_and_enterprise_only() {
829        let registry = registry();
830        let entry = registry.find("team.seat_management").unwrap();
831        assert_eq!(entry.price_class, UserPriceClass::Hive);
832        assert_eq!(entry.hive_plan, Some(HivePlan::Team));
833        assert_eq!(entry.runtime_delivery, RuntimeDelivery::HiveService);
834        assert!(entry.account_required && entry.managed_cloud_available);
835        assert_eq!(entry.enterprise_entitlement, "none");
836        assert!(!registry.allows(Plan::Community, &entry.id));
837        assert!(!registry.allows(Plan::Pro, &entry.id));
838        assert!(registry.allows(Plan::Team, &entry.id));
839        assert!(registry.allows(Plan::Enterprise, &entry.id));
840    }
841
842    #[test]
843    fn aha_paywalls_public_crown_jewels_and_unclassified_telemetry_fail_closed() {
844        for (field, value) in [
845            ("price_class", "cloud_paid"),
846            ("moat_sensitivity", "crown_jewel"),
847            ("license_class", "free_runtime"),
848            ("source_visibility", "private_source"),
849            ("telemetry_event_class", "arbitrary_runtime_text"),
850            ("enterprise_entitlement", "enterprise.unrelated"),
851        ] {
852            let malformed = mutated_first(field, value.into());
853            assert!(
854                ProductCapabilityRegistry::parse(&malformed).is_err(),
855                "accepted {field}={value}"
856            );
857        }
858        assert!(
859            ProductCapabilityRegistry::parse(&mutated_first("account_required", true.into()))
860                .is_err()
861        );
862    }
863
864    #[test]
865    fn free_private_intelligence_does_not_grant_oem_or_source_rights() {
866        // Exercise the free/private rights class without advertising a retired
867        // capability or changing the commercial classification of a live one.
868        let mut entry = registry()
869            .find("pro.runtime.adaptive_context")
870            .unwrap()
871            .clone();
872        entry.price_class = UserPriceClass::Free;
873        entry.license_class = LicenseClass::FreeRuntime;
874        entry.allowed_commercial_use = CommercialUse::FreeSmallTeamInternal;
875        entry.aha_criticality = AhaCriticality::IndividualAha;
876        entry.account_required = false;
877        assert!(validate_record(&entry).is_ok());
878        assert_eq!(entry.minimum_plan(), Plan::Community);
879        let mut leaking = entry.clone();
880        leaking.redistribution_oem_rights = RedistributionRights::ApacheRights;
881        assert!(validate_record(&leaking).is_err());
882        leaking = entry.clone();
883        leaking.allowed_commercial_use = CommercialUse::ApacheRights;
884        assert!(validate_record(&leaking).is_err());
885        // The public projection withholds moat and Aha classifications; the
886        // crown-jewel redistribution and individual-Aha account rules can only
887        // be exercised where they are disclosed (the canonical registry).
888        let disclosed = !registry().is_public_projection();
889        if disclosed {
890            leaking = entry.clone();
891            leaking.license_class = LicenseClass::ThirdParty;
892            leaking.redistribution_oem_rights = RedistributionRights::ThirdPartyTerms;
893            assert!(validate_record(&leaking).is_err());
894        }
895        for license in [
896            LicenseClass::PrivateService,
897            LicenseClass::CommercialSource,
898            LicenseClass::SdkOem,
899        ] {
900            leaking = entry.clone();
901            leaking.license_class = license;
902            leaking.redistribution_oem_rights = RedistributionRights::ApacheRights;
903            assert!(validate_record(&leaking).is_err());
904        }
905        leaking = entry.clone();
906        leaking.runtime_delivery = RuntimeDelivery::SourceBuild;
907        assert!(validate_record(&leaking).is_err());
908        if disclosed {
909            leaking = entry.clone();
910            leaking.account_required = true;
911            assert!(validate_record(&leaking).is_err());
912        }
913    }
914
915    #[test]
916    fn legacy_ids_do_not_paywall_local_or_small_team_aha() {
917        for entry in registry().entries() {
918            if matches!(
919                entry.aha_criticality,
920                AhaCriticality::IndividualAha | AhaCriticality::TeamAha
921            ) {
922                assert!(
923                    registry().allows(Plan::Community, &entry.id),
924                    "{}",
925                    entry.id
926                );
927            }
928        }
929        for id in [
930            "enterprise.execution_policy",
931            "enterprise.sso_scim",
932            "team.sso_oidc",
933        ] {
934            assert!(!registry().allows(Plan::Community, id));
935            assert!(!registry().allows(Plan::Team, id));
936        }
937    }
938
939    /// D7: the public export withholds strategy (moat, Aha, maturity, private
940    /// source locations) without changing a single access decision.
941    #[test]
942    fn public_projection_withholds_strategy_but_keeps_every_access_decision() {
943        let canonical = ProductCapabilityRegistry::parse(REGISTRY_SOURCE).unwrap();
944        if canonical.is_public_projection() {
945            // Public export: parse() already enforced the projection; the
946            // canonical comparison runs where the canonical registry lives.
947            assert!(!REGISTRY_SOURCE.contains("crown_jewel"));
948            return;
949        }
950        let source = public_projection(REGISTRY_SOURCE).unwrap();
951        let projected = ProductCapabilityRegistry::parse(&source).unwrap();
952        assert!(projected.is_public_projection());
953        assert_eq!(public_projection(&source).unwrap(), source);
954        assert!(!source.contains("crown_jewel"));
955        assert_eq!(projected.entries().len(), canonical.entries().len());
956        for (before, after) in canonical.entries().iter().zip(projected.entries()) {
957            if before.source_visibility == SourceVisibility::PublicApache {
958                assert_eq!(before, after);
959            } else {
960                assert_eq!(after.moat_sensitivity, MoatSensitivity::Undisclosed);
961                assert_eq!(after.aha_criticality, AhaCriticality::Undisclosed);
962                assert_eq!(after.public_maturity, PublicStatus::Private);
963                assert_eq!(after.source_path, UNDISCLOSED_SOURCE);
964                let mut restored = after.clone();
965                restored.aha_criticality = before.aha_criticality;
966                restored.moat_sensitivity = before.moat_sensitivity;
967                restored.public_maturity = before.public_maturity;
968                restored.source_path.clone_from(&before.source_path);
969                assert_eq!(
970                    before, &restored,
971                    "projection changed rights of {}",
972                    before.id
973                );
974            }
975            for plan in Plan::all() {
976                assert_eq!(
977                    canonical.allows(*plan, &before.id),
978                    projected.allows(*plan, &before.id)
979                );
980            }
981        }
982
983        let hidden = canonical
984            .entries()
985            .iter()
986            .find(|entry| entry.moat_sensitivity == MoatSensitivity::CrownJewel)
987            .unwrap();
988        let block = format!("id = \"{}\"", hidden.id);
989        let (head, tail) = source.split_once(&block).unwrap();
990        let partial = format!(
991            "{head}{block}{}",
992            tail.replacen(
993                "moat_sensitivity = \"undisclosed\"",
994                "moat_sensitivity = \"crown_jewel\"",
995                1
996            )
997        );
998        for (registry, case) in [
999            (
1000                mutated_first("moat_sensitivity", "undisclosed".into()),
1001                "canonical",
1002            ),
1003            (partial, "partial projection"),
1004            (
1005                source.replacen("projection = \"public\"", "projection = \"other\"", 1),
1006                "unknown",
1007            ),
1008            (
1009                source.replacen(
1010                    &format!("source_path = \"{}\"", registry().entries()[0].source_path),
1011                    &format!("source_path = \"{UNDISCLOSED_SOURCE}\""),
1012                    1,
1013                ),
1014                "public record withheld",
1015            ),
1016        ] {
1017            assert!(
1018                ProductCapabilityRegistry::parse(&registry).is_err(),
1019                "accepted {case}"
1020            );
1021        }
1022    }
1023
1024    /// A3 (Hive): local cockpit views are free, filter packs and team or
1025    /// governance rights are Hive plans, Personal Cloud is retired, and no
1026    /// schema-3 record keeps a legacy price class.
1027    #[test]
1028    fn hive_classification_keeps_local_free_and_retires_personal_cloud() {
1029        let registry = registry();
1030        for entry in registry.entries() {
1031            assert!(
1032                matches!(
1033                    entry.price_class,
1034                    UserPriceClass::Free
1035                        | UserPriceClass::Hive
1036                        | UserPriceClass::Retired
1037                        | UserPriceClass::SdkOemCommercial
1038                ),
1039                "{} keeps a legacy price class",
1040                entry.id
1041            );
1042            if entry.price_class == UserPriceClass::Free {
1043                assert!(!entry.account_required, "{}", entry.id);
1044            }
1045        }
1046        for id in [
1047            "pro.dashboard.knowledge_workflows",
1048            "pro.runtime.adaptive_context",
1049            "pro.runtime.memory_curation",
1050            "pro.runtime.context_policy",
1051        ] {
1052            let entry = registry.find(id).unwrap();
1053            assert_eq!(entry.price_class, UserPriceClass::Free, "{id}");
1054            assert!(registry.allows(Plan::Community, id), "{id}");
1055        }
1056        for (id, plan) in [
1057            ("pro.runtime.semantic_detectors", HivePlan::Personal),
1058            ("pro.runtime.code_security", HivePlan::Personal),
1059            ("pro.runtime.personal_protection", HivePlan::Personal),
1060            ("team.private_registry", HivePlan::Team),
1061            ("team.managed_connectors", HivePlan::Team),
1062            ("team.sso_oidc", HivePlan::Enterprise),
1063            ("team.audit_retention", HivePlan::Enterprise),
1064            (
1065                "enterprise.runtime.org_policy_verifier",
1066                HivePlan::Enterprise,
1067            ),
1068        ] {
1069            let entry = registry.find(id).unwrap();
1070            assert_eq!(entry.price_class, UserPriceClass::Hive, "{id}");
1071            assert_eq!(entry.hive_plan, Some(plan), "{id}");
1072            assert_eq!(entry.minimum_plan(), plan.plan(), "{id}");
1073            assert!(!registry.allows(Plan::Community, id), "{id}");
1074            assert!(registry.allows(Plan::Enterprise, id), "{id}");
1075        }
1076        for id in ["pro.cloud_sync", "pro.hosted_index", "team.revenue_share"] {
1077            let entry = registry.find(id).unwrap();
1078            assert_eq!(entry.price_class, UserPriceClass::Retired, "{id}");
1079            assert_ne!(entry.minimum_plan(), Plan::Community, "{id}");
1080            for plan in Plan::all() {
1081                assert!(!registry.allows(*plan, id), "{id} granted to {plan:?}");
1082            }
1083        }
1084        // Stable lookup names survive the reclassification.
1085        assert_eq!(registry.find("cloud_sync").unwrap().id, "pro.cloud_sync");
1086        assert_eq!(registry.find("sso_oidc").unwrap().id, "team.sso_oidc");
1087
1088        let hive = registry.find("team.context.shared").unwrap().clone();
1089        for legacy in [
1090            UserPriceClass::CloudPaid,
1091            UserPriceClass::ProPaid,
1092            UserPriceClass::TeamPaid,
1093        ] {
1094            let mut broken = hive.clone();
1095            broken.price_class = legacy;
1096            broken.hive_plan = None;
1097            broken.runtime_delivery = RuntimeDelivery::ManagedCloudService;
1098            assert!(validate_record(&broken).is_err(), "{legacy:?} accepted");
1099        }
1100        let mut planless = hive.clone();
1101        planless.hive_plan = None;
1102        assert!(validate_record(&planless).is_err());
1103        let mut stray = registry.find("free.cockpit.ui").unwrap().clone();
1104        stray.hive_plan = Some(HivePlan::Personal);
1105        assert!(validate_record(&stray).is_err());
1106        let mut local = hive;
1107        local.managed_cloud_available = false;
1108        assert!(validate_record(&local).is_err());
1109    }
1110
1111    #[test]
1112    fn legacy_aliases_cannot_shadow_ids_or_point_to_unclassified_capabilities() {
1113        for target in ["missing.capability", "compression"] {
1114            let mut document: toml::Value = toml::from_str(REGISTRY_SOURCE).unwrap();
1115            document["aliases"]
1116                .as_table_mut()
1117                .unwrap()
1118                .insert("bad_alias".into(), target.into());
1119            assert!(
1120                ProductCapabilityRegistry::parse(&toml::to_string(&document).unwrap()).is_err()
1121            );
1122        }
1123        let mut document: toml::Value = toml::from_str(REGISTRY_SOURCE).unwrap();
1124        document["aliases"].as_table_mut().unwrap().insert(
1125            "trust.runtime.compression".into(),
1126            "trust.runtime.caching".into(),
1127        );
1128        assert!(ProductCapabilityRegistry::parse(&toml::to_string(&document).unwrap()).is_err());
1129    }
1130}