Skip to main content

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};