Skip to main content

omena_evidence_graph/
lib.rs

1//! Typed evidence graph and guarantee-family contracts shared by Omena products.
2
3use serde::{Deserialize, Serialize};
4#[cfg(any(test, feature = "test-support"))]
5use std::cell::Cell;
6use std::collections::{BTreeMap, BTreeSet};
7
8pub const EVIDENCE_GRAPH_SCHEMA_VERSION_V0: &str = "0";
9pub const EVIDENCE_GRAPH_PRODUCT_V0: &str = "omena-evidence-graph.graph";
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub enum GuaranteeKindV0 {
14    Floor,
15    SampledFixtureWitness,
16    SchedulerPriorityFixtureWitness,
17    MetricInputFixtureWitness,
18    IncrementalLayerEvidenceOnly,
19    AlphaRenamingStableHashFixtureWitness,
20    NotClaimedExactTraversal,
21}
22
23impl GuaranteeKindV0 {
24    pub const fn for_label_less_family() -> Self {
25        Self::Floor
26    }
27
28    pub const fn existing_label(self) -> Option<&'static str> {
29        match self {
30            Self::Floor => None,
31            Self::SampledFixtureWitness => Some("sampledFixtureWitnessNotEquivalenceProof"),
32            Self::SchedulerPriorityFixtureWitness => Some("fixtureWitnessSchedulerPriority"),
33            Self::MetricInputFixtureWitness => Some("fixtureWitnessMetricInput"),
34            Self::IncrementalLayerEvidenceOnly => Some("m6IncrementalLayerEvidenceOnly"),
35            Self::AlphaRenamingStableHashFixtureWitness => {
36                Some("fixtureWitnessAlphaRenamingStableHash")
37            }
38            Self::NotClaimedExactTraversal => Some("notClaimedExactTraversal"),
39        }
40    }
41
42    pub fn from_existing_label(label: &str) -> Option<Self> {
43        match label {
44            "sampledFixtureWitnessNotEquivalenceProof" => Some(Self::SampledFixtureWitness),
45            "fixtureWitnessSchedulerPriority" => Some(Self::SchedulerPriorityFixtureWitness),
46            "fixtureWitnessMetricInput" => Some(Self::MetricInputFixtureWitness),
47            "m6IncrementalLayerEvidenceOnly" => Some(Self::IncrementalLayerEvidenceOnly),
48            "fixtureWitnessAlphaRenamingStableHash" => {
49                Some(Self::AlphaRenamingStableHashFixtureWitness)
50            }
51            "notClaimedExactTraversal" => Some(Self::NotClaimedExactTraversal),
52            _ => None,
53        }
54    }
55}
56
57/// `GuaranteeKindV0` records what a node guarantees; this records how that
58/// guarantee was earned.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase")]
61pub enum GuaranteeFamilyV0 {
62    ByteIdentityOracle,
63    ExternalReplicaDifferential,
64    ExternalTool,
65    PropertyCorpusWitness,
66    TypedInvariantWitness,
67    ProseObligationDischarged,
68    FloorAssumption,
69    LedgerBackedObligationDischarge,
70    SampledFixtureCorpus,
71}
72
73impl GuaranteeFamilyV0 {
74    pub const fn describe(self) -> &'static str {
75        match self {
76            Self::ByteIdentityOracle => "byteIdentityOracle",
77            Self::ExternalReplicaDifferential => "externalReplicaDifferential",
78            Self::ExternalTool => "externalTool",
79            Self::PropertyCorpusWitness => "propertyCorpusWitness",
80            Self::TypedInvariantWitness => "typedInvariantWitness",
81            Self::ProseObligationDischarged => "proseObligationDischarged",
82            Self::FloorAssumption => "floorAssumption",
83            Self::LedgerBackedObligationDischarge => "ledgerBackedObligationDischarge",
84            Self::SampledFixtureCorpus => "sampledFixtureCorpus",
85        }
86    }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90struct FamilyStampSealV0(());
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub struct ByteIdentityOracleTokenV0(FamilyStampSealV0);
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub struct ExternalReplicaDifferentialTokenV0(FamilyStampSealV0);
97
98/// Facts identifying one external tool invocation without assigning a verdict
99/// to the tool's output.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct ExternalToolRunWitnessV0 {
102    pub tool_name: String,
103    pub tool_version: String,
104    pub input_digest: String,
105    pub exit_status: i32,
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub struct PropertyCorpusWitnessTokenV0(FamilyStampSealV0);
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub struct PropertyCorpusWitnessEvidenceV0 {
113    pub record_count: usize,
114    pub measured_comparison_count: usize,
115    pub all_records_have_one_verdict: bool,
116    pub all_oracle_baselines_match: bool,
117    pub all_verdicts_match_measurements: bool,
118    pub all_divergences_reasoned: bool,
119    pub all_passes_accounted_for: bool,
120    pub all_families_non_vacuous_or_named_gap: bool,
121}
122
123impl PropertyCorpusWitnessTokenV0 {
124    pub fn from_conformance_ledger(evidence: PropertyCorpusWitnessEvidenceV0) -> Option<Self> {
125        (evidence.record_count > 0
126            && evidence.measured_comparison_count > 0
127            && evidence.all_records_have_one_verdict
128            && evidence.all_oracle_baselines_match
129            && evidence.all_verdicts_match_measurements
130            && evidence.all_divergences_reasoned
131            && evidence.all_passes_accounted_for
132            && evidence.all_families_non_vacuous_or_named_gap)
133            .then_some(Self(FamilyStampSealV0(())))
134    }
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub struct SampledFixtureCorpusTokenV0(FamilyStampSealV0);
139
140impl SampledFixtureCorpusTokenV0 {
141    pub fn from_matching_string_vectors(
142        sampled_values: &[String],
143        control_values: &[String],
144        reported_match: bool,
145    ) -> Option<Self> {
146        let recomputed_match = sampled_values == control_values;
147        (!sampled_values.is_empty()
148            && !control_values.is_empty()
149            && recomputed_match
150            && reported_match == recomputed_match)
151            .then_some(Self(FamilyStampSealV0(())))
152    }
153}
154
155/// Reserved for evidence produced by a real typed invariant.
156///
157/// Incremental fixture evidence cannot mint this token.
158///
159/// ```compile_fail
160/// use omena_evidence_graph::TypedInvariantWitnessTokenV0;
161///
162/// let _ = TypedInvariantWitnessTokenV0::from_incremental_layer_evidence();
163/// ```
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub struct TypedInvariantWitnessTokenV0(FamilyStampSealV0);
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub struct ProseObligationProvenanceV0(FamilyStampSealV0);
169
170impl ProseObligationProvenanceV0 {
171    pub fn from_provenance_labels(labels: &[String]) -> Option<Self> {
172        labels
173            .iter()
174            .any(|label| {
175                label.starts_with("obligation:")
176                    || label.starts_with("cascadeObligationDeclared:")
177                    || label.starts_with("enforcedAt:")
178                    || label.starts_with("primitive:")
179            })
180            .then_some(Self(FamilyStampSealV0(())))
181    }
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub struct LedgerDischargeWitnessV0(FamilyStampSealV0);
186
187impl LedgerDischargeWitnessV0 {
188    pub fn from_discharge_cell_key_v0(cell_key: &str) -> Option<Self> {
189        (cell_key.len() == 64 && cell_key.bytes().all(|byte| byte.is_ascii_hexdigit()))
190            .then_some(Self(FamilyStampSealV0(())))
191    }
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub struct FamilyStampV0 {
196    earned_via: GuaranteeFamilyV0,
197    _seal: FamilyStampSealV0,
198}
199
200impl FamilyStampV0 {
201    pub const fn floor_assumption() -> Self {
202        Self::from_family(GuaranteeFamilyV0::FloorAssumption)
203    }
204
205    pub const fn byte_identity_oracle(_token: &ByteIdentityOracleTokenV0) -> Self {
206        Self::from_family(GuaranteeFamilyV0::ByteIdentityOracle)
207    }
208
209    pub const fn external_replica_differential(
210        _token: &ExternalReplicaDifferentialTokenV0,
211    ) -> Self {
212        Self::from_family(GuaranteeFamilyV0::ExternalReplicaDifferential)
213    }
214
215    /// Records that an external tool invocation occurred.
216    ///
217    /// The witness is mandatory because the family records execution facts,
218    /// not an independent correctness verdict.
219    ///
220    /// ```compile_fail
221    /// use omena_evidence_graph::FamilyStampV0;
222    ///
223    /// let _ = FamilyStampV0::external_tool();
224    /// ```
225    pub const fn external_tool(_witness: &ExternalToolRunWitnessV0) -> Self {
226        Self::from_family(GuaranteeFamilyV0::ExternalTool)
227    }
228
229    pub const fn property_corpus_witness(_token: &PropertyCorpusWitnessTokenV0) -> Self {
230        Self::from_family(GuaranteeFamilyV0::PropertyCorpusWitness)
231    }
232
233    pub const fn sampled_fixture_corpus(_token: &SampledFixtureCorpusTokenV0) -> Self {
234        Self::from_family(GuaranteeFamilyV0::SampledFixtureCorpus)
235    }
236
237    pub const fn typed_invariant_witness(_token: &TypedInvariantWitnessTokenV0) -> Self {
238        Self::from_family(GuaranteeFamilyV0::TypedInvariantWitness)
239    }
240
241    pub const fn prose_obligation_discharged(_provenance: &ProseObligationProvenanceV0) -> Self {
242        Self::from_family(GuaranteeFamilyV0::ProseObligationDischarged)
243    }
244
245    pub const fn ledger_backed_obligation_discharge(_witness: &LedgerDischargeWitnessV0) -> Self {
246        Self::from_family(GuaranteeFamilyV0::LedgerBackedObligationDischarge)
247    }
248
249    pub const fn earned_via(self) -> GuaranteeFamilyV0 {
250        self.earned_via
251    }
252
253    const fn from_family(earned_via: GuaranteeFamilyV0) -> Self {
254        Self {
255            earned_via,
256            _seal: FamilyStampSealV0(()),
257        }
258    }
259}
260
261#[cfg(any(test, feature = "test-support"))]
262thread_local! {
263    static EARNED_GUARANTEE_FAMILY_READS_V0: Cell<u64> = const { Cell::new(0) };
264}
265
266#[cfg(any(test, feature = "test-support"))]
267pub fn reset_earned_guarantee_family_read_count_v0() {
268    EARNED_GUARANTEE_FAMILY_READS_V0.with(|count| count.set(0));
269}
270
271#[cfg(any(test, feature = "test-support"))]
272pub fn earned_guarantee_family_read_count_v0() -> u64 {
273    EARNED_GUARANTEE_FAMILY_READS_V0.with(Cell::get)
274}
275
276pub const REWRITE_OBLIGATION_FAMILY_PRODUCT_V0: &str =
277    "omena-evidence-graph.rewrite-obligation-family-closure";
278pub const REWRITE_OBLIGATION_FAMILY_COUNT_V0: usize = 45;
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
281#[serde(rename_all = "camelCase")]
282pub enum ObligationFamilyIdV0 {
283    CascadeSafetyFloor,
284    CascadeObligationDeclaration,
285    ComputedValuePreservation,
286    WhitespaceBoundary,
287    CommentSourceMapProvenance,
288    NumericLiteralEquivalence,
289    DimensionComputedValue,
290    ColorLiteralEquivalence,
291    UrlTokenGrammar,
292    StringTextAndFontValue,
293    SelectorSpecificityAndCascade,
294    LonghandShorthandCascadeOutcome,
295    DeclarationCascadeOrder,
296    RuleMergeWinnerOrder,
297    SelectorIdentityAndModuleSemantics,
298    SemanticMarkerRetention,
299    TargetPrefixAddition,
300    StalePrefixRemovalMapping,
301    TargetFallbackBranch,
302    ColorSpaceTargetEquivalence,
303    TargetColorPrecision,
304    DirectionalityOption,
305    NestedSelectorSpecificity,
306    ScopedMatching,
307    LayerOrderComparison,
308    TargetFeaturePredicate,
309    MediaPredicate,
310    ContainerPredicate,
311    NativeCssStaticValue,
312    CalcExpressionEquivalence,
313    ImportWrapperProvenance,
314    ScssNamespaceProvenance,
315    LessNamespaceProvenance,
316    SelectorIdentityMap,
317    ComposedClassProvenance,
318    ValueGraphResolution,
319    CustomPropertyFixedPoint,
320    SourceClassReachability,
321    AnimationNameReachability,
322    ValueGraphReachability,
323    VarReachability,
324    DeadMediaWitness,
325    DeadSupportsWitness,
326    DesignTokenPackageProvenance,
327    SourceMapTransformTrace,
328}
329
330impl ObligationFamilyIdV0 {
331    pub const fn as_str(self) -> &'static str {
332        match self {
333            Self::CascadeSafetyFloor => "cascadeSafetyFloor",
334            Self::CascadeObligationDeclaration => "cascadeObligationDeclaration",
335            Self::ComputedValuePreservation => "computedValuePreservation",
336            Self::WhitespaceBoundary => "whitespaceBoundary",
337            Self::CommentSourceMapProvenance => "commentSourceMapProvenance",
338            Self::NumericLiteralEquivalence => "numericLiteralEquivalence",
339            Self::DimensionComputedValue => "dimensionComputedValue",
340            Self::ColorLiteralEquivalence => "colorLiteralEquivalence",
341            Self::UrlTokenGrammar => "urlTokenGrammar",
342            Self::StringTextAndFontValue => "stringTextAndFontValue",
343            Self::SelectorSpecificityAndCascade => "selectorSpecificityAndCascade",
344            Self::LonghandShorthandCascadeOutcome => "longhandShorthandCascadeOutcome",
345            Self::DeclarationCascadeOrder => "declarationCascadeOrder",
346            Self::RuleMergeWinnerOrder => "ruleMergeWinnerOrder",
347            Self::SelectorIdentityAndModuleSemantics => "selectorIdentityAndModuleSemantics",
348            Self::SemanticMarkerRetention => "semanticMarkerRetention",
349            Self::TargetPrefixAddition => "targetPrefixAddition",
350            Self::StalePrefixRemovalMapping => "stalePrefixRemovalMapping",
351            Self::TargetFallbackBranch => "targetFallbackBranch",
352            Self::ColorSpaceTargetEquivalence => "colorSpaceTargetEquivalence",
353            Self::TargetColorPrecision => "targetColorPrecision",
354            Self::DirectionalityOption => "directionalityOption",
355            Self::NestedSelectorSpecificity => "nestedSelectorSpecificity",
356            Self::ScopedMatching => "scopedMatching",
357            Self::LayerOrderComparison => "layerOrderComparison",
358            Self::TargetFeaturePredicate => "targetFeaturePredicate",
359            Self::MediaPredicate => "mediaPredicate",
360            Self::ContainerPredicate => "containerPredicate",
361            Self::NativeCssStaticValue => "nativeCssStaticValue",
362            Self::CalcExpressionEquivalence => "calcExpressionEquivalence",
363            Self::ImportWrapperProvenance => "importWrapperProvenance",
364            Self::ScssNamespaceProvenance => "scssNamespaceProvenance",
365            Self::LessNamespaceProvenance => "lessNamespaceProvenance",
366            Self::SelectorIdentityMap => "selectorIdentityMap",
367            Self::ComposedClassProvenance => "composedClassProvenance",
368            Self::ValueGraphResolution => "valueGraphResolution",
369            Self::CustomPropertyFixedPoint => "customPropertyFixedPoint",
370            Self::SourceClassReachability => "sourceClassReachability",
371            Self::AnimationNameReachability => "animationNameReachability",
372            Self::ValueGraphReachability => "valueGraphReachability",
373            Self::VarReachability => "varReachability",
374            Self::DeadMediaWitness => "deadMediaWitness",
375            Self::DeadSupportsWitness => "deadSupportsWitness",
376            Self::DesignTokenPackageProvenance => "designTokenPackageProvenance",
377            Self::SourceMapTransformTrace => "sourceMapTransformTrace",
378        }
379    }
380
381    pub const fn descriptor(self) -> RewriteObligationFamilyDescriptorV0 {
382        match self {
383            Self::CascadeSafetyFloor => {
384                descriptor(self, "", GuaranteeKindV0::for_label_less_family())
385            }
386            Self::CascadeObligationDeclaration => descriptor(
387                self,
388                "must declare the rewrite-safety obligation family before cascade-sensitive rewrite evidence is emitted",
389                GuaranteeKindV0::for_label_less_family(),
390            ),
391            Self::ComputedValuePreservation => descriptor(
392                self,
393                "must preserve computed value semantics when a rewrite candidate claims computed-value preservation",
394                GuaranteeKindV0::for_label_less_family(),
395            ),
396            Self::WhitespaceBoundary => descriptor(
397                self,
398                "may remove only whitespace outside string, url, attr, and calc-sensitive token boundaries",
399                GuaranteeKindV0::for_label_less_family(),
400            ),
401            Self::CommentSourceMapProvenance => descriptor(
402                self,
403                "may remove comments only when source-map provenance preserves the removed span",
404                GuaranteeKindV0::for_label_less_family(),
405            ),
406            Self::NumericLiteralEquivalence => descriptor(
407                self,
408                "may rewrite only numerically equivalent literal tokens",
409                GuaranteeKindV0::for_label_less_family(),
410            ),
411            Self::DimensionComputedValue => descriptor(
412                self,
413                "may normalize only dimension values whose computed value is unchanged",
414                GuaranteeKindV0::for_label_less_family(),
415            ),
416            Self::ColorLiteralEquivalence => descriptor(
417                self,
418                "may rewrite only color-equivalent literal tokens",
419                GuaranteeKindV0::for_label_less_family(),
420            ),
421            Self::UrlTokenGrammar => descriptor(
422                self,
423                "may remove url quotes only when the unquoted token grammar remains equivalent",
424                GuaranteeKindV0::for_label_less_family(),
425            ),
426            Self::StringTextAndFontValue => descriptor(
427                self,
428                "may normalize string quotes and font keyword aliases only when computed text and font values remain equivalent",
429                GuaranteeKindV0::for_label_less_family(),
430            ),
431            Self::SelectorSpecificityAndCascade => descriptor(
432                self,
433                "must preserve selector specificity, keyframe timeline positions, and matching semantics under the cascade model",
434                GuaranteeKindV0::for_label_less_family(),
435            ),
436            Self::LonghandShorthandCascadeOutcome => descriptor(
437                self,
438                "must prove longhand and shorthand cascade outcomes are equivalent",
439                GuaranteeKindV0::for_label_less_family(),
440            ),
441            Self::DeclarationCascadeOrder => descriptor(
442                self,
443                "must preserve origin, layer, specificity, and order for every surviving declaration",
444                GuaranteeKindV0::for_label_less_family(),
445            ),
446            Self::RuleMergeWinnerOrder => descriptor(
447                self,
448                "must prove merged rule order cannot change declaration winners",
449                GuaranteeKindV0::for_label_less_family(),
450            ),
451            Self::SelectorIdentityAndModuleSemantics => descriptor(
452                self,
453                "must preserve selector identity and post-hash module semantics",
454                GuaranteeKindV0::for_label_less_family(),
455            ),
456            Self::SemanticMarkerRetention => descriptor(
457                self,
458                "may remove rules only when no source-visible semantic marker is attached",
459                GuaranteeKindV0::for_label_less_family(),
460            ),
461            Self::TargetPrefixAddition => descriptor(
462                self,
463                "must add target-required prefixed declarations without changing modern target outcomes",
464                GuaranteeKindV0::for_label_less_family(),
465            ),
466            Self::StalePrefixRemovalMapping => descriptor(
467                self,
468                "may remove prefixed declarations only when an explicit mapping and exact unprefixed peer prove the prefix stale",
469                GuaranteeKindV0::for_label_less_family(),
470            ),
471            Self::TargetFallbackBranch => descriptor(
472                self,
473                "must lower only when target data requires fallback branches and provenance tracks both branches",
474                GuaranteeKindV0::for_label_less_family(),
475            ),
476            Self::ColorSpaceTargetEquivalence => descriptor(
477                self,
478                "must lower only when color-space conversion is target-equivalent",
479                GuaranteeKindV0::for_label_less_family(),
480            ),
481            Self::TargetColorPrecision => descriptor(
482                self,
483                "must preserve color semantics within the configured target fallback precision",
484                GuaranteeKindV0::for_label_less_family(),
485            ),
486            Self::DirectionalityOption => descriptor(
487                self,
488                "must run only under explicit directionality options",
489                GuaranteeKindV0::for_label_less_family(),
490            ),
491            Self::NestedSelectorSpecificity => descriptor(
492                self,
493                "must preserve nested selector expansion and specificity",
494                GuaranteeKindV0::for_label_less_family(),
495            ),
496            Self::ScopedMatching => descriptor(
497                self,
498                "must preserve scoped matching semantics or emit a blocked result",
499                GuaranteeKindV0::for_label_less_family(),
500            ),
501            Self::LayerOrderComparison => descriptor(
502                self,
503                "must preserve layer order in CascadeKey comparison",
504                GuaranteeKindV0::for_label_less_family(),
505            ),
506            Self::TargetFeaturePredicate => descriptor(
507                self,
508                "may remove branches only when the target feature predicate is known",
509                GuaranteeKindV0::for_label_less_family(),
510            ),
511            Self::MediaPredicate => descriptor(
512                self,
513                "may remove branches only when the configured media predicate is known",
514                GuaranteeKindV0::for_label_less_family(),
515            ),
516            Self::ContainerPredicate => descriptor(
517                self,
518                "may remove @container branches only when the size condition is provably unsatisfiable regardless of container context",
519                GuaranteeKindV0::for_label_less_family(),
520            ),
521            Self::NativeCssStaticValue => descriptor(
522                self,
523                "may fold native CSS if() and function calls only when the evaluator proves a concrete static value and preserves runtime-dependent constructs verbatim",
524                GuaranteeKindV0::for_label_less_family(),
525            ),
526            Self::CalcExpressionEquivalence => descriptor(
527                self,
528                "may reduce only syntax-equivalent or computed-value-equivalent calc expressions",
529                GuaranteeKindV0::for_label_less_family(),
530            ),
531            Self::ImportWrapperProvenance => descriptor(
532                self,
533                "must preserve import-site media, supports, layer wrappers, and source provenance",
534                GuaranteeKindV0::for_label_less_family(),
535            ),
536            Self::ScssNamespaceProvenance => descriptor(
537                self,
538                "must preserve SCSS namespace, show/hide, mixin, variable, and source provenance facts",
539                GuaranteeKindV0::for_label_less_family(),
540            ),
541            Self::LessNamespaceProvenance => descriptor(
542                self,
543                "must preserve Less variable, mixin, namespace, and source provenance facts",
544                GuaranteeKindV0::for_label_less_family(),
545            ),
546            Self::SelectorIdentityMap => descriptor(
547                self,
548                "must rewrite every source and style reference through the same selector identity map",
549                GuaranteeKindV0::for_label_less_family(),
550            ),
551            Self::ComposedClassProvenance => descriptor(
552                self,
553                "must preserve exported class set and composed class provenance",
554                GuaranteeKindV0::for_label_less_family(),
555            ),
556            Self::ValueGraphResolution => descriptor(
557                self,
558                "must preserve @value graph resolution and cycle diagnostics",
559                GuaranteeKindV0::for_label_less_family(),
560            ),
561            Self::CustomPropertyFixedPoint => descriptor(
562                self,
563                "must preserve custom-property fixed-point semantics or emit a provenance-backed blocked result",
564                GuaranteeKindV0::for_label_less_family(),
565            ),
566            Self::SourceClassReachability => descriptor(
567                self,
568                "may remove classes only when bridge reachability proves no reachable source expression observes them",
569                GuaranteeKindV0::for_label_less_family(),
570            ),
571            Self::AnimationNameReachability => descriptor(
572                self,
573                "may remove keyframes only when animation-name reachability proves they are unobservable",
574                GuaranteeKindV0::for_label_less_family(),
575            ),
576            Self::ValueGraphReachability => descriptor(
577                self,
578                "may remove @value declarations only when value-graph traversal proves they are unreachable",
579                GuaranteeKindV0::for_label_less_family(),
580            ),
581            Self::VarReachability => descriptor(
582                self,
583                "may remove custom properties only when var() reachability proves they are unobservable",
584                GuaranteeKindV0::for_label_less_family(),
585            ),
586            Self::DeadMediaWitness => descriptor(
587                self,
588                "may remove @media branches only when target and cascade witnesses prove deadness",
589                GuaranteeKindV0::for_label_less_family(),
590            ),
591            Self::DeadSupportsWitness => descriptor(
592                self,
593                "may remove @supports branches only when target and cascade witnesses prove deadness",
594                GuaranteeKindV0::for_label_less_family(),
595            ),
596            Self::DesignTokenPackageProvenance => descriptor(
597                self,
598                "must preserve design-token provenance while routing declarations across package boundaries",
599                GuaranteeKindV0::for_label_less_family(),
600            ),
601            Self::SourceMapTransformTrace => descriptor(
602                self,
603                "must emit a source-map trace for every non-trivia transformed span",
604                GuaranteeKindV0::for_label_less_family(),
605            ),
606        }
607    }
608
609    pub const fn declares_cascade_obligation(self) -> bool {
610        !matches!(self, Self::CascadeSafetyFloor)
611    }
612
613    pub const fn preserves_computed_value(self) -> bool {
614        matches!(self, Self::ComputedValuePreservation)
615    }
616
617    pub const fn from_computed_value_preservation(preserved: bool) -> Self {
618        if preserved {
619            Self::ComputedValuePreservation
620        } else {
621            Self::CascadeSafetyFloor
622        }
623    }
624}
625
626#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
627#[serde(rename_all = "camelCase")]
628pub struct RewriteObligationFamilyDescriptorV0 {
629    pub id: ObligationFamilyIdV0,
630    pub family_name: &'static str,
631    pub obligation: &'static str,
632    pub justifiable_guarantee: GuaranteeKindV0,
633}
634
635const fn descriptor(
636    id: ObligationFamilyIdV0,
637    obligation: &'static str,
638    justifiable_guarantee: GuaranteeKindV0,
639) -> RewriteObligationFamilyDescriptorV0 {
640    RewriteObligationFamilyDescriptorV0 {
641        id,
642        family_name: id.as_str(),
643        obligation,
644        justifiable_guarantee,
645    }
646}
647
648pub const fn list_rewrite_obligation_families_v0()
649-> [RewriteObligationFamilyDescriptorV0; REWRITE_OBLIGATION_FAMILY_COUNT_V0] {
650    [
651        ObligationFamilyIdV0::CascadeSafetyFloor.descriptor(),
652        ObligationFamilyIdV0::CascadeObligationDeclaration.descriptor(),
653        ObligationFamilyIdV0::ComputedValuePreservation.descriptor(),
654        ObligationFamilyIdV0::WhitespaceBoundary.descriptor(),
655        ObligationFamilyIdV0::CommentSourceMapProvenance.descriptor(),
656        ObligationFamilyIdV0::NumericLiteralEquivalence.descriptor(),
657        ObligationFamilyIdV0::DimensionComputedValue.descriptor(),
658        ObligationFamilyIdV0::ColorLiteralEquivalence.descriptor(),
659        ObligationFamilyIdV0::UrlTokenGrammar.descriptor(),
660        ObligationFamilyIdV0::StringTextAndFontValue.descriptor(),
661        ObligationFamilyIdV0::SelectorSpecificityAndCascade.descriptor(),
662        ObligationFamilyIdV0::LonghandShorthandCascadeOutcome.descriptor(),
663        ObligationFamilyIdV0::DeclarationCascadeOrder.descriptor(),
664        ObligationFamilyIdV0::RuleMergeWinnerOrder.descriptor(),
665        ObligationFamilyIdV0::SelectorIdentityAndModuleSemantics.descriptor(),
666        ObligationFamilyIdV0::SemanticMarkerRetention.descriptor(),
667        ObligationFamilyIdV0::TargetPrefixAddition.descriptor(),
668        ObligationFamilyIdV0::StalePrefixRemovalMapping.descriptor(),
669        ObligationFamilyIdV0::TargetFallbackBranch.descriptor(),
670        ObligationFamilyIdV0::ColorSpaceTargetEquivalence.descriptor(),
671        ObligationFamilyIdV0::TargetColorPrecision.descriptor(),
672        ObligationFamilyIdV0::DirectionalityOption.descriptor(),
673        ObligationFamilyIdV0::NestedSelectorSpecificity.descriptor(),
674        ObligationFamilyIdV0::ScopedMatching.descriptor(),
675        ObligationFamilyIdV0::LayerOrderComparison.descriptor(),
676        ObligationFamilyIdV0::TargetFeaturePredicate.descriptor(),
677        ObligationFamilyIdV0::MediaPredicate.descriptor(),
678        ObligationFamilyIdV0::ContainerPredicate.descriptor(),
679        ObligationFamilyIdV0::NativeCssStaticValue.descriptor(),
680        ObligationFamilyIdV0::CalcExpressionEquivalence.descriptor(),
681        ObligationFamilyIdV0::ImportWrapperProvenance.descriptor(),
682        ObligationFamilyIdV0::ScssNamespaceProvenance.descriptor(),
683        ObligationFamilyIdV0::LessNamespaceProvenance.descriptor(),
684        ObligationFamilyIdV0::SelectorIdentityMap.descriptor(),
685        ObligationFamilyIdV0::ComposedClassProvenance.descriptor(),
686        ObligationFamilyIdV0::ValueGraphResolution.descriptor(),
687        ObligationFamilyIdV0::CustomPropertyFixedPoint.descriptor(),
688        ObligationFamilyIdV0::SourceClassReachability.descriptor(),
689        ObligationFamilyIdV0::AnimationNameReachability.descriptor(),
690        ObligationFamilyIdV0::ValueGraphReachability.descriptor(),
691        ObligationFamilyIdV0::VarReachability.descriptor(),
692        ObligationFamilyIdV0::DeadMediaWitness.descriptor(),
693        ObligationFamilyIdV0::DeadSupportsWitness.descriptor(),
694        ObligationFamilyIdV0::DesignTokenPackageProvenance.descriptor(),
695        ObligationFamilyIdV0::SourceMapTransformTrace.descriptor(),
696    ]
697}
698
699#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
700#[serde(rename_all = "camelCase")]
701pub struct RewriteObligationFamilyRetirementRecordV0 {
702    pub family_name: &'static str,
703    pub reason: &'static str,
704}
705
706#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
707#[serde(rename_all = "camelCase")]
708pub struct RewriteObligationFamilyCarrierBindingV0 {
709    pub family: ObligationFamilyIdV0,
710    pub carrier: &'static str,
711}
712
713#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
714#[serde(rename_all = "camelCase")]
715pub struct RewriteObligationFamilyClosureSummaryV0 {
716    pub schema_version: &'static str,
717    pub product: &'static str,
718    pub registered_family_count: usize,
719    pub carrier_bound_family_count: usize,
720    pub retirement_record_count: usize,
721    pub orphan_family_names: Vec<&'static str>,
722    pub extra_carrier_family_names: Vec<&'static str>,
723    pub retirement_records: Vec<RewriteObligationFamilyRetirementRecordV0>,
724    pub untyped_carrier_count: usize,
725    pub untyped_prose_arm_count: usize,
726    pub closure_passed: bool,
727}
728
729pub fn summarize_rewrite_obligation_family_closure_v0(
730    carrier_bindings: impl IntoIterator<Item = RewriteObligationFamilyCarrierBindingV0>,
731    retirement_records: impl IntoIterator<Item = RewriteObligationFamilyRetirementRecordV0>,
732    untyped_carrier_count: usize,
733    untyped_prose_arm_count: usize,
734) -> RewriteObligationFamilyClosureSummaryV0 {
735    summarize_rewrite_obligation_family_closure_from_names_v0(
736        carrier_bindings
737            .into_iter()
738            .map(|binding| binding.family.as_str()),
739        retirement_records,
740        untyped_carrier_count,
741        untyped_prose_arm_count,
742    )
743}
744
745pub fn summarize_rewrite_obligation_family_closure_from_names_v0(
746    carrier_bound_family_names: impl IntoIterator<Item = &'static str>,
747    retirement_records: impl IntoIterator<Item = RewriteObligationFamilyRetirementRecordV0>,
748    untyped_carrier_count: usize,
749    untyped_prose_arm_count: usize,
750) -> RewriteObligationFamilyClosureSummaryV0 {
751    let registered_family_names = list_rewrite_obligation_families_v0()
752        .into_iter()
753        .map(|descriptor| descriptor.family_name)
754        .collect::<BTreeSet<_>>();
755    let carrier_bound_family_names = carrier_bound_family_names
756        .into_iter()
757        .collect::<BTreeSet<_>>();
758    let retirement_records = retirement_records.into_iter().collect::<Vec<_>>();
759    let retired_family_names = retirement_records
760        .iter()
761        .map(|record| record.family_name)
762        .collect::<BTreeSet<_>>();
763
764    let orphan_family_names = registered_family_names
765        .difference(&carrier_bound_family_names)
766        .copied()
767        .filter(|family| !retired_family_names.contains(family))
768        .collect::<Vec<_>>();
769    let extra_carrier_family_names = carrier_bound_family_names
770        .difference(&registered_family_names)
771        .copied()
772        .collect::<Vec<_>>();
773    let closure_passed = orphan_family_names.is_empty()
774        && extra_carrier_family_names.is_empty()
775        && untyped_carrier_count == 0
776        && untyped_prose_arm_count == 0;
777
778    RewriteObligationFamilyClosureSummaryV0 {
779        schema_version: EVIDENCE_GRAPH_SCHEMA_VERSION_V0,
780        product: REWRITE_OBLIGATION_FAMILY_PRODUCT_V0,
781        registered_family_count: registered_family_names.len(),
782        carrier_bound_family_count: carrier_bound_family_names.len(),
783        retirement_record_count: retirement_records.len(),
784        orphan_family_names,
785        extra_carrier_family_names,
786        retirement_records,
787        untyped_carrier_count,
788        untyped_prose_arm_count,
789        closure_passed,
790    }
791}
792
793#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
794#[serde(rename_all = "camelCase")]
795pub struct EvidenceNodeKeyV0 {
796    pub query_identity: String,
797    pub input_identity: String,
798}
799
800impl EvidenceNodeKeyV0 {
801    pub fn new(query_identity: impl Into<String>, input_identity: impl Into<String>) -> Self {
802        Self {
803            query_identity: query_identity.into(),
804            input_identity: input_identity.into(),
805        }
806    }
807}
808
809#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
810#[serde(rename_all = "camelCase")]
811pub struct EvidenceNodeSeedV0 {
812    pub key: EvidenceNodeKeyV0,
813    pub provenance: Vec<String>,
814    #[serde(skip_serializing_if = "Option::is_none")]
815    pub precision: Option<EvidenceAnalysisPrecisionV0>,
816    pub guarantee: GuaranteeKindV0,
817    pub earned_via: GuaranteeFamilyV0,
818}
819
820impl EvidenceNodeSeedV0 {
821    pub fn new(
822        key: EvidenceNodeKeyV0,
823        provenance: Vec<String>,
824        guarantee: GuaranteeKindV0,
825    ) -> Self {
826        Self::with_precision(key, provenance, None, guarantee)
827    }
828
829    pub fn with_precision(
830        key: EvidenceNodeKeyV0,
831        provenance: Vec<String>,
832        precision: Option<EvidenceAnalysisPrecisionV0>,
833        guarantee: GuaranteeKindV0,
834    ) -> Self {
835        Self::with_precision_and_family(
836            key,
837            provenance,
838            precision,
839            guarantee,
840            FamilyStampV0::floor_assumption(),
841        )
842    }
843
844    pub fn with_family(
845        key: EvidenceNodeKeyV0,
846        provenance: Vec<String>,
847        guarantee: GuaranteeKindV0,
848        family_stamp: FamilyStampV0,
849    ) -> Self {
850        Self::with_precision_and_family(key, provenance, None, guarantee, family_stamp)
851    }
852
853    pub fn with_precision_and_family(
854        key: EvidenceNodeKeyV0,
855        provenance: Vec<String>,
856        precision: Option<EvidenceAnalysisPrecisionV0>,
857        guarantee: GuaranteeKindV0,
858        family_stamp: FamilyStampV0,
859    ) -> Self {
860        Self {
861            key,
862            provenance,
863            precision,
864            guarantee,
865            earned_via: family_stamp.earned_via(),
866        }
867    }
868}
869
870#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
871#[serde(rename_all = "camelCase")]
872pub struct EvidenceAnalysisPrecisionV0 {
873    pub product: String,
874    pub value_domain: String,
875    pub flow_sensitivity: String,
876    pub context_sensitivity: String,
877    pub revision_axis: String,
878}
879
880impl EvidenceAnalysisPrecisionV0 {
881    pub fn new(
882        product: impl Into<String>,
883        value_domain: impl Into<String>,
884        flow_sensitivity: impl Into<String>,
885        context_sensitivity: impl Into<String>,
886        revision_axis: impl Into<String>,
887    ) -> Self {
888        Self {
889            product: product.into(),
890            value_domain: value_domain.into(),
891            flow_sensitivity: flow_sensitivity.into(),
892            context_sensitivity: context_sensitivity.into(),
893            revision_axis: revision_axis.into(),
894        }
895    }
896}
897
898#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
899#[serde(rename_all = "camelCase")]
900pub struct EvidenceDemandEdgeV0 {
901    pub from_query_identity: String,
902    pub to_node_key: EvidenceNodeKeyV0,
903    pub edge_kind: String,
904}
905
906impl EvidenceDemandEdgeV0 {
907    pub fn new(
908        from_query_identity: impl Into<String>,
909        to_node_key: EvidenceNodeKeyV0,
910        edge_kind: impl Into<String>,
911    ) -> Self {
912        Self {
913            from_query_identity: from_query_identity.into(),
914            to_node_key,
915            edge_kind: edge_kind.into(),
916        }
917    }
918}
919
920#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
921#[serde(rename_all = "camelCase")]
922pub struct EvidenceNodeV0 {
923    pub key: EvidenceNodeKeyV0,
924    pub provenance: Vec<String>,
925    #[serde(skip_serializing_if = "Option::is_none")]
926    pub precision: Option<EvidenceAnalysisPrecisionV0>,
927    pub guarantee: GuaranteeKindV0,
928    earned_via: GuaranteeFamilyV0,
929}
930
931impl EvidenceNodeV0 {
932    pub fn earned_via(&self) -> GuaranteeFamilyV0 {
933        #[cfg(any(test, feature = "test-support"))]
934        EARNED_GUARANTEE_FAMILY_READS_V0.with(|count| count.set(count.get().saturating_add(1)));
935        self.earned_via
936    }
937}
938
939#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
940#[serde(rename_all = "camelCase")]
941pub struct EvidenceGraphV0 {
942    pub schema_version: &'static str,
943    pub product: &'static str,
944    pub nodes: Vec<EvidenceNodeV0>,
945    pub edges: Vec<EvidenceDemandEdgeV0>,
946}
947
948impl EvidenceGraphV0 {
949    pub fn node_input_identities(&self) -> BTreeSet<String> {
950        self.nodes
951            .iter()
952            .map(|node| node.key.input_identity.clone())
953            .collect()
954    }
955
956    pub fn edge_input_identities(&self) -> BTreeSet<String> {
957        self.edges
958            .iter()
959            .map(|edge| edge.to_node_key.input_identity.clone())
960            .collect()
961    }
962}
963
964#[derive(Debug, Clone, PartialEq, Eq)]
965pub enum EvidenceGraphBuildErrorV0 {
966    MissingDemandNode(EvidenceNodeKeyV0),
967}
968
969fn fixture_witness_family_matches(
970    guarantee: GuaranteeKindV0,
971    earned_via: GuaranteeFamilyV0,
972) -> bool {
973    !matches!(
974        guarantee,
975        GuaranteeKindV0::SampledFixtureWitness
976            | GuaranteeKindV0::SchedulerPriorityFixtureWitness
977            | GuaranteeKindV0::MetricInputFixtureWitness
978            | GuaranteeKindV0::AlphaRenamingStableHashFixtureWitness
979    ) || matches!(
980        earned_via,
981        GuaranteeFamilyV0::SampledFixtureCorpus | GuaranteeFamilyV0::FloorAssumption
982    )
983}
984
985pub fn build_salsa_demand_evidence_graph_v0(
986    all_node_seeds: impl IntoIterator<Item = EvidenceNodeSeedV0>,
987    demand_edges: impl IntoIterator<Item = EvidenceDemandEdgeV0>,
988) -> Result<EvidenceGraphV0, EvidenceGraphBuildErrorV0> {
989    build_evidence_graph_from_edges_v0(all_node_seeds, demand_edges)
990}
991
992pub fn build_evidence_graph_from_edges_v0(
993    all_node_seeds: impl IntoIterator<Item = EvidenceNodeSeedV0>,
994    demand_edges: impl IntoIterator<Item = EvidenceDemandEdgeV0>,
995) -> Result<EvidenceGraphV0, EvidenceGraphBuildErrorV0> {
996    let all_nodes = all_node_seeds
997        .into_iter()
998        .map(|seed| (seed.key.clone(), seed))
999        .collect::<BTreeMap<_, _>>();
1000    let edges = demand_edges.into_iter().collect::<Vec<_>>();
1001    let demanded_keys = edges
1002        .iter()
1003        .map(|edge| edge.to_node_key.clone())
1004        .collect::<BTreeSet<_>>();
1005
1006    let mut nodes = Vec::new();
1007    for key in demanded_keys {
1008        let Some(seed) = all_nodes.get(&key) else {
1009            return Err(EvidenceGraphBuildErrorV0::MissingDemandNode(key));
1010        };
1011        // STRUCTURAL ENTAILMENT: the evidence-graph semver-intent operator owns this assertion.
1012        // Current workspace producers can emit fixture-witness guarantees only with sampled-corpus
1013        // or floor-assumption families; re-enter when a fixture producer or a typed-invariant token
1014        // constructor is added. Panic is deliberate: returning this internal producer bug would
1015        // require a breaking variant on the published, exhaustive EvidenceGraphBuildErrorV0 surface.
1016        assert!(
1017            fixture_witness_family_matches(seed.guarantee, seed.earned_via),
1018            "evidence node {:?} pairs fixture-witness guarantee {:?} with earned family {:?}",
1019            seed.key,
1020            seed.guarantee,
1021            seed.earned_via,
1022        );
1023        nodes.push(EvidenceNodeV0 {
1024            key: seed.key.clone(),
1025            provenance: seed.provenance.clone(),
1026            precision: seed.precision.clone(),
1027            guarantee: seed.guarantee,
1028            earned_via: seed.earned_via,
1029        });
1030    }
1031
1032    Ok(EvidenceGraphV0 {
1033        schema_version: EVIDENCE_GRAPH_SCHEMA_VERSION_V0,
1034        product: EVIDENCE_GRAPH_PRODUCT_V0,
1035        nodes,
1036        edges,
1037    })
1038}
1039
1040#[cfg(test)]
1041mod tests {
1042    use super::*;
1043
1044    fn node_seed(input_identity: &str) -> EvidenceNodeSeedV0 {
1045        EvidenceNodeSeedV0::new(
1046            EvidenceNodeKeyV0::new("memo_style_fact_entry", input_identity),
1047            vec!["fixture-provenance".to_string()],
1048            GuaranteeKindV0::for_label_less_family(),
1049        )
1050    }
1051
1052    #[test]
1053    fn rewrite_obligation_family_registry_is_closed_and_label_honest() {
1054        let descriptors = list_rewrite_obligation_families_v0();
1055        assert_eq!(descriptors.len(), REWRITE_OBLIGATION_FAMILY_COUNT_V0);
1056        let family_names = descriptors
1057            .iter()
1058            .map(|descriptor| descriptor.family_name)
1059            .collect::<BTreeSet<_>>();
1060        assert_eq!(family_names.len(), REWRITE_OBLIGATION_FAMILY_COUNT_V0);
1061        for descriptor in descriptors {
1062            assert_eq!(descriptor.family_name, descriptor.id.as_str());
1063            assert!(
1064                [
1065                    GuaranteeKindV0::Floor,
1066                    GuaranteeKindV0::SampledFixtureWitness,
1067                    GuaranteeKindV0::SchedulerPriorityFixtureWitness,
1068                    GuaranteeKindV0::MetricInputFixtureWitness,
1069                    GuaranteeKindV0::IncrementalLayerEvidenceOnly,
1070                    GuaranteeKindV0::AlphaRenamingStableHashFixtureWitness,
1071                    GuaranteeKindV0::NotClaimedExactTraversal,
1072                ]
1073                .contains(&descriptor.justifiable_guarantee)
1074            );
1075        }
1076    }
1077
1078    #[test]
1079    fn rewrite_obligation_family_closure_reports_both_directions() {
1080        let summary = summarize_rewrite_obligation_family_closure_from_names_v0(
1081            ["computedValuePreservation", "unknownFamily"],
1082            [RewriteObligationFamilyRetirementRecordV0 {
1083                family_name: "cascadeSafetyFloor",
1084                reason: "test fixture",
1085            }],
1086            0,
1087            0,
1088        );
1089        assert!(
1090            summary
1091                .orphan_family_names
1092                .contains(&"cascadeObligationDeclaration")
1093        );
1094        assert!(
1095            summary
1096                .extra_carrier_family_names
1097                .contains(&"unknownFamily")
1098        );
1099        assert!(!summary.closure_passed);
1100    }
1101
1102    #[test]
1103    fn rewrite_obligation_family_closure_has_non_vacuous_retirement_branch() {
1104        let all_live_except_floor = list_rewrite_obligation_families_v0()
1105            .into_iter()
1106            .filter(|descriptor| descriptor.family_name != "cascadeSafetyFloor")
1107            .map(|descriptor| RewriteObligationFamilyCarrierBindingV0 {
1108                family: descriptor.id,
1109                carrier: "fixture",
1110            })
1111            .collect::<Vec<_>>();
1112        let summary = summarize_rewrite_obligation_family_closure_v0(
1113            all_live_except_floor,
1114            [RewriteObligationFamilyRetirementRecordV0 {
1115                family_name: "cascadeSafetyFloor",
1116                reason: "test fixture",
1117            }],
1118            0,
1119            0,
1120        );
1121        assert!(summary.closure_passed);
1122        assert_eq!(summary.retirement_record_count, 1);
1123    }
1124
1125    #[test]
1126    fn guarantee_kind_round_trips_existing_labels_without_upgrading_label_less_nodes() {
1127        for kind in [
1128            GuaranteeKindV0::SampledFixtureWitness,
1129            GuaranteeKindV0::SchedulerPriorityFixtureWitness,
1130            GuaranteeKindV0::MetricInputFixtureWitness,
1131            GuaranteeKindV0::IncrementalLayerEvidenceOnly,
1132            GuaranteeKindV0::AlphaRenamingStableHashFixtureWitness,
1133            GuaranteeKindV0::NotClaimedExactTraversal,
1134        ] {
1135            let label = kind.existing_label();
1136            assert_eq!(
1137                label.and_then(GuaranteeKindV0::from_existing_label),
1138                Some(kind)
1139            );
1140        }
1141        assert_eq!(
1142            GuaranteeKindV0::for_label_less_family(),
1143            GuaranteeKindV0::Floor
1144        );
1145        assert_eq!(GuaranteeKindV0::Floor.existing_label(), None);
1146    }
1147
1148    #[test]
1149    fn guarantee_family_descriptions_are_closed_and_honest() {
1150        let families = [
1151            (GuaranteeFamilyV0::ByteIdentityOracle, "byteIdentityOracle"),
1152            (
1153                GuaranteeFamilyV0::ExternalReplicaDifferential,
1154                "externalReplicaDifferential",
1155            ),
1156            (GuaranteeFamilyV0::ExternalTool, "externalTool"),
1157            (
1158                GuaranteeFamilyV0::PropertyCorpusWitness,
1159                "propertyCorpusWitness",
1160            ),
1161            (
1162                GuaranteeFamilyV0::SampledFixtureCorpus,
1163                "sampledFixtureCorpus",
1164            ),
1165            (
1166                GuaranteeFamilyV0::TypedInvariantWitness,
1167                "typedInvariantWitness",
1168            ),
1169            (
1170                GuaranteeFamilyV0::ProseObligationDischarged,
1171                "proseObligationDischarged",
1172            ),
1173            (GuaranteeFamilyV0::FloorAssumption, "floorAssumption"),
1174            (
1175                GuaranteeFamilyV0::LedgerBackedObligationDischarge,
1176                "ledgerBackedObligationDischarge",
1177            ),
1178        ];
1179
1180        assert_eq!(families.len(), 9);
1181        for (family, description) in families {
1182            assert_eq!(family.describe(), description);
1183        }
1184    }
1185
1186    #[test]
1187    fn evidence_nodes_preserve_floor_and_mechanism_families() -> Result<(), &'static str> {
1188        let floor_key = EvidenceNodeKeyV0::new("floor_query", "floor_input");
1189        let prose_key = EvidenceNodeKeyV0::new("prose_query", "prose_input");
1190        let prose_labels = vec![
1191            "pass:rule-merge".to_string(),
1192            "obligation:preserve declaration winner order".to_string(),
1193        ];
1194        let Some(prose_provenance) =
1195            ProseObligationProvenanceV0::from_provenance_labels(&prose_labels)
1196        else {
1197            return Err("prose provenance labels must mint a wrapper");
1198        };
1199        let graph = build_evidence_graph_from_edges_v0(
1200            [
1201                EvidenceNodeSeedV0::new(
1202                    floor_key.clone(),
1203                    vec!["floor-input".to_string()],
1204                    GuaranteeKindV0::for_label_less_family(),
1205                ),
1206                EvidenceNodeSeedV0::with_family(
1207                    prose_key.clone(),
1208                    prose_labels,
1209                    GuaranteeKindV0::for_label_less_family(),
1210                    FamilyStampV0::prose_obligation_discharged(&prose_provenance),
1211                ),
1212            ],
1213            [
1214                EvidenceDemandEdgeV0::new("floor_query", floor_key, "fixture-edge"),
1215                EvidenceDemandEdgeV0::new("prose_query", prose_key, "fixture-edge"),
1216            ],
1217        )
1218        .map_err(|_| "fixture graph must build")?;
1219
1220        assert_eq!(graph.nodes[0].guarantee, GuaranteeKindV0::Floor);
1221        assert_eq!(
1222            graph.nodes[0].earned_via(),
1223            GuaranteeFamilyV0::FloorAssumption
1224        );
1225        assert_eq!(graph.nodes[1].guarantee, GuaranteeKindV0::Floor);
1226        assert_eq!(
1227            graph.nodes[1].earned_via(),
1228            GuaranteeFamilyV0::ProseObligationDischarged
1229        );
1230        Ok(())
1231    }
1232
1233    #[test]
1234    fn fixture_witness_kind_rejects_an_unearned_family() -> Result<(), &'static str> {
1235        let key = EvidenceNodeKeyV0::new("fixture_query", "fixture_input");
1236        let panic = match std::panic::catch_unwind(|| {
1237            build_evidence_graph_from_edges_v0(
1238                [EvidenceNodeSeedV0 {
1239                    key: key.clone(),
1240                    provenance: vec!["fixture-input".to_string()],
1241                    precision: None,
1242                    guarantee: GuaranteeKindV0::SampledFixtureWitness,
1243                    earned_via: GuaranteeFamilyV0::TypedInvariantWitness,
1244                }],
1245                [EvidenceDemandEdgeV0::new(
1246                    "fixture_query",
1247                    key,
1248                    "fixture-edge",
1249                )],
1250            )
1251        }) {
1252            Ok(_) => return Err("mismatched fixture witness family must be rejected"),
1253            Err(panic) => panic,
1254        };
1255        let message = panic
1256            .downcast_ref::<String>()
1257            .map(String::as_str)
1258            .or_else(|| panic.downcast_ref::<&str>().copied())
1259            .ok_or("panic must carry the mismatch detail")?;
1260
1261        for detail in [
1262            "fixture_input",
1263            "SampledFixtureWitness",
1264            "TypedInvariantWitness",
1265        ] {
1266            assert!(
1267                message.contains(detail),
1268                "mismatch message must name {detail}: {message}"
1269            );
1270        }
1271        Ok(())
1272    }
1273
1274    #[test]
1275    fn sampled_fixture_corpus_token_requires_nonempty_matching_evidence() -> Result<(), &'static str>
1276    {
1277        let sampled = vec!["source".to_string(), "style".to_string()];
1278        let control = sampled.clone();
1279        let mismatch = vec!["source".to_string()];
1280
1281        assert!(
1282            SampledFixtureCorpusTokenV0::from_matching_string_vectors(&sampled, &mismatch, false,)
1283                .is_none()
1284        );
1285        assert!(
1286            SampledFixtureCorpusTokenV0::from_matching_string_vectors(&sampled, &mismatch, true,)
1287                .is_none()
1288        );
1289        assert!(
1290            SampledFixtureCorpusTokenV0::from_matching_string_vectors(&sampled, &control, false,)
1291                .is_none()
1292        );
1293        assert!(
1294            SampledFixtureCorpusTokenV0::from_matching_string_vectors(&[], &[], true).is_none()
1295        );
1296
1297        let token =
1298            SampledFixtureCorpusTokenV0::from_matching_string_vectors(&sampled, &control, true)
1299                .ok_or("consistent nonempty fixture evidence should mint a token")?;
1300        assert_eq!(
1301            FamilyStampV0::sampled_fixture_corpus(&token).earned_via(),
1302            GuaranteeFamilyV0::SampledFixtureCorpus
1303        );
1304        Ok(())
1305    }
1306
1307    #[test]
1308    fn ledger_discharge_stamp_requires_cell_key_shape() {
1309        assert!(
1310            LedgerDischargeWitnessV0::from_discharge_cell_key_v0(
1311                "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
1312            )
1313            .is_some()
1314        );
1315        assert!(LedgerDischargeWitnessV0::from_discharge_cell_key_v0("not-a-cell").is_none());
1316    }
1317
1318    #[test]
1319    fn external_tool_stamp_preserves_invocation_facts_without_a_verdict() {
1320        let witness = ExternalToolRunWitnessV0 {
1321            tool_name: "fixture-tool".to_string(),
1322            tool_version: "1.2.3".to_string(),
1323            input_digest: "0123456789abcdef".to_string(),
1324            exit_status: 0,
1325        };
1326
1327        assert_eq!(witness.tool_name, "fixture-tool");
1328        assert_eq!(witness.tool_version, "1.2.3");
1329        assert_eq!(witness.input_digest, "0123456789abcdef");
1330        assert_eq!(witness.exit_status, 0);
1331        assert_eq!(
1332            FamilyStampV0::external_tool(&witness).earned_via(),
1333            GuaranteeFamilyV0::ExternalTool
1334        );
1335    }
1336
1337    #[test]
1338    fn earned_family_read_counter_records_access_path() -> Result<(), &'static str> {
1339        reset_earned_guarantee_family_read_count_v0();
1340        let key = EvidenceNodeKeyV0::new("counter_query", "counter_input");
1341        let graph = build_evidence_graph_from_edges_v0(
1342            [EvidenceNodeSeedV0::new(
1343                key.clone(),
1344                vec!["counter-input".to_string()],
1345                GuaranteeKindV0::for_label_less_family(),
1346            )],
1347            [EvidenceDemandEdgeV0::new(
1348                "counter_query",
1349                key,
1350                "fixture-edge",
1351            )],
1352        )
1353        .map_err(|_| "fixture graph must build")?;
1354
1355        assert_eq!(graph.nodes.len(), 1);
1356        assert_eq!(earned_guarantee_family_read_count_v0(), 0);
1357        assert_eq!(
1358            graph.nodes[0].earned_via(),
1359            GuaranteeFamilyV0::FloorAssumption
1360        );
1361        assert_eq!(
1362            graph.nodes[0].earned_via(),
1363            GuaranteeFamilyV0::FloorAssumption
1364        );
1365        assert_eq!(earned_guarantee_family_read_count_v0(), 2);
1366        Ok(())
1367    }
1368
1369    #[test]
1370    fn salsa_demand_graph_keys_on_edges_not_the_full_node_list() -> Result<(), &'static str> {
1371        let graph = build_salsa_demand_evidence_graph_v0(
1372            [
1373                node_seed("/workspace/src/App.module.scss"),
1374                node_seed("/workspace/src/_theme.scss"),
1375            ],
1376            [EvidenceDemandEdgeV0::new(
1377                "memo_workspace_diagnostics_substrate",
1378                EvidenceNodeKeyV0::new("memo_style_fact_entry", "/workspace/src/App.module.scss"),
1379                "salsa-demand-read",
1380            )],
1381        )
1382        .map_err(|_| "demand edge must target a known node")?;
1383
1384        assert_eq!(
1385            graph.node_input_identities(),
1386            BTreeSet::from(["/workspace/src/App.module.scss".to_string()])
1387        );
1388        assert_eq!(
1389            graph.edge_input_identities(),
1390            BTreeSet::from(["/workspace/src/App.module.scss".to_string()])
1391        );
1392        assert_eq!(graph.nodes.len(), 1);
1393        assert_eq!(graph.edges.len(), 1);
1394        Ok(())
1395    }
1396
1397    #[test]
1398    fn salsa_demand_graph_rejects_fabricated_edges() {
1399        let result = build_salsa_demand_evidence_graph_v0(
1400            [node_seed("/workspace/src/App.module.scss")],
1401            [EvidenceDemandEdgeV0::new(
1402                "memo_workspace_diagnostics_substrate",
1403                EvidenceNodeKeyV0::new("memo_style_fact_entry", "/workspace/src/_missing.scss"),
1404                "salsa-demand-read",
1405            )],
1406        );
1407
1408        assert_eq!(
1409            result,
1410            Err(EvidenceGraphBuildErrorV0::MissingDemandNode(
1411                EvidenceNodeKeyV0::new("memo_style_fact_entry", "/workspace/src/_missing.scss")
1412            ))
1413        );
1414    }
1415
1416    #[test]
1417    fn graph_serializes_without_shape_specific_runtime_dependencies() -> Result<(), &'static str> {
1418        let graph = build_salsa_demand_evidence_graph_v0(
1419            [node_seed("/workspace/src/App.module.scss")],
1420            [EvidenceDemandEdgeV0::new(
1421                "memo_workspace_diagnostics_substrate",
1422                EvidenceNodeKeyV0::new("memo_style_fact_entry", "/workspace/src/App.module.scss"),
1423                "salsa-demand-read",
1424            )],
1425        )
1426        .map_err(|_| "demand edge must target a known node")?;
1427        let json = serde_json::to_value(&graph).map_err(|_| "graph must serialize")?;
1428        assert_eq!(json["schemaVersion"], "0");
1429        assert_eq!(json["product"], EVIDENCE_GRAPH_PRODUCT_V0);
1430        assert_eq!(json["nodes"][0]["guarantee"], "floor");
1431        Ok(())
1432    }
1433
1434    #[test]
1435    fn graph_preserves_optional_precision_payload() -> Result<(), &'static str> {
1436        let key = EvidenceNodeKeyV0::new("source_diagnostic_precision", "missingSelector");
1437        let graph = build_salsa_demand_evidence_graph_v0(
1438            [EvidenceNodeSeedV0::with_precision(
1439                key.clone(),
1440                vec!["omena-query.source-syntax-index".to_string()],
1441                Some(EvidenceAnalysisPrecisionV0::new(
1442                    "omena-query.analysis-precision",
1443                    "classValueResolution",
1444                    "sourceSyntaxIndex",
1445                    "perSourceReference",
1446                    "OmenaQuerySourceDiagnosticsForFileV0.input",
1447                )),
1448                GuaranteeKindV0::for_label_less_family(),
1449            )],
1450            [EvidenceDemandEdgeV0::new(
1451                "source_diagnostic_precision",
1452                key,
1453                "diagnostic-evidence",
1454            )],
1455        )
1456        .map_err(|_| "precision edge must target a known node")?;
1457
1458        let precision = graph.nodes[0]
1459            .precision
1460            .as_ref()
1461            .ok_or("precision payload must round-trip through the graph")?;
1462        assert_eq!(precision.value_domain, "classValueResolution");
1463        assert_eq!(precision.flow_sensitivity, "sourceSyntaxIndex");
1464        assert_eq!(precision.context_sensitivity, "perSourceReference");
1465        Ok(())
1466    }
1467}