1use 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
17pub const MIGRATION_ASSERTION_FINGERPRINT_DOMAIN: &str = "typebridge.query.migration-assertion";
19pub 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
30pub 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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
50#[serde(transparent)]
51pub struct BindingId(u16);
52
53impl BindingId {
54 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 pub const fn get(self) -> u16 {
69 self.0
70 }
71}
72
73#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
75pub struct QueryVariable(String);
76
77impl QueryVariable {
78 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 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#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
120pub struct AssertionBinding {
121 id: BindingId,
122 variable: QueryVariable,
123}
124
125impl AssertionBinding {
126 pub const fn new(id: BindingId, variable: QueryVariable) -> Self {
128 Self { id, variable }
129 }
130
131 pub const fn id(&self) -> BindingId {
133 self.id
134 }
135
136 pub const fn variable(&self) -> &QueryVariable {
138 &self.variable
139 }
140}
141
142#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
144pub struct AssertionRolePlayer {
145 player: BindingId,
146 role: RoleId,
147}
148
149impl AssertionRolePlayer {
150 pub const fn new(role: RoleId, player: BindingId) -> Self {
152 Self { player, role }
153 }
154
155 pub const fn role(&self) -> &RoleId {
157 &self.role
158 }
159
160 pub const fn player(&self) -> BindingId {
162 self.player
163 }
164}
165
166#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
168#[serde(rename_all = "snake_case")]
169pub enum ValueComparator {
170 Equal,
172 NotEqual,
174 Less,
176 LessOrEqual,
178 Greater,
180 GreaterOrEqual,
182}
183
184#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
186#[serde(tag = "kind", rename_all = "snake_case")]
187pub enum ValueOperand {
188 Binding {
190 binding: BindingId,
192 },
193 Literal {
195 value: CanonicalValue,
197 },
198}
199
200impl ValueOperand {
201 pub const fn binding(binding: BindingId) -> Self {
203 Self::Binding { binding }
204 }
205
206 pub const fn literal(value: CanonicalValue) -> Self {
208 Self::Literal { value }
209 }
210}
211
212#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
214#[serde(tag = "kind", rename_all = "snake_case")]
215pub enum AssertionPattern {
216 Isa {
218 binding: BindingId,
220 include_subtypes: bool,
222 type_id: TypeId,
224 },
225 Has {
227 attribute: BindingId,
229 attribute_id: AttributeId,
231 owner: BindingId,
233 },
234 Links {
236 players: Vec<AssertionRolePlayer>,
238 relation: BindingId,
240 relation_id: TypeId,
242 },
243 Value {
245 comparator: ValueComparator,
247 left: ValueOperand,
249 right: ValueOperand,
251 },
252 Not {
254 patterns: Vec<AssertionPattern>,
256 },
257}
258
259#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
261#[serde(rename_all = "snake_case")]
262pub enum AssertionExpectation {
263 NoRows,
265}
266
267#[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 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 pub const fn format(&self) -> FormatVersion {
376 self.format
377 }
378
379 pub const fn managed_semantics(&self) -> &ManagedSemanticSchemaFingerprint {
381 &self.managed_semantics
382 }
383
384 pub fn bindings(&self) -> &[AssertionBinding] {
386 &self.bindings
387 }
388
389 pub fn binding(&self, id: BindingId) -> Option<&AssertionBinding> {
391 self.bindings.get(usize::from(id.get()))
392 }
393
394 pub fn patterns(&self) -> &[AssertionPattern] {
396 &self.patterns
397 }
398
399 pub fn outputs(&self) -> &[BindingId] {
401 &self.outputs
402 }
403
404 pub fn witnesses(&self) -> &[BindingId] {
406 &self.witnesses
407 }
408
409 pub const fn expectation(&self) -> AssertionExpectation {
411 self.expectation
412 }
413
414 pub const fn required_capabilities(&self) -> &CapabilitySet {
416 &self.required_capabilities
417 }
418
419 pub fn canonical_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
421 encode_migration_assertion_plan(self)
422 }
423
424 pub fn fingerprint(&self) -> Result<MigrationAssertionPlanFingerprint, Diagnostic> {
426 MigrationAssertionPlanFingerprint::compute(self)
427 }
428}
429
430#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
432#[serde(transparent)]
433pub struct MigrationAssertionPlanFingerprint(Fingerprint);
434
435impl MigrationAssertionPlanFingerprint {
436 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 pub const fn as_fingerprint(&self) -> &Fingerprint {
448 &self.0
449 }
450}
451
452pub fn encode_migration_assertion_plan(
454 plan: &MigrationAssertionPlan,
455) -> Result<Vec<u8>, Diagnostic> {
456 to_canonical_json(plan)
457}
458
459pub 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}