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("team-server", "team-server-contract-v1.md", 1, Frozen),
100        doc("context-ir", "context-ir-v1.md", 1, Frozen),
101        doc(
102            "local-free-invariant",
103            "local-free-invariant-v1.md",
104            1,
105            Frozen,
106        ),
107        doc(
108            "oss-plane-separation",
109            "oss-plane-separation-v1.md",
110            1,
111            Frozen,
112        ),
113        doc("billing-plane", "billing-plane-v1.md", 1, Frozen),
114        doc("wasm-abi", "wasm-abi-v1.md", 1, Frozen),
115        // Release promotion, offline rollback rehearsal and signing-key
116        // rotation are externally consumed fail-closed supply-chain formats.
117        // Their exact v1 semantics are immutable; evolution requires v2.
118        doc("delivery-manifest", "delivery-manifest-v1.md", 1, Frozen),
119        doc(
120            "deployment-rehearsal",
121            "deployment-rehearsal-v1.md",
122            1,
123            Frozen,
124        ),
125        doc(
126            "release-key-rotation",
127            "release-key-rotation-v1.md",
128            1,
129            Frozen,
130        ),
131        // ── Stable: additive evolution allowed ──────────────────────────────
132        // capabilities is additive BY DESIGN: its drift test binds the doc's
133        // key list to TOP_LEVEL_KEYS, so the doc grows with every new key —
134        // freezing the file would contradict its own contract.
135        doc("capabilities", "capabilities-contract-v1.md", 1, Stable),
136        doc("billing-plane-v2", "billing-plane-v2.md", 2, Stable),
137        // v2 = v1 + storageQuotaBytes/roiWebhookUrl (GL #387/#388); v1 stays frozen.
138        doc("billing-plane-v3", "billing-plane-v3.md", 3, Stable),
139        // v3 = v1 + sso_oidc entitlement (GL #460/#533); additive. Business merged into Team in v3.9.
140        doc("evidence-bundle", "evidence-bundle-v1.md", 1, Stable),
141        // Offline-verifiable audit evidence ZIP (GL #425, H3 Epic A).
142        doc(
143            "settlement-evidence",
144            "settlement-evidence-v2.md",
145            2,
146            Stable,
147        ),
148        // Payload-free OSS eligibility evidence; approval/invoice authority stays private.
149        doc("team-server-v2", "team-server-contract-v2.md", 2, Stable),
150        doc("a2a", "a2a-contract-v1.md", 1, Stable),
151        doc(
152            "attention-layout-driver",
153            "attention-layout-driver-v1.md",
154            1,
155            Stable,
156        ),
157        doc("autonomy-drivers", "autonomy-drivers-v1.md", 1, Stable),
158        doc("ccp-session-bundle", "ccp-session-bundle-v1.md", 1, Stable),
159        doc("conformance", "conformance-v1.md", 1, Stable),
160        doc(
161            "ocla-verifier-conformance",
162            "ocla-verifier-conformance-v1.md",
163            1,
164            Stable,
165        ),
166        doc("degradation-policy", "degradation-policy-v1.md", 1, Stable),
167        doc("extension-trust", "extension-trust-v1.md", 1, Stable),
168        doc("extractors", "extractors-v1.md", 1, Stable),
169        doc("contracts-readme", "README.md", 1, Stable),
170        doc("deprecation-policy", "DEPRECATION.md", 1, Stable),
171        doc("audit-schedule", "audit-schedule-v1.md", 1, Stable),
172        doc("branch-protection", "branch-protection-v1.md", 1, Stable),
173        doc(
174            "certification-levels",
175            "certification-levels-v1.md",
176            1,
177            Stable,
178        ),
179        doc("release-integrity", "release-integrity-v1.md", 1, Stable),
180        doc(
181            "gotchas-reminders",
182            "gotchas-reminders-contract-v1.md",
183            1,
184            Stable,
185        ),
186        doc(
187            "graph-reproducibility",
188            "graph-reproducibility-contract-v1.md",
189            1,
190            Stable,
191        ),
192        doc(
193            "handoff-transfer-bundle",
194            "handoff-transfer-bundle-v1.md",
195            1,
196            Stable,
197        ),
198        doc("intent-route", "intent-route-v1.md", 1, Stable),
199        doc(
200            "knowledge-policy",
201            "knowledge-policy-contract-v1.md",
202            1,
203            Stable,
204        ),
205        doc(
206            "memory-boundary",
207            "memory-boundary-contract-v1.md",
208            1,
209            Stable,
210        ),
211        doc("persona-spec", "persona-spec-v1.md", 1, Stable),
212        doc(
213            "provider-framework",
214            "provider-framework-contract-v1.md",
215            1,
216            Stable,
217        ),
218        doc(
219            "tokenizer-translation-driver",
220            "tokenizer-translation-driver-v1.md",
221            1,
222            Stable,
223        ),
224        doc(
225            "workflow-evidence-ledger",
226            "workflow-evidence-ledger-v1.md",
227            1,
228            Stable,
229        ),
230        doc("wrapped-permalink", "wrapped-permalink-v1.md", 1, Stable),
231        // Community addon manifest (#858): self-declared stable (v1); the format
232        // evolves additively (new optional fields), so Stable, not Frozen.
233        doc("addon-manifest", "addon-manifest-v1.md", 1, Stable),
234        // ── Experimental: may change without notice ─────────────────────────
235        // W0/W1 token-intelligence foundations are locally verified but do not
236        // yet claim complete hotpath adoption or externally consumed stability.
237        doc(
238            "context-candidate-admission",
239            "context-candidate-admission-v1.md",
240            1,
241            Experimental,
242        ),
243        doc(
244            "multi-agent-efficiency-benchmark",
245            "multi-agent-efficiency-benchmark-v1.md",
246            1,
247            Experimental,
248        ),
249        // The committed artifact is conformance-only; external deployment use
250        // and governed attestor adoption remain deliberately unclaimed.
251        doc(
252            "test-deployment-evidence",
253            "test-deployment-evidence-v1.md",
254            1,
255            Experimental,
256        ),
257        // Local Rust adapter only; external callability, authenticated approval,
258        // and recovery orchestration are deliberately not claimed by v2.
259        doc(
260            "ocla-config-tuning",
261            "ocla-config-tuning-v2.md",
262            2,
263            Experimental,
264        ),
265        doc(
266            "hosted-personal-index",
267            "hosted-personal-index-v1.md",
268            1,
269            Experimental,
270        ),
271        doc(
272            "personal-cloud-encryption",
273            "personal-cloud-encryption-v1.md",
274            1,
275            Experimental,
276        ),
277        // 2026-06 org/cloud-plane wave — fresh surfaces, not yet consumed by
278        // external integrations; promote to Stable deliberately, not by default.
279        doc(
280            "context-policy-packs",
281            "context-policy-packs-v1.md",
282            1,
283            Experimental,
284        ),
285        doc("device-overview", "device-overview-v1.md", 1, Experimental),
286        doc("email-digest", "email-digest-v1.md", 1, Experimental),
287        doc("org-audit-log", "org-audit-log-v1.md", 1, Experimental),
288        doc("org-sso-oidc", "org-sso-oidc-v1.md", 1, Experimental),
289        // Quality loop (GL #494): edit-failure feedback into mode selection.
290        doc("quality-loop", "quality-loop-v1.md", 1, Experimental),
291        // Edit metering (GL #1144): anchored-vs-str_replace efficiency channel.
292        doc("edit-metering", "edit-metering-v1.md", 1, Experimental),
293        // Hosted ctxpkg registry (GL #406): fresh server surface.
294        doc("ctxpkg-registry", "ctxpkg-registry-v1.md", 1, Experimental),
295        // Context Time Machine (GL #1022/#1023): git-anchored, signed temporal
296        // snapshot format — fresh surface, evolving additively until stable.
297        doc(
298            "context-snapshot",
299            "context-snapshot-v1.md",
300            1,
301            Experimental,
302        ),
303        doc(
304            "team-invite-links",
305            "team-invite-links-v1.md",
306            1,
307            Experimental,
308        ),
309        // Org policy & compliance surfaces, still evolving with the Enterprise
310        // plane — Experimental until they stabilise. Commercial Enterprise
311        // licensing (#667) and success-fee billing (#669) live in the private
312        // cloud plane, not in the open engine (oss-plane-separation-v1).
313        doc("org-policy", "org-policy-v1.md", 1, Experimental),
314        doc(
315            "compliance-report",
316            "compliance-report-v1.md",
317            1,
318            Experimental,
319        ),
320        doc("pillar-boundaries", "pillar-boundaries-v1.md", 1, Stable),
321        // Logical editor-session telemetry is an additive local-dashboard surface;
322        // keep it experimental until multiple integrations implement the lifecycle.
323        doc(
324            "logical-session-presence",
325            "logical-session-presence-v1.md",
326            1,
327            Experimental,
328        ),
329        doc(
330            "tokenizer-calibration",
331            "tokenizer-calibration-v1.md",
332            1,
333            Experimental,
334        ),
335    ]
336}
337
338/// Contract-id → stability status, exported through `/v1/capabilities` so
339/// clients can verify compatibility before relying on a surface (GL #394).
340pub fn status_kv() -> BTreeMap<&'static str, &'static str> {
341    contract_docs()
342        .into_iter()
343        .map(|d| (d.id, d.status.as_str()))
344        .collect()
345}
346
347pub fn versions_kv() -> BTreeMap<&'static str, u32> {
348    BTreeMap::from([
349        (
350            "leanctx.contract.mcp_manifest.schema_version",
351            MCP_MANIFEST_SCHEMA_VERSION,
352        ),
353        (
354            "leanctx.contract.context_proof_v1.schema_version",
355            CONTEXT_PROOF_V1_SCHEMA_VERSION,
356        ),
357        (
358            "leanctx.contract.context_ir_v1.schema_version",
359            CONTEXT_IR_V1_SCHEMA_VERSION,
360        ),
361        (
362            "leanctx.contract.intent_route_v1.schema_version",
363            INTENT_ROUTE_V1_SCHEMA_VERSION,
364        ),
365        (
366            "leanctx.contract.degradation_policy_v1.schema_version",
367            DEGRADATION_POLICY_V1_SCHEMA_VERSION,
368        ),
369        (
370            "leanctx.contract.workflow_evidence_ledger_v1.schema_version",
371            WORKFLOW_EVIDENCE_LEDGER_V1_SCHEMA_VERSION,
372        ),
373        (
374            "leanctx.contract.autonomy_drivers_v1.schema_version",
375            AUTONOMY_DRIVERS_V1_SCHEMA_VERSION,
376        ),
377        (
378            "leanctx.contract.tokenizer_translation_driver_v1.schema_version",
379            TOKENIZER_TRANSLATION_DRIVER_V1_SCHEMA_VERSION,
380        ),
381        (
382            "leanctx.contract.attention_layout_driver_v1.schema_version",
383            ATTENTION_LAYOUT_DRIVER_V1_SCHEMA_VERSION,
384        ),
385        (
386            "leanctx.contract.verification_observability_v1.schema_version",
387            VERIFICATION_OBSERVABILITY_V1_SCHEMA_VERSION,
388        ),
389        (
390            "leanctx.contract.handoff_ledger_v1.schema_version",
391            HANDOFF_LEDGER_V1_SCHEMA_VERSION,
392        ),
393        (
394            "leanctx.contract.handoff_transfer_bundle_v1.schema_version",
395            HANDOFF_TRANSFER_BUNDLE_V1_SCHEMA_VERSION,
396        ),
397        (
398            "leanctx.contract.ccp_session_bundle_v1.schema_version",
399            CCP_SESSION_BUNDLE_V1_SCHEMA_VERSION,
400        ),
401        (
402            "leanctx.contract.knowledge_policy_v1.schema_version",
403            KNOWLEDGE_POLICY_V1_SCHEMA_VERSION,
404        ),
405        (
406            "leanctx.contract.graph_reproducibility_v1.schema_version",
407            GRAPH_REPRODUCIBILITY_V1_SCHEMA_VERSION,
408        ),
409        (
410            "leanctx.contract.a2a_snapshot_v1.schema_version",
411            A2A_SNAPSHOT_V1_SCHEMA_VERSION,
412        ),
413        (
414            "leanctx.contract.memory_boundary_v1.schema_version",
415            MEMORY_BOUNDARY_V1_SCHEMA_VERSION,
416        ),
417        (
418            "leanctx.contract.gotchas_reminders_v1.schema_version",
419            GOTCHAS_REMINDERS_V1_SCHEMA_VERSION,
420        ),
421        (
422            "leanctx.contract.provider_framework_v1.schema_version",
423            PROVIDER_FRAMEWORK_V1_SCHEMA_VERSION,
424        ),
425        (
426            "leanctx.contract.context_package_v1.schema_version",
427            CONTEXT_PACKAGE_V1_SCHEMA_VERSION,
428        ),
429        (
430            "leanctx.contract.context_package_v2.schema_version",
431            CONTEXT_PACKAGE_V2_SCHEMA_VERSION,
432        ),
433        (
434            "leanctx.contract.context_snapshot_v1.schema_version",
435            CONTEXT_SNAPSHOT_V1_SCHEMA_VERSION,
436        ),
437        (
438            "leanctx.contract.http_mcp.contract_version",
439            HTTP_MCP_CONTRACT_VERSION,
440        ),
441        (
442            "leanctx.contract.team_server.contract_version",
443            TEAM_SERVER_CONTRACT_VERSION,
444        ),
445        (
446            "leanctx.contract.capabilities.contract_version",
447            CAPABILITIES_CONTRACT_VERSION,
448        ),
449    ])
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    #[test]
457    fn contract_docs_have_unique_ids_and_files() {
458        let docs = contract_docs();
459        let mut ids: Vec<_> = docs.iter().map(|d| d.id).collect();
460        let mut files: Vec<_> = docs.iter().map(|d| d.doc_file).collect();
461        ids.sort_unstable();
462        files.sort_unstable();
463        let unique_ids: std::collections::BTreeSet<_> = ids.iter().collect();
464        let unique_files: std::collections::BTreeSet<_> = files.iter().collect();
465        assert_eq!(unique_ids.len(), docs.len(), "duplicate contract id");
466        assert_eq!(unique_files.len(), docs.len(), "duplicate doc file");
467    }
468
469    #[test]
470    fn frozen_set_covers_the_platform_promises() {
471        // The freeze (GL #394) is only meaningful if the externally consumed
472        // surfaces are actually in it. Removing one of these from `Frozen`
473        // is itself a breaking policy change.
474        let docs = contract_docs();
475        for id in [
476            "http-mcp",
477            "team-server",
478            "context-ir",
479            "local-free-invariant",
480            "oss-plane-separation",
481            "billing-plane",
482            "wasm-abi",
483            "delivery-manifest",
484            "deployment-rehearsal",
485            "release-key-rotation",
486        ] {
487            let entry = docs.iter().find(|d| d.id == id).expect("listed");
488            assert_eq!(
489                entry.status,
490                ContractStatus::Frozen,
491                "{id} must stay frozen"
492            );
493        }
494    }
495
496    #[test]
497    fn status_kv_matches_docs() {
498        let kv = status_kv();
499        assert_eq!(kv.len(), contract_docs().len());
500        assert_eq!(kv["http-mcp"], "frozen");
501        assert_eq!(kv["hosted-personal-index"], "experimental");
502        assert_eq!(kv["personal-cloud-encryption"], "experimental");
503        assert_eq!(kv["context-candidate-admission"], "experimental");
504        assert_eq!(kv["multi-agent-efficiency-benchmark"], "experimental");
505        assert_eq!(kv["test-deployment-evidence"], "experimental");
506    }
507
508    #[test]
509    fn doc_files_follow_versioned_naming() {
510        // v1→v2 rule: every doc file carries its version suffix so a breaking
511        // change lands as a NEW file instead of mutating the old one.
512        // Governance metadata (README, DEPRECATION) are exempt.
513        for d in contract_docs() {
514            if d.doc_file == "README.md" || d.doc_file == "DEPRECATION.md" {
515                continue;
516            }
517            assert!(
518                d.doc_file.ends_with(&format!("-v{}.md", d.version)),
519                "{} must end in -v{}.md",
520                d.doc_file,
521                d.version
522            );
523        }
524    }
525}