How Dino Discover Works
A glossary won’t do this section justice. The ideas in Dino Discover hang together — once you understand how a quiz becomes a recommendation, the admin UI and the REST API are both easier to read.
The pieces
Section titled “The pieces”| Term | What it means |
|---|---|
| Quiz | A named question flow with a slug, one or more variants, and a publish history. The thing a merchant edits. |
| Question | One step in the flow. Has a prompt, a type, and one or more answers. |
| Answer | One choice on a question. Carries an upvote mapping that weights products. Carries a routing pill that says what question comes next. |
| Variant | A version of the flow used for A/B testing. Each variant has its own routing graph and its own upvote weights. |
| Snapshot | An immutable, hashed serialisation of a published quiz. The storefront only ever reads snapshots. |
| Session | A single shopper’s run through a quiz. Carries answer history, the variant they were assigned, and the recommendation they were given. |
| Lead | An email captured during a session, with consent and routing metadata. |
A quiz is the unit a merchant edits. A session is the unit a shopper experiences. A snapshot is what connects them safely.
The four engines
Section titled “The four engines”Almost everything Dino Discover does runs through one of four engine
interfaces in includes/Engine/:
Question Engine
Section titled “Question Engine”Decides what question to ask next.
The default — RulesQuestionEngine — reads the answer history, looks up
the routing pill on the answer the shopper just chose, and returns the
next node decision (continue, end, jump to a specific question). Every
decision passes through the filter dino_discover_next_question, so a
plugin can override the choice per request.
Swap it via dino_discover_question_engine to use, for example, a
learned model that picks the most informative next question based on the
current answer history rather than following a static graph.
Recommendation Engine
Section titled “Recommendation Engine”Turns an answer history into a ranked list of products.
The default — UpvoteRecommendationEngine — sums each answer’s upvote
weights, applies eligibility filters (in-stock, purchasable, variant
matching, optional taxonomy gates), and returns a ranked
RecommendationSet. The final result list passes through
dino_discover_recommendation_results, so you can suppress, re-rank,
or inject items at the last second.
Swap it via dino_discover_recommendation_engine to substitute an
embedding-based or LLM-based ranker; the storefront and REST API don’t
change.
Coverage Engine
Section titled “Coverage Engine”Measures whether the question flow can actually reach every product in the catalogue.
The default — CoverageCalculator — walks every possible path through
the published snapshot, computes the maximum upvote score each product
could achieve, and writes the per-product result to a coverage table.
The result powers the Catalogue Coverage admin page: a scatter chart
of reachability, plus a “needs attention” list of products that the
current flow can’t reach.
Coverage runs nightly on a schedule, on every publish (debounced), and when you click Refresh now.
Statistics Engine
Section titled “Statistics Engine”Computes confidence intervals and sample-size verdicts for A/B variants.
The default — PointEstimateStatistics — returns a point estimate with
the CI bounds null. The interface is in place for a v2 backend that
returns proper intervals; the v1 default is the placeholder.
The analytics surface (EventAggregator, SankeyBuilder,
AttributionLinker) sits alongside these four — see
Engine Interfaces for the longer story.
How a shopper request flows through
Section titled “How a shopper request flows through”- The shopper opens a page that hosts the quiz. Dino Discover injects only a small bootstrap — variant-agnostic, year-cacheable.
- The shopper interacts with the bootstrap. The full storefront bundle
loads (built against
preact/compatto stay small). - The bundle calls
GET /wp-json/dino-discover/v1/storefront/quizzes/{slug}/bootstrap. That endpoint validatespublished_snapshot_id IS NOT NULLand returns the snapshot. If the quiz isn’t published, it returns 404. - The bundle calls
POST /storefront/sessionsto start a session. The variant assignor weighted-hashes the shopper into one of the active variants and returns a session bearer token. - The bundle renders the first question. Each answer fires a
POST /storefront/sessions/{sid}/eventsto record the interaction — batched, async, off the request path. - After the last question,
POST /storefront/sessions/{sid}/recommendruns the recommendation engine and returns the ranked products. - (Optional) The shopper enters an email;
POST /storefront/sessions/{sid}/leadstores it and pushes it to the configured integrations asynchronously.
The storefront never sees a draft. The whole pipeline runs against the immutable snapshot the merchant published.
How a merchant edit becomes a published snapshot
Section titled “How a merchant edit becomes a published snapshot”- The Builder makes a
PATCH /quizzes/{id}with the edit and theexpected_draft_versionit last saw. If the draft has moved on, the patch returns 409 — the merchant clicks Refresh and sees the newer state. - Edits accumulate in the draft tables. The Builder shows an “unsaved changes” banner; the storefront sees none of them.
- When the merchant clicks Publish, the Builder calls
POST /quizzes/{id}/publishwith the expected draft version. The publisher validates the draft, serialises it, hashes it, writes onedd_quiz_snapshotsrow, and atomically flipsdd_quizzes.published_snapshot_idto the new row. - The
dino_discover_quiz_publishedaction fires.
That’s it. There’s no half-published state, and there’s no second endpoint that ever reads from drafts.
Two safety properties worth keeping
Section titled “Two safety properties worth keeping”The published-only invariant. The storefront route only reads
published snapshots. This is enforced in code by a custom PHPStan rule
(PublishedSnapshotOnlyRule) — if a contributor accidentally adds a
storefront endpoint that reads drafts, the static analyzer fails the
build.
Optimistic locking. Every PATCH and every publish carries an
expected_draft_version. If two people are editing the same quiz, the
second writer hits 409 and reloads instead of clobbering the first
writer’s edits.
What’s next
Section titled “What’s next”- Drafts, Publishing & Snapshots — the lifecycle in more detail, including the rollback workflow.
- Question Builder — the merchant-facing how-to for shaping the flow.
- Recommendation Rules — how upvotes map answers to products.
- Engine Interfaces — the developer view of the same picture.