Skip to main content

lean_ctx/core/
contracts.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::collections::BTreeMap;
4
5// Machine-verified contract versions.
6pub const MCP_MANIFEST_SCHEMA_VERSION: u32 = 1;
7pub const CONTEXT_PROOF_V1_SCHEMA_VERSION: u32 = 1;
8pub const CONTEXT_IR_V1_SCHEMA_VERSION: u32 = 1;
9pub const ATTENTION_LAYOUT_DRIVER_V1_SCHEMA_VERSION: u32 = 1;
10pub const VERIFICATION_OBSERVABILITY_V1_SCHEMA_VERSION: u32 = 1;
11pub const HANDOFF_LEDGER_V1_SCHEMA_VERSION: u32 = 1;
12pub const HANDOFF_TRANSFER_BUNDLE_V1_SCHEMA_VERSION: u32 = 1;
13pub const CCP_SESSION_BUNDLE_V1_SCHEMA_VERSION: u32 = 1;
14pub const KNOWLEDGE_POLICY_V1_SCHEMA_VERSION: u32 = 1;
15pub const GRAPH_REPRODUCIBILITY_V1_SCHEMA_VERSION: u32 = 1;
16pub const A2A_SNAPSHOT_V1_SCHEMA_VERSION: u32 = 1;
17pub const MEMORY_BOUNDARY_V1_SCHEMA_VERSION: u32 = 1;
18pub const GOTCHAS_REMINDERS_V1_SCHEMA_VERSION: u32 = 1;
19pub const PROVIDER_FRAMEWORK_V1_SCHEMA_VERSION: u32 = 1;
20pub const CONTEXT_PACKAGE_V1_SCHEMA_VERSION: u32 = 1;
21pub const CONTEXT_PACKAGE_V2_SCHEMA_VERSION: u32 = 2;
22pub const CONTEXT_SNAPSHOT_V1_SCHEMA_VERSION: u32 = 1;
23pub const TEAM_SEAT_VALUE_V1_SCHEMA_VERSION: u32 = 1;
24
25pub const PACKAGE_EXTENSION: &str = "ctxpkg";
26pub const LEGACY_PACKAGE_EXTENSION: &str = "lctxpkg";
27pub const MAX_PACKAGE_FILE_BYTES: u64 = 10 * 1024 * 1024; // 10 MB
28
29pub fn is_package_file(path: &std::path::Path) -> bool {
30    path.extension()
31        .and_then(|e| e.to_str())
32        .is_some_and(|ext| ext == PACKAGE_EXTENSION || ext == LEGACY_PACKAGE_EXTENSION)
33}
34
35pub fn default_package_filename(name: &str, version: &str) -> String {
36    format!("{name}-{version}.{PACKAGE_EXTENSION}")
37}
38
39// Documentation-level contracts (do not have a schema field in payloads).
40pub const HTTP_MCP_CONTRACT_VERSION: u32 = 2;
41pub const TEAM_SERVER_CONTRACT_VERSION: u32 = 1;
42pub const CAPABILITIES_CONTRACT_VERSION: u32 = 1;
43
44/// Stability classification of a contract document (GL #394).
45///
46/// The classification is normative — `tests/contracts_frozen.rs` enforces it:
47/// * `Frozen` — the normative surface is immutable. Any change to the doc file
48///   fails CI; semantic evolution requires a new `-v2.md` file (the v1 file
49///   stays in place for existing integrations).
50/// * `Stable` — additive evolution allowed (new optional fields, new sections);
51///   breaking changes still require a version bump per CONTRACTS.md rules.
52/// * `Experimental` — may change or disappear without notice; not covered by
53///   the deprecation policy.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum ContractStatus {
56    Frozen,
57    Stable,
58    Experimental,
59}
60
61impl ContractStatus {
62    pub fn as_str(self) -> &'static str {
63        match self {
64            ContractStatus::Frozen => "frozen",
65            ContractStatus::Stable => "stable",
66            ContractStatus::Experimental => "experimental",
67        }
68    }
69}
70
71/// One contract document under `docs/contracts/`, classified for the
72/// stability matrix in CONTRACTS.md and the `/v1/capabilities` response.
73pub struct ContractDoc {
74    /// Short stable identifier (used in capabilities `contract_status`).
75    pub id: &'static str,
76    /// File name inside `docs/contracts/` (the normative artifact).
77    pub doc_file: &'static str,
78    pub version: u32,
79    pub status: ContractStatus,
80}
81
82/// The complete classified inventory of `docs/contracts/*.md` — the single
83/// source of truth for the stability matrix. `tests/contracts_frozen.rs`
84/// asserts that every file in the directory is listed here (no contract can
85/// stay unclassified) and that frozen docs never change.
86pub fn contract_docs() -> Vec<ContractDoc> {
87    use ContractStatus::{Experimental, Frozen, Stable};
88    let doc = |id, doc_file, version, status| ContractDoc {
89        id,
90        doc_file,
91        version,
92        status,
93    };
94    vec![
95        // ── Frozen: externally consumed platform/transport promises ────────
96        doc("http-mcp", "http-mcp-contract-v1.md", 1, Frozen),
97        doc("http-mcp-v2", "http-mcp-contract-v2.md", 2, Stable),
98        doc("context-ir", "context-ir-v1.md", 1, Frozen),
99        doc(
100            "local-free-invariant",
101            "local-free-invariant-v1.md",
102            1,
103            Frozen,
104        ),
105        // v2 = binding for 3.12 (LeanCTX | Hive): paid rights only through
106        // Hive, plan checks only in core/hive; v1 stays the record.
107        doc(
108            "local-free-invariant-v2",
109            "local-free-invariant-v2.md",
110            2,
111            Stable,
112        ),
113        doc(
114            "oss-plane-separation",
115            "oss-plane-separation-v1.md",
116            1,
117            Frozen,
118        ),
119        // v2 = Apache host + private commercial components (v4); v1 stays frozen.
120        doc(
121            "oss-plane-separation-v2",
122            "oss-plane-separation-v2.md",
123            2,
124            Stable,
125        ),
126        doc("wasm-abi", "wasm-abi-v1.md", 1, Frozen),
127        // Release promotion, offline rollback rehearsal and signing-key
128        // rotation are externally consumed fail-closed supply-chain formats.
129        // Their exact v1 semantics are immutable; evolution requires v2.
130        doc("delivery-manifest", "delivery-manifest-v1.md", 1, Frozen),
131        doc(
132            "deployment-rehearsal",
133            "deployment-rehearsal-v1.md",
134            1,
135            Frozen,
136        ),
137        doc(
138            "release-key-rotation",
139            "release-key-rotation-v1.md",
140            1,
141            Frozen,
142        ),
143        // ── Stable: additive evolution allowed ──────────────────────────────
144        // capabilities is additive BY DESIGN: its drift test binds the doc's
145        // key list to TOP_LEVEL_KEYS, so the doc grows with every new key —
146        // freezing the file would contradict its own contract.
147        doc("capabilities", "capabilities-contract-v1.md", 1, Stable),
148        // v2 = v1 + storageQuotaBytes/roiWebhookUrl (GL #387/#388); v1 stays frozen.
149        // v3 = v1 + sso_oidc entitlement (GL #460/#533); additive. Business merged into Team in v3.9.
150        doc("evidence-bundle", "evidence-bundle-v1.md", 1, Stable),
151        doc("a2a", "a2a-contract-v1.md", 1, Stable),
152        doc(
153            "attention-layout-driver",
154            "attention-layout-driver-v1.md",
155            1,
156            Stable,
157        ),
158        doc("conformance", "conformance-v1.md", 1, Stable),
159        doc(
160            "context-plan-projection-digest",
161            "context-plan-projection-digest-v1.md",
162            1,
163            Stable,
164        ),
165        doc(
166            "ocla-cache-validator",
167            "ocla-cache-validator-v1.md",
168            1,
169            Stable,
170        ),
171        doc(
172            "ocla-verifier-conformance",
173            "ocla-verifier-conformance-v1.md",
174            1,
175            Stable,
176        ),
177        doc("extension-trust", "extension-trust-v1.md", 1, Stable),
178        doc("extractors", "extractors-v1.md", 1, Stable),
179        doc("contracts-readme", "README.md", 1, Stable),
180        doc("deprecation-policy", "DEPRECATION.md", 1, Stable),
181        doc("audit-schedule", "audit-schedule-v1.md", 1, Stable),
182        doc("branch-protection", "branch-protection-v1.md", 1, Stable),
183        doc(
184            "certification-levels",
185            "certification-levels-v1.md",
186            1,
187            Stable,
188        ),
189        doc("release-integrity", "release-integrity-v1.md", 1, Stable),
190        doc("evidence-bundle-v2", "evidence-bundle-v2.md", 2, Stable),
191        doc(
192            "evidence-bundle-v2-verification",
193            "evidence-bundle-v2-verification-v1.md",
194            1,
195            Stable,
196        ),
197        doc(
198            "public-product-claims",
199            "public-product-claims-v1.md",
200            1,
201            Stable,
202        ),
203        doc(
204            "gotchas-reminders",
205            "gotchas-reminders-contract-v1.md",
206            1,
207            Stable,
208        ),
209        doc(
210            "graph-reproducibility",
211            "graph-reproducibility-contract-v1.md",
212            1,
213            Stable,
214        ),
215        doc(
216            "handoff-transfer-bundle",
217            "handoff-transfer-bundle-v1.md",
218            1,
219            Stable,
220        ),
221        doc(
222            "knowledge-policy",
223            "knowledge-policy-contract-v1.md",
224            1,
225            Stable,
226        ),
227        doc(
228            "memory-boundary",
229            "memory-boundary-contract-v1.md",
230            1,
231            Stable,
232        ),
233        doc("persona-spec", "persona-spec-v1.md", 1, Stable),
234        doc(
235            "provider-framework",
236            "provider-framework-contract-v1.md",
237            1,
238            Stable,
239        ),
240        doc("wrapped-permalink", "wrapped-permalink-v1.md", 1, Stable),
241        // Community addon manifest (#858): self-declared stable (v1); the format
242        // evolves additively (new optional fields), so Stable, not Frozen.
243        doc("addon-manifest", "addon-manifest-v1.md", 1, Stable),
244        // Additive signing profile; production adoption remains a separate gate.
245        doc(
246            "decision-record-signature",
247            "decision-record-signature-v1.md",
248            1,
249            Stable,
250        ),
251        // ── Experimental: may change without notice ─────────────────────────
252        // Integration/source-pin notes are not release or protocol-stability
253        // promises. Classify them explicitly; never promote a note to Frozen
254        // merely to satisfy the inventory check.
255        doc(
256            "team-context-source",
257            "team-context-v1.SOURCE-v1.md",
258            1,
259            Experimental,
260        ),
261        // W0/W1 token-intelligence foundations are locally verified but do not
262        // yet claim complete hotpath adoption or externally consumed stability.
263        doc(
264            "context-candidate-admission",
265            "context-candidate-admission-v1.md",
266            1,
267            Experimental,
268        ),
269        // Evidence tiers, eval verdicts, retention probes and the quality
270        // receipt; the receipt section is still being extended additively.
271        doc("context-quality", "context-quality-v1.md", 1, Experimental),
272        doc(
273            "multi-agent-efficiency-benchmark",
274            "multi-agent-efficiency-benchmark-v1.md",
275            1,
276            Experimental,
277        ),
278        // The committed artifact is conformance-only; external deployment use
279        // and governed attestor adoption remain deliberately unclaimed.
280        doc(
281            "test-deployment-evidence",
282            "test-deployment-evidence-v1.md",
283            1,
284            Experimental,
285        ),
286        // Local Rust adapter only; external callability, authenticated approval,
287        // and recovery orchestration are deliberately not claimed by v2.
288        doc(
289            "ocla-config-tuning",
290            "ocla-config-tuning-v2.md",
291            2,
292            Experimental,
293        ),
294        doc(
295            "personal-cloud-encryption",
296            "personal-cloud-encryption-v1.md",
297            1,
298            Experimental,
299        ),
300        // 2026-06 org/cloud-plane wave — fresh surfaces, not yet consumed by
301        // external integrations; promote to Stable deliberately, not by default.
302        doc(
303            "context-policy-packs",
304            "context-policy-packs-v1.md",
305            1,
306            Experimental,
307        ),
308        // Quality loop (GL #494): edit-failure feedback into mode selection.
309        doc("quality-loop", "quality-loop-v1.md", 1, Experimental),
310        // Edit metering (GL #1144): anchored-vs-str_replace efficiency channel.
311        doc("edit-metering", "edit-metering-v1.md", 1, Experimental),
312        // Context Time Machine (GL #1022/#1023): git-anchored, signed temporal
313        // snapshot format — fresh surface, evolving additively until stable.
314        doc(
315            "context-snapshot",
316            "context-snapshot-v1.md",
317            1,
318            Experimental,
319        ),
320        doc(
321            "team-invite-links",
322            "team-invite-links-v1.md",
323            1,
324            Experimental,
325        ),
326        doc("pillar-boundaries", "pillar-boundaries-v1.md", 1, Stable),
327        // Logical editor-session telemetry is an additive local-dashboard surface;
328        // keep it experimental until multiple integrations implement the lifecycle.
329        doc(
330            "logical-session-presence",
331            "logical-session-presence-v1.md",
332            1,
333            Experimental,
334        ),
335        doc(
336            "tokenizer-calibration",
337            "tokenizer-calibration-v1.md",
338            1,
339            Experimental,
340        ),
341        doc(
342            "engine-interface-compatibility",
343            "engine-interface-compatibility-v1.md",
344            1,
345            Stable,
346        ),
347        // P2 protocol artifact: a strict canonical surface with exact Rust and
348        // schema conformance tests; its hash is pinned in frozen-hashes.json.
349        // (`invocation-context-binding-v1` and `invocation-evidence-manifest-v1`
350        // were withdrawn on 2026-10-16: nothing ever produced or consumed them.)
351        doc("receipt-document", "receipt-document-v1.md", 1, Frozen),
352        doc(
353            "native-engine-context-proof",
354            "native-engine-context-proof-v1.md",
355            1,
356            Experimental,
357        ),
358        // ── Experimental: Context Gateway, unreleased ──────────────────────
359        // Canonical decision vocabulary (`lean_ctx_protocol::context_gateway`).
360        doc("context-gateway", "context-gateway-v1.md", 1, Experimental),
361        // v2: keyed receipt objects, additive to v1.
362        doc(
363            "context-gateway-v2",
364            "context-gateway-v2.md",
365            2,
366            Experimental,
367        ),
368        // Egress lifecycle of the proxy (admission → bind → dispatch →
369        // observation) and the Team context-outcome report: written with the
370        // Context Gateway work, unreleased, not yet consumed externally.
371        doc(
372            "egress-lifecycle",
373            "egress-lifecycle-v1.md",
374            1,
375            Experimental,
376        ),
377        doc(
378            "team-context-outcome",
379            "team-context-outcome-v1.md",
380            1,
381            Experimental,
382        ),
383        // ── Experimental: v4 Engine interface, unreleased ───────────────────
384        // Additive local operator surfaces that ship with the v4 Engine. They
385        // may still change before the release that publishes them; promote to
386        // Stable/Frozen then, never before.
387        doc(
388            "engine-context-plan",
389            "engine-context-plan-v1.md",
390            1,
391            Experimental,
392        ),
393        doc(
394            "engine-context-sources",
395            "engine-context-sources-v1.md",
396            1,
397            Experimental,
398        ),
399        // Presenter capability: additive to v1, first Engine release 3.11.0.
400        doc(
401            "engine-context-sources-v2",
402            "engine-context-sources-v2.md",
403            2,
404            Experimental,
405        ),
406        doc("engine-outcome", "engine-outcome-v1.md", 1, Experimental),
407        doc(
408            "engine-context-store",
409            "engine-context-store-v1.md",
410            1,
411            Experimental,
412        ),
413        doc("local-read", "local-read-v1.md", 1, Experimental),
414        doc("client-calls", "client-calls-v1.md", 1, Experimental),
415        doc("local-service", "local-service-v1.md", 1, Experimental),
416        doc(
417            "engine-provider-execution",
418            "engine-provider-execution-v1.md",
419            1,
420            Experimental,
421        ),
422        doc(
423            "engine-source-execution",
424            "engine-source-execution-v1.md",
425            1,
426            Experimental,
427        ),
428        doc(
429            "intelligence-runtime-install",
430            "intelligence-runtime-install-v1.md",
431            1,
432            Experimental,
433        ),
434        doc(
435            "reference-scheduler-estimates",
436            "reference-scheduler-estimates-v1.md",
437            1,
438            Experimental,
439        ),
440        doc("change-envelope", "change-envelope-v1.md", 1, Experimental),
441        doc(
442            "change-attestation",
443            "change-attestation-v1.md",
444            1,
445            Experimental,
446        ),
447        doc("managed-config", "managed-config-v1.md", 1, Experimental),
448    ]
449}
450
451/// Contract-id → stability status, exported through `/v1/capabilities` so
452/// clients can verify compatibility before relying on a surface (GL #394).
453pub fn status_kv() -> BTreeMap<&'static str, &'static str> {
454    contract_docs()
455        .into_iter()
456        .map(|d| (d.id, d.status.as_str()))
457        .collect()
458}
459
460pub fn versions_kv() -> BTreeMap<&'static str, u32> {
461    BTreeMap::from([
462        (
463            "leanctx.contract.mcp_manifest.schema_version",
464            MCP_MANIFEST_SCHEMA_VERSION,
465        ),
466        (
467            "leanctx.contract.context_proof_v1.schema_version",
468            CONTEXT_PROOF_V1_SCHEMA_VERSION,
469        ),
470        (
471            "leanctx.contract.context_ir_v1.schema_version",
472            CONTEXT_IR_V1_SCHEMA_VERSION,
473        ),
474        (
475            "leanctx.contract.attention_layout_driver_v1.schema_version",
476            ATTENTION_LAYOUT_DRIVER_V1_SCHEMA_VERSION,
477        ),
478        (
479            "leanctx.contract.verification_observability_v1.schema_version",
480            VERIFICATION_OBSERVABILITY_V1_SCHEMA_VERSION,
481        ),
482        (
483            "leanctx.contract.handoff_ledger_v1.schema_version",
484            HANDOFF_LEDGER_V1_SCHEMA_VERSION,
485        ),
486        (
487            "leanctx.contract.handoff_transfer_bundle_v1.schema_version",
488            HANDOFF_TRANSFER_BUNDLE_V1_SCHEMA_VERSION,
489        ),
490        (
491            "leanctx.contract.ccp_session_bundle_v1.schema_version",
492            CCP_SESSION_BUNDLE_V1_SCHEMA_VERSION,
493        ),
494        (
495            "leanctx.contract.knowledge_policy_v1.schema_version",
496            KNOWLEDGE_POLICY_V1_SCHEMA_VERSION,
497        ),
498        (
499            "leanctx.contract.graph_reproducibility_v1.schema_version",
500            GRAPH_REPRODUCIBILITY_V1_SCHEMA_VERSION,
501        ),
502        (
503            "leanctx.contract.a2a_snapshot_v1.schema_version",
504            A2A_SNAPSHOT_V1_SCHEMA_VERSION,
505        ),
506        (
507            "leanctx.contract.memory_boundary_v1.schema_version",
508            MEMORY_BOUNDARY_V1_SCHEMA_VERSION,
509        ),
510        (
511            "leanctx.contract.gotchas_reminders_v1.schema_version",
512            GOTCHAS_REMINDERS_V1_SCHEMA_VERSION,
513        ),
514        (
515            "leanctx.contract.provider_framework_v1.schema_version",
516            PROVIDER_FRAMEWORK_V1_SCHEMA_VERSION,
517        ),
518        (
519            "leanctx.contract.context_package_v1.schema_version",
520            CONTEXT_PACKAGE_V1_SCHEMA_VERSION,
521        ),
522        (
523            "leanctx.contract.context_package_v2.schema_version",
524            CONTEXT_PACKAGE_V2_SCHEMA_VERSION,
525        ),
526        (
527            "leanctx.contract.context_snapshot_v1.schema_version",
528            CONTEXT_SNAPSHOT_V1_SCHEMA_VERSION,
529        ),
530        (
531            "leanctx.contract.http_mcp.contract_version",
532            HTTP_MCP_CONTRACT_VERSION,
533        ),
534        (
535            "leanctx.contract.team_server.contract_version",
536            TEAM_SERVER_CONTRACT_VERSION,
537        ),
538        (
539            "leanctx.contract.capabilities.contract_version",
540            CAPABILITIES_CONTRACT_VERSION,
541        ),
542        (
543            "leanctx.contract.team_seat_value_v1.schema_version",
544            TEAM_SEAT_VALUE_V1_SCHEMA_VERSION,
545        ),
546    ])
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552
553    #[test]
554    fn contract_docs_have_unique_ids_and_files() {
555        let docs = contract_docs();
556        let mut ids: Vec<_> = docs.iter().map(|d| d.id).collect();
557        let mut files: Vec<_> = docs.iter().map(|d| d.doc_file).collect();
558        ids.sort_unstable();
559        files.sort_unstable();
560        let unique_ids: std::collections::BTreeSet<_> = ids.iter().collect();
561        let unique_files: std::collections::BTreeSet<_> = files.iter().collect();
562        assert_eq!(unique_ids.len(), docs.len(), "duplicate contract id");
563        assert_eq!(unique_files.len(), docs.len(), "duplicate doc file");
564    }
565
566    #[test]
567    fn frozen_set_covers_the_platform_promises() {
568        // The freeze (GL #394) is only meaningful if the externally consumed
569        // surfaces are actually in it. Removing one of these from `Frozen`
570        // is itself a breaking policy change.
571        let docs = contract_docs();
572        for id in [
573            "http-mcp",
574            "context-ir",
575            "local-free-invariant",
576            "oss-plane-separation",
577            "wasm-abi",
578            "delivery-manifest",
579            "deployment-rehearsal",
580            "release-key-rotation",
581        ] {
582            let entry = docs.iter().find(|d| d.id == id).expect("listed");
583            assert_eq!(
584                entry.status,
585                ContractStatus::Frozen,
586                "{id} must stay frozen"
587            );
588        }
589    }
590
591    #[test]
592    fn status_kv_matches_docs() {
593        let kv = status_kv();
594        assert_eq!(kv.len(), contract_docs().len());
595        assert_eq!(kv["http-mcp"], "frozen");
596        assert_eq!(kv["personal-cloud-encryption"], "experimental");
597        assert_eq!(kv["context-candidate-admission"], "experimental");
598        assert_eq!(kv["multi-agent-efficiency-benchmark"], "experimental");
599        assert_eq!(kv["test-deployment-evidence"], "experimental");
600    }
601
602    #[test]
603    fn doc_files_follow_versioned_naming() {
604        // v1→v2 rule: every doc file carries its version suffix so a breaking
605        // change lands as a NEW file instead of mutating the old one.
606        // Governance metadata (README, DEPRECATION) are exempt.
607        for d in contract_docs() {
608            if d.doc_file == "README.md"
609                || d.doc_file == "DEPRECATION.md"
610                || d.doc_file.starts_with("v4-")
611            {
612                continue;
613            }
614            assert!(
615                d.doc_file.ends_with(&format!("-v{}.md", d.version)),
616                "{} must end in -v{}.md",
617                d.doc_file,
618                d.version
619            );
620        }
621    }
622}