v1.0.5
Fixed
- CI asset-drift guard now verifies the WASM pair in its manifest-absent fallback, not just
scolta.js. The "Verify duplicated assets" CI step is manifest-driven: when scolta-php shipsassets/ASSETS.sha256it checksums all four canonical runtime assets (assets/js/scolta.js,assets/css/scolta.css,assets/wasm/scolta_core.js,assets/wasm/scolta_core_bg.wasm) and is correct. But its backward-compat fallback — taken when the vendored scolta-php predates the manifest — checksummed onlyassets/js/scolta.jsand thenexit 0'd, so a sync that refreshed the JS but leftscolta_core.js/scolta_core_bg.wasm(orscolta.css) stale passed CI. That JS-only verification path is the class of gap behind demos shipping a stale browser WASM scorer. The fallback now verifies every canonical asset directly against the vendored scolta-php source (vendor/tag1/scolta-php/assets/<rel>), covering the WASM glue + binary pair and the CSS, so a stale WASM can no longer slip through regardless of which branch runs. Guarded byStructuralIntegrityTest::test_ci_asset_verify_fallback_covers_wasm_pair, which asserts the fallback branch referencesscolta_core.js,scolta_core_bg.wasm, andscolta.css(it fails against the oldscolta.js-only fallback). The Composercopy-assetshook, the manifest-present CI branch, and thebuild-dist.sh/validate-dist.shdist paths already covered both WASM files and are unchanged. - Amazee credentials provisioned without resolved model names now self-heal instead of breaking AI permanently. The Amazee.ai trial provisioner persists credentials (LiteLLM token + URL) and resolves the model names in two steps;
scolta_auto_provision_amazee()stored the credentials but discarded the resolved models (it passed noonModelsResolved), soScolta_Ai_Service::from_options()drove the LiteLLM gateway with the shipped dated defaultclaude-sonnet-4-5-20250929, which the gateway rejects with HTTP 400 — summarize silently returned nothing and expand ran unexpanded. (A distinct failure class from the expired-key recovery above: a400 invalid-modelfrom a half-provisioned credentials store, not an auth failure.) Two changes fix it: (1)scolta_auto_provision_amazee()now passes a guardedonModelsResolved(scolta_amazee_persist_resolved_models()— persists the resolved names intoscolta_settings, only filling the dated default / empty expansion so an admin's explicit model choice is never clobbered, mirroring the admin Amazee connect flow) and scolta-php's newhasResolvedModelspredicate (scolta_amazee_models_resolved()), so when credentials are stored but only the dated default is persisted the library re-resolves against the already-stored key (never a fresh trial) and self-heals; the predicate deliberately treats the dated default as unresolved, because settings seedai_modelto it and a naive "non-empty" check would never fire the heal. (2)from_options()now degrades on the Amazee path when no resolved model is present: it does not inject the Amazee key, so a key-less client throwsApiKeyMissingException(degraded to an unexpanded/no-summary HTTP 200 by the REST controllers) rather than sending the gateway the dated default (HTTP 400). The explicit-key path is unchanged (a dated model is valid on the real Anthropic API); only the Amazee branch degrades. Requires scolta-php ≥ 1.0.4 (unreleased) (AutoProvisioner::ensureAiAvailable()'s$hasResolvedModelsparameter); the committedcomposer.lockstays pinned at the stable release and acomposer update tag1/scolta-phplock bump lands once scolta-php 1.0.4 is tagged. Covered byModelResolutionSelfHealTest(predicate reports unresolved for the dated default; the realAutoProvisionerre-resolves against the stored key, driven by the actual callbacks; the persist guard preserves a user-configured model; a naive non-empty predicate provably would not heal; the degraded service throwsApiKeyMissingExceptionwithout calling the gateway) and the newAmazeeAiServiceTest/AutoProvisioningTestcases. scripts/validate-dist.shno longer writes fixed-name scratch files into the working directory or leaks them on failure. The dist validator wrote nine fixed-name scratch files (zip-contents.txt,zip-files.txt,zip-nonsource.txt,network-callsites-raw.txt,network-callsites.txt,network-hosts-found.txt,network-hosts-allowed.txt,readme-external-services.txt,readme-compiled-assets.txt) into the current working directory and onlyrm -f'd them on the success path — underset -euo pipefailwith ~12exit 1paths, any failure left all nine plus themktemp -dextract dir behind. Predictable names in a shared/reused CWD are a TOCTOU surface (a pre-existing or concurrently-written file of the same name can be read instead of the script's own output, and the script silently clobbers anything by those names), and cleanup-only-on-success pollutes the tree. All scratch state — including the extract dir — now lives under one privatemktemp -dwork dir removed by a singletrap 'rm -rf "$WORK"' EXITthat fires on every exit. Validation behavior, checks, fixtures, and messages are unchanged; this script is a CI/build tool and does not ship inside the plugin ZIP.- Expired Amazee trial keys now recover instead of silently killing AI, and
/healthtells the truth. An Amazee.ai trial key is revoked server-side when the trial ends; the next AI call then returned400 expired_keywhile the adapter no-opped on the stored (dead) credentials — so expand echoed the query, summarize returned nothing, andGET /wp-json/scolta/v1/healthstill reportedai_configured: true(the django demo outage, 2026-06-09).Scolta_Ai_Service::from_options()now wires scolta-php'sKeyExpiryRecoveryinto the AI call path only on the auto-provisioned Amazee path (stored credentials, no explicit key): an auth-class failure triggers a one-shot re-provision of a fresh trial key (guarded to one attempt per window, markers in the transient store) and a single retry. The explicit-key path (SCOLTA_API_KEYenv/constant or a database key) returns before the wiring, so a user's own key is never re-provisioned behind their back; budget-exhaustion is excluded byKeyExpiryRecoveryso it can't reset the spend ceiling. The health endpoint hands the same transient-backedScolta_Cache_DrivertoHealthChecker, so a recorded auth failure surfaces asai_usable: false/ai_auth_failing: true(statusdegraded) while the key is known-bad. Requires scolta-php ≥ 1.0.4 (unreleased) (AiServiceAdapter::setKeyExpiryRecovery(),KeyExpiryRecovery,HealthChecker's$cacheparameter); the committedcomposer.lockstays pinned at the stable release — same accepted interim state as the mergedisBudgetError()/AuthenticatedCipheradoptions — and acomposer update tag1/scolta-phplock bump lands once scolta-php 1.0.4 is tagged. Covered byKeyExpiryRecoveryWiringTest(recovery wired only on the Amazee path, once-per-window re-provision through the transient bridge, health truthfulness). - Re-sync
assets/js/scolta.js,assets/css/scolta.css, andassets/js/scolta.js.sha256from canonical scolta-php (AI-Overview stall fix + drift since scolta-php#205). Headline (scolta-php#217): the sub-word frequency guard computed its corpus denominator with a match-allpagefind.search(null), which downloads the entire word index — 5,678 chunks / 111 MB on a 6,909-page long-form corpus — and the AI summary is sequenced behind it, so the AI Overview stalled for minutes on large sites while the summarize endpoint answers in seconds. The denominator now comes from init-time cached totals (pagefind-entry.jsonpage counts,pagefind.filters()value counts) and the guard never touches the word index. The byte-identical re-sync also carries the previously-unsynced scolta-php main changes: the LLM filter-hint recall guard with offered chips (scolta-php#213 — the CSS gains the offered-chip styles the synced JS references), the unmatched-subject sort fallback (scolta-php#210), and the PER-CS/renderer-parity cleanups (scolta-php#205/#206). No WordPress-side logic changed.
Changed
- The
Generic.CodeAnalysis.UnusedFunctionParameterphpcs exclude is scoped to the files that hold fixed-signature callbacks instead of excluded repo-wide, and a deadNonEnqueuedScriptinline ignore was removed (tooling only, no behavior change). The exclude existed because WordPress hook, REST/shortcode, WP-CLI command, and interface signatures are fixed by their contracts, so those callbacks legitimately ignore trailing parameters — but<exclude-pattern>*</exclude-pattern>silenced the sniff everywhere, including code where a genuinely unused parameter is a real finding. Running the sniff without the exclude showed it fires in exactly six files (scolta.php,cli/class-scolta-cli.php, andincludes/class-scolta-{rest-api,shortcode,build-status-progress-reporter,amazee-budget-handler}.php), each a contract-fixed callback or interface implementation; the exclude is now pinned to those paths so unused parameters anywhere else surface. Separately, an inline// phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScripton aScolta_Admin::sanitize_settings()regression test intests/AdminSettingsSanitizeExtrasTest.phpsuppressed a finding that fires nowhere in the codebase (andtests/is not even in the phpcs<file>set), so it was removed rather than relocated — there is no location that genuinely needs it. Full phpcs and the 747-test suite stay green. - The WordPress.org distribution is now opt-in for ALL remote AI functionality: activation contacts no remote service and AI features default off (WP.org review round 3, Guidelines 7 & 9). Root cause: Amazee.ai auto-provisioning shipped (rc2) as a zero-config onboarding feature with configuration gates (
scolta_has_explicit_api_key(), stored credentials) but no consent gate — activation POSTed the site admin email to api.amazee.ai and enabledai_expand_query/ai_summarizeby default, and no test asserted what activation must NOT do. One code path now serves both products via a build-time constant:SCOLTA_AUTO_PROVISION_DEFAULT(source defaulttruefor self-distributed/partner builds;scripts/build-dist.shflips it tofalsein the staged wp.org copy, failing the build unless the define line matches exactly once, andscripts/validate-dist.shfails any zip whose default is notfalse). With the constant false, activation schedules only the local index build, defaults both AI feature settings off, and records ascolta_ai_optin_pendingoption that drives a dismissible admin notice (server-side dismissal viascolta_ai_optin_notice_dismissed) plus an "Enable AI features" control on the settings page that states — before confirmation — that enabling provisions a free Amazee.ai trial, sends the site admin email address to amazee.ai (api.amazee.ai), and links Amazee.ai's terms and privacy policy. Theadmin_post_scolta_enable_aihandler reuses the existing provisioning path, enables the AI settings only on success, and surfaces provisioning failure as an admin error notice with the features left off. Configuring an explicit API key remains its own consent act and bypasses the opt-in flow entirely.scolta_auto_provision_amazee()now returns the provisioning result and exposes ascolta_pre_auto_provisionshort-circuit filter (thepre_http_requestpattern) so tests can observe provisioning attempts with zero network contact. readme.txt's FAQ and External Services sections now state the opt-in default and exactly what is sent on enable. - Plugin Check warnings are now fatal in CI. The
plugin-checkjob dropped--ignore-warnings: the current result is 0 errors / 0 warnings, so this costs nothing today and stops a warning-grade issue from riding into a review round. If a future warning is a confirmed false positive, the route is plugin-check's--exclude-checksflag in an explicit, justified commit — never re-adding--ignore-warnings(noted in the job comment). - The distribution archive is now fail-closed end to end, because vendor/ was the one fail-open surface (WP.org review round 3, "not permitted files"). Root cause:
build-dist.shallowlists root files/sources/assets but copiesvendor/wholesale and prunes a denylist, whilevalidate-dist.shchecked known-bad instances — so each review round could flag a new file the checker had never heard of. The validator now sweeps EVERY file in the zip (vendor included) against an extension allowlist (php,js,css,wasm,pagefind,json,lock,txt,md) plus enumerated, justified exceptions (LICENSE*/COPYING*, the four Pagefind runtime files by path,composer.json/composer.lock), failing with the path of anything else; it also asserts the four Pagefind runtime files are PRESENT (the indexer copies them into every generated index and silently skips them when missing — upstream defect filed as tag1consulting/scolta-php#212). The reviewer-flaggable surface is under change control:tests/fixtures/dist-manifest-nonsource.txtis the committed sorted list of every non-php/css/js file in the zip, and validation diffs the built archive against it, so any new binary/data file fails CI until the manifest is updated in an explicit commit.build-dist.shnow also prunes vendor dotfiles,.github/,*.yml/*.yaml,*.html,*.xml,*.dist,*.log, andpackage-lock.json. The flagged files themselves were all legitimately required and are retained — the new readme.txt "Source code and compiled assets" section documents where each compiled asset comes from (scolta_core_bg.wasmfrom the public scolta-core repo viawasm-pack, the unmodified Pagefind runtime, dependency LICENSE files kept per their license terms) so the justification travels with the zip.
Added
-
Class-level WP.org review guards (
tests/WpOrgComplianceTest.php): every shipped first-party PHP file is scanned, not just the file a reviewer cited. The review team flags issue classes across rounds ("we may not share all cases of the same issue"), so the round-2 instance tests now have class-wide companions: one test bansini_set(),error_reporting(), anddisplay_errorsacrossincludes/,admin/,cli/,scolta.php, anduninstall.php(round 2 cited only the CLI; the CLI-only test keeps the citation), and the other fails anyABSPATHuse outside an allowlist of site-root-only patterns (defined()guards,require_once ABSPATH . 'wp-admin/...'core includes, and enumerated site-root resolutions) — ABSPATH must never locate plugin files; that is whatSCOLTA_PLUGIN_DIRis for (round 2, REST health handler). Allowed constructs are stripped by pattern before the check, so a disallowed second use hiding on an allowed line still fails, and refactors don't churn line numbers. -
The distribution archive's outbound-network surface is under change control, like the non-source file manifest (WP.org Guidelines 7 & 9 — round 3's root cause was a new feature adding a remote call with nobody noticing).
scripts/validate-dist.shnow extracts the built zip and (1) scans every shipped.phpfile — vendor included, that's where the AI clients live — for outbound-HTTP markers (wp_remote_*,wp_safe_remote_*,wp_remote_fopen,download_url,curl_init/curl_exec,fsockopen,stream_socket_client, andfile_get_contents/fopenwith anhttpliteral on the same line) and diffs the normalizedpath<TAB>markerlist againsttests/fixtures/dist-network-callsites.txt; (2) extracts everyhttps?://host literal from first-party shipped PHP (plugin code plusvendor/tag1/scolta-php/src— first-party remote calls go through Guzzle, so a new endpoint changes a host literal, not a transport call site) and diffs againsttests/fixtures/dist-network-hosts.txt, which categorizes each host asservice(contactable) orreference(docs/UI links only); and (3) asserts everyservicehost appears in the shipped readme.txt== External Services ==section. A new call site, a new host, or an undisclosed service fails the build until the change is consciously gated, disclosed, and committed to the fixture. Seeded from a clean-clone dist build and reviewed entry by entry: five call sites (the CLI's GitHub-API Pagefind version check anddownload_url()release download, plus Guzzle's curl transport, used exclusively by scolta-php's AI clients) and five service hosts (api.github.com, github.com, api.anthropic.com, api.openai.com, api.amazee.ai). readme.txt now names the literal endpoint hosts (api.anthropic.com, api.openai.com, github.com for the Pagefind release download) so the disclosure text matches the hosts the code can contact. -
Shipped-binary justifications are structurally tied to the zip (round 3 re-flagged binaries whose justification lived only in PR threads).
scripts/validate-dist.shasserts each enumerated binary exception (scolta_core_bg.wasmand the four Pagefind runtime files) appears by name in the shipped readme.txt== Source code and compiled assets ==section, so a binary can never again ship without its in-artifact justification. -
WP.org Plugin Check runs in CI against the built zip (
wordpress/plugin-check-action, slugscolta-ai-search): errors fail the job, warnings are reported. Verified locally against the dist-built plugin on a clean WordPress install: zero errors, zero warnings. -
Activation-path network regression tests at the layer the defect lived in (
tests/ActivationNetworkTest.php+tests/integration/activation-network-probe.php): a subprocess probe with a real minimal hook registry runs the actual activation hook (register_activation_hook→do_action, not a hand-called function) under recorders onpre_http_requestand thescolta_pre_auto_provisionseam. Asserts: with the constant false, activation performs zero outbound HTTP, schedules no provisioning, defaults AI off, and sets the pending flag (while still scheduling the local index build); with the constant true, provisioning is scheduled and attempted (intercepted, never real); and the opt-in action provisions exactly once, flips the AI settings, and clears the pending flag. The probe is a subprocess because the build-time constant must be defined before scolta.php loads. -
The
/healthendpoint returns only the overallstatusto anonymous requesters — this is a user-visible API change.GET /wp-json/scolta/v1/healthis publicly reachable (by design, for uptime monitoring) but returned the full diagnostic payload to everyone: AI provider and configuration state, resolved Pagefind binary path and version, index/state directory paths, indexer flags, stale-artifact details, and the index integrity breakdown — reconnaissance detail an anonymous caller has no use for. Anonymous responses now contain exactly{"status": "..."}(HTTP 200 unchanged, and the status still reflects index integrity, e.g.degraded); the full payload requires themanage_optionscapability via an authenticated REST request. Monitoring that only checksstatusis unaffected; monitoring that scraped detail fields anonymously must now authenticate. The admin settings page and dashboard widget read index state throughScolta_Admin::get_health_status()(direct filesystem reads), not this endpoint, so the admin UI is unaffected. -
Ordinary page views no longer pay two non-autoloaded option reads per request. On hosts without a persistent object cache, every request cost one DB query for
scolta_prompt_cache_version(the prompt-cache staleness check onplugins_loaded) and one forscolta_settings(Scolta_Auto_Rebuild::init()reading theauto_rebuildflag oninit). Now: (1) the version scalar is written withautoload=true— it is exactly the option the per-request check reads; existing installs converge without a migration because the value changes on every plugin update, which is whenupdate_option()applies a new autoload flag. (2) Auto-rebuild hooks are registered unconditionally and theauto_rebuildflag is checked inside the callbacks, soscolta_settingsis only read when content actually changes.trigger_rebuild()re-checks the flag at fire time, preserving the old behavior that a debounce event queued before the setting was turned off does not rebuild after it.scolta_settings(growing serialized array, can hold a legacy API key) andscolta_resolved_prompts(read only by AI endpoint requests) intentionally stay non-autoloaded. -
Scheduler and admin builds now run the same streamed, budget-aware orchestrator pipeline as
wp scolta build. Both paths previously violated the gatherer's own generator contract by materializing the entire corpus in RAM (exportToItems()), hardcoded 100-post chunks while the UI documentedchunk_size/memory_budget_profileas applying to PHP builds, and the Action Scheduler path serialized the full corpus into ONEscolta_build_chunkstransient that could exceedmax_allowed_packetand fail silently. Both now stream the gatherer generator throughIndexBuildOrchestratorexactly like the CLI: unchanged posts come back asCachedContentReferences via the timestamp manifest, the configured memory budget drives the chunk math, and resumable chunk state lives in the orchestrator's on-disk state directory — no corpus transient at all. Background rebuilds run as time-boxed slices (~20s per Action Scheduler invocation, checked at chunk boundaries alongside a memory-pressure probe) that resume from committed state, so a large site neither times out a single action nor loses progress; progress is mirrored intoscolta_build_statusby a newScolta_Build_Status_Progress_Reporter, preserving the admin polling contract. Behavior notes: the scheduler's previous whole-corpusshouldBuild()fingerprint no-op is gone (the manifest makes unchanged content near-free instead, andforcestill bypasses it), and a failed background build now reportsFailed: …in the build status instead of silently leaving stale chunk transients. -
The two content pipelines now produce identical ContentItems via one shared mapper (
Scolta_Content_Gatherer::to_content_item()). They had diverged on every field: ID scheme (post-{ID}vs{post_type}-{ID}), date (publish vs modified — recency scoring differed per pipeline), title (rawpost_titlevs entity-decodedget_the_title()), and WooCommerce metadata (PHP pipeline only). Canonical semantics, documented on the mapper:post-{ID}(keeps existing PHP-pipeline indexes and caches valid), the publish date (a typo fix must not bump a post's recency; the export pipeline previously usedpost_modified), entity-decoded filtered titles, and WooCommerce metadata in both pipelines. Thescolta_content_itemfilter now runs inside the mapper, so it also fires for the tracker-drivenget_changed_content()path, which previously skipped it. A parity regression test maps the same post through both pipelines and asserts identical output. -
The Amazee.ai admin step UI is no longer a second source of truth for its strings.
assets/js/amazee-admin.jsre-rendered the PHP-side i18n-wrapped step markup as hardcoded English (with a stray PHP-style translators comment). The JS now useswp.i18n.__()/sprintf()with the same source strings as the PHP step renderers (one set of translations), and the script is registered with thewp-i18ndependency pluswp_set_script_translations( 'scolta-amazee-admin', 'scolta-ai-search' ). -
Amazee.ai credentials are now encrypted with authenticated encryption (scolta-php's
AuthenticatedCipher, encrypt-then-MAC with HKDF key separation) instead of bare AES-256-CBC. [needs unreleased scolta-php] The old format had no MAC, and ANY decrypt failure — including a routine AUTH_KEY salt rotation — returned null = "not connected", silently dropping a working Amazee connection. Now: new credentials are stored asscolta-enc:v1:envelopes; an envelope that fails authentication logs once (throttled) and queues a persistent "reconnect to Amazee.ai" admin notice instead of silently disconnecting; legacy CBC blobs still decrypt via the (decrypt-only) legacy path and are immediately re-encrypted through the helper, so the migration is transparent. No crypto is hand-rolled locally; key derivation lives in scolta-php. -
The duplicated
'Budget has been exceeded!'magic string is gone. [needs unreleased scolta-php]Scolta_Ai_Service::handlePossibleBudgetException()now delegates budget-error detection to scolta-php's publicBudgetAwareProviderDecorator::isBudgetError(), which owns the Amazee budget signal (and checks the exception cause chain, not just the top-level message). -
All four WordPress-minimum declarations agree on 6.1 (the code requires it —
wp_cache_flush_group()): the plugin header and readme.txt header already said 6.1; the readme.txt body Requirements line andphpcs.xml.distminimum_supported_wp_versionsaid 6.0 and now match.PluginHeaderTestasserts all four stay in lockstep. -
readme.txt changelog backfilled with the released 1.0.2/1.0.3/1.0.4 sections (condensed from CHANGELOG.md — wordpress.org users only see readme.txt, which stopped at 1.0.1), and the release workflow gained a
check-readme-changelogjob that fails a tag whose version has no= X.Y.Z =readme.txt section. A test keeps every stable CHANGELOG.md release ≥ 1.0.0 covered. -
Untranslated user-facing strings wrapped for i18n (
scolta-ai-searchtext domain): the activation admin notice, the shortcode "index not built yet" fallback, and theajax_remove_db_keyJSON responses.
Added
- PHPStan static analysis (level 5) with the szepeviktor/phpstan-wordpress extension, a
composer analysescript, and a CIstatic-analysisjob (running against scolta-php dev-main like the test matrix). The rollout immediately caught real bugs: the WP REST controllers'isset( $result['exception'] )debug-log blocks were dead code — the vendor handler never returns that key; it logs through its injected PSR-3 logger, which the WP controller never supplied, so AI endpoint failures were being silently swallowed.make_handler()now passesScolta_Loggerand the dead blocks are removed. One entry was initially baselined — scolta-php'shandleFollowUp()@returnarray shape omitted thelimitkey it actually returns — and removed again once the upstream fix (tag1consulting/scolta-php#208) merged; the baseline is now empty. - phpcs now enforces WordPress-Docs on
includes/,admin/, andcli/, checksuninstall.php, and counts warnings (the errors-only-nflag is gone). The one-time fallout triage added ~120 real docblocks, replaced stale sniff names in existing ignores, gave every remaining discouraged-function warning a targeted written justification, removed an orphaned duplicate docblock inScolta_Admin, and renamed two reserved-keyword parameters. Two sniffs are excluded with written justification: the soft line-length warning (the hard limit stays enforced) and unused-parameter (WP hook signatures are fixed by core).
Fixed
- Starting an Amazee.ai trial no longer overwrites a customized AI model. An operator-precedence bug in the trial-start handler (
$settings['ai_model'] ?? $default === $defaultparses as?? ($default === $default)— always truthy) made the "only adopt the provisioned model when the site is still on the default" guard a no-op, so provisioning clobbered ANY configuredai_model. The null-coalesce is now parenthesized, and a source-parse test bans the?? $x ===pattern acrossadmin/. - The Amazee.ai budget-exceeded admin notice can now actually display. The handler registered its
admin_noticescallback during the REST search request in which the budget error fires — a request where that hook never runs — while still setting the 24h throttle transient, so the trial budget could exhaust, visitors silently lose AI search, and the administrator was never told.handle()now persists a pending-notice transient andScolta_Admin::init()unconditionally hooks a renderer that shows and clears it on the administrator's next admin page load (non-admin page views don't consume it). - The Scolta dashboard widget is no longer registered for non-administrators. It rendered index status, AI configuration state, and a rebuild button (whose POST handler is gated on
manage_options) to every role that can reach the dashboard — info disclosure plus a guaranteed-broken button.add_dashboard_widget()now requiresmanage_options. - The CI "unbounded WP_Query" guard actually fires now. Its grep pattern started at the bare key, so WP's quoted array syntax (
'posts_per_page' => -1) could never match — the guard has been inert since it was added, while the pattern it polices existed ingather_count(). The regex now matches the quoted form (and also scansadmin/andcli/),gather_count()switched from an ids-only unboundedWP_Querytowp_count_posts()(cached COUNT(*), loads nothing) likeScolta_Content_Source::get_total_count(), and an in-suite mirror test re-checks the source tree and proves the CI regex matches the quoted form. - The "Reset to default" button on the custom-prompt fields works now. Its inline
onclickdereferenced two nulls:closest('div').querySelector('textarea')(the textarea is a sibling after the div) and.scolta-badge(the class existed nowhere). The handler moved intoenqueue_admin_scripts()viawp_add_inline_script(), the badge span now carriesscolta-badge, the textarea has an id the button references viadata-textarea-id, andAdminInlineScriptTestnow asserts noonclick=attribute (and no.innerHTMLassignment) anywhere inadmin/— the gap that let this survive the rc4 inline-script cleanup. - The admin "Rebuild Now" handler honors the shared build lock and logs failures. It ran a full synchronous build with no check of
Scolta_Rebuild_Scheduler::LOCK_KEY(the REST sibling 409s on it) and itscatch (\Throwable $e)discarded the exception. It now refuses with an "already in progress" notice while the lock is held, holds the lock itself for the duration (released infinally), and logs the throwable viaScolta_Logger. wp scolta clear-cachedeletes the transient timeout rows too. The singleLIKE '%_transient_scolta_%'could never match_transient_timeout_scolta_*(SQL_matches exactly one character), stranding live expiry rows — the 2026-06-09 fleet regression found 157 of them on one demo after a cache flush. The command now deletes the same value/timeout pattern pair as deactivation and uninstall; a regression test runs the command against a LIKE-emulating wpdb stub and asserts zero_transient%scolta%rows remain.- Health checks resolve the index directory the builder actually wrote to when
output_dirends in/pagefind(2026-06-09 fleet regression, P2). The REST/healthenrichment and the dashboardget_health_status()appended/pagefindto the raw setting, so an already-suffixedoutput_dirpermanently reporteddegraded/fragments: 0against a healthy index. A new sharedscolta_normalize_output_dir()applies the builder's exact normalization (strip one trailing/pagefind); health, the dashboard widget, and the shortcode (which previously duplicated the logic inline) all use it. Regression tests assert suffixed and unsuffixed configs report identical fragment counts and status. Scolta_Tracker::mark_all_for_reindex()early-returns 0 on an emptypost_typessetting instead of interpolating an invalidIN ()clause into the INSERT…SELECT.ai_base_urlis validated as an http(s) URL (esc_url_rawwith an http/https scheme allowlist) instead ofsanitize_text_field, andrecency_curvevalidates its decoded shape ([[days, boost], ...]numeric pairs; anything else is rejected wholesale) instead of accepting any JSON array.- The resolved-prompt cache write on settings save passes
autoload=false, matching its sibling in the version-staleness refresh path — the blob is only read at AI request time. - The test-connection result is rendered via
textContent/createTextNodeinstead of concatenating server-returned values (provider,model,response_time,error) intoinnerHTML. - Removed the dead
--bundleforwarding in the CLI auto-resume path. No command docblock ever declared--bundle, so WP-CLI rejected it before the handler could see it — the branch was unreachable. - Saving the settings form no longer stores a silently-truncated copy of an over-5000-character default prompt as a stale "custom" prompt.
sanitize_prompt()truncated the submitted textarea value to the 5000-character storage cap before comparing it against the built-in scolta-php default, so any default longer than the cap could never match itself and round-trip back to''("use the default"): the truncated text was stored as a custom prompt, silently mangling the prompt and pinning the site to a stale copy that no longer follows upstream default updates. This was latent on every release since the cap was added — the vendored scolta-php 1.0.3summarizedefault is already 5415 characters — and became visible when scolta-php#201 pushed theexpand_querydefault past the cap too, turning scolta-php's cross-repo "Adapter install — WordPress" job red on every scolta-php PR since (the adapter's own CI stayed green because the old test only exercised the under-capexpand_query). The default-match comparison now happens on the untruncated sanitized value (with the default passed through the samesanitize_textarea_field()so the comparison survives WP form sanitization), and the cap applies only to genuinely custom prompts.AdminSanitizeTest::test_prompt_clears_when_matches_defaultnow derives the expected text from the installed scolta-php for all three prompts — making it drift-proof by construction in both CI contexts, whatever prompt text upstream currently ships — and a new test pins the 5000-character cap for custom values. (tag1consulting/scolta-php#201) - Synced
assets/js/scolta.js(and its.sha256) from scolta-php — four shared browser-render bug fixes (tag1consulting/scolta-php#199). Asset-only re-sync of the canonical browser script; no WordPress-side logic changed. (1) Zero-result blank panel: a search that returned no matches blanked the result panel for the entire duration of the asynchronous AI query-expansion round-trip —renderResults()cleared the container and header and returned early while expansion was in flight, so "No results found." only appeared after the expand promise settled (on a slow expand endpoint, a multi-second blank). It now shows a neutral "Searching…" in-progress state while expansion runs, then the terminal results-or-"No results found." once it settles. (2) "1 results": the header now pluralizes the noun on the count (1 resultvsN results). (3) Doubled quotes: a quoted-phrase query rendered as""merge conflict""because the header wraps the query in its own quotes andescapeHtml()does not escape"; a matched surrounding quote pair is now stripped before the template re-adds one. (4) Citation duplication: AI-summary context (buildLLMContext) now collapses results that resolve to the same URL, so the summarizer is no longer handed — and no longer cites — duplicate sources. This resync also carries scolta-php main's previously-unsynced facet-count fix (computeUnionFacetCounts): facet counts no longer read all-zero on a long conversational query whose strict-AND search is empty — counts follow the same per-term OR union the result list uses. The committed copy (and its.sha256) is byte-identical to scolta-php's canonicalassets/js/scolta.js; CI's asset-drift check compares against scolta-phpdev-main, so it passes once scolta-php#199 merges to main.