Skip to main content

omena_evidence_graph/
lib.rs

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