TenantAtlas/specs/202-governance-subject-taxonomy/research.md
ahmido 7541b1eb41 Spec 202: implement governance subject taxonomy and baseline scope V2 (#232)
## 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
2026-04-13 15:33:33 +00:00

8.0 KiB

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.