Complete scope plugin API before RC1: persistence, form encapsulation, validation, capability flags, and documented interface sections
# Complete scope plugin API before RC1: persistence, form encapsulation, validation, capability flags, and documented interface sections **Component:** ai_context **Priority:** High (pre-RC1 architecture) **Blocks:** Multi-vocabulary taxonomy scope plugin, further scope plugin extensibility --- ## Summary The scope plugin API is asymmetric and leaking storage details into callers. **Read and match** are abstracted (`getValuesFromEntity()`, `matchesCurrentContext()`), but **write, clear, index, cleanup, form integration, and save-time validation** are not. `AiContextItemForm`, entity presave logic, `hook_entity_delete()`, and `AiContextScopeIndexService` contain special cases — most notably the `target_entities` DER field — that will repeat as new scopes are added. Recent global-scope work added more ad hoc handling (`hasNonGlobalScopeData()`, manual `target_entities` clearing, inline form detection). Beta/RC1 is the last low-cost window to finish the contract before 1.0. This issue completes the scope plugin API in one pass: 1. **Persistence contract** — entity + form read/write/clear via plugins and manager orchestration 2. **Index and cleanup** — plugin-driven indexing and entity-delete cleanup (replace hardcoded hooks) 3. **Form widget encapsulation** — move target entity DER graft into the plugin (no form special cases) 4. **Stale value validation on save** — `validateStoredValues()` per plugin (proactive integrity) 5. **Capability flags** — replace overloaded `supportsSubscriptions()` with explicit flags 6. **API documentation shape** — single interface with grouped sections (not sub-interfaces) ### Design principles - **Default storage:** scope map field (`scope[plugin_id] => string[]`). - **Custom storage:** exception (only `target_entity` today) — plugin overrides persistence + form methods. - **Single interface:** extend `AiContextScopeInterface` with new methods grouped by section comments; do **not** split into sub-interfaces. - **Implementation organization:** optional internal traits or clearly labeled sections in `AiContextScopeBase` mirroring interface groups; custom scope authors still extend the base class only. - **Upcoming multi-vocab taxonomy scope:** scope map with term ID strings (like `tag`, not like `target_entity`). Separate issue; **depends on this**. **Explicitly out of scope:** migrating `target_entity` from DER field into the scope map; building the multi-vocabulary taxonomy scope plugin itself. --- ## Problem / current gaps | Concern | Today | Problem | |---------|-------|---------| | Detect values on item | Map loop + `hasTargetEntities()` | Callers know storage | | Clear values on item | Map strip + DER field clear | Same | | Save from form | `extractFormValues()` → manual `setScope()` / DER | `target_entity.extractFormValues()` returns `[]` | | Form UI | DER widget built at form root, grafted in `AiContextItemForm` | Plugin owns placeholder only; form + module hooks know DER | | Scope index | Reads `$item->getScope()` only | `target_entity` never indexed | | Entity delete cleanup | Hardcoded in `hook_entity_delete()` for `tag` + DER | Won't scale | | Stale values | Reactive cleanup only (delete hooks, config subscriber) | Invalid IDs can persist until external event | | Global wipe | String checks for `'global'` scattered | Magic plugin ID | | Capabilities | Single `supportsSubscriptions()` boolean | Overloaded: global vs target_entity behave differently | | Orchestration | Logic in form/entity hooks | Manager is discovery + summaries only | | API clarity | ~20 methods on one interface, growing | Needs grouped sections in interface + docs, not interface proliferation | --- ## Proposed resolution Extend `AiContextScopeInterface` and `AiContextScopeBase` with a complete value lifecycle contract. Move looping and orchestration into `AiContextScopeManager`. Refactor all callers to delegate to manager/plugins. Organize the interface and developer docs by **sections, not splits**. --- ## Interface organization: sections, not splits Keep **one** `AiContextScopeInterface`. Group methods with comment sections (and matching headings in `scope_api.md` / `custom_scopes.md`): | Section | Methods (existing + new) | |---------|--------------------------| | Identity & capabilities | `getId()`, `getLabel()`, `getDescription()`, `getWeight()`, capability flags | | Value catalog | `getValues()`, `allowsMultiple()`, `isDynamic()`, `getSelectedValueLabels()` | | Persistence — entity | `getValuesFromEntity()`, `hasValuesOnEntity()`, `applyValuesToEntity()`, `clearValuesOnEntity()` | | Persistence — form | `buildValueForm()`, `extractFormValues()`, `getDefaultFormValues()`, `hasSubmittedFormValues()`, `clearSubmittedFormValues()`, `applyFormValuesToEntity()`, `integrateContextItemForm()` | | Matching | `getCurrentValue()`, `matchesCurrentContext()` | | Index & lifecycle | `getIndexableValues()`, `getCleanupValuesForDeletedEntity()`, `validateStoredValues()` | | Settings | `getConfigName()`, `defaultConfiguration()`, `buildSettingsForm()`, `validateSettingsForm()`, `submitSettingsForm()`, `isEnabled()`, `getManageRoute()`, `getManageLabel()` | Do **not** introduce sub-interfaces (`AiContextScopePersistenceInterface`, etc.). Callers continue to type-hint `AiContextScopeInterface`. `AiContextScopeBase` may use internal traits or labeled method groups matching these sections. Custom scope plugins extend the base and override methods in the relevant section only. --- ## Capability flags: replace `supportsSubscriptions()` `supportsSubscriptions()` currently overloads two different concepts: | Plugin | `supportsSubscriptions()` | Actual behavior | |--------|---------------------------|-----------------| | `tag`, `language`, etc. | `TRUE` | Agents can subscribe on agent form | | `global` | `FALSE` | Applies to all agents automatically; not subscription-based | | `target_entity` | `FALSE` | Context-auto-inclusion via `matchesCurrentContext()` | Replace with explicit flags (names open to review): ```php /** * Whether this scope appears on the agent configuration form. * * FALSE for global (automatic) and scopes not meaningful for agents. */ public function supportsAgentSubscriptions(): bool; /** * Whether a positive matchesCurrentContext() match auto-includes the item. * * TRUE for target_entity. FALSE for global (handled via isGlobal()). */ public function supportsContextAutoInclusion(): bool; /** * Whether this plugin is the global on/off scope. * * TRUE only on AiContextScopeGlobal. Replaces 'global' string checks. */ public function isGlobalScope(): bool; ``` **Migration:** - Deprecate `supportsSubscriptions()` in 1.x; default implementation maps to `supportsAgentSubscriptions()` for BC. - Update `AiContextAgentForm` to use `supportsAgentSubscriptions()`. - Update `AiContextScopeResolver::getContextAutoIncludedItems()` to use `supportsContextAutoInclusion()` instead of `!supportsSubscriptions()` + `'global'` skip. - Remove magic `'global'` plugin ID checks where `isGlobalScope()` applies. --- ## API additions ### Entity persistence ```php public function hasValuesOnEntity(AiContextItem $item): bool; public function clearValuesOnEntity(AiContextItem $item): bool; public function applyValuesToEntity(AiContextItem $item, array $values): void; ``` - **Base:** scope map via `getValuesFromEntity()` / `setScopeValues()` / clear with `[]`. - **Target entity override:** DER field; canonical values remain `entity_type:entity_id` strings. ### Form persistence ```php public function getDefaultFormValues(AiContextItem $item): array; public function hasSubmittedFormValues(FormStateInterface $form_state): bool; public function clearSubmittedFormValues(FormStateInterface $form_state): void; public function applyFormValuesToEntity(AiContextItem $item, FormStateInterface $form_state): void; ``` - **Base:** reads `context_scope[$plugin_id]`. - **Target entity override:** top-level `target_entities` form values. ### Form widget encapsulation (target entity) Move all DER-specific form logic out of `AiContextItemForm` and `ai_context.module` into `AiContextScopeTargetEntity`: - `integrateContextItemForm(array &$form, FormStateInterface $form_state, AiContextItem $entity): void` — move DER widget from form root into scope details; set `#parents`; control `#open` when targets exist; hide when scope disabled or no allowed types - Move `hook_field_widget_single_element_form_alter()` validation and `_ai_context_hide_target_entities_if_empty()` into plugin callbacks (`#process`, `#after_build`, or equivalent) - `buildValueForm()` builds the full scope UI (no placeholder + form graft pattern) - Remove from `AiContextItemForm`: `$scope_id === 'target_entity'` default-value branch, manual widget move block, separate `target_entities` global-wipe handling After this, `AiContextItemForm` scope build loop is uniform: ```php foreach ($enabled_plugins as $plugin) { $form['context_scope'][$id] = $plugin->buildValueForm(...); $plugin->integrateContextItemForm($form, $form_state, $entity); } ``` (Exact method signature can be folded into `buildValueForm()` if preferred — goal is zero plugin-ID conditionals in the form.) ### Index integration ```php public function getIndexableValues(AiContextItem $item): array; ``` Default: `getValuesFromEntity($item)`. Update `AiContextScopeIndexService::indexItem()` to loop enabled plugins. ### Cleanup integration ```php public function getCleanupValuesForDeletedEntity(EntityInterface $entity): array; ``` Refactor `ai_context_entity_delete()` to loop plugins → `AiContextScopeCleanupService::removeScopeValues()`. Remove hardcoded tag vocabulary and DER-specific paths. ### Stale value validation on save ```php /** * Validates stored scope value IDs; returns sanitized list. * * Default: return $values unchanged (no-op for static scopes). * Dynamic scopes: remove IDs that no longer exist or are disallowed. */ public function validateStoredValues(array $values): array; ``` - **Policy (default):** prune invalid IDs on save; optional messenger when values were removed. - **Call site:** manager or `AiContextItemValidator` / presave subscriber during save — after reading values, before `applyValuesToEntity()` and index update. | Scope | Validation | |-------|------------| | `tag` | Term exists in AI context vocabulary | | `entity_bundle` | Bundle exists | | `site_section` | ID in scope settings config | | `target_entity` | Entity loadable; type in allowed list | | `language` | Langcode enabled | | `global` | N/A | Complements reactive cleanup (`getCleanupValuesForDeletedEntity()`); does not replace it. --- ## Manager orchestration Add to `AiContextScopeManager`: | Method | Purpose | |--------|---------| | `getEnabledScopePlugins(bool $excludeGlobal = FALSE)` | Filtered plugin list | | `itemHasNonGlobalScopeData(AiContextItem $item)` | Loop plugins; skip global | | `clearNonGlobalScopeData(AiContextItem $item)` | Loop plugins; skip global | | `applyItemScopeFromForm(AiContextItem $item, FormStateInterface $fs)` | Replace `buildEntity()` scope loop | | `submittedFormHasNonGlobalScopeData(FormStateInterface $fs, ?AiContextItem $original = NULL)` | Global wipe detection + status message | | `clearSubmittedNonGlobalScopeData(FormStateInterface $fs)` | Global wipe in `submitForm()` | | `validateAndApplyItemScope(AiContextItem $item)` | Run `validateStoredValues()` per scope; apply sanitized values; return TRUE if anything changed | --- ## Implementation phases ### Part 1 — Interface sections + capability flags - Add section comments to `AiContextScopeInterface` - Add `supportsAgentSubscriptions()`, `supportsContextAutoInclusion()`, `isGlobalScope()` - Deprecate `supportsSubscriptions()`; update agent form and resolver - Update `scope_api.md` with section headings ### Part 2 — Persistence + manager orchestration - Entity and form persistence methods on interface/base - Manager orchestration methods - Refactor `AiContextItemForm` save/build/submit (except DER graft) - Refactor entity presave / global strip via manager - Index and cleanup plugin integration ### Part 3 — Target entity form encapsulation - Move DER graft, widget alters, after-build into target entity plugin - Remove all `target_entities` / `target_entity` special cases from form and module ### Part 4 — Stale value validation - `validateStoredValues()` on interface/base; overrides on dynamic scopes - Wire into save path via manager - Tests for prune-on-save ### Part 5 — Tests, docs, cleanup - Full regression suite; update developer docs; remove deprecated patterns --- ## Refactor targets - [ ] `AiContextScopeInterface` — section groups + new methods - [ ] `AiContextScopeBase` — defaults (optional internal traits by section) - [ ] `AiContextScopeGlobal`, `AiContextScopeTargetEntity` — capability + persistence + form overrides - [ ] `AiContextScopeManager` — orchestration methods - [ ] `AiContextItemForm` — delegate entirely to manager/plugins - [ ] `AiContextItem` — thin presave; manager for strip/validate - [ ] `AiContextScopeIndexService`, `AiContextScopeCleanupService` - [ ] `ai_context_entity_delete()`, `ai_context.module` (remove target entity widget hooks) - [ ] `AiContextAgentForm`, `AiContextScopeResolver` — new capability flags - [ ] `docs/developers/scope_api.md`, `custom_scopes.md`, `docs/features/scopes.md` --- ## Tests - [ ] Unit: base persistence defaults (map read/write/clear/has) - [ ] Unit: capability flag defaults and overrides per plugin - [ ] Kernel: target entity persistence + form encapsulation (no form graft) - [ ] Kernel: scope index includes target entity via `getIndexableValues()` - [ ] Kernel: term delete cleanup via `getCleanupValuesForDeletedEntity()` - [ ] Kernel: `validateStoredValues()` prunes deleted tag / invalid section ID on save - [ ] Functional: global save clears all scopes (including target entities) + status message - [ ] Functional: agent form respects `supportsAgentSubscriptions()` - [ ] Functional: context-auto-inclusion respects `supportsContextAutoInclusion()` - [ ] Update existing scope plugin, cleanup, and form tests --- ## Acceptance criteria - [ ] Single `AiContextScopeInterface` with documented sections; no sub-interfaces - [ ] `supportsSubscriptions()` deprecated; agent form and resolver use explicit capability flags - [ ] No caller outside scope plugins references `target_entities` or `'target_entity'` string checks (except entity field definition) - [ ] `AiContextItemForm` has no plugin-ID-specific scope branches - [ ] Global scope wipe, validation, index, and cleanup go through manager + plugin methods only - [ ] Stale dynamic scope values pruned on save (with tests) - [ ] All existing scope tests pass; new tests cover full contract - [ ] PHPCS, PHPStan, `./lint.sh` pass - [ ] Developer docs updated with interface sections and override guidance --- ## Out of scope - Moving `target_entity` storage from DER field into scope map - Splitting `AiContextScopeInterface` into sub-interfaces - Intermediate bases for documentation only (e.g. `AiContextScopeMapStorageBase`) - Multi-vocabulary taxonomy scope plugin (separate issue; depends on this) --- ## Risks | Risk | Mitigation | |------|------------| | Large single issue | Phased implementation; clear phase boundaries in MR commits | | Interface growth | Section comments + docs; optional internal traits in base | | DER form build order | Encapsulate in plugin `#process` / `#after_build`; functional tests | | Deprecating `supportsSubscriptions()` | BC alias in base; update all in-module callers in same issue | | Entity presave + services | Presave hook or event subscriber thin glue | | Validation policy | Default prune; document; no hard form errors unless later requested | --- ## Follow-up issues 1. **Multi-vocabulary taxonomy scope plugin** — map storage; depends on this issue 2. **Remove deprecated `supportsSubscriptions()` in 2.0** — delete method and document in CHANGELOG --- ## Filing metadata - **Project:** AI Context (`ai_context`) - **Version:** 1.x-dev - **Category:** Task or Plan - **Tags:** API, architecture, RC1, scope plugins - **Related:** Global scope wipe work (#3586214 area), stale scope values (#3586145 area), upcoming taxonomy scope ## AI usage - [x] AI assisted issue - [ ] AI generated code
issue