maskforge_core/diagnostics.rs
1//! The provenance sidecar. A `Diagnostic` records where and why an occurrence was rejected;
2//! it is keyed by occurrence and never enters the semantic hash.
3
4use bincode::{Decode, Encode};
5
6use crate::ir::StrRef;
7use crate::primitives::NodeId;
8
9/// Why a keyword or shape is not accepted by the regular-subset engine. The discriminant is the
10/// stable machine code (folded into the content hash and pinned by a wire test); variants are
11/// append-only. `advisory` is display-only.
12#[non_exhaustive]
13#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Encode, Decode)]
14pub enum UnsupportedReason {
15 /// A keyword with no supported arm in this dialect.
16 UnsupportedKeyword = 0,
17 /// A `type` union or array of types (never narrowed to one).
18 UnionType = 1,
19 /// An object whose additional properties are open (absent or a non-`false` schema).
20 OpenAdditionalProperties = 2,
21 /// `additionalProperties` given as a subschema rather than `false`.
22 AdditionalPropertiesSchema = 3,
23 /// A cycle in the `$ref` graph (needs a pushdown grammar).
24 RecursiveReference = 4,
25 /// A `$ref` to an external document or the network.
26 NonRegularRef = 5,
27 /// A regex construct the anchored byte engine cannot accept.
28 InvalidPattern = 6,
29 /// `type:number`, `multipleOf`, or a numeric bound outside the supported integer subset.
30 NumericRangeUnsupported = 7,
31 /// `uniqueItems` (set distinctness is context-sensitive).
32 SetDistinctness = 8,
33 /// `oneOf` (exactly-one is context-sensitive; never downgraded to a union).
34 ExactlyOne = 9,
35 /// `not` (complement).
36 Complement = 10,
37 /// `if`/`then`/`else` or `dependentSchemas` (conditional applicator).
38 ConditionalApplicator = 11,
39 /// `dependentRequired` (a deferred obligation).
40 DeferredObligation = 12,
41 /// A keyword whose meaning depends on collected annotations.
42 AnnotationDependent = 13,
43 /// `$dynamicRef`/`$dynamicAnchor` (dynamic scope).
44 DynamicScope = 14,
45 /// A non-scalar `const`/`enum` member.
46 NonScalarLiteral = 15,
47 /// A length-bounded string that also carries a pattern or a non-ASCII body.
48 LengthWithPattern = 16,
49 /// A `format` this dialect does not enforce even under `format_assertion`.
50 FormatUnsupported = 17,
51 /// A `format` combined with `pattern`, `minLength`, or `maxLength` on the same string.
52 FormatWithPattern = 18,
53 /// A `const`/`enum` numeric member whose lexical form is not a plain JSON integer token
54 /// (a fraction, exponent, or magnitude outside `i64`), e.g. `1.0`, `1e0`.
55 NonIntegerNumericLiteral = 19,
56 /// `allOf` (language intersection needs a product-automaton construction this dialect does not
57 /// build; never approximated as a union or a regex concatenation).
58 AllOfIntersectionUnsupported = 20,
59 /// A `minimum`/`maximum` bound on `type:number`. The engine does not build a decimal-comparison
60 /// automaton for a bounded real range under the current profile, so only unbounded `type:number`
61 /// is supported. This is a current-implementation limit, not a proof of non-representability.
62 NumberBoundUnsupported = 21,
63 /// Unconstrained array items (bare `prefixItems`, `items:true`, or no item schema): they admit
64 /// arbitrary-depth JSON, which is not regular. A regular-item array or `items:false` is supported.
65 OpenArrayTail = 22,
66 /// An `OpenObject` reached the byte-DFA compiler; it needs the structured backend instead.
67 StructuredBackendRequired = 23,
68 /// A look-around assertion is outside the supported anchored regular subset.
69 RegexAssertionUnsupported = 24,
70 /// A numeric or named regex backreference is not compiled as an automaton.
71 RegexBackreferenceUnsupported = 25,
72}
73
74impl UnsupportedReason {
75 /// A short, display-only description. Not the machine contract (the discriminant is).
76 #[must_use]
77 pub fn advisory(self) -> &'static str {
78 match self {
79 Self::UnsupportedKeyword => "keyword is not supported in this dialect",
80 Self::UnionType => "a type union is never narrowed; declare one type",
81 Self::OpenAdditionalProperties => {
82 "object has open additionalProperties; a closed-object profile is required"
83 }
84 Self::AdditionalPropertiesSchema => {
85 "additionalProperties as a subschema is not supported"
86 }
87 Self::RecursiveReference => "a cyclic reference needs a pushdown grammar",
88 Self::NonRegularRef => "external or network references are not supported",
89 Self::InvalidPattern => "the regex uses a construct the byte engine cannot accept",
90 Self::NumericRangeUnsupported => {
91 "only signed 64-bit integers with integer bounds are supported"
92 }
93 Self::SetDistinctness => "uniqueItems is context-sensitive",
94 Self::ExactlyOne => "oneOf is context-sensitive and is not downgraded to a union",
95 Self::Complement => "not is context-sensitive",
96 Self::ConditionalApplicator => "conditional applicators are not supported",
97 Self::DeferredObligation => "dependentRequired is not supported",
98 Self::AnnotationDependent => "the keyword depends on collected annotations",
99 Self::DynamicScope => "dynamic scope resolution is not supported",
100 Self::NonScalarLiteral => "a const/enum member must be a scalar",
101 Self::LengthWithPattern => {
102 "a length-bounded string with a pattern or non-ASCII body is not supported"
103 }
104 Self::FormatUnsupported => "this format is not enforced",
105 Self::FormatWithPattern => {
106 "format combined with pattern or a length bound is not supported"
107 }
108 Self::NonIntegerNumericLiteral => "not a plain JSON integer token (fraction/exponent)",
109 Self::AllOfIntersectionUnsupported => {
110 "allOf is a language intersection, which needs a product-automaton construction \
111 this dialect does not build; never approximated as a union or concatenation"
112 }
113 Self::NumberBoundUnsupported => {
114 "a bounded real-number range is not implemented; only unbounded type:number \
115 is supported"
116 }
117 Self::OpenArrayTail => {
118 "unconstrained trailing items admit arbitrary-depth JSON (not regular); use \
119 items:false or a schema-valued items to close the array"
120 }
121 Self::StructuredBackendRequired => {
122 "this schema needs the structured backend, not the byte-DFA compiler: check \
123 SchemaIR::requires_structured_backend() before calling compile_ir, or use a \
124 caller that already dispatches automatically (Python: ConstraintSession.from_json_schema)"
125 }
126 Self::RegexAssertionUnsupported => {
127 "the regex assertion is outside the supported anchored regular subset"
128 }
129 Self::RegexBackreferenceUnsupported => {
130 "regex backreferences are not supported by the automaton compiler"
131 }
132 }
133 }
134}
135
136/// A byte span into the original source text, when the frontend tracks one.
137#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Encode, Decode)]
138pub struct Span {
139 /// Byte offset of the span start.
140 pub start: u32,
141 /// Byte offset one past the span end.
142 pub end: u32,
143}
144
145/// One rejected occurrence: the node it produced, the keyword, its RFC 6901 pointer, and the
146/// machine reason. Keyed by occurrence; never hashed into the content address.
147#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)]
148pub struct Diagnostic {
149 /// The `Unsupported` node this occurrence lowered to.
150 pub node: NodeId,
151 /// The offending keyword, as an interned string reference.
152 pub keyword: StrRef,
153 /// The RFC 6901 pointer to the occurrence, as an interned string reference.
154 pub json_pointer: StrRef,
155 /// The source span, when the frontend tracked one.
156 pub source_span: Option<Span>,
157 /// The machine-stable reason.
158 pub reason: UnsupportedReason,
159}