Skip to main content

lean_ctx/core/
contracts.rs

1use std::collections::BTreeMap;
2
3// Machine-verified contract versions.
4pub const MCP_MANIFEST_SCHEMA_VERSION: u32 = 1;
5pub const CONTEXT_PROOF_V1_SCHEMA_VERSION: u32 = 1;
6pub const CONTEXT_IR_V1_SCHEMA_VERSION: u32 = 1;
7pub const INTENT_ROUTE_V1_SCHEMA_VERSION: u32 = 1;
8pub const DEGRADATION_POLICY_V1_SCHEMA_VERSION: u32 = 1;
9pub const WORKFLOW_EVIDENCE_LEDGER_V1_SCHEMA_VERSION: u32 = 1;
10pub const AUTONOMY_DRIVERS_V1_SCHEMA_VERSION: u32 = 1;
11pub const TOKENIZER_TRANSLATION_DRIVER_V1_SCHEMA_VERSION: u32 = 1;
12pub const ATTENTION_LAYOUT_DRIVER_V1_SCHEMA_VERSION: u32 = 1;
13pub const VERIFICATION_OBSERVABILITY_V1_SCHEMA_VERSION: u32 = 1;
14pub const HANDOFF_LEDGER_V1_SCHEMA_VERSION: u32 = 1;
15pub const HANDOFF_TRANSFER_BUNDLE_V1_SCHEMA_VERSION: u32 = 1;
16pub const CCP_SESSION_BUNDLE_V1_SCHEMA_VERSION: u32 = 1;
17pub const KNOWLEDGE_POLICY_V1_SCHEMA_VERSION: u32 = 1;
18pub const GRAPH_REPRODUCIBILITY_V1_SCHEMA_VERSION: u32 = 1;
19pub const A2A_SNAPSHOT_V1_SCHEMA_VERSION: u32 = 1;
20pub const MEMORY_BOUNDARY_V1_SCHEMA_VERSION: u32 = 1;
21pub const GOTCHAS_REMINDERS_V1_SCHEMA_VERSION: u32 = 1;
22pub const PROVIDER_FRAMEWORK_V1_SCHEMA_VERSION: u32 = 1;
23pub const CONTEXT_PACKAGE_V1_SCHEMA_VERSION: u32 = 1;
24pub const CONTEXT_PACKAGE_V2_SCHEMA_VERSION: u32 = 2;
25
26pub const PACKAGE_EXTENSION: &str = "ctxpkg";
27pub const LEGACY_PACKAGE_EXTENSION: &str = "lctxpkg";
28pub const MAX_PACKAGE_FILE_BYTES: u64 = 10 * 1024 * 1024; // 10 MB
29
30pub fn is_package_file(path: &std::path::Path) -> bool {
31    path.extension()
32        .and_then(|e| e.to_str())
33        .is_some_and(|ext| ext == PACKAGE_EXTENSION || ext == LEGACY_PACKAGE_EXTENSION)
34}
35
36pub fn default_package_filename(name: &str, version: &str) -> String {
37    format!("{name}-{version}.{PACKAGE_EXTENSION}")
38}
39
40// Documentation-level contracts (do not have a schema field in payloads).
41pub const HTTP_MCP_CONTRACT_VERSION: u32 = 1;
42pub const TEAM_SERVER_CONTRACT_VERSION: u32 = 1;
43pub const CAPABILITIES_CONTRACT_VERSION: u32 = 1;
44
45/// Stability classification of a contract document (GL #394).
46///
47/// The classification is normative — `tests/contracts_frozen.rs` enforces it:
48/// * `Frozen` — the normative surface is immutable. Any change to the doc file
49///   fails CI; semantic evolution requires a new `-v2.md` file (the v1 file
50///   stays in place for existing integrations).
51/// * `Stable` — additive evolution allowed (new optional fields, new sections);
52///   breaking changes still require a version bump per CONTRACTS.md rules.
53/// * `Experimental` — may change or disappear without notice; not covered by
54///   the deprecation policy.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum ContractStatus {
57    Frozen,
58    Stable,
59    Experimental,
60}
61
62impl ContractStatus {
63    pub fn as_str(self) -> &'static str {
64        match self {
65            ContractStatus::Frozen => "frozen",
66            ContractStatus::Stable => "stable",
67            ContractStatus::Experimental => "experimental",
68        }
69    }
70}
71
72/// One contract document under `docs/contracts/`, classified for the
73/// stability matrix in CONTRACTS.md and the `/v1/capabilities` response.
74pub struct ContractDoc {
75    /// Short stable identifier (used in capabilities `contract_status`).
76    pub id: &'static str,
77    /// File name inside `docs/contracts/` (the normative artifact).
78    pub doc_file: &'static str,
79    pub version: u32,
80    pub status: ContractStatus,
81}
82
83/// The complete classified inventory of `docs/contracts/*.md` — the single
84/// source of truth for the stability matrix. `tests/contracts_frozen.rs`
85/// asserts that every file in the directory is listed here (no contract can
86/// stay unclassified) and that frozen docs never change.
87pub fn contract_docs() -> Vec<ContractDoc> {
88    use ContractStatus::{Experimental, Frozen, Stable};
89    let doc = |id, doc_file, version, status| ContractDoc {
90        id,
91        doc_file,
92        version,
93        status,
94    };
95    vec![
96        // ── Frozen: externally consumed platform/transport promises ────────
97        doc("http-mcp", "http-mcp-contract-v1.md", 1, Frozen),
98        doc("team-server", "team-server-contract-v1.md", 1, Frozen),
99        doc("context-ir", "context-ir-v1.md", 1, Frozen),
100        doc(
101            "local-free-invariant",
102            "local-free-invariant-v1.md",
103            1,
104            Frozen,
105        ),
106        doc(
107            "oss-plane-separation",
108            "oss-plane-separation-v1.md",
109            1,
110            Frozen,
111        ),
112        doc("billing-plane", "billing-plane-v1.md", 1, Frozen),
113        doc("wasm-abi", "wasm-abi-v1.md", 1, Frozen),
114        // ── Stable: additive evolution allowed ──────────────────────────────
115        // capabilities is additive BY DESIGN: its drift test binds the doc's
116        // key list to TOP_LEVEL_KEYS, so the doc grows with every new key —
117        // freezing the file would contradict its own contract.
118        doc("capabilities", "capabilities-contract-v1.md", 1, Stable),
119        doc("billing-plane-v2", "billing-plane-v2.md", 2, Stable),
120        // v2 = v1 + storageQuotaBytes/roiWebhookUrl (GL #387/#388); v1 stays frozen.
121        doc("billing-plane-v3", "billing-plane-v3.md", 3, Stable),
122        // v3 = v1 + business plan + sso_oidc entitlement (GL #460/#533); additive.
123        doc("evidence-bundle", "evidence-bundle-v1.md", 1, Stable),
124        // Offline-verifiable audit evidence ZIP (GL #425, H3 Epic A).
125        doc("team-server-v2", "team-server-contract-v2.md", 2, Stable),
126        doc("a2a", "a2a-contract-v1.md", 1, Stable),
127        doc(
128            "attention-layout-driver",
129            "attention-layout-driver-v1.md",
130            1,
131            Stable,
132        ),
133        doc("autonomy-drivers", "autonomy-drivers-v1.md", 1, Stable),
134        doc("ccp-session-bundle", "ccp-session-bundle-v1.md", 1, Stable),
135        doc("conformance", "conformance-v1.md", 1, Stable),
136        doc("degradation-policy", "degradation-policy-v1.md", 1, Stable),
137        doc("extension-trust", "extension-trust-v1.md", 1, Stable),
138        doc("extractors", "extractors-v1.md", 1, Stable),
139        doc(
140            "gotchas-reminders",
141            "gotchas-reminders-contract-v1.md",
142            1,
143            Stable,
144        ),
145        doc(
146            "graph-reproducibility",
147            "graph-reproducibility-contract-v1.md",
148            1,
149            Stable,
150        ),
151        doc(
152            "handoff-transfer-bundle",
153            "handoff-transfer-bundle-v1.md",
154            1,
155            Stable,
156        ),
157        doc("intent-route", "intent-route-v1.md", 1, Stable),
158        doc(
159            "knowledge-policy",
160            "knowledge-policy-contract-v1.md",
161            1,
162            Stable,
163        ),
164        doc(
165            "memory-boundary",
166            "memory-boundary-contract-v1.md",
167            1,
168            Stable,
169        ),
170        doc("persona-spec", "persona-spec-v1.md", 1, Stable),
171        doc(
172            "provider-framework",
173            "provider-framework-contract-v1.md",
174            1,
175            Stable,
176        ),
177        doc(
178            "tokenizer-translation-driver",
179            "tokenizer-translation-driver-v1.md",
180            1,
181            Stable,
182        ),
183        doc(
184            "workflow-evidence-ledger",
185            "workflow-evidence-ledger-v1.md",
186            1,
187            Stable,
188        ),
189        doc("wrapped-permalink", "wrapped-permalink-v1.md", 1, Stable),
190        // Community addon manifest (#858): self-declared stable (v1); the format
191        // evolves additively (new optional fields), so Stable, not Frozen.
192        doc("addon-manifest", "addon-manifest-v1.md", 1, Stable),
193        // ── Experimental: may change without notice ─────────────────────────
194        doc(
195            "hosted-personal-index",
196            "hosted-personal-index-v1.md",
197            1,
198            Experimental,
199        ),
200        doc(
201            "personal-cloud-encryption",
202            "personal-cloud-encryption-v1.md",
203            1,
204            Experimental,
205        ),
206        // 2026-06 org/cloud-plane wave — fresh surfaces, not yet consumed by
207        // external integrations; promote to Stable deliberately, not by default.
208        doc(
209            "context-policy-packs",
210            "context-policy-packs-v1.md",
211            1,
212            Experimental,
213        ),
214        doc("device-overview", "device-overview-v1.md", 1, Experimental),
215        doc("email-digest", "email-digest-v1.md", 1, Experimental),
216        doc("org-audit-log", "org-audit-log-v1.md", 1, Experimental),
217        doc("org-sso-oidc", "org-sso-oidc-v1.md", 1, Experimental),
218        // Quality loop (GL #494): edit-failure feedback into mode selection.
219        doc("quality-loop", "quality-loop-v1.md", 1, Experimental),
220        // Hosted ctxpkg registry (GL #406): fresh server surface.
221        doc("ctxpkg-registry", "ctxpkg-registry-v1.md", 1, Experimental),
222        doc(
223            "team-invite-links",
224            "team-invite-links-v1.md",
225            1,
226            Experimental,
227        ),
228        // Org policy & compliance surfaces, still evolving with the Enterprise
229        // plane — Experimental until they stabilise. Commercial Enterprise
230        // licensing (#667) and success-fee billing (#669) live in the private
231        // cloud plane, not in the open engine (oss-plane-separation-v1).
232        doc("org-policy", "org-policy-v1.md", 1, Experimental),
233        doc(
234            "compliance-report",
235            "compliance-report-v1.md",
236            1,
237            Experimental,
238        ),
239    ]
240}
241
242/// Contract-id → stability status, exported through `/v1/capabilities` so
243/// clients can verify compatibility before relying on a surface (GL #394).
244pub fn status_kv() -> BTreeMap<&'static str, &'static str> {
245    contract_docs()
246        .into_iter()
247        .map(|d| (d.id, d.status.as_str()))
248        .collect()
249}
250
251pub fn versions_kv() -> BTreeMap<&'static str, u32> {
252    BTreeMap::from([
253        (
254            "leanctx.contract.mcp_manifest.schema_version",
255            MCP_MANIFEST_SCHEMA_VERSION,
256        ),
257        (
258            "leanctx.contract.context_proof_v1.schema_version",
259            CONTEXT_PROOF_V1_SCHEMA_VERSION,
260        ),
261        (
262            "leanctx.contract.context_ir_v1.schema_version",
263            CONTEXT_IR_V1_SCHEMA_VERSION,
264        ),
265        (
266            "leanctx.contract.intent_route_v1.schema_version",
267            INTENT_ROUTE_V1_SCHEMA_VERSION,
268        ),
269        (
270            "leanctx.contract.degradation_policy_v1.schema_version",
271            DEGRADATION_POLICY_V1_SCHEMA_VERSION,
272        ),
273        (
274            "leanctx.contract.workflow_evidence_ledger_v1.schema_version",
275            WORKFLOW_EVIDENCE_LEDGER_V1_SCHEMA_VERSION,
276        ),
277        (
278            "leanctx.contract.autonomy_drivers_v1.schema_version",
279            AUTONOMY_DRIVERS_V1_SCHEMA_VERSION,
280        ),
281        (
282            "leanctx.contract.tokenizer_translation_driver_v1.schema_version",
283            TOKENIZER_TRANSLATION_DRIVER_V1_SCHEMA_VERSION,
284        ),
285        (
286            "leanctx.contract.attention_layout_driver_v1.schema_version",
287            ATTENTION_LAYOUT_DRIVER_V1_SCHEMA_VERSION,
288        ),
289        (
290            "leanctx.contract.verification_observability_v1.schema_version",
291            VERIFICATION_OBSERVABILITY_V1_SCHEMA_VERSION,
292        ),
293        (
294            "leanctx.contract.handoff_ledger_v1.schema_version",
295            HANDOFF_LEDGER_V1_SCHEMA_VERSION,
296        ),
297        (
298            "leanctx.contract.handoff_transfer_bundle_v1.schema_version",
299            HANDOFF_TRANSFER_BUNDLE_V1_SCHEMA_VERSION,
300        ),
301        (
302            "leanctx.contract.ccp_session_bundle_v1.schema_version",
303            CCP_SESSION_BUNDLE_V1_SCHEMA_VERSION,
304        ),
305        (
306            "leanctx.contract.knowledge_policy_v1.schema_version",
307            KNOWLEDGE_POLICY_V1_SCHEMA_VERSION,
308        ),
309        (
310            "leanctx.contract.graph_reproducibility_v1.schema_version",
311            GRAPH_REPRODUCIBILITY_V1_SCHEMA_VERSION,
312        ),
313        (
314            "leanctx.contract.a2a_snapshot_v1.schema_version",
315            A2A_SNAPSHOT_V1_SCHEMA_VERSION,
316        ),
317        (
318            "leanctx.contract.memory_boundary_v1.schema_version",
319            MEMORY_BOUNDARY_V1_SCHEMA_VERSION,
320        ),
321        (
322            "leanctx.contract.gotchas_reminders_v1.schema_version",
323            GOTCHAS_REMINDERS_V1_SCHEMA_VERSION,
324        ),
325        (
326            "leanctx.contract.provider_framework_v1.schema_version",
327            PROVIDER_FRAMEWORK_V1_SCHEMA_VERSION,
328        ),
329        (
330            "leanctx.contract.context_package_v1.schema_version",
331            CONTEXT_PACKAGE_V1_SCHEMA_VERSION,
332        ),
333        (
334            "leanctx.contract.context_package_v2.schema_version",
335            CONTEXT_PACKAGE_V2_SCHEMA_VERSION,
336        ),
337        (
338            "leanctx.contract.http_mcp.contract_version",
339            HTTP_MCP_CONTRACT_VERSION,
340        ),
341        (
342            "leanctx.contract.team_server.contract_version",
343            TEAM_SERVER_CONTRACT_VERSION,
344        ),
345        (
346            "leanctx.contract.capabilities.contract_version",
347            CAPABILITIES_CONTRACT_VERSION,
348        ),
349    ])
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    #[test]
357    fn contract_docs_have_unique_ids_and_files() {
358        let docs = contract_docs();
359        let mut ids: Vec<_> = docs.iter().map(|d| d.id).collect();
360        let mut files: Vec<_> = docs.iter().map(|d| d.doc_file).collect();
361        ids.sort_unstable();
362        files.sort_unstable();
363        let unique_ids: std::collections::BTreeSet<_> = ids.iter().collect();
364        let unique_files: std::collections::BTreeSet<_> = files.iter().collect();
365        assert_eq!(unique_ids.len(), docs.len(), "duplicate contract id");
366        assert_eq!(unique_files.len(), docs.len(), "duplicate doc file");
367    }
368
369    #[test]
370    fn frozen_set_covers_the_platform_promises() {
371        // The freeze (GL #394) is only meaningful if the externally consumed
372        // surfaces are actually in it. Removing one of these from `Frozen`
373        // is itself a breaking policy change.
374        let docs = contract_docs();
375        for id in [
376            "http-mcp",
377            "team-server",
378            "context-ir",
379            "local-free-invariant",
380            "oss-plane-separation",
381            "billing-plane",
382            "wasm-abi",
383        ] {
384            let entry = docs.iter().find(|d| d.id == id).expect("listed");
385            assert_eq!(
386                entry.status,
387                ContractStatus::Frozen,
388                "{id} must stay frozen"
389            );
390        }
391    }
392
393    #[test]
394    fn status_kv_matches_docs() {
395        let kv = status_kv();
396        assert_eq!(kv.len(), contract_docs().len());
397        assert_eq!(kv["http-mcp"], "frozen");
398        assert_eq!(kv["hosted-personal-index"], "experimental");
399        assert_eq!(kv["personal-cloud-encryption"], "experimental");
400    }
401
402    #[test]
403    fn doc_files_follow_versioned_naming() {
404        // v1→v2 rule: every doc file carries its version suffix so a breaking
405        // change lands as a NEW file instead of mutating the old one.
406        for d in contract_docs() {
407            assert!(
408                d.doc_file.ends_with(&format!("-v{}.md", d.version)),
409                "{} must end in -v{}.md",
410                d.doc_file,
411                d.version
412            );
413        }
414    }
415}