Issue #3586388: Align scope scoring and contextual detection.

Description

Aligns scope plugin weight, subscription scoring, UI display order, language detection, contextual hard filtering, and function-call listing so selection behavior matches the documented model from #3586365 (closed).

UI labels after #3586276 (closed) are Entity Types (entity_type) and Specific Entities (entity_item). Stored Entity Types values remain entity_type:bundle (for example node:article).

Scope weight and scoring

  • Built-in weights are now Global 100, Use Case 70, Entity Types 60, Site Section 50, Tag 40, Language 30, and Specific Entities 10.
  • Weight 20 is reserved for the planned taxonomy scope plugin (#3586237 (closed)), which will appear between Language and Specific Entities.
  • Higher weight means earlier UI placement and greater subscription scoring influence.
  • AiContextScopeManager sorts all scope plugins by weight descending, with plugin ID as a tiebreaker. Custom scopes can appear between built-in scopes when given intermediate weights.
  • AiContextScopeResolver::calculateScopeScore() now uses plugin weight directly (weights below 1 are clamped to 1 for scoring only).
  • Subscription scoring skips non-subscription scopes (global, entity_item, and any plugin with supportsSubscriptions() === FALSE). This prevents Specific Entities or other contextual scopes from distorting agent subscription ranking.

Shared content-language service

  • Adds AiContextLanguageService (ai_context.language_service) with:
    • detect() — shared content-language detection
    • translateItem() / translateItems() — centralized translation helpers
  • Replaces the earlier detector/trait approach. Consumers now include:
    • AiContextSelector
    • AiContextScopeLanguage
    • ListAiContextItems
    • LoadAiContextItemById
  • Detection order:
    1. Request path prefix
    2. Same-site Referer path prefix for AJAX requests (Referer host must match the current request host; external Referer URLs are ignored)
    3. Drupal content-language negotiation

Selection cache metadata

  • Adds ai_context.detected_language cache context so selection results vary correctly when language is inferred from the Referer header on unprefixed AJAX paths.
  • AiContextSelector uses this context instead of relying on languages:language_content alone for translated output.
  • The cache context value is the detected language code; Referer is read when computing that value. A generic headers cache context is not needed.

Fail-closed contextual scopes

  • Entity Types: items with type(+bundle) values are rejected when no current entity can be resolved.
  • Site Section: items with section values are rejected when no current request/path is available (matchesCurrentContext() returns FALSE instead of NULL when the request is missing).

Items with no values for those scopes remain neutral/unrestricted.

Function call fixes

  • ListAiContextItems seeds entity context via AiContextCurrentEntityResolver::setCurrentEntity() before filterByCurrentContext() when entity_type and entity_id are provided. This fixes Entity Types-scoped items being incorrectly excluded on admin or other routes without a current route entity.
  • Seeded entity context is cleared in a try/finally block after filtering so later function calls in the same request are not affected by stale entity context.
  • LoadAiContextItemById uses AiContextLanguageService::translateItems() so load-by-id translation follows the same Referer-aware detection as selection and listing.

Review feedback addressed

  • Entity context leak: ListAiContextItems clears seeded entity context after filtering. Covered by ListAiContextItemsEntityTypeTest::testListClearsSeededEntityContextAfterCall.
  • External Referer spoofing: AiContextLanguageService::detect() only uses Referer path prefixes from same-site URLs. Covered by AiContextLanguageServiceTest::testIgnoresExternalRefererLanguagePrefix.
  • Stale PHPUnit paths: testing instructions use AiContextScopeEntityTypeTest.php (renamed from AiContextScopeEntityBundleTest.php) and ListAiContextItemsEntityTypeTest.php (renamed from ListAiContextItemsEntityBundleTest.php).

Documentation

Updates:

  • docs/developers/scope_api.md
  • docs/developers/services.md
  • docs/developers/custom_scopes.md (positive custom-scope weight example)
  • docs/features/scopes.md
  • docs/features/multilingual.md (same-site Referer note)
  • docs/features/context_selection.md (relative scoring order)

Testing instructions

  1. Clear caches after applying the branch: ddev exec drush cr

  2. Verify scope UI order on a context item edit form and agent subscription form. Expected order: Global, Use Case, Entity Types, Site Section, Tag, Language, Specific Entities.

  3. Verify subscription scoring influence:

    • Create two published context items with different Use Case values.
    • Configure an agent subscribed to one use case.
    • Confirm the matching item ranks above the nonmatching item during selection.
  4. Verify non-subscription scopes do not affect subscription scoring:

    • Create a context item with a Specific Entities scope value.
    • Configure an agent with an entity_item subscription entry (Specific Entities is hidden on the subscription form; set this in config or via drush ev).
    • Confirm Specific Entities subscription values do not change ranking for unrelated items.
  5. Verify shared language detection:

    • Configure at least two languages with URL prefixes.
    • Create a French context item and an English context item.
    • From a language-prefixed page, trigger an AJAX selection request whose path has no prefix but whose Referer does.
    • Confirm Language scope filtering and rendered translation both use the prefixed language.
  6. Verify external Referer is ignored:

    • Send a request with no path prefix and Referer https://fr.wikipedia.org/fr/Foo.
    • Confirm content language falls back to negotiation (not fr from the external Referer).
  7. Verify fail-closed Entity Types behavior:

    • Create a context item scoped to a specific type(+bundle), for example Content: Article.
    • Run selection with no current entity context.
    • Confirm the Entity Types-scoped item is excluded.
  8. Verify fail-closed Site Section behavior:

    • Create a context item scoped to a site section/path pattern.
    • Run selection with no current request/path.
    • Confirm the site-section-scoped item is excluded.
  9. Verify list function call Entity Types filtering:

    • Create published context items scoped to node:article and node:page.
    • From an admin path (no route entity), call ai_context_list_ai_context_items with entity_type=node and entity_id for an article node.
    • Confirm the article-scoped item appears and the page-scoped item does not.
  10. Verify list function call clears seeded entity context:

    • Call ai_context_list_ai_context_items with entity_type and entity_id as above.
    • Call again with no entity parameters.
    • Confirm the second call does not still filter as if the article entity were current (only global/unrestricted items should remain when Entity Types context is unavailable).
  11. Run automated coverage:

cd web/modules/contrib/ai_context
./lint.sh

ddev phpunit \
  web/modules/contrib/ai_context/tests/src/Unit/AiContextScopeScoreCalculationTest.php \
  web/modules/contrib/ai_context/tests/src/Unit/AiContextLanguageServiceTest.php \
  web/modules/contrib/ai_context/tests/src/Kernel/AiContextScopeManagerTest.php \
  web/modules/contrib/ai_context/tests/src/Kernel/AiContextScopeResolverTest.php \
  web/modules/contrib/ai_context/tests/src/Kernel/AiContextScopeEntityTypeTest.php \
  web/modules/contrib/ai_context/tests/src/Kernel/AiContextScopeLanguageTest.php \
  web/modules/contrib/ai_context/tests/src/Kernel/AiContextSelectorTranslationTest.php \
  web/modules/contrib/ai_context/tests/src/Kernel/AiContextRequestFactoryResultTest.php \
  web/modules/contrib/ai_context/tests/src/Kernel/ListAiContextItemsTranslationTest.php \
  web/modules/contrib/ai_context/tests/src/Kernel/ListAiContextItemsEntityTypeTest.php
  1. Smoke-test admin pages after cache rebuild:
    • /admin/config/ai/context/items
    • /admin/config/ai/context/settings/general
    • One context item view page

Checklist

  • I have updated the MR title to use format: Issue #1234: My issue title.
  • I have updated the MR description to include: Closes #1234
  • I have performed a self-review of my own code
  • I have added or updated tests, or explained in the description why this change is not covered by tests
  • I have updated documentation for any new or changed functionality
  • I have written testing instructions and verified them locally
  • I have noted any required post-merge steps (config imports, cache rebuilds, manual changes)
  • This MR contains no breaking API or hook changes, or they are explicitly documented in the description

Post-merge steps

  • Run drush cr after deployment.

AI Compliance

Note

Check the one that best describes your usage, or leave all unchecked if AI was not significantly used.

  • AI Assisted Code
    Mainly written by a human; AI used for autocomplete or partial generation under full human supervision.

  • AI Generated Code
    Mainly generated by AI, reviewed and approved by a human before this MR was created.

  • Vibe Coded
    Generated by AI and only functionally reviewed before this MR was created.

Closes #3586388 (closed)

Edited by Kristen Pol

Merge request reports

Loading