Backwards Compatibility Policy

This page tracks API and behavioural compatibility guarantees for the stygian workspace, plus migration guidance for consumers upgrading across releases.

The project is pre-1.0 (currently 0.14.x). Per the OpenSSF best practices for pre-1.0 crates, breaking changes are allowed in minor releases but should be:

  • documented in the PR description,
  • called out in PROGRESS.md accumulated learnings,
  • covered with migration steps in this guide.

0.17.0 (Web Scraping Guide followup — 2026-08-24)

The 0.17.0 cut is mostly additive but ships two behaviour changes that downstream consumers should know about, plus a dependency upgrade that affects the MSRV.

MSRV bumped from 1.94.0 to 1.95.0

wasmtime@48.0.0 (pulled in by the stygian-graph wasm-plugins feature under --all-features) requires rustc ≥ 1.95.0. The MSRV job in CI now installs toolchain 1.95.0; local development against the workspace --all-features needs the same. Code targeting only default features is unaffected.

Dependency bumps

CrateBumpNotes
ulid1.2 → 3.0Ulid::new() renamed to Ulid::generate() across all call sites; stygian-plugin's previously independent ulid pin now tracks the workspace version
syn2 → 3One non-exhaustive-pattern fixup in stygian-extract-derive for TypePath's new attrs field
tokio-tungstenite0.29 → 0.30No code changes required
base640.22 → 0.23No code changes required
wasmtime46.0.2 → 48.0.0Security backports; no code changes; MSRV bumped to 1.95.0 as a side effect
h20.4.15 → 0.4.19Resolves RUSTSEC-2026-0258 (unbounded empty DATA frames)
CodeQL action pins4.37.7 → 4.37.8In codeql.yml / scorecard.yml

Behavioural changes

  • stygian-graph: SinkRecord::fetched_at is now a required field. SinkRecord::new(...) keeps its signature and falls back to Utc::now(); the auditable constructor is now SinkRecord::with_fetched_at(...). See the SinkRecord §fetched_at section in the data-sinks chapter for the migration shape.
  • stygian-graph: HttpAdapter::HttpConfig::allow_plain_http now defaults to false. Library-banner User-Agents are refused before hitting the wire. Set allow_plain_http: true to opt in explicitly, or set stealth_profile to pick a matching browser UA. See the HTTP Adapter §Catalogue-fingerprint rejection section in the adapters chapter.

New types (additive, no signature breaks)

Every other 0.17.0 change is a new public type or a new optional field. The full list lives in CHANGELOG.md §0.17.0; the headline new types are:

CrateNew type / portReference
stygian-browsertls::Ja4qbrowser/stealth.md §JA4Q
stygian-browserdiagnostic::DiagnosticHint + BrowserConfig::diagnostic_hints()browser/stealth.md §Diagnostic hints
stygian-proxyProxyStatusParser port + Rfc9209Parserproxy/strategies.md §Failure attribution
stygian-proxyGeofeedVerifier port + InMemoryGeofeedAdapterproxy/strategies.md §Geofeed verifier
stygian-charonContentTypeShiftDetector port + RollingBaselineDetectorcharon/operations.md §Cloaking detector
stygian-charonFieldAnomalyDetector port + StatisticalFieldAnomalyDetector (feature field-anomaly)charon/operations.md §Poisoned-data detector
stygian-graphRobotsPolicy + RobotsPolicyGuard port + PermissiveRobotsGuardgraph/architecture.md §Robots policy reconciliation
stygian-mcpPromptInjectionGuard port + DefaultPromptInjectionGuard (wired into aggregator tools/call dispatch)mcp/aggregator.md §Response sanitisation seam

CodeQL / supply chain

Dependabot resolved PRs #119–#124 ahead of the 0.17.0 cut. See CHANGELOG.md §0.17.0 for the full dep table.


0.14.0 (Phase 14 wave — 2026-06-19)

The 0.14.0 cut is strictly additive: every change is a new public API addition or a new optional field with #[serde(default)]. No signatures were renamed, removed, or reordered. No pub enums gained variants. No behavioural defaults changed in a way that breaks existing tests.

Migration burden for downstream consumers: none for code that does not explicitly opt into the new types. For code that wants to use them, each new type is in a new module — adding use stygian_proxy::stickiness::*; or use stygian_browser::OuterHtmlStrategy is the only change required.

New modules and types

CrateNew type / modulePurpose
stygian-proxystickiness::{StickinessPolicy, VendorStickinessMap, SessionDecision} (feature vendor-stickiness)Per-vendor sticky session routing
stygian-proxycoherence::{CoherencePort, DefaultCoherenceValidator, CoherenceContext, CoherencePolicy, CoherenceVerdict, MismatchField, MismatchSeverity} (feature coherence-validation)Five-vector identity coherence check
stygian-proxystrategy::thompson::ThompsonStrategy (feature bayesian-rotation)Per-proxy Beta(α, β) Bayesian rotation
stygian-proxyvendor_quirks::{VendorQuirk, QuirkSeverity, QuirkMatch, ProxyUrl, check, VENDOR_QUIRKS, CRAWLERA_8011_QUIRK, ZYTE_8011_QUIRK, BRD_SUPERPROXY_QUIRK, IPROYAL_QUIRK} (always compiled)Ingest-time vendor URL trap detection
stygian-browserOuterHtmlStrategy, OuterHtmlResult, NodeHandle::outer_html_with_strategyDeep outer-HTML resolution (issue #66)

New pub fields on existing types (safe serde, safe struct literal with ..Default::default())

TypeNew fields
ProxyCapabilities (stygian-proxy)ip_class: IpClass, target_compatibility: TargetVendorCompatibility, asn: Option<u32>, city: Option<String>, postal_code: Option<String>
Proxy (stygian-proxy)ip_class: IpClass, target_compatibility: TargetVendorCompatibility
CapabilityRequirement (stygian-proxy)require_ip_class: Option<IpClassRequirement>, target_vendor: Option<VendorId>, require_asn: Option<u32>, require_city: Option<String>, require_postal_code: Option<String>

All new fields are #[serde(default, skip_serializing_if = "Option::is_none")] (or #[serde(default)] for plain types like IpClass). Serialised Proxy / ProxyCapabilities / CapabilityRequirement payloads from 0.13.x deserialise unchanged into 0.14.0. Struct-literal Rust callers can either use ..Default::default() or pin to 0.13.x.

New error variants

ErrorNew variants#[non_exhaustive]?
stygian_proxy::error::ProxyErrorCoherenceMismatch { field, observed, expected }, InvalidGeoMetadata { reason }No (the enum is already #[non_exhaustive])

ProxyError was already #[non_exhaustive] going into 0.14.0, so exhaustive pattern matches in downstream code are not broken by the new variants. If you have wildcard _ => arms you will see no compile break; if you have an exhaustive match (rare), the #[non_exhaustive] attribute already requires a wildcard arm.

Behavioural drift (no signature change)

  • Free-list fetchers now tag ingested proxies as IpClass::Datacenter with TargetVendorCompatibility::default_blocked() so operators cannot accidentally route premium-vendor traffic through public free-list pools. Consumers using FreeListFetcher, FreeApiProxiesFetcher, or DnsTxtFetcher without an explicit target_vendor filter will see the same proxies as before but with a vendor-compat gate that fails closed. Who is affected: any consumer whose free-list pipeline relied on target_vendor = None as a permissive default. Migration: add target_vendor: None explicitly to the CapabilityRequirement only if you genuinely want to bypass the gate; otherwise leave the field absent so the gate stays fail-secure.

  • NodeHandle::outer_html() is now a thin wrapper around outer_html_with_strategy(OuterHtmlStrategy::Current). The existing Result<String> contract is preserved (Empty and Failed both flatten to Ok(String::new())). Consumers that want to distinguish Empty / Content / Failed should call outer_html_with_strategy directly and inspect the OuterHtmlResult variant. The new Recursive strategy resolves the Wix Studio / Editor X / large-SPA empty-payload case (issue #66) without any Wix-specific selectors.


Active compatibility notes (carry-over from 0.13.x)

The following observations still apply. New consumers upgrading from 0.12.x or earlier should read these too.

stygian-charon default features now include caching

Shipped in: 0.13.x (Phase 13 wave)

Consumers depending on stygian-charon with default features now transitively pull in the lru crate (via the T83/T88/T91/T93 modules that share the LruTtlStore primitive).

Who is affected: Any consumer using stygian-charon = "0.13" (or newer) without explicitly opting out of default features.

Migration:

# Preserve previous behaviour
stygian-charon = { version = "0.14", default-features = false }

No source-level changes are required for downstream code that does not import the new challenge_feedback, change_feed, pow_profile, or token_lifecycle modules.

Serde representation of 4 stygian-charon enums changed to snake_case

Shipped in: 0.13.x (Phase 13 wave)

TargetClass, ExecutionMode, SessionMode, and TelemetryLevel now serialise as snake_case instead of the prior PascalCase default.

Example:

BeforeAfter
"target_class": "ContentSite""target_class": "content_site"
"execution_mode": "Browser""execution_mode": "browser"

Who is affected: Any consumer with stored InvestigationBundle, RuntimePolicy, or RequirementsProfile JSON / TOML / YAML payloads in PascalCase. In-memory types are unchanged.

Migration: regenerate stored payloads from the current version, or pin to the previous 0.13.x patch release that still emits PascalCase. A one-shot migration script can jq the affected string fields.

StageFailureKind gained two variants (0.13.x, still active)

crates/stygian-browser/src/acquisition.rs: added ReplayDefenseTriggered and InterstitialRouted variants to a non-#[non_exhaustive] enum. Downstream exhaustive match without a wildcard arm may need a _ arm.


Recent risky changes (no source-level break)

These do not break compilation but may affect observable behaviour. Document them in your test plans before upgrading.

NodeHandle::outer_html() now has a Recursive strategy (0.14.0)

crates/stygian-browser/src/page.rs: the new outer_html_with_strategy(OuterHtmlStrategy::Recursive) uses CDP DOM.getOuterHTML (single round-trip, browser-side serialisation, shadow-DOM included by default) with a Rust-side DOM.describeNode walk fallback. Consumers that previously observed intermittent empty payloads from heavily dynamic pages (Wix Studio / Editor X meshes, large SPAs, shadow-DOM subtrees) may now see content they used to see as empty. The legacy Current strategy is the default and preserves the historical behaviour.

Free-list ingest tags IpClass::Datacenter (0.14.0)

See the "Behavioural drift" section above. Documented again here for visibility — any test that asserts on the ip_class of an auto-ingested free-list proxy must be updated.

hickory-resolver 0.24 → 0.26.1 in stygian-proxy (0.13.x)

Under the existing dns-fetcher feature. The tokio-runtime feature was renamed to tokio. New transitive dependencies: hickory-net, moka, crossbeam-channel, critical-section, ndk-context, jni, prefix-trie, tagptr, simdutf8, simd_cesu8.

No public API change, but the dependency graph shifts. Cargo's [patch.crates-io] or [replace] directives targeting the old versions will need updating.

NodeHandle::outer_html() now has a JS-evaluation fallback (0.13.x)

crates/stygian-browser/src/page.rs: if the primary CDP path returns an empty payload, the method now falls back to a JS evaluation (this.outerHTML via XMLSerializer) before returning "".

Signature unchanged. Highly dynamic pages that previously returned empty may now return the serialised DOM. Any consumer that asserted on empty results for known-empty elements will need to update its expectations.

mul_add reorder in build_runtime_policy (0.13.x)

crates/stygian-charon/src/policy.rs: f64::mul_add is bit-exact vs. the prior two-rounding expression. Output may differ by 1 ULP in edge cases. The score is clamped to [0.0, 1.0] and the function is deterministic, so this should not produce visibly different runtime behaviour — but consumers with snapshot tests on exact float values should regenerate those snapshots after upgrading.

MCP acquisition_result_to_tool_output gains a top-level freshness field (0.13.x)

crates/stygian-browser/src/mcp.rs: the JSON object returned to MCP clients of the browser server now includes a top-level "freshness" key.

Strict JSON-schema validators that assert no additional properties may fail. Most MCP clients ignore unknown fields; this is a minor concern for downstream consumers of the MCP surface.

cargo l alias now passes --all-features (0.13.x)

.cargo/config.toml: the l alias now runs strict clippy with --all-features permanently. Local preflight takes longer (compiles feature-gated lints). Intentional — codifies the feature-gated lesson learned during Phase 13 (see PROGRESS.md T-feature-gated-clippy: entry).


How compatibility changes are decided

  1. Additive changes are preferred. New modules, new optional fields with #[serde(default, skip_serializing_if = "Option::is_none")], new trait impls on local types, and new variants on enums marked #[non_exhaustive] are all preferred over breaking changes.

  2. Breaking changes require a documented migration path in this page AND in the PR description before merge.

  3. Pre-1.0 minor releases may include breaking changes with the conditions above. Post-1.0 (planned for a future major version line), breaking changes will be limited to major releases.

  4. Behavioural drift without a signature change (e.g. NodeHandle::outer_html()'s new fallback) is documented as "risky" with concrete test-impact guidance.


Compatibility tools

  • cargo l — strict workspace clippy with --all-features. The CI gate for new code.
  • cargo test --workspace --all-features — full test suite (lib + integration + doctest across all 7 crates; a handful of #[ignore] tests require live Chrome).
  • cargo build --workspace --all-features — release build gate.
  • actionlint for .github/workflows/*.yml files.