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