[\\d]+)/font-faces"
+ },
+ {
+ "key": "B",
+ "text": "font-faces"
+ },
+ {
+ "key": "C",
+ "text": "fonts/faces"
+ },
+ {
+ "key": "D",
+ "text": "typography/font-faces"
+ }
+ ],
+ "correct_answer": "A",
+ "explanation": "Font face routes are nested under their parent font family: font-families/{font_family_id}/font-faces. This is one of the few REST API endpoints in WordPress that uses a nested parent-child route structure.",
+ "references": [
+ "https://developer.wordpress.org/rest-api/reference/font-faces/"
+ ]
+ },
+ {
+ "id": "k-font-006",
+ "category": "gb-font-library",
+ "subcategory": "collections",
+ "difficulty": "hard",
+ "prompt": "When a WP_Font_Collection loads font data from a remote URL, how is the fetched JSON cached?",
+ "type": "multiple_choice",
+ "choices": [
+ {
+ "key": "A",
+ "text": "Site transient with DAY_IN_SECONDS TTL"
+ },
+ {
+ "key": "B",
+ "text": "Object cache with no expiration"
+ },
+ {
+ "key": "C",
+ "text": "File-based cache in wp-content/cache/"
+ },
+ {
+ "key": "D",
+ "text": "Browser localStorage via REST response headers"
+ }
+ ],
+ "correct_answer": "A",
+ "explanation": "WP_Font_Collection uses set_site_transient() with DAY_IN_SECONDS TTL to cache remote JSON data. The transient key is limited to 167 characters to fit the database column constraint.",
+ "references": [
+ "https://developer.wordpress.org/reference/classes/wp_font_collection/"
+ ]
+ },
+ {
+ "id": "k-font-007",
+ "category": "gb-font-library",
+ "subcategory": "css-generation",
+ "difficulty": "hard",
+ "prompt": "Which of these CSS properties is accepted by WP_Font_Face for @font-face declarations?",
+ "type": "multiple_choice",
+ "choices": [
+ {
+ "key": "A",
+ "text": "size-adjust"
+ },
+ {
+ "key": "B",
+ "text": "font-kerning"
+ },
+ {
+ "key": "C",
+ "text": "letter-spacing"
+ },
+ {
+ "key": "D",
+ "text": "text-rendering"
+ }
+ ],
+ "correct_answer": "A",
+ "explanation": "WP_Font_Face validates a specific set of @font-face CSS properties including size-adjust, ascent-override, descent-override, font-display, font-family, font-stretch, font-style, font-weight, font-variant, font-feature-settings, font-variation-settings, line-gap-override, src, and unicode-range. Properties like font-kerning, letter-spacing, and text-rendering are not valid @font-face properties.",
+ "references": [
+ "https://developer.wordpress.org/reference/classes/wp_font_face/"
+ ]
+ },
+ {
+ "id": "k-font-008",
+ "category": "gb-font-library",
+ "subcategory": "ui",
+ "difficulty": "hard",
+ "prompt": "What is the supported way to hide the Font Library UI across editors in WordPress 6.9?",
+ "type": "multiple_choice",
+ "choices": [
+ {
+ "key": "A",
+ "text": "Remove the Fonts menu via remove_menu_page()"
+ },
+ {
+ "key": "B",
+ "text": "Set fontLibraryEnabled=false in the block_editor_settings_all filter"
+ },
+ {
+ "key": "C",
+ "text": "Define DISABLE_FONT_LIBRARY in wp-config.php"
+ },
+ {
+ "key": "D",
+ "text": "Unregister wp_font_family post type"
+ }
+ ],
+ "correct_answer": "B",
+ "explanation": "Filtering block_editor_settings_all and setting fontLibraryEnabled to false disables the Font Library UI globally without unregistering post types.",
+ "references": [
+ "https://developer.wordpress.org/news/snippets/how-to-disable-the-font-library/"
+ ]
+ }
+ ]
+}
diff --git a/datasets/suites/wp-core-v1/knowledge/gb-interactivity-api.json b/datasets/suites/wp-core-v1/knowledge/gb-interactivity-api.json
new file mode 100644
index 0000000..e5c677e
--- /dev/null
+++ b/datasets/suites/wp-core-v1/knowledge/gb-interactivity-api.json
@@ -0,0 +1,392 @@
+{
+ "id": "wp-core-knowledge-v1-interactivity_api",
+ "version": "1.3.0",
+ "metadata": {
+ "name": "WordPress Core Knowledge - Interactivity API",
+ "description": "Tests knowledge of WordPress Interactivity API",
+ "wp_version": "6.9",
+ "created_at": "2025-12-16"
+ },
+ "tests": [
+ {
+ "id": "k-interactivity-001",
+ "category": "gb-interactivity-api",
+ "subcategory": "directives",
+ "difficulty": "basic",
+ "prompt": "What HTML attribute marks an element as part of an interactive region in the WordPress Interactivity API?",
+ "type": "short_answer",
+ "correct_answer": "data-wp-interactive",
+ "answer_type": "contains",
+ "explanation": "The data-wp-interactive attribute is the entry point for the Interactivity API. It specifies the namespace for the interactive region (e.g., data-wp-interactive=\"core/image\").",
+ "references": [
+ "https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/"
+ ]
+ },
+ {
+ "id": "k-interactivity-002",
+ "category": "gb-interactivity-api",
+ "subcategory": "runtime",
+ "difficulty": "basic",
+ "prompt": "What is the script module ID for the WordPress Interactivity API runtime?",
+ "type": "short_answer",
+ "correct_answer": "@wordpress/interactivity",
+ "answer_type": "contains",
+ "explanation": "The Interactivity API runtime is registered as the @wordpress/interactivity script module. It is loaded with fetchpriority 'low' and in_footer true. A companion module @wordpress/interactivity-router provides client-side navigation.",
+ "references": [
+ "https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/"
+ ]
+ },
+ {
+ "id": "k-interactivity-003",
+ "category": "gb-interactivity-api",
+ "subcategory": "php-api",
+ "difficulty": "basic",
+ "prompt": "What PHP function returns the WP_Interactivity_API singleton instance?",
+ "type": "multiple_choice",
+ "choices": [
+ {
+ "key": "A",
+ "text": "wp_interactivity()"
+ },
+ {
+ "key": "B",
+ "text": "WP_Interactivity_API::get_instance()"
+ },
+ {
+ "key": "C",
+ "text": "get_interactivity_api()"
+ },
+ {
+ "key": "D",
+ "text": "wp_get_interactivity()"
+ }
+ ],
+ "correct_answer": "A",
+ "explanation": "wp_interactivity() returns the WP_Interactivity_API singleton. Unlike most WordPress registries, it uses a global variable rather than a traditional get_instance() method.",
+ "references": [
+ "https://developer.wordpress.org/reference/functions/wp_interactivity/"
+ ]
+ },
+ {
+ "id": "k-interactivity-004",
+ "category": "gb-interactivity-api",
+ "subcategory": "directives",
+ "difficulty": "intermediate",
+ "prompt": "What is the purpose of processing Interactivity API directives on the server side in PHP?",
+ "type": "multiple_choice",
+ "choices": [
+ {
+ "key": "A",
+ "text": "To provide server-rendered initial HTML so interactive regions display correct content before JavaScript loads"
+ },
+ {
+ "key": "B",
+ "text": "To validate directive syntax and catch errors before sending HTML to the client"
+ },
+ {
+ "key": "C",
+ "text": "To compile directives into optimized JavaScript bundles"
+ },
+ {
+ "key": "D",
+ "text": "To generate CSS stylesheets from directive values"
+ }
+ ],
+ "correct_answer": "A",
+ "explanation": "Server-side directive processing (for directives like data-wp-bind, data-wp-text, data-wp-class, data-wp-style, data-wp-context, and data-wp-each) ensures the initial HTML sent to the browser already reflects the correct state. This avoids a flash of incorrect content while waiting for JavaScript to hydrate the page.",
+ "references": [
+ "https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/"
+ ]
+ },
+ {
+ "id": "k-interactivity-005",
+ "category": "gb-interactivity-api",
+ "subcategory": "directives",
+ "difficulty": "intermediate",
+ "prompt": "Which of these Interactivity API directives is processed server-side in PHP?",
+ "type": "multiple_choice",
+ "choices": [
+ {
+ "key": "A",
+ "text": "data-wp-on"
+ },
+ {
+ "key": "B",
+ "text": "data-wp-init"
+ },
+ {
+ "key": "C",
+ "text": "data-wp-text"
+ },
+ {
+ "key": "D",
+ "text": "data-wp-watch"
+ }
+ ],
+ "correct_answer": "C",
+ "explanation": "data-wp-text is one of the 8 server-side directives. data-wp-on, data-wp-init, data-wp-watch, data-wp-run, and data-wp-key are client-side only directives that have no server-side processing.",
+ "references": [
+ "https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/"
+ ]
+ },
+ {
+ "id": "k-interactivity-006",
+ "category": "gb-interactivity-api",
+ "subcategory": "namespaces",
+ "difficulty": "intermediate",
+ "prompt": "What separator character(s) are used in Interactivity API directive values to specify a namespace?",
+ "type": "short_answer",
+ "correct_answer": "::",
+ "answer_type": "exact",
+ "explanation": "The double colon '::' separates the namespace from the reference in directive values. For example: data-wp-text=\"myPlugin::state.title\" references the 'title' state in the 'myPlugin' namespace.",
+ "references": [
+ "https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/"
+ ]
+ },
+ {
+ "id": "k-interactivity-007",
+ "category": "gb-interactivity-api",
+ "subcategory": "directives",
+ "difficulty": "intermediate",
+ "prompt": "In the data-wp-interactive attribute, a namespace can be specified in two formats. What are they?",
+ "type": "multiple_choice",
+ "choices": [
+ {
+ "key": "A",
+ "text": "A plain string or a JSON object with a namespace key"
+ },
+ {
+ "key": "B",
+ "text": "A data attribute or a CSS class"
+ },
+ {
+ "key": "C",
+ "text": "A URL hash or query parameter"
+ },
+ {
+ "key": "D",
+ "text": "An HTML comment or meta tag"
+ }
+ ],
+ "correct_answer": "A",
+ "explanation": "data-wp-interactive accepts either a plain string namespace (e.g., data-wp-interactive=\"myPlugin\") or a JSON object with a namespace key (e.g., data-wp-interactive='{\"namespace\":\"myPlugin\"}'). Core blocks use the simple string form.",
+ "references": [
+ "https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/"
+ ]
+ },
+ {
+ "id": "k-interactivity-008",
+ "category": "gb-interactivity-api",
+ "subcategory": "rendering",
+ "difficulty": "hard",
+ "prompt": "Which HTML elements are completely skipped during server-side Interactivity API directive processing?",
+ "type": "multiple_choice",
+ "choices": [
+ {
+ "key": "A",
+ "text": "SVG and MATH elements"
+ },
+ {
+ "key": "B",
+ "text": "Only IFRAME elements"
+ },
+ {
+ "key": "C",
+ "text": "TABLE and FORM elements"
+ },
+ {
+ "key": "D",
+ "text": "No elements are skipped"
+ }
+ ],
+ "correct_answer": "A",
+ "explanation": "SVG and MATH elements and their children are completely skipped during server-side directive processing. Additionally, SCRIPT, IFRAME, NOEMBED, NOFRAMES, STYLE, TEXTAREA, TITLE, and XMP tags don't visit their closers.",
+ "references": [
+ "https://developer.wordpress.org/reference/classes/wp_interactivity_api/"
+ ]
+ },
+ {
+ "id": "k-interactivity-009",
+ "category": "gb-interactivity-api",
+ "subcategory": "state",
+ "difficulty": "hard",
+ "prompt": "State values in the Interactivity API can be PHP Closures. What are these called and how are they handled?",
+ "type": "multiple_choice",
+ "choices": [
+ {
+ "key": "A",
+ "text": "Derived state -- closures are called during server evaluation and tracked for client serialization"
+ },
+ {
+ "key": "B",
+ "text": "Lazy state -- closures are serialized and executed on the client"
+ },
+ {
+ "key": "C",
+ "text": "Computed state -- closures are memoized and cached in the object cache"
+ },
+ {
+ "key": "D",
+ "text": "Reactive state -- closures trigger re-renders when dependencies change"
+ }
+ ],
+ "correct_answer": "A",
+ "explanation": "PHP Closures in Interactivity API state are 'derived state'. When encountered during evaluate(), they are called and their result is used. Since 6.9, accessed derived state closures are tracked in $derived_state_closures and serialized to the client as derivedStateClosures.",
+ "references": [
+ "https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/"
+ ]
+ },
+ {
+ "id": "k-interactivity-010",
+ "category": "gb-interactivity-api",
+ "subcategory": "block-support",
+ "difficulty": "hard",
+ "prompt": "In block.json, the interactivity support can be set to true or to an object. What are the two properties in the object form?",
+ "type": "multiple_choice",
+ "choices": [
+ {
+ "key": "A",
+ "text": "interactive and clientNavigation"
+ },
+ {
+ "key": "B",
+ "text": "enabled and router"
+ },
+ {
+ "key": "C",
+ "text": "serverRendered and clientHydrated"
+ },
+ {
+ "key": "D",
+ "text": "namespace and directives"
+ }
+ ],
+ "correct_answer": "A",
+ "explanation": "The object form is { \"interactive\": true, \"clientNavigation\": true }. Setting interactivity to boolean true is shorthand for both properties being true. The interactive property marks the block as an interactive root; clientNavigation marks it as compatible with SPA navigation.",
+ "references": [
+ "https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/"
+ ]
+ },
+ {
+ "id": "k-interactivity-attach-001",
+ "category": "gb-interactivity-api",
+ "subcategory": "router",
+ "difficulty": "hard",
+ "prompt": "In WordPress 6.9, what does the Interactivity API router option attachTo solve?",
+ "type": "multiple_choice",
+ "choices": [
+ {
+ "key": "A",
+ "text": "Persisting server state across page loads"
+ },
+ {
+ "key": "B",
+ "text": "Choosing the DOM parent where a router region renders to avoid collisions"
+ },
+ {
+ "key": "C",
+ "text": "Deferring hydration until window.onload"
+ },
+ {
+ "key": "D",
+ "text": "Forcing interactivity scripts into the footer"
+ }
+ ],
+ "correct_answer": "B",
+ "explanation": "attachTo is a selector telling the router which parent node to render into, preventing conflicting renders when multiple router regions share a page.",
+ "references": [
+ "https://make.wordpress.org/core/2025/11/25/wordpress-6-9-field-guide/"
+ ]
+ },
+ {
+ "id": "k-interactivity-module-001",
+ "category": "gb-interactivity-api",
+ "subcategory": "client-navigation",
+ "difficulty": "hard",
+ "prompt": "When manually registering a script module for the Interactivity API in WordPress 6.9, how do you mark it as compatible with client navigation so it loads on SPA transitions?",
+ "type": "multiple_choice",
+ "choices": [
+ {
+ "key": "A",
+ "text": "Set supports.interactivity=true in block.json"
+ },
+ {
+ "key": "B",
+ "text": "Pass 'client_navigation' => true to wp_register_script_module()"
+ },
+ {
+ "key": "C",
+ "text": "Call wp_interactivity()->add_client_navigation_support_to_script_module( 'module-id' )"
+ },
+ {
+ "key": "D",
+ "text": "Prefix the handle with 'interactivity-'"
+ }
+ ],
+ "correct_answer": "C",
+ "explanation": "Manually enqueued script modules must be flagged with add_client_navigation_support_to_script_module so router navigation imports them during SPA transitions.",
+ "references": [
+ "https://make.wordpress.org/core/2025/11/12/interactivity-apis-client-navigation-improvements-in-wordpress-6-9/"
+ ]
+ },
+ {
+ "id": "k-interactivity-011",
+ "category": "gb-interactivity-api",
+ "subcategory": "client-api",
+ "difficulty": "basic",
+ "prompt": "What are the three main functions exported by the @wordpress/interactivity package for use in client-side store definitions?",
+ "type": "multiple_choice",
+ "choices": [
+ {
+ "key": "A",
+ "text": "store, getContext, getElement"
+ },
+ {
+ "key": "B",
+ "text": "init, watch, render"
+ },
+ {
+ "key": "C",
+ "text": "createStore, useContext, useRef"
+ },
+ {
+ "key": "D",
+ "text": "reactive, computed, effect"
+ }
+ ],
+ "correct_answer": "A",
+ "explanation": "The @wordpress/interactivity module exports store() for creating/managing reactive stores with state and actions, getContext() for retrieving an element's inherited context from data-wp-context, and getElement() for accessing a read-only representation of the current DOM element including its ref and attributes. Additional exports include getServerState() and getConfig().",
+ "references": [
+ "https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/"
+ ]
+ },
+ {
+ "id": "k-interactivity-012",
+ "category": "gb-interactivity-api",
+ "subcategory": "directives",
+ "difficulty": "intermediate",
+ "prompt": "What directive prevents the Interactivity API from processing directives on an element's children?",
+ "type": "short_answer",
+ "correct_answer": "data-wp-ignore",
+ "answer_type": "contains",
+ "explanation": "The data-wp-ignore directive tells the Interactivity API runtime to skip directive processing on the element's subtree. This is useful for portions of the DOM that should remain static or are managed by other libraries.",
+ "references": [
+ "https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/"
+ ]
+ },
+ {
+ "id": "k-interactivity-013",
+ "category": "gb-interactivity-api",
+ "subcategory": "router",
+ "difficulty": "hard",
+ "prompt": "What is the default timeout in milliseconds for the Interactivity API router's navigate() function?",
+ "type": "short_answer",
+ "correct_answer": "10000",
+ "answer_type": "exact",
+ "explanation": "The @wordpress/interactivity-router's navigate() function defaults to a 10000ms (10 second) timeout. It also accepts options for force (re-fetch), replace (history entry), html (use provided HTML), loadingAnimation, and screenReaderAnnouncement.",
+ "references": [
+ "https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/"
+ ]
+ }
+ ]
+}
diff --git a/datasets/suites/wp-core-v1/knowledge/gotchas.json b/datasets/suites/wp-core-v1/knowledge/gotchas.json
index 71062ff..2d0f163 100644
--- a/datasets/suites/wp-core-v1/knowledge/gotchas.json
+++ b/datasets/suites/wp-core-v1/knowledge/gotchas.json
@@ -1,6 +1,6 @@
{
"id": "wp-core-knowledge-v1-gotchas",
- "version": "1.1.0",
+ "version": "1.2.0",
"metadata": {
"name": "WordPress Core Knowledge - Gotchas",
"description": "Tests knowledge of common WordPress pitfalls and edge cases",
@@ -9,8 +9,9 @@
},
"tests": [
{
- "id": "k-meta-gotcha-001",
+ "id": "k-gotchas-001",
"category": "gotchas",
+ "subcategory": "meta",
"difficulty": "hard",
"prompt": "What does get_post_meta($post_id, 'missing_key', true) return when the meta key does not exist?",
"type": "multiple_choice",
@@ -21,8 +22,185 @@
{ "key": "D", "text": "WP_Error" }
],
"correct_answer": "C",
- "explanation": "With $single=true and no row present, get_post_meta returns an empty string—easy to misread as a real value with loose checks.",
+ "explanation": "With $single=true and no row present, get_post_meta returns an empty string — easy to misread as a real value with loose checks. With $single=false (the default), it returns an empty array instead. This inconsistency between the two modes is a common source of bugs.",
"references": ["https://developer.wordpress.org/reference/functions/get_post_meta/"]
+ },
+ {
+ "id": "k-gotchas-002",
+ "category": "gotchas",
+ "subcategory": "queries",
+ "difficulty": "intermediate",
+ "prompt": "After running a secondary WP_Query loop, what function must you call to restore the global $post to the main query's current post?",
+ "type": "multiple_choice",
+ "choices": [
+ { "key": "A", "text": "wp_reset_query()" },
+ { "key": "B", "text": "wp_reset_postdata()" },
+ { "key": "C", "text": "rewind_posts()" },
+ { "key": "D", "text": "wp_reset_post()" }
+ ],
+ "correct_answer": "B",
+ "explanation": "wp_reset_postdata() restores the global $post to the main query's current post. wp_reset_query() is for undoing query_posts() (which should be avoided). Calling wp_reset_query() after a WP_Query loop destroys the main query's state. This is one of the most common WordPress loop mistakes.",
+ "references": ["https://developer.wordpress.org/reference/functions/wp_reset_postdata/"]
+ },
+ {
+ "id": "k-gotchas-003",
+ "category": "gotchas",
+ "subcategory": "meta",
+ "difficulty": "intermediate",
+ "prompt": "update_post_meta() returns false in two different situations. Besides database failure, when else does it return false?",
+ "type": "multiple_choice",
+ "choices": [
+ { "key": "A", "text": "When the meta key doesn't exist yet" },
+ { "key": "B", "text": "When the new value is identical to the existing value" },
+ { "key": "C", "text": "When the post doesn't exist" },
+ { "key": "D", "text": "When the meta value exceeds the column size" }
+ ],
+ "correct_answer": "B",
+ "explanation": "update_post_meta() (via update_metadata) returns false when the new value is identical to the existing value, since no update is needed. This makes it impossible to distinguish 'no change needed' from 'update failed' using the return value alone.",
+ "references": ["https://developer.wordpress.org/reference/functions/update_post_meta/"]
+ },
+ {
+ "id": "k-gotchas-004",
+ "category": "gotchas",
+ "subcategory": "functions",
+ "difficulty": "basic",
+ "prompt": "What does is_admin() actually check in WordPress?",
+ "type": "multiple_choice",
+ "choices": [
+ { "key": "A", "text": "Whether the current user has the Administrator role" },
+ { "key": "B", "text": "Whether the current request is for an administrative interface page (URL path)" },
+ { "key": "C", "text": "Whether the current user has the manage_options capability" },
+ { "key": "D", "text": "Whether the current user is a super admin on multisite" }
+ ],
+ "correct_answer": "B",
+ "explanation": "is_admin() checks if the current request is for an admin page (WP_ADMIN constant), NOT whether the user is an administrator. It returns true on admin-ajax.php too. Use current_user_can() to check user roles.",
+ "references": ["https://developer.wordpress.org/reference/functions/is_admin/"]
+ },
+ {
+ "id": "k-gotchas-005",
+ "category": "gotchas",
+ "subcategory": "redirects",
+ "difficulty": "basic",
+ "prompt": "What must you call immediately after wp_redirect() or wp_safe_redirect()?",
+ "type": "multiple_choice",
+ "choices": [
+ { "key": "A", "text": "return" },
+ { "key": "B", "text": "wp_die()" },
+ { "key": "C", "text": "exit" },
+ { "key": "D", "text": "Nothing — they terminate execution automatically" }
+ ],
+ "correct_answer": "C",
+ "explanation": "Neither wp_redirect() nor wp_safe_redirect() call exit or die. They only set the Location header and return. If you don't call exit afterward, PHP continues executing the rest of the script.",
+ "references": ["https://developer.wordpress.org/reference/functions/wp_redirect/"]
+ },
+ {
+ "id": "k-gotchas-006",
+ "category": "gotchas",
+ "subcategory": "options",
+ "difficulty": "intermediate",
+ "prompt": "What is the key behavioral difference between add_option() and update_option() when the option already exists?",
+ "type": "multiple_choice",
+ "choices": [
+ { "key": "A", "text": "add_option() silently does nothing and returns false; update_option() overwrites the value" },
+ { "key": "B", "text": "add_option() throws an error; update_option() succeeds" },
+ { "key": "C", "text": "Both overwrite the existing value" },
+ { "key": "D", "text": "add_option() appends to the value; update_option() replaces it" }
+ ],
+ "correct_answer": "A",
+ "explanation": "add_option() returns false without modifying existing options. update_option() creates the option if it doesn't exist and updates it if it does. This makes update_option() the more common choice for most use cases.",
+ "references": ["https://developer.wordpress.org/reference/functions/add_option/"]
+ },
+ {
+ "id": "k-gotchas-007",
+ "category": "gotchas",
+ "subcategory": "database",
+ "difficulty": "intermediate",
+ "prompt": "$wpdb->insert() does NOT return the new row's auto-increment ID. Where is the insert ID stored?",
+ "type": "short_answer",
+ "correct_answer": "insert_id",
+ "answer_type": "contains",
+ "explanation": "$wpdb->insert() returns int|false (rows affected count, typically 1). The auto-increment ID is stored in $wpdb->insert_id, which is set after any successful query via mysqli_insert_id().",
+ "references": ["https://developer.wordpress.org/reference/classes/wpdb/insert/"]
+ },
+ {
+ "id": "k-gotchas-008",
+ "category": "gotchas",
+ "subcategory": "urls",
+ "difficulty": "intermediate",
+ "prompt": "What is the difference between home_url() and site_url() in WordPress?",
+ "type": "multiple_choice",
+ "choices": [
+ { "key": "A", "text": "home_url() returns where visitors access the site; site_url() returns where WordPress files are installed" },
+ { "key": "B", "text": "home_url() includes the port; site_url() does not" },
+ { "key": "C", "text": "home_url() is for the front-end only; site_url() works everywhere" },
+ { "key": "D", "text": "They always return the same value" }
+ ],
+ "correct_answer": "A",
+ "explanation": "home_url() returns the public-facing URL (option 'home'). site_url() returns where WordPress application files live (option 'siteurl'). They differ when WordPress is installed in a subdirectory — e.g., home_url() = example.com, site_url() = example.com/wp.",
+ "references": ["https://developer.wordpress.org/reference/functions/home_url/"]
+ },
+ {
+ "id": "k-gotchas-009",
+ "category": "gotchas",
+ "subcategory": "filters",
+ "difficulty": "intermediate",
+ "prompt": "In the the_content filter chain, does wpautop run before or after do_shortcode?",
+ "type": "multiple_choice",
+ "choices": [
+ { "key": "A", "text": "wpautop runs BEFORE do_shortcode (priority 10 vs 11)" },
+ { "key": "B", "text": "do_shortcode runs BEFORE wpautop" },
+ { "key": "C", "text": "They run at the same priority" },
+ { "key": "D", "text": "do_shortcode is not hooked to the_content" }
+ ],
+ "correct_answer": "A",
+ "explanation": "wpautop runs at default priority 10, do_shortcode at priority 11. This means paragraph tags are inserted BEFORE shortcodes are processed, which can cause unwanted and
tags around shortcode output.",
+ "references": ["https://developer.wordpress.org/reference/hooks/the_content/"]
+ },
+ {
+ "id": "k-gotchas-010",
+ "category": "gotchas",
+ "subcategory": "http",
+ "difficulty": "basic",
+ "prompt": "What does wp_remote_get() return on failure?",
+ "type": "multiple_choice",
+ "choices": [
+ { "key": "A", "text": "false" },
+ { "key": "B", "text": "null" },
+ { "key": "C", "text": "A WP_Error object" },
+ { "key": "D", "text": "An empty array" }
+ ],
+ "correct_answer": "C",
+ "explanation": "wp_remote_get() returns array|WP_Error. On failure (connection timeout, DNS failure, etc.), it returns a WP_Error object, NOT false. Always check with is_wp_error() before accessing the response.",
+ "references": ["https://developer.wordpress.org/reference/functions/wp_remote_get/"]
+ },
+ {
+ "id": "k-gotchas-011",
+ "category": "gotchas",
+ "subcategory": "functions",
+ "difficulty": "intermediate",
+ "prompt": "wp_update_post() is implemented by calling which function internally?",
+ "type": "short_answer",
+ "correct_answer": "wp_insert_post",
+ "answer_type": "contains",
+ "explanation": "wp_update_post() merges the provided data with the existing post and then calls wp_insert_post() to save. This means all the same hooks (save_post, wp_insert_post, etc.) fire on both create and update.",
+ "references": ["https://developer.wordpress.org/reference/functions/wp_update_post/"]
+ },
+ {
+ "id": "k-gotchas-012",
+ "category": "gotchas",
+ "subcategory": "functions",
+ "difficulty": "hard",
+ "prompt": "The __return_* utility functions were not all introduced in the same version. Which one was added later than the others?",
+ "type": "multiple_choice",
+ "choices": [
+ { "key": "A", "text": "__return_false (3.0.0)" },
+ { "key": "B", "text": "__return_zero (3.0.0)" },
+ { "key": "C", "text": "__return_empty_string (3.7.0)" },
+ { "key": "D", "text": "__return_empty_array (3.0.0)" }
+ ],
+ "correct_answer": "C",
+ "explanation": "__return_true, __return_false, __return_zero, and __return_empty_array were all introduced in 3.0.0. __return_null was added in 3.4.0 and __return_empty_string in 3.7.0.",
+ "references": ["https://developer.wordpress.org/reference/functions/__return_empty_string/"]
}
]
}
diff --git a/datasets/suites/wp-core-v1/knowledge/hooks.json b/datasets/suites/wp-core-v1/knowledge/hooks.json
index 3fad692..3ce49a4 100644
--- a/datasets/suites/wp-core-v1/knowledge/hooks.json
+++ b/datasets/suites/wp-core-v1/knowledge/hooks.json
@@ -1,6 +1,6 @@
{
"id": "wp-core-knowledge-v1-hooks",
- "version": "1.1.0",
+ "version": "1.2.0",
"metadata": {
"name": "WordPress Core Knowledge - Hooks",
"description": "Tests knowledge of WordPress hooks (actions and filters)",
@@ -39,7 +39,7 @@
{ "key": "D", "text": "query_vars" }
],
"correct_answer": "B",
- "explanation": "pre_get_posts fires before the query is executed and allows modification of query variables.",
+ "explanation": "pre_get_posts fires after query vars are parsed but before the SQL query is built, allowing modification of query variables.",
"references": ["https://developer.wordpress.org/reference/hooks/pre_get_posts/"]
},
{
@@ -58,6 +58,265 @@
"correct_answer": "C",
"explanation": "wp_enqueue_scripts is the proper hook for enqueueing scripts and styles on the front-end.",
"references": ["https://developer.wordpress.org/reference/hooks/wp_enqueue_scripts/"]
+ },
+ {
+ "id": "k-hooks-004",
+ "category": "hooks",
+ "subcategory": "internals",
+ "difficulty": "basic",
+ "prompt": "In WordPress core, add_action() is implemented as a wrapper around which function?",
+ "type": "short_answer",
+ "correct_answer": "add_filter",
+ "answer_type": "contains",
+ "explanation": "add_action() literally calls add_filter() with the same arguments. Internally, actions and filters use the same registration mechanism in the $wp_filter global.",
+ "references": ["https://developer.wordpress.org/reference/functions/add_action/"]
+ },
+ {
+ "id": "k-hooks-005",
+ "category": "hooks",
+ "subcategory": "internals",
+ "difficulty": "basic",
+ "prompt": "What is the default priority when registering a hook callback with add_action() or add_filter()?",
+ "type": "short_answer",
+ "correct_answer": "10",
+ "answer_type": "exact",
+ "explanation": "Both add_action() and add_filter() default to priority 10. Lower numbers execute earlier.",
+ "references": ["https://developer.wordpress.org/reference/functions/add_filter/"]
+ },
+ {
+ "id": "k-hooks-006",
+ "category": "hooks",
+ "subcategory": "internals",
+ "difficulty": "basic",
+ "prompt": "How many arguments does a hook callback receive by default if $accepted_args is not specified?",
+ "type": "short_answer",
+ "correct_answer": "1",
+ "answer_type": "exact",
+ "explanation": "The default $accepted_args parameter in add_filter() and add_action() is 1. Callbacks receive only the first argument unless a higher value is specified.",
+ "references": ["https://developer.wordpress.org/reference/functions/add_filter/"]
+ },
+ {
+ "id": "k-hooks-007",
+ "category": "hooks",
+ "subcategory": "internals",
+ "difficulty": "intermediate",
+ "prompt": "When you call has_filter($hook, $callback), what does it return if the callback IS registered?",
+ "type": "multiple_choice",
+ "choices": [
+ { "key": "A", "text": "true" },
+ { "key": "B", "text": "The priority (int) at which the callback is registered" },
+ { "key": "C", "text": "The callback function itself" },
+ { "key": "D", "text": "The number of times the callback is registered" }
+ ],
+ "correct_answer": "B",
+ "explanation": "has_filter() with a callback argument returns the priority (int) if found, or false if not. Since priority 0 is valid and loosely equals false, you must use === false to check for absence.",
+ "references": ["https://developer.wordpress.org/reference/functions/has_filter/"]
+ },
+ {
+ "id": "k-hooks-008",
+ "category": "hooks",
+ "subcategory": "internals",
+ "difficulty": "basic",
+ "prompt": "Does do_action() return a value?",
+ "type": "multiple_choice",
+ "choices": [
+ { "key": "A", "text": "Yes, it returns the filtered value like apply_filters()" },
+ { "key": "B", "text": "No, it returns void" },
+ { "key": "C", "text": "It returns the number of callbacks executed" },
+ { "key": "D", "text": "It returns true on success, false on failure" }
+ ],
+ "correct_answer": "B",
+ "explanation": "do_action() returns void. Unlike apply_filters() which returns a filtered value, do_action() simply executes callbacks without tracking a return value.",
+ "references": ["https://developer.wordpress.org/reference/functions/do_action/"]
+ },
+ {
+ "id": "k-hooks-009",
+ "category": "hooks",
+ "subcategory": "internals",
+ "difficulty": "intermediate",
+ "prompt": "What does did_action() return?",
+ "type": "multiple_choice",
+ "choices": [
+ { "key": "A", "text": "true if the action has fired at least once" },
+ { "key": "B", "text": "The number of times the action has fired during the current request" },
+ { "key": "C", "text": "The timestamp of when the action last fired" },
+ { "key": "D", "text": "An array of callbacks that were executed" }
+ ],
+ "correct_answer": "B",
+ "explanation": "did_action() returns the number of times the action has fired during the current request (from the $wp_actions global), or 0 if it has never fired.",
+ "references": ["https://developer.wordpress.org/reference/functions/did_action/"]
+ },
+ {
+ "id": "k-hooks-010",
+ "category": "hooks",
+ "subcategory": "internals",
+ "difficulty": "intermediate",
+ "prompt": "In which WordPress version was the WP_Hook class introduced to replace the plain array-based hook system?",
+ "type": "short_answer",
+ "correct_answer": "4.7",
+ "answer_type": "contains",
+ "explanation": "WP_Hook was introduced in WordPress 4.7.0 as a proper class implementing Iterator and ArrayAccess interfaces, replacing the old $wp_filter plain array system while maintaining backward compatibility.",
+ "references": ["https://developer.wordpress.org/reference/classes/wp_hook/"]
+ },
+ {
+ "id": "k-hooks-011",
+ "category": "hooks",
+ "subcategory": "loading_sequence",
+ "difficulty": "intermediate",
+ "prompt": "Which is the correct order of these WordPress loading sequence hooks?",
+ "type": "multiple_choice",
+ "choices": [
+ { "key": "A", "text": "plugins_loaded → init → after_setup_theme → wp_loaded" },
+ { "key": "B", "text": "plugins_loaded → after_setup_theme → init → wp_loaded" },
+ { "key": "C", "text": "init → plugins_loaded → after_setup_theme → wp_loaded" },
+ { "key": "D", "text": "after_setup_theme → plugins_loaded → init → wp_loaded" }
+ ],
+ "correct_answer": "B",
+ "explanation": "The correct loading order in wp-settings.php is: muplugins_loaded → plugins_loaded → setup_theme → after_setup_theme → init → wp_loaded.",
+ "references": ["https://developer.wordpress.org/reference/hooks/wp_loaded/"]
+ },
+ {
+ "id": "k-hooks-012",
+ "category": "hooks",
+ "subcategory": "internals",
+ "difficulty": "intermediate",
+ "prompt": "What is the relationship between doing_action() and doing_filter() in WordPress?",
+ "type": "multiple_choice",
+ "choices": [
+ { "key": "A", "text": "doing_action() is an alias that calls doing_filter() — they check the same $wp_current_filter stack" },
+ { "key": "B", "text": "They check separate stacks: doing_action() uses $wp_actions, doing_filter() uses $wp_filters" },
+ { "key": "C", "text": "doing_action() only works for do_action hooks, doing_filter() only for apply_filters hooks" },
+ { "key": "D", "text": "doing_action() returns an action name, doing_filter() returns a boolean" }
+ ],
+ "correct_answer": "A",
+ "explanation": "doing_action() literally calls doing_filter(). Both check the $wp_current_filter stack. Since actions and filters share the same internal mechanism, there is no separate tracking.",
+ "references": ["https://developer.wordpress.org/reference/functions/doing_action/"]
+ },
+ {
+ "id": "k-hooks-013",
+ "category": "hooks",
+ "subcategory": "internals",
+ "difficulty": "hard",
+ "prompt": "What does the special 'all' hook do in WordPress?",
+ "type": "multiple_choice",
+ "choices": [
+ { "key": "A", "text": "It fires once after all hooks have been processed at the end of the request" },
+ { "key": "B", "text": "It fires for every single action and filter, receiving the hook name as the first argument" },
+ { "key": "C", "text": "It registers a callback that runs for all hooks of a specific type (action or filter)" },
+ { "key": "D", "text": "It returns a list of all registered hooks and their callbacks" }
+ ],
+ "correct_answer": "B",
+ "explanation": "Both apply_filters() and do_action() check for isset($wp_filter['all']). If present, callbacks on the 'all' hook fire for every hook invocation, receiving all arguments with the hook name prepended as the first argument.",
+ "references": ["https://developer.wordpress.org/reference/functions/_wp_call_all_hook/"]
+ },
+ {
+ "id": "k-hooks-014",
+ "category": "hooks",
+ "subcategory": "actions",
+ "difficulty": "hard",
+ "prompt": "The pre_get_posts action uses do_action_ref_array instead of do_action. What is the practical significance of this?",
+ "type": "multiple_choice",
+ "choices": [
+ { "key": "A", "text": "It allows callbacks to receive multiple arguments instead of just one" },
+ { "key": "B", "text": "The WP_Query object is passed by reference, so callbacks can modify it directly" },
+ { "key": "C", "text": "It makes the hook fire asynchronously" },
+ { "key": "D", "text": "It ensures the hook fires before all other actions" }
+ ],
+ "correct_answer": "B",
+ "explanation": "do_action_ref_array passes arguments as an array, preserving PHP references. For pre_get_posts, this means the WP_Query instance is passed by reference (&$this), allowing callbacks to modify query variables directly on the original object.",
+ "references": ["https://developer.wordpress.org/reference/functions/do_action_ref_array/"]
+ },
+ {
+ "id": "k-hooks-015",
+ "category": "hooks",
+ "subcategory": "actions",
+ "difficulty": "hard",
+ "prompt": "When wp_insert_post() fires post-save hooks, in what order do save_post_{$post_type}, save_post, and wp_insert_post fire?",
+ "type": "multiple_choice",
+ "choices": [
+ { "key": "A", "text": "save_post → save_post_{$post_type} → wp_insert_post" },
+ { "key": "B", "text": "wp_insert_post → save_post → save_post_{$post_type}" },
+ { "key": "C", "text": "save_post_{$post_type} → save_post → wp_insert_post" },
+ { "key": "D", "text": "save_post → wp_insert_post → save_post_{$post_type}" }
+ ],
+ "correct_answer": "C",
+ "explanation": "The dynamic save_post_{$post_type} fires first (line 5212 in post.php), then save_post (line 5223), then wp_insert_post (line 5234). The most specific hook fires first.",
+ "references": ["https://developer.wordpress.org/reference/hooks/save_post/"]
+ },
+ {
+ "id": "k-hooks-016",
+ "category": "hooks",
+ "subcategory": "filters",
+ "difficulty": "hard",
+ "prompt": "The cron_schedules filter is unusual because core schedules are merged AFTER the filter runs. What initial value do callbacks receive?",
+ "type": "multiple_choice",
+ "choices": [
+ { "key": "A", "text": "An array containing the three built-in schedules (hourly, twicedaily, daily)" },
+ { "key": "B", "text": "An empty array" },
+ { "key": "C", "text": "null" },
+ { "key": "D", "text": "The previously registered custom schedules only" }
+ ],
+ "correct_answer": "B",
+ "explanation": "The cron_schedules filter passes an empty array: apply_filters('cron_schedules', array()). Core schedules (hourly, twicedaily, daily, weekly) are merged in AFTER the filter via array_merge. Callbacks should return their additional schedules, not the core ones.",
+ "references": ["https://developer.wordpress.org/reference/hooks/cron_schedules/"]
+ },
+ {
+ "id": "k-hooks-017",
+ "category": "hooks",
+ "subcategory": "filters",
+ "difficulty": "intermediate",
+ "prompt": "How many arguments does the 'the_title' filter pass to callbacks?",
+ "type": "short_answer",
+ "correct_answer": "2",
+ "answer_type": "exact",
+ "explanation": "the_title passes 2 arguments: the title string and the post ID. Callbacks must set $accepted_args to 2 in add_filter() to receive both. The the_content filter, by contrast, passes only 1 argument.",
+ "references": ["https://developer.wordpress.org/reference/hooks/the_title/"]
+ },
+ {
+ "id": "k-hooks-018",
+ "category": "hooks",
+ "subcategory": "internals",
+ "difficulty": "hard",
+ "prompt": "In which WordPress version was did_filter() introduced, allowing you to count how many times a filter has been applied?",
+ "type": "short_answer",
+ "correct_answer": "6.1",
+ "answer_type": "contains",
+ "explanation": "did_filter() was introduced in WordPress 6.1.0, much later than did_action() which has existed since 2.1.0. They use separate globals: $wp_filters for filters and $wp_actions for actions.",
+ "references": ["https://developer.wordpress.org/reference/functions/did_filter/"]
+ },
+ {
+ "id": "k-hooks-019",
+ "category": "hooks",
+ "subcategory": "actions",
+ "difficulty": "intermediate",
+ "prompt": "The wp_enqueue_scripts action is itself hooked to which other hook to fire at the right time?",
+ "type": "multiple_choice",
+ "choices": [
+ { "key": "A", "text": "init" },
+ { "key": "B", "text": "wp_head at priority 1" },
+ { "key": "C", "text": "template_redirect" },
+ { "key": "D", "text": "wp_loaded" }
+ ],
+ "correct_answer": "B",
+ "explanation": "The wp_enqueue_scripts() function is hooked to wp_head at priority 1 in default-filters.php. This function then fires the 'wp_enqueue_scripts' action, allowing plugins and themes to enqueue their assets.",
+ "references": ["https://developer.wordpress.org/reference/hooks/wp_enqueue_scripts/"]
+ },
+ {
+ "id": "k-hooks-020",
+ "category": "hooks",
+ "subcategory": "filters",
+ "difficulty": "hard",
+ "prompt": "The 'authenticate' filter starts with what initial value before any callback processes it?",
+ "type": "multiple_choice",
+ "choices": [
+ { "key": "A", "text": "false" },
+ { "key": "B", "text": "null" },
+ { "key": "C", "text": "An empty WP_User object" },
+ { "key": "D", "text": "A WP_Error object" }
+ ],
+ "correct_answer": "B",
+ "explanation": "The authenticate filter starts with null: apply_filters('authenticate', null, $username, $password). Callbacks return WP_User on success, WP_Error on failure, or null to pass to the next handler in the chain.",
+ "references": ["https://developer.wordpress.org/reference/hooks/authenticate/"]
}
]
}
diff --git a/datasets/suites/wp-core-v1/knowledge/html-api.json b/datasets/suites/wp-core-v1/knowledge/html-api.json
index af17af0..693fe56 100644
--- a/datasets/suites/wp-core-v1/knowledge/html-api.json
+++ b/datasets/suites/wp-core-v1/knowledge/html-api.json
@@ -1,6 +1,6 @@
{
"id": "wp-core-knowledge-v1-html_api",
- "version": "1.1.0",
+ "version": "1.2.0",
"metadata": {
"name": "WordPress Core Knowledge - HTML API",
"description": "Tests knowledge of WordPress HTML API (WP_HTML_Tag_Processor, WP_HTML_Processor)",
@@ -11,6 +11,7 @@
{
"id": "k-html-serialize-001",
"category": "html-api",
+ "subcategory": "processor",
"difficulty": "hard",
"prompt": "What does WP_HTML_Processor::serialize_token() return now that the method is public in WordPress 6.9?",
"type": "multiple_choice",
@@ -27,6 +28,7 @@
{
"id": "k-html-script-reject-001",
"category": "html-api",
+ "subcategory": "tag_processor",
"difficulty": "hard",
"prompt": "What does WP_HTML_Tag_Processor::set_modifiable_text() do when you try to set script contents that include '