=== Seekmodo Search ===
Contributors: numinix
Tags: search, ai-search, autocomplete, woocommerce, typeahead
Requires at least: 6.2
Tested up to: 7.0
Requires PHP: 8.0
Stable tag: 0.11.7
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

AI-powered search for WordPress and WooCommerce. Vector + keyword recall, commerce-aware reranking, native search permanently armed as a fallback.

== Description ==

**Seekmodo Search** replaces the built-in WordPress site search (and the
WooCommerce product search, when WooCommerce is active) with an AI-powered
search experience served by the Seekmodo platform at mcp.seekmodo.com.
One plugin, two activatable verticals:

* **Content vertical** — WordPress core search via `pre_get_posts` +
  `posts_pre_query`. Auto-detects public Custom Post Types so blog posts,
  pages, docs, and your custom CPTs all participate in the same ranked
  result set.
* **Commerce vertical** — WooCommerce product search via
  `woocommerce_product_query` + `posts_search`. Indexes products plus
  variations, stock state, price, SKU, and attributes. Auto-on when
  WooCommerce is detected; dormant otherwise.

What you get:

* AI search out of the box — vector recall + commerce-aware reranking
  on top of Typesense, with no relevance tuning required to start.
* Typo tolerance, partial-word matching, and prefix-matched typeahead
  via a single REST endpoint your theme can hit from any autocomplete
  widget.
* Click-through telemetry feeds a self-tuning learn-to-rank pipeline.
  Relevance gets better as your shoppers interact.
* Always-on safety net — every gateway-talking entry point falls
  through to native `WP_Query` (or `WC_Query` for products) inside the
  same request when the platform is unreachable. Visitors never see an
  empty results page. A per-tenant circuit breaker prevents repeated
  timeouts from compounding.
* Multi-environment safe — Seekmodo locks each tenant to a canonical
  storefront host. Staging / dev clones are auto-detected and return
  no-op from every gateway entry, so your production index is never
  polluted by a non-production deploy.
* Settings tunable remotely — flip search mode, adjust ranking knobs,
  and toggle the "Powered by Seekmodo" badge from
  <https://admin.seekmodo.com>; the connector picks the change up on
  the next 5-min cadence.

**External service dependency.** Seekmodo Search is the WordPress
connector for the Seekmodo AI search platform. The platform is a
hosted service operated by Numinix Technology Inc. at
<https://mcp.seekmodo.com> and <https://admin.seekmodo.com>. The
plugin connects your site to a Seekmodo tenant via an HMAC-signed
pairing flow; once paired, the plugin sends indexable post / product
data to mcp.seekmodo.com and queries it for search results, typeahead
suggestions, and click telemetry.

**Free trial.** Every Seekmodo plan ships with a 14-day free trial at
$0 — full features, unlimited searches, no card required upfront.
Cancel before day 14 and you pay nothing. After the trial, plans
start at $29/month. Pricing details, including how the plan auto-sizes
to your real traffic during the trial: <https://seekmodo.com/pricing>.

**What happens if you stop paying.** The plugin does not break your
site. If the gateway is unreachable, your subscription lapses, or
you simply uninstall the plugin, every search entry point falls back
to native WordPress / WooCommerce search inside the same request. No
white screens, no fatal errors, no broken search box.

**Privacy + data handling.**
<https://seekmodo.com/legal/privacy> lays out the full picture, but
in short: searchable post / product content you choose to index is
sent to mcp.seekmodo.com; visitor search queries and clicks are sent
to mcp.seekmodo.com as telemetry; no personal data about your
visitors leaves your site unless you opt in to additional
analytics integrations from the Seekmodo admin. The plugin
self-checks for stale indexes, configuration drift, and update
availability — all such phone-home requests are vanilla HTTPS GETs
that carry no tenant ID and no site-identifying data; they're
static-cacheable from our perspective.

== Installation ==

Either:

1. In WordPress admin, go to **Plugins → Add New** and search for
   "Seekmodo".
2. Click **Install Now** on "Seekmodo Search", then **Activate**.

Or upload manually:

1. Download the latest zip from
   <https://seekmodo.com/plugins/wordpress>.
2. Upload via **Plugins → Add New → Upload Plugin** → choose the
   zip → **Install Now** → **Activate**.

Then in either case:

3. Go to **Settings → Seekmodo Search → Connect to Seekmodo**.
4. Click **Start free trial / sign in** and approve the pairing on
   seekmodo.com.
5. (Optional) Run `wp seekmodo index --full` to seed your tenant
   immediately. (The plugin will otherwise reindex incrementally on
   its own.)
6. Flip search mode from `shadow` to `active` in the connector
   settings once you've spot-checked a few searches.

Headless / CI install:

`wp plugin install seekmodo --activate`
`wp seekmodo connect --tenant=<tenant_id> --secret=<shared_secret>`
`wp seekmodo index --full`
`wp option update seekmodo_wp_mode shadow`

== Frequently Asked Questions ==

= Do I need a Seekmodo account? =

Yes. Seekmodo Search is the WordPress connector for the Seekmodo
hosted AI search platform; the plugin itself is free and GPLv2, but
it needs a paired Seekmodo tenant to do its job. Every plan ships
with a 14-day free trial at $0 with full features and unlimited
searches. After the trial, plans start at $29/month. Pricing details:
<https://seekmodo.com/pricing>.

= Do I need WooCommerce? =

No. The **content vertical** works on any WordPress site (posts,
pages, public custom post types). The **commerce vertical** activates
automatically when WooCommerce is detected, and idles otherwise.

= What happens if the gateway is down? =

Every gateway-talking entry point falls through to native `WP_Query`
(or `WC_Query` for products) inside the same request. Visitors never
see an empty results page. A per-tenant circuit breaker prevents
repeated timeouts from compounding, and Seekmodo health metrics
auto-demote the connector back to native search if our gateway is
unhealthy.

= What happens if I stop paying / cancel my subscription? =

Same as the previous answer. The plugin stays installed, the
gateway-talking entry points start returning "no results" (since the
tenant is gated), and every entry point falls back to native search
in the same request. Your site keeps working; you just stop getting
AI relevance. Re-pairing with a paid tenant restores full
functionality.

= Will this work on a staging clone? =

Yes — Seekmodo locks each tenant to a canonical storefront host.
Staging / dev hosts are auto-detected and return no-op from every
gateway-talking entry point, so your production search index is never
overwritten by a non-production deploy.

= Where are my credentials stored? =

In `wp_options` (`seekmodo_wp_tenant_id`, `seekmodo_wp_shared_secret`).
`wp seekmodo connect` can be re-run any time to rotate the pair.

= Where does the plugin send data? =

Indexable post / product content goes to `mcp.seekmodo.com` (the
gateway). Visitor search queries and clicks go to `mcp.seekmodo.com`
as telemetry. The update check (12 h cadence) goes to
`seekmodo.com/api/plugins/seekmodo`. Configuration pull (5 min
cadence) goes to `mcp.seekmodo.com/v1/tenant/snapshot`. All
gateway-bound requests carry an HMAC signature derived from the
pairing secret. Full details in
<https://seekmodo.com/legal/privacy>.

= Does the plugin support translations? =

The admin UI is localised via the `seekmodo` text domain
and `/languages/` directory. We ship English (`en_US`) as the source
locale; contributions for other locales are welcome.

= How do I get help? =

* Support email: <support@seekmodo.com>
* Docs: <https://seekmodo.com/docs/wordpress>
* WordPress.org support forum (replies within ~1 business day during
  weekdays)

== Screenshots ==

1. **Settings → Seekmodo Search.** Pair status, storefront-host lock
   indicator, Re-pair control, and Health (mode + circuit breaker).
2. **Settings → Health + indexable types.** Mode / breaker readouts,
   indexable post-type selector, and search ranking priority.
3. **Storefront suggest widget.** Gateway-ranked products, categories,
   and keyword suggestions for a live storefront search.

== Upgrade Notice ==

= 0.11.7 =
WordPress.org compliance: text domain seekmodo, no custom updater, branding off by default, privacy IP/UA disclosure.

= 0.11.6 =
WordPress.org submission readiness: vendored web-components bundle
(no default CDN), complete uninstall cleanup, Tested up to 7.0,
privacy-policy guide text, Health widget restricted to admins.

= 0.6.0 =
SM-606 Universal Suggest Widget. When the "Enable connector typeahead
UI" master toggle is on, v0.6.0 swaps the legacy flat-row vanilla-JS
dropdown for the new `<seekmodo-suggest>` web component, which renders
the rich /v1/suggest envelope (recent + trending + keywords +
products + categories + did-you-mean + "View all N results" CTA) and
shares its UX with the BigCommerce / Zen Cart / AKS connectors.
Existing sites can opt back into the legacy dropdown for one major-
version cycle via Settings → Seekmodo → "Use legacy flat-row typeahead"
or the new `seekmodo_wp_suggest_legacy` option. Bundle adds a single
~7 KB gzip JS asset under `assets/suggest/`. Adds a public
`POST /wp-json/seekmodo/v1/browser-token` route so long-running tabs
can refresh the gateway-direct token without a page reload.

= 0.5.3 =
Two follow-ups from PM-303. Operator UX: `wp seekmodo index --full` now
covers BOTH verticals (content + commerce) by default and accepts a
new `--vertical=content|commerce|all` flag. Pre-0.5.3 it only ran the
content indexer, forcing commerce-only operators to drop into
`wp eval '\Numinix\SeekmodoWordPress\Commerce\Indexer::reindexFull();'`.
Safety: a stricter write-side host-lock gate (`DomainLock::canIndex()`)
fails closed by default on non-prod hosts (`dev.*`, `staging.*`,
`stage.*`, `test.*`, `qa.*`, `preview.*`, `beta.*`, `preprod.*`,
`sandbox.*`) when the storefront-host lock is self-referential — the
exact regression that put `dev.pocomarine.com` URLs into POCO's
production Typesense index after the prod cutover. Read paths
(typeahead, search swap) are unchanged. Operators with a legitimate
non-prod indexing setup (e.g. paired to a separate `*-dev` tenant)
opt back in via:
    add_filter('seekmodo_allow_nonprod_indexing', '__return_true');
in a mu-plugin. Mandatory upgrade for any tenant with a paired dev /
staging WordPress install.

= 0.5.2 =
Hotfix for the gateway-direct typeahead introduced in v0.5.0. The
storefront browser script was rendering an empty "No results" state
on commerce-vertical sites because the canonical commerce row from
`/v1/typeahead` (per `CONNECTOR_TYPEAHEAD_SPEC.md`) ships rows as
`name` / `url` / `image` / `in_stock`, while the renderer only knew
the legacy WP REST proxy shape (`title` / `permalink` / `image_url` /
`stock_status`). The gateway-direct fast path therefore looked up
`r.title` on every row, found `undefined`, and skipped them all —
the user saw "No results" for every keystroke even though the wire
returned 8 rows. Mandatory upgrade for any commerce-vertical install
running v0.5.0 or v0.5.1.

= 0.5.1 =
Self-healing cache flushes. The connector now detects on-disk version
bumps on the very first request after any deploy (rsync, wp-cli, or
WordPress's native upgrader) and fans out cache invalidations to WP
Rocket, LiteSpeed, W3 Total Cache, WP Super Cache, WP-Optimize,
Hummingbird, Cache Enabler, Pantheon, Kinsta, Cloudflare WP, plus
OPCache and the WordPress object cache. Fixes the "I deployed v0.5.0
but the site still loads v0.4.0" class of regression on hosts with
WP Rocket page + minify caching configured for 1 year. Mandatory
upgrade if you have any page-cache plugin installed.

= 0.5.0 =
Performance: typeahead now talks directly to the Seekmodo gateway from
the storefront browser, skipping the WordPress REST hop. Drops the
per-keystroke latency from ~2 seconds (WordPress full-stack boot on
each request) to ~80–150 ms on heavy WooCommerce / Wordfence sites.
Also adds a 32-entry client-side LRU cache so backspace + retype
on a previously-fetched prefix renders instantly without a network
round trip. Falls back to the WordPress REST endpoint automatically
on any gateway-direct failure (token expiry, gateway 5xx, breaker
open) — your search bar never breaks.

= 0.4.0 =
New feature: drop-in typeahead dropdown UI. The connector now ships a
vanilla-JS search-suggestions box that auto-binds to your theme's
`<input type="search">` / `<input name="s">` and renders gateway-
powered results as you type, with vertical-aware row shapes (blog
posts vs WooCommerce product cards). Default-OFF — flip it on from
Settings → Seekmodo Search → Typeahead dropdown when you're ready,
or leave it off if your theme already provides its own typeahead.
Optional "Powered by Seekmodo" footer is opt-in (white-label by
default).

= 0.3.4 =
New feature: editorial post-type ranking priority. Search results
can now be bucket-sorted so that pages outrank blog posts (or any
other order you specify) while textual relevance is preserved within
each tier. No-op upgrade for sites that don't set the new option —
gateway-relevance order keeps flowing through untouched.

= 0.3.3 =
Critical fix: the v0.3.2 pair-callback REST route was registered
behind `is_admin()` so it never appeared on the server-to-server
callback POST from seekmodo.com. Pairing was still impossible.
v0.3.3 lifts the registration out of the admin-only block. Mandatory
upgrade for anyone who hasn't paired yet.

= 0.3.2 =
Critical fix: the "Connect to Seekmodo" pairing flow has been rebuilt
on the canonical install_token + signed-JWT protocol. v0.3.0 / v0.3.1
sent legacy `nonce` / `return_to` query params that seekmodo.com
rejects with "This link is incomplete" — pairing through the browser
was impossible. Superseded by v0.3.3 — install that one instead.

= 0.3.1 =
Bug fix: the "Connect to Seekmodo" button silently redirected to the
WP admin homepage instead of seekmodo.com (wp_safe_redirect() rejecting
the off-site host). Pairing was impossible without manual wp-cli
fallback. Superseded by v0.3.2 — install that one instead.

= 0.3.0 =
Commerce vertical realigned to the gateway's Typesense schema —
WooCommerce indexing now actually works (was 400'ing every upsert).
Schema-version auto-promotes a one-time full reindex on the
commerce side; content vertical is undisturbed. Ships the
self-service "Promote dev to production" admin card so merchants
can flip their canonical from a dev site to production without an
operator round-trip. Worth picking up before standing up your
WooCommerce dev site.

= 0.1.4 =
Adds WordPress.org Plugin Directory listing support: the `Update URI`
header now routes update checks to seekmodo.com so we can ship
hot-fixes faster than the w.org SVN sync cadence. No upgrade work
required; just update.

= 0.1.3 =
Per-tenant "Powered by Seekmodo" branding toggle. Off by default;
opt-in for partner sites.

= 0.1.0 =
Initial public release.

== Changelog ==

= 0.11.7 =
* Text domain seekmodo; removed Update URI / WporgUpdateClient for Directory updates.
* Suggest branding defaults off; privacy guide mentions IP/UA/referer.
* Plugin Check ERROR cleanups (escaping, SQL prepare, ABSPATH, packaging excludes).

= 0.11.6 =

* WordPress.org submission readiness: vendored full
  `@seekmodo/web-components` ESM under `assets/web-components/` (no
  default CDN load; override via `seekmodo_web_components_src`).
* Complete uninstall cleanup (options, local-index tables, crons,
  transients).
* `Tested up to: 7.0`. Health dashboard widget requires
  `manage_options`. Suggested privacy-policy guide text added.
* Cart cascade ranks multi-line carts by `support_count` across up
  to 10 anchors.

= 0.11.5 =

* PDP/cart recommendation cascades (`pdp-cascade`, `cart`) via
  `/wp-json/seekmodo/v1/recommendations` with cross-section de-dupe and
  in-cart `exclude_doc_ids`, matching the AKS cascade contract.
* Auto-injects placeholders on WooCommerce PDP/cart templates and
  refreshes after cart fragments; enqueues `@seekmodo/web-components`
  on those templates even without Gutenberg blocks.

= 0.11.4 =

* Stop requiring REQUEST fingerprints for add_to_cart mirrors (theme/custom AJAX was dropping all ATCs). Session-restore skip remains the spam guard.
* Also emit purchases on woocommerce_payment_complete (deduped) when thankyou never renders.

= 0.11.3 =

* Events flush uses an 8s timeout and smaller batches (clicks first) so ATC attribution no longer wedges the queue.
* Skips cart-session restore add_to_cart mirrors; only intentional shopper adds are queued.
* Persists sm_seid cookie for SERP click attribution when footer beacon localization races.

= 0.11.2 =

* Stamps per-shopper session_id + visitor IP/UA on search and click events for bot-check and fleet linkage.
* Drops unlinked SERP beacon clicks (missing search_event_id) that were polluting LTR / fleet health.

= 0.8.9 =

* Shared thumbnail resolver for content typeahead and indexing (featured
  image, inline content image, seekmodo_thumbnail_url filter).

= 0.8.8 =

* Content typeahead unions gateway hits with native WP rows and applies
  post-type priority so pages surface before posts (trek.ca parity with SERP).
* Suggest widget always merges the REST typeahead fallback when configured.

= 0.8.7 =

* Fixes WooCommerce SERP showing product categories instead of ranked
  gateway products after the search swap.
* Suggest dropdown merges REST typeahead fallback when gateway suggest
  returns no product rows (content sites like trek.ca).

= 0.8.6 =

* Fixes WooCommerce SERP titles showing `Search results: ""` after the
  gateway search swap clears the query `s` var.

= 0.8.4 =

* Fixes mobile split-rail draggable divider (both panels resize again).
* Refreshes the vendored `@seekmodo/web-components` suggest bundle.

= 0.8.3 =

* Typeahead product-row clicks thread `search_event_id` from `/v1/suggest`
  meta for LTR click attribution.
* Refreshes the vendored `@seekmodo/web-components` suggest bundle (split-rail
  mobile divider, native title tooltips, anchor performance fixes).

= 0.8.2 =

* Fixes typeahead and search-swap click attribution (rank/surface metadata).
* Refreshes suggest bundle with split-rail scroll and thumb scaling fixes.

= 0.8.1 =

* CORS-block UX when gateway script loads or suggest fetches are blocked.
* Refreshes suggest bundle to v0.2.2.

= 0.8.0 =

* Enhanced Native search layer (Tier 1 + Tier 2) for unpaired installs.

= 0.6.0 =

* **SM-606 Universal Suggest Widget**: when the operator-facing
  "Enable connector typeahead UI" toggle is on, v0.6.0 enqueues the
  new `<seekmodo-suggest>` custom element (~7 KB gzip, lives at
  `assets/suggest/seekmodo-suggest.global.js`) instead of the legacy
  flat-row dropdown. The web component renders the rich `/v1/suggest`
  envelope from the gateway (WS-2): recent + trending + keywords +
  products + categories + did-you-mean + "View all N results" CTA,
  all from one server round-trip. UX is shared 1:1 with the
  BigCommerce / Zen Cart / AKS / WordPress connectors so a tenant
  with multiple storefronts gets one suggestions UI everywhere.
* **Opt-out flag**: new `seekmodo_wp_suggest_legacy` per-site option
  (Settings → Seekmodo → "Use legacy flat-row typeahead instead of the
  universal suggest widget") flips back to the v0.5.x dropdown for one
  major-version cycle. The two surfaces are mutually exclusive —
  `Frontend\TypeaheadUI::isEnabled()` returns false when the flag is
  off, preventing double-attaching dropdowns to the same input.
* **Browser-token refresh**: new public `POST /wp-json/seekmodo/v1/browser-token`
  route returns `{token, expires_at, session_id}` so the in-bundle SDK
  can refresh a 5-min JWT without a full page reload. Cached behind
  the existing per-tenant transient in `Gateway\BrowserToken` so a
  long-running tab amounts to ~one mint per ~4 min regardless of how
  many keystrokes the shopper triggers.
* **Customization surface (theme-side)**:
    * `seekmodo_suggest_selectors` — CSS-selector list the autoboot
       walks (default `input[type="search"]`, `input[name="s"]`,
       `input[data-seekmodo-suggest]`, `input[data-seekmodo-typeahead]`).
    * `seekmodo_suggest_blocks` — CSV of blocks the widget should
       render in order (default `recent,did_you_mean,keywords,trending,
       products,categories`). Drop any block by leaving it out.
    * `seekmodo_suggest_view_all_href` — URL template for the CTA.
       Default `/?s={q}` (or `/?s={q}&post_type=product` on Woo sites).
    * `seekmodo_suggest_extra_attrs` — assoc map of additional
       attributes (e.g. `min-length`, `debounce-ms`, `limit`,
       `cache-size`, `lang`) forwarded to every spawned element.
    * `seekmodo_suggest_config` — last-resort filter on the localized
       JS config.
    * Shadow-DOM CSS Custom Properties (`--seekmodo-suggest-bg`,
       `--seekmodo-suggest-row-hover`, `--seekmodo-suggest-thumb-size`,
       etc.) so child themes can theme the dropdown without forking
       its markup.
    * `<slot name="recent|keywords|trending|products|categories|empty|
       header|footer">` lets a theme drop bespoke HTML inside an
       element and override any one block's rendering wholesale.
* **Performance**: matches the AKS-3801e baseline (sub-300 ms p95
  cold cache, <16 ms warm-cache render) via the bundle's built-in
  150 ms keystroke debounce, AbortController cancel-on-next-keystroke,
  32-entry LRU cache, and single rAF-batched render. Skeleton loader
  masks the first-render latency on a cold cache.

= 0.5.3 =

* **Operator UX**: `wp seekmodo index --full` now reindexes every
  gateway-enabled vertical for the tenant (content + commerce, in
  series), and accepts a new `--vertical=content|commerce|all` flag
  for surgical per-vertical reindexes. Pre-0.5.3 the command only
  invoked `Content/Indexer::reindexFull()`; commerce-only operators
  (and dual-vertical operators chasing a stale product index) had to
  drop into `wp eval '\Numinix\SeekmodoWordPress\Commerce\Indexer::reindexFull();'`.
  Verticals the gateway hasn't enabled for this tenant — or whose
  runtime dependency is missing (e.g. WooCommerce inactive) — are
  skipped silently with an info-level log line; the command reports
  the per-vertical totals separately so the operator can tell at a
  glance whether each vertical actually executed.
* **Safety**: stricter write-side host-lock gate, exposed as
  `DomainLock::canIndex()` and surfaced in `wp seekmodo health` /
  `wp seekmodo diagnose` as a new `can_index` row. Used by every
  gateway-mutating helper (`numinix_seekmodo_run_bulk_upsert`,
  `numinix_seekmodo_mirror_click`) plus the cron-tick early-out in
  both `Content/Indexer::tick()` and `Commerce/Indexer::tick()`.
  Failure modes the new gate catches that the looser
  `DomainLock::matches()` did NOT:
  - **Self-referential lock on a non-prod host.** `dev.example.com`
    seeds its `seekmodo_wp_storefront_host` to its own home_url() at
    activation; `matches()` returns true (current==locked); pre-0.5.3
    indexer cron would happily push every product save to the prod
    tenant's Typesense collection, with `dev.example.com` URLs
    poisoning the customer-visible search results. Exact PM-303
    regression.
  - **Unset lock on a non-prod host.** A freshly-installed dev clone
    that hasn't completed its first 5-min `tenant.snapshot` pull yet:
    pre-0.5.3 `matches()` returned true (no lock → tentative allow).
    0.5.3 fails closed.
* **Safety**: read paths (typeahead, search-swap, click-beacon
  enqueue) keep gating on `numinix_seekmodo_enabled()` so an
  operator smoke-testing search behavior on a staging clone still
  gets results — only writes are tightened.
* **Safety**: two override filters, intentionally separated so a
  read-side bypass doesn't accidentally also unlock writes:
  - `seekmodo_disable_storefront_host_lock` (existing, since v0.1.0)
    — read AND write; bypasses every check. Used by blue/green CDN
    setups where the request reaches WP under a different Host
    header than the visitor sees.
  - `seekmodo_allow_nonprod_indexing` (NEW, v0.5.3) — write only;
    consenting non-prod indexer for installs paired to a separate
    `*-dev` tenant. Defaults false.
* **Operator UX**: `wp seekmodo health` now prints `current_host`,
  `locked_host`, `host_lock_match`, `can_index`, and
  `looks_like_nonprod` instead of the older single `host_lock_match`
  row, so an operator chasing "why isn't dev indexing?" can see
  every input the gate consults at a glance.

= 0.5.1 =

* **Reliability**: deploy-channel-agnostic version-bump detector. The
  connector now compares the on-disk `SEEKMODO_WP_VERSION` constant
  to the last-seen value stored in `wp_options` on every request,
  and on a mismatch fans out cache invalidations to every page-cache
  / minify / object-cache / OPCache plugin we know about. Closes the
  "I rsync'd v0.5.0 onto the server but visitors still load v0.4.0"
  regression that surfaced on POCO after the v0.5.0 push: WP Rocket
  was serving the cached v0.4.0 minified JS for 1 year because it
  only invalidates on `upgrader_process_complete` (which rsync
  deploys never fire).
* **Reliability**: explicit OPCache reset on version bump. Hosts
  with `opcache.validate_timestamps=0` (typical on shared hosting
  for performance) silently kept executing the old PHP class
  bytecode after rsync. The reset is wrapped in a function-exists
  guard so PHP builds without OPCache compiled in stay safe.
* **Reliability**: browser-token transient explicitly invalidated
  on version bump so the next typeahead keystroke re-mints a token
  signed against whatever the new gateway expects, instead of
  inheriting a payload-shape from the prior connector version.
* **Maintenance**: fan-out is gated behind a one-shot atomic option
  write so two concurrent first-bump requests don't double-fire the
  cache flush. Hosts running bespoke cache infra can suppress the
  fan-out entirely with the new `seekmodo_skip_upgrade_flush`
  filter, or extend it with extra do_action keys via
  `seekmodo_upgrade_flush_actions`.
* No new operator-facing settings; mass-deployable as a no-op
  update on every connector site.

= 0.5.0 =

* **Performance**: typeahead now POSTs `mcp.seekmodo.com/v1/typeahead`
  directly from the storefront browser using a short-lived browser
  token minted server-side at page render. Skips the WordPress REST
  hop (and the ~1.5–2 s WordPress full-stack boot it pays on a heavy
  WooCommerce + Wordfence install) on every keystroke.
  Per-keystroke latency on POCO observed dropping from ~2,250 ms to
  ~80–150 ms.
* **Performance**: 32-entry client-side LRU cache keyed on the
  normalized `(vertical, max, q)` prefix. Backspace + retype on a
  previously-fetched prefix renders synchronously from in-memory
  cache, no network round-trip. Cache size is operator-tunable via
  the `seekmodo_typeahead_config` filter.
* **Performance**: server-side `Cache-Control: public, s-maxage=30,
  must-revalidate` + `Vary: X-Seekmodo-Tenant` on gateway typeahead
  responses lets Cloudflare / Varnish absorb keystroke storms on
  hot prefixes without leaking rows across tenants.
* **Performance**: per-FPM-process APCu micro-cache (10 s TTL) on
  the gateway side defends Typesense from a hot prefix when the
  edge cache is cold.
* **Added**: `seekmodo_typeahead_gateway_direct` filter. Defaults
  `true`. Site can `__return_false` to force every typeahead call
  back to the WordPress REST endpoint — useful for incident-response
  / debugging without a connector update.
* **Added**: `Gateway/BrowserToken.php` — caches the per-tenant
  browser-token mint in a WordPress transient (refresh window
  honored, hard-cap 240 s). The mint failure mode silently falls
  back to the REST hop, so the typeahead never breaks regardless
  of token availability.
* **Fixed**: pre-existing regression where `Client::typeahead()`
  POSTed `/v1/typeahead` against the gateway, the gateway returned
  `404 tool_not_found`, and the connector fell back to a native
  `WP_Query` LIKE search on every keystroke (~2.3 s on POCO,
  ~1.1 s on Trek.ca). The new gateway-side `TypeaheadTool` (SM-602
  on the platform side) is what makes the gateway-direct path
  return real Typesense rows in ~80 ms.

= 0.4.0 =

* **Added**: front-end typeahead dropdown UI. The connector now ships
  `assets/typeahead/seekmodo-typeahead.{js,css}` plus
  `src/Frontend/TypeaheadUI.php` — a vanilla-JS dropdown that
  auto-binds to common search inputs (`input[type="search"]`,
  `input[name="s"]`, `input[data-seekmodo-typeahead]`), debounces
  input at 150ms, and renders rows from the existing
  `/wp-json/seekmodo/v1/typeahead` REST endpoint. Two row shapes:
  content rows (title + post-type meta + excerpt + thumbnail) and
  commerce rows (product card with price / sale / stock badges).
* **Added**: optional "Powered by Seekmodo" footer in the dropdown,
  driven by the existing `seekmodo_show_branding` filter +
  `OPT_SHOW_BRANDING` option. Default OFF — partner sites flip it on
  via the Settings page or have it pushed down by the gateway's
  tenant-snapshot config pull. White-label by default.
* **Added**: per-site "Enable typeahead" + "Show branding" toggles
  on Settings → Seekmodo Search → Typeahead dropdown. The enable
  toggle defaults OFF so an upgrade on a site that already has its
  own theme-level typeahead (e.g. trek.ca's
  `inc/seekmodo-search.php`) doesn't double-render.
* **Added**: `seekmodo_enqueue_typeahead`,
  `seekmodo_typeahead_selectors`, and `seekmodo_typeahead_config`
  filters so themes can suppress or customise the connector UI
  without forking the JS.
* **Compat**: existing `assets/typeahead/seekmodo-beacon.js` click
  attribution picks up `data-seekmodo-id` / `data-seekmodo-pos` /
  `data-seekmodo-surface="typeahead"` attributes the new dropdown
  emits — no second beacon, no separate REST endpoint, click-through
  data flows through the same EventsQueue pipeline.

= 0.3.4 =

* **Added**: editorial post-type ranking priority. After the gateway
  scores results by textual relevance, hits whose post type appears
  earlier in the configured priority list are bucket-sorted to the
  top. Within each tier the gateway's relevance order is preserved
  -- we don't re-score textual relevance, just bucket-sort by post
  type. Configurable via the new `Search ranking priority` field on
  Settings → Seekmodo Search, or by setting the
  `seekmodo_wp_post_type_priority` option directly. The
  `seekmodo_post_type_priority` filter overrides for site-specific
  mu-plugins. Default is empty -- no re-rank, gateway order flows
  through untouched. Fixes the trek.ca complaint where blog posts
  dominated `?s=PPC` while the high-intent `/services/` pages got
  buried below the fold.

= 0.3.3 =

* **Fixed**: the v0.3.2 pair-callback REST route at
  `/wp-json/seekmodo/v1/connect/callback` was registered behind
  `Bootstrap::boot()`'s `is_admin()` gate, so the route never
  appeared on the server-to-server POST from seekmodo.com (REST API
  requests are not "admin" in WordPress's classification). The
  admin-side Connect button worked, but the JWT callback POST
  silently 404'd and pairing died at the last step. v0.3.3 lifts
  `ConnectFlow::register()` out of the `is_admin()` block so the
  `rest_api_init` hook always runs. The
  `admin_post_seekmodo_wp_connect` hook stays registered globally
  too (harmless -- it only fires on `admin-post.php` hits).

= 0.3.2 =

* **Fixed**: the "Connect to Seekmodo" pairing flow has been
  rebuilt on the canonical `install_token` + signed-JWT protocol
  (the same protocol Seekmodo for Zen Cart has used since v1.0.x).
  v0.3.0 and v0.3.1 sent legacy `nonce` + `return_to` query params
  that seekmodo.com rejects with "This link is incomplete" — the
  redirect-back side of that protocol was an aspirational design
  that was never implemented on the seekmodo.com side. Pairing
  through the browser was impossible until this release.

  - `Admin\ConnectFlow::initiate()` now mints a 32-hex
    `install_token` (10-min TTL) and sends
    `?install_token=<t>&callback=<rest-url>` to seekmodo.com/connect.
  - The new `Admin\ConnectFlow::handleCallback()` is registered as a
    public REST endpoint at
    `wp-json/seekmodo/v1/connect/callback`. seekmodo.com POSTs
    `{token, install_token}` to that URL with the
    `X-Seekmodo-Install-Token` header.
  - The new `Connect\Pairing` class verifies the EdDSA-signed JWT
    against `https://seekmodo.com/.well-known/jwks.json`, validates
    the standard claims (iss/aud/exp/install_token), and persists
    `tenant_id` + `shared_secret` + `mcp_url`.

* **Removed**: `Gateway\Client::verifyClaimToken()` and the matching
  `wp seekmodo connect --token=<...>` CLI flag. They both round-tripped
  through a `/v1/connect/verify` endpoint that the gateway never
  implemented; users hitting that path were getting silent failures.
  The CLI pair still works via `--tenant=` `--secret=` for
  headless / CI installs.

* **Required**: PHP `sodium` extension (ext-sodium) -- needed for the
  JWT signature verify. It's part of core PHP since 7.2 and shipped
  on default cPanel/EasyApache builds, but if it's not loaded the
  pair-callback surfaces a cPanel-specific install hint
  (`yum install -y ea-php82-php-sodium`).

= 0.3.1 =

* **Fixed**: `Admin\ConnectFlow::initiate()` called
  `wp_safe_redirect('https://seekmodo.com/connect?...')` without
  whitelisting the connect host. WP's redirect validator silently
  rewrote the destination back to the site root, landing the
  operator on the WP admin homepage instead of the connect page —
  pairing was impossible via the browser flow. The `wp seekmodo
  connect` CLI path was unaffected. Now registers a one-shot
  `allowed_redirect_hosts` filter for the connect host (overridable
  via the existing `seekmodo_connect_base_url` filter) for the
  duration of the redirect call only.

= 0.3.0 =

* **Changed**: Commerce vertical doc shape realigned to the gateway
  Typesense schema. `Commerce\ProductFields::buildDoc()` now emits
  gateway-canonical field names — `name`, `url`, `in_stock` (bool),
  `category_id[]`, `category_breadcrumbs[]`, `image_url`,
  `description` (combined excerpt + content_plain, HTML-stripped, 4
  KB cap), `last_indexed_at`, plus optional `on_sale`, `sale_price`,
  `currency`. Removes pre-v0.3.0 WP-shaped names (`title`,
  `permalink`, `stock_status` string, `regular_price`, `excerpt`,
  `content_plain`, `attributes`, `variations[]`, `categories`,
  `post_type`, `extra`) that the gateway's strict-schema Typesense
  was rejecting on every upsert.
* **Changed**: Commerce schema version split out from the content
  schema version so a commerce-only realignment doesn't trigger an
  unnecessary content-side full reindex. New
  `Options::OPT_COMMERCE_SCHEMA_VERSION` (`commerce_schema_version`
  in wp_options) is consulted by `Commerce\Indexer::tick()` and
  triggers a one-shot full reindex on upgrade — same trick as the
  v0.1.1 content-side bump.
* **Added**: Self-service "Promote dev → production" card on the
  Seekmodo Search settings page. Surfaces when the connector's
  current host looks like a non-prod environment
  (`dev|staging|stage|test|qa|preview|sandbox`) and the canonical
  lock matches that host. One-click POST to the gateway's new
  `tenant.domain.promote` MCP tool flips `locked_domain` to the
  selected production host. Within 5 min the dev plugin's next
  `tenant.snapshot` pull sees the new lock and goes silent
  (DomainLock mismatch), so dev / staging mirrors stop polluting
  production search analytics automatically. Closes the operator
  round-trip the v0.2.x flow required.
* **Added**: `Gateway\Client::promoteCanonicalDomain()` thin HTTP
  helper for the new tool. Same HMAC envelope as every other
  gateway call.
* **Added**: Admin-app mirror — `tenant-settings-form.tsx` now shows
  a "Promote dev to production" hint band + one-click action when
  the tenant is locked to a non-prod host and a prod-looking
  connector has authenticated. Pre-fills the locked-domain field
  with the most-recently-seen prod host; operator clicks Save to
  apply.

= 0.1.4 =

* **Added**: WordPress.org Plugin Directory submission readiness.
  Plugin header `Update URI: https://seekmodo.com/plugins/seekmodo`
  (WP 5.8+) routes update checks to our marketing-site endpoint, so
  the plugin lists at <https://wordpress.org/plugins/seekmodo/> for
  initial install + searchability while all subsequent
  version-checks and zip downloads happen against seekmodo.com.
* **Added**: `Numinix\SeekmodoWordPress\Updates\WporgUpdateClient` —
  hooks `update_plugins_seekmodo.com` (the filter key WP core
  synthesises from our `Update URI` header) and the
  `plugins_api → plugin_information` "View details" modal. Reads
  from `https://seekmodo.com/api/plugins/seekmodo`, cached 12 h on
  success and 5 min on failure. Endpoint is filterable via
  `seekmodo_wporg_update_endpoint` for staging.
* **Changed**: Plugin name shortened from "Seekmodo Search for
  WordPress" to "Seekmodo Search" (w.org plugin-review guideline:
  the "for WordPress" suffix is discouraged when the plugin is
  listed there).
* **Changed**: Plugin description rewritten for w.org's plugin-card
  preview, with explicit SaaS-dependency disclosure and free-trial
  note matching the Mailchimp / Algolia / Akismet pattern.
* **Changed**: `Tested up to: 6.6` added to plugin header + readme so
  the WP-admin compatibility filter and the w.org directory listing
  reflect the latest tested WordPress release.
* **Fixed**: `tools/build_release.py --auto-pr` now writes a manifest
  entry whose schema matches the live `platforms.zen_cart` entry
  exactly (`sha256` / `sig` / `sig_kid` / `signed_at` / `signed_with`
  / `released_at` / `release_notes_url`) instead of the orphan
  `sha256_url` / `signature_url` / `released_at_utc` shape it
  produced before. Also replaces a Unix-only
  `subprocess.check_output(["date", ...])` with portable
  `datetime.now(timezone.utc)`.

= 0.1.3 =

* Add: per-tenant "Powered by Seekmodo" branding toggle. Off by default;
  operators turn it on from the admin.seekmodo.com tenant settings page
  for partner / specific-deal sites. New helpers
  `numinix_seekmodo_show_branding()` and `numinix_seekmodo_brand_url()`
  for themes, with `seekmodo_show_branding` and `seekmodo_brand_url`
  filters.

= 0.1.2 =

* Fix: `AutoPromoter` substate FSM is now actually wired up. Previous
  versions never advanced the `active+shadow → active+enforce`
  substate, and the SearchSwaps consulted the headline mode instead
  of the effective mode, so `auto_promote=true` was decorative.
  `MODE=active` now correctly bakes into enforce on healthy metrics
  and demotes back to shadow on a breach.
* Fix: `MODE=active` + `auto_promote=false` now correctly collapses
  to a hard enforce (operator opt-out of the safety hop).

= 0.1.1 =

* Fix: Content indexer doc shape aligned with the gateway's strict
  Typesense `content` collection schema. Previous releases would
  have 400'd on every `index.upsert`.
* Bumped `Options::SCHEMA_VERSION` to force a full reindex on
  upgrade.

= 0.1.0 =

* Initial release. Sprints 0-5 shipped: content + commerce verticals,
  mode FSM with auto-promoter, shadow-mode diff logger, typeahead,
  click beacon, events queue, health widget, signed-zip release
  pipeline.
