# BLOCK_DATASET_CONFIG — v0.2 (instance-based · DB + API) _Central contract for every dataset block. config = Frontend CMS (admin) · resolver = Backend CMS (API) · single source · no hardcode._ ## Model — INSTANCE-based (v0.2) A block placement on a page is an **instance** (`block_instances`): ```jsonc { "instance_id": "inst-sc-1-home-A8-content-feed", "placement": { "page": "sc-1-home", "slot": "A8-content-feed" }, "source": { "mode": "db | api_a1 | api_a2", "ref": "poi | currency-exchange | weather", "params": {} }, "config": { /* the 8-control pipeline below */ }, "owner": "...", "capability": "v1-deterministic" } ``` - **db** — query the POI master (SQL pipeline). - **api_a1** — pull from a provider adapter (API Providers registry) → normalized list. - **api_a2** — read the latest webhook snapshot (push) → normalized list. All three feed the **same pipeline** (filter → sort≤3 → rank → limit). DB runs it in SQL; API runs it in-memory — **parity-tested** (P1/P2). ## The 8 controls (config) | # | control | field | |---|---|---| | 1 | how many | `present.limit` | | 2 | order by | `sort[0]` | | 3 | filter (e.g. editor_choice=true) | `filter[]` | | 4 | sort ≤3 keys in order | `sort[]` | | 5 | pin top-N · random · sponsored | `rank` (top_rank=v1) · `rank_v2` (sponsored/random=v2) | | 6 | persona filter | `personalize.persona_match` (v2) | | 7 | festival / date window | `filter` active_window (between on a date field) | | 8 | user favorites | `personalize.favorites_first` (v2) | ## Resolver pipeline `filter` (whitelist: indexed + non-PII) → `sort` (≤3, **+ stable id tiebreak** → deterministic) → `rank` (pinned/sponsored top, sequential/random fill) → `limit`. - **default** (no config): `business_status=OPEN · sort rating desc · limit 10`. - **no-fallback**: filters to empty → empty (never a fixture/fake). - **whitelist guard**: non-indexed / PII fields (phone, owner_name) are rejected. ## v1 deterministic (cacheable · certifiable) covers 1,2,3,4,7 + `rank.top_rank`/sequential. Same result every request. ## v2 per-user / sponsored - **persona / favorites** — per-user → no-store · gated on **consent** (`consent_records`) · output through `ScrubConsumerPii` (no raw attrs / SEC-6 markers). - **random fill** — seed `daily+uid` (deterministic per user/day → certifiable). - **sponsored** — pin from `block_sponsored_slots` · label **"ได้รับการสนับสนุน"** · `sponsored_audit` (who-paid · `user_hash`=sha256, never raw) · budget decrement (impressions) · link `campaigns` (Revenue/Commission). ## Admin DB/API tab field-picker = whitelist dropdown (no free text) · filter builder · sort×3 · active_window · pinned · limit/layout · (v2) persona/favorites/sponsored toggles · **source picker** (db / api_a1 provider / api_a2) · **"ลองยิง/Test"** button → live preview JSON (sandbox for API). ## API mode - **A1**: `source.ref` = provider slug (← API Providers) · `params` mapped to the adapter · sandbox samples when no live creds (deterministic for tests). Registered: Currency, Weather. - **A2**: push `POST /api/webhooks/snapshot/{source_ref}` → latest snapshot → resolver reads it. - 8 controls apply in API mode too; favorites/persona = **N/A** for non-poi sources (e.g. currency). ## Test matrices (all PASS) - **v1 T1–T7** + **active_window** + whitelist-guard. - **v2 T8–T12** (persona / favorites / sponsored+audit / seed-stable / PDPA). - **API**: A1-T1..3 (currency pull) · A2-T1..3 (weather snapshot) · **Parity P1** (DB SQL == in-memory) · **P2** (in-memory determinism). ## Certification hook FRONTEND-CMS criterion = **config round-trip**: edit config in admin → API + beta reflect → revert. A8-content-feed is frozen `v1-deterministic` in `BLOCK_LOCK.json`. _v0.2 · 2026-06-05 · implemented in platform (block_instances + BlockDatasetResolver + /api/mobile/block/{page}/{slot}/dataset + /admin/block-config)._