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.
The four interfaces
Section titled “The four interfaces”All four live in includes/Engine/.
QuestionEngineInterface
Section titled “QuestionEngineInterface”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 timenext_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:
| Case | Means |
|---|---|
Question | Render the question identified by instance_id. |
Transition | Render the transition node identified by instance_id. |
LeadCapture | Render the lead-capture step identified by instance_id. |
Terminal | End the quiz (move to results); instance_id is null. |
RecommendationEngineInterface
Section titled “RecommendationEngineInterface”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 theRecommendationSet. 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.
CoverageEngineInterface
Section titled “CoverageEngineInterface”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 thedd_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).
StatisticsInterface
Section titled “StatisticsInterface”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()andpairwise_significance()both returnnullin v1. Rate-typed metric keys carry an inline sample-size verdict on thePointEstimate. 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
Section titled “The analytics surface”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.
How to substitute an engine
Section titled “How to substitute an engine”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_resultsfilter instead — it runs on the result of the default engine and is cheaper.
Per-quiz overrides
Section titled “Per-quiz overrides”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.
Custom question types
Section titled “Custom question types”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.
Null implementations
Section titled “Null implementations”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.
What’s next
Section titled “What’s next”- 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.