{
  "guide_version": 3,
  "contract": {
    "guide": 3,
    "sdkPath": "v1",
    "collectionSchema": 1,
    "catalogManifest": 1,
    "apiPrefix": "/api/v1"
  },
  "classes": {
    "automatic": "built-in automatic",
    "markup": "semantic markup required",
    "hook": "custom hook required",
    "vendor": "vendor integration required",
    "not_implemented": "not implemented in Kibble"
  },
  "sdk_features_url": "<collector origin>/sdk/v1/features.json",
  "capabilities": [
    {
      "id": "consent",
      "name": "Consent gate",
      "class": [
        "hook"
      ],
      "interface": "kibble('consent','granted'|'denied'); data-consent=\"google-consent-mode\" follows analytics_storage; kibble('forget')",
      "fires": "Nothing is captured or sent until granted. denied stops at once and deletes the browser id. The SDK does not remember the choice.",
      "fields": "granted | denied",
      "privacy": "Analytics purpose only; never map advertising consent. Events before a grant are never replayed.",
      "duplicates": "Re-applying the same state is a no-op.",
      "verify": "No /v1/collect before consent; kibble.debug.consent reads granted after the grant (after load).",
      "seen": "Manage → Health (/manage/health)",
      "section": "consent",
      "sdkFeature": "consent.manual",
      "routes": [
        "/manage/health"
      ]
    },
    {
      "id": "page_view",
      "name": "Page views and SPA navigation",
      "class": [
        "automatic"
      ],
      "interface": "Automatic. Content id/type from <meta name=\"kibble:content\"> and <meta name=\"kibble:type\"> (optional markup).",
      "fires": "On load after consent, and on history navigation (pushState, replaceState with a new path, popstate, back-forward cache restore). Not on hash-route or fragment-only changes.",
      "fields": "host, path without query or fragment, content id/type, sanitized title and the og:image address without query or fragment (page views only; other events carry the page identity without them). The collector keeps the image only when it is https on one of the property's website hosts or listed image hosts; signed or session-bound addresses are never sent",
      "privacy": "Query strings, fragments and identifier-like path segments never leave the browser. Reports name a page by its title and image only when at least three browsers saw the same value on a day; account, checkout, confirmation and search pages, and search-results titles, are always shown by their id.",
      "duplicates": "A custom page_view call next to the automatic tracking double counts. Do not add one.",
      "verify": "One page_view per navigation in Validation.",
      "seen": "Content (/content), Journeys (/journeys), exit findings in Opportunities (/opportunities); MCP content, journeys",
      "retention": "Detail 30 days; daily aggregates 13 months.",
      "section": "automatic",
      "sdkFeature": "auto.page-view.history",
      "routes": [
        "/content",
        "/journeys",
        "/opportunities"
      ],
      "mcp": [
        "journeys",
        "content"
      ]
    },
    {
      "id": "acquisition",
      "name": "Acquisition and campaigns",
      "class": [
        "automatic"
      ],
      "interface": "Automatic on the first page view after consent.",
      "fires": "Once per landing (first page view after activation).",
      "fields": "Referrer host; utm_source, utm_medium, utm_campaign (cleaned tokens); which click id was present (gclid, fbclid, msclkid, ttclid), never its value",
      "privacy": "utm_term, utm_content and every other parameter are never read.",
      "duplicates": "None.",
      "verify": "Land with ?utm_source=test&utm_medium=test; the visit shows that source.",
      "seen": "Acquisition (/acquisition); MCP acquisition",
      "section": "collects",
      "routes": [
        "/acquisition"
      ],
      "mcp": [
        "acquisition"
      ]
    },
    {
      "id": "attention",
      "name": "Attention time",
      "class": [
        "automatic"
      ],
      "interface": "Automatic.",
      "fires": "Active time (visible, focused, recent input) emitted at navigation, hide or every 60 s.",
      "fields": "active_ms only",
      "privacy": "No input content is read.",
      "duplicates": "None.",
      "verify": "attention rows appear in Validation after interacting.",
      "seen": "Content (/content): average active attention per visit for each page; MCP content",
      "section": "automatic",
      "sdkFeature": "auto.attention",
      "routes": [
        "/content"
      ],
      "mcp": [
        "content"
      ]
    },
    {
      "id": "sections",
      "name": "Section visibility",
      "class": [
        "markup"
      ],
      "interface": "data-kibble-section=\"<id>\"",
      "fires": "Once per id per page view when at least half the element is visible for one second. Not scroll depth.",
      "fields": "Section id: lowercase letters, digits, _ . : - /; no spaces",
      "privacy": "Structural id only.",
      "duplicates": "Two elements with the same id count once per page view.",
      "verify": "Scroll the section into view; section_view appears in Validation.",
      "seen": "Validation (/site-check) shows each section_view; kept as 13-month aggregates, not yet shown in a report or MCP tool",
      "section": "markup",
      "sdkFeature": "markup.section",
      "routes": [
        "/site-check"
      ]
    },
    {
      "id": "clicks",
      "name": "Meaningful clicks",
      "class": [
        "markup",
        "automatic"
      ],
      "interface": "data-kibble-click=\"<id>\" with optional data-kibble-type (cta|nav|product|room|link|filter|other). Links to connected hosts are clicked automatically.",
      "fires": "On click of the marked element (or a link to a connected host).",
      "fields": "target id, target type",
      "privacy": "No link URLs or text.",
      "duplicates": "Do not also call track('click') for a marked element.",
      "verify": "Click it; a click row appears in Validation.",
      "seen": "Validation (/site-check) shows each click; kept as 13-month aggregates, not yet shown in a report or MCP tool",
      "section": "markup",
      "sdkFeature": "markup.click",
      "routes": [
        "/site-check"
      ]
    },
    {
      "id": "forms",
      "name": "Form starts, submits and validation errors",
      "class": [
        "markup",
        "automatic"
      ],
      "interface": "form[data-kibble-form=\"<id>\"][data-kibble-form-type=\"contact|booking|checkout|newsletter|other\"]; data-kibble-field; data-kibble-error + data-kibble-error-code (required|format|invalid|server)",
      "fires": "form_start on first focus or input; form_submit on the submit event (an attempt); form_error on native invalid events and on rendered error markup.",
      "fields": "form id, field names, error codes",
      "privacy": "Field values and error message text are never read.",
      "duplicates": "Native validation plus error markup for the same check counts twice. form.submit() fires no submit event (use requestSubmit()).",
      "verify": "Start, fail and submit a form with test data; rows appear in Validation.",
      "seen": "Forms in Opportunities (/opportunities?focus=forms); MCP forms",
      "retention": "Per-form stage counts in 13-month aggregates.",
      "section": "forms",
      "sdkFeature": "markup.forms",
      "routes": [
        "/opportunities"
      ],
      "mcp": [
        "forms"
      ]
    },
    {
      "id": "form_accepted",
      "name": "Confirmed form acceptance",
      "class": [
        "hook",
        "automatic",
        "vendor"
      ],
      "interface": "kibble('formAccepted', '<form id>', { ref? }); Contact Form 7 automatic; WPForms and Gravity Forms through the WordPress plugin",
      "fires": "Only where the server or application accepted the submission. Never in onSubmit.",
      "fields": "form id; optional stable submission reference (hashed in the browser)",
      "privacy": "No form values. A reference is hashed before sending.",
      "duplicates": "Do not add a CF7 hook (automatic). Without a reference, repeats of one form in one tab within 60 s count once.",
      "verify": "Submit a test form the server accepts and one it rejects: only the accepted one shows form_accepted.",
      "seen": "Forms (/opportunities?focus=forms), Outcomes as leads (/outcomes); MCP forms, outcomes",
      "section": "forms",
      "sdkFeature": "auto.cf7-accepted",
      "routes": [
        "/opportunities",
        "/outcomes"
      ],
      "mcp": [
        "forms",
        "outcomes"
      ]
    },
    {
      "id": "site_search",
      "name": "Site search and anonymous phrases",
      "class": [
        "hook"
      ],
      "interface": "kibble('search', { phrase, results, scope: 'site' })",
      "fires": "Once per search the visitor ran, after results render, only on a genuine site search.",
      "fields": "results: the real count when known (Shopify theme: search.results_count; Hydrogen: search_viewed searchResults.total), 0 only when the search returned none, omitted when unknown; scope site|catalog|availability (availability is a booking engine request, not site search); error: true when the search itself failed (own \"Search errors\" signal, send no results with it)",
      "privacy": "The phrase goes only to the separate phrase pipeline (no browser, visit or event ids, delayed 5–10 s), at most once per page view; phrases with emails or long numbers are dropped; shown only at 10 or more searches.",
      "duplicates": "Do not call for autosuggest keystrokes. Never infer zero results from a missing payload.",
      "verify": "Site Check shows results some/none, never words; POST /v1/phrases carries no browser_id or stream_id.",
      "seen": "Big Picture → Search (/search) and Opportunities → Search; Data tracking → Events (visits that reported a result count); MCP search",
      "retention": "Phrase counts by day, 13 months; shown only at 10 or more occurrences by default.",
      "section": "search",
      "sdkFeature": "search.phrases",
      "routes": [
        "/search",
        "/opportunities"
      ],
      "mcp": [
        "search"
      ]
    },
    {
      "id": "booking_criteria",
      "name": "Booking widget criteria at submission",
      "class": [
        "markup"
      ],
      "interface": "form[data-kibble-form][data-kibble-form-type=\"booking\"] with data-kibble-criteria=\"property|destination|start|end|nights|adults|children|guests\" on explicitly marked fields",
      "fires": "Read once at the submit event, attached to form_submit (an attempt, not availability or acceptance).",
      "fields": "subject (public business id), subject_type property|destination, intended_start YYYY-MM-DD, nights 1–60, guests 1–20",
      "privacy": "Promo codes, names, emails and free text are never marked or read; typed names are never subjects.",
      "duplicates": "Markup or the explicit track('form_submit') call, never both. Never send availability from the widget.",
      "verify": "Validation: \"Searched for a stay\" with hotel, start, nights, guests.",
      "seen": "Validation (/site-check)",
      "retention": "30-day detail only; not in summaries or report pages.",
      "section": "booking",
      "sdkFeature": "markup.booking-criteria",
      "routes": [
        "/site-check"
      ]
    },
    {
      "id": "availability",
      "name": "Booking engine availability results",
      "class": [
        "hook",
        "vendor"
      ],
      "interface": "dataLayer availability_search (or kibble_availability) with hotel_id or destination, check_in, nights, guests, available; or kibble('track','availability', …)",
      "fires": "When the booking engine shows results.",
      "fields": "available true only when rooms are offered, false only when the engine says none, omitted when unknown",
      "privacy": "Business ids and stay facts only.",
      "duplicates": "Never also from the website widget submit.",
      "verify": "Validation: \"Availability shown\" with Available or Nothing available when the engine reports it.",
      "seen": "Big Picture demand (/), Opportunities (/opportunities); MCP big_picture, opportunities",
      "section": "booking",
      "sdkFeature": "datalayer",
      "routes": [
        "/",
        "/opportunities"
      ],
      "mcp": [
        "big_picture",
        "opportunities"
      ]
    },
    {
      "id": "tee-times",
      "name": "Golf tee-time searches and selection",
      "class": [
        "hook",
        "vendor"
      ],
      "interface": "dataLayer tee_time_search with course_id, play_date, tee_time, players, holes, available; tee_time_selected; or kibble('track','availability', { subject, subject_type: 'course', intended_start, intended_time, available }, { players, holes }) and kibble('track','checkout_step', { step: 'tee_time' })",
      "fires": "When the booking engine shows tee times for a search, and when the visitor picks a time. Booking details, payment and the confirmed booking reuse booking_step and booking_confirmed.",
      "fields": "subject_type course; play_date YYYY-MM-DD and tee_time HH:MM are the course-local date and wall-clock time, never converted; players 1–8; holes 9 or 18; available as for availability. Out-of-range values are dropped, not clamped.",
      "privacy": "Course ids, dates, a time and counts only; no names, emails or member numbers.",
      "duplicates": "One source per event (GTM dataLayer or code, not both). A click on the website to the engine is a click, never a booking. Never send a hotel availability event for a course.",
      "verify": "Validation: \"Tee-time search\" with course, play date, time, players and holes; the checkout step \"Tee time chosen\"; an authorized test booking shows \"Booking confirmed\".",
      "seen": "Big Picture demand by play date (/), the Tee-time booking funnel in Outcomes (/outcomes), Reporting tee-time demand (/reporting); hour and party-size breakdowns where collected are in the Big Picture response (tee_times) and MCP big_picture; MCP outcomes",
      "retention": "Hour and party-size breakdowns: 13 months of daily summaries, from days processed with definitions 2026.10.8; searches without a time or party size are not in them.",
      "section": "booking",
      "sdkFeature": "datalayer",
      "routes": [
        "/",
        "/outcomes",
        "/reporting"
      ],
      "mcp": [
        "big_picture",
        "outcomes"
      ]
    },
    {
      "id": "commerce",
      "name": "Product views, cart and checkout stages",
      "class": [
        "vendor",
        "hook"
      ],
      "interface": "GA4 dataLayer view_item, add_to_cart, begin_checkout, add_shipping_info, add_payment_info; booking_step; Shopify pixel; BigCommerce BODL; or kibble('track', 'product_view'|'add_to_cart'|'checkout_step', …)",
      "fires": "When the platform reports the step.",
      "fields": "product id, category (collector id rules), price, quantity, value; step cart|details|shipping|payment|review|guest|room|tee_time (golf: a chosen tee time)",
      "privacy": "Identifiers and amounts only; no customer fields.",
      "duplicates": "One source per step: GTM dataLayer or code, not both. A checkout step is never a conversion. With data-datalayer-gtag, a gtag command and a dataLayer object for the same event are counted twice (a purchase reference still counts once).",
      "verify": "Walk a test cart; product views (product, category), cart additions (product, quantity) and steps appear in Validation.",
      "seen": "Journeys (/journeys), demand in Big Picture (/), the connected checkout funnel in Outcomes (/outcomes: a hotel reads availability → step room → guest → payment → booking, a shop add to cart or step cart → details → shipping → payment → purchase; send the steps the site has, in order); MCP journeys, big_picture, outcomes",
      "section": "gtm",
      "sdkFeature": "datalayer",
      "routes": [
        "/journeys",
        "/"
      ],
      "mcp": [
        "journeys",
        "big_picture"
      ]
    },
    {
      "id": "outcomes",
      "name": "Confirmed outcomes (purchase, booking, lead)",
      "class": [
        "hook",
        "vendor",
        "markup"
      ],
      "interface": "kibble('outcome', { type, ref, value?, currency?, items? }); dataLayer purchase (transaction_id), booking_confirmed (booking_ref), kibble_outcome; data-kibble-confirmation markup; Shopify checkout_completed; BigCommerce orderPurchased",
      "fires": "Only from the confirmed step with a stable business reference. Never on submit, click, URL or thank-you heading alone.",
      "fields": "type purchase|booking|lead; ref required for purchase and booking (hashed in the browser); value, currency ISO 4217, items",
      "privacy": "The raw reference never leaves the browser.",
      "duplicates": "Same reference counts once across signals. A GA4 purchase on a hotel engine is a purchase until staff map it; do not reinterpret it in code.",
      "verify": "Test mode or a test property only: exactly one outcome per test confirmation.",
      "seen": "Outcomes (/outcomes); MCP outcomes",
      "retention": "Detail 30 days; outcome aggregates 13 months.",
      "section": "outcomes",
      "sdkFeature": "markup.confirmation",
      "routes": [
        "/outcomes"
      ],
      "mcp": [
        "outcomes"
      ]
    },
    {
      "id": "outcome_candidates",
      "name": "Outcome candidates",
      "class": [
        "automatic"
      ],
      "interface": "Automatic unless data-auto-candidates=\"false\".",
      "fires": "On ambiguous signals: confirmation-looking URL, GA4 generate_lead/sign_up, purchase or booking signals without a reference.",
      "fields": "signal name only",
      "privacy": "The URL is never sent.",
      "duplicates": "A candidate is never a confirmed outcome until staff approve it.",
      "verify": "Report the candidates you triggered; staff review them.",
      "seen": "Data tracking → Events, Needs a decision (/manage/data-tracking/events)",
      "section": "outcomes",
      "sdkFeature": "auto.outcome-candidates",
      "routes": [
        "/manage/data-tracking/events"
      ]
    },
    {
      "id": "page_performance",
      "name": "Page performance (LCP, INP, CLS, TTFB)",
      "class": [
        "automatic"
      ],
      "interface": "Automatic. data-performance=\"off\" disables it; data-performance=\"0.5\" (or kibbleConfig.performance) samples page loads.",
      "fires": "One web_vitals snapshot per page load when the page is hidden (and again only if a value changed), riding the batch already sent then. Measured only when consent was in effect as the SDK started on the page; a back/forward-cache restore is a new page load.",
      "fields": "lcp, inp, cls, ttfb numbers; rev; sample rate; support bits. Page = where the page load started.",
      "privacy": "Timings only: no element, text, URL, stack or interaction target. Nothing from before consent is read or sent.",
      "duplicates": "Do not add web-vitals or another performance script for Kibble. Retries and later revisions never add page loads.",
      "verify": "Validation shows \"Page speed measured\" after hiding the tab; the page view shows perf: measured. One visit proves collection, not speed.",
      "seen": "Big Picture → Performance (/performance); MCP performance; readiness in Health → MCP",
      "retention": "30-day detail; 13-month daily summaries. No history before collection began.",
      "section": "automatic",
      "sdkFeature": "auto.page-performance",
      "routes": [
        "/performance"
      ],
      "mcp": [
        "performance",
        "data_health"
      ]
    },
    {
      "id": "tech_errors",
      "name": "Technical errors",
      "class": [
        "automatic"
      ],
      "interface": "Automatic.",
      "fires": "Uncaught JavaScript errors and failed resource loads, deduplicated per page view, at most 25.",
      "fields": "class js|resource only",
      "privacy": "No message, stack or URL.",
      "duplicates": "None. HTTP/fetch errors and unhandled promise rejections are not captured.",
      "verify": "Not usually needed.",
      "seen": "Health → MCP (/manage/health/mcp); MCP data_health",
      "section": "automatic",
      "sdkFeature": "auto.tech-error.js-resource",
      "routes": [
        "/manage/health/mcp"
      ],
      "mcp": [
        "data_health"
      ]
    },
    {
      "id": "cross_domain",
      "name": "Cross-domain continuity",
      "class": [
        "automatic",
        "markup"
      ],
      "interface": "data-connected-hosts on both sides (`host` or `host/pattern` entries); kibble('decorate', url) for script navigation; optional data-cookie-domain across website hosts only",
      "fires": "Links, GET forms and form actions to a configured destination carry a single-use _kbh token (120 s, one per destination, renewed only after visitor activity) that the destination redeems after its own consent.",
      "fields": "none (token only; the browser id never appears in a URL)",
      "privacy": "Consent is never inferred and is asked on each host. The token is removed from the address bar (query string and fragment untouched) before any event; a provider can see it in the first request URL.",
      "duplicates": "None. Redirect pages, iframes, window.open and server-side redirects are not decorated.",
      "verify": "The destination first page view is a handoff and _kbh is gone from the URL.",
      "seen": "Journeys (/journeys); Validation (/site-check)",
      "section": "gateways",
      "sdkFeature": "cross-domain.handoff",
      "routes": [
        "/journeys",
        "/site-check"
      ]
    },
    {
      "id": "connection_scope",
      "name": "External connection scope (provider hosts)",
      "class": [
        "vendor"
      ],
      "interface": "A connection registered on the property (provider, host, tenant, routes); on its pages data-tenant, data-routes, data-storage=\"property\"; rezStream tag recipe; Shopify custom pixel",
      "fires": "Only on the connection host, inside its routes, with tn equal to the tenant; collection stops (HTTP 403) for a missing or different tenant.",
      "fields": "tn (tenant id). Never guest names, emails, cart ids or customer fields.",
      "privacy": "Property-scoped storage, no cookie. A connection never authorizes manifest fetches, catalog URLs or images on the provider host. Tenant and key are browser-supplied: they stop accidental cross-tenant collection, they do not authenticate the sender.",
      "duplicates": "One installation per connection page; do not install the website snippet on a provider host.",
      "verify": "Network tab on the provider page: one kibble.js, collect 200 with tn; another tenant's page sends nothing. rezStream is a recipe and Shopify an adapter: neither is verified on a live site.",
      "seen": "Data tracking → Setup (/manage/data-tracking/setup); Data tracking → Events (/manage/data-tracking/events)",
      "section": "gateways",
      "sdkFeature": "config.property-scope",
      "routes": [
        "/manage/data-tracking/setup",
        "/manage/data-tracking/events"
      ]
    },
    {
      "id": "settings_by_key",
      "name": "Settings by key (the standard GTM tag)",
      "class": [
        "vendor"
      ],
      "interface": "A tag or snippet with only data-key (and data-endpoint behind a gateway); no data-consent. Settings fetched from <collector origin>/sdk/v1/settings/<key>.json",
      "fires": "Once per page load, before the SDK starts (at most 4 s); cached up to five minutes.",
      "fields": "Consent source and category, dataLayer and gtag switches, learning switch, cookie domain, link entries, connection scopes, event decisions. No secrets, no visitor data.",
      "privacy": "The request carries no cookie and no referrer. A failed fetch collects nothing extra: consent is manual and the dataLayer is not read. An attribute on the page wins over a fetched value.",
      "duplicates": "An installation that sets data-consent never fetches settings; a second SDK copy is ignored.",
      "verify": "Network tab: one settings request, then (after consent) collect 202. Validation shows the events.",
      "seen": "Data tracking → Setup (/manage/data-tracking/setup)",
      "section": "gtm",
      "sdkFeature": "config.settings-by-key",
      "routes": [
        "/manage/data-tracking/setup"
      ]
    },
    {
      "id": "consent_platforms",
      "name": "Consent platforms: TrustArc, OneTrust, CookieYes (key-only installations)",
      "class": [
        "automatic"
      ],
      "interface": "Chosen per property in Kibble; read from the platform's cookie or browser API",
      "fires": "On load, on the platform's own change events, and when the page regains focus or visibility.",
      "fields": "granted or denied only; the analytics category is configurable (TrustArc 2, OneTrust C0002, CookieYes analytics by default)",
      "privacy": "Fails closed: no decision means no collection; malformed or disagreeing values are no decision; a later non-grant withdraws and deletes the browser id. Fixture-tested, not verified on a live site.",
      "duplicates": "Choose Google Consent Mode instead when the platform also publishes it.",
      "verify": "Before a choice no /v1/collect; after a grant collect 202; after withdrawal nothing more is sent.",
      "seen": "Data tracking → Setup (/manage/data-tracking/setup)",
      "section": "consent",
      "sdkFeature": "consent.trustarc",
      "routes": [
        "/manage/data-tracking/setup"
      ]
    },
    {
      "id": "event_names",
      "name": "Event names learned from the dataLayer, and staff decisions",
      "class": [
        "automatic"
      ],
      "interface": "Key-only installations with the dataLayer and learning on; decisions in Data tracking → Events",
      "fires": "After consent, the name of a dataLayer event that neither the allowlist nor a decision maps, once per name per page load (at most 20).",
      "fields": "The name only (≤ 60 characters; no @, no 6-digit runs, no gtm.*). A decision reads only its kind's fields, from an event path or a page-address parameter for stay fields.",
      "privacy": "Never parameters, user_data, emails, names, addresses or search terms; never a browser, visit or stream id. Paths naming personal data are refused. References are hashed in the browser.",
      "duplicates": "A decision for a standard name replaces the standard mapping; consecutive identical mapped signals count once.",
      "verify": "Network tab: POST /v1/event-names with names only; the names appear in Data tracking → Events within a minute.",
      "seen": "Data tracking → Events (/manage/data-tracking/events)",
      "retention": "35 days of daily counts per name and host",
      "section": "gtm",
      "sdkFeature": "datalayer.learn-names",
      "routes": [
        "/manage/data-tracking/events"
      ]
    },
    {
      "id": "gateway",
      "name": "First-party gateway",
      "class": [
        "vendor"
      ],
      "interface": "Serve the SDK and the collection paths through the site; set data-endpoint to the gateway base",
      "fires": "Every SDK request goes through the gateway.",
      "fields": "Only the allowlisted Kibble paths; only Content-Type, Origin and User-Agent forwarded",
      "privacy": "Never forward cookies, authorization or client IP headers.",
      "duplicates": "One SDK installation per page, whatever its source.",
      "verify": "Requests go to the gateway; responses carry X-Kibble-SDK-Version.",
      "seen": "Health (/manage/health)",
      "section": "gateways",
      "routes": [
        "/manage/health"
      ]
    },
    {
      "id": "site_check",
      "name": "Validation (Site Check)",
      "class": [
        "automatic"
      ],
      "interface": "A staff member starts a validation in Manage → Validation; the tester opens the link on a page that runs the SDK and gets a movable K that opens the Validation panel.",
      "fires": "Only in the tester's tab while the validation is active; never grants consent.",
      "fields": "Per-event safe detail and evidence state (stored = the collector confirmed it). The tester's panel also lists dataLayer pushes and what the observation module read, and a Developer view shows each request to Kibble with its status and scrubbed payload.",
      "privacy": "The shared timeline carries no references, values, titles or referrers. The tester's own panel shows the payload their tab sent (values included) with browser and stream ids, the property key, capabilities and references hidden; it stays in that tab. dataLayer values are shown only for fields Kibble reads.",
      "duplicates": "Check traffic is real traffic and counts in reports.",
      "verify": "This is the verification tool.",
      "seen": "Validation (/site-check); MCP site_checks",
      "retention": "Event detail until 24 h after the check ends; summary 90 days.",
      "section": "verify",
      "sdkFeature": "site-check",
      "routes": [
        "/site-check"
      ],
      "mcp": [
        "site_checks"
      ]
    },
    {
      "id": "site_catalog",
      "name": "Site catalog (readable names for business ids)",
      "class": [
        "vendor"
      ],
      "interface": "A site manifest (kibble_catalog 1), uploaded by staff or synchronized from https://<registered host>/kibble/site.json",
      "fires": "Never at collection or report time; applied by an admin, or by an opted-in daily check for additions and name changes.",
      "fields": "namespace, type hotel|destination|room|product, string id, name, short_name, locality, page_url, image_url, provenance",
      "privacy": "Public business entities only: no keys, people, visitors, form values or phrases.",
      "duplicates": "Ids that differ only by letter case collide and are refused.",
      "verify": "Preview in Catalog shows no problems; report subjects show names.",
      "seen": "Catalog (/catalog); names in Big Picture and Opportunities; MCP big_picture, opportunities",
      "section": "site-catalog",
      "routes": [
        "/catalog",
        "/",
        "/opportunities"
      ],
      "mcp": [
        "big_picture",
        "opportunities"
      ]
    },
    {
      "id": "name_suggestions",
      "name": "Learning from pages: names, images, page details, forms (opt-in)",
      "class": [
        "markup",
        "automatic"
      ],
      "interface": "Four independent opt-ins per property, set by an admin in Catalog → Settings (all off by default; one switch turns all four on or off together, and \"Choose what Kibble learns\" shows each). Images: the image a JSON-LD hotel, room, product or destination declares for itself (image, image[0], image.url or image.contentUrl) beside its identifier. Names: JSON-LD, data-kibble-entity / data-kibble-entity-type / data-kibble-name markup, GA4 dataLayer item_id and item_name, and booking-form property and destination select options. Page details: the kibble:content, description, robots/googlebot and canonical metas, the html lang attribute and the first h1 text. Forms without Kibble markup: up to 10 forms lacking data-kibble-form (id or name, action path or other host, method, control kind counts, autocomplete tokens).",
      "fires": "About one to three seconds after load and after in-page navigation, only after consent is granted, at most once per two seconds; skips account, checkout, confirmation, search and sign-in pages.",
      "fields": "Names: type hotel|destination|room|product, id (collector id rules), name (1–120 characters); with Images on, the item's image address (no query or fragment, at most 500 characters). Page: description (300), h1 (160), canonical path or other-host, noindex, lang. Forms: id, action, method, field counts by kind, autocomplete tokens.",
      "privacy": "Never other page text, labels, placeholders, form values, option or button text, other page images, dataLayer objects, visitor ids or query strings; no browser, visit or stream id is sent. Names, page details and form structures are shown to staff only (page details and form structures after two days of evidence), never in reports. A discovered image is shown beside a named item in reports only when it is that item's single image seen on two days, or staff chose it; a manifest or staff image always wins. Not available in the Shopify sandboxed pixel.",
      "duplicates": "Unchanged content on a page is not sent again in a session.",
      "verify": "Findings appear in Catalog for staff; nothing appears in reports.",
      "seen": "Catalog (/catalog), staff only; opt-ins under Settings → Learning (/catalog/settings)",
      "section": "site-catalog",
      "sdkFeature": "observe.name-suggestions",
      "routes": [
        "/catalog",
        "/catalog/settings"
      ]
    },
    {
      "id": "server_outcomes",
      "name": "Server-side outcome confirmation",
      "class": [
        "not_implemented"
      ],
      "interface": "None. Outcomes come only from the browser signals above.",
      "fires": "n/a",
      "fields": "n/a",
      "privacy": "n/a",
      "duplicates": "n/a",
      "verify": "Do not build a server-to-server call to /v1/collect; it is a browser endpoint.",
      "seen": "n/a",
      "section": "outcomes"
    },
    {
      "id": "not_implemented_other",
      "name": "Other things Kibble does not do",
      "class": [
        "not_implemented"
      ],
      "interface": "Scroll-depth percentages; hash-router page views; HTTP/fetch error capture; automatic site-search detection; form tracking without data-kibble-form; a published npm SDK; catalog access through MCP; runtime AI enrichment or crawling.",
      "fires": "n/a",
      "fields": "n/a",
      "privacy": "n/a",
      "duplicates": "n/a",
      "verify": "List any the site owner asked for as \"not implemented in Kibble\".",
      "seen": "n/a",
      "section": "limits"
    }
  ]
}
