## Summary - introduce the governance subject taxonomy registry and canonical Baseline Scope V2 normalization and persistence - update baseline profile Filament surfaces, validation, capture/compare gating, and add the optional scope backfill command with audit logging - add focused unit, feature, Filament, and browser smoke coverage for save-forward behavior, operation truth, authorization continuity, and invalid-scope rendering - remove the duplicate legacy spec plan under `specs/001-governance-subject-taxonomy/plan.md` ## Verification - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec202GovernanceSubjectTaxonomySmokeTest.php` - focused Spec 202 regression pack: `56 passed (300 assertions)` - `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` ## Notes - no schema migration required - no new Filament asset registration required - branch includes the final browser smoke test coverage for the current feature Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #232
102 lines
8.0 KiB
Markdown
102 lines
8.0 KiB
Markdown
# Research: Governance Subject Taxonomy and Baseline Scope V2
|
|
|
|
## Decision: Evolve the existing `BaselineScope` entrypoint into the V2-aware scope orchestration layer
|
|
|
|
### Rationale
|
|
|
|
`BaselineProfile::scopeJsonb()` already normalizes persisted scope through `BaselineScope`, and both `BaselineCaptureService` and `BaselineCompareService` resolve their effective scope through the same entrypoint. Reusing that integration point keeps the rollout narrow and avoids a split where legacy and V2 scope models coexist as competing top-level contracts.
|
|
|
|
### Alternatives considered
|
|
|
|
- Introduce a parallel `BaselineScopeV2` class and migrate consumers later: rejected because it would duplicate the current integration boundary and make rollout ambiguity worse before it gets better.
|
|
- Leave `BaselineScope` untouched and normalize only in the model cast: rejected because capture, compare, tests, and operation context already rely on `BaselineScope` behavior outside the model cast.
|
|
|
|
## Decision: Build the taxonomy registry by composing existing config and support metadata contributors
|
|
|
|
### Rationale
|
|
|
|
The current truth for selectable baseline subjects already exists in three places: `tenantpilot.supported_policy_types`, `tenantpilot.foundation_types`, and `InventoryPolicyTypeMeta::baselineSupportContract()`. The narrowest registry is therefore a consolidation layer that reads and shapes those contributors into one authoritative contract for baseline selection.
|
|
|
|
### Alternatives considered
|
|
|
|
- Add a new config file dedicated to governance subjects: rejected because it would create a second manual source of truth for the same policy and foundation types.
|
|
- Hardcode baseline subject mappings inside a registry class: rejected because it would duplicate labels, support flags, and future updates already owned by config and existing support metadata.
|
|
|
|
## Decision: Introduce platform-facing taxonomy vocabulary separate from the current baseline support enums
|
|
|
|
### Rationale
|
|
|
|
The existing `SubjectClass` and `ResolutionPath` enums describe support resolution internals such as `policy_backed` and `foundation_inventory`. They do not describe operator-facing governed-subject shape. Reusing them as the new taxonomy would leak implementation semantics into the platform contract.
|
|
|
|
### Alternatives considered
|
|
|
|
- Reuse `SubjectClass` as the new platform taxonomy: rejected because `policy_backed`, `foundation_backed`, and `derived` encode comparison internals rather than stable platform vocabulary.
|
|
- Collapse taxonomy and support metadata into one enum family: rejected because operation support and governed-subject shape are related but distinct concerns.
|
|
|
|
## Decision: Map current Intune policy types to `intune/policy` and current foundations to `platform_foundation/configuration_resource`
|
|
|
|
### Rationale
|
|
|
|
The current baseline scope has two real buckets: standard Intune policy types and non-policy foundation artifacts. Mapping the first bucket to `domain_key = intune` and `subject_class = policy` keeps the Intune adapter explicit. Mapping the second bucket to `domain_key = platform_foundation` and `subject_class = configuration_resource` removes the unnamed special category while staying broad enough to cover the concrete current foundation artifacts such as assignment filters, scope tags, notification templates, and Intune RBAC definitions.
|
|
|
|
### Alternatives considered
|
|
|
|
- Map foundations under the `intune` domain: rejected because it would keep policy and non-policy baseline subjects collapsed inside the adapter taxonomy that this spec is trying to make explicit.
|
|
- Use `posture_dimension` for all current foundation types: rejected because the current concrete foundation artifacts are configuration resources, not derived posture dimensions.
|
|
|
|
## Decision: Use tolerant read plus save-forward as the rollout strategy
|
|
|
|
### Rationale
|
|
|
|
The current repo has broad test and feature coverage that writes legacy `scope_jsonb` shapes directly. Tolerant read plus save-forward allows those rows to remain valid while new or updated baseline profiles become canonical V2 immediately. This lowers rollout risk and avoids an all-at-once migration event.
|
|
|
|
### Alternatives considered
|
|
|
|
- Eagerly rewrite all existing rows in a migration: rejected because it increases rollout churn and couples semantic correction to data-rewrite risk.
|
|
- Leave legacy and V2 rows mixed indefinitely: rejected because it would preserve two canonical shapes instead of one.
|
|
|
|
## Decision: Keep the baseline UI Intune-first and derive V2 internally
|
|
|
|
### Rationale
|
|
|
|
The feature is a contract upgrade, not a multi-domain product launch. The current Filament baseline profile form already exposes `policy_types` and `foundation_types` separately. Keeping that operator-facing shape for now avoids false breadth while still allowing save-forward persistence into explicit V2 entries and better normalized summaries.
|
|
|
|
### Alternatives considered
|
|
|
|
- Replace the form with a multi-domain taxonomy picker now: rejected because only Intune-backed baseline subjects are currently ready for operator use.
|
|
- Keep the UI and continue persisting legacy arrays: rejected because the persistence contract is the core defect this spec is solving.
|
|
|
|
## Decision: Persist canonical effective scope in operation context and keep compatibility projections only as a transition aid
|
|
|
|
### Rationale
|
|
|
|
`BaselineCaptureService` and `BaselineCompareService` already write `effective_scope` into `OperationRun.context`. Making that payload canonical V2 improves auditability and debug clarity. Some legacy-compatible projections may still be needed by current surfaces or tests during rollout, but they should be derived from V2 instead of remaining authoritative.
|
|
|
|
### Alternatives considered
|
|
|
|
- Keep only the legacy context payload: rejected because it would leave operation auditability tied to the very ambiguity this spec is removing.
|
|
- Emit only V2 immediately with no compatibility projection: rejected because current code and tests may still read the old keys during the transition.
|
|
|
|
## Decision: Merge duplicate entries only when they are semantically identical and reject ambiguous overlaps otherwise
|
|
|
|
### Rationale
|
|
|
|
Legacy scope behaves like a union of subject types. V2 should preserve that determinism without hiding ambiguous entry definitions. If two entries share the same `domain_key`, `subject_class`, and normalized `filters`, their `subject_type_keys` can be merged safely. If overlapping subject types appear across entries with different filters, the system should reject the payload until filter semantics are explicitly supported.
|
|
|
|
### Alternatives considered
|
|
|
|
- Always merge duplicate entries regardless of filters: rejected because it would silently flatten distinct meanings once filters become real.
|
|
- Reject every repeated entry shape: rejected because the legacy shape and current operator intent behave like set union, not strict uniqueness by payload object.
|
|
|
|
## Decision: Deliver cleanup as an optional Artisan command rather than a mandatory migration step
|
|
|
|
### Rationale
|
|
|
|
The product only needs one canonical shape going forward. Rewriting historic rows is housekeeping, not a functional prerequisite. An optional command keeps that concern explicit and schedulable after rollout confidence exists. To remain aligned with the constitution's write-safety rules, the command should default to preview mode, require explicit write confirmation for committed rewrites, and emit audit entries only when a real mutation occurs.
|
|
|
|
This cleanup decision applies only to `baseline_profiles.scope_jsonb` in this release. Tenant assignment overrides stay on tolerant-read normalization so the rollout remains narrow and compare behavior stays correct without introducing a second maintenance rewrite surface.
|
|
|
|
### Alternatives considered
|
|
|
|
- Omit cleanup entirely: rejected because the repo should still have a supported path to remove legacy rows once the rollout stabilizes.
|
|
- Hide cleanup inside a request path or scheduled job: rejected because schema-shape rewrites should remain a deliberate operator or maintenance action with explicit write intent and auditable consequences. |