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(
414            "engine-provider-execution",
415            "engine-provider-execution-v1.md",
416            1,
417            Experimental,
418        ),
419        doc(
420            "engine-source-execution",
421            "engine-source-execution-v1.md",
422            1,
423            Experimental,
424        ),
425        doc(
426            "intelligence-runtime-install",
427            "intelligence-runtime-install-v1.md",
428            1,
429            Experimental,
430        ),
431        doc(
432            "reference-scheduler-estimates",
433            "reference-scheduler-estimates-v1.md",
434            1,
435            Experimental,
436        ),
437    ]
438}
439
440/// Contract-id → stability status, exported through `/v1/capabilities` so
441/// clients can verify compatibility before relying on a surface (GL #394).
442pub fn status_kv() -> BTreeMap<&'static str, &'static str> {
443    contract_docs()
444        .into_iter()
445        .map(|d| (d.id, d.status.as_str()))
446        .collect()
447}
448
449pub fn versions_kv() -> BTreeMap<&'static str, u32> {
450    BTreeMap::from([
451        (
452            "leanctx.contract.mcp_manifest.schema_version",
453            MCP_MANIFEST_SCHEMA_VERSION,
454        ),
455        (
456            "leanctx.contract.context_proof_v1.schema_version",
457            CONTEXT_PROOF_V1_SCHEMA_VERSION,
458        ),
459        (
460            "leanctx.contract.context_ir_v1.schema_version",
461            CONTEXT_IR_V1_SCHEMA_VERSION,
462        ),
463        (
464            "leanctx.contract.attention_layout_driver_v1.schema_version",
465            ATTENTION_LAYOUT_DRIVER_V1_SCHEMA_VERSION,
466        ),
467        (
468            "leanctx.contract.verification_observability_v1.schema_version",
469            VERIFICATION_OBSERVABILITY_V1_SCHEMA_VERSION,
470        ),
471        (
472            "leanctx.contract.handoff_ledger_v1.schema_version",
473            HANDOFF_LEDGER_V1_SCHEMA_VERSION,
474        ),
475        (
476            "leanctx.contract.handoff_transfer_bundle_v1.schema_version",
477            HANDOFF_TRANSFER_BUNDLE_V1_SCHEMA_VERSION,
478        ),
479        (
480            "leanctx.contract.ccp_session_bundle_v1.schema_version",
481            CCP_SESSION_BUNDLE_V1_SCHEMA_VERSION,
482        ),
483        (
484            "leanctx.contract.knowledge_policy_v1.schema_version",
485            KNOWLEDGE_POLICY_V1_SCHEMA_VERSION,
486        ),
487        (
488            "leanctx.contract.graph_reproducibility_v1.schema_version",
489            GRAPH_REPRODUCIBILITY_V1_SCHEMA_VERSION,
490        ),
491        (
492            "leanctx.contract.a2a_snapshot_v1.schema_version",
493            A2A_SNAPSHOT_V1_SCHEMA_VERSION,
494        ),
495        (
496            "leanctx.contract.memory_boundary_v1.schema_version",
497            MEMORY_BOUNDARY_V1_SCHEMA_VERSION,
498        ),
499        (
500            "leanctx.contract.gotchas_reminders_v1.schema_version",
501            GOTCHAS_REMINDERS_V1_SCHEMA_VERSION,
502        ),
503        (
504            "leanctx.contract.provider_framework_v1.schema_version",
505            PROVIDER_FRAMEWORK_V1_SCHEMA_VERSION,
506        ),
507        (
508            "leanctx.contract.context_package_v1.schema_version",
509            CONTEXT_PACKAGE_V1_SCHEMA_VERSION,
510        ),
511        (
512            "leanctx.contract.context_package_v2.schema_version",
513            CONTEXT_PACKAGE_V2_SCHEMA_VERSION,
514        ),
515        (
516            "leanctx.contract.context_snapshot_v1.schema_version",
517            CONTEXT_SNAPSHOT_V1_SCHEMA_VERSION,
518        ),
519        (
520            "leanctx.contract.http_mcp.contract_version",
521            HTTP_MCP_CONTRACT_VERSION,
522        ),
523        (
524            "leanctx.contract.team_server.contract_version",
525            TEAM_SERVER_CONTRACT_VERSION,
526        ),
527        (
528            "leanctx.contract.capabilities.contract_version",
529            CAPABILITIES_CONTRACT_VERSION,
530        ),
531        (
532            "leanctx.contract.team_seat_value_v1.schema_version",
533            TEAM_SEAT_VALUE_V1_SCHEMA_VERSION,
534        ),
535    ])
536}
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541
542    #[test]
543    fn contract_docs_have_unique_ids_and_files() {
544        let docs = contract_docs();
545        let mut ids: Vec<_> = docs.iter().map(|d| d.id).collect();
546        let mut files: Vec<_> = docs.iter().map(|d| d.doc_file).collect();
547        ids.sort_unstable();
548        files.sort_unstable();
549        let unique_ids: std::collections::BTreeSet<_> = ids.iter().collect();
550        let unique_files: std::collections::BTreeSet<_> = files.iter().collect();
551        assert_eq!(unique_ids.len(), docs.len(), "duplicate contract id");
552        assert_eq!(unique_files.len(), docs.len(), "duplicate doc file");
553    }
554
555    #[test]
556    fn frozen_set_covers_the_platform_promises() {
557        // The freeze (GL #394) is only meaningful if the externally consumed
558        // surfaces are actually in it. Removing one of these from `Frozen`
559        // is itself a breaking policy change.
560        let docs = contract_docs();
561        for id in [
562            "http-mcp",
563            "context-ir",
564            "local-free-invariant",
565            "oss-plane-separation",
566            "wasm-abi",
567            "delivery-manifest",
568            "deployment-rehearsal",
569            "release-key-rotation",
570        ] {
571            let entry = docs.iter().find(|d| d.id == id).expect("listed");
572            assert_eq!(
573                entry.status,
574                ContractStatus::Frozen,
575                "{id} must stay frozen"
576            );
577        }
578    }
579
580    #[test]
581    fn status_kv_matches_docs() {
582        let kv = status_kv();
583        assert_eq!(kv.len(), contract_docs().len());
584        assert_eq!(kv["http-mcp"], "frozen");
585        assert_eq!(kv["personal-cloud-encryption"], "experimental");
586        assert_eq!(kv["context-candidate-admission"], "experimental");
587        assert_eq!(kv["multi-agent-efficiency-benchmark"], "experimental");
588        assert_eq!(kv["test-deployment-evidence"], "experimental");
589    }
590
591    #[test]
592    fn doc_files_follow_versioned_naming() {
593        // v1→v2 rule: every doc file carries its version suffix so a breaking
594        // change lands as a NEW file instead of mutating the old one.
595        // Governance metadata (README, DEPRECATION) are exempt.
596        for d in contract_docs() {
597            if d.doc_file == "README.md"
598                || d.doc_file == "DEPRECATION.md"
599                || d.doc_file.starts_with("v4-")
600            {
601                continue;
602            }
603            assert!(
604                d.doc_file.ends_with(&format!("-v{}.md", d.version)),
605                "{} must end in -v{}.md",
606                d.doc_file,
607                d.version
608            );
609        }
610    }
611}