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