You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Added
Regression snapshot test pinning the summarize CORPUS AWARENESS prompt bullet. A new test (testSummarizeCorpusAwarenessMatchesCanonicalSnapshot) reads tests/fixtures/corpus-awareness-bullet.txt and asserts the resolved 'summarize' template (DefaultPrompts::getTemplate()) contains that exact bullet byte-for-byte, guarding against silent drift (follow-up to the tag1consulting/scolta-core#33 corpus-statistic fix). The fixture is kept hand-identical to the matching bullet in scolta-core's SUMMARIZE constant. Test-only; no runtime change.
Category-member and context decomposition rules in the default expand_query prompt (tag1consulting/scolta-core#36). The 'expand_query' template (resolved server-side by DefaultPrompts for the AI path) gains two rules so the model decomposes a grouping into concrete terms instead of restating it as an abstract synonym: rule 13 (CATEGORY → MEMBERS) expands a category/family/region into its well-known members ("version control systems" → Git/Mercurial/Subversion; "Southeast Asian food" → Thai/Vietnamese/Indonesian), and rule 14 (CONTEXT / USE-CASE → CONCRETE ITEMS) expands a context/occasion into the item types that serve it ("home office setup" → standing desk/ergonomic chair/monitor arm). Both lead with non-food examples so the behavior generalizes across domains, and rule 13 forbids fabricating members for categories the model does not know — those fall back to normal alternate phrasings. The 2-4 term cap is reconciled to allow up to 6 concrete members when decomposing, and rule 7 is narrowed to taxonomy/filter-label matching so it no longer contradicts rule 13. Additive: queries that are not categories or contexts expand exactly as before. The template is byte-identical to scolta-core's EXPAND_QUERY constant; the browser-side WASM must be rebuilt downstream to match.
Round-robin AI-summary candidate selection across expansion sub-queries (#170). When a query fans out into distinct sub-topics of unequal corpus size (e.g. "traditional dishes from Southeast Asia" → Thai, Vietnamese, Indonesian, …), the relevance-union top-N that feeds the AI summarizer is filled entirely by the single largest sub-query, so the overview can only ever describe that one sub-topic. Two new browser-side config keys address this: expansionCombineMode (relevance_union default | round_robin) and expansionPerTermTopK (default 3). Under round_robin, scolta.js stamps each loaded result with the expansion sub-query that produced it (__scoltaSourceTerm), groups the summary candidate pool by that provenance, and deals the top-K from each sub-query in turn until aiSummaryTopN is filled — so the summarizer sees breadth across sub-topics. The reallocation stays within the existing aiSummaryTopN / aiSummaryMaxChars budgets and never exceeds them. The default relevance_union reproduces current behavior exactly, and the visible ranked result list is unchanged (always relevance-sorted) — round-robin only affects what the summarizer is shown. A single-sub-query pool (focused single-intent query) is identical to relevance_union.
ConfigReferenceDocTest — a CI guard that keeps docs/CONFIG_REFERENCE.md in sync with ScoltaConfig. The new test parses the property tables and preset table in CONFIG_REFERENCE.md and asserts that every documented scalar default equals the live ScoltaConfig default, that every scalar property is documented, and that every non-default preset override is documented with a matching value. It fails loudly if the tables can no longer be parsed, so a future doc reformat surfaces as a fixable failure rather than a silently-skipped check. This is the guard that would have caught the README default drift fixed in this release. The test also pins docs/TUNING.md: that guide restates the current global default for each scoring parameter it discusses (its **Config:** … — **Default: X** lines), and those restatements can drift independently of CONFIG_REFERENCE.md, so a new test_documented_tuning_guide_defaults_match_live_config asserts each one equals the live ScoltaConfig default too.
docs/TUNING.md — the canonical scoring-tuning evidence guide. Opens with a plain "choose your site type → preset" section for admins, then the full scoring-sweep evidence (precision-cliff data, per-parameter sweeps, methodology, and which defaults are still open findings) for maintainers. Cross-links CONFIG_REFERENCE.md for the property list and the site-type → preset table rather than duplicating them; CONFIG_REFERENCE.md's preset section now links back to it.
assets/ASSETS.sha256 manifest covering all duplicated front-end assets. The four browser assets duplicated into scolta-drupal and scolta-wp (js/scolta.js, css/scolta.css, wasm/scolta_core.js, wasm/scolta_core_bg.wasm) are now hashed into a single manifest, regenerated by the new composer update-asset-manifest script. The adapters drive both their dev-time copy and their CI drift guard from this one manifest, so a newly added asset can no longer be guarded in one place and forgotten in another. update-browser-wasm now regenerates the manifest after copying the WASM. The existing per-file assets/js/scolta.js.sha256 is retained (scolta-laravel's HealthController and StatusCommand read it at runtime as a bare hash) but is now derived from ASSETS.sha256 rather than hashed independently: update-js-checksum regenerates the manifest and then extracts the js/scolta.js line into the standalone file, so every asset SHA-256 comes from one computation and the two files can never diverge. A new AssetManifestTest fails CI if the committed manifest is stale, any listed asset is missing, the standalone checksum drifts from the manifest's js/scolta.js line, or that file stops being a bare 64-hex hash.
AiServiceAdapter::handlePossibleBudgetException() hook. The base message(), conversation(), and messageForOperation() methods now wrap their AI call in a try/catch (\RuntimeException) that invokes a new protected handlePossibleBudgetException() hook before re-throwing. The base hook is a no-op, so behavior is unchanged for callers; platform adapters (scolta-drupal, scolta-laravel, scolta-wp) override it to convert an Amazee budget-exhaustion error into AmazeeBudgetExceededException (and notify a budget handler) without each having to override all three AI methods. This removes the need for the three near-identical try/catch overrides duplicated in every adapter. Backward-compatible: an un-updated adapter that still overrides the three methods keeps working because the wrapper exception's message does not contain the Budget has been exceeded! guard string, so the doubled hook call is an idempotent no-op on the second pass.
Fixed
fromArray() now treats null as "not set," so adapters can fall through to a Site Type preset. The override loop previously assigned every key present in the input over the preset's values, with no way to express "this field is unset — use the preset." Adapters that omit a key (Drupal, WordPress) got preset fall-through for free, but an adapter whose config layer always emits a key for every field (notably Laravel's config/scolta.php) could never reach a preset's value — its concrete config default always won, leaving the entire Site Type preset (~12–15 scoring/display fields, not just expansionCombineMode) inert. fromArray() now skips any null value (null = "not set" → use the preset, or the base default when no preset is named); an explicit non-null value still overrides. This makes the unset contract explicit for every adapter and removes a latent TypeError (assigning null to a typed property previously threw). This is the central fix that enables adapters — especially Laravel — to honor presets; the Laravel config defaults switch to null in its own PR. CONFIG_REFERENCE.md documents the contract in prose.
AI summaries no longer truncate mid-sentence on multi-item results (#168).aiSummaryMaxTokens defaulted to 512, but max_tokens is a hard ceiling, not a target — a 6-item summary with one-sentence descriptions plus ad-hoc subcategory headers overran it and was cut at the token boundary mid-word. Two complementary fixes: the default is raised to 1024 (the prompt structurally bounds real output to ~300–600 tokens, so this adds headroom without inviting longer output), and the 'summarize' template's FORMAT RULES now state an explicit output-length budget — keep the summary under ~150 words, with a single flat bulleted list and no section/sub-category headers. The raised ceiling guarantees nothing is cut; the stated budget keeps natural output short. No preset overrides ai_summary_max_tokens below the default. The prompt line matches scolta-core's SUMMARIZE constant.
Removed the Wikipedia-specific corpus statistic from the default summarize prompt. The CORPUS AWARENESS rule in the 'summarize' template shipped a hard-coded "~6,900 Featured Articles" example that described only the Wikipedia demo, reached every site using the default prompt (the server resolves this template for the AI overview), and taught the model to fabricate corpus counts. The example is now count-free and frames gaps via the site description's scope, and the rule explicitly forbids inventing statistics (counts, totals, sizes). Matches the same change in scolta-core's SUMMARIZE constant (tag1consulting/scolta-core#33).
Changed
Facet panel is now index-driven and static, with exact typed-query counts that don't move on AI expansion or when you click a facet. The panel previously derived its dimensions, values, and counts from the current result set and recomputed them on every facet toggle, so under AI expansion (which lands asynchronously and nondeterministically) the numbers reshuffled and dimensions could appear/disappear/reorder between paints — the confusing behavior the recently-merged drop-self fix only partially addressed. The model is rebuilt in scolta.js: (1) the panel's dimension and value lists come from the index taxonomy (pagefind.filters()), shown in a fixed alphabetical order on every search, so nothing ever appears, disappears, or reorders; (2) facet counts are the exact breakdown of the typed query, taken straight from Pagefind's native per-search .filters — a single Pagefind search on the typed query (scoped only by structural filters such as the auto-language default, never by user facet selections) supplies per-value counts, computed once in computeQueryFacetCounts() when the query is submitted and reused for every later render; (3) clicking a facet filters the results and updates only the header count — no facet count changes. Zero-count values render disabled (greyed) unless currently active. This supersedes the drop-self faceting fix (computeDropSelfFacets), which is removed: collapse is now structurally impossible because the panel never derives from results. Deliberate consequence: because the counts describe the typed search term, they can read lower than the header "N results" once AI expansion pulls extra docs into the result list — the header reflects the expanded result set, the counts reflect your term's breakdown of the index. They are exact, stable across expansion, and never move on a facet click. (An expansion-aware variant that re-summed .filters across the expanded term set was tried and dropped: Pagefind has no way to count a union of multiple search terms — its any/all/none/not operators apply to filter values, not search terms — so summing per-term .filters double-counts any doc matching more than one expanded term, producing counts that exceed the result total. The typed-query counts are exact and free; the summed counts were irreducibly over-counted.) With two or more facets selected across dimensions the counts remain descriptive, not exact predictors of the combined filter — a value's count reflects how the typed query breaks down by dimension, not how the active filter stack resolves. Covered by a no-jump-on-click regression test, a counts-stable-across-expansion test, and taxonomy-driven panel tests in tests/js/faceting.test.js.
expansionCombineMode is now a per-Site-Type preset default, and expansionPerTermTopK (K) is locked at 3. Following the #170 evaluation, round_robin is the better summary-candidate strategy on faceted corpora and a safe no-op elsewhere, so the content_catalog, blog, and ecommerce presets now default expansionCombineMode to round_robin; reference, none, and the base default stay relevance_union. The mode resolves with the same precedence as the other preset-overridable scoring fields (titleMatchBoost, recencyBoostMax, expandSubwordMaxFrequency): explicit per-site value > preset default > base default, so a site can still override it. The per-sub-query top-K is no longer a tunable setting — evaluation found no benefit above 3 and over-reach below it — so K is locked at 3: it stays a ScoltaConfig::$expansionPerTermTopK internal constant always emitted to the JS config as 3, and fromArray() now ignores any expansion_per_term_top_k input. CONFIG_REFERENCE.md documents the per-preset combine-mode defaults and flags expansionPerTermTopK as internal; ConfigReferenceDocTest is extended to pin both. No scolta.js logic change — it already reads the locked K through the existing config plumbing. (Platform adapters drop their expansion_per_term_top_k admin field/config key in their own PRs.)
De-duplicated the configuration defaults catalog: CONFIG_REFERENCE.md is now the single source of truth.README.md's ## Configuration Reference section restated the entire property catalog (with types and defaults) and had drifted from the code — it showed recencyBoostMax 0.5 (actual 0.25), titleMatchBoost 1.0 (actual 2.0), and crossListBonus 0.15 (actual 0.05). The duplicated property tables are replaced with a pointer to docs/CONFIG_REFERENCE.md, which carries the correct values and is now verified in CI by ConfigReferenceDocTest. No default values changed in ScoltaConfig.
Rebuilt the bundled scolta-core WASM (assets/wasm/) so the browser asset carries the updated summarize prompt. The prior change updated the PHP prompt source (resolved server-side, on the live AI path) but left the committed WASM binary stale. The binary is rebuilt from merged scolta-core main and now embeds the explicit output-length budget (under ~150 words, single flat bulleted list, no section/sub-category headers; #168) plus the count-free CORPUS AWARENESS rule. Platform adapters copy this bundle via their copy-assets step, so refreshing it here is what propagates the new browser asset downstream. (tag1consulting/scolta-core#37)