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