Skip to content

Engine Interfaces

Dino Discover’s recommendation behaviour is split into four PHP interfaces, each replaceable via a single site-wide filter. The storefront bundle and the REST API don’t change when you substitute an engine — they call the interface, and the container returns whatever the filter resolves.

This page is the contract reference. The defaults that ship with 0.1.0 are all listed below; if you’re implementing a replacement, this is what you’re returning.

All four live in includes/Engine/.

Decides what question to ask next.

namespace DinoDiscover\Engine;
interface QuestionEngineInterface {
public function next_node(
QuizContext $context,
AnswerHistory $history
): NextNodeDecision;
}
  • Default impl: RulesQuestionEngine — honours the routing pills the merchant set in the Builder, with a snapshot-aware fallback for answers that have disappeared since the session started.
  • Site-wide filter: dino_discover_question_engine.
  • Per-call filter on the result: dino_discover_next_question — fires every time next_node() returns, with the decision and context. Subscribers can override the decision.

NextNodeDecision is a readonly value object (in includes/Engine/ValueObject/) carrying a NodeType enum and an optional instance_id. NodeType has four cases:

CaseMeans
QuestionRender the question identified by instance_id.
TransitionRender the transition node identified by instance_id.
LeadCaptureRender the lead-capture step identified by instance_id.
TerminalEnd the quiz (move to results); instance_id is null.

Turns an answer history into a ranked product list.

namespace DinoDiscover\Engine;
interface RecommendationEngineInterface {
public function recommend(
QuizContext $context,
AnswerHistory $history
): RecommendationSet;
}
  • Default impl: UpvoteRecommendationEngine — sums per-answer upvote weights, applies eligibility filters (purchasable, in-stock, catalogue-visible), ranks by total upvotes with stable tie-breaking.
  • Site-wide filter: dino_discover_recommendation_engine.
  • Per-call filter on the result: dino_discover_recommendation_results — the last word on the recommendation, fires after the engine returns the RecommendationSet. This is where you re-rank, suppress, or inject items.

RecommendationSet carries a list of RecommendationItem values (each with product_id, score, rationale) plus engine metadata.

Measures whether the published flow can reach every product.

namespace DinoDiscover\Engine;
interface CoverageEngineInterface {
public function refresh(
\DateTimeImmutable $now,
?array $product_ids = null
): CoverageReport;
}
  • Default impl: CoverageCalculator — walks every path through the published snapshot, computes the maximum upvote score each product could achieve, writes per-product results to the dd_coverage_* tables. Runs nightly via Action Scheduler, on every publish (debounced), and on demand via the Coverage page’s Refresh now button.
  • Site-wide filter: dino_discover_coverage_engine.
  • Null impl: NullCoverageEngine (used in unit tests; returns an empty report).

CoverageReport carries the per-product score table plus aggregate statistics (total products, unreachable count, mean score).

Computes point estimates, confidence intervals, and significance for A/B variants in the Metrics tab. Three slots, three v1 behaviours:

namespace DinoDiscover\Engine\Statistics;
interface StatisticsInterface {
public function point_estimate(
string $metric,
MetricInput $input
): PointEstimate;
public function confidence_interval(
string $metric,
MetricInput $input,
float $alpha = 0.05
): ?ConfidenceInterval;
public function pairwise_significance(
MetricInput $control,
MetricInput $variant,
float $alpha = 0.05
): ?SignificanceResult;
public function method(): string;
}
  • Default impl: PointEstimateStatistics — returns a point estimate; confidence_interval() and pairwise_significance() both return null in v1. Rate-typed metric keys carry an inline sample-size verdict on the PointEstimate. A v2 statistics engine drops in via the same filter and fills the CI / significance slots without dashboard re-layout.
  • Site-wide filter: dino_discover_statistics_engine.
  • Null impl: NullStatistics.

SampleSizeVerdict is an enum with three cases: insufficient, marginal, sufficient.

The analytics surface is not a single PHP interface. It’s implemented as three concrete classes that the container wires together:

  • EventAggregator (includes/Engine/Analytics/EventAggregator.php) — aggregates raw events into the rolled-up dashboard tables.
  • SankeyBuilder (includes/Engine/Analytics/SankeyBuilder.php) — computes the per-edge flow counts for the planned Sankey visualisation.
  • AttributionLinker (includes/Engine/Analytics/AttributionLinker.php) — links completed orders back to the originating quiz session within the configured attribution window.

These aren’t filterable as a single unit today. If you need to swap analytics behaviour, the lower-level pattern is to subscribe to the lifecycle actions (see Hook Reference) and run your own aggregator alongside.

The pattern is identical for all four engine interfaces. Here’s swapping the recommendation engine:

// In your plugin or mu-plugin.
add_filter( 'dino_discover_recommendation_engine', function( $default ) {
// Build your replacement (typically through your own container
// or directly).
return new \MyAgency\Engine\EmbeddingRecommendationEngine(
/* dependencies */
);
} );

A few practical notes:

  • The filter runs at container-bind time, not per-request. You’re returning an instance, not a callable.
  • Type the return. The container expects an instance of RecommendationEngineInterface (or the relevant interface). If you return the wrong type, you get a fatal at bind time, not a silent fallback.
  • Don’t call the default and post-process. If you only want to re-rank the output, use the dino_discover_recommendation_results filter instead — it runs on the result of the default engine and is cheaper.

The technical design names a per-quiz filter variant for each engine — e.g. dino_discover_recommendation_engine_for_quiz — which would let a single site run different engines for different quizzes. As of 0.1.0 only the global single-arg filters fire. The per-quiz variants are wired for a v2 swap but not emitted today.

Question types are filterable via dino_discover_register_question_types. A registered question type carries:

  • A descriptor (label, icon, validators).
  • An admin editor (React component).
  • A storefront renderer (Preact component).
  • A server-side validator (PHP).
  • A JSON schema for the answer payload.

See includes/Engine/QuestionType/Registry.php for the registration contract.

Every interface ships a Null<Name> implementation alongside the default. The null impls return safe defaults (empty result, no-op refresh). They exist so tests can mount a controller without mounting a real engine.

If you’re writing tests for a service that depends on one of the engines, prefer the null impls over hand-mocking the interface.

  • Architecture — the systems map that shows where these engines fit.
  • Hook Reference — the filters above plus the lifecycle actions you’d subscribe to from a custom engine.
  • REST API Reference — the storefront endpoints that call into the engines.
  • Recommendation Rules — the merchant-facing side of the default upvote engine.