Skip to main content

whipplescript_core/
lib.rs

1//! Shared types for the WhippleScript rule-machine runtime.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5pub mod json;
6
7/// Implementation-stage label shown alongside the version (for project tracking;
8/// does not replace the semantic version).
9pub const IMPLEMENTATION_STAGE: &str = "release";
10
11/// Returns the workspace package version.
12pub fn version() -> &'static str {
13    env!("CARGO_PKG_VERSION")
14}
15
16/// Compile-time library and effect-contract registry.
17///
18/// This is the concrete data boundary between source-level package/library
19/// meaning and runtime provider execution. It is intentionally plain data:
20/// compiler and CLI surfaces can expose it without giving package code any
21/// parser or runtime authority.
22#[derive(Clone, Debug, Default, Eq, PartialEq)]
23pub struct ContractRegistry {
24    pub libraries: Vec<LibraryRegistration>,
25    pub constructs: Vec<ConstructRegistration>,
26    pub effect_contracts: Vec<EffectContract>,
27}
28
29#[derive(Clone, Debug, Eq, PartialEq)]
30pub struct LibraryRegistration {
31    pub id: String,
32    pub version: String,
33    pub standard: bool,
34}
35
36#[derive(Clone, Debug, Eq, PartialEq)]
37pub struct ConstructRegistration {
38    pub id: String,
39    pub library_id: String,
40    pub version: String,
41    pub construct_family: String,
42    pub keyword: String,
43    pub scope: String,
44    /// The DR-0011 grammar object, when the manifest declares one
45    /// (spec/construct-grammar.md "Two-Shape Meta-Grammar"). `fields` is then
46    /// derived from it (`ConstructGrammar::derive_fields`) rather than read
47    /// from the manifest. `None` for constructs registered without a grammar
48    /// (legacy flat `fields[]` manifests, artifact round-trips).
49    pub grammar: Option<ConstructGrammar>,
50    pub fields: Vec<ConstructField>,
51    pub requires: Vec<ConstructInterface>,
52    pub provides: Vec<ConstructInterface>,
53    pub lowering_target: String,
54    pub target_capability: Option<String>,
55}
56
57#[derive(Clone, Debug, Eq, PartialEq)]
58pub struct ConstructField {
59    pub name: String,
60    pub kind: String,
61    pub required: bool,
62}
63
64/// A DR-0011 grammar object: the single source of the construct's parse shape.
65/// Kept as plain validated strings, matching the rest of the registration data.
66/// Two shapes are manifest-expressible: `effect_operation`
67/// (`CONSTRUCT_GRAMMAR_SHAPE_EFFECT_OPERATION`, `<keyword> [<connective>
68/// <slot>]* [{ payload }]? as <binding>`) and `declaration_block`
69/// (`CONSTRUCT_GRAMMAR_SHAPE_DECLARATION_BLOCK`, an order-free block of named
70/// clauses). The `shape` string discriminates: `clauses` is `Some` exactly for
71/// `declaration_block`, and `slots`/`payload`/`binding`/`target_capability`
72/// carry the `effect_operation` shape (empty/`None`/`"none"`/empty otherwise).
73#[derive(Clone, Debug, Eq, PartialEq)]
74pub struct ConstructGrammar {
75    pub shape: String,
76    pub keyword: String,
77    pub slots: Vec<ConstructGrammarSlot>,
78    /// `None` = no payload block; `Some(fields)` = a `{ ... }` block of named
79    /// expression fields.
80    pub payload: Option<Vec<ConstructGrammarPayloadField>>,
81    /// `required` | `optional` | `none` — the trailing `as <binding>` policy.
82    pub binding: String,
83    pub target_capability: String,
84    /// `Some(clauses)` for a `declaration_block` shape; `None` for
85    /// `effect_operation` (the `shape` string discriminates — the design note
86    /// picked `Option` over a shape enum because `shape` already carries the
87    /// discriminant, for the smaller diff).
88    pub clauses: Option<Vec<ConstructGrammarClause>>,
89}
90
91/// One ordered grammar slot: a named value (`identifier` | `expression`),
92/// optionally introduced by a fixed connective word from
93/// `CONSTRUCT_GRAMMAR_CONNECTIVES`.
94#[derive(Clone, Debug, Eq, PartialEq)]
95pub struct ConstructGrammarSlot {
96    pub name: String,
97    pub kind: String,
98    pub connective: Option<String>,
99}
100
101/// One field inside the optional payload block: a named expression, required
102/// or not.
103#[derive(Clone, Debug, Eq, PartialEq)]
104pub struct ConstructGrammarPayloadField {
105    pub name: String,
106    pub kind: String,
107    pub required: bool,
108}
109
110/// One clause of a `declaration_block` grammar: a named value (the `name` may
111/// be multi-word), a `kind` from `CONSTRUCT_GRAMMAR_CLAUSE_KINDS`, whether it
112/// is `required`, whether it takes a `list` of values, and an optional
113/// introducing connective from `CONSTRUCT_GRAMMAR_CLAUSE_CONNECTIVES`. A `flag`
114/// clause carries no value: it is never a `list` and never has a `connective`
115/// (DR-0011 amendment, mirrored in `build.rs`).
116#[derive(Clone, Debug, Eq, PartialEq)]
117pub struct ConstructGrammarClause {
118    pub name: String,
119    pub kind: String,
120    pub required: bool,
121    pub list: bool,
122    pub connective: Option<String>,
123}
124
125pub const CONSTRUCT_GRAMMAR_SHAPE_EFFECT_OPERATION: &str = "effect_operation";
126pub const CONSTRUCT_GRAMMAR_SHAPE_DECLARATION_BLOCK: &str = "declaration_block";
127pub const CONSTRUCT_GRAMMAR_CONNECTIVES: &[&str] = &["from", "for", "into", "to", "via"];
128pub const CONSTRUCT_GRAMMAR_SLOT_KINDS: &[&str] = &["identifier", "expression"];
129pub const CONSTRUCT_GRAMMAR_BINDING_MODES: &[&str] = &["required", "optional", "none"];
130/// `declaration_block` clause value kinds (DR-0011 Shape 1, mirrors
131/// `build.rs`'s `CLAUSE_KINDS`).
132pub const CONSTRUCT_GRAMMAR_CLAUSE_KINDS: &[&str] = &[
133    "identifier",
134    "expression",
135    "duration",
136    "glob",
137    "schema",
138    "scalar",
139    "flag",
140];
141/// `declaration_block` clause connectives: the Shape 2 slot connectives plus
142/// `by` (ledger `partition by`). Mirrors `build.rs`'s `CLAUSE_CONNECTIVES`.
143pub const CONSTRUCT_GRAMMAR_CLAUSE_CONNECTIVES: &[&str] =
144    &["from", "for", "into", "to", "via", "by"];
145
146/// The DR-0015 agent feature-class taxonomy, verbatim (spec/std-agent.md
147/// "Capability reports"). Shared vocabulary: the parser checks
148/// `requires [<feature.class>]` membership against it (slice 6), and the
149/// kernel's compiled provider feature reports may only state these classes
150/// (slice 5) — one list, no drift.
151pub const AGENT_FEATURE_CLASS_TAXONOMY: &[&str] = &[
152    "context.compact",
153    "context.auto_compact",
154    "session.resume",
155    "session.fork",
156    "session.clone",
157    "session.export",
158    "turn.cancel",
159    "turn.steer",
160    "turn.follow_up",
161    "subagent.spawn",
162    "subagent.observe",
163    "subagent.steer",
164    "skill.attach",
165    "plugin.load",
166    "hook.lifecycle",
167    "native.command.dispatch",
168    "permission.policy",
169    "model.select",
170    "reasoning.select",
171    "goal.track",
172    "command.list",
173    "feature.report",
174];
175
176impl ConstructGrammar {
177    /// Derive the flat `fields[]` view downstream consumers read.
178    ///
179    /// For a `declaration_block` shape, each clause becomes one field: a `flag`
180    /// clause maps to an optional `boolean` field (a flag carries no value, so
181    /// it is never required); a `list` clause flattens into the `list` field
182    /// kind (carrying the clause's own required flag); any other clause maps its
183    /// value kind through `field_kind_for_clause_kind` with the clause's
184    /// required flag. A declaration_block has no trailing `as <binding>`, so no
185    /// binding field is appended.
186    ///
187    /// For an `effect_operation` shape: the ordered slots (always required),
188    /// then the payload fields with their own required flags, then — unless the
189    /// binding mode is `none` — the trailing binding as an identifier field
190    /// named `binding` (required when the mode is `required`).
191    pub fn derive_fields(&self) -> Vec<ConstructField> {
192        if let Some(clauses) = &self.clauses {
193            return clauses
194                .iter()
195                .map(|clause| {
196                    let (kind, required) = if clause.kind == "flag" {
197                        ("boolean".to_owned(), false)
198                    } else if clause.list {
199                        ("list".to_owned(), clause.required)
200                    } else {
201                        (
202                            field_kind_for_clause_kind(&clause.kind).to_owned(),
203                            clause.required,
204                        )
205                    };
206                    ConstructField {
207                        name: clause.name.clone(),
208                        kind,
209                        required,
210                    }
211                })
212                .collect();
213        }
214        let mut fields = Vec::new();
215        for slot in &self.slots {
216            fields.push(ConstructField {
217                name: slot.name.clone(),
218                kind: slot.kind.clone(),
219                required: true,
220            });
221        }
222        for field in self.payload.iter().flatten() {
223            fields.push(ConstructField {
224                name: field.name.clone(),
225                kind: field.kind.clone(),
226                required: field.required,
227            });
228        }
229        if self.binding != "none" {
230            fields.push(ConstructField {
231                name: "binding".to_owned(),
232                kind: "identifier".to_owned(),
233                required: self.binding == "required",
234            });
235        }
236        fields
237    }
238}
239
240/// Map a `declaration_block` clause value kind (`CONSTRUCT_GRAMMAR_CLAUSE_KINDS`
241/// minus `flag`, and excluding the list case which flattens to `list`) onto the
242/// platform `ConstructField` kind vocabulary (`PLATFORM_CONSTRUCT_CATALOG
243/// .field_kinds`): a `glob`/`scalar` is a `string`, a `schema` is a `type_ref`;
244/// `identifier`/`expression`/`duration` pass through. An unknown kind passes
245/// through unchanged so the field-kind validator reports it rather than this
246/// mapping silently coercing it.
247fn field_kind_for_clause_kind(kind: &str) -> &str {
248    match kind {
249        "glob" | "scalar" => "string",
250        "schema" => "type_ref",
251        other => other,
252    }
253}
254
255#[derive(Clone, Debug, Eq, PartialEq)]
256pub struct ConstructInterface {
257    pub kind: String,
258    pub name: Option<String>,
259    pub type_ref: Option<String>,
260    pub phase: String,
261    pub cardinality: String,
262}
263
264pub const CORE_CAPABILITY_CALL_CONSTRUCT_ID: &str = "core.capability.call";
265pub const CONSTRUCT_FAMILY_DECLARATION_BLOCK: &str = "declaration_block";
266pub const CONSTRUCT_FAMILY_EFFECT_OPERATION: &str = "effect_operation";
267pub const CONSTRUCT_FAMILY_EFFECT_CONTRACT: &str = "effect_contract";
268pub const CONSTRUCT_FAMILY_SOURCE_DECLARATION: &str = "source_declaration";
269pub const CONSTRUCT_FAMILY_ASSERTION: &str = "assertion";
270pub const CONSTRUCT_FAMILY_RULE: &str = "rule";
271pub const CONSTRUCT_FAMILY_PROJECTION_READ: &str = "projection_read";
272pub const CONSTRUCT_LOWERING_METADATA: &str = "metadata";
273pub const CONSTRUCT_LOWERING_METADATA_ONLY: &str = "metadata_only";
274pub const CONSTRUCT_LOWERING_CAPABILITY_CALL: &str = "capability_call";
275pub const CONSTRUCT_LOWERING_TYPED_EFFECT_CALL: &str = "typed_effect_call";
276pub const CONSTRUCT_LOWERING_RESOURCE_EFFECT: &str = "resource_effect";
277pub const CONSTRUCT_LOWERING_CORE_EFFECT: &str = "core_effect";
278pub const CONSTRUCT_LOWERING_SIGNAL_EMIT: &str = "signal_emit";
279pub const CONSTRUCT_LOWERING_SIGNAL_SOURCE: &str = "signal_source";
280pub const CONSTRUCT_LOWERING_CLOCK_SOURCE: &str = "clock_source";
281pub const CONSTRUCT_LOWERING_SCHEDULE_EMITTER: &str = "schedule_emitter";
282pub const CONSTRUCT_LOWERING_RULE_TEMPLATE: &str = "rule_template";
283pub const CONSTRUCT_LOWERING_PROJECTION_VIEW: &str = "projection_view";
284pub const CONSTRUCT_LOWERING_ASSERTION_CHECK: &str = "assertion_check";
285pub const CONSTRUCT_SCOPE_RULE_BODY: &str = "rule_body";
286pub const CONSTRUCT_INTERFACE_CAPABILITY: &str = "Capability";
287pub const CONSTRUCT_INTERFACE_EFFECT_HANDLE: &str = "EffectHandle";
288pub const CONSTRUCT_INTERFACE_PHASE_COMPILE_RUNTIME: &str = "compile/runtime";
289pub const CONSTRUCT_INTERFACE_CARDINALITY_EXACTLY_ONE: &str = "exactly-one";
290
291pub const CONSTRUCT_STATIC_DETERMINISTIC: &str = "deterministic";
292pub const CONSTRUCT_STATIC_CONTRACT_PINNED: &str = "contract_pinned";
293pub const CONSTRUCT_STATIC_NO_RUNTIME_INPUTS: &str = "no_runtime_inputs";
294pub const CONSTRUCT_STATIC_NO_HIDDEN_AUTHORITY: &str = "no_hidden_authority";
295pub const CONSTRUCT_STATIC_NO_PACKAGE_SCHEDULER: &str = "no_package_scheduler";
296pub const CONSTRUCT_STATIC_NO_PACKAGE_LIFECYCLE: &str = "no_package_lifecycle";
297pub const CONSTRUCT_STATIC_NO_DIRECT_FACT_WRITE: &str = "no_direct_fact_write";
298pub const CONSTRUCT_STATIC_NO_DIRECT_RULE_FIRE: &str = "no_direct_rule_fire";
299
300pub const CONSTRUCT_PLATFORM_STATIC_GUARANTEES: &[&str] = &[
301    CONSTRUCT_STATIC_DETERMINISTIC,
302    CONSTRUCT_STATIC_CONTRACT_PINNED,
303    CONSTRUCT_STATIC_NO_RUNTIME_INPUTS,
304    CONSTRUCT_STATIC_NO_HIDDEN_AUTHORITY,
305    CONSTRUCT_STATIC_NO_PACKAGE_SCHEDULER,
306    CONSTRUCT_STATIC_NO_PACKAGE_LIFECYCLE,
307    CONSTRUCT_STATIC_NO_DIRECT_FACT_WRITE,
308    CONSTRUCT_STATIC_NO_DIRECT_RULE_FIRE,
309];
310
311#[derive(Clone, Copy, Debug, Eq, PartialEq)]
312pub enum ConstructTargetCapabilityPolicy {
313    Forbidden,
314    RequiredCapabilityCallContract,
315}
316
317impl ConstructTargetCapabilityPolicy {
318    pub fn as_str(self) -> &'static str {
319        match self {
320            Self::Forbidden => "forbidden",
321            Self::RequiredCapabilityCallContract => "required_capability_call_contract",
322        }
323    }
324}
325
326#[derive(Clone, Copy, Debug, Eq, PartialEq)]
327pub enum ConstructLoweringAuthorityProfile {
328    None,
329    CapabilityScoped,
330    EventAdmission,
331    ProjectionSource,
332}
333
334impl ConstructLoweringAuthorityProfile {
335    pub fn as_str(self) -> &'static str {
336        match self {
337            Self::None => "none",
338            Self::CapabilityScoped => "capability_scoped",
339            Self::EventAdmission => "event_admission",
340            Self::ProjectionSource => "projection_source",
341        }
342    }
343}
344
345/// The single diagnostic severity scale, aligned 1:1 with the LSP severities
346/// (spec/error-handling.md). This is the canonical set for every diagnostic-
347/// producing surface (check, lint, LSP, test). `note` is NOT a severity — it is
348/// related information attached to a diagnostic. Inbox-item / notification
349/// "severity" is a distinct concept and is not this type.
350#[derive(Clone, Copy, Debug, Eq, PartialEq)]
351pub enum Severity {
352    Error,
353    Warning,
354    Info,
355    Hint,
356}
357
358impl Severity {
359    /// Every severity, highest to lowest, for exhaustive iteration.
360    pub const ALL: [Severity; 4] = [Self::Error, Self::Warning, Self::Info, Self::Hint];
361
362    /// The stable wire/serialized token for this severity.
363    pub fn as_str(self) -> &'static str {
364        match self {
365            Self::Error => "error",
366            Self::Warning => "warning",
367            Self::Info => "info",
368            Self::Hint => "hint",
369        }
370    }
371
372    /// Parse a wire token back into a severity. Returns `None` for any token
373    /// outside the canonical set (e.g. the unrelated inbox `"normal"`).
374    pub fn from_wire(value: &str) -> Option<Severity> {
375        match value {
376            "error" => Some(Self::Error),
377            "warning" => Some(Self::Warning),
378            "info" => Some(Self::Info),
379            "hint" => Some(Self::Hint),
380            _ => None,
381        }
382    }
383
384    /// The Language Server Protocol `DiagnosticSeverity` number for this severity.
385    /// The canonical set aligns 1:1 with LSP (Error=1, Warning=2, Info=3, Hint=4),
386    /// so editor tooling can map a diagnostic severity without a lookup table.
387    pub fn lsp_code(self) -> i32 {
388        match self {
389            Self::Error => 1,
390            Self::Warning => 2,
391            Self::Info => 3,
392            Self::Hint => 4,
393        }
394    }
395}
396
397#[derive(Clone, Copy, Debug, Eq, PartialEq)]
398pub struct PlatformConstructFamily {
399    pub id: &'static str,
400    pub description: &'static str,
401}
402
403#[derive(Clone, Copy, Debug, Eq, PartialEq)]
404pub struct PlatformConstructLowering {
405    pub id: &'static str,
406    pub compatible_families: &'static [&'static str],
407    pub package_authorable: bool,
408    pub required_scope: Option<&'static str>,
409    pub target_capability: ConstructTargetCapabilityPolicy,
410    pub required_interfaces: &'static [&'static str],
411    pub provided_interfaces: &'static [&'static str],
412    pub lifecycle_profiles: &'static [&'static str],
413    pub authority_profile: ConstructLoweringAuthorityProfile,
414    pub static_guarantees: &'static [&'static str],
415}
416
417#[derive(Clone, Copy, Debug, Eq, PartialEq)]
418pub struct PlatformReservedKeywordPrivilege {
419    pub keyword: &'static str,
420    pub library_id: &'static str,
421    pub construct_family: &'static str,
422    pub scope: &'static str,
423    pub lowering_target: &'static str,
424}
425
426#[derive(Clone, Copy, Debug, Eq, PartialEq)]
427pub struct PlatformConstructCatalog {
428    pub families: &'static [PlatformConstructFamily],
429    pub lowerings: &'static [PlatformConstructLowering],
430    pub scopes: &'static [&'static str],
431    pub field_kinds: &'static [&'static str],
432    pub interface_kinds: &'static [&'static str],
433    pub interface_phases: &'static [&'static str],
434    pub interface_cardinalities: &'static [&'static str],
435    pub reserved_keywords: &'static [&'static str],
436    pub reserved_keyword_privileges: &'static [PlatformReservedKeywordPrivilege],
437}
438
439impl PlatformConstructCatalog {
440    pub fn family(&self, id: &str) -> Option<&'static PlatformConstructFamily> {
441        self.families.iter().find(|family| family.id == id)
442    }
443
444    pub fn lowering(&self, id: &str) -> Option<&'static PlatformConstructLowering> {
445        self.lowerings.iter().find(|lowering| lowering.id == id)
446    }
447
448    pub fn family_ids(&self) -> impl Iterator<Item = &'static str> + '_ {
449        self.families.iter().map(|family| family.id)
450    }
451
452    pub fn lowering_ids(&self) -> impl Iterator<Item = &'static str> + '_ {
453        self.lowerings.iter().map(|lowering| lowering.id)
454    }
455
456    pub fn lowerings_for_family<'a>(
457        &'a self,
458        family: &'a str,
459    ) -> impl Iterator<Item = &'a PlatformConstructLowering> + 'a {
460        self.lowerings
461            .iter()
462            .filter(move |lowering| lowering.compatible_families.contains(&family))
463    }
464
465    pub fn contains_scope(&self, scope: &str) -> bool {
466        self.scopes.contains(&scope)
467    }
468
469    pub fn contains_field_kind(&self, kind: &str) -> bool {
470        self.field_kinds.contains(&kind)
471    }
472
473    pub fn contains_interface_kind(&self, kind: &str) -> bool {
474        self.interface_kinds.contains(&kind)
475    }
476
477    pub fn contains_interface_phase(&self, phase: &str) -> bool {
478        self.interface_phases.contains(&phase)
479    }
480
481    pub fn contains_interface_cardinality(&self, cardinality: &str) -> bool {
482        self.interface_cardinalities.contains(&cardinality)
483    }
484
485    pub fn contains_reserved_keyword(&self, keyword: &str) -> bool {
486        self.reserved_keywords.contains(&keyword)
487    }
488
489    pub fn reserved_keyword_privilege(
490        &self,
491        library_id: &str,
492        keyword: &str,
493        construct_family: &str,
494        scope: &str,
495        lowering_target: &str,
496    ) -> Option<&'static PlatformReservedKeywordPrivilege> {
497        self.reserved_keyword_privileges.iter().find(|privilege| {
498            privilege.library_id == library_id
499                && privilege.keyword == keyword
500                && privilege.construct_family == construct_family
501                && privilege.scope == scope
502                && privilege.lowering_target == lowering_target
503        })
504    }
505}
506
507pub const PLATFORM_CONSTRUCT_CATALOG: PlatformConstructCatalog = PlatformConstructCatalog {
508    families: &[
509        PlatformConstructFamily {
510            id: CONSTRUCT_FAMILY_DECLARATION_BLOCK,
511            description: "package-declared block syntax that lowers to metadata",
512        },
513        PlatformConstructFamily {
514            id: CONSTRUCT_FAMILY_EFFECT_OPERATION,
515            description: "rule-body operation syntax that lowers to a core effect template",
516        },
517        PlatformConstructFamily {
518            id: CONSTRUCT_FAMILY_EFFECT_CONTRACT,
519            description: "package effect-contract metadata used by capability resolution",
520        },
521        PlatformConstructFamily {
522            id: CONSTRUCT_FAMILY_SOURCE_DECLARATION,
523            description: "top-level source blocks that lower to signal-source and clock-source admission templates",
524        },
525        PlatformConstructFamily {
526            id: CONSTRUCT_FAMILY_ASSERTION,
527            description: "assertions that lower to assertion checks",
528        },
529        PlatformConstructFamily {
530            id: CONSTRUCT_FAMILY_RULE,
531            description: "rules that lower to rule templates and fact writes",
532        },
533        PlatformConstructFamily {
534            id: CONSTRUCT_FAMILY_PROJECTION_READ,
535            description: "checker-owned projection reads used by rules and assertions",
536        },
537    ],
538    lowerings: &[
539        PlatformConstructLowering {
540            id: CONSTRUCT_LOWERING_METADATA,
541            compatible_families: &[
542                CONSTRUCT_FAMILY_EFFECT_CONTRACT,
543                CONSTRUCT_FAMILY_PROJECTION_READ,
544            ],
545            package_authorable: false,
546            required_scope: None,
547            target_capability: ConstructTargetCapabilityPolicy::Forbidden,
548            required_interfaces: &[],
549            provided_interfaces: &[],
550            lifecycle_profiles: &["none"],
551            authority_profile: ConstructLoweringAuthorityProfile::None,
552            static_guarantees: CONSTRUCT_PLATFORM_STATIC_GUARANTEES,
553        },
554        PlatformConstructLowering {
555            id: CONSTRUCT_LOWERING_METADATA_ONLY,
556            compatible_families: &[CONSTRUCT_FAMILY_DECLARATION_BLOCK],
557            package_authorable: true,
558            required_scope: None,
559            target_capability: ConstructTargetCapabilityPolicy::Forbidden,
560            required_interfaces: &[],
561            provided_interfaces: &[],
562            lifecycle_profiles: &["none"],
563            authority_profile: ConstructLoweringAuthorityProfile::None,
564            static_guarantees: CONSTRUCT_PLATFORM_STATIC_GUARANTEES,
565        },
566        PlatformConstructLowering {
567            id: CONSTRUCT_LOWERING_CAPABILITY_CALL,
568            compatible_families: &[CONSTRUCT_FAMILY_EFFECT_OPERATION],
569            package_authorable: true,
570            required_scope: Some(CONSTRUCT_SCOPE_RULE_BODY),
571            target_capability: ConstructTargetCapabilityPolicy::RequiredCapabilityCallContract,
572            required_interfaces: &[CONSTRUCT_INTERFACE_CAPABILITY],
573            provided_interfaces: &[CONSTRUCT_INTERFACE_EFFECT_HANDLE],
574            lifecycle_profiles: &["effect_graph", "typed_effect_graph"],
575            authority_profile: ConstructLoweringAuthorityProfile::CapabilityScoped,
576            static_guarantees: CONSTRUCT_PLATFORM_STATIC_GUARANTEES,
577        },
578        PlatformConstructLowering {
579            id: CONSTRUCT_LOWERING_TYPED_EFFECT_CALL,
580            compatible_families: &[CONSTRUCT_FAMILY_EFFECT_OPERATION],
581            // Promoted to package-authorable for `std.files` (DR-0019 / DR-0020
582            // chain): read/write/import/export lower through `typed_effect_call`
583            // (`requires Capability<…>` + typed output, `target_capability`
584            // Forbidden — distinct from `capability_call`).
585            package_authorable: true,
586            required_scope: Some(CONSTRUCT_SCOPE_RULE_BODY),
587            target_capability: ConstructTargetCapabilityPolicy::Forbidden,
588            required_interfaces: &[CONSTRUCT_INTERFACE_CAPABILITY],
589            provided_interfaces: &[CONSTRUCT_INTERFACE_EFFECT_HANDLE],
590            lifecycle_profiles: &["typed_effect_graph"],
591            authority_profile: ConstructLoweringAuthorityProfile::CapabilityScoped,
592            static_guarantees: CONSTRUCT_PLATFORM_STATIC_GUARANTEES,
593        },
594        PlatformConstructLowering {
595            id: CONSTRUCT_LOWERING_RESOURCE_EFFECT,
596            compatible_families: &[CONSTRUCT_FAMILY_EFFECT_OPERATION],
597            package_authorable: false,
598            required_scope: Some(CONSTRUCT_SCOPE_RULE_BODY),
599            target_capability: ConstructTargetCapabilityPolicy::Forbidden,
600            required_interfaces: &["Resource"],
601            provided_interfaces: &[CONSTRUCT_INTERFACE_EFFECT_HANDLE],
602            lifecycle_profiles: &["resource_effect_graph"],
603            authority_profile: ConstructLoweringAuthorityProfile::CapabilityScoped,
604            static_guarantees: CONSTRUCT_PLATFORM_STATIC_GUARANTEES,
605        },
606        PlatformConstructLowering {
607            id: CONSTRUCT_LOWERING_CORE_EFFECT,
608            compatible_families: &[CONSTRUCT_FAMILY_EFFECT_OPERATION],
609            package_authorable: false,
610            required_scope: Some(CONSTRUCT_SCOPE_RULE_BODY),
611            target_capability: ConstructTargetCapabilityPolicy::Forbidden,
612            required_interfaces: &[],
613            provided_interfaces: &[CONSTRUCT_INTERFACE_EFFECT_HANDLE],
614            lifecycle_profiles: &["effect_graph", "typed_effect_graph"],
615            authority_profile: ConstructLoweringAuthorityProfile::CapabilityScoped,
616            static_guarantees: CONSTRUCT_PLATFORM_STATIC_GUARANTEES,
617        },
618        PlatformConstructLowering {
619            id: CONSTRUCT_LOWERING_SIGNAL_EMIT,
620            compatible_families: &[CONSTRUCT_FAMILY_EFFECT_OPERATION],
621            package_authorable: false,
622            required_scope: Some(CONSTRUCT_SCOPE_RULE_BODY),
623            target_capability: ConstructTargetCapabilityPolicy::Forbidden,
624            required_interfaces: &["Event"],
625            provided_interfaces: &[],
626            lifecycle_profiles: &["event_record"],
627            authority_profile: ConstructLoweringAuthorityProfile::EventAdmission,
628            static_guarantees: CONSTRUCT_PLATFORM_STATIC_GUARANTEES,
629        },
630        PlatformConstructLowering {
631            id: CONSTRUCT_LOWERING_SIGNAL_SOURCE,
632            compatible_families: &[CONSTRUCT_FAMILY_SOURCE_DECLARATION],
633            package_authorable: false,
634            required_scope: None,
635            target_capability: ConstructTargetCapabilityPolicy::Forbidden,
636            required_interfaces: &[],
637            provided_interfaces: &[],
638            lifecycle_profiles: &["signal_source_template"],
639            authority_profile: ConstructLoweringAuthorityProfile::EventAdmission,
640            static_guarantees: CONSTRUCT_PLATFORM_STATIC_GUARANTEES,
641        },
642        PlatformConstructLowering {
643            id: CONSTRUCT_LOWERING_CLOCK_SOURCE,
644            compatible_families: &[CONSTRUCT_FAMILY_SOURCE_DECLARATION],
645            package_authorable: false,
646            required_scope: None,
647            target_capability: ConstructTargetCapabilityPolicy::Forbidden,
648            required_interfaces: &[],
649            provided_interfaces: &[],
650            lifecycle_profiles: &["clock_source_template"],
651            authority_profile: ConstructLoweringAuthorityProfile::EventAdmission,
652            static_guarantees: CONSTRUCT_PLATFORM_STATIC_GUARANTEES,
653        },
654        PlatformConstructLowering {
655            id: CONSTRUCT_LOWERING_SCHEDULE_EMITTER,
656            compatible_families: &[CONSTRUCT_FAMILY_EFFECT_OPERATION],
657            package_authorable: false,
658            required_scope: Some(CONSTRUCT_SCOPE_RULE_BODY),
659            target_capability: ConstructTargetCapabilityPolicy::Forbidden,
660            required_interfaces: &[],
661            provided_interfaces: &[CONSTRUCT_INTERFACE_EFFECT_HANDLE],
662            lifecycle_profiles: &["schedule_template"],
663            authority_profile: ConstructLoweringAuthorityProfile::EventAdmission,
664            static_guarantees: CONSTRUCT_PLATFORM_STATIC_GUARANTEES,
665        },
666        PlatformConstructLowering {
667            id: CONSTRUCT_LOWERING_RULE_TEMPLATE,
668            compatible_families: &[CONSTRUCT_FAMILY_RULE],
669            package_authorable: false,
670            required_scope: None,
671            target_capability: ConstructTargetCapabilityPolicy::Forbidden,
672            required_interfaces: &[],
673            provided_interfaces: &[],
674            lifecycle_profiles: &["rule_template"],
675            authority_profile: ConstructLoweringAuthorityProfile::None,
676            static_guarantees: CONSTRUCT_PLATFORM_STATIC_GUARANTEES,
677        },
678        PlatformConstructLowering {
679            id: CONSTRUCT_LOWERING_PROJECTION_VIEW,
680            compatible_families: &[CONSTRUCT_FAMILY_PROJECTION_READ],
681            package_authorable: false,
682            required_scope: None,
683            target_capability: ConstructTargetCapabilityPolicy::Forbidden,
684            required_interfaces: &["Projection"],
685            provided_interfaces: &[],
686            lifecycle_profiles: &["event_projection"],
687            authority_profile: ConstructLoweringAuthorityProfile::ProjectionSource,
688            static_guarantees: CONSTRUCT_PLATFORM_STATIC_GUARANTEES,
689        },
690        PlatformConstructLowering {
691            id: CONSTRUCT_LOWERING_ASSERTION_CHECK,
692            compatible_families: &[CONSTRUCT_FAMILY_ASSERTION],
693            package_authorable: false,
694            required_scope: None,
695            target_capability: ConstructTargetCapabilityPolicy::Forbidden,
696            required_interfaces: &[],
697            provided_interfaces: &[],
698            lifecycle_profiles: &["assertion_check"],
699            authority_profile: ConstructLoweringAuthorityProfile::None,
700            static_guarantees: CONSTRUCT_PLATFORM_STATIC_GUARANTEES,
701        },
702    ],
703    scopes: &[
704        "top_level",
705        CONSTRUCT_SCOPE_RULE_BODY,
706        "workflow_body",
707        "expression",
708    ],
709    field_kinds: &[
710        "identifier",
711        "string",
712        "number",
713        "boolean",
714        "duration",
715        "type_ref",
716        "provider_ref",
717        "capability_ref",
718        "event_ref",
719        "effect_ref",
720        "expression",
721        "predicate",
722        "list",
723        "record",
724        "enum",
725    ],
726    interface_kinds: &[
727        "Resource",
728        "Projection",
729        "Event",
730        "SignalSource",
731        "EffectContract",
732        "Operation",
733        CONSTRUCT_INTERFACE_CAPABILITY,
734        "ProviderKind",
735        "Profile",
736        "Binding",
737        CONSTRUCT_INTERFACE_EFFECT_HANDLE,
738        "TerminalOutput",
739        "Value",
740        "ContextArtifact",
741        "Diagnostic",
742    ],
743    interface_phases: &[
744        "compile",
745        "runtime",
746        CONSTRUCT_INTERFACE_PHASE_COMPILE_RUNTIME,
747    ],
748    interface_cardinalities: &[
749        CONSTRUCT_INTERFACE_CARDINALITY_EXACTLY_ONE,
750        "optional-one",
751        "many",
752        "named-many",
753    ],
754    reserved_keywords: &[
755        // `acquire`/`append`/`consume` are reserved in the SAME change as their
756        // std.coord privilege tuples (spec/std-coord.md "Surface"): privilege
757        // rows for unreserved words are dead data, and the bare verbs would
758        // stay squattable by any future package construct path.
759        "acquire", "agent", "append", "ask", "call", "cancel", "case", "claim", "class", "coerce",
760        "complete", "consume", "counter", "decide", "effect", "else", "emit", "enum", "event",
761        "fail", "flow", "from", "harness", "if", "ledger", "lease", "let", "match", "release",
762        "renew", "rule", "tracker", "signal", "tell", "then", "use", "when", "workflow",
763    ],
764    reserved_keyword_privileges: &[
765        PlatformReservedKeywordPrivilege {
766            keyword: "claim",
767            library_id: "std.tracker",
768            construct_family: CONSTRUCT_FAMILY_EFFECT_OPERATION,
769            scope: CONSTRUCT_SCOPE_RULE_BODY,
770            lowering_target: CONSTRUCT_LOWERING_TYPED_EFFECT_CALL,
771        },
772        PlatformReservedKeywordPrivilege {
773            keyword: "renew",
774            library_id: "std.tracker",
775            construct_family: CONSTRUCT_FAMILY_EFFECT_OPERATION,
776            scope: CONSTRUCT_SCOPE_RULE_BODY,
777            lowering_target: CONSTRUCT_LOWERING_TYPED_EFFECT_CALL,
778        },
779        PlatformReservedKeywordPrivilege {
780            keyword: "release",
781            library_id: "std.tracker",
782            construct_family: CONSTRUCT_FAMILY_EFFECT_OPERATION,
783            scope: CONSTRUCT_SCOPE_RULE_BODY,
784            lowering_target: CONSTRUCT_LOWERING_TYPED_EFFECT_CALL,
785        },
786        // The migrating declaration-family keywords (DR-0011 decl-migration):
787        // each is a platform-reserved word whose top-level `declaration_block`
788        // construct the platform's own std grammar library is authorized to
789        // provide. These lower to `metadata_only` (no capability, no runtime
790        // authority), so the authorization is purely syntactic.
791        PlatformReservedKeywordPrivilege {
792            keyword: "tracker",
793            library_id: "std.tracker",
794            construct_family: CONSTRUCT_FAMILY_DECLARATION_BLOCK,
795            scope: "top_level",
796            lowering_target: CONSTRUCT_LOWERING_METADATA_ONLY,
797        },
798        PlatformReservedKeywordPrivilege {
799            keyword: "counter",
800            library_id: "std.coord",
801            construct_family: CONSTRUCT_FAMILY_DECLARATION_BLOCK,
802            scope: "top_level",
803            lowering_target: CONSTRUCT_LOWERING_METADATA_ONLY,
804        },
805        PlatformReservedKeywordPrivilege {
806            keyword: "lease",
807            library_id: "std.coord",
808            construct_family: CONSTRUCT_FAMILY_DECLARATION_BLOCK,
809            scope: "top_level",
810            lowering_target: CONSTRUCT_LOWERING_METADATA_ONLY,
811        },
812        PlatformReservedKeywordPrivilege {
813            keyword: "ledger",
814            library_id: "std.coord",
815            construct_family: CONSTRUCT_FAMILY_DECLARATION_BLOCK,
816            scope: "top_level",
817            lowering_target: CONSTRUCT_LOWERING_METADATA_ONLY,
818        },
819        // std.coord effect operations (spec/std-coord.md "Surface", E4 first
820        // `resource_effect` producer): each tuple both clears the reserved
821        // keyword AND — because its lowering target is a non-authorable class —
822        // opens the authorability door for exactly this
823        // (library, keyword, family, scope, lowering) tuple (M5 "Authorability
824        // door"; models/maude/std-construct-authorization.maude
825        // [door-privileged]). The `release` row COEXISTS with std.tracker's:
826        // privilege rows are exact tuples, so the shared bare verb is two
827        // rows, not a collision.
828        PlatformReservedKeywordPrivilege {
829            keyword: "acquire",
830            library_id: "std.coord",
831            construct_family: CONSTRUCT_FAMILY_EFFECT_OPERATION,
832            scope: CONSTRUCT_SCOPE_RULE_BODY,
833            lowering_target: CONSTRUCT_LOWERING_RESOURCE_EFFECT,
834        },
835        PlatformReservedKeywordPrivilege {
836            keyword: "append",
837            library_id: "std.coord",
838            construct_family: CONSTRUCT_FAMILY_EFFECT_OPERATION,
839            scope: CONSTRUCT_SCOPE_RULE_BODY,
840            lowering_target: CONSTRUCT_LOWERING_RESOURCE_EFFECT,
841        },
842        PlatformReservedKeywordPrivilege {
843            keyword: "consume",
844            library_id: "std.coord",
845            construct_family: CONSTRUCT_FAMILY_EFFECT_OPERATION,
846            scope: CONSTRUCT_SCOPE_RULE_BODY,
847            lowering_target: CONSTRUCT_LOWERING_RESOURCE_EFFECT,
848        },
849        PlatformReservedKeywordPrivilege {
850            keyword: "release",
851            library_id: "std.coord",
852            construct_family: CONSTRUCT_FAMILY_EFFECT_OPERATION,
853            scope: CONSTRUCT_SCOPE_RULE_BODY,
854            lowering_target: CONSTRUCT_LOWERING_RESOURCE_EFFECT,
855        },
856        // std.ingress rows (spec/std-ingress.md "Catalog privilege additions"):
857        // `signal` and `emit` are reserved keywords, so the embedded std.ingress
858        // manifest's construct rows need exact privilege tuples to clear
859        // `reserved_keyword_privilege_error`. The `emit` tuple's lowering target
860        // (`signal_emit`) is a non-authorable class, so — like the std.coord
861        // `resource_effect` tuples above — it also opens the authorability door
862        // for exactly this tuple. `source` is not reserved and needs no tuple;
863        // its non-authorable `signal_source` lowering is admitted through the
864        // embedded-copy leg (`manifest_is_embedded_copy`). NOTE: the design
865        // sketch named lowering `metadata` for `signal`, but `metadata` is not
866        // compatible with `declaration_block` — the shipped decl-block lowering
867        // is `metadata_only` (the std.tracker/std.coord precedent).
868        PlatformReservedKeywordPrivilege {
869            keyword: "signal",
870            library_id: "std.ingress",
871            construct_family: CONSTRUCT_FAMILY_DECLARATION_BLOCK,
872            scope: "top_level",
873            lowering_target: CONSTRUCT_LOWERING_METADATA_ONLY,
874        },
875        PlatformReservedKeywordPrivilege {
876            keyword: "emit",
877            library_id: "std.ingress",
878            construct_family: CONSTRUCT_FAMILY_EFFECT_OPERATION,
879            scope: CONSTRUCT_SCOPE_RULE_BODY,
880            lowering_target: CONSTRUCT_LOWERING_SIGNAL_EMIT,
881        },
882    ],
883};
884
885#[derive(Clone, Debug, Eq, PartialEq)]
886pub struct EffectContract {
887    pub id: String,
888    pub library_id: String,
889    pub version: String,
890    pub effect_kind: String,
891    pub source_forms: Vec<String>,
892    pub input_schema: Option<String>,
893    pub output_schema: Option<String>,
894    pub required_capabilities: Vec<String>,
895    pub provider_kinds: Vec<String>,
896    pub projected_facts: Vec<String>,
897    pub validation: TypedOutputValidation,
898}
899
900#[derive(Clone, Copy, Debug, Eq, PartialEq)]
901pub enum TypedOutputValidation {
902    None,
903    RuntimeBoundary,
904}
905
906impl TypedOutputValidation {
907    pub fn as_str(self) -> &'static str {
908        match self {
909            Self::None => "none",
910            Self::RuntimeBoundary => "runtime_boundary",
911        }
912    }
913}
914
915#[derive(Clone, Debug, Eq, PartialEq)]
916pub struct ContractRegistryDiagnostic {
917    pub code: String,
918    pub message: String,
919}
920
921// --- Embedded standard-package manifest data (M5) -----------------------------
922//
923// std packages ship as data compiled into the binary rather than as scattered
924// per-package builtin functions. The `std_*` functions below are the reference
925// data for `std.messaging` (`send`): the shipped registration now comes from the
926// embedded `std/manifests/messaging.json` manifest (S6d-3), and a CLI guard
927// test asserts the manifest transcribes these functions field-for-field so the
928// two can never drift while both exist. The schema strings are the JSON-fragment
929// form the package-manifest validator accepts (named schema references are not
930// manifest-expressible).
931
932/// The `messaging.send` capability id — the target of the `send` construct and
933/// the id of its `capability.call` effect contract.
934pub const MESSAGING_SEND_CAPABILITY: &str = "messaging.send";
935
936/// `std.messaging` `send` construct registration (effect_operation → capability.call).
937pub fn std_messaging_send_construct() -> ConstructRegistration {
938    ConstructRegistration {
939        id: MESSAGING_SEND_CAPABILITY.to_owned(),
940        library_id: "std.messaging".to_owned(),
941        version: "0.1.0".to_owned(),
942        construct_family: CONSTRUCT_FAMILY_EFFECT_OPERATION.to_owned(),
943        keyword: "send".to_owned(),
944        scope: CONSTRUCT_SCOPE_RULE_BODY.to_owned(),
945        // The DR-0011 grammar: `send via <channel> { text <expr> [markdown
946        // <expr>] [thread_id <expr>] } as <binding>`. This is the reference
947        // value the embedded manifest's `grammar` object must transcribe.
948        grammar: Some(ConstructGrammar {
949            shape: CONSTRUCT_GRAMMAR_SHAPE_EFFECT_OPERATION.to_owned(),
950            keyword: "send".to_owned(),
951            slots: vec![ConstructGrammarSlot {
952                name: "channel".to_owned(),
953                kind: "identifier".to_owned(),
954                connective: Some("via".to_owned()),
955            }],
956            payload: Some(vec![
957                ConstructGrammarPayloadField {
958                    name: "text".to_owned(),
959                    kind: "expression".to_owned(),
960                    required: true,
961                },
962                ConstructGrammarPayloadField {
963                    name: "markdown".to_owned(),
964                    kind: "expression".to_owned(),
965                    required: false,
966                },
967                ConstructGrammarPayloadField {
968                    name: "thread_id".to_owned(),
969                    kind: "expression".to_owned(),
970                    required: false,
971                },
972            ]),
973            binding: "required".to_owned(),
974            target_capability: MESSAGING_SEND_CAPABILITY.to_owned(),
975            clauses: None,
976        }),
977        // The grammar-derived flat view (slots, payload fields, binding) —
978        // written out explicitly so the transcription guard compares two
979        // independent spellings.
980        fields: vec![
981            ConstructField {
982                name: "channel".to_owned(),
983                kind: "identifier".to_owned(),
984                required: true,
985            },
986            ConstructField {
987                name: "text".to_owned(),
988                kind: "expression".to_owned(),
989                required: true,
990            },
991            ConstructField {
992                name: "markdown".to_owned(),
993                kind: "expression".to_owned(),
994                required: false,
995            },
996            ConstructField {
997                name: "thread_id".to_owned(),
998                kind: "expression".to_owned(),
999                required: false,
1000            },
1001            ConstructField {
1002                name: "binding".to_owned(),
1003                kind: "identifier".to_owned(),
1004                required: true,
1005            },
1006        ],
1007        requires: vec![ConstructInterface {
1008            kind: CONSTRUCT_INTERFACE_CAPABILITY.to_owned(),
1009            name: Some(MESSAGING_SEND_CAPABILITY.to_owned()),
1010            type_ref: None,
1011            phase: CONSTRUCT_INTERFACE_PHASE_COMPILE_RUNTIME.to_owned(),
1012            cardinality: CONSTRUCT_INTERFACE_CARDINALITY_EXACTLY_ONE.to_owned(),
1013        }],
1014        // `capability_call` lowering requires a provided EffectHandle; the
1015        // type_ref names the built-in receipt class a `send … as r` binding sees.
1016        provides: vec![ConstructInterface {
1017            kind: CONSTRUCT_INTERFACE_EFFECT_HANDLE.to_owned(),
1018            name: None,
1019            type_ref: Some("MessageSendReceipt".to_owned()),
1020            phase: CONSTRUCT_INTERFACE_PHASE_COMPILE_RUNTIME.to_owned(),
1021            cardinality: CONSTRUCT_INTERFACE_CARDINALITY_EXACTLY_ONE.to_owned(),
1022        }],
1023        lowering_target: CONSTRUCT_LOWERING_CAPABILITY_CALL.to_owned(),
1024        target_capability: Some(MESSAGING_SEND_CAPABILITY.to_owned()),
1025    }
1026}
1027
1028/// `std.messaging` `messaging.send` `capability.call` effect contract — the
1029/// target the `send` construct lowers to.
1030pub fn std_messaging_send_effect_contract() -> EffectContract {
1031    EffectContract {
1032        id: MESSAGING_SEND_CAPABILITY.to_owned(),
1033        library_id: "std.messaging".to_owned(),
1034        version: "0.1.0".to_owned(),
1035        effect_kind: "capability.call".to_owned(),
1036        source_forms: vec!["send".to_owned()],
1037        // JSON-fragment schemas (the package-manifest-expressible form; keys
1038        // serialize sorted). The output fragment is the `MessageSendReceipt`
1039        // built-in class shape; the construct's provided EffectHandle carries
1040        // the class name.
1041        input_schema: Some(r#"{"channel":"string","text":"string"}"#.to_owned()),
1042        // The full `MessageSendReceipt` shape (spec/std-messaging.md): every
1043        // provider returns all eight fields; correlation fields the provider
1044        // cannot report are empty strings (the fragment validator has no
1045        // optional marker), `status` is `accepted` in v1, and failure is not
1046        // a receipt (it settles `capability.call.failed`).
1047        output_schema: Some(
1048            r#"{"accepted_at":"string","channel":"string","destination":"string","message_id":"string","provider":"string","provider_message_id":"string","status":"string","thread_id":"string"}"#.to_owned(),
1049        ),
1050        required_capabilities: vec![MESSAGING_SEND_CAPABILITY.to_owned()],
1051        provider_kinds: vec!["messaging".to_owned()],
1052        projected_facts: vec!["effect.output".to_owned()],
1053        validation: TypedOutputValidation::RuntimeBoundary,
1054    }
1055}
1056
1057impl ContractRegistry {
1058    pub fn merge(&mut self, other: ContractRegistry) {
1059        for library in other.libraries {
1060            self.upsert_library(library);
1061        }
1062        for form in other.constructs {
1063            self.upsert_construct(form);
1064        }
1065        for contract in other.effect_contracts {
1066            self.upsert_effect_contract(contract);
1067        }
1068        self.libraries.sort_by(|left, right| left.id.cmp(&right.id));
1069        self.constructs.sort_by(|left, right| {
1070            left.id
1071                .cmp(&right.id)
1072                .then_with(|| left.version.cmp(&right.version))
1073        });
1074        self.effect_contracts.sort_by(|left, right| {
1075            left.id
1076                .cmp(&right.id)
1077                .then_with(|| left.version.cmp(&right.version))
1078        });
1079    }
1080
1081    pub fn upsert_library(&mut self, library: LibraryRegistration) {
1082        if let Some(existing) = self
1083            .libraries
1084            .iter_mut()
1085            .find(|existing| existing.id == library.id)
1086        {
1087            if existing.version == "unlocked" && library.version != "unlocked" {
1088                *existing = library;
1089            } else if existing.version == library.version {
1090                existing.standard |= library.standard;
1091            } else if library.version != "unlocked" {
1092                self.libraries.push(library);
1093            }
1094            return;
1095        }
1096        self.libraries.push(library);
1097    }
1098
1099    pub fn upsert_construct(&mut self, form: ConstructRegistration) {
1100        if self.constructs.iter().any(|existing| {
1101            existing.id == form.id && existing.version == form.version && existing == &form
1102        }) {
1103            return;
1104        }
1105        self.constructs.push(form);
1106    }
1107
1108    pub fn upsert_effect_contract(&mut self, contract: EffectContract) {
1109        if let Some(existing) = self
1110            .effect_contracts
1111            .iter_mut()
1112            .find(|existing| existing.id == contract.id && existing.version == contract.version)
1113        {
1114            if existing.library_id == contract.library_id
1115                && existing.effect_kind == contract.effect_kind
1116                && existing.input_schema == contract.input_schema
1117                && existing.output_schema == contract.output_schema
1118                && existing.validation == contract.validation
1119            {
1120                merge_unique_list(&mut existing.source_forms, &contract.source_forms);
1121                merge_unique_list(
1122                    &mut existing.required_capabilities,
1123                    &contract.required_capabilities,
1124                );
1125                merge_unique_list(&mut existing.provider_kinds, &contract.provider_kinds);
1126                merge_unique_list(&mut existing.projected_facts, &contract.projected_facts);
1127            } else {
1128                self.effect_contracts.push(contract);
1129            }
1130            return;
1131        }
1132        self.effect_contracts.push(contract);
1133    }
1134
1135    pub fn validate(&self) -> Vec<ContractRegistryDiagnostic> {
1136        let mut diagnostics = Vec::new();
1137        let mut libraries = BTreeSet::new();
1138
1139        for library in &self.libraries {
1140            if library.id.trim().is_empty() {
1141                diagnostics.push(registry_diagnostic(
1142                    "library_id_empty",
1143                    "library registration has an empty id",
1144                ));
1145            }
1146            if library.version.trim().is_empty() {
1147                diagnostics.push(registry_diagnostic(
1148                    "library_version_empty",
1149                    format!("library `{}` has an empty version", library.id),
1150                ));
1151            }
1152            if !libraries.insert(library.id.clone()) {
1153                diagnostics.push(registry_diagnostic(
1154                    "library_duplicate",
1155                    format!("library `{}` is registered more than once", library.id),
1156                ));
1157            }
1158        }
1159
1160        let library_ids = self
1161            .libraries
1162            .iter()
1163            .map(|library| library.id.as_str())
1164            .collect::<BTreeSet<_>>();
1165        let mut constructs = BTreeSet::new();
1166        let mut construct_keywords = BTreeSet::new();
1167
1168        for form in &self.constructs {
1169            if form.id.trim().is_empty() {
1170                diagnostics.push(registry_diagnostic(
1171                    "construct_id_empty",
1172                    "construct has an empty id",
1173                ));
1174            }
1175            if form.version.trim().is_empty() {
1176                diagnostics.push(registry_diagnostic(
1177                    "construct_version_empty",
1178                    format!("construct `{}` has an empty version", form.id),
1179                ));
1180            }
1181            if form.keyword.trim().is_empty() {
1182                diagnostics.push(registry_diagnostic(
1183                    "construct_keyword_empty",
1184                    format!("construct `{}` has an empty keyword", form.id),
1185                ));
1186            }
1187            if form.construct_family.trim().is_empty() {
1188                diagnostics.push(registry_diagnostic(
1189                    "construct_family_empty",
1190                    format!("construct `{}` has an empty construct family", form.id),
1191                ));
1192            }
1193            if form.scope.trim().is_empty() {
1194                diagnostics.push(registry_diagnostic(
1195                    "construct_scope_empty",
1196                    format!("construct `{}` has an empty scope", form.id),
1197                ));
1198            }
1199            if form.lowering_target.trim().is_empty() {
1200                diagnostics.push(registry_diagnostic(
1201                    "construct_lowering_target_empty",
1202                    format!("construct `{}` has an empty lowering target", form.id),
1203                ));
1204            }
1205            if form
1206                .target_capability
1207                .as_deref()
1208                .is_some_and(|target| target.trim().is_empty())
1209            {
1210                diagnostics.push(registry_diagnostic(
1211                    "construct_target_capability_empty",
1212                    format!("construct `{}` has an empty target capability", form.id),
1213                ));
1214            }
1215            if !library_ids.contains(form.library_id.as_str()) {
1216                diagnostics.push(registry_diagnostic(
1217                    "construct_unknown_library",
1218                    format!(
1219                        "construct `{}` references unknown library `{}`",
1220                        form.id, form.library_id
1221                    ),
1222                ));
1223            }
1224            if !constructs.insert((form.id.clone(), form.version.clone())) {
1225                diagnostics.push(registry_diagnostic(
1226                    "construct_duplicate",
1227                    format!(
1228                        "construct `{}` version `{}` is registered more than once",
1229                        form.id, form.version
1230                    ),
1231                ));
1232            }
1233            if !construct_keywords.insert((form.scope.clone(), form.keyword.clone())) {
1234                diagnostics.push(registry_diagnostic(
1235                    "construct_keyword_duplicate",
1236                    format!(
1237                        "construct keyword `{}` is registered more than once for `{}`",
1238                        form.keyword, form.scope
1239                    ),
1240                ));
1241            }
1242            validate_construct_fields(form, &mut diagnostics);
1243            validate_construct_interfaces(form, "requires", &form.requires, &mut diagnostics);
1244            validate_construct_interfaces(form, "provides", &form.provides, &mut diagnostics);
1245        }
1246
1247        let mut contracts = BTreeSet::new();
1248
1249        for contract in &self.effect_contracts {
1250            if contract.id.trim().is_empty() {
1251                diagnostics.push(registry_diagnostic(
1252                    "effect_contract_id_empty",
1253                    "effect contract has an empty id",
1254                ));
1255            }
1256            if contract.version.trim().is_empty() {
1257                diagnostics.push(registry_diagnostic(
1258                    "effect_contract_version_empty",
1259                    format!("effect contract `{}` has an empty version", contract.id),
1260                ));
1261            }
1262            if contract.effect_kind.trim().is_empty() {
1263                diagnostics.push(registry_diagnostic(
1264                    "effect_kind_empty",
1265                    format!("effect contract `{}` has an empty effect kind", contract.id),
1266                ));
1267            }
1268            if contract.source_forms.is_empty() {
1269                diagnostics.push(registry_diagnostic(
1270                    "source_forms_empty",
1271                    format!("effect contract `{}` declares no source forms", contract.id),
1272                ));
1273            }
1274            if !library_ids.contains(contract.library_id.as_str()) {
1275                diagnostics.push(registry_diagnostic(
1276                    "effect_contract_unknown_library",
1277                    format!(
1278                        "effect contract `{}` references unknown library `{}`",
1279                        contract.id, contract.library_id
1280                    ),
1281                ));
1282            }
1283            if !contracts.insert((contract.id.clone(), contract.version.clone())) {
1284                diagnostics.push(registry_diagnostic(
1285                    "effect_contract_duplicate",
1286                    format!(
1287                        "effect contract `{}` version `{}` is registered more than once",
1288                        contract.id, contract.version
1289                    ),
1290                ));
1291            }
1292            validate_unique_list(
1293                "required_capability_duplicate",
1294                &format!("effect contract `{}`", contract.id),
1295                "required capability",
1296                &contract.required_capabilities,
1297                &mut diagnostics,
1298            );
1299            validate_unique_list(
1300                "provider_kind_duplicate",
1301                &format!("effect contract `{}`", contract.id),
1302                "provider kind",
1303                &contract.provider_kinds,
1304                &mut diagnostics,
1305            );
1306            validate_unique_list(
1307                "projected_fact_duplicate",
1308                &format!("effect contract `{}`", contract.id),
1309                "projected fact",
1310                &contract.projected_facts,
1311                &mut diagnostics,
1312            );
1313            if contract.validation == TypedOutputValidation::RuntimeBoundary
1314                && contract.output_schema.is_none()
1315            {
1316                diagnostics.push(registry_diagnostic(
1317                    "runtime_validation_without_output_schema",
1318                    format!(
1319                        "effect contract `{}` uses runtime validation without an output schema",
1320                        contract.id
1321                    ),
1322                ));
1323            }
1324            if !contract.projected_facts.is_empty() && contract.output_schema.is_none() {
1325                diagnostics.push(registry_diagnostic(
1326                    "projection_without_output_schema",
1327                    format!(
1328                        "effect contract `{}` projects facts without an output schema",
1329                        contract.id
1330                    ),
1331                ));
1332            }
1333        }
1334
1335        diagnostics
1336    }
1337}
1338
1339fn validate_construct_fields(
1340    form: &ConstructRegistration,
1341    diagnostics: &mut Vec<ContractRegistryDiagnostic>,
1342) {
1343    let mut fields = BTreeMap::new();
1344    for field in &form.fields {
1345        if field.name.trim().is_empty() {
1346            diagnostics.push(registry_diagnostic(
1347                "construct_field_name_empty",
1348                format!("construct `{}` has a field with an empty name", form.id),
1349            ));
1350        }
1351        if field.kind.trim().is_empty() {
1352            diagnostics.push(registry_diagnostic(
1353                "construct_field_kind_empty",
1354                format!(
1355                    "construct `{}` field `{}` has an empty kind",
1356                    form.id, field.name
1357                ),
1358            ));
1359        }
1360        let count = fields.entry(field.name.clone()).or_insert(0usize);
1361        *count += 1;
1362    }
1363    for (field, count) in fields {
1364        if count > 1 {
1365            diagnostics.push(registry_diagnostic(
1366                "construct_field_duplicate",
1367                format!(
1368                    "construct `{}` declares field `{field}` more than once",
1369                    form.id
1370                ),
1371            ));
1372        }
1373    }
1374}
1375
1376fn validate_construct_interfaces(
1377    form: &ConstructRegistration,
1378    direction: &str,
1379    interfaces: &[ConstructInterface],
1380    diagnostics: &mut Vec<ContractRegistryDiagnostic>,
1381) {
1382    for interface in interfaces {
1383        if interface.kind.trim().is_empty() {
1384            diagnostics.push(registry_diagnostic(
1385                "construct_interface_kind_empty",
1386                format!(
1387                    "construct `{}` {direction} interface has an empty kind",
1388                    form.id
1389                ),
1390            ));
1391        }
1392        if interface
1393            .name
1394            .as_deref()
1395            .is_some_and(|name| name.trim().is_empty())
1396        {
1397            diagnostics.push(registry_diagnostic(
1398                "construct_interface_name_empty",
1399                format!(
1400                    "construct `{}` {direction} interface `{}` has an empty name",
1401                    form.id, interface.kind
1402                ),
1403            ));
1404        }
1405        if interface
1406            .type_ref
1407            .as_deref()
1408            .is_some_and(|type_ref| type_ref.trim().is_empty())
1409        {
1410            diagnostics.push(registry_diagnostic(
1411                "construct_interface_type_empty",
1412                format!(
1413                    "construct `{}` {direction} interface `{}` has an empty type",
1414                    form.id, interface.kind
1415                ),
1416            ));
1417        }
1418        if interface.phase.trim().is_empty() {
1419            diagnostics.push(registry_diagnostic(
1420                "construct_interface_phase_empty",
1421                format!(
1422                    "construct `{}` {direction} interface `{}` has an empty phase",
1423                    form.id, interface.kind
1424                ),
1425            ));
1426        }
1427        if interface.cardinality.trim().is_empty() {
1428            diagnostics.push(registry_diagnostic(
1429                "construct_interface_cardinality_empty",
1430                format!(
1431                    "construct `{}` {direction} interface `{}` has an empty cardinality",
1432                    form.id, interface.kind
1433                ),
1434            ));
1435        }
1436    }
1437}
1438
1439fn merge_unique_list(target: &mut Vec<String>, values: &[String]) {
1440    for value in values {
1441        if !target.contains(value) {
1442            target.push(value.clone());
1443        }
1444    }
1445    target.sort();
1446}
1447
1448fn registry_diagnostic(
1449    code: impl Into<String>,
1450    message: impl Into<String>,
1451) -> ContractRegistryDiagnostic {
1452    ContractRegistryDiagnostic {
1453        code: code.into(),
1454        message: message.into(),
1455    }
1456}
1457
1458fn validate_unique_list(
1459    code: &str,
1460    owner: &str,
1461    label: &str,
1462    values: &[String],
1463    diagnostics: &mut Vec<ContractRegistryDiagnostic>,
1464) {
1465    let mut seen = BTreeMap::new();
1466    for value in values {
1467        let count = seen.entry(value).or_insert(0usize);
1468        *count += 1;
1469    }
1470    for (value, count) in seen {
1471        if count > 1 {
1472            diagnostics.push(registry_diagnostic(
1473                code,
1474                format!("{owner} declares {label} `{value}` more than once"),
1475            ));
1476        }
1477    }
1478}
1479
1480#[cfg(test)]
1481mod tests {
1482    use super::*;
1483
1484    #[test]
1485    fn exposes_stage_marker() {
1486        assert_eq!(IMPLEMENTATION_STAGE, "release");
1487    }
1488
1489    #[test]
1490    fn exposes_version() {
1491        assert!(!version().is_empty());
1492    }
1493
1494    #[test]
1495    fn derive_fields_maps_declaration_block_clauses() {
1496        // A declaration_block grammar with a flag, a connective-introduced
1497        // value clause, and a list clause. Each clause becomes exactly one
1498        // field; there is no trailing binding field.
1499        let grammar = ConstructGrammar {
1500            shape: CONSTRUCT_GRAMMAR_SHAPE_DECLARATION_BLOCK.to_owned(),
1501            keyword: "ledger".to_owned(),
1502            slots: Vec::new(),
1503            payload: None,
1504            binding: "none".to_owned(),
1505            target_capability: String::new(),
1506            clauses: Some(vec![
1507                ConstructGrammarClause {
1508                    name: "shared".to_owned(),
1509                    kind: "flag".to_owned(),
1510                    required: false,
1511                    list: false,
1512                    connective: None,
1513                },
1514                ConstructGrammarClause {
1515                    name: "partition".to_owned(),
1516                    kind: "identifier".to_owned(),
1517                    required: true,
1518                    list: false,
1519                    connective: Some("by".to_owned()),
1520                },
1521                ConstructGrammarClause {
1522                    name: "allow read".to_owned(),
1523                    kind: "glob".to_owned(),
1524                    required: false,
1525                    list: true,
1526                    connective: None,
1527                },
1528            ]),
1529        };
1530
1531        let fields = grammar.derive_fields();
1532        assert_eq!(
1533            fields,
1534            vec![
1535                // A flag maps to an optional boolean (it carries no value).
1536                ConstructField {
1537                    name: "shared".to_owned(),
1538                    kind: "boolean".to_owned(),
1539                    required: false,
1540                },
1541                // A value clause maps its kind through the field vocabulary and
1542                // keeps its own required flag; the connective is not a field.
1543                ConstructField {
1544                    name: "partition".to_owned(),
1545                    kind: "identifier".to_owned(),
1546                    required: true,
1547                },
1548                // A list clause flattens into the `list` field kind.
1549                ConstructField {
1550                    name: "allow read".to_owned(),
1551                    kind: "list".to_owned(),
1552                    required: false,
1553                },
1554            ]
1555        );
1556
1557        // Bite: every derived kind is in the platform field-kind vocabulary, so
1558        // the manifest consistency check accepts a grammar-only construct.
1559        for field in &fields {
1560            assert!(
1561                PLATFORM_CONSTRUCT_CATALOG.contains_field_kind(&field.kind),
1562                "derived field kind `{}` must be a platform field kind",
1563                field.kind
1564            );
1565        }
1566    }
1567
1568    #[test]
1569    fn severity_round_trips_the_canonical_set() {
1570        assert_eq!(Severity::ALL.len(), 4);
1571        for severity in Severity::ALL {
1572            assert_eq!(Severity::from_wire(severity.as_str()), Some(severity));
1573        }
1574        assert_eq!(
1575            Severity::ALL.map(Severity::as_str),
1576            ["error", "warning", "info", "hint"]
1577        );
1578        // `note` is related-information, not a severity; inbox `normal` is unrelated.
1579        assert_eq!(Severity::from_wire("note"), None);
1580        assert_eq!(Severity::from_wire("normal"), None);
1581    }
1582
1583    #[test]
1584    fn severity_lsp_codes_align_one_to_one() {
1585        // The canonical set maps 1:1 onto LSP DiagnosticSeverity numbers.
1586        assert_eq!(
1587            Severity::ALL.map(Severity::lsp_code),
1588            [1, 2, 3, 4],
1589            "error/warning/info/hint must map to LSP 1/2/3/4"
1590        );
1591    }
1592
1593    #[test]
1594    fn platform_construct_catalog_defines_current_executable_slice() {
1595        assert!(PLATFORM_CONSTRUCT_CATALOG
1596            .family(CONSTRUCT_FAMILY_DECLARATION_BLOCK)
1597            .is_some());
1598        assert!(PLATFORM_CONSTRUCT_CATALOG
1599            .family(CONSTRUCT_FAMILY_EFFECT_OPERATION)
1600            .is_some());
1601
1602        let capability_call = PLATFORM_CONSTRUCT_CATALOG
1603            .lowering(CONSTRUCT_LOWERING_CAPABILITY_CALL)
1604            .expect("capability_call lowering");
1605        assert_eq!(
1606            capability_call.compatible_families,
1607            &[CONSTRUCT_FAMILY_EFFECT_OPERATION]
1608        );
1609        assert!(capability_call.package_authorable);
1610        assert_eq!(
1611            capability_call.required_scope,
1612            Some(CONSTRUCT_SCOPE_RULE_BODY)
1613        );
1614        assert_eq!(
1615            capability_call.target_capability,
1616            ConstructTargetCapabilityPolicy::RequiredCapabilityCallContract
1617        );
1618        assert_eq!(
1619            capability_call.required_interfaces,
1620            &[CONSTRUCT_INTERFACE_CAPABILITY]
1621        );
1622        assert_eq!(
1623            capability_call.provided_interfaces,
1624            &[CONSTRUCT_INTERFACE_EFFECT_HANDLE]
1625        );
1626        assert_eq!(
1627            capability_call.lifecycle_profiles,
1628            &["effect_graph", "typed_effect_graph"]
1629        );
1630        assert_eq!(
1631            capability_call.authority_profile,
1632            ConstructLoweringAuthorityProfile::CapabilityScoped
1633        );
1634        assert_eq!(
1635            capability_call.static_guarantees,
1636            CONSTRUCT_PLATFORM_STATIC_GUARANTEES
1637        );
1638
1639        let metadata_only = PLATFORM_CONSTRUCT_CATALOG
1640            .lowering(CONSTRUCT_LOWERING_METADATA_ONLY)
1641            .expect("metadata_only lowering");
1642        assert_eq!(
1643            metadata_only.compatible_families,
1644            &[CONSTRUCT_FAMILY_DECLARATION_BLOCK]
1645        );
1646        assert!(metadata_only.package_authorable);
1647        assert_eq!(
1648            metadata_only.target_capability,
1649            ConstructTargetCapabilityPolicy::Forbidden
1650        );
1651
1652        assert!(PLATFORM_CONSTRUCT_CATALOG
1653            .family(CONSTRUCT_FAMILY_SOURCE_DECLARATION)
1654            .is_some());
1655
1656        let signal_source = PLATFORM_CONSTRUCT_CATALOG
1657            .lowering(CONSTRUCT_LOWERING_SIGNAL_SOURCE)
1658            .expect("signal_source lowering");
1659        assert!(!signal_source.package_authorable);
1660        assert_eq!(
1661            signal_source.compatible_families,
1662            &[CONSTRUCT_FAMILY_SOURCE_DECLARATION]
1663        );
1664        assert_eq!(
1665            signal_source.lifecycle_profiles,
1666            &["signal_source_template"]
1667        );
1668
1669        let clock_source = PLATFORM_CONSTRUCT_CATALOG
1670            .lowering(CONSTRUCT_LOWERING_CLOCK_SOURCE)
1671            .expect("clock_source lowering");
1672        assert!(!clock_source.package_authorable);
1673        assert_eq!(
1674            clock_source.compatible_families,
1675            &[CONSTRUCT_FAMILY_SOURCE_DECLARATION]
1676        );
1677        assert_eq!(clock_source.lifecycle_profiles, &["clock_source_template"]);
1678        assert_eq!(
1679            clock_source.authority_profile,
1680            ConstructLoweringAuthorityProfile::EventAdmission
1681        );
1682
1683        assert!(PLATFORM_CONSTRUCT_CATALOG.contains_reserved_keyword("claim"));
1684        assert!(PLATFORM_CONSTRUCT_CATALOG.contains_reserved_keyword("lease"));
1685        // Tracker verbs are typed dedicated effect kinds, so their reserved-keyword
1686        // privilege authorizes `typed_effect_call` (not `capability_call`, which is
1687        // for plain request/response). The old `capability_call` privilege is gone.
1688        assert!(PLATFORM_CONSTRUCT_CATALOG
1689            .reserved_keyword_privilege(
1690                "std.tracker",
1691                "claim",
1692                CONSTRUCT_FAMILY_EFFECT_OPERATION,
1693                CONSTRUCT_SCOPE_RULE_BODY,
1694                CONSTRUCT_LOWERING_TYPED_EFFECT_CALL,
1695            )
1696            .is_some());
1697        assert!(PLATFORM_CONSTRUCT_CATALOG
1698            .reserved_keyword_privilege(
1699                "std.tracker",
1700                "claim",
1701                CONSTRUCT_FAMILY_EFFECT_OPERATION,
1702                CONSTRUCT_SCOPE_RULE_BODY,
1703                CONSTRUCT_LOWERING_CAPABILITY_CALL,
1704            )
1705            .is_none());
1706        assert!(PLATFORM_CONSTRUCT_CATALOG
1707            .reserved_keyword_privilege(
1708                "memory",
1709                "claim",
1710                CONSTRUCT_FAMILY_EFFECT_OPERATION,
1711                CONSTRUCT_SCOPE_RULE_BODY,
1712                CONSTRUCT_LOWERING_TYPED_EFFECT_CALL,
1713            )
1714            .is_none());
1715    }
1716
1717    #[test]
1718    fn validates_duplicate_and_malformed_contracts() {
1719        let registry = ContractRegistry {
1720            libraries: vec![LibraryRegistration {
1721                id: "std.coercion".to_owned(),
1722                version: "v0".to_owned(),
1723                standard: true,
1724            }],
1725            constructs: vec![
1726                ConstructRegistration {
1727                    id: "coerce.form".to_owned(),
1728                    library_id: "std.coercion".to_owned(),
1729                    version: "v0".to_owned(),
1730                    construct_family: "declaration_block".to_owned(),
1731                    keyword: "coerce".to_owned(),
1732                    scope: "top_level".to_owned(),
1733                    grammar: None,
1734                    fields: vec![ConstructField {
1735                        name: "name".to_owned(),
1736                        kind: "identifier".to_owned(),
1737                        required: true,
1738                    }],
1739                    requires: Vec::new(),
1740                    provides: Vec::new(),
1741                    lowering_target: "metadata_only".to_owned(),
1742                    target_capability: None,
1743                },
1744                ConstructRegistration {
1745                    id: "coerce.form".to_owned(),
1746                    library_id: "missing".to_owned(),
1747                    version: "v0".to_owned(),
1748                    construct_family: String::new(),
1749                    keyword: "coerce".to_owned(),
1750                    scope: "top_level".to_owned(),
1751                    grammar: None,
1752                    fields: vec![
1753                        ConstructField {
1754                            name: "name".to_owned(),
1755                            kind: "identifier".to_owned(),
1756                            required: true,
1757                        },
1758                        ConstructField {
1759                            name: "name".to_owned(),
1760                            kind: String::new(),
1761                            required: false,
1762                        },
1763                    ],
1764                    requires: vec![ConstructInterface {
1765                        kind: String::new(),
1766                        name: Some(String::new()),
1767                        type_ref: Some(String::new()),
1768                        phase: String::new(),
1769                        cardinality: String::new(),
1770                    }],
1771                    provides: Vec::new(),
1772                    lowering_target: String::new(),
1773                    target_capability: Some(String::new()),
1774                },
1775            ],
1776            effect_contracts: vec![
1777                EffectContract {
1778                    id: "schema.coerce".to_owned(),
1779                    library_id: "std.coercion".to_owned(),
1780                    version: "v0".to_owned(),
1781                    effect_kind: "schema.coerce".to_owned(),
1782                    source_forms: vec!["coerce".to_owned()],
1783                    input_schema: Some("schema.coerce.input".to_owned()),
1784                    output_schema: Some("typed-provider-output".to_owned()),
1785                    required_capabilities: vec!["model.invoke".to_owned()],
1786                    provider_kinds: vec!["model".to_owned()],
1787                    projected_facts: vec!["effect.output".to_owned()],
1788                    validation: TypedOutputValidation::RuntimeBoundary,
1789                },
1790                EffectContract {
1791                    id: "schema.coerce".to_owned(),
1792                    library_id: "missing".to_owned(),
1793                    version: "v0".to_owned(),
1794                    effect_kind: "schema.coerce".to_owned(),
1795                    source_forms: Vec::new(),
1796                    input_schema: None,
1797                    output_schema: None,
1798                    required_capabilities: vec![
1799                        "model.invoke".to_owned(),
1800                        "model.invoke".to_owned(),
1801                    ],
1802                    provider_kinds: Vec::new(),
1803                    projected_facts: vec!["effect.output".to_owned()],
1804                    validation: TypedOutputValidation::RuntimeBoundary,
1805                },
1806            ],
1807        };
1808
1809        let codes = registry
1810            .validate()
1811            .into_iter()
1812            .map(|diagnostic| diagnostic.code)
1813            .collect::<BTreeSet<_>>();
1814
1815        assert!(codes.contains("effect_contract_unknown_library"));
1816        assert!(codes.contains("effect_contract_duplicate"));
1817        assert!(codes.contains("construct_unknown_library"));
1818        assert!(codes.contains("construct_duplicate"));
1819        assert!(codes.contains("construct_keyword_duplicate"));
1820        assert!(codes.contains("construct_family_empty"));
1821        assert!(codes.contains("construct_field_duplicate"));
1822        assert!(codes.contains("construct_field_kind_empty"));
1823        assert!(codes.contains("construct_interface_kind_empty"));
1824        assert!(codes.contains("construct_interface_name_empty"));
1825        assert!(codes.contains("construct_interface_type_empty"));
1826        assert!(codes.contains("construct_interface_phase_empty"));
1827        assert!(codes.contains("construct_interface_cardinality_empty"));
1828        assert!(codes.contains("construct_lowering_target_empty"));
1829        assert!(codes.contains("source_forms_empty"));
1830        assert!(codes.contains("required_capability_duplicate"));
1831        assert!(codes.contains("runtime_validation_without_output_schema"));
1832        assert!(codes.contains("projection_without_output_schema"));
1833    }
1834
1835    #[test]
1836    fn merge_replaces_unlocked_import_with_locked_library() {
1837        let mut registry = ContractRegistry {
1838            libraries: vec![LibraryRegistration {
1839                id: "memory".to_owned(),
1840                version: "unlocked".to_owned(),
1841                standard: false,
1842            }],
1843            constructs: Vec::new(),
1844            effect_contracts: Vec::new(),
1845        };
1846
1847        registry.merge(ContractRegistry {
1848            libraries: vec![LibraryRegistration {
1849                id: "memory".to_owned(),
1850                version: "0.1.0".to_owned(),
1851                standard: false,
1852            }],
1853            constructs: Vec::new(),
1854            effect_contracts: Vec::new(),
1855        });
1856
1857        assert_eq!(
1858            registry.libraries,
1859            vec![LibraryRegistration {
1860                id: "memory".to_owned(),
1861                version: "0.1.0".to_owned(),
1862                standard: false,
1863            }]
1864        );
1865        assert_eq!(registry.validate(), Vec::new());
1866    }
1867}