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;
25pub const CONTEXT_SNAPSHOT_V1_SCHEMA_VERSION: u32 = 1;
26
27pub const PACKAGE_EXTENSION: &str = "ctxpkg";
28pub const LEGACY_PACKAGE_EXTENSION: &str = "lctxpkg";
29pub const MAX_PACKAGE_FILE_BYTES: u64 = 10 * 1024 * 1024; // 10 MB
30
31pub fn is_package_file(path: &std::path::Path) -> bool {
32    path.extension()
33        .and_then(|e| e.to_str())
34        .is_some_and(|ext| ext == PACKAGE_EXTENSION || ext == LEGACY_PACKAGE_EXTENSION)
35}
36
37pub fn default_package_filename(name: &str, version: &str) -> String {
38    format!("{name}-{version}.{PACKAGE_EXTENSION}")
39}
40
41// Documentation-level contracts (do not have a schema field in payloads).
42pub const HTTP_MCP_CONTRACT_VERSION: u32 = 1;
43pub const TEAM_SERVER_CONTRACT_VERSION: u32 = 1;
44pub const CAPABILITIES_CONTRACT_VERSION: u32 = 1;
45
46/// Stability classification of a contract document (GL #394).
47///
48/// The classification is normative — `tests/contracts_frozen.rs` enforces it:
49/// * `Frozen` — the normative surface is immutable. Any change to the doc file
50///   fails CI; semantic evolution requires a new `-v2.md` file (the v1 file
51///   stays in place for existing integrations).
52/// * `Stable` — additive evolution allowed (new optional fields, new sections);
53///   breaking changes still require a version bump per CONTRACTS.md rules.
54/// * `Experimental` — may change or disappear without notice; not covered by
55///   the deprecation policy.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum ContractStatus {
58    Frozen,
59    Stable,
60    Experimental,
61}
62
63impl ContractStatus {
64    pub fn as_str(self) -> &'static str {
65        match self {
66            ContractStatus::Frozen => "frozen",
67            ContractStatus::Stable => "stable",
68            ContractStatus::Experimental => "experimental",
69        }
70    }
71}
72
73/// One contract document under `docs/contracts/`, classified for the
74/// stability matrix in CONTRACTS.md and the `/v1/capabilities` response.
75pub struct ContractDoc {
76    /// Short stable identifier (used in capabilities `contract_status`).
77    pub id: &'static str,
78    /// File name inside `docs/contracts/` (the normative artifact).
79    pub doc_file: &'static str,
80    pub version: u32,
81    pub status: ContractStatus,
82}
83
84/// The complete classified inventory of `docs/contracts/*.md` — the single
85/// source of truth for the stability matrix. `tests/contracts_frozen.rs`
86/// asserts that every file in the directory is listed here (no contract can
87/// stay unclassified) and that frozen docs never change.
88pub fn contract_docs() -> Vec<ContractDoc> {
89    use ContractStatus::{Experimental, Frozen, Stable};
90    let doc = |id, doc_file, version, status| ContractDoc {
91        id,
92        doc_file,
93        version,
94        status,
95    };
96    vec![
97        // ── Frozen: externally consumed platform/transport promises ────────
98        doc("http-mcp", "http-mcp-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("wasm-abi", "wasm-abi-v1.md", 1, Frozen),
113        // Release promotion, offline rollback rehearsal and signing-key
114        // rotation are externally consumed fail-closed supply-chain formats.
115        // Their exact v1 semantics are immutable; evolution requires v2.
116        doc("delivery-manifest", "delivery-manifest-v1.md", 1, Frozen),
117        doc(
118            "deployment-rehearsal",
119            "deployment-rehearsal-v1.md",
120            1,
121            Frozen,
122        ),
123        doc(
124            "release-key-rotation",
125            "release-key-rotation-v1.md",
126            1,
127            Frozen,
128        ),
129        // ── Stable: additive evolution allowed ──────────────────────────────
130        // capabilities is additive BY DESIGN: its drift test binds the doc's
131        // key list to TOP_LEVEL_KEYS, so the doc grows with every new key —
132        // freezing the file would contradict its own contract.
133        doc("capabilities", "capabilities-contract-v1.md", 1, Stable),
134        // v2 = v1 + storageQuotaBytes/roiWebhookUrl (GL #387/#388); v1 stays frozen.
135        // v3 = v1 + sso_oidc entitlement (GL #460/#533); additive. Business merged into Team in v3.9.
136        doc("evidence-bundle", "evidence-bundle-v1.md", 1, Stable),
137        doc("a2a", "a2a-contract-v1.md", 1, Stable),
138        doc(
139            "attention-layout-driver",
140            "attention-layout-driver-v1.md",
141            1,
142            Stable,
143        ),
144        doc("autonomy-drivers", "autonomy-drivers-v1.md", 1, Stable),
145        doc("conformance", "conformance-v1.md", 1, Stable),
146        doc(
147            "ocla-verifier-conformance",
148            "ocla-verifier-conformance-v1.md",
149            1,
150            Stable,
151        ),
152        doc("degradation-policy", "degradation-policy-v1.md", 1, Stable),
153        doc("extension-trust", "extension-trust-v1.md", 1, Stable),
154        doc("extractors", "extractors-v1.md", 1, Stable),
155        doc("contracts-readme", "README.md", 1, Stable),
156        doc("deprecation-policy", "DEPRECATION.md", 1, Stable),
157        doc("audit-schedule", "audit-schedule-v1.md", 1, Stable),
158        doc("branch-protection", "branch-protection-v1.md", 1, Stable),
159        doc(
160            "certification-levels",
161            "certification-levels-v1.md",
162            1,
163            Stable,
164        ),
165        doc("release-integrity", "release-integrity-v1.md", 1, Stable),
166        doc(
167            "gotchas-reminders",
168            "gotchas-reminders-contract-v1.md",
169            1,
170            Stable,
171        ),
172        doc(
173            "graph-reproducibility",
174            "graph-reproducibility-contract-v1.md",
175            1,
176            Stable,
177        ),
178        doc(
179            "handoff-transfer-bundle",
180            "handoff-transfer-bundle-v1.md",
181            1,
182            Stable,
183        ),
184        doc("intent-route", "intent-route-v1.md", 1, Stable),
185        doc(
186            "knowledge-policy",
187            "knowledge-policy-contract-v1.md",
188            1,
189            Stable,
190        ),
191        doc(
192            "memory-boundary",
193            "memory-boundary-contract-v1.md",
194            1,
195            Stable,
196        ),
197        doc("persona-spec", "persona-spec-v1.md", 1, Stable),
198        doc(
199            "provider-framework",
200            "provider-framework-contract-v1.md",
201            1,
202            Stable,
203        ),
204        doc(
205            "tokenizer-translation-driver",
206            "tokenizer-translation-driver-v1.md",
207            1,
208            Stable,
209        ),
210        doc(
211            "workflow-evidence-ledger",
212            "workflow-evidence-ledger-v1.md",
213            1,
214            Stable,
215        ),
216        doc("wrapped-permalink", "wrapped-permalink-v1.md", 1, Stable),
217        // Community addon manifest (#858): self-declared stable (v1); the format
218        // evolves additively (new optional fields), so Stable, not Frozen.
219        doc("addon-manifest", "addon-manifest-v1.md", 1, Stable),
220        // ── Experimental: may change without notice ─────────────────────────
221        // W0/W1 token-intelligence foundations are locally verified but do not
222        // yet claim complete hotpath adoption or externally consumed stability.
223        doc(
224            "context-candidate-admission",
225            "context-candidate-admission-v1.md",
226            1,
227            Experimental,
228        ),
229        doc(
230            "multi-agent-efficiency-benchmark",
231            "multi-agent-efficiency-benchmark-v1.md",
232            1,
233            Experimental,
234        ),
235        // The committed artifact is conformance-only; external deployment use
236        // and governed attestor adoption remain deliberately unclaimed.
237        doc(
238            "test-deployment-evidence",
239            "test-deployment-evidence-v1.md",
240            1,
241            Experimental,
242        ),
243        // Local Rust adapter only; external callability, authenticated approval,
244        // and recovery orchestration are deliberately not claimed by v2.
245        doc(
246            "ocla-config-tuning",
247            "ocla-config-tuning-v2.md",
248            2,
249            Experimental,
250        ),
251        doc(
252            "personal-cloud-encryption",
253            "personal-cloud-encryption-v1.md",
254            1,
255            Experimental,
256        ),
257        // 2026-06 org/cloud-plane wave — fresh surfaces, not yet consumed by
258        // external integrations; promote to Stable deliberately, not by default.
259        doc(
260            "context-policy-packs",
261            "context-policy-packs-v1.md",
262            1,
263            Experimental,
264        ),
265        // Quality loop (GL #494): edit-failure feedback into mode selection.
266        doc("quality-loop", "quality-loop-v1.md", 1, Experimental),
267        // Edit metering (GL #1144): anchored-vs-str_replace efficiency channel.
268        doc("edit-metering", "edit-metering-v1.md", 1, Experimental),
269        // Hosted ctxpkg registry (GL #406): fresh server surface.
270        doc("ctxpkg-registry", "ctxpkg-registry-v1.md", 1, Experimental),
271        // Context Time Machine (GL #1022/#1023): git-anchored, signed temporal
272        // snapshot format — fresh surface, evolving additively until stable.
273        doc(
274            "context-snapshot",
275            "context-snapshot-v1.md",
276            1,
277            Experimental,
278        ),
279        doc(
280            "team-invite-links",
281            "team-invite-links-v1.md",
282            1,
283            Experimental,
284        ),
285        doc("pillar-boundaries", "pillar-boundaries-v1.md", 1, Stable),
286        // Logical editor-session telemetry is an additive local-dashboard surface;
287        // keep it experimental until multiple integrations implement the lifecycle.
288        doc(
289            "logical-session-presence",
290            "logical-session-presence-v1.md",
291            1,
292            Experimental,
293        ),
294        doc(
295            "tokenizer-calibration",
296            "tokenizer-calibration-v1.md",
297            1,
298            Experimental,
299        ),
300    ]
301}
302
303/// Contract-id → stability status, exported through `/v1/capabilities` so
304/// clients can verify compatibility before relying on a surface (GL #394).
305pub fn status_kv() -> BTreeMap<&'static str, &'static str> {
306    contract_docs()
307        .into_iter()
308        .map(|d| (d.id, d.status.as_str()))
309        .collect()
310}
311
312pub fn versions_kv() -> BTreeMap<&'static str, u32> {
313    BTreeMap::from([
314        (
315            "leanctx.contract.mcp_manifest.schema_version",
316            MCP_MANIFEST_SCHEMA_VERSION,
317        ),
318        (
319            "leanctx.contract.context_proof_v1.schema_version",
320            CONTEXT_PROOF_V1_SCHEMA_VERSION,
321        ),
322        (
323            "leanctx.contract.context_ir_v1.schema_version",
324            CONTEXT_IR_V1_SCHEMA_VERSION,
325        ),
326        (
327            "leanctx.contract.intent_route_v1.schema_version",
328            INTENT_ROUTE_V1_SCHEMA_VERSION,
329        ),
330        (
331            "leanctx.contract.degradation_policy_v1.schema_version",
332            DEGRADATION_POLICY_V1_SCHEMA_VERSION,
333        ),
334        (
335            "leanctx.contract.workflow_evidence_ledger_v1.schema_version",
336            WORKFLOW_EVIDENCE_LEDGER_V1_SCHEMA_VERSION,
337        ),
338        (
339            "leanctx.contract.autonomy_drivers_v1.schema_version",
340            AUTONOMY_DRIVERS_V1_SCHEMA_VERSION,
341        ),
342        (
343            "leanctx.contract.tokenizer_translation_driver_v1.schema_version",
344            TOKENIZER_TRANSLATION_DRIVER_V1_SCHEMA_VERSION,
345        ),
346        (
347            "leanctx.contract.attention_layout_driver_v1.schema_version",
348            ATTENTION_LAYOUT_DRIVER_V1_SCHEMA_VERSION,
349        ),
350        (
351            "leanctx.contract.verification_observability_v1.schema_version",
352            VERIFICATION_OBSERVABILITY_V1_SCHEMA_VERSION,
353        ),
354        (
355            "leanctx.contract.handoff_ledger_v1.schema_version",
356            HANDOFF_LEDGER_V1_SCHEMA_VERSION,
357        ),
358        (
359            "leanctx.contract.handoff_transfer_bundle_v1.schema_version",
360            HANDOFF_TRANSFER_BUNDLE_V1_SCHEMA_VERSION,
361        ),
362        (
363            "leanctx.contract.ccp_session_bundle_v1.schema_version",
364            CCP_SESSION_BUNDLE_V1_SCHEMA_VERSION,
365        ),
366        (
367            "leanctx.contract.knowledge_policy_v1.schema_version",
368            KNOWLEDGE_POLICY_V1_SCHEMA_VERSION,
369        ),
370        (
371            "leanctx.contract.graph_reproducibility_v1.schema_version",
372            GRAPH_REPRODUCIBILITY_V1_SCHEMA_VERSION,
373        ),
374        (
375            "leanctx.contract.a2a_snapshot_v1.schema_version",
376            A2A_SNAPSHOT_V1_SCHEMA_VERSION,
377        ),
378        (
379            "leanctx.contract.memory_boundary_v1.schema_version",
380            MEMORY_BOUNDARY_V1_SCHEMA_VERSION,
381        ),
382        (
383            "leanctx.contract.gotchas_reminders_v1.schema_version",
384            GOTCHAS_REMINDERS_V1_SCHEMA_VERSION,
385        ),
386        (
387            "leanctx.contract.provider_framework_v1.schema_version",
388            PROVIDER_FRAMEWORK_V1_SCHEMA_VERSION,
389        ),
390        (
391            "leanctx.contract.context_package_v1.schema_version",
392            CONTEXT_PACKAGE_V1_SCHEMA_VERSION,
393        ),
394        (
395            "leanctx.contract.context_package_v2.schema_version",
396            CONTEXT_PACKAGE_V2_SCHEMA_VERSION,
397        ),
398        (
399            "leanctx.contract.context_snapshot_v1.schema_version",
400            CONTEXT_SNAPSHOT_V1_SCHEMA_VERSION,
401        ),
402        (
403            "leanctx.contract.http_mcp.contract_version",
404            HTTP_MCP_CONTRACT_VERSION,
405        ),
406        (
407            "leanctx.contract.team_server.contract_version",
408            TEAM_SERVER_CONTRACT_VERSION,
409        ),
410        (
411            "leanctx.contract.capabilities.contract_version",
412            CAPABILITIES_CONTRACT_VERSION,
413        ),
414    ])
415}
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420
421    #[test]
422    fn contract_docs_have_unique_ids_and_files() {
423        let docs = contract_docs();
424        let mut ids: Vec<_> = docs.iter().map(|d| d.id).collect();
425        let mut files: Vec<_> = docs.iter().map(|d| d.doc_file).collect();
426        ids.sort_unstable();
427        files.sort_unstable();
428        let unique_ids: std::collections::BTreeSet<_> = ids.iter().collect();
429        let unique_files: std::collections::BTreeSet<_> = files.iter().collect();
430        assert_eq!(unique_ids.len(), docs.len(), "duplicate contract id");
431        assert_eq!(unique_files.len(), docs.len(), "duplicate doc file");
432    }
433
434    #[test]
435    fn frozen_set_covers_the_platform_promises() {
436        // The freeze (GL #394) is only meaningful if the externally consumed
437        // surfaces are actually in it. Removing one of these from `Frozen`
438        // is itself a breaking policy change.
439        let docs = contract_docs();
440        for id in [
441            "http-mcp",
442            "context-ir",
443            "local-free-invariant",
444            "oss-plane-separation",
445            "wasm-abi",
446            "delivery-manifest",
447            "deployment-rehearsal",
448            "release-key-rotation",
449        ] {
450            let entry = docs.iter().find(|d| d.id == id).expect("listed");
451            assert_eq!(
452                entry.status,
453                ContractStatus::Frozen,
454                "{id} must stay frozen"
455            );
456        }
457    }
458
459    #[test]
460    fn status_kv_matches_docs() {
461        let kv = status_kv();
462        assert_eq!(kv.len(), contract_docs().len());
463        assert_eq!(kv["http-mcp"], "frozen");
464        assert_eq!(kv["personal-cloud-encryption"], "experimental");
465        assert_eq!(kv["context-candidate-admission"], "experimental");
466        assert_eq!(kv["multi-agent-efficiency-benchmark"], "experimental");
467        assert_eq!(kv["test-deployment-evidence"], "experimental");
468    }
469
470    #[test]
471    fn doc_files_follow_versioned_naming() {
472        // v1→v2 rule: every doc file carries its version suffix so a breaking
473        // change lands as a NEW file instead of mutating the old one.
474        // Governance metadata (README, DEPRECATION) are exempt.
475        for d in contract_docs() {
476            if d.doc_file == "README.md" || d.doc_file == "DEPRECATION.md" {
477                continue;
478            }
479            assert!(
480                d.doc_file.ends_with(&format!("-v{}.md", d.version)),
481                "{} must end in -v{}.md",
482                d.doc_file,
483                d.version
484            );
485        }
486    }
487}