stygian_charon/vendor_resolver/mod.rs
1//! Vendor-to-playbook auto-resolution (T90).
2//!
3//! This module bridges the [`vendor_classifier`][crate::vendor_classifier]
4//! (T89) and the [`playbooks`][crate::playbooks] resolver (T85):
5//! given a [`VendorClassification`], it picks the right codified
6//! [`Playbook`][crate::playbooks::Playbook] for the runner and
7//! ships a rationale bundle the diagnostic payload can render.
8//!
9//! ## Resolution rule table
10//!
11//! The baseline rule bundle lives in
12//! `crates/stygian-charon/data/vendor_playbook_rules/` and is
13//! embedded into the binary at compile time via `include_str!`.
14//! The four baseline rules and their precedence are:
15//!
16//! | Priority | Rule id | Vendors | Resolves to | Merge strategy |
17//! |----------|------------------------|--------------------------------------------------------------|------------------------|--------------------|
18//! | `0` | `tier2-hostile` | `DataDome`, `PerimeterX`, `Akamai`, `Kasada`, `Imperva`, `ShapeSecurity` | `tier2-hostile` / `high_security` | `StrongestVendor` |
19//! | `10` | `tier1-js-cloudflare` | `Cloudflare`, `Hcaptcha`, `Recaptcha`, `FingerprintCom` | `tier1-js` / `content_site` | `StrongestVendor` |
20//! | `100` | `tier1-static` | `Unknown` (require_unknown_vendor = `true`) | `tier1-static` / `content_site` | `Single` |
21//! | `1000` | `default-manual` | *(catch-all)* | `Manual` strategy marker | `Manual` |
22//!
23//! Lower priority numbers win. When a rule's
24//! [`ResolutionRule::min_confidence`]
25//! gate passes **and** at least one of its listed vendors is in the
26//! classifier's ranked scoreboard, the rule fires.
27//!
28//! ## Multi-vendor precedence + merge
29//!
30//! The classifier emits a ranked scoreboard (top vendor first,
31//! ties broken by [`VendorId`][crate::vendor_classifier::VendorId]
32//! discriminant order). When the scoreboard lists multiple
33//! vendors that match the fired rule, the
34//! [`MergeStrategy`]
35//! determines how the rule consolidates them into a single
36//! decision:
37//!
38//! | `MergeStrategy` | Behaviour |
39//! |-------------------|---------------------------------------------------------------------------------------------------------|
40//! | `StrongestVendor` | Pick the listed vendor with the highest per-rule weight. Used by the two high-priority rules. |
41//! | `Single` | Pick the single listed vendor (ties broken by `VendorId` discriminant order). Used by `tier1-static`. |
42//! | `Manual` | Defer to manual mode — return [`StrategyMarker::Manual`]. Used by the `default-manual` sentinel. |
43//!
44//! ## Low-confidence fallback
45//!
46//! When no specific rule fires, the resolver falls through to the
47//! `default-manual` sentinel and returns
48//! [`StrategyMarker::Manual`]. The existing manual mode
49//! selection is **not** modified by the resolver — the caller keeps
50//! whatever mode it had in effect. This is the
51//! "non-breaking integration with existing manual mode selection"
52//! guarantee from the T90 spec.
53//!
54//! ## Determinism
55//!
56//! The resolver is **fully deterministic**:
57//!
58//! - Rules are sorted by `(priority ASC, id ASC)` on construction,
59//! so two rules with the same priority are tie-broken by their
60//! stable `id`.
61//! - The vendor scoreboard is supplied by the classifier, which
62//! is itself deterministic (T89 — `VendorId` discriminant
63//! order on ties).
64//! - The `rationale.contributing_vendors` list is sorted by
65//! `(score DESC, VendorId ASC)` so the JSON form is byte-stable.
66//!
67//! ## Backward compatibility
68//!
69//! The resolver is **additive only** — no existing public type or
70//! method gains a new field. The new module lives at
71//! `crates/stygian-charon/src/vendor_resolver/` and is exposed via
72//! the `vendor_resolver` re-exports below. No new feature gate is
73//! introduced (per the T90 spec — `## Feature flag`).
74//!
75//! ## Feature flag
76//!
77//! The module is **default-on**. It is compiled into every build
78//! of `stygian-charon` (which already defaults to the `caching`
79//! feature). No new feature gate is introduced because the new
80//! surface is purely additive — no existing public type gains a
81//! new field, no existing behaviour changes, and the manual
82//! fallback is non-breaking with the pre-T90 acquisition runner.
83//!
84//! # Example
85//!
86//! ```
87//! use stygian_charon::types::TargetClass;
88//! use stygian_charon::vendor_classifier::{VendorClassifier, VendorId};
89//! use stygian_charon::vendor_resolver::{StrategyMarker, VendorResolver};
90//! use std::collections::BTreeMap;
91//!
92//! let resolver = VendorResolver::with_builtin_defaults();
93//! let classifier = VendorClassifier::with_builtin_defaults();
94//! let cookies = vec!["datadome=abc; Path=/".to_string()];
95//! let mut headers = BTreeMap::new();
96//! headers.insert("x-datadome".to_string(), "protected".to_string());
97//! headers.insert("x-datadome-cid".to_string(), "abc".to_string());
98//! let classification =
99//! classifier.classify(&cookies, &headers, None, "https://example.com/");
100//!
101//! let resolution = resolver.resolve(&classification);
102//! assert!(resolution.is_resolved());
103//! match resolution.strategy {
104//! StrategyMarker::Resolved { playbook_id, target_class } => {
105//! assert_eq!(playbook_id, "tier2-hostile");
106//! assert_eq!(target_class, TargetClass::HighSecurity);
107//! }
108//! StrategyMarker::Manual => panic!("DataDome should resolve, not defer"),
109//! }
110//! ```
111
112mod builtins;
113mod error;
114mod resolver;
115mod rules;
116
117pub use error::VendorResolverError;
118pub use resolver::{
119 AppliedRule, PlaybookResolverExt, ResolutionRationale, StrategyMarker, VendorResolution,
120 VendorResolver,
121};
122pub use rules::{MergeStrategy, ResolutionRule, VendorRuleMatch, parse_resolution_rule};