Skip to main content

type_bridge_contract/
migration_assertion.rs

1//! Context-free canonical syntax for migration assertions.
2
3use std::collections::BTreeSet;
4use std::fmt;
5
6use serde::{Serialize, Serializer};
7
8use crate::capability::{CapabilityId, CapabilitySet};
9use crate::codec::{FormatVersion, to_canonical_json};
10use crate::diagnostic::{Diagnostic, DiagnosticCategory, DiagnosticCode};
11use crate::fingerprint::{CanonicalizationVersion, Fingerprint, FingerprintDomain};
12use crate::id::{AttributeId, RoleId, TypeId};
13use crate::limits::{MAX_BINDINGS, MAX_OUTPUT_NAME_BYTES, StructuralLimits};
14use crate::schema_fingerprint::ManagedSemanticSchemaFingerprint;
15use crate::value::CanonicalValue;
16
17/// Fingerprint domain for canonical migration assertions.
18pub const MIGRATION_ASSERTION_FINGERPRINT_DOMAIN: &str = "typebridge.query.migration-assertion";
19/// Canonicalization identifier for canonical migration assertions.
20pub const MIGRATION_ASSERTION_CANONICALIZATION: &str = "typebridge.migration-assertion/v1";
21
22const CAP_ASSERTION: &str = "query.migration-assertion";
23const CAP_ISA: &str = "query.pattern.isa";
24const CAP_ISA_SUBTYPES: &str = "query.pattern.isa-subtypes";
25const CAP_HAS: &str = "query.pattern.has";
26const CAP_LINKS: &str = "query.pattern.links";
27const CAP_VALUE: &str = "query.pattern.value";
28const CAP_NEGATION: &str = "query.pattern.negation";
29
30/// Return the complete capability vocabulary used by canonical migration assertions.
31pub fn migration_assertion_capability_vocabulary() -> CapabilitySet {
32    let mut capabilities = CapabilitySet::new();
33    for capability in [
34        CAP_ASSERTION,
35        CAP_ISA,
36        CAP_ISA_SUBTYPES,
37        CAP_HAS,
38        CAP_LINKS,
39        CAP_VALUE,
40        CAP_NEGATION,
41    ] {
42        insert_capability(&mut capabilities, capability)
43            .expect("static migration assertion capability ID is canonical");
44    }
45    capabilities
46}
47
48/// Dense zero-based identity of one binding in a typed assertion plan.
49#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
50#[serde(transparent)]
51pub struct BindingId(u16);
52
53impl BindingId {
54    /// Construct a binding ordinal within the canonical binding ceiling.
55    pub fn new(value: u16) -> Result<Self, Diagnostic> {
56        if usize::from(value) < MAX_BINDINGS {
57            Ok(Self(value))
58        } else {
59            Err(assertion_failure(
60                DiagnosticCategory::ResourceLimit,
61                "migration_assertion_binding_id_out_of_range",
62                "binding ID exceeds the canonical structural ceiling",
63            ))
64        }
65    }
66
67    /// Return the zero-based ordinal.
68    pub const fn get(self) -> u16 {
69        self.0
70    }
71}
72
73/// Validated canonical query-variable spelling without a `$` sigil.
74#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
75pub struct QueryVariable(String);
76
77impl QueryVariable {
78    /// Validate a bounded lowercase variable spelling.
79    pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
80        let value = value.into();
81        let mut bytes = value.bytes();
82        let valid = !value.is_empty()
83            && value.len() <= MAX_OUTPUT_NAME_BYTES
84            && bytes.next().is_some_and(|byte| byte.is_ascii_lowercase())
85            && bytes.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_');
86        if valid {
87            Ok(Self(value))
88        } else {
89            Err(assertion_failure(
90                DiagnosticCategory::InvalidContract,
91                "migration_assertion_invalid_variable",
92                "query variable must be a bounded lowercase identifier",
93            ))
94        }
95    }
96
97    /// Return the canonical spelling.
98    pub fn as_str(&self) -> &str {
99        &self.0
100    }
101}
102
103impl fmt::Display for QueryVariable {
104    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
105        formatter.write_str(self.as_str())
106    }
107}
108
109impl Serialize for QueryVariable {
110    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
111    where
112        S: Serializer,
113    {
114        serializer.serialize_str(self.as_str())
115    }
116}
117
118/// One explicit dense binding declaration.
119#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
120pub struct AssertionBinding {
121    id: BindingId,
122    variable: QueryVariable,
123}
124
125impl AssertionBinding {
126    /// Construct one typed binding declaration.
127    pub const fn new(id: BindingId, variable: QueryVariable) -> Self {
128        Self { id, variable }
129    }
130
131    /// Return the dense binding identity.
132    pub const fn id(&self) -> BindingId {
133        self.id
134    }
135
136    /// Return the user-facing query variable.
137    pub const fn variable(&self) -> &QueryVariable {
138        &self.variable
139    }
140}
141
142/// One role-qualified relation player.
143#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
144pub struct AssertionRolePlayer {
145    player: BindingId,
146    role: RoleId,
147}
148
149impl AssertionRolePlayer {
150    /// Construct a typed role player.
151    pub const fn new(role: RoleId, player: BindingId) -> Self {
152        Self { player, role }
153    }
154
155    /// Return the role identity.
156    pub const fn role(&self) -> &RoleId {
157        &self.role
158    }
159
160    /// Return the player binding.
161    pub const fn player(&self) -> BindingId {
162        self.player
163    }
164}
165
166/// Closed comparator vocabulary for typed canonical values.
167#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
168#[serde(rename_all = "snake_case")]
169pub enum ValueComparator {
170    /// Equal values.
171    Equal,
172    /// Unequal values.
173    NotEqual,
174    /// Strictly less.
175    Less,
176    /// Less or equal.
177    LessOrEqual,
178    /// Strictly greater.
179    Greater,
180    /// Greater or equal.
181    GreaterOrEqual,
182}
183
184/// One typed operand in a value comparison.
185#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
186#[serde(tag = "kind", rename_all = "snake_case")]
187pub enum ValueOperand {
188    /// Read the scalar value of an attribute binding.
189    Binding {
190        /// The attribute binding whose value is read.
191        binding: BindingId,
192    },
193    /// Compare with an exact canonical literal.
194    Literal {
195        /// The exact canonical scalar to compare against.
196        value: CanonicalValue,
197    },
198}
199
200impl ValueOperand {
201    /// Construct a binding operand.
202    pub const fn binding(binding: BindingId) -> Self {
203        Self::Binding { binding }
204    }
205
206    /// Construct a literal operand.
207    pub const fn literal(value: CanonicalValue) -> Self {
208        Self::Literal { value }
209    }
210}
211
212/// Minimal closed typed pattern algebra used by migration assertions.
213#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
214#[serde(tag = "kind", rename_all = "snake_case")]
215pub enum AssertionPattern {
216    /// Constrain a binding to one schema type, optionally including subtypes.
217    Isa {
218        /// The binding the type constraint applies to.
219        binding: BindingId,
220        /// Whether subtypes of the named type also satisfy the constraint.
221        include_subtypes: bool,
222        /// The schema type the binding must instantiate.
223        type_id: TypeId,
224    },
225    /// Bind an owned attribute through an effective ownership.
226    Has {
227        /// The binding that receives the owned attribute instance.
228        attribute: BindingId,
229        /// The schema attribute type being read.
230        attribute_id: AttributeId,
231        /// The binding that owns the attribute.
232        owner: BindingId,
233    },
234    /// Bind a relation and role-qualified players.
235    Links {
236        /// The role-qualified players the relation must link.
237        players: Vec<AssertionRolePlayer>,
238        /// The binding that receives the relation instance.
239        relation: BindingId,
240        /// The schema relation type being matched.
241        relation_id: TypeId,
242    },
243    /// Compare two exact typed scalar operands.
244    Value {
245        /// The comparison operator.
246        comparator: ValueComparator,
247        /// The left operand.
248        left: ValueOperand,
249        /// The right operand.
250        right: ValueOperand,
251    },
252    /// Negate a closed nested conjunction.
253    Not {
254        /// The conjunction that must not match.
255        patterns: Vec<AssertionPattern>,
256    },
257}
258
259/// The only expectation admitted by the first migration assertion revision.
260#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
261#[serde(rename_all = "snake_case")]
262pub enum AssertionExpectation {
263    /// The validated query must produce no rows.
264    NoRows,
265}
266
267/// A context-free, canonical typed migration assertion.
268#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
269pub struct MigrationAssertionPlan {
270    bindings: Vec<AssertionBinding>,
271    expectation: AssertionExpectation,
272    format: FormatVersion,
273    managed_semantics: ManagedSemanticSchemaFingerprint,
274    outputs: Vec<BindingId>,
275    patterns: Vec<AssertionPattern>,
276    required_capabilities: CapabilitySet,
277    witnesses: Vec<BindingId>,
278}
279
280impl MigrationAssertionPlan {
281    /// Validate and construct one canonical plan under fixed protocol limits.
282    pub fn new(
283        bindings: Vec<AssertionBinding>,
284        patterns: Vec<AssertionPattern>,
285        outputs: Vec<BindingId>,
286        witnesses: Vec<BindingId>,
287        managed_semantics: ManagedSemanticSchemaFingerprint,
288        expectation: AssertionExpectation,
289    ) -> Result<Self, Diagnostic> {
290        Self::new_with_limits(
291            bindings,
292            patterns,
293            outputs,
294            witnesses,
295            managed_semantics,
296            expectation,
297            StructuralLimits::CANONICAL,
298        )
299    }
300
301    fn new_with_limits(
302        bindings: Vec<AssertionBinding>,
303        patterns: Vec<AssertionPattern>,
304        mut outputs: Vec<BindingId>,
305        mut witnesses: Vec<BindingId>,
306        managed_semantics: ManagedSemanticSchemaFingerprint,
307        expectation: AssertionExpectation,
308        limits: StructuralLimits,
309    ) -> Result<Self, Diagnostic> {
310        if bindings.is_empty() || !limits.allows_bindings(bindings.len()) {
311            return Err(assertion_failure(
312                DiagnosticCategory::ResourceLimit,
313                "migration_assertion_binding_limit",
314                "assertion binding count is empty or exceeds the structural ceiling",
315            ));
316        }
317        let mut variables = BTreeSet::new();
318        for (index, binding) in bindings.iter().enumerate() {
319            if usize::from(binding.id.get()) != index {
320                return Err(assertion_failure(
321                    DiagnosticCategory::InvalidContract,
322                    "migration_assertion_bindings_not_dense",
323                    "assertion binding IDs must be ordered dense zero-based ordinals",
324                ));
325            }
326            if !variables.insert(binding.variable.clone()) {
327                return Err(assertion_failure(
328                    DiagnosticCategory::InvalidContract,
329                    "migration_assertion_duplicate_variable",
330                    "assertion query variables must be unique",
331                ));
332            }
333        }
334        canonical_binding_set(&mut outputs, bindings.len(), "output")?;
335        canonical_binding_set(&mut witnesses, bindings.len(), "witness")?;
336        if !limits.allows_selected_slots(outputs.len()) {
337            return Err(assertion_failure(
338                DiagnosticCategory::ResourceLimit,
339                "migration_assertion_output_limit",
340                "assertion output count exceeds the structural ceiling",
341            ));
342        }
343        if outputs.iter().any(|id| witnesses.binary_search(id).is_ok()) {
344            return Err(assertion_failure(
345                DiagnosticCategory::InvalidContract,
346                "migration_assertion_witness_is_output",
347                "a hidden witness cannot also be an output binding",
348            ));
349        }
350        if patterns.is_empty() || patterns.len() > limits.boolean_terms {
351            return Err(assertion_failure(
352                DiagnosticCategory::ResourceLimit,
353                "migration_assertion_pattern_limit",
354                "assertion root conjunction is empty or exceeds the term ceiling",
355            ));
356        }
357        let mut stats = PatternStats::default();
358        for pattern in &patterns {
359            inspect_pattern(pattern, 1, bindings.len(), limits, &mut stats)?;
360        }
361        let required_capabilities = derive_capabilities(&patterns)?;
362        Ok(Self {
363            bindings,
364            expectation,
365            format: FormatVersion::V1,
366            managed_semantics,
367            outputs,
368            patterns,
369            required_capabilities,
370            witnesses,
371        })
372    }
373
374    /// Return the owning format version.
375    pub const fn format(&self) -> FormatVersion {
376        self.format
377    }
378
379    /// Return the exact managed semantic schema this plan was validated against.
380    pub const fn managed_semantics(&self) -> &ManagedSemanticSchemaFingerprint {
381        &self.managed_semantics
382    }
383
384    /// Return dense binding declarations.
385    pub fn bindings(&self) -> &[AssertionBinding] {
386        &self.bindings
387    }
388
389    /// Look up one dense binding declaration.
390    pub fn binding(&self, id: BindingId) -> Option<&AssertionBinding> {
391        self.bindings.get(usize::from(id.get()))
392    }
393
394    /// Return the ordered positive/negative pattern conjunction.
395    pub fn patterns(&self) -> &[AssertionPattern] {
396        &self.patterns
397    }
398
399    /// Return canonical-sorted output bindings.
400    pub fn outputs(&self) -> &[BindingId] {
401        &self.outputs
402    }
403
404    /// Return canonical-sorted hidden witness bindings.
405    pub fn witnesses(&self) -> &[BindingId] {
406        &self.witnesses
407    }
408
409    /// Return the closed assertion expectation.
410    pub const fn expectation(&self) -> AssertionExpectation {
411        self.expectation
412    }
413
414    /// Return open capabilities derived from syntax.
415    pub const fn required_capabilities(&self) -> &CapabilitySet {
416        &self.required_capabilities
417    }
418
419    /// Encode exact canonical bytes.
420    pub fn canonical_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
421        encode_migration_assertion_plan(self)
422    }
423
424    /// Compute a canonical domain-separated plan fingerprint.
425    pub fn fingerprint(&self) -> Result<MigrationAssertionPlanFingerprint, Diagnostic> {
426        MigrationAssertionPlanFingerprint::compute(self)
427    }
428}
429
430/// Fingerprint of exact canonical migration assertion bytes.
431#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
432#[serde(transparent)]
433pub struct MigrationAssertionPlanFingerprint(Fingerprint);
434
435impl MigrationAssertionPlanFingerprint {
436    /// Compute the fixed-domain fingerprint of a trusted plan.
437    pub fn compute(plan: &MigrationAssertionPlan) -> Result<Self, Diagnostic> {
438        Ok(Self(Fingerprint::compute(
439            FingerprintDomain::new(MIGRATION_ASSERTION_FINGERPRINT_DOMAIN)?,
440            CanonicalizationVersion::new(MIGRATION_ASSERTION_CANONICALIZATION)?,
441            None,
442            &plan.canonical_bytes()?,
443        )))
444    }
445
446    /// Return the generic fingerprint.
447    pub const fn as_fingerprint(&self) -> &Fingerprint {
448        &self.0
449    }
450}
451
452/// Encode a trusted plan as bounded canonical JSON.
453pub fn encode_migration_assertion_plan(
454    plan: &MigrationAssertionPlan,
455) -> Result<Vec<u8>, Diagnostic> {
456    to_canonical_json(plan)
457}
458
459/// Decode canonical bytes through private constructor-rebuilding wire types.
460pub fn decode_migration_assertion_plan(bytes: &[u8]) -> Result<MigrationAssertionPlan, Diagnostic> {
461    crate::migration_assertion_wire::decode_migration_assertion_plan(bytes)
462}
463
464#[derive(Default)]
465struct PatternStats {
466    nodes: usize,
467}
468
469fn inspect_pattern(
470    pattern: &AssertionPattern,
471    depth: usize,
472    binding_count: usize,
473    limits: StructuralLimits,
474    stats: &mut PatternStats,
475) -> Result<(), Diagnostic> {
476    stats.nodes += 1;
477    if !limits.allows_predicate_nodes(stats.nodes) {
478        return Err(assertion_failure(
479            DiagnosticCategory::ResourceLimit,
480            "migration_assertion_pattern_node_limit",
481            "assertion pattern count exceeds the structural ceiling",
482        ));
483    }
484    if !limits.allows_predicate_depth(depth) {
485        return Err(assertion_failure(
486            DiagnosticCategory::ResourceLimit,
487            "migration_assertion_pattern_depth_limit",
488            "assertion pattern depth exceeds the structural ceiling",
489        ));
490    }
491    match pattern {
492        AssertionPattern::Isa { binding, .. } => check_binding(*binding, binding_count),
493        AssertionPattern::Has {
494            owner, attribute, ..
495        } => {
496            check_binding(*owner, binding_count)?;
497            check_binding(*attribute, binding_count)
498        }
499        AssertionPattern::Links {
500            relation, players, ..
501        } => {
502            check_binding(*relation, binding_count)?;
503            if players.is_empty() || players.len() > limits.boolean_terms {
504                return Err(assertion_failure(
505                    DiagnosticCategory::ResourceLimit,
506                    "migration_assertion_role_player_limit",
507                    "links pattern has no players or exceeds the term ceiling",
508                ));
509            }
510            for player in players {
511                check_binding(player.player(), binding_count)?;
512            }
513            Ok(())
514        }
515        AssertionPattern::Value { left, right, .. } => {
516            check_operand(left, binding_count)?;
517            check_operand(right, binding_count)
518        }
519        AssertionPattern::Not { patterns } => {
520            if patterns.is_empty() || patterns.len() > limits.boolean_terms {
521                return Err(assertion_failure(
522                    DiagnosticCategory::ResourceLimit,
523                    "migration_assertion_negation_term_limit",
524                    "negation is empty or exceeds the boolean-term ceiling",
525                ));
526            }
527            for child in patterns {
528                inspect_pattern(child, depth + 1, binding_count, limits, stats)?;
529            }
530            Ok(())
531        }
532    }
533}
534
535fn check_operand(operand: &ValueOperand, binding_count: usize) -> Result<(), Diagnostic> {
536    match operand {
537        ValueOperand::Binding { binding } => check_binding(*binding, binding_count),
538        ValueOperand::Literal { .. } => Ok(()),
539    }
540}
541
542fn check_binding(binding: BindingId, binding_count: usize) -> Result<(), Diagnostic> {
543    if usize::from(binding.get()) < binding_count {
544        Ok(())
545    } else {
546        Err(assertion_failure(
547            DiagnosticCategory::InvalidContract,
548            "migration_assertion_unknown_binding",
549            "assertion pattern references an undeclared binding",
550        ))
551    }
552}
553
554fn canonical_binding_set(
555    bindings: &mut [BindingId],
556    binding_count: usize,
557    kind: &'static str,
558) -> Result<(), Diagnostic> {
559    bindings.sort();
560    if bindings.windows(2).any(|pair| pair[0] == pair[1]) {
561        return Err(assertion_failure(
562            DiagnosticCategory::InvalidContract,
563            "migration_assertion_duplicate_binding_set_member",
564            kind,
565        ));
566    }
567    for binding in bindings.iter().copied() {
568        check_binding(binding, binding_count)?;
569    }
570    Ok(())
571}
572
573fn derive_capabilities(patterns: &[AssertionPattern]) -> Result<CapabilitySet, Diagnostic> {
574    let mut capabilities = CapabilitySet::new();
575    insert_capability(&mut capabilities, CAP_ASSERTION)?;
576    for pattern in patterns {
577        collect_capabilities(pattern, &mut capabilities)?;
578    }
579    Ok(capabilities)
580}
581
582fn collect_capabilities(
583    pattern: &AssertionPattern,
584    capabilities: &mut CapabilitySet,
585) -> Result<(), Diagnostic> {
586    match pattern {
587        AssertionPattern::Isa {
588            include_subtypes, ..
589        } => {
590            insert_capability(capabilities, CAP_ISA)?;
591            if *include_subtypes {
592                insert_capability(capabilities, CAP_ISA_SUBTYPES)?;
593            }
594        }
595        AssertionPattern::Has { .. } => insert_capability(capabilities, CAP_HAS)?,
596        AssertionPattern::Links { .. } => insert_capability(capabilities, CAP_LINKS)?,
597        AssertionPattern::Value { .. } => insert_capability(capabilities, CAP_VALUE)?,
598        AssertionPattern::Not { patterns } => {
599            insert_capability(capabilities, CAP_NEGATION)?;
600            for child in patterns {
601                collect_capabilities(child, capabilities)?;
602            }
603        }
604    }
605    Ok(())
606}
607
608fn insert_capability(
609    capabilities: &mut CapabilitySet,
610    value: &'static str,
611) -> Result<(), Diagnostic> {
612    capabilities.insert(CapabilityId::new(value)?);
613    Ok(())
614}
615
616pub(crate) fn assertion_failure(
617    category: DiagnosticCategory,
618    code: &'static str,
619    message: &'static str,
620) -> Diagnostic {
621    Diagnostic::new(
622        category,
623        DiagnosticCode::new(code).expect("static assertion diagnostic code is canonical"),
624        message,
625    )
626}