Introduce explicit scope types and item inclusion modes; clarify selection in UI and docs
## Summary Follow-up to: - #3586243+s - #3586270+s - #3586196+s Blocked by: #3586243+s _**The two decisions still worth locking before this lands: **(1)** `type` derived vs declared, and **(2)** Entity bundle's default inclusion mode.**_ **Builds on (must land first):** #3586243+s — the scope plugin persistence API and capability-flag refactor. **Keep in mind when architecting:** #3586197+s — boundaries / exclusions builds on this. Beta feedback shows site builders don't have an accurate mental model of how context **scopes** decide what actually reaches an agent's prompt. After #3586243 the *plumbing* is clean (symmetric persistence, explicit capability flags, no scattered `'global'` checks), but the **user-facing model is still missing**: 1. **Matching ≠ inclusion.** Builders expect an item scoped to the current task (e.g. Entity bundle = `node:article`) to be **always included** when the agent works on that content. In practice most scoped items are only **eligible**, then **ranked** by subscription scoring, subject to per-agent limits. Only a few paths guarantee inclusion (Global, **Always include** override, Target entity match). 2. **Scopes don't all combine the same way.** The UI shows every scope in one flat list, so builders read it as either "must match all" (AND) or "any one guarantees inclusion." Neither is correct: some scopes are **additive (OR, ranking)** and some are **filtering (AND, eligibility)**, and that distinction is invisible. 3. **The model exists in the API but not in the product.** #3586243 split behavior into `supportsAgentSubscriptions()` and `supportsContextAutoInclusion()`, but there is still **no single concept of a scope's role** surfaced to users, **no per-item control** over guaranteed inclusion, and contextual scopes (Language, Site section, Entity bundle) still **double as ranking signals** because they remain subscribable. This issue turns the capability flags into a coherent, teachable model and the behavior to match it. We're in beta, so rather than paper over this with copy alone, this issue **completes the scope model on top of #3586243** and makes the UI and docs reflect it. ## Problem ### How selection actually works today (for reference) Runtime selection (`AiContextSelector::loadCandidateItems()`) runs in this order: 1. **Overrides** — `Never include` removes items; `Always include` force-adds them. 2. **Hard context filters** — `AiContextScopeResolver::filterByCurrentContext()`. Filtering scopes (Language, Site section, Entity bundle, Target entity) **exclude** items whose values don't match the current context. AND/eligibility gate. 3. **Guaranteed inclusion** — Global items and Target-entity *positive* matches (`supportsContextAutoInclusion()`) are auto-included (within caps). 4. **Subscription scoring** — remaining eligible items are scored against the agent's subscriptions via `calculateScopeScore()`. Additive/OR: more matches raise the score; none are individually required. 5. **Limits** — top scores kept up to **max items** / **max tokens** (the guaranteed buckets are sliced to the same cap). ### Where the model stands after #3586243 (the remaining gap) #3586243 already replaced the overloaded `supportsSubscriptions()` boolean with two explicit capability flags. The remaining problem is conceptual, not plumbing: | Plugin | `supportsAgentSubscriptions` | `supportsContextAutoInclusion` | Hard filter? (`getCurrentValue` non-null) | Role today | |--------|:---:|:---:|:---:|-----------| | Global | false | — (own constant) | no | always include | | Use Cases | true | false | no | additive ranking | | Tags | true | false | no | additive ranking | | Language | **true** | false | **yes** | filter **and** ranking | | Site Sections | **true** | false | **yes** | filter **and** ranking | | Entity Bundle | **true** | false | **yes** | filter **and** ranking | | Target Entity | false | true | yes (positive ⇒ auto-include) | filter + guarantee | The flags now describe behavior precisely, but **(a)** there's no derived "type" the UI/docs can group by, **(b)** the **bold** rows still set `supportsAgentSubscriptions() = true` *and* hard-filter — the dual role that confuses users — and **(c)** auto-inclusion is a *scope* capability only (Target entity), with no way for an author to mark an ordinary filtered item as "always include when relevant." ### Root causes (UI/docs) 1. **Scope language over-promises.** The item form says scope values "define when this context item should be included," which reads as *guaranteed* inclusion. 2. **Additive vs filtering is indistinguishable** in a single flat list. 3. **Subscriptions sound like filtering but behave like ranking**, with no worked example of eligibility → scoring → limits. 4. **Entity bundle vs Target entity blur two roles** — both say "when the agent operates on…", but bundle only filters while target entity guarantees. 5. **Limits are buried** under Advanced, yet they decide whether a matching item actually appears. 6. **Inclusion Overrides terminology is opaque.** ## Proposed solution ### Part 1 — Complete the scope model (on top of #3586243's flags) > The model below is being ratified in **#3586196**; this issue implements it. It **consumes** #3586243's capability flags and must not reintroduce an overloaded `supportsSubscriptions()`-style signal. **1a. Surface a scope `type` for grouping and docs — derived, not a new stored signal.** The three roles are already implied by #3586243's flags plus hard-filter behavior; expose them as a computed accessor (e.g. `getScopeType()` on `AiContextScopeBase`) rather than a second source of truth: | Type | Derived from | |------|--------------| | **`Categorization`** — additive OR ranking; subscribable | `supportsAgentSubscriptions() === true` and not a hard filter → Use Cases, Tags | | **`ContextFilter`** — hard AND eligibility gate | has a current value / hard-filters → Language, Site section, Entity bundle, Target entity | | **`Global`** — always include | `AiContextScopeGlobal` (existing constant) | > **Open design point (for #3586196):** whether `type` is purely derived, or becomes a declared attribute property that the capability flags are themselves derived *from*. Recommendation: derived accessor first, to avoid two competing taxonomies. Either way, **one** representation wins. **1b. Make contextual scopes filter-only.** Set `supportsAgentSubscriptions() = false` for Language, Site section, and Entity bundle so they stop doubling as ranking signals. Ranking is then driven only by `Categorization` scopes (Use Cases, Tags). This is the key behavior change. **1c. Introduce a per-item `inclusion mode`** — decoupled from scope identity because *importance is a property of the item, not the scope*: - **`ranked`** *(default)* — passing filters makes the item **eligible**; it then competes on `Categorization` ranking within limits. - **`guaranteed_when_relevant`** — if the item passes **all** its `ContextFilter` scopes, it is **auto-included** (joins the guaranteed bucket), bypassing ranking. **1d. Reconcile auto-inclusion with #3586243's `supportsContextAutoInclusion()`.** Today that scope-level flag (Target entity) is the *only* path to auto-include. Generalize it: the scope flag gates *which scopes can drive* guaranteed-when-relevant; the per-item mode *opts the item in*. Target entity items default to `guaranteed_when_relevant`, preserving today's behavior without the special case. **Resulting model (four buckets, cleanly mapped):** | Bucket | Mechanism | |--------|-----------| | **Always included** | `Global` items; items set to `guaranteed_when_relevant` that pass their filters; per-agent **Always include** override (within global/max limits) | | **Eligible + ranked** | Pass all `ContextFilter` scopes; scored against `Categorization` subscriptions (OR); selected up to max items / tokens | | **Excluded** | **Never include**; `ContextFilter` mismatch; unpublished; zero score when subscriptions exist | > **Boundary-aware (for #3586197):** the per-`ContextFilter` control introduced in Part 3 should be designed so a later `None / Include / Exclude` polarity slots in without reworking storage or the form layout. The four-bucket model has room for a veto pass (exclusion) ahead of eligibility. ### Part 2 — Behavior change + migration (beta) - **Language, Site section, Entity bundle leave the agent subscription form** (Part 1b). They become pure eligibility filters. - **`post_update` hook:** strip `language` / `site_section` / `entity_bundle` keys from existing agent `scope_subscriptions`; rebuild the scope index via #3586243's `getIndexableValues()` path. Item scope values are untouched — they still drive filtering. ### Part 3 — Context item edit UI (`AiContextItemForm`) 1. Revise the **Context scope** description to distinguish **eligibility** (filters narrow by current page/task) from **agent configuration** (subscriptions, overrides, limits decide actual injection). No more "define when this is included." 2. Group the scope controls by `type` (the two-tier model from #3586196), using the uniform plugin build loop established in #3586243: - **Match any of these** (`Categorization`): "Helps this item rank higher for agents subscribed to these; not required, not guaranteed." - **Filter by** (`ContextFilter`): "Narrows when this item can be considered; the current context must match. Does not guarantee inclusion." 3. Add the per-item **inclusion mode** control (`ranked` / `guaranteed when relevant`), with help text explaining that "guaranteed when relevant" injects the item whenever its filters match. ### Part 4 — Agent configuration UI (`AiContextAgentForm`) 1. Add a short, always-visible intro explaining the buckets in plain language. 2. **Scope Subscriptions** now only lists `Categorization` scopes (a direct consequence of Part 1b); state that subscriptions **prioritize** among eligible items — they don't force inclusion. 3. **Inclusion Overrides** column: clarify options — - "Default (eligible + prioritized by subscriptions)" - "Always include (force into prompt)" - "Never include (exclude from this agent)" 4. One-line reminder near the items table about **max context items** / **max tokens** (link to Advanced). 5. Contextual help link → docs anchor for "Scope matching examples." ### Part 5 — Documentation (`docs/features/scopes.md`, `agent_configuration.md`, `context_items.md`) Document the three scope types, the two inclusion modes, and the four buckets, with worked examples: - **A — Eligibility, not guarantee.** "Article editorial standards" filtered to `node:article`, mode `ranked`, agent edits an article → eligible, but only injected if it wins ranking within the cap. - **B — Guaranteed when relevant.** Same item set to `guaranteed_when_relevant` → auto-injected whenever the current bundle is `article` (subject to budget). - **C — Forced inclusion.** Same item with agent **Always include** override → always injected for that agent. - **D — Additive vs filtering.** Item with Use Case = Working with Text (Categorization, ranks) **and** Language = French (ContextFilter): in an English context it's **excluded** regardless of the use-case match. Extends the interface/section docs #3586243 added in `scope_api.md` / `custom_scopes.md` with the type/inclusion-mode concepts. ### Part 6 — Scope plugin help Align `getDescription()` / `getSubscriptionDescription()` wording with the type roles so form text matches the docs. ### Optional follow-up (out of scope unless cheap) - Selection preview / "why was this item included / excluded / skipped" hint (ties to `ai_agents_debugger`). ## Open questions 1. **`type`: derived accessor vs declared property.** Recommendation: derive from #3586243's flags + hard-filter to avoid a competing taxonomy. (See Part 1a.) 2. **Entity bundle default inclusion mode.** Should bundle-scoped items default to `ranked` (recommended — bundles are broad, auto-include risks flooding) or `guaranteed_when_relevant` (matches the naive expectation)? Target entity is narrow, so it safely defaults to `guaranteed_when_relevant`; bundles are not. 3. **Ordering when `guaranteed_when_relevant` items exceed `maxItems`.** Proposal: narrower filter match wins (target entity > bundle > site section > language), then most-recently-changed. 4. **Budget protection.** Should there be a sub-cap or warning so a flag-happy site can't let guaranteed-when-relevant items starve ranked items entirely? ## Workaround (today) For any item that must always reach a specific agent when relevant: 1. Open `/admin/config/ai/context/settings/agents/{agent_id}/edit` 2. Set the item's **Inclusion Overrides** to **Always include context item** Or use **Global** / **Target entity** scopes where those semantics fit. ## Affected functional areas - Scope model: `#[AiContextScope]` attribute / `AiContextScopeBase` (derived `type`), `AiContextScopeResolver`, `AiContextSelector` — **building on #3586243's interface and flags** - Contextual scope plugins (Language, Site section, Entity bundle): `supportsAgentSubscriptions()` → false - Context item entity + edit UI (`AiContextItemForm`) — new inclusion mode field - Agent configuration UI (`AiContextAgentForm`) - Config migration (`ai_context.post_update.php`) + scope index rebuild - Docs: `docs/features/scopes.md`, `agent_configuration.md`, `context_items.md`, plus `scope_api.md` / `custom_scopes.md` - Inline scope help (`getDescription()` / `getSubscriptionDescription()`) ## Acceptance criteria - [ ] A single scope-role representation exists (derived `type` over #3586243's capability flags, or a declared property the flags derive from) — **not** a second competing signal; no `supportsSubscriptions()` reintroduced. - [ ] Language, Site section, and Entity bundle have `supportsAgentSubscriptions() = false` and no longer appear on the agent subscription form; ranking uses only `Categorization` scopes. - [ ] Context items have an **inclusion mode** (`ranked` default / `guaranteed_when_relevant`); Target entity items default to guaranteed-when-relevant, preserving current behavior, and the old scope-only auto-include special case is removed in favor of the item mode + `supportsContextAutoInclusion()` gate. - [ ] A `post_update` hook removes obsolete subscription keys and rebuilds the scope index; existing item scope values still filter correctly. - [ ] Item form groups scopes into **Match any** vs **Filter by** and exposes the inclusion mode with clear help; the per-filter control is structured to later accept `None / Include / Exclude` (#3586197) without storage/layout rework. - [ ] Agent form copy states subscriptions **prioritize**, not **guarantee**, unless overridden. - [ ] Docs explain the three types, two inclusion modes, and four buckets, with at least examples A–D. - [ ] A beta tester can answer "If I scope an item to articles only, will it always be injected?" without reading source. ## Related - **Builds on:** #3586243+s (scope persistence API + capability flags) - **Decision record:** #3586196+s - **Enables:** #3586197+s (boundaries / exclusions) - #3586270+s - #3586284+s ## AI usage - [x] AI assisted issue
issue