feat: migrate tcm first coverage core cutover (#481)
Automated PR provided by Codex via Gitea API. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #481
This commit is contained in:
parent
fdd9eb2e82
commit
dfda397eb6
@ -1,35 +1,41 @@
|
|||||||
<!--
|
<!--
|
||||||
Sync Impact Report
|
Sync Impact Report
|
||||||
|
|
||||||
- Version change: 2.14.0 -> 2.15.0
|
- Version change: 2.15.0 -> 2.16.0
|
||||||
- Modified principles:
|
- Modified principles:
|
||||||
- Added Product Surface Contract Gate (PSC-001) so future UI-affecting
|
- First Provider Is Not Platform Core (PROV-001): clarified by ownership
|
||||||
specs must prove page archetype, surface budgets, technical demotion,
|
schema alignment so provider-native identifiers remain metadata, not
|
||||||
browser/no-browser proof, human sanity review, and close-out evidence
|
platform-core ownership truth
|
||||||
- Clarified that completed historical specs are context only and must not
|
- Scope & Ownership Clarification (SCOPE-001): amended from a tenant_id
|
||||||
be rewritten to satisfy newer product-surface gate wording
|
database convention to workspace + managed_environment ownership
|
||||||
- Added sections:
|
- Pre-Production Lean Doctrine (LEAN-001): reinforced by banning tenant_id
|
||||||
- Product Surface Contract Gate (PSC-001)
|
compatibility aliases, dual-write targets, fallback readers, and parallel
|
||||||
|
ownership truth
|
||||||
|
- Added sections: None
|
||||||
- Removed sections: None
|
- Removed sections: None
|
||||||
- Templates requiring updates:
|
- Templates requiring updates:
|
||||||
- .specify/templates/spec-template.md: add Product Surface Impact,
|
- ✅ `.specify/templates/*.md`, `.specify/README.md`, `AGENTS.md`, and
|
||||||
browser verification, human sanity, no-legacy, and merge gate prompts
|
`README.md`: no old mandatory `workspace_id` + `tenant_id` database
|
||||||
- .specify/templates/plan-template.md: add Filament/Livewire/search/action/
|
convention found
|
||||||
asset/deployment posture and Product Surface close-out planning
|
- ⚠ pending `docs/HANDOVER.md`: historical schema inventory still contains
|
||||||
- .specify/templates/tasks-template.md: add Product Surface implementation
|
legacy `tenant_id` table/column references; docs cleanup is outside this
|
||||||
report, browser/no-browser, human sanity, and stop-before-runtime-UI tasks
|
constitution-only task
|
||||||
- .specify/templates/checklist-template.md: add Product Surface review checks
|
- ⚠ pending `docs/research/golden-master-baseline-drift-deep-analysis.md`,
|
||||||
- .specify/README.md: add Product Surface Contract workflow gate
|
`docs/audits/filter-audit-comprehensive.md`, and
|
||||||
- AGENTS.md: add Product Surface implementation rule
|
`docs/audits/2026-03-09-enterprise-rbac-scope-audit.md`: historical
|
||||||
- .gitea/pull_request_template.md: add Product Surface close-out block
|
research/audit references still mention `tenant_id`; review separately if
|
||||||
- CI/script updates:
|
they are promoted back into current guidance
|
||||||
- apps/platform/tests/Feature/Guards/ProductSurfaceContractGateTest.php:
|
- CI/script updates: None
|
||||||
add deterministic file-based workflow guard
|
|
||||||
- apps/platform/tests/Support/TestLaneManifest.php and tests/Pest.php:
|
|
||||||
register the guard as surface-guard / heavy-governance
|
|
||||||
- Commands checked:
|
- Commands checked:
|
||||||
- N/A `.specify/templates/commands/*.md` directory is not present
|
- N/A `.specify/templates/commands/*.md` directory is not present
|
||||||
- Follow-up TODOs: None
|
- Follow-up TODOs:
|
||||||
|
- Spec 414 still contains `tenant_id` terminology and must be patched in
|
||||||
|
its own task to use `workspace_id` + `managed_environment_id` +
|
||||||
|
`provider_connection_id`
|
||||||
|
- This amendment does not migrate runtime code. Current database schema
|
||||||
|
scan found provider-native `entra_tenant_id` metadata columns and no
|
||||||
|
literal `tenant_id` columns; source-level `tenant_id` references remain
|
||||||
|
outside this constitution-only task.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# TenantPilot Constitution
|
# TenantPilot Constitution
|
||||||
@ -228,18 +234,57 @@ ### Tenant Isolation is Non-negotiable
|
|||||||
Scope & Ownership Clarification (SCOPE-001)
|
Scope & Ownership Clarification (SCOPE-001)
|
||||||
|
|
||||||
- The system MUST enforce a strict ownership model:
|
- The system MUST enforce a strict ownership model:
|
||||||
- Workspace-owned objects define standards, templates, and global configuration (e.g., Baseline Profiles, Notification Targets, Alert Routing Rules, Framework/Control catalogs).
|
- Workspace-owned objects define standards, templates, and global configuration.
|
||||||
- Tenant-owned objects represent observed state, evidence, and operational artifacts for a specific tenant (e.g., Inventory, Backups/Snapshots, OperationRuns for tenant operations, Drift/Findings, Exceptions/Risk Acceptance, EvidenceItems, StoredReports/Exports).
|
- Environment-owned objects represent observed state, evidence, and operational
|
||||||
- Workspace-owned objects MUST NOT directly embed or persist tenant-owned records (no “copying tenant data into templates”).
|
artifacts for a specific managed environment.
|
||||||
- Tenant-owned objects MUST always be bound to an established workspace + tenant scope at authorization time.
|
- Provider-owned metadata represents external provider identity, permissions,
|
||||||
|
source IDs, and source payload context.
|
||||||
|
- Workspace-owned objects MUST NOT directly embed or persist environment-owned records.
|
||||||
|
- Environment-owned objects MUST always be bound to an established workspace +
|
||||||
|
managed environment scope at authorization time.
|
||||||
|
|
||||||
Database convention:
|
Database convention:
|
||||||
|
|
||||||
- Tenant-owned tables MUST include workspace_id and tenant_id as NOT NULL.
|
- Environment-owned operational tables MUST include `workspace_id` and
|
||||||
- Workspace-owned tables MUST include workspace_id and MUST NOT include tenant_id.
|
`managed_environment_id` as NOT NULL.
|
||||||
- Exception: OperationRun MAY have tenant_id nullable to support canonical workspace-context monitoring views; however, revealing any tenant-bound runs still MUST enforce entitlement checks to the referenced tenant scope.
|
- Provider-sourced environment records SHOULD include `provider_connection_id`
|
||||||
- Exception: AlertDelivery MAY have tenant_id nullable for workspace-scoped, non-tenant-operational artifacts (e.g., `event_type=alerts.test`). Tenant-bound delivery records still MUST enforce tenant entitlement checks, and tenantless delivery rows MUST NOT contain tenant-specific data.
|
when provenance, permission context, capture source, provider isolation, or
|
||||||
- Exception: `managed_tenant_onboarding_sessions` MAY keep `tenant_id` nullable as a workspace-scoped workflow-coordination record. It begins before tenant identification, may later reference a tenant for authorization continuity and resume semantics, and MUST always enforce workspace entitlement plus tenant entitlement once a tenant reference is attached. This exception is specific to onboarding draft workflow state and does not create a general precedent for workspace-owned domain records.
|
external execution identity depends on a concrete provider connection.
|
||||||
|
- Any `provider_connection_id` used for an environment-owned record MUST belong
|
||||||
|
to the same `workspace_id` and `managed_environment_id`.
|
||||||
|
- Workspace-owned tables MUST include `workspace_id` and MUST NOT include
|
||||||
|
`managed_environment_id` unless the record is explicitly environment-bound.
|
||||||
|
- Provider-native tenant, directory, subscription, account, organization, or
|
||||||
|
similar external scope identifiers MUST be stored as provider/source metadata
|
||||||
|
such as `provider_tenant_id`, `external_tenant_id`, `provider_directory_id`,
|
||||||
|
`subscription_id`, `source_tenant_id`, permission context, or raw evidence
|
||||||
|
metadata. They MUST NOT become platform-core ownership keys unless a
|
||||||
|
provider-owned seam explicitly and narrowly requires that identifier.
|
||||||
|
- Tenant-plane and tenant-scoped wording in this Constitution refers to the
|
||||||
|
governed environment authorization plane, not to a mandatory `tenant_id`
|
||||||
|
database column.
|
||||||
|
- New provider-agnostic platform-core persistence MUST NOT introduce `tenant_id`
|
||||||
|
as a compatibility alias, dual-write target, fallback reader, or parallel
|
||||||
|
ownership truth.
|
||||||
|
- Exception: `OperationRun` MAY have `managed_environment_id` nullable to support
|
||||||
|
canonical workspace-context monitoring views; however, revealing any
|
||||||
|
environment-bound runs still MUST enforce entitlement checks to the referenced
|
||||||
|
managed environment scope.
|
||||||
|
- Exception: `AlertDelivery` MAY have `managed_environment_id` nullable for
|
||||||
|
workspace-scoped, non-environment-operational artifacts. Environment-bound
|
||||||
|
delivery records still MUST enforce environment entitlement checks, and
|
||||||
|
environmentless delivery rows MUST NOT contain environment-specific data.
|
||||||
|
- Exception: `managed_environment_onboarding_sessions` or the current
|
||||||
|
repo-equivalent onboarding workflow record MAY keep `managed_environment_id`
|
||||||
|
nullable as a workspace-scoped workflow-coordination record. It begins before
|
||||||
|
environment identification, may later reference a managed environment for
|
||||||
|
authorization continuity and resume semantics, and MUST always enforce
|
||||||
|
workspace entitlement plus environment entitlement once an environment
|
||||||
|
reference is attached.
|
||||||
|
- Exception: the current onboarding workflow table may retain its existing name
|
||||||
|
until a separate runtime/schema cleanup spec renames it, but its ownership
|
||||||
|
semantics are managed-environment semantics, not a platform-core `tenant_id`
|
||||||
|
requirement.
|
||||||
|
|
||||||
### RBAC & UI Enforcement Standards (RBAC-UX)
|
### RBAC & UI Enforcement Standards (RBAC-UX)
|
||||||
|
|
||||||
@ -1889,4 +1934,4 @@ ### Versioning Policy (SemVer)
|
|||||||
- **MINOR**: new principle/section or materially expanded guidance.
|
- **MINOR**: new principle/section or materially expanded guidance.
|
||||||
- **MAJOR**: removing/redefining principles in a backward-incompatible way.
|
- **MAJOR**: removing/redefining principles in a backward-incompatible way.
|
||||||
|
|
||||||
**Version**: 2.15.0 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-06-21
|
**Version**: 2.16.0 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-06-25
|
||||||
|
|||||||
57
apps/platform/app/Models/TenantConfigurationResourceType.php
Normal file
57
apps/platform/app/Models/TenantConfigurationResourceType.php
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Support\TenantConfiguration\ClaimState;
|
||||||
|
use App\Support\TenantConfiguration\CoverageLevel;
|
||||||
|
use App\Support\TenantConfiguration\EvidenceState;
|
||||||
|
use App\Support\TenantConfiguration\IdentityState;
|
||||||
|
use App\Support\TenantConfiguration\ResourceClass;
|
||||||
|
use App\Support\TenantConfiguration\RestoreTier;
|
||||||
|
use App\Support\TenantConfiguration\SourceClass;
|
||||||
|
use App\Support\TenantConfiguration\SupportState;
|
||||||
|
use App\Support\TenantConfiguration\Workload;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class TenantConfigurationResourceType extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'source_class' => SourceClass::class,
|
||||||
|
'workload' => Workload::class,
|
||||||
|
'resource_class' => ResourceClass::class,
|
||||||
|
'support_state' => SupportState::class,
|
||||||
|
'default_coverage_level' => CoverageLevel::class,
|
||||||
|
'default_evidence_state' => EvidenceState::class,
|
||||||
|
'default_identity_state' => IdentityState::class,
|
||||||
|
'default_claim_state' => ClaimState::class,
|
||||||
|
'restore_tier' => RestoreTier::class,
|
||||||
|
'allows_beta_claims' => 'boolean',
|
||||||
|
'allows_graph_fallback_claims' => 'boolean',
|
||||||
|
'allows_certified_claims' => 'boolean',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'metadata' => 'array',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Builder<self> $query
|
||||||
|
* @return Builder<self>
|
||||||
|
*/
|
||||||
|
public function scopeActive(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->where('is_active', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Support\TenantConfiguration\CoverageLevel;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class TenantConfigurationSupportedScope extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'minimum_coverage_level' => CoverageLevel::class,
|
||||||
|
'included_resource_types' => 'array',
|
||||||
|
'allow_beta' => 'boolean',
|
||||||
|
'allow_graph_fallback' => 'boolean',
|
||||||
|
'customer_claims_allowed' => 'boolean',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'metadata' => 'array',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Builder<self> $query
|
||||||
|
* @return Builder<self>
|
||||||
|
*/
|
||||||
|
public function scopeActive(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->where('is_active', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\TenantConfiguration;
|
||||||
|
|
||||||
|
use App\Support\TenantConfiguration\ClaimState;
|
||||||
|
use App\Support\TenantConfiguration\CoverageLevel;
|
||||||
|
use App\Support\TenantConfiguration\RestoreTier;
|
||||||
|
use App\Support\TenantConfiguration\SourceClass;
|
||||||
|
|
||||||
|
final class ClaimGuard
|
||||||
|
{
|
||||||
|
public function evaluate(
|
||||||
|
?string $scopeKey,
|
||||||
|
CoverageLevel|string $requestedLevel,
|
||||||
|
CoverageLevel|string $actualLevel,
|
||||||
|
bool $scopeComplete,
|
||||||
|
bool $customerFacing = false,
|
||||||
|
bool $customerClaimsAllowed = true,
|
||||||
|
bool $unscoped = false,
|
||||||
|
?int $percentage = null,
|
||||||
|
SourceClass|string|null $sourceClass = null,
|
||||||
|
RestoreTier|string|null $restoreTier = null,
|
||||||
|
bool $restoreClaim = false,
|
||||||
|
bool $allowsBetaClaims = false,
|
||||||
|
bool $allowsCertifiedClaims = false,
|
||||||
|
): ClaimState {
|
||||||
|
$requested = $this->coverageLevel($requestedLevel);
|
||||||
|
$actual = $this->coverageLevel($actualLevel);
|
||||||
|
$source = $this->sourceClass($sourceClass);
|
||||||
|
$restore = $this->restoreTier($restoreTier);
|
||||||
|
|
||||||
|
if (($scopeKey === null || $unscoped) && $percentage === 100) {
|
||||||
|
return ClaimState::ClaimBlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($source?->isBetaExperimental() === true) {
|
||||||
|
if (! $allowsBetaClaims) {
|
||||||
|
return ClaimState::ClaimBlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($requested === CoverageLevel::Certified && ! $allowsCertifiedClaims) {
|
||||||
|
return ClaimState::ClaimBlocked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($restoreClaim || $requested->meets(CoverageLevel::Restorable)) && $restore !== RestoreTier::Restorable) {
|
||||||
|
return ClaimState::ClaimBlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($customerFacing && (! $scopeComplete || ! $customerClaimsAllowed)) {
|
||||||
|
return ClaimState::ClaimBlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scopeKey === null || ! $actual->meets($requested)) {
|
||||||
|
return ClaimState::ClaimLimited;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ClaimState::ClaimAllowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function coverageLevel(CoverageLevel|string $level): CoverageLevel
|
||||||
|
{
|
||||||
|
return $level instanceof CoverageLevel ? $level : CoverageLevel::from($level);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sourceClass(SourceClass|string|null $sourceClass): ?SourceClass
|
||||||
|
{
|
||||||
|
if ($sourceClass === null || $sourceClass instanceof SourceClass) {
|
||||||
|
return $sourceClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SourceClass::from($sourceClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function restoreTier(RestoreTier|string|null $restoreTier): ?RestoreTier
|
||||||
|
{
|
||||||
|
if ($restoreTier === null || $restoreTier instanceof RestoreTier) {
|
||||||
|
return $restoreTier;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RestoreTier::from($restoreTier);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,200 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\TenantConfiguration;
|
||||||
|
|
||||||
|
use App\Models\TenantConfigurationResourceType;
|
||||||
|
use App\Support\TenantConfiguration\ClaimState;
|
||||||
|
use App\Support\TenantConfiguration\CoverageLevel;
|
||||||
|
use App\Support\TenantConfiguration\EvidenceState;
|
||||||
|
use App\Support\TenantConfiguration\IdentityState;
|
||||||
|
use App\Support\TenantConfiguration\ResourceClass;
|
||||||
|
use App\Support\TenantConfiguration\RestoreTier;
|
||||||
|
use App\Support\TenantConfiguration\SourceClass;
|
||||||
|
use App\Support\TenantConfiguration\SupportState;
|
||||||
|
use App\Support\TenantConfiguration\Workload;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
final class ResourceTypeRegistry
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return list<array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public static function defaultDefinitions(): array
|
||||||
|
{
|
||||||
|
$tcmDefaults = [
|
||||||
|
'source_class' => SourceClass::Tcm->value,
|
||||||
|
'workload' => Workload::Intune->value,
|
||||||
|
'resource_class' => ResourceClass::Configuration->value,
|
||||||
|
'support_state' => SupportState::Supported->value,
|
||||||
|
'default_coverage_level' => CoverageLevel::ContentBacked->value,
|
||||||
|
'default_evidence_state' => EvidenceState::ContentBacked->value,
|
||||||
|
'default_identity_state' => IdentityState::Stable->value,
|
||||||
|
'default_claim_state' => ClaimState::ClaimAllowed->value,
|
||||||
|
'restore_tier' => RestoreTier::PreviewOnly->value,
|
||||||
|
'allows_beta_claims' => false,
|
||||||
|
'allows_graph_fallback_claims' => false,
|
||||||
|
'allows_certified_claims' => false,
|
||||||
|
'is_active' => true,
|
||||||
|
'metadata' => ['kernel' => 'coverage_v2', 'provider_owned_source' => false],
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
...$tcmDefaults,
|
||||||
|
'canonical_type' => 'deviceAndAppManagementAssignmentFilter',
|
||||||
|
'display_name' => 'Device and app management assignment filter',
|
||||||
|
'description' => 'TCM-backed Intune assignment filter configuration.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
...$tcmDefaults,
|
||||||
|
'canonical_type' => 'deviceEnrollmentLimitRestriction',
|
||||||
|
'display_name' => 'Device enrollment limit restriction',
|
||||||
|
'description' => 'TCM-backed device enrollment limit restriction.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
...$tcmDefaults,
|
||||||
|
'canonical_type' => 'deviceEnrollmentPlatformRestriction',
|
||||||
|
'display_name' => 'Device enrollment platform restriction',
|
||||||
|
'description' => 'TCM-backed device enrollment platform restriction.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
...$tcmDefaults,
|
||||||
|
'canonical_type' => 'deviceEnrollmentStatusPageWindows10',
|
||||||
|
'display_name' => 'Windows enrollment status page',
|
||||||
|
'description' => 'TCM-backed Windows enrollment status page configuration.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
...$tcmDefaults,
|
||||||
|
'canonical_type' => 'appProtectionPolicyAndroid',
|
||||||
|
'display_name' => 'Android app protection policy',
|
||||||
|
'description' => 'TCM-backed Android app protection policy configuration.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
...$tcmDefaults,
|
||||||
|
'canonical_type' => 'appProtectionPolicyiOS',
|
||||||
|
'display_name' => 'iOS app protection policy',
|
||||||
|
'description' => 'TCM-backed iOS app protection policy configuration.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'canonical_type' => 'notificationMessageTemplate',
|
||||||
|
'display_name' => 'Notification message template',
|
||||||
|
'description' => 'Graph v1 fallback Intune notification message template.',
|
||||||
|
'source_class' => SourceClass::GraphV1Fallback->value,
|
||||||
|
'workload' => Workload::Intune->value,
|
||||||
|
'resource_class' => ResourceClass::Configuration->value,
|
||||||
|
'support_state' => SupportState::FallbackSupported->value,
|
||||||
|
'default_coverage_level' => CoverageLevel::Detected->value,
|
||||||
|
'default_evidence_state' => EvidenceState::Captured->value,
|
||||||
|
'default_identity_state' => IdentityState::Stable->value,
|
||||||
|
'default_claim_state' => ClaimState::ClaimLimited->value,
|
||||||
|
'restore_tier' => RestoreTier::NotRestorable->value,
|
||||||
|
'allows_beta_claims' => false,
|
||||||
|
'allows_graph_fallback_claims' => true,
|
||||||
|
'allows_certified_claims' => false,
|
||||||
|
'is_active' => true,
|
||||||
|
'metadata' => ['kernel' => 'coverage_v2', 'provider_owned_source' => true],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'canonical_type' => 'roleScopeTag',
|
||||||
|
'display_name' => 'Role scope tag',
|
||||||
|
'description' => 'Graph beta experimental Intune role scope tag.',
|
||||||
|
'source_class' => SourceClass::GraphBetaExperimental->value,
|
||||||
|
'workload' => Workload::Intune->value,
|
||||||
|
'resource_class' => ResourceClass::Configuration->value,
|
||||||
|
'support_state' => SupportState::Experimental->value,
|
||||||
|
'default_coverage_level' => CoverageLevel::Detected->value,
|
||||||
|
'default_evidence_state' => EvidenceState::SchemaUnknown->value,
|
||||||
|
'default_identity_state' => IdentityState::Derived->value,
|
||||||
|
'default_claim_state' => ClaimState::InternalOnly->value,
|
||||||
|
'restore_tier' => RestoreTier::NotRestorable->value,
|
||||||
|
'allows_beta_claims' => false,
|
||||||
|
'allows_graph_fallback_claims' => false,
|
||||||
|
'allows_certified_claims' => false,
|
||||||
|
'is_active' => true,
|
||||||
|
'metadata' => ['kernel' => 'coverage_v2', 'provider_owned_source' => true],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public static function defaultCanonicalTypes(): array
|
||||||
|
{
|
||||||
|
return array_values(array_map(
|
||||||
|
static fn (array $definition): string => (string) $definition['canonical_type'],
|
||||||
|
self::defaultDefinitions(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function syncDefaults(): void
|
||||||
|
{
|
||||||
|
DB::table('tenant_configuration_resource_types')->upsert(
|
||||||
|
$this->rowsForUpsert(self::defaultDefinitions()),
|
||||||
|
['canonical_type', 'source_class'],
|
||||||
|
[
|
||||||
|
'display_name',
|
||||||
|
'description',
|
||||||
|
'workload',
|
||||||
|
'resource_class',
|
||||||
|
'support_state',
|
||||||
|
'default_coverage_level',
|
||||||
|
'default_evidence_state',
|
||||||
|
'default_identity_state',
|
||||||
|
'default_claim_state',
|
||||||
|
'restore_tier',
|
||||||
|
'allows_beta_claims',
|
||||||
|
'allows_graph_fallback_claims',
|
||||||
|
'allows_certified_claims',
|
||||||
|
'is_active',
|
||||||
|
'metadata',
|
||||||
|
'updated_at',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, TenantConfigurationResourceType>
|
||||||
|
*/
|
||||||
|
public function active(): Collection
|
||||||
|
{
|
||||||
|
return TenantConfigurationResourceType::query()
|
||||||
|
->active()
|
||||||
|
->orderBy('canonical_type')
|
||||||
|
->orderBy('source_class')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findActive(string $canonicalType, ?SourceClass $sourceClass = null): ?TenantConfigurationResourceType
|
||||||
|
{
|
||||||
|
return TenantConfigurationResourceType::query()
|
||||||
|
->active()
|
||||||
|
->where('canonical_type', $canonicalType)
|
||||||
|
->when(
|
||||||
|
$sourceClass instanceof SourceClass,
|
||||||
|
fn ($query) => $query->where('source_class', $sourceClass->value),
|
||||||
|
)
|
||||||
|
->orderBy('source_class')
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array<string, mixed>> $definitions
|
||||||
|
* @return list<array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private function rowsForUpsert(array $definitions): array
|
||||||
|
{
|
||||||
|
$now = now();
|
||||||
|
|
||||||
|
return array_map(static function (array $definition) use ($now): array {
|
||||||
|
$definition['metadata'] = json_encode($definition['metadata'] ?? null, JSON_THROW_ON_ERROR);
|
||||||
|
$definition['created_at'] = $now;
|
||||||
|
$definition['updated_at'] = $now;
|
||||||
|
|
||||||
|
return $definition;
|
||||||
|
}, $definitions);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,255 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\TenantConfiguration;
|
||||||
|
|
||||||
|
use App\Models\TenantConfigurationResourceType;
|
||||||
|
use App\Models\TenantConfigurationSupportedScope;
|
||||||
|
use App\Support\TenantConfiguration\CoverageLevel;
|
||||||
|
use App\Support\TenantConfiguration\SourceClass;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use UnexpectedValueException;
|
||||||
|
|
||||||
|
final class SupportedScopeResolver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return list<array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public static function defaultDefinitions(): array
|
||||||
|
{
|
||||||
|
$tcmCoreTypes = [
|
||||||
|
'deviceAndAppManagementAssignmentFilter',
|
||||||
|
'deviceEnrollmentLimitRestriction',
|
||||||
|
'deviceEnrollmentPlatformRestriction',
|
||||||
|
'deviceEnrollmentStatusPageWindows10',
|
||||||
|
'appProtectionPolicyAndroid',
|
||||||
|
'appProtectionPolicyiOS',
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'scope_key' => 'intune_tcm_core',
|
||||||
|
'display_name' => 'Intune TCM core',
|
||||||
|
'description' => 'Initial TCM-backed Intune configuration denominator.',
|
||||||
|
'minimum_coverage_level' => CoverageLevel::ContentBacked->value,
|
||||||
|
'included_resource_types' => $tcmCoreTypes,
|
||||||
|
'allow_beta' => false,
|
||||||
|
'allow_graph_fallback' => false,
|
||||||
|
'customer_claims_allowed' => true,
|
||||||
|
'is_active' => true,
|
||||||
|
'metadata' => ['kernel' => 'coverage_v2', 'claim_surface' => 'future_activation'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'scope_key' => 'intune_tcm_core_with_graph_fallback',
|
||||||
|
'display_name' => 'Intune TCM core with Graph fallback',
|
||||||
|
'description' => 'Initial TCM-backed denominator with explicitly allowed Graph v1 fallback resource types.',
|
||||||
|
'minimum_coverage_level' => CoverageLevel::Detected->value,
|
||||||
|
'included_resource_types' => [
|
||||||
|
...$tcmCoreTypes,
|
||||||
|
'notificationMessageTemplate',
|
||||||
|
],
|
||||||
|
'allow_beta' => false,
|
||||||
|
'allow_graph_fallback' => true,
|
||||||
|
'customer_claims_allowed' => true,
|
||||||
|
'is_active' => true,
|
||||||
|
'metadata' => ['kernel' => 'coverage_v2', 'claim_surface' => 'future_activation'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function syncDefaults(): void
|
||||||
|
{
|
||||||
|
DB::table('tenant_configuration_supported_scopes')->upsert(
|
||||||
|
$this->rowsForUpsert(self::defaultDefinitions()),
|
||||||
|
['scope_key'],
|
||||||
|
[
|
||||||
|
'display_name',
|
||||||
|
'description',
|
||||||
|
'minimum_coverage_level',
|
||||||
|
'included_resource_types',
|
||||||
|
'allow_beta',
|
||||||
|
'allow_graph_fallback',
|
||||||
|
'customer_claims_allowed',
|
||||||
|
'is_active',
|
||||||
|
'metadata',
|
||||||
|
'updated_at',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, TenantConfigurationSupportedScope>
|
||||||
|
*/
|
||||||
|
public function activeScopes(): Collection
|
||||||
|
{
|
||||||
|
return TenantConfigurationSupportedScope::query()
|
||||||
|
->active()
|
||||||
|
->orderBy('scope_key')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findActive(string $scopeKey): ?TenantConfigurationSupportedScope
|
||||||
|
{
|
||||||
|
return TenantConfigurationSupportedScope::query()
|
||||||
|
->active()
|
||||||
|
->where('scope_key', $scopeKey)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed>|TenantConfigurationSupportedScope $scope
|
||||||
|
* @param iterable<int, array<string, mixed>|TenantConfigurationResourceType>|null $resourceTypes
|
||||||
|
* @return array{scope_key: string, minimum_coverage_level: CoverageLevel, included_resource_types: list<string>, excluded_resource_types: list<string>, allow_beta: bool, allow_graph_fallback: bool, customer_claims_allowed: bool}
|
||||||
|
*/
|
||||||
|
public function resolveDefinition(array|TenantConfigurationSupportedScope $scope, ?iterable $resourceTypes = null): array
|
||||||
|
{
|
||||||
|
$scopeDefinition = $this->normalizeScope($scope);
|
||||||
|
$resourceTypes ??= ResourceTypeRegistry::defaultDefinitions();
|
||||||
|
$explicitDenominator = array_values(array_map('strval', $scopeDefinition['included_resource_types']));
|
||||||
|
$resourceTypesByCanonicalType = $this->indexResourceTypesByCanonicalType($resourceTypes);
|
||||||
|
$unknownResourceTypes = array_values(array_diff($explicitDenominator, array_keys($resourceTypesByCanonicalType)));
|
||||||
|
$included = [];
|
||||||
|
$excluded = [];
|
||||||
|
|
||||||
|
if ($unknownResourceTypes !== []) {
|
||||||
|
throw new UnexpectedValueException(sprintf(
|
||||||
|
'Supported scope [%s] references unknown resource type(s): %s.',
|
||||||
|
$scopeDefinition['scope_key'],
|
||||||
|
implode(', ', $unknownResourceTypes),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($explicitDenominator as $canonicalType) {
|
||||||
|
$definition = $resourceTypesByCanonicalType[$canonicalType];
|
||||||
|
$sourceClass = SourceClass::from($definition['source_class']);
|
||||||
|
|
||||||
|
if ($sourceClass->isBetaExperimental() && ! $scopeDefinition['allow_beta']) {
|
||||||
|
$excluded[] = $definition['canonical_type'];
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sourceClass->isGraphFallback() && ! $scopeDefinition['allow_graph_fallback']) {
|
||||||
|
$excluded[] = $definition['canonical_type'];
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$included[] = $definition['canonical_type'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'scope_key' => $scopeDefinition['scope_key'],
|
||||||
|
'minimum_coverage_level' => CoverageLevel::from($scopeDefinition['minimum_coverage_level']),
|
||||||
|
'included_resource_types' => array_values(array_unique($included)),
|
||||||
|
'excluded_resource_types' => array_values(array_unique($excluded)),
|
||||||
|
'allow_beta' => $scopeDefinition['allow_beta'],
|
||||||
|
'allow_graph_fallback' => $scopeDefinition['allow_graph_fallback'],
|
||||||
|
'customer_claims_allowed' => $scopeDefinition['customer_claims_allowed'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $observedCanonicalTypes
|
||||||
|
* @param array<string, mixed>|TenantConfigurationSupportedScope $scope
|
||||||
|
* @param iterable<int, array<string, mixed>|TenantConfigurationResourceType>|null $resourceTypes
|
||||||
|
*/
|
||||||
|
public function isScopeComplete(array $observedCanonicalTypes, array|TenantConfigurationSupportedScope $scope, ?iterable $resourceTypes = null): bool
|
||||||
|
{
|
||||||
|
$resolved = $this->resolveDefinition($scope, $resourceTypes);
|
||||||
|
|
||||||
|
return array_diff($resolved['included_resource_types'], $observedCanonicalTypes) === [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function meetsMinimum(CoverageLevel|string $actualLevel, array|TenantConfigurationSupportedScope $scope): bool
|
||||||
|
{
|
||||||
|
$actual = $actualLevel instanceof CoverageLevel ? $actualLevel : CoverageLevel::from($actualLevel);
|
||||||
|
$scopeDefinition = $this->normalizeScope($scope);
|
||||||
|
|
||||||
|
return $actual->meets(CoverageLevel::from($scopeDefinition['minimum_coverage_level']));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array<string, mixed>> $definitions
|
||||||
|
* @return list<array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private function rowsForUpsert(array $definitions): array
|
||||||
|
{
|
||||||
|
$now = now();
|
||||||
|
|
||||||
|
return array_map(static function (array $definition) use ($now): array {
|
||||||
|
$definition['included_resource_types'] = json_encode($definition['included_resource_types'], JSON_THROW_ON_ERROR);
|
||||||
|
$definition['metadata'] = json_encode($definition['metadata'] ?? null, JSON_THROW_ON_ERROR);
|
||||||
|
$definition['created_at'] = $now;
|
||||||
|
$definition['updated_at'] = $now;
|
||||||
|
|
||||||
|
return $definition;
|
||||||
|
}, $definitions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{scope_key: string, minimum_coverage_level: string, included_resource_types: list<string>, allow_beta: bool, allow_graph_fallback: bool, customer_claims_allowed: bool}
|
||||||
|
*/
|
||||||
|
private function normalizeScope(array|TenantConfigurationSupportedScope $scope): array
|
||||||
|
{
|
||||||
|
if ($scope instanceof TenantConfigurationSupportedScope) {
|
||||||
|
return [
|
||||||
|
'scope_key' => (string) $scope->scope_key,
|
||||||
|
'minimum_coverage_level' => $scope->minimum_coverage_level instanceof CoverageLevel
|
||||||
|
? $scope->minimum_coverage_level->value
|
||||||
|
: (string) $scope->minimum_coverage_level,
|
||||||
|
'included_resource_types' => array_values($scope->included_resource_types ?? []),
|
||||||
|
'allow_beta' => (bool) $scope->allow_beta,
|
||||||
|
'allow_graph_fallback' => (bool) $scope->allow_graph_fallback,
|
||||||
|
'customer_claims_allowed' => (bool) $scope->customer_claims_allowed,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'scope_key' => (string) $scope['scope_key'],
|
||||||
|
'minimum_coverage_level' => (string) $scope['minimum_coverage_level'],
|
||||||
|
'included_resource_types' => array_values($scope['included_resource_types'] ?? []),
|
||||||
|
'allow_beta' => (bool) ($scope['allow_beta'] ?? false),
|
||||||
|
'allow_graph_fallback' => (bool) ($scope['allow_graph_fallback'] ?? false),
|
||||||
|
'customer_claims_allowed' => (bool) ($scope['customer_claims_allowed'] ?? false),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param iterable<int, array<string, mixed>|TenantConfigurationResourceType> $resourceTypes
|
||||||
|
* @return array<string, array{canonical_type: string, source_class: string}>
|
||||||
|
*/
|
||||||
|
private function indexResourceTypesByCanonicalType(iterable $resourceTypes): array
|
||||||
|
{
|
||||||
|
$indexed = [];
|
||||||
|
|
||||||
|
foreach ($resourceTypes as $resourceType) {
|
||||||
|
$definition = $this->normalizeResourceType($resourceType);
|
||||||
|
$indexed[$definition['canonical_type']] = $definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $indexed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{canonical_type: string, source_class: string}
|
||||||
|
*/
|
||||||
|
private function normalizeResourceType(array|TenantConfigurationResourceType $resourceType): array
|
||||||
|
{
|
||||||
|
if ($resourceType instanceof TenantConfigurationResourceType) {
|
||||||
|
return [
|
||||||
|
'canonical_type' => (string) $resourceType->canonical_type,
|
||||||
|
'source_class' => $resourceType->source_class instanceof SourceClass
|
||||||
|
? $resourceType->source_class->value
|
||||||
|
: (string) $resourceType->source_class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'canonical_type' => (string) $resourceType['canonical_type'],
|
||||||
|
'source_class' => (string) $resourceType['source_class'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
21
apps/platform/app/Support/TenantConfiguration/ClaimState.php
Normal file
21
apps/platform/app/Support/TenantConfiguration/ClaimState.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Support\TenantConfiguration;
|
||||||
|
|
||||||
|
enum ClaimState: string
|
||||||
|
{
|
||||||
|
case ClaimAllowed = 'claim_allowed';
|
||||||
|
case ClaimLimited = 'claim_limited';
|
||||||
|
case ClaimBlocked = 'claim_blocked';
|
||||||
|
case InternalOnly = 'internal_only';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public static function values(): array
|
||||||
|
{
|
||||||
|
return array_map(static fn (self $case): string => $case->value, self::cases());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Support\TenantConfiguration;
|
||||||
|
|
||||||
|
enum CoverageLevel: string
|
||||||
|
{
|
||||||
|
case Detected = 'detected';
|
||||||
|
case ContentBacked = 'content_backed';
|
||||||
|
case Comparable = 'comparable';
|
||||||
|
case Renderable = 'renderable';
|
||||||
|
case Restorable = 'restorable';
|
||||||
|
case Certified = 'certified';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public static function values(): array
|
||||||
|
{
|
||||||
|
return array_map(static fn (self $case): string => $case->value, self::cases());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rank(): int
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::Detected => 10,
|
||||||
|
self::ContentBacked => 20,
|
||||||
|
self::Comparable => 30,
|
||||||
|
self::Renderable => 40,
|
||||||
|
self::Restorable => 50,
|
||||||
|
self::Certified => 60,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function meets(self $minimum): bool
|
||||||
|
{
|
||||||
|
return $this->rank() >= $minimum->rank();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Support\TenantConfiguration;
|
||||||
|
|
||||||
|
enum EvidenceState: string
|
||||||
|
{
|
||||||
|
case NotCaptured = 'not_captured';
|
||||||
|
case Captured = 'captured';
|
||||||
|
case ContentBacked = 'content_backed';
|
||||||
|
case PermissionBlocked = 'permission_blocked';
|
||||||
|
case SourceUnavailable = 'source_unavailable';
|
||||||
|
case SchemaUnknown = 'schema_unknown';
|
||||||
|
case CaptureFailed = 'capture_failed';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public static function values(): array
|
||||||
|
{
|
||||||
|
return array_map(static fn (self $case): string => $case->value, self::cases());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Support\TenantConfiguration;
|
||||||
|
|
||||||
|
enum IdentityState: string
|
||||||
|
{
|
||||||
|
case Stable = 'stable';
|
||||||
|
case Derived = 'derived';
|
||||||
|
case IdentityConflict = 'identity_conflict';
|
||||||
|
case MissingExternalId = 'missing_external_id';
|
||||||
|
case UnsupportedIdentity = 'unsupported_identity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public static function values(): array
|
||||||
|
{
|
||||||
|
return array_map(static fn (self $case): string => $case->value, self::cases());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Support\TenantConfiguration;
|
||||||
|
|
||||||
|
enum ResourceClass: string
|
||||||
|
{
|
||||||
|
case Configuration = 'configuration';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public static function values(): array
|
||||||
|
{
|
||||||
|
return array_map(static fn (self $case): string => $case->value, self::cases());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Support\TenantConfiguration;
|
||||||
|
|
||||||
|
enum RestoreTier: string
|
||||||
|
{
|
||||||
|
case NotRestorable = 'not_restorable';
|
||||||
|
case PreviewOnly = 'preview_only';
|
||||||
|
case Restorable = 'restorable';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public static function values(): array
|
||||||
|
{
|
||||||
|
return array_map(static fn (self $case): string => $case->value, self::cases());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Support\TenantConfiguration;
|
||||||
|
|
||||||
|
enum SourceClass: string
|
||||||
|
{
|
||||||
|
case Tcm = 'tcm';
|
||||||
|
case GraphV1Fallback = 'graph_v1_fallback';
|
||||||
|
case GraphBetaExperimental = 'graph_beta_experimental';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public static function values(): array
|
||||||
|
{
|
||||||
|
return array_map(static fn (self $case): string => $case->value, self::cases());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isBetaExperimental(): bool
|
||||||
|
{
|
||||||
|
return $this === self::GraphBetaExperimental;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isGraphFallback(): bool
|
||||||
|
{
|
||||||
|
return $this === self::GraphV1Fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Support\TenantConfiguration;
|
||||||
|
|
||||||
|
enum SupportState: string
|
||||||
|
{
|
||||||
|
case Supported = 'supported';
|
||||||
|
case FallbackSupported = 'fallback_supported';
|
||||||
|
case Experimental = 'experimental';
|
||||||
|
case Unsupported = 'unsupported';
|
||||||
|
case OutOfScope = 'out_of_scope';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public static function values(): array
|
||||||
|
{
|
||||||
|
return array_map(static fn (self $case): string => $case->value, self::cases());
|
||||||
|
}
|
||||||
|
}
|
||||||
18
apps/platform/app/Support/TenantConfiguration/Workload.php
Normal file
18
apps/platform/app/Support/TenantConfiguration/Workload.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Support\TenantConfiguration;
|
||||||
|
|
||||||
|
enum Workload: string
|
||||||
|
{
|
||||||
|
case Intune = 'intune';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public static function values(): array
|
||||||
|
{
|
||||||
|
return array_map(static fn (self $case): string => $case->value, self::cases());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\TenantConfigurationResourceType;
|
||||||
|
use App\Support\TenantConfiguration\ClaimState;
|
||||||
|
use App\Support\TenantConfiguration\CoverageLevel;
|
||||||
|
use App\Support\TenantConfiguration\EvidenceState;
|
||||||
|
use App\Support\TenantConfiguration\IdentityState;
|
||||||
|
use App\Support\TenantConfiguration\ResourceClass;
|
||||||
|
use App\Support\TenantConfiguration\RestoreTier;
|
||||||
|
use App\Support\TenantConfiguration\SourceClass;
|
||||||
|
use App\Support\TenantConfiguration\SupportState;
|
||||||
|
use App\Support\TenantConfiguration\Workload;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends Factory<TenantConfigurationResourceType>
|
||||||
|
*/
|
||||||
|
class TenantConfigurationResourceTypeFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = TenantConfigurationResourceType::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'canonical_type' => fake()->unique()->slug(),
|
||||||
|
'display_name' => fake()->words(3, true),
|
||||||
|
'description' => fake()->sentence(),
|
||||||
|
'source_class' => SourceClass::Tcm->value,
|
||||||
|
'workload' => Workload::Intune->value,
|
||||||
|
'resource_class' => ResourceClass::Configuration->value,
|
||||||
|
'support_state' => SupportState::Supported->value,
|
||||||
|
'default_coverage_level' => CoverageLevel::ContentBacked->value,
|
||||||
|
'default_evidence_state' => EvidenceState::ContentBacked->value,
|
||||||
|
'default_identity_state' => IdentityState::Stable->value,
|
||||||
|
'default_claim_state' => ClaimState::ClaimAllowed->value,
|
||||||
|
'restore_tier' => RestoreTier::PreviewOnly->value,
|
||||||
|
'allows_beta_claims' => false,
|
||||||
|
'allows_graph_fallback_claims' => false,
|
||||||
|
'allows_certified_claims' => false,
|
||||||
|
'is_active' => true,
|
||||||
|
'metadata' => ['factory' => 'tenant_configuration_resource_type'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\TenantConfigurationSupportedScope;
|
||||||
|
use App\Support\TenantConfiguration\CoverageLevel;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends Factory<TenantConfigurationSupportedScope>
|
||||||
|
*/
|
||||||
|
class TenantConfigurationSupportedScopeFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = TenantConfigurationSupportedScope::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'scope_key' => fake()->unique()->slug(),
|
||||||
|
'display_name' => fake()->words(3, true),
|
||||||
|
'description' => fake()->sentence(),
|
||||||
|
'minimum_coverage_level' => CoverageLevel::ContentBacked->value,
|
||||||
|
'included_resource_types' => [],
|
||||||
|
'allow_beta' => false,
|
||||||
|
'allow_graph_fallback' => false,
|
||||||
|
'customer_claims_allowed' => false,
|
||||||
|
'is_active' => true,
|
||||||
|
'metadata' => ['factory' => 'tenant_configuration_supported_scope'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,340 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
private const SOURCE_CLASSES = ['tcm', 'graph_v1_fallback', 'graph_beta_experimental'];
|
||||||
|
|
||||||
|
private const WORKLOADS = ['intune'];
|
||||||
|
|
||||||
|
private const RESOURCE_CLASSES = ['configuration'];
|
||||||
|
|
||||||
|
private const SUPPORT_STATES = ['supported', 'fallback_supported', 'experimental', 'unsupported', 'out_of_scope'];
|
||||||
|
|
||||||
|
private const COVERAGE_LEVELS = ['detected', 'content_backed', 'comparable', 'renderable', 'restorable', 'certified'];
|
||||||
|
|
||||||
|
private const EVIDENCE_STATES = ['not_captured', 'captured', 'content_backed', 'permission_blocked', 'source_unavailable', 'schema_unknown', 'capture_failed'];
|
||||||
|
|
||||||
|
private const IDENTITY_STATES = ['stable', 'derived', 'identity_conflict', 'missing_external_id', 'unsupported_identity'];
|
||||||
|
|
||||||
|
private const CLAIM_STATES = ['claim_allowed', 'claim_limited', 'claim_blocked', 'internal_only'];
|
||||||
|
|
||||||
|
private const RESTORE_TIERS = ['not_restorable', 'preview_only', 'restorable'];
|
||||||
|
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('tenant_configuration_resource_types', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('canonical_type');
|
||||||
|
$table->string('display_name');
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->string('source_class');
|
||||||
|
$table->string('workload');
|
||||||
|
$table->string('resource_class');
|
||||||
|
$table->string('support_state');
|
||||||
|
$table->string('default_coverage_level');
|
||||||
|
$table->string('default_evidence_state');
|
||||||
|
$table->string('default_identity_state');
|
||||||
|
$table->string('default_claim_state');
|
||||||
|
$table->string('restore_tier')->nullable();
|
||||||
|
$table->boolean('allows_beta_claims')->default(false);
|
||||||
|
$table->boolean('allows_graph_fallback_claims')->default(false);
|
||||||
|
$table->boolean('allows_certified_claims')->default(false);
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->jsonb('metadata')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['canonical_type', 'source_class'], 'tenant_config_resource_types_canonical_source_unique');
|
||||||
|
$table->index(['source_class', 'is_active'], 'tenant_config_resource_types_source_active_idx');
|
||||||
|
$table->index(['support_state', 'default_coverage_level'], 'tenant_config_resource_types_support_coverage_idx');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('tenant_configuration_supported_scopes', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('scope_key')->unique();
|
||||||
|
$table->string('display_name');
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->string('minimum_coverage_level');
|
||||||
|
$table->jsonb('included_resource_types');
|
||||||
|
$table->boolean('allow_beta')->default(false);
|
||||||
|
$table->boolean('allow_graph_fallback')->default(false);
|
||||||
|
$table->boolean('customer_claims_allowed')->default(false);
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->jsonb('metadata')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index(['is_active', 'minimum_coverage_level'], 'tenant_config_supported_scopes_active_level_idx');
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->addPostgresConstraints();
|
||||||
|
|
||||||
|
DB::table('tenant_configuration_resource_types')->upsert(
|
||||||
|
$this->rowsForUpsert($this->resourceTypeDefinitions()),
|
||||||
|
['canonical_type', 'source_class'],
|
||||||
|
[
|
||||||
|
'display_name',
|
||||||
|
'description',
|
||||||
|
'workload',
|
||||||
|
'resource_class',
|
||||||
|
'support_state',
|
||||||
|
'default_coverage_level',
|
||||||
|
'default_evidence_state',
|
||||||
|
'default_identity_state',
|
||||||
|
'default_claim_state',
|
||||||
|
'restore_tier',
|
||||||
|
'allows_beta_claims',
|
||||||
|
'allows_graph_fallback_claims',
|
||||||
|
'allows_certified_claims',
|
||||||
|
'is_active',
|
||||||
|
'metadata',
|
||||||
|
'updated_at',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
DB::table('tenant_configuration_supported_scopes')->upsert(
|
||||||
|
$this->rowsForUpsert($this->supportedScopeDefinitions()),
|
||||||
|
['scope_key'],
|
||||||
|
[
|
||||||
|
'display_name',
|
||||||
|
'description',
|
||||||
|
'minimum_coverage_level',
|
||||||
|
'included_resource_types',
|
||||||
|
'allow_beta',
|
||||||
|
'allow_graph_fallback',
|
||||||
|
'customer_claims_allowed',
|
||||||
|
'is_active',
|
||||||
|
'metadata',
|
||||||
|
'updated_at',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('tenant_configuration_supported_scopes');
|
||||||
|
Schema::dropIfExists('tenant_configuration_resource_types');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addPostgresConstraints(): void
|
||||||
|
{
|
||||||
|
if (DB::getDriverName() !== 'pgsql') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::statement($this->checkIn('tenant_configuration_resource_types', 'source_class', self::SOURCE_CLASSES, 'tenant_config_resource_types_source_class_check'));
|
||||||
|
DB::statement($this->checkIn('tenant_configuration_resource_types', 'workload', self::WORKLOADS, 'tenant_config_resource_types_workload_check'));
|
||||||
|
DB::statement($this->checkIn('tenant_configuration_resource_types', 'resource_class', self::RESOURCE_CLASSES, 'tenant_config_resource_types_resource_class_check'));
|
||||||
|
DB::statement($this->checkIn('tenant_configuration_resource_types', 'support_state', self::SUPPORT_STATES, 'tenant_config_resource_types_support_state_check'));
|
||||||
|
DB::statement($this->checkIn('tenant_configuration_resource_types', 'default_coverage_level', self::COVERAGE_LEVELS, 'tenant_config_resource_types_coverage_level_check'));
|
||||||
|
DB::statement($this->checkIn('tenant_configuration_resource_types', 'default_evidence_state', self::EVIDENCE_STATES, 'tenant_config_resource_types_evidence_state_check'));
|
||||||
|
DB::statement($this->checkIn('tenant_configuration_resource_types', 'default_identity_state', self::IDENTITY_STATES, 'tenant_config_resource_types_identity_state_check'));
|
||||||
|
DB::statement($this->checkIn('tenant_configuration_resource_types', 'default_claim_state', self::CLAIM_STATES, 'tenant_config_resource_types_claim_state_check'));
|
||||||
|
DB::statement($this->nullableCheckIn('tenant_configuration_resource_types', 'restore_tier', self::RESTORE_TIERS, 'tenant_config_resource_types_restore_tier_check'));
|
||||||
|
DB::statement("ALTER TABLE tenant_configuration_resource_types ADD CONSTRAINT tenant_config_resource_types_metadata_object_check CHECK (metadata IS NULL OR jsonb_typeof(metadata) = 'object')");
|
||||||
|
|
||||||
|
DB::statement($this->checkIn('tenant_configuration_supported_scopes', 'minimum_coverage_level', self::COVERAGE_LEVELS, 'tenant_config_supported_scopes_minimum_level_check'));
|
||||||
|
DB::statement("ALTER TABLE tenant_configuration_supported_scopes ADD CONSTRAINT tenant_config_supported_scopes_denominator_array_check CHECK (jsonb_typeof(included_resource_types) = 'array')");
|
||||||
|
DB::statement("ALTER TABLE tenant_configuration_supported_scopes ADD CONSTRAINT tenant_config_supported_scopes_metadata_object_check CHECK (metadata IS NULL OR jsonb_typeof(metadata) = 'object')");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private function resourceTypeDefinitions(): array
|
||||||
|
{
|
||||||
|
$tcmDefaults = [
|
||||||
|
'source_class' => 'tcm',
|
||||||
|
'workload' => 'intune',
|
||||||
|
'resource_class' => 'configuration',
|
||||||
|
'support_state' => 'supported',
|
||||||
|
'default_coverage_level' => 'content_backed',
|
||||||
|
'default_evidence_state' => 'content_backed',
|
||||||
|
'default_identity_state' => 'stable',
|
||||||
|
'default_claim_state' => 'claim_allowed',
|
||||||
|
'restore_tier' => 'preview_only',
|
||||||
|
'allows_beta_claims' => false,
|
||||||
|
'allows_graph_fallback_claims' => false,
|
||||||
|
'allows_certified_claims' => false,
|
||||||
|
'is_active' => true,
|
||||||
|
'metadata' => ['kernel' => 'coverage_v2', 'provider_owned_source' => false],
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
...$tcmDefaults,
|
||||||
|
'canonical_type' => 'deviceAndAppManagementAssignmentFilter',
|
||||||
|
'display_name' => 'Device and app management assignment filter',
|
||||||
|
'description' => 'TCM-backed Intune assignment filter configuration.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
...$tcmDefaults,
|
||||||
|
'canonical_type' => 'deviceEnrollmentLimitRestriction',
|
||||||
|
'display_name' => 'Device enrollment limit restriction',
|
||||||
|
'description' => 'TCM-backed device enrollment limit restriction.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
...$tcmDefaults,
|
||||||
|
'canonical_type' => 'deviceEnrollmentPlatformRestriction',
|
||||||
|
'display_name' => 'Device enrollment platform restriction',
|
||||||
|
'description' => 'TCM-backed device enrollment platform restriction.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
...$tcmDefaults,
|
||||||
|
'canonical_type' => 'deviceEnrollmentStatusPageWindows10',
|
||||||
|
'display_name' => 'Windows enrollment status page',
|
||||||
|
'description' => 'TCM-backed Windows enrollment status page configuration.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
...$tcmDefaults,
|
||||||
|
'canonical_type' => 'appProtectionPolicyAndroid',
|
||||||
|
'display_name' => 'Android app protection policy',
|
||||||
|
'description' => 'TCM-backed Android app protection policy configuration.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
...$tcmDefaults,
|
||||||
|
'canonical_type' => 'appProtectionPolicyiOS',
|
||||||
|
'display_name' => 'iOS app protection policy',
|
||||||
|
'description' => 'TCM-backed iOS app protection policy configuration.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'canonical_type' => 'notificationMessageTemplate',
|
||||||
|
'display_name' => 'Notification message template',
|
||||||
|
'description' => 'Graph v1 fallback Intune notification message template.',
|
||||||
|
'source_class' => 'graph_v1_fallback',
|
||||||
|
'workload' => 'intune',
|
||||||
|
'resource_class' => 'configuration',
|
||||||
|
'support_state' => 'fallback_supported',
|
||||||
|
'default_coverage_level' => 'detected',
|
||||||
|
'default_evidence_state' => 'captured',
|
||||||
|
'default_identity_state' => 'stable',
|
||||||
|
'default_claim_state' => 'claim_limited',
|
||||||
|
'restore_tier' => 'not_restorable',
|
||||||
|
'allows_beta_claims' => false,
|
||||||
|
'allows_graph_fallback_claims' => true,
|
||||||
|
'allows_certified_claims' => false,
|
||||||
|
'is_active' => true,
|
||||||
|
'metadata' => ['kernel' => 'coverage_v2', 'provider_owned_source' => true],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'canonical_type' => 'roleScopeTag',
|
||||||
|
'display_name' => 'Role scope tag',
|
||||||
|
'description' => 'Graph beta experimental Intune role scope tag.',
|
||||||
|
'source_class' => 'graph_beta_experimental',
|
||||||
|
'workload' => 'intune',
|
||||||
|
'resource_class' => 'configuration',
|
||||||
|
'support_state' => 'experimental',
|
||||||
|
'default_coverage_level' => 'detected',
|
||||||
|
'default_evidence_state' => 'schema_unknown',
|
||||||
|
'default_identity_state' => 'derived',
|
||||||
|
'default_claim_state' => 'internal_only',
|
||||||
|
'restore_tier' => 'not_restorable',
|
||||||
|
'allows_beta_claims' => false,
|
||||||
|
'allows_graph_fallback_claims' => false,
|
||||||
|
'allows_certified_claims' => false,
|
||||||
|
'is_active' => true,
|
||||||
|
'metadata' => ['kernel' => 'coverage_v2', 'provider_owned_source' => true],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private function supportedScopeDefinitions(): array
|
||||||
|
{
|
||||||
|
$tcmCoreTypes = [
|
||||||
|
'deviceAndAppManagementAssignmentFilter',
|
||||||
|
'deviceEnrollmentLimitRestriction',
|
||||||
|
'deviceEnrollmentPlatformRestriction',
|
||||||
|
'deviceEnrollmentStatusPageWindows10',
|
||||||
|
'appProtectionPolicyAndroid',
|
||||||
|
'appProtectionPolicyiOS',
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'scope_key' => 'intune_tcm_core',
|
||||||
|
'display_name' => 'Intune TCM core',
|
||||||
|
'description' => 'Initial TCM-backed Intune configuration denominator.',
|
||||||
|
'minimum_coverage_level' => 'content_backed',
|
||||||
|
'included_resource_types' => $tcmCoreTypes,
|
||||||
|
'allow_beta' => false,
|
||||||
|
'allow_graph_fallback' => false,
|
||||||
|
'customer_claims_allowed' => true,
|
||||||
|
'is_active' => true,
|
||||||
|
'metadata' => ['kernel' => 'coverage_v2', 'claim_surface' => 'future_activation'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'scope_key' => 'intune_tcm_core_with_graph_fallback',
|
||||||
|
'display_name' => 'Intune TCM core with Graph fallback',
|
||||||
|
'description' => 'Initial TCM-backed denominator with explicitly allowed Graph v1 fallback resource types.',
|
||||||
|
'minimum_coverage_level' => 'detected',
|
||||||
|
'included_resource_types' => [
|
||||||
|
...$tcmCoreTypes,
|
||||||
|
'notificationMessageTemplate',
|
||||||
|
],
|
||||||
|
'allow_beta' => false,
|
||||||
|
'allow_graph_fallback' => true,
|
||||||
|
'customer_claims_allowed' => true,
|
||||||
|
'is_active' => true,
|
||||||
|
'metadata' => ['kernel' => 'coverage_v2', 'claim_surface' => 'future_activation'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array<string, mixed>> $definitions
|
||||||
|
* @return list<array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private function rowsForUpsert(array $definitions): array
|
||||||
|
{
|
||||||
|
$now = now();
|
||||||
|
|
||||||
|
return array_map(static function (array $definition) use ($now): array {
|
||||||
|
if (isset($definition['included_resource_types'])) {
|
||||||
|
$definition['included_resource_types'] = json_encode($definition['included_resource_types'], JSON_THROW_ON_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
$definition['metadata'] = json_encode($definition['metadata'] ?? null, JSON_THROW_ON_ERROR);
|
||||||
|
$definition['created_at'] = $now;
|
||||||
|
$definition['updated_at'] = $now;
|
||||||
|
|
||||||
|
return $definition;
|
||||||
|
}, $definitions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $values
|
||||||
|
*/
|
||||||
|
private function checkIn(string $table, string $column, array $values, string $constraint): string
|
||||||
|
{
|
||||||
|
return sprintf(
|
||||||
|
"ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IN ('%s'))",
|
||||||
|
$table,
|
||||||
|
$constraint,
|
||||||
|
$column,
|
||||||
|
implode("','", $values),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $values
|
||||||
|
*/
|
||||||
|
private function nullableCheckIn(string $table, string $column, array $values, string $constraint): string
|
||||||
|
{
|
||||||
|
return sprintf(
|
||||||
|
"ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IS NULL OR %s IN ('%s'))",
|
||||||
|
$table,
|
||||||
|
$constraint,
|
||||||
|
$column,
|
||||||
|
$column,
|
||||||
|
implode("','", $values),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Services\TenantConfiguration\ClaimGuard;
|
||||||
|
use App\Services\TenantConfiguration\ResourceTypeRegistry;
|
||||||
|
use App\Services\TenantConfiguration\SupportedScopeResolver;
|
||||||
|
use App\Support\TenantConfiguration\ClaimState;
|
||||||
|
use App\Support\TenantConfiguration\CoverageLevel;
|
||||||
|
|
||||||
|
it('Spec414 blocks unsafe customer-facing claims using persisted kernel definitions', function () {
|
||||||
|
$resolver = new SupportedScopeResolver;
|
||||||
|
$scope = $resolver->findActive('intune_tcm_core');
|
||||||
|
$roleScopeTag = (new ResourceTypeRegistry)->findActive('roleScopeTag');
|
||||||
|
$guard = new ClaimGuard;
|
||||||
|
|
||||||
|
expect($scope)->not->toBeNull()
|
||||||
|
->and($roleScopeTag)->not->toBeNull();
|
||||||
|
|
||||||
|
expect($guard->evaluate(
|
||||||
|
scopeKey: $scope?->scope_key,
|
||||||
|
requestedLevel: CoverageLevel::Certified,
|
||||||
|
actualLevel: CoverageLevel::Certified,
|
||||||
|
scopeComplete: true,
|
||||||
|
customerFacing: true,
|
||||||
|
sourceClass: $roleScopeTag?->source_class,
|
||||||
|
restoreTier: $roleScopeTag?->restore_tier,
|
||||||
|
allowsBetaClaims: (bool) $roleScopeTag?->allows_beta_claims,
|
||||||
|
allowsCertifiedClaims: (bool) $roleScopeTag?->allows_certified_claims,
|
||||||
|
))->toBe(ClaimState::ClaimBlocked);
|
||||||
|
|
||||||
|
expect($guard->evaluate(
|
||||||
|
scopeKey: $scope?->scope_key,
|
||||||
|
requestedLevel: CoverageLevel::ContentBacked,
|
||||||
|
actualLevel: CoverageLevel::ContentBacked,
|
||||||
|
scopeComplete: false,
|
||||||
|
customerFacing: true,
|
||||||
|
customerClaimsAllowed: (bool) $scope?->customer_claims_allowed,
|
||||||
|
))->toBe(ClaimState::ClaimBlocked);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Spec414 allows an exact persisted scope claim at the required level', function () {
|
||||||
|
$resolver = new SupportedScopeResolver;
|
||||||
|
$scope = $resolver->findActive('intune_tcm_core');
|
||||||
|
$guard = new ClaimGuard;
|
||||||
|
|
||||||
|
expect($scope)->not->toBeNull()
|
||||||
|
->and($resolver->meetsMinimum(CoverageLevel::ContentBacked, $scope))->toBeTrue();
|
||||||
|
|
||||||
|
expect($guard->evaluate(
|
||||||
|
scopeKey: $scope?->scope_key,
|
||||||
|
requestedLevel: $scope?->minimum_coverage_level,
|
||||||
|
actualLevel: CoverageLevel::Comparable,
|
||||||
|
scopeComplete: true,
|
||||||
|
customerFacing: true,
|
||||||
|
customerClaimsAllowed: (bool) $scope?->customer_claims_allowed,
|
||||||
|
))->toBe(ClaimState::ClaimAllowed);
|
||||||
|
});
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
it('Spec414 required kernel definition tables do not introduce ownership columns', function () {
|
||||||
|
foreach (['tenant_configuration_resource_types', 'tenant_configuration_supported_scopes'] as $table) {
|
||||||
|
expect(Schema::hasTable($table))->toBeTrue();
|
||||||
|
|
||||||
|
$columns = Schema::getColumnListing($table);
|
||||||
|
|
||||||
|
expect($columns)
|
||||||
|
->not->toContain('tenant_id')
|
||||||
|
->not->toContain('workspace_id')
|
||||||
|
->not->toContain('managed_environment_id')
|
||||||
|
->not->toContain('provider_connection_id');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Spec414 keeps provider-native tenant identifiers out of Coverage v2 ownership schema', function () {
|
||||||
|
$coverageColumns = collect([
|
||||||
|
...Schema::getColumnListing('tenant_configuration_resource_types'),
|
||||||
|
...Schema::getColumnListing('tenant_configuration_supported_scopes'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($coverageColumns->filter(
|
||||||
|
fn (string $column): bool => str_contains($column, 'tenant_id')
|
||||||
|
|| str_contains($column, 'entra_tenant_id')
|
||||||
|
|| str_contains($column, 'provider_tenant_id')
|
||||||
|
|| str_contains($column, 'source_tenant_id')
|
||||||
|
|| str_contains($column, 'external_tenant_id')
|
||||||
|
)->values()->all())->toBe([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Spec414 keeps historical migration seed semantics independent from mutable runtime defaults', function () {
|
||||||
|
$migration = file_get_contents(database_path('migrations/2026_06_25_000414_create_tenant_configuration_kernel_tables.php'));
|
||||||
|
|
||||||
|
expect($migration)->not->toContain('App\\Services\\TenantConfiguration\\ResourceTypeRegistry')
|
||||||
|
->not->toContain('App\\Services\\TenantConfiguration\\SupportedScopeResolver')
|
||||||
|
->not->toContain('App\\Support\\TenantConfiguration')
|
||||||
|
->and($migration)->toContain('private function resourceTypeDefinitions')
|
||||||
|
->toContain('private function supportedScopeDefinitions')
|
||||||
|
->toContain('private const SOURCE_CLASSES');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Spec414 proves PostgreSQL JSONB and check constraints for kernel definitions', function () {
|
||||||
|
if (DB::getDriverName() !== 'pgsql') {
|
||||||
|
$this->markTestSkipped('PostgreSQL-specific Coverage v2 kernel schema proof runs in the pgsql lane.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$columns = collect(DB::select(<<<'SQL'
|
||||||
|
SELECT table_name, column_name, data_type
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name IN ('tenant_configuration_resource_types', 'tenant_configuration_supported_scopes')
|
||||||
|
AND column_name IN ('metadata', 'included_resource_types')
|
||||||
|
SQL))->mapWithKeys(
|
||||||
|
fn (object $row): array => [$row->table_name.'.'.$row->column_name => $row->data_type],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($columns['tenant_configuration_resource_types.metadata'])->toBe('jsonb')
|
||||||
|
->and($columns['tenant_configuration_supported_scopes.metadata'])->toBe('jsonb')
|
||||||
|
->and($columns['tenant_configuration_supported_scopes.included_resource_types'])->toBe('jsonb');
|
||||||
|
|
||||||
|
$constraints = collect(DB::select(<<<'SQL'
|
||||||
|
SELECT conname
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conrelid IN (
|
||||||
|
'tenant_configuration_resource_types'::regclass,
|
||||||
|
'tenant_configuration_supported_scopes'::regclass
|
||||||
|
)
|
||||||
|
SQL))->pluck('conname')->all();
|
||||||
|
|
||||||
|
expect($constraints)
|
||||||
|
->toContain('tenant_config_resource_types_source_class_check')
|
||||||
|
->toContain('tenant_config_supported_scopes_denominator_array_check');
|
||||||
|
});
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Models\TenantConfigurationResourceType;
|
||||||
|
use App\Services\TenantConfiguration\ResourceTypeRegistry;
|
||||||
|
use App\Support\TenantConfiguration\SourceClass;
|
||||||
|
|
||||||
|
it('Spec414 persists the required initial Coverage v2 registry entries', function () {
|
||||||
|
$registry = new ResourceTypeRegistry;
|
||||||
|
|
||||||
|
expect(TenantConfigurationResourceType::query()->count())->toBe(8)
|
||||||
|
->and($registry->active())->toHaveCount(8)
|
||||||
|
->and($registry->findActive('notificationMessageTemplate')?->source_class)->toBe(SourceClass::GraphV1Fallback)
|
||||||
|
->and($registry->findActive('roleScopeTag')?->source_class)->toBe(SourceClass::GraphBetaExperimental);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Spec414 keeps default registry setup upsert-safe', function () {
|
||||||
|
$registry = new ResourceTypeRegistry;
|
||||||
|
|
||||||
|
$registry->syncDefaults();
|
||||||
|
$registry->syncDefaults();
|
||||||
|
|
||||||
|
expect(TenantConfigurationResourceType::query()->count())->toBe(8)
|
||||||
|
->and(TenantConfigurationResourceType::query()
|
||||||
|
->where('canonical_type', 'roleScopeTag')
|
||||||
|
->where('source_class', SourceClass::GraphBetaExperimental->value)
|
||||||
|
->count())->toBe(1);
|
||||||
|
});
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Models\TenantConfigurationSupportedScope;
|
||||||
|
use App\Services\TenantConfiguration\ResourceTypeRegistry;
|
||||||
|
use App\Services\TenantConfiguration\SupportedScopeResolver;
|
||||||
|
use App\Support\TenantConfiguration\CoverageLevel;
|
||||||
|
|
||||||
|
it('Spec414 persists supported-scope denominators and minimum levels', function () {
|
||||||
|
$resolver = new SupportedScopeResolver;
|
||||||
|
$scope = $resolver->findActive('intune_tcm_core');
|
||||||
|
|
||||||
|
expect($scope)->not->toBeNull()
|
||||||
|
->and($scope?->minimum_coverage_level)->toBe(CoverageLevel::ContentBacked)
|
||||||
|
->and($scope?->included_resource_types)->toBe([
|
||||||
|
'deviceAndAppManagementAssignmentFilter',
|
||||||
|
'deviceEnrollmentLimitRestriction',
|
||||||
|
'deviceEnrollmentPlatformRestriction',
|
||||||
|
'deviceEnrollmentStatusPageWindows10',
|
||||||
|
'appProtectionPolicyAndroid',
|
||||||
|
'appProtectionPolicyiOS',
|
||||||
|
])
|
||||||
|
->and($scope?->allow_beta)->toBeFalse()
|
||||||
|
->and($scope?->allow_graph_fallback)->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Spec414 resolves persisted scopes against persisted active resource types', function () {
|
||||||
|
$resolver = new SupportedScopeResolver;
|
||||||
|
$scope = $resolver->findActive('intune_tcm_core_with_graph_fallback');
|
||||||
|
$resourceTypes = (new ResourceTypeRegistry)->active();
|
||||||
|
|
||||||
|
expect($scope)->not->toBeNull();
|
||||||
|
|
||||||
|
$resolved = $resolver->resolveDefinition($scope, $resourceTypes);
|
||||||
|
|
||||||
|
expect($resolved['included_resource_types'])->toContain('notificationMessageTemplate')
|
||||||
|
->and($resolved['included_resource_types'])->not->toContain('roleScopeTag')
|
||||||
|
->and($resolved['allow_graph_fallback'])->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Spec414 fails closed for persisted supported scopes with unknown denominator entries', function () {
|
||||||
|
$scope = TenantConfigurationSupportedScope::factory()->create([
|
||||||
|
'scope_key' => 'invalid_persisted_scope',
|
||||||
|
'minimum_coverage_level' => CoverageLevel::ContentBacked->value,
|
||||||
|
'included_resource_types' => [
|
||||||
|
'deviceAndAppManagementAssignmentFilter',
|
||||||
|
'unknownResourceType',
|
||||||
|
],
|
||||||
|
'customer_claims_allowed' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(fn () => (new SupportedScopeResolver)->resolveDefinition($scope, (new ResourceTypeRegistry)->active()))
|
||||||
|
->toThrow(UnexpectedValueException::class, 'unknownResourceType');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Spec414 keeps default supported-scope setup upsert-safe', function () {
|
||||||
|
$resolver = new SupportedScopeResolver;
|
||||||
|
|
||||||
|
$resolver->syncDefaults();
|
||||||
|
$resolver->syncDefaults();
|
||||||
|
|
||||||
|
expect(TenantConfigurationSupportedScope::query()->count())->toBe(2)
|
||||||
|
->and(TenantConfigurationSupportedScope::query()
|
||||||
|
->where('scope_key', 'intune_tcm_core')
|
||||||
|
->count())->toBe(1);
|
||||||
|
});
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Services\TenantConfiguration\ClaimGuard;
|
||||||
|
use App\Support\TenantConfiguration\ClaimState;
|
||||||
|
use App\Support\TenantConfiguration\CoverageLevel;
|
||||||
|
use App\Support\TenantConfiguration\RestoreTier;
|
||||||
|
use App\Support\TenantConfiguration\SourceClass;
|
||||||
|
|
||||||
|
it('Spec414 blocks unscoped 100 percent claims', function () {
|
||||||
|
$guard = new ClaimGuard;
|
||||||
|
|
||||||
|
expect($guard->evaluate(
|
||||||
|
scopeKey: null,
|
||||||
|
requestedLevel: CoverageLevel::ContentBacked,
|
||||||
|
actualLevel: CoverageLevel::ContentBacked,
|
||||||
|
scopeComplete: true,
|
||||||
|
unscoped: true,
|
||||||
|
percentage: 100,
|
||||||
|
))->toBe(ClaimState::ClaimBlocked);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Spec414 blocks certified claims for beta experimental resource types by default', function () {
|
||||||
|
$guard = new ClaimGuard;
|
||||||
|
|
||||||
|
expect($guard->evaluate(
|
||||||
|
scopeKey: 'intune_tcm_core',
|
||||||
|
requestedLevel: CoverageLevel::Certified,
|
||||||
|
actualLevel: CoverageLevel::Certified,
|
||||||
|
scopeComplete: true,
|
||||||
|
sourceClass: SourceClass::GraphBetaExperimental,
|
||||||
|
allowsBetaClaims: false,
|
||||||
|
allowsCertifiedClaims: false,
|
||||||
|
))->toBe(ClaimState::ClaimBlocked);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Spec414 blocks restore claims when the resource type is not restorable', function () {
|
||||||
|
$guard = new ClaimGuard;
|
||||||
|
|
||||||
|
expect($guard->evaluate(
|
||||||
|
scopeKey: 'intune_tcm_core',
|
||||||
|
requestedLevel: CoverageLevel::Restorable,
|
||||||
|
actualLevel: CoverageLevel::Restorable,
|
||||||
|
scopeComplete: true,
|
||||||
|
restoreTier: RestoreTier::PreviewOnly,
|
||||||
|
restoreClaim: true,
|
||||||
|
))->toBe(ClaimState::ClaimBlocked);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Spec414 blocks customer-facing claims for incomplete supported scopes', function () {
|
||||||
|
$guard = new ClaimGuard;
|
||||||
|
|
||||||
|
expect($guard->evaluate(
|
||||||
|
scopeKey: 'intune_tcm_core',
|
||||||
|
requestedLevel: CoverageLevel::ContentBacked,
|
||||||
|
actualLevel: CoverageLevel::ContentBacked,
|
||||||
|
scopeComplete: false,
|
||||||
|
customerFacing: true,
|
||||||
|
))->toBe(ClaimState::ClaimBlocked);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Spec414 allows exact scope and level claims', function () {
|
||||||
|
$guard = new ClaimGuard;
|
||||||
|
|
||||||
|
expect($guard->evaluate(
|
||||||
|
scopeKey: 'intune_tcm_core',
|
||||||
|
requestedLevel: CoverageLevel::ContentBacked,
|
||||||
|
actualLevel: CoverageLevel::Comparable,
|
||||||
|
scopeComplete: true,
|
||||||
|
customerFacing: true,
|
||||||
|
))->toBe(ClaimState::ClaimAllowed);
|
||||||
|
});
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Support\TenantConfiguration\ClaimState;
|
||||||
|
use App\Support\TenantConfiguration\CoverageLevel;
|
||||||
|
use App\Support\TenantConfiguration\EvidenceState;
|
||||||
|
use App\Support\TenantConfiguration\IdentityState;
|
||||||
|
use App\Support\TenantConfiguration\ResourceClass;
|
||||||
|
use App\Support\TenantConfiguration\RestoreTier;
|
||||||
|
use App\Support\TenantConfiguration\SourceClass;
|
||||||
|
use App\Support\TenantConfiguration\SupportState;
|
||||||
|
use App\Support\TenantConfiguration\Workload;
|
||||||
|
|
||||||
|
it('Spec414 exposes exactly the approved Coverage v2 kernel value families', function () {
|
||||||
|
expect(SourceClass::values())->toBe([
|
||||||
|
'tcm',
|
||||||
|
'graph_v1_fallback',
|
||||||
|
'graph_beta_experimental',
|
||||||
|
])
|
||||||
|
->and(Workload::values())->toBe(['intune'])
|
||||||
|
->and(ResourceClass::values())->toBe(['configuration'])
|
||||||
|
->and(SupportState::values())->toBe([
|
||||||
|
'supported',
|
||||||
|
'fallback_supported',
|
||||||
|
'experimental',
|
||||||
|
'unsupported',
|
||||||
|
'out_of_scope',
|
||||||
|
])
|
||||||
|
->and(CoverageLevel::values())->toBe([
|
||||||
|
'detected',
|
||||||
|
'content_backed',
|
||||||
|
'comparable',
|
||||||
|
'renderable',
|
||||||
|
'restorable',
|
||||||
|
'certified',
|
||||||
|
])
|
||||||
|
->and(EvidenceState::values())->toBe([
|
||||||
|
'not_captured',
|
||||||
|
'captured',
|
||||||
|
'content_backed',
|
||||||
|
'permission_blocked',
|
||||||
|
'source_unavailable',
|
||||||
|
'schema_unknown',
|
||||||
|
'capture_failed',
|
||||||
|
])
|
||||||
|
->and(IdentityState::values())->toBe([
|
||||||
|
'stable',
|
||||||
|
'derived',
|
||||||
|
'identity_conflict',
|
||||||
|
'missing_external_id',
|
||||||
|
'unsupported_identity',
|
||||||
|
])
|
||||||
|
->and(ClaimState::values())->toBe([
|
||||||
|
'claim_allowed',
|
||||||
|
'claim_limited',
|
||||||
|
'claim_blocked',
|
||||||
|
'internal_only',
|
||||||
|
])
|
||||||
|
->and(RestoreTier::values())->toBe([
|
||||||
|
'not_restorable',
|
||||||
|
'preview_only',
|
||||||
|
'restorable',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Spec414 orders coverage levels by claim strength', function () {
|
||||||
|
expect(CoverageLevel::Certified->meets(CoverageLevel::Detected))->toBeTrue()
|
||||||
|
->and(CoverageLevel::Restorable->meets(CoverageLevel::Renderable))->toBeTrue()
|
||||||
|
->and(CoverageLevel::ContentBacked->meets(CoverageLevel::Comparable))->toBeFalse();
|
||||||
|
});
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Services\TenantConfiguration\ResourceTypeRegistry;
|
||||||
|
|
||||||
|
it('Spec414 defines the initial Coverage v2 resource type registry', function () {
|
||||||
|
$definitions = collect(ResourceTypeRegistry::defaultDefinitions());
|
||||||
|
|
||||||
|
expect($definitions->pluck('canonical_type')->all())->toBe([
|
||||||
|
'deviceAndAppManagementAssignmentFilter',
|
||||||
|
'deviceEnrollmentLimitRestriction',
|
||||||
|
'deviceEnrollmentPlatformRestriction',
|
||||||
|
'deviceEnrollmentStatusPageWindows10',
|
||||||
|
'appProtectionPolicyAndroid',
|
||||||
|
'appProtectionPolicyiOS',
|
||||||
|
'notificationMessageTemplate',
|
||||||
|
'roleScopeTag',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($definitions->where('source_class', 'tcm'))->toHaveCount(6)
|
||||||
|
->and($definitions->firstWhere('canonical_type', 'notificationMessageTemplate')['source_class'])->toBe('graph_v1_fallback')
|
||||||
|
->and($definitions->firstWhere('canonical_type', 'roleScopeTag')['source_class'])->toBe('graph_beta_experimental');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Spec414 keeps beta and fallback source posture explicit', function () {
|
||||||
|
$definitions = collect(ResourceTypeRegistry::defaultDefinitions());
|
||||||
|
$fallback = $definitions->firstWhere('canonical_type', 'notificationMessageTemplate');
|
||||||
|
$beta = $definitions->firstWhere('canonical_type', 'roleScopeTag');
|
||||||
|
|
||||||
|
expect($fallback['allows_graph_fallback_claims'])->toBeTrue()
|
||||||
|
->and($fallback['allows_certified_claims'])->toBeFalse()
|
||||||
|
->and($beta['allows_beta_claims'])->toBeFalse()
|
||||||
|
->and($beta['allows_certified_claims'])->toBeFalse()
|
||||||
|
->and($beta['default_claim_state'])->toBe('internal_only');
|
||||||
|
});
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Services\TenantConfiguration\ResourceTypeRegistry;
|
||||||
|
use App\Services\TenantConfiguration\SupportedScopeResolver;
|
||||||
|
use App\Support\TenantConfiguration\CoverageLevel;
|
||||||
|
|
||||||
|
it('Spec414 resolves explicit supported-scope denominators', function () {
|
||||||
|
$resolver = new SupportedScopeResolver;
|
||||||
|
$scope = collect(SupportedScopeResolver::defaultDefinitions())->firstWhere('scope_key', 'intune_tcm_core');
|
||||||
|
$resolved = $resolver->resolveDefinition($scope, ResourceTypeRegistry::defaultDefinitions());
|
||||||
|
|
||||||
|
expect($resolved['minimum_coverage_level'])->toBe(CoverageLevel::ContentBacked)
|
||||||
|
->and($resolved['included_resource_types'])->toBe([
|
||||||
|
'deviceAndAppManagementAssignmentFilter',
|
||||||
|
'deviceEnrollmentLimitRestriction',
|
||||||
|
'deviceEnrollmentPlatformRestriction',
|
||||||
|
'deviceEnrollmentStatusPageWindows10',
|
||||||
|
'appProtectionPolicyAndroid',
|
||||||
|
'appProtectionPolicyiOS',
|
||||||
|
])
|
||||||
|
->and($resolved['included_resource_types'])->not->toContain('notificationMessageTemplate')
|
||||||
|
->and($resolved['included_resource_types'])->not->toContain('roleScopeTag');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Spec414 fails closed when a supported scope references an unknown resource type', function () {
|
||||||
|
$resolver = new SupportedScopeResolver;
|
||||||
|
$scope = [
|
||||||
|
'scope_key' => 'invalid_scope',
|
||||||
|
'minimum_coverage_level' => CoverageLevel::ContentBacked->value,
|
||||||
|
'included_resource_types' => [
|
||||||
|
'deviceAndAppManagementAssignmentFilter',
|
||||||
|
'unknownResourceType',
|
||||||
|
],
|
||||||
|
'allow_beta' => false,
|
||||||
|
'allow_graph_fallback' => false,
|
||||||
|
'customer_claims_allowed' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(fn () => $resolver->resolveDefinition($scope, ResourceTypeRegistry::defaultDefinitions()))
|
||||||
|
->toThrow(UnexpectedValueException::class, 'unknownResourceType')
|
||||||
|
->and(fn () => $resolver->isScopeComplete([], $scope, ResourceTypeRegistry::defaultDefinitions()))
|
||||||
|
->toThrow(UnexpectedValueException::class, 'unknownResourceType');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Spec414 includes Graph fallback only for scopes that explicitly allow it', function () {
|
||||||
|
$resolver = new SupportedScopeResolver;
|
||||||
|
$scopes = collect(SupportedScopeResolver::defaultDefinitions());
|
||||||
|
|
||||||
|
$defaultScope = $resolver->resolveDefinition(
|
||||||
|
$scopes->firstWhere('scope_key', 'intune_tcm_core'),
|
||||||
|
ResourceTypeRegistry::defaultDefinitions(),
|
||||||
|
);
|
||||||
|
$fallbackScope = $resolver->resolveDefinition(
|
||||||
|
$scopes->firstWhere('scope_key', 'intune_tcm_core_with_graph_fallback'),
|
||||||
|
ResourceTypeRegistry::defaultDefinitions(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($defaultScope['included_resource_types'])->not->toContain('notificationMessageTemplate')
|
||||||
|
->and($fallbackScope['included_resource_types'])->toContain('notificationMessageTemplate');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Spec414 excludes beta resources by default even when they are listed', function () {
|
||||||
|
$resolver = new SupportedScopeResolver;
|
||||||
|
$scope = [
|
||||||
|
'scope_key' => 'test_beta_scope',
|
||||||
|
'minimum_coverage_level' => CoverageLevel::Detected->value,
|
||||||
|
'included_resource_types' => ['roleScopeTag'],
|
||||||
|
'allow_beta' => false,
|
||||||
|
'allow_graph_fallback' => false,
|
||||||
|
'customer_claims_allowed' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
$resolved = $resolver->resolveDefinition($scope, ResourceTypeRegistry::defaultDefinitions());
|
||||||
|
|
||||||
|
expect($resolved['included_resource_types'])->toBe([])
|
||||||
|
->and($resolved['excluded_resource_types'])->toBe(['roleScopeTag']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Spec414 proves exact scope completeness from explicit denominator membership', function () {
|
||||||
|
$resolver = new SupportedScopeResolver;
|
||||||
|
$scope = collect(SupportedScopeResolver::defaultDefinitions())->firstWhere('scope_key', 'intune_tcm_core');
|
||||||
|
|
||||||
|
expect($resolver->isScopeComplete([
|
||||||
|
'deviceAndAppManagementAssignmentFilter',
|
||||||
|
'deviceEnrollmentLimitRestriction',
|
||||||
|
'deviceEnrollmentPlatformRestriction',
|
||||||
|
'deviceEnrollmentStatusPageWindows10',
|
||||||
|
'appProtectionPolicyAndroid',
|
||||||
|
'appProtectionPolicyiOS',
|
||||||
|
], $scope))->toBeTrue()
|
||||||
|
->and($resolver->isScopeComplete([
|
||||||
|
'deviceAndAppManagementAssignmentFilter',
|
||||||
|
], $scope))->toBeFalse();
|
||||||
|
});
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
# Specification Quality Checklist: Spec 414 - TCM-First Coverage v2 Kernel
|
||||||
|
|
||||||
|
**Purpose**: Validate specification completeness and quality before proceeding to implementation
|
||||||
|
**Created**: 2026-06-25
|
||||||
|
**Feature**: `specs/414-tcm-first-coverage-core-cutover/spec.md`
|
||||||
|
|
||||||
|
## Content Quality
|
||||||
|
|
||||||
|
- [x] CHK001 The spec is reframed as `TCM-First Coverage v2 Kernel`.
|
||||||
|
- [x] CHK002 The folder name remains unchanged.
|
||||||
|
- [x] CHK003 The previous full-cutover readiness failure is acknowledged.
|
||||||
|
- [x] CHK004 The spec keeps strategic hard-cutover direction without making this slice the cutover.
|
||||||
|
- [x] CHK005 The spec states Coverage v2 is inactive after Spec 414.
|
||||||
|
- [x] CHK006 The spec states Coverage v1 remains active runtime truth until a later activation/cutover spec.
|
||||||
|
- [x] CHK007 Mandatory sections for candidate check, scope, no-legacy posture, UI/product surface impact, proportionality, tests, user stories, requirements, success criteria, assumptions, risks, and follow-ups are completed.
|
||||||
|
|
||||||
|
## Bounded Kernel Scope
|
||||||
|
|
||||||
|
- [x] CHK010 The scope is limited to value families, minimal persistence, registry, supported scope, claim guard, source class, provider provenance, and tests.
|
||||||
|
- [x] CHK011 Full UI cutover is deferred.
|
||||||
|
- [x] CHK012 Evidence Overview conversion is deferred.
|
||||||
|
- [x] CHK013 Customer Review Workspace conversion is deferred.
|
||||||
|
- [x] CHK014 Review Pack/report conversion is deferred.
|
||||||
|
- [x] CHK015 Restore readiness conversion is deferred.
|
||||||
|
- [x] CHK016 Full baseline/compare conversion is deferred.
|
||||||
|
- [x] CHK017 Legacy runtime deletion and broad v1 test rewrite are deferred.
|
||||||
|
- [x] CHK018 OperationRun-backed capture/evaluation is deferred.
|
||||||
|
- [x] CHK019 TCM/Graph remote capture and generic content-backed evidence capture are deferred.
|
||||||
|
- [x] CHK020 Browser proof across customer surfaces is not required unless UI scope is amended.
|
||||||
|
|
||||||
|
## Ownership And Provider Provenance
|
||||||
|
|
||||||
|
- [x] CHK030 Coverage v2 internal ownership uses `workspace_id` and `managed_environment_id` where environment-owned rows exist.
|
||||||
|
- [x] CHK031 `tenant_id` is prohibited as Coverage v2 internal ownership truth.
|
||||||
|
- [x] CHK032 Provider-native external IDs are metadata only.
|
||||||
|
- [x] CHK033 `provider_connection_id` same-workspace and same-managed-environment validation is required where stored.
|
||||||
|
- [x] CHK034 Same-scope provider provenance is included in requirements and tasks.
|
||||||
|
|
||||||
|
## Source Classes And Claim Safety
|
||||||
|
|
||||||
|
- [x] CHK040 Initial required registry entries are listed.
|
||||||
|
- [x] CHK041 TCM-aligned Intune resource types map to `source_class = tcm`.
|
||||||
|
- [x] CHK042 `notificationMessageTemplate` maps to `source_class = graph_v1_fallback`.
|
||||||
|
- [x] CHK043 `roleScopeTag` maps to `source_class = graph_beta_experimental`.
|
||||||
|
- [x] CHK044 Supported scopes require explicit denominator and minimum coverage level.
|
||||||
|
- [x] CHK045 Beta resources are excluded by default.
|
||||||
|
- [x] CHK046 Graph fallback is included only when scope allows it.
|
||||||
|
- [x] CHK047 Claim guard blocks unscoped 100% claims.
|
||||||
|
- [x] CHK048 Claim guard blocks beta certification by default.
|
||||||
|
- [x] CHK049 Claim guard blocks non-restorable restore claims and incomplete supported-scope customer claims.
|
||||||
|
- [x] CHK050 Claim guard allows only exact scope + level claims.
|
||||||
|
- [x] CHK051 Kernel value-family allowed values are fixed and must not be expanded during implementation without amending artifacts.
|
||||||
|
- [x] CHK052 `tenantpilot_internal` is not part of the Spec 414 initial source-class implementation scope.
|
||||||
|
|
||||||
|
## Kernel Persistence Shape
|
||||||
|
|
||||||
|
- [x] CHK055 `tenant_configuration_resource_types` required fields are defined.
|
||||||
|
- [x] CHK056 `tenant_configuration_supported_scopes` required fields are defined.
|
||||||
|
- [x] CHK057 Required kernel definition tables are platform-seeded definitions, not workspace/environment/provider-connection-owned records.
|
||||||
|
- [x] CHK058 Required kernel definitions have deterministic uniqueness and upsert-safe seed/migration semantics.
|
||||||
|
- [x] CHK059 PostgreSQL lane triggers are explicit for JSONB, composite FKs, partial unique indexes, same-scope provider constraints, or other PostgreSQL-specific behavior.
|
||||||
|
|
||||||
|
## No Legacy / No Dual Truth
|
||||||
|
|
||||||
|
- [x] CHK060 No compatibility shim is allowed.
|
||||||
|
- [x] CHK061 No dual writes are allowed.
|
||||||
|
- [x] CHK062 No fallback readers are allowed.
|
||||||
|
- [x] CHK063 No v1-to-v2 translator is allowed.
|
||||||
|
- [x] CHK064 No old snapshot promotion into v2 proof is allowed.
|
||||||
|
- [x] CHK065 No old gap taxonomy is allowed as v2 logic.
|
||||||
|
- [x] CHK066 No customer-facing dual truth is allowed.
|
||||||
|
|
||||||
|
## Product Surface And OperationRun
|
||||||
|
|
||||||
|
- [x] CHK070 Product Surface Impact is `N/A - no rendered UI surface changed`.
|
||||||
|
- [x] CHK071 Browser proof is `N/A - no rendered UI surface changed`.
|
||||||
|
- [x] CHK072 Human Product Sanity is not required for a rendered page.
|
||||||
|
- [x] CHK073 Stop-and-amend rule exists for any UI file change.
|
||||||
|
- [x] CHK074 No OperationRun-producing workflow is introduced by default.
|
||||||
|
- [x] CHK075 OperationRun-backed capture/evaluation is deferred to a follow-up spec.
|
||||||
|
|
||||||
|
## Task Readiness
|
||||||
|
|
||||||
|
- [x] CHK080 `tasks.md` is bounded to preflight, tests, value families, minimal persistence, registry, supported scope, claim guard, boundary guards, and validation.
|
||||||
|
- [x] CHK081 Tasks include unit tests for registry, supported scope, claim guard, and value families.
|
||||||
|
- [x] CHK082 Tasks include feature tests for registry persistence, supported scopes, claim guard, no `tenant_id`, and same-scope provider connection validation where applicable.
|
||||||
|
- [x] CHK083 Tasks include no UI surface impact validation.
|
||||||
|
- [x] CHK084 Tasks include implementation report close-out.
|
||||||
|
- [x] CHK085 Tasks include focused validation commands.
|
||||||
|
|
||||||
|
## Gate Results
|
||||||
|
|
||||||
|
- [x] CHK090 Candidate Selection Gate result: PASS after narrowing.
|
||||||
|
- [x] CHK091 Spec Readiness Gate preparation status: ready for bounded kernel implementation.
|
||||||
|
- [x] CHK092 Workflow outcome: keep as inactive Coverage v2 kernel slice.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Follow-up specs are recommendations only until explicitly prepared: 415 Generic Content-Backed Capture, 416 Canonical Identity Engine, 417 Coverage v2 Operator Surface, 418 Legacy Coverage Cutover & Removal, 419 Intune Core Comparable/Renderable Pack, 420 Certified Intune Core Coverage Pack, 421 Pilot Readiness Gate.
|
||||||
|
- Supply-chain remediation, if still open, remains a release/pilot gate outside Spec 414.
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
# Implementation Report: Spec 414 - TCM-First Coverage v2 Kernel
|
||||||
|
|
||||||
|
## Preflight
|
||||||
|
|
||||||
|
- Branch: `414-tcm-first-coverage-core-cutover`
|
||||||
|
- Starting HEAD: `fdd9eb2e feat: add focused pilot gate recheck (#480)`
|
||||||
|
- Starting dirty state: `.specify/memory/constitution.md` modified; `specs/414-tcm-first-coverage-core-cutover/` untracked.
|
||||||
|
- Dirty-state assessment: active Spec 414 preparation artifacts only; no runtime code was dirty before implementation.
|
||||||
|
|
||||||
|
## Scope Close-Out
|
||||||
|
|
||||||
|
- Kernel status: inactive Coverage v2 kernel only.
|
||||||
|
- Kernel tables: `tenant_configuration_resource_types`, `tenant_configuration_supported_scopes`.
|
||||||
|
- Kernel models: `TenantConfigurationResourceType`, `TenantConfigurationSupportedScope`.
|
||||||
|
- Kernel services: `ResourceTypeRegistry`, `SupportedScopeResolver`, `ClaimGuard`.
|
||||||
|
- Kernel value families: `SourceClass`, `Workload`, `ResourceClass`, `SupportState`, `CoverageLevel`, `EvidenceState`, `IdentityState`, `ClaimState`, `RestoreTier`.
|
||||||
|
- Runtime UI impact: none.
|
||||||
|
- Browser proof: `N/A - no rendered UI surface changed`.
|
||||||
|
- Human Product Sanity: `N/A - no rendered UI surface changed`; workflow sanity result is that the slice remains inactive and does not create customer-facing dual truth.
|
||||||
|
- OperationRun impact: none.
|
||||||
|
- Remote provider calls: none.
|
||||||
|
- Legacy compatibility: no v1-to-v2 adapter, fallback reader, dual write, old snapshot promotion, or old gap-taxonomy runtime dependency introduced.
|
||||||
|
- Optional concrete resource/evidence tables: deferred; the required definition tables and service tests prove the kernel scope without environment-owned observation rows.
|
||||||
|
- Provider provenance: required definition tables intentionally omit `workspace_id`, `managed_environment_id`, and `provider_connection_id`; provider-native tenant IDs remain outside Coverage v2 ownership schema.
|
||||||
|
- `tenant_id` proof: required Coverage v2 tables omit `tenant_id` and any provider-native tenant identifier columns.
|
||||||
|
- Policy posture: no policies were added because the new models are inactive platform-seeded definitions with no route, Filament resource, API, or mutation surface. Later activation must add policy/authorization coverage before exposure.
|
||||||
|
|
||||||
|
## Manual Review Finding Remediation
|
||||||
|
|
||||||
|
- PASS: Supported-scope denominator integrity is fail-closed. `SupportedScopeResolver` now rejects unknown canonical resource types instead of silently shrinking the denominator before completeness checks.
|
||||||
|
- PASS: Denominator fail-closed behavior is covered in both unit and feature lanes, including persisted supported-scope rows.
|
||||||
|
- PASS: Spec 414 migration seed semantics are frozen in the migration and no longer depend on mutable runtime registry/resolver services or enum value lists.
|
||||||
|
- PASS: A focused schema guard verifies the historical migration does not import `App\Services\TenantConfiguration\*` or `App\Support\TenantConfiguration\*` runtime defaults.
|
||||||
|
- PASS: Coverage v2 factories now emit JSONB object-shaped `metadata`, matching the PostgreSQL object check constraints.
|
||||||
|
|
||||||
|
## Product Surface Close-Out
|
||||||
|
|
||||||
|
- Livewire v4 compliance: Livewire 4.1.4 confirmed; no Livewire code changed.
|
||||||
|
- Provider registration location: no panel provider change; Laravel 12 providers remain in `apps/platform/bootstrap/providers.php`.
|
||||||
|
- Global search posture: no Filament resource or global search change.
|
||||||
|
- Destructive/high-impact actions: none introduced.
|
||||||
|
- Asset strategy: no assets registered; `filament:assets` is not required for this spec.
|
||||||
|
- Visible complexity outcome: neutral; no rendered product surface changed.
|
||||||
|
- Deployment impact: additive migrations for inactive kernel definition tables only; no env vars, queues, scheduler, storage, or asset step.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- PASS: `cd apps/platform && ./vendor/bin/sail bin pint app/Services/TenantConfiguration/SupportedScopeResolver.php database/migrations/2026_06_25_000414_create_tenant_configuration_kernel_tables.php tests/Unit/Support/TenantConfiguration/SupportedScopeResolverTest.php tests/Feature/TenantConfiguration/TenantConfigurationSupportedScopeTest.php tests/Feature/TenantConfiguration/TenantConfigurationKernelSchemaTest.php --format agent`
|
||||||
|
- PASS: `cd apps/platform && ./vendor/bin/sail bin pint database/factories/TenantConfigurationResourceTypeFactory.php database/factories/TenantConfigurationSupportedScopeFactory.php tests/Feature/TenantConfiguration/TenantConfigurationSupportedScopeTest.php --format agent`
|
||||||
|
- PASS: `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/TenantConfiguration` (14 tests, 40 assertions)
|
||||||
|
- PASS: `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantConfiguration` (11 passed, 1 PostgreSQL-only skipped, 43 assertions)
|
||||||
|
- NOTE: `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest -c phpunit.pgsql.xml --filter=TenantConfiguration` matched no tests in this repo.
|
||||||
|
- PASS: `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest -c phpunit.pgsql.xml tests/Feature/TenantConfiguration` (12 tests, 48 assertions)
|
||||||
|
- PASS: `git diff --check`
|
||||||
|
- PASS: untracked implementation-file whitespace check via `git diff --no-index --check /dev/null <file>`
|
||||||
|
|
||||||
|
## Final Dirty State
|
||||||
|
|
||||||
|
- `.specify/memory/constitution.md`
|
||||||
|
- `apps/platform/app/Models/TenantConfigurationResourceType.php`
|
||||||
|
- `apps/platform/app/Models/TenantConfigurationSupportedScope.php`
|
||||||
|
- `apps/platform/app/Services/TenantConfiguration/*`
|
||||||
|
- `apps/platform/app/Support/TenantConfiguration/*`
|
||||||
|
- `apps/platform/database/factories/TenantConfigurationResourceTypeFactory.php`
|
||||||
|
- `apps/platform/database/factories/TenantConfigurationSupportedScopeFactory.php`
|
||||||
|
- `apps/platform/database/migrations/2026_06_25_000414_create_tenant_configuration_kernel_tables.php`
|
||||||
|
- `apps/platform/tests/Feature/TenantConfiguration/*`
|
||||||
|
- `apps/platform/tests/Unit/Support/TenantConfiguration/*`
|
||||||
|
- `specs/414-tcm-first-coverage-core-cutover/*`
|
||||||
|
|
||||||
|
## Follow-Up Candidates
|
||||||
|
|
||||||
|
- Spec 415 - Generic Content-Backed Capture.
|
||||||
|
- Spec 416 - Canonical Identity Engine.
|
||||||
|
- Spec 417 - Coverage v2 Operator Surface.
|
||||||
|
- Spec 418 - Legacy Coverage Cutover & Removal.
|
||||||
|
- Spec 419 - Intune Core Comparable/Renderable Pack.
|
||||||
|
- Spec 420 - Certified Intune Core Coverage Pack.
|
||||||
281
specs/414-tcm-first-coverage-core-cutover/plan.md
Normal file
281
specs/414-tcm-first-coverage-core-cutover/plan.md
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
# Implementation Plan: Spec 414 - TCM-First Coverage v2 Kernel
|
||||||
|
|
||||||
|
**Branch**: `414-tcm-first-coverage-core-cutover` | **Date**: 2026-06-25 | **Spec**: `specs/414-tcm-first-coverage-core-cutover/spec.md`
|
||||||
|
**Input**: Patched feature specification from `/specs/414-tcm-first-coverage-core-cutover/spec.md`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Introduce the inactive Coverage v2 kernel required for later hard cutover. This slice adds value families, minimal persistence, initial resource type registry, supported-scope contract, source-class model, claim guard, provider provenance rules, and focused tests.
|
||||||
|
|
||||||
|
Coverage v2 is not activated as customer-facing or operator-facing truth in this spec. Existing v1 runtime coverage remains active until a later explicit cutover spec. No v1-to-v2 compatibility adapter, dual write, fallback reader, old snapshot promotion, or customer-facing dual truth is allowed.
|
||||||
|
|
||||||
|
## Technical Context
|
||||||
|
|
||||||
|
**Language/Version**: PHP 8.4.15, Laravel 12.52
|
||||||
|
**Primary Dependencies**: Filament 5.2.1, Livewire 4.1.4, Pest 4.3.1, PostgreSQL via Laravel Sail/Dokploy
|
||||||
|
**Storage**: PostgreSQL; JSONB for supported-scope denominator lists and metadata only where the kernel needs structured definitions, plus optional evidence/resource payloads only if those optional tables are introduced
|
||||||
|
**Testing**: Pest unit/feature tests; PostgreSQL lane when schema constraints or JSONB behavior require it
|
||||||
|
**Validation Lanes**: fast-feedback, confidence, pgsql when needed
|
||||||
|
**Target Platform**: Laravel monolith in `apps/platform`
|
||||||
|
**Project Type**: web application / backend kernel only for this slice
|
||||||
|
**Performance Goals**: registry/scope/claim guard evaluation is deterministic and local; no remote/provider calls
|
||||||
|
**Constraints**: inactive kernel only, no rendered UI changes, no `tenant_id`, same-scope `provider_connection_id`, no compatibility shims, no Graph/TCM calls, no OperationRun-producing workflow by default
|
||||||
|
**Scale/Scope**: initial required resource type entries and supported-scope contract only; catalog expansion and activation deferred
|
||||||
|
|
||||||
|
## UI / Surface Guardrail Plan
|
||||||
|
|
||||||
|
- **Guardrail scope**: no operator-facing surface change.
|
||||||
|
- **Affected routes/pages/actions/states/navigation/panel/provider surfaces**: N/A.
|
||||||
|
- **No-impact class, if applicable**: backend/domain-kernel only.
|
||||||
|
- **Native vs custom classification summary**: N/A.
|
||||||
|
- **Shared-family relevance**: future activation will touch status/evidence/report families; this spec does not.
|
||||||
|
- **State layers in scope**: domain kernel state only.
|
||||||
|
- **Audience modes in scope**: N/A.
|
||||||
|
- **Decision/diagnostic/raw hierarchy plan**: N/A for rendered UI.
|
||||||
|
- **Raw/support gating plan**: N/A.
|
||||||
|
- **One-primary-action / duplicate-truth control**: no UI action introduced; Coverage v2 must not become parallel product truth.
|
||||||
|
- **Handling modes by drift class or surface**: hard-stop if runtime UI changes are needed before spec amendment.
|
||||||
|
- **Repository-signal treatment**: old v1 runtime terms are context only; broad removal is deferred.
|
||||||
|
- **Special surface test profiles**: `N/A - no rendered UI surface changed`.
|
||||||
|
- **Required tests or manual smoke**: unit and feature tests for kernel behavior.
|
||||||
|
- **Exception path and spread control**: none.
|
||||||
|
- **Active feature PR close-out entry**: Kernel / No UI / No dual truth.
|
||||||
|
- **UI/Productization coverage decision**: No UI surface impact.
|
||||||
|
- **Coverage artifacts to update**: none.
|
||||||
|
- **No-impact rationale**: the spec creates inactive persistence/services only.
|
||||||
|
- **Navigation / Filament provider-panel handling**: no panel provider or navigation change.
|
||||||
|
- **Screenshot or page-report need**: no.
|
||||||
|
|
||||||
|
## Product Surface Contract Plan
|
||||||
|
|
||||||
|
- **Product Surface Contract reference**: `docs/product/standards/product-surface-contract.md`.
|
||||||
|
- **No-legacy posture**: inactive kernel plus later hard cutover; no compatibility adapter.
|
||||||
|
- **Page archetype and surface budget plan**: N/A - no rendered product surface changed.
|
||||||
|
- **Technical Annex and deep-link demotion plan**: N/A.
|
||||||
|
- **Canonical status vocabulary plan**: N/A for rendered UI.
|
||||||
|
- **Product Surface exceptions**: none.
|
||||||
|
- **Browser verification plan**: `N/A - no rendered UI surface changed`.
|
||||||
|
- **Human Product Sanity plan**: workflow sanity in implementation report; no rendered-page review.
|
||||||
|
- **Visible complexity outcome target**: neutral; no visible product surface change.
|
||||||
|
- **Implementation report target**: `specs/414-tcm-first-coverage-core-cutover/implementation-report.md`.
|
||||||
|
|
||||||
|
## Filament / Livewire / Deployment Posture
|
||||||
|
|
||||||
|
- **Livewire v4 compliance**: Livewire 4.1.4 confirmed. No Livewire UI changes planned.
|
||||||
|
- **Panel provider registration location**: no panel provider change. Laravel 12 panel providers remain in `apps/platform/bootstrap/providers.php`.
|
||||||
|
- **Global search posture**: no Filament resource/global search change planned.
|
||||||
|
- **Destructive/high-impact action posture**: no destructive or high-impact action introduced.
|
||||||
|
- **Asset strategy**: no new assets; `filament:assets` not required by this spec.
|
||||||
|
- **Testing plan**: unit tests for fixed value families/registry/scope/claim guard; feature tests for deterministic upsert-safe platform-seeded definitions, persistence seed/defaults, claim guard behavior, same-scope provider connection validation where optional concrete tables store `provider_connection_id`, and no-`tenant_id` schema proof.
|
||||||
|
- **Deployment impact**: migrations for required kernel tables; no env vars, queue worker requirement, scheduler change, storage volume change, or asset deployment step by default.
|
||||||
|
|
||||||
|
## Shared Pattern & System Fit
|
||||||
|
|
||||||
|
- **Cross-cutting feature marker**: yes, but kernel-only.
|
||||||
|
- **Systems touched**: new Coverage v2 domain namespace, migrations/models/factories for required kernel tables, registry/scope/claim services.
|
||||||
|
- **Shared abstractions reused**: Laravel models/migrations/factories, existing workspace and managed-environment relationships, existing provider connection model, Pest conventions.
|
||||||
|
- **New abstraction introduced? why?**: ResourceTypeRegistry, SupportedScopeResolver, and ClaimGuard are introduced because supported-scope and claim boundaries must be deterministic before UI/runtime activation.
|
||||||
|
- **Why the existing abstraction was sufficient or insufficient**: v1 inventory/baseline coverage abstractions remain active but cannot express Coverage v2 source class, beta/fallback inclusion, exact scope claims, or no-`tenant_id` ownership.
|
||||||
|
- **Bounded deviation / spread control**: v2 kernel must not be consumed by product surfaces until later activation spec.
|
||||||
|
|
||||||
|
## OperationRun UX Impact
|
||||||
|
|
||||||
|
- **Touches OperationRun start/completion/link UX?**: no.
|
||||||
|
- **Central contract reused**: N/A.
|
||||||
|
- **Delegated UX behaviors**: N/A.
|
||||||
|
- **Surface-owned behavior kept local**: N/A.
|
||||||
|
- **Queued DB-notification policy**: N/A.
|
||||||
|
- **Terminal notification path**: N/A.
|
||||||
|
- **Exception path**: none.
|
||||||
|
|
||||||
|
No remote TCM/Graph capture is implemented in Spec 414. No OperationRun-producing workflow is introduced unless a future amendment adds an operational command/job. OperationRun-backed capture/evaluation is deferred to Generic Content-Backed Capture.
|
||||||
|
|
||||||
|
## Provider Boundary & Portability Fit
|
||||||
|
|
||||||
|
- **Shared provider/platform boundary touched?**: yes.
|
||||||
|
- **Provider-owned seams**: Microsoft TCM source class, Graph v1 fallback source class, Graph beta experimental source class, provider-native IDs as source metadata.
|
||||||
|
- **Platform-core seams**: resource type, source class, workload, resource class, support state, coverage level, evidence state, identity state, claim state, supported scope.
|
||||||
|
- **Neutral platform terms / contracts preserved**: provider, source class, managed environment, supported scope, claim guard.
|
||||||
|
- **Retained provider-specific semantics and why**: `tcm`, `graph_v1_fallback`, and `graph_beta_experimental` remain because this is a TCM-first kernel.
|
||||||
|
- **Bounded extraction or follow-up path**: follow-up specs own catalog expansion, identity engine, capture, and activation.
|
||||||
|
|
||||||
|
## Constitution Check
|
||||||
|
|
||||||
|
*GATE: Must pass before implementation. Re-check after design.*
|
||||||
|
|
||||||
|
- Inventory-first / snapshots-second: PASS. This spec creates inactive registry/scope kernel, not active snapshot truth.
|
||||||
|
- Read/write separation: PASS. No remote write/change action.
|
||||||
|
- Single Graph contract path: PASS. No Graph calls in this spec.
|
||||||
|
- Deterministic capabilities: PASS. Claim/scope rules are deterministic and test-backed.
|
||||||
|
- Proportionality: PASS. The previous full cutover was too broad; this patch narrows to minimum kernel.
|
||||||
|
- No premature abstraction: PASS with justified exception. Registry/scope/claim guard are required for claim safety before activation.
|
||||||
|
- Persisted truth: PASS. Required tables represent kernel source-of-truth definitions, not UI convenience.
|
||||||
|
- Behavioral state: PASS. States affect future claim eligibility and testable guard behavior.
|
||||||
|
- UI semantics: PASS. No UI framework or rendered UI changes.
|
||||||
|
- Product Surface Contract: PASS. No runtime UI surface impact; stop-and-amend rule applies.
|
||||||
|
- Workspace isolation: PASS. Kernel ownership uses `workspace_id` and `managed_environment_id` where environment-owned records exist.
|
||||||
|
- Tenant isolation: PASS in current terminology; no `tenant_id` ownership column is introduced.
|
||||||
|
- Provider boundary: PASS. Provider-native tenant/directory/account IDs stay metadata only.
|
||||||
|
- OperationRun UX: PASS. No OperationRun workflow introduced.
|
||||||
|
- Test governance: PASS. Unit/feature/pgsql lanes are sufficient for kernel behavior.
|
||||||
|
- LEAN-001: PASS. No compatibility shim, fallback reader, dual write, or legacy alias.
|
||||||
|
|
||||||
|
## Test Governance Check
|
||||||
|
|
||||||
|
- **Test purpose / classification by changed surface**: Unit for pure kernel decisions; Feature for persistence/seed/schema/scope behavior; PostgreSQL for JSONB/composite constraint proof when needed.
|
||||||
|
- **Affected validation lanes**: fast-feedback, confidence, pgsql when schema adds JSONB fields, composite foreign keys, partial unique indexes, same-scope provider constraints, or other PostgreSQL-specific behavior.
|
||||||
|
- **Why this lane mix is the narrowest sufficient proof**: no rendered UI, browser, OperationRun, or remote provider workflow exists in this slice.
|
||||||
|
- **Narrowest proving command(s)**:
|
||||||
|
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/TenantConfiguration`
|
||||||
|
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantConfiguration`
|
||||||
|
- `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest -c phpunit.pgsql.xml tests/Feature/TenantConfiguration` if migrations add JSONB fields, composite foreign keys, partial unique indexes, same-scope provider constraints, or other PostgreSQL-specific behavior
|
||||||
|
- `git diff --check`
|
||||||
|
- **Fixture / helper / factory / seed / context cost risks**: new Coverage v2 factories must stay opt-in.
|
||||||
|
- **Expensive defaults or shared helper growth introduced?**: no.
|
||||||
|
- **Heavy-family additions, promotions, or visibility changes**: none by default.
|
||||||
|
- **Surface-class relief / special coverage rule**: `N/A - no rendered UI surface changed`.
|
||||||
|
- **Closing validation and reviewer handoff**: verify no UI wiring, no `tenant_id`, no v1 translator, no compatibility shim, correct source classes, exact scope claim guard, same-scope provider connection validation.
|
||||||
|
- **Budget / baseline / trend follow-up**: document in implementation report if test runtime materially increases.
|
||||||
|
- **Review-stop questions**: did kernel stay inactive; did scope creep into UI/capture/legacy removal; did provider-native ID become ownership truth.
|
||||||
|
- **Escalation path**: document-in-feature.
|
||||||
|
- **Active feature PR close-out entry**: Kernel / No UI / No dual truth.
|
||||||
|
- **Why no dedicated follow-up spec is needed**: this spec is the prerequisite kernel. Activation/capture/cutover follow-ups are listed separately.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Documentation (this feature)
|
||||||
|
|
||||||
|
```text
|
||||||
|
specs/414-tcm-first-coverage-core-cutover/
|
||||||
|
├── checklists/requirements.md
|
||||||
|
├── plan.md
|
||||||
|
├── spec.md
|
||||||
|
└── tasks.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source Code (repository root)
|
||||||
|
|
||||||
|
Likely implementation surfaces:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/platform/app/Support/TenantConfiguration/
|
||||||
|
├── ClaimState.php
|
||||||
|
├── CoverageLevel.php
|
||||||
|
├── EvidenceState.php
|
||||||
|
├── IdentityState.php
|
||||||
|
├── ResourceClass.php
|
||||||
|
├── SourceClass.php
|
||||||
|
├── SupportState.php
|
||||||
|
└── Workload.php
|
||||||
|
|
||||||
|
apps/platform/app/Services/TenantConfiguration/
|
||||||
|
├── ClaimGuard.php
|
||||||
|
├── ResourceTypeRegistry.php
|
||||||
|
└── SupportedScopeResolver.php
|
||||||
|
|
||||||
|
apps/platform/app/Models/
|
||||||
|
├── TenantConfigurationResourceType.php
|
||||||
|
└── TenantConfigurationSupportedScope.php
|
||||||
|
|
||||||
|
apps/platform/database/migrations/
|
||||||
|
└── *_create_tenant_configuration_kernel_tables.php
|
||||||
|
|
||||||
|
apps/platform/database/factories/
|
||||||
|
└── TenantConfiguration*.php
|
||||||
|
|
||||||
|
apps/platform/tests/Unit/Support/TenantConfiguration/
|
||||||
|
apps/platform/tests/Feature/TenantConfiguration/
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional only if implementation proves they are needed for tests or clean service boundaries:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/platform/app/Models/TenantConfigurationResource.php
|
||||||
|
apps/platform/app/Models/TenantConfigurationResourceEvidence.php
|
||||||
|
```
|
||||||
|
|
||||||
|
**Structure Decision**: Use existing Laravel monolith conventions. No new base folder outside established `app/Support`, `app/Services`, `app/Models`, `database`, and `tests` structure.
|
||||||
|
|
||||||
|
## Complexity Tracking
|
||||||
|
|
||||||
|
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||||
|
|---|---|---|
|
||||||
|
| New kernel persistence | Registry/scope definitions must be queryable and testable before activation | Config-only definitions would not prove schema ownership and provider provenance |
|
||||||
|
| New enum/state families | Claim guard needs explicit behavioral boundaries | Reusing v1 gap terms would preserve wrong semantics |
|
||||||
|
| New registry/scope/claim services | Exact scope claims must be programmatically blocked or allowed | UI copy rules cannot prevent future overclaims |
|
||||||
|
|
||||||
|
## Proportionality Review
|
||||||
|
|
||||||
|
- **Current operator problem**: later Coverage v2 activation must not overclaim customer coverage.
|
||||||
|
- **Existing structure is insufficient because**: v1 coverage does not encode source class, exact supported scope, beta/fallback rules, or claim guard eligibility.
|
||||||
|
- **Narrowest correct implementation**: inactive value families, required kernel tables, initial registry entries, supported-scope resolver, claim guard, tests.
|
||||||
|
- **Ownership cost created**: domain namespace, migrations/models, services, factories, tests, implementation report.
|
||||||
|
- **Alternative intentionally rejected**: full cutover in Spec 414; v1-to-v2 compatibility mapping; label-only UI rewrite.
|
||||||
|
- **Release truth**: current-release kernel needed before later activation.
|
||||||
|
|
||||||
|
## Implementation Phases
|
||||||
|
|
||||||
|
### Phase 0 - Preflight
|
||||||
|
|
||||||
|
- Capture branch, HEAD, and dirty state.
|
||||||
|
- Confirm constitution ownership alignment.
|
||||||
|
- Confirm only active spec artifacts were patched during preparation.
|
||||||
|
- Confirm no UI/capture/OperationRun/legacy-removal scope remains in Spec 414 implementation tasks.
|
||||||
|
|
||||||
|
### Phase 1 - Kernel Value Families
|
||||||
|
|
||||||
|
- Add value objects/enums for source class, workload, resource class, support state, coverage level, evidence state, identity state, claim state, and restore tier only if needed for claim guard.
|
||||||
|
- Keep allowed values fixed to the spec-defined kernel list: source classes `tcm`, `graph_v1_fallback`, `graph_beta_experimental`; workload `intune`; resource class `configuration`; and the explicitly listed support, coverage, evidence, identity, claim, and optional restore-tier values.
|
||||||
|
- Keep labels internal/domain-level, not product-facing UI vocabulary.
|
||||||
|
|
||||||
|
### Phase 2 - Minimal Kernel Persistence
|
||||||
|
|
||||||
|
- Add required kernel tables: `tenant_configuration_resource_types` and `tenant_configuration_supported_scopes`.
|
||||||
|
- Treat required kernel tables as platform-seeded definition tables, not workspace/environment/provider-connection-owned records.
|
||||||
|
- Enforce deterministic uniqueness: `(canonical_type, source_class)` for resource types and `scope_key` for supported scopes.
|
||||||
|
- Keep seed/migration behavior upsert-safe so re-running the kernel setup cannot duplicate definitions.
|
||||||
|
- Add optional resource/evidence tables only if implementation proves they are needed for tests or clean service boundaries.
|
||||||
|
- Enforce no `tenant_id`.
|
||||||
|
- Enforce `workspace_id` and `managed_environment_id` where environment-owned rows exist.
|
||||||
|
- Enforce same-scope `provider_connection_id` where provider provenance is stored in optional concrete resource/evidence rows; required kernel definition tables must not store `provider_connection_id`.
|
||||||
|
|
||||||
|
### Phase 3 - Initial Registry And Supported Scope
|
||||||
|
|
||||||
|
- Seed required initial resource type entries.
|
||||||
|
- Classify TCM, Graph v1 fallback, and Graph beta experimental source classes.
|
||||||
|
- Add supported-scope denominator/minimum-level rules.
|
||||||
|
- Exclude beta by default and include fallback only when allowed by scope.
|
||||||
|
|
||||||
|
### Phase 4 - Claim Guard
|
||||||
|
|
||||||
|
- Block unscoped 100% claims.
|
||||||
|
- Block beta certification by default.
|
||||||
|
- Block restore claims where resource type is not restorable.
|
||||||
|
- Block customer-facing claims for incomplete supported scopes.
|
||||||
|
- Allow only exact scope + level claims.
|
||||||
|
|
||||||
|
### Phase 5 - Tests And Validation
|
||||||
|
|
||||||
|
- Add focused unit tests.
|
||||||
|
- Add focused feature tests.
|
||||||
|
- Add PostgreSQL-specific tests when schema constraints or JSONB require PostgreSQL proof.
|
||||||
|
- Write implementation report.
|
||||||
|
- Run Pint, focused tests, optional pgsql lane, and `git diff --check`.
|
||||||
|
|
||||||
|
## Rollout And Deployment Considerations
|
||||||
|
|
||||||
|
- Staging validation is mandatory before production.
|
||||||
|
- Migrations introduce inactive kernel tables only.
|
||||||
|
- No env vars are planned.
|
||||||
|
- No queue worker, scheduler, storage volume, or asset deployment change is planned.
|
||||||
|
- No `filament:assets` requirement unless a future amendment adds registered assets.
|
||||||
|
- Supply-chain remediation, if still open, remains a release/pilot gate outside this spec.
|
||||||
|
|
||||||
|
## Risk Controls
|
||||||
|
|
||||||
|
- Stop if implementation requires rendered UI changes.
|
||||||
|
- Stop if implementation requires remote TCM/Graph capture.
|
||||||
|
- Stop if implementation requires OperationRun-backed evaluation.
|
||||||
|
- Stop if `tenant_id` appears in Coverage v2 ownership fields.
|
||||||
|
- Stop if v1-to-v2 compatibility adapter, dual write, or fallback reader appears.
|
||||||
|
- Stop if old gap taxonomy becomes Coverage v2 runtime truth.
|
||||||
621
specs/414-tcm-first-coverage-core-cutover/spec.md
Normal file
621
specs/414-tcm-first-coverage-core-cutover/spec.md
Normal file
@ -0,0 +1,621 @@
|
|||||||
|
# Feature Specification: Spec 414 - TCM-First Coverage v2 Kernel
|
||||||
|
|
||||||
|
**Feature Branch**: `414-tcm-first-coverage-core-cutover`
|
||||||
|
**Created**: 2026-06-25
|
||||||
|
**Status**: Draft / Ready for implementation preparation review
|
||||||
|
**Input**: User-provided patch prompt narrowing Spec 414 from a full runtime/UI cutover into an inactive Coverage v2 kernel slice.
|
||||||
|
|
||||||
|
## Repo-Truth Adjustment
|
||||||
|
|
||||||
|
The previous Spec 414 preparation correctly identified a P0 product-truth risk: TenantPilot has multiple overlapping coverage and gap concepts that can overstate evidence completeness, supported scope, and customer-facing claims. The implementation-readiness gate then correctly stopped before code changes because the original scope required a full cross-cutting cutover in one loop: new persisted tables, state families, OperationRun-backed evaluation, multiple UI/customer surface migrations, legacy runtime/test deletion, browser proof, and full validation.
|
||||||
|
|
||||||
|
This patched Spec 414 keeps the strategic hard-cutover direction, but narrows the current implementation to the inactive Coverage v2 kernel needed for later activation.
|
||||||
|
|
||||||
|
Coverage v2 may be persisted and tested after this spec, but it must not be presented as active customer-facing proof and must not replace existing baseline, evidence, review, report, restore, provider-readiness, or operator proof surfaces yet.
|
||||||
|
|
||||||
|
## Kernel Slice Decision
|
||||||
|
|
||||||
|
Spec 414 is intentionally narrowed to the Coverage v2 kernel.
|
||||||
|
|
||||||
|
This spec does not complete the full runtime cutover. It creates the provider-agnostic Coverage v2 foundation that later specs will activate.
|
||||||
|
|
||||||
|
Coverage v2 is not yet the active customer-facing or operator-facing coverage truth. Coverage v1 remains the active runtime truth until a later explicit activation/cutover spec.
|
||||||
|
|
||||||
|
The no-legacy rule still applies:
|
||||||
|
|
||||||
|
- no v1-to-v2 compatibility adapter
|
||||||
|
- no dual writes
|
||||||
|
- no fallback readers
|
||||||
|
- no old snapshot promotion into v2 proof
|
||||||
|
- no old gap taxonomy as v2 runtime truth
|
||||||
|
- no customer-facing dual truth
|
||||||
|
|
||||||
|
Coverage v2 may exist after Spec 414, but it must not be wired into customer-facing claims or active operator proof surfaces as a parallel truth. Any visible surface exposure in this spec must be internal/dev-only or absent.
|
||||||
|
|
||||||
|
## Candidate Selection Gate
|
||||||
|
|
||||||
|
- **Selected candidate**: Spec 414 - TCM-First Coverage v2 Kernel.
|
||||||
|
- **Source**: Direct user patch prompt for active `specs/414-tcm-first-coverage-core-cutover/`.
|
||||||
|
- **Why selected**: This kernel is the smallest safe first slice after the full-cutover readiness gate failed on scope size.
|
||||||
|
- **Roadmap relationship**: This is the foundation for later Coverage v2 activation, content-backed capture, identity hardening, operator UI, legacy removal, certification, and pilot readiness.
|
||||||
|
- **Completed-spec guardrail result**: Specs 382 and 383 remain completed historical dependency context only. This spec must not rewrite completed specs.
|
||||||
|
- **Smallest viable implementation slice**: Add inactive Coverage v2 value families, minimal persistence, initial resource type registry, supported-scope contract, claim guard, and focused unit/feature tests.
|
||||||
|
- **Gate result**: PASS after narrowing. The active slice is implementable without creating a partial second customer-facing truth.
|
||||||
|
|
||||||
|
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
|
||||||
|
|
||||||
|
- **Problem**: TenantPilot needs a canonical Coverage v2 denominator and claim-safety model before it can safely replace active coverage/gap semantics.
|
||||||
|
- **Today's failure**: The full cutover cannot be safely implemented in one loop without creating partial dual truth.
|
||||||
|
- **User-visible improvement**: This spec does not change user-visible surfaces. It creates tested kernel truth so later specs can activate Coverage v2 without inventing persistence and claim rules at the same time.
|
||||||
|
- **Smallest enterprise-capable version**: Value families, resource type registry, supported scopes, source-class defaults, claim guard, provider provenance rules, and focused tests.
|
||||||
|
- **Explicit non-goals**: No UI cutover, no customer-facing claims, no OperationRun-backed capture/evaluation, no TCM/Graph remote capture, no generic content-backed evidence capture, no legacy runtime deletion, no browser proof unless UI changes are introduced by amendment.
|
||||||
|
- **Permanent complexity imported**: Coverage v2 enum/value families, minimal persistence, registry/scope/claim services, seed/default registry entries, and tests.
|
||||||
|
- **Why now**: Later activation cannot safely block overclaiming unless the kernel rules exist first.
|
||||||
|
- **Why not local**: A local label patch would preserve the old denominator and would not enforce source class, supported scope, beta exclusion, fallback inclusion, provider provenance, or claim boundaries.
|
||||||
|
- **Approval class**: Core Enterprise.
|
||||||
|
- **Red flags triggered**: New persisted truth, new enum/status families, registry/resolver/claim guard services. Defense: the kernel is inactive, bounded, and intentionally avoids UI/runtime activation.
|
||||||
|
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 1 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 11/12**
|
||||||
|
- **Decision**: approve as an inactive kernel slice.
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
TenantPilot needs a Coverage v2 kernel that can answer:
|
||||||
|
|
||||||
|
> Which tenant configuration resource type is in supported scope, from which source class, at what minimum coverage level, and what claims are allowed?
|
||||||
|
|
||||||
|
Spec 414 must not yet answer that question on customer-facing or active operator surfaces. It must only create the tested kernel that later activation specs can use.
|
||||||
|
|
||||||
|
## Business / Product Value
|
||||||
|
|
||||||
|
- Prevents later Coverage v2 activation from mixing persistence, claim rules, UI migration, and legacy removal in one unsafe change.
|
||||||
|
- Establishes source-class and supported-scope truth before customer claims are rendered.
|
||||||
|
- Blocks unsafe claim semantics such as unscoped 100%, beta certification, unsupported restore-readiness, and incomplete-scope claims at the kernel level.
|
||||||
|
- Preserves the hard-cutover strategy without adding legacy compatibility shims.
|
||||||
|
|
||||||
|
## Primary Users / Operators
|
||||||
|
|
||||||
|
- Release reviewer validating that Coverage v2 is safe to activate later.
|
||||||
|
- Platform engineer implementing Coverage v2 persistence and claim guard.
|
||||||
|
- MSP/operator and customer-safe reviewers are downstream beneficiaries, but no visible workflow changes in this slice.
|
||||||
|
|
||||||
|
## Spec Scope Fields *(mandatory)*
|
||||||
|
|
||||||
|
- **Scope**: inactive Coverage v2 kernel under workspace and managed-environment boundaries.
|
||||||
|
- **Primary Routes**: none. No reachable runtime UI surface is changed by default.
|
||||||
|
- **Data Ownership**: Coverage v2 kernel ownership truth is `workspace_id` plus `managed_environment_id` where environment-owned records are introduced. `provider_connection_id` is used where provider-sourced provenance, permission context, capture source, provider isolation, or execution identity depends on a concrete provider connection.
|
||||||
|
- **RBAC**: No new UI or mutation endpoint is introduced by default. The platform-seeded definition models are not exposed through a route, Filament resource, API, or mutation surface in this inactive slice, so no policy is introduced in Spec 414. Any later activation or reachable surface must add explicit policy/authorization coverage before exposure. If implementation unexpectedly adds an operational command/action, stop and update spec/plan/tasks before continuing.
|
||||||
|
|
||||||
|
For canonical-view specs:
|
||||||
|
|
||||||
|
- **Default filter behavior when tenant-context is active**: N/A - no canonical view or rendered surface is introduced.
|
||||||
|
- **Explicit entitlement checks preventing cross-tenant leakage**: Kernel persistence and tests must enforce workspace and managed-environment scope. Same-scope provider connection validation is required when `provider_connection_id` is stored.
|
||||||
|
|
||||||
|
## Ownership Correction
|
||||||
|
|
||||||
|
Coverage v2 required internal scope fields use:
|
||||||
|
|
||||||
|
- `workspace_id`
|
||||||
|
- `managed_environment_id`
|
||||||
|
- `provider_connection_id` where provider-sourced or provenance-specific
|
||||||
|
|
||||||
|
Coverage v2 must not introduce `tenant_id` as an ownership key, compatibility alias, fallback reader, dual-write target, or parallel truth.
|
||||||
|
|
||||||
|
Provider-native external IDs such as Microsoft tenant ID, Entra tenant ID, directory ID, subscription ID, account ID, or provider tenant ID are provider/source metadata only. They must not become Coverage v2 internal ownership keys.
|
||||||
|
|
||||||
|
When `provider_connection_id` is stored on a Coverage v2 record, it must belong to the same `workspace_id` and `managed_environment_id` as the record.
|
||||||
|
|
||||||
|
The required kernel definition tables in Spec 414 are platform-seeded definitions, not environment-owned operational observations:
|
||||||
|
|
||||||
|
- `tenant_configuration_resource_types` MUST NOT store `workspace_id`, `managed_environment_id`, or `provider_connection_id`.
|
||||||
|
- `tenant_configuration_supported_scopes` MUST NOT store `workspace_id`, `managed_environment_id`, or `provider_connection_id`.
|
||||||
|
- Workspace-specific overrides, environment-specific denominators, and provider-connection-specific definitions are out of scope for Spec 414.
|
||||||
|
|
||||||
|
If optional concrete resource or evidence tables are added in Spec 414, `provider_connection_id` is required for provider-observed rows with `source_class` values `tcm`, `graph_v1_fallback`, or `graph_beta_experimental`. It is nullable only for explicitly non-provider kernel rows approved by this spec. Any stored `provider_connection_id` must match the record's `workspace_id` and `managed_environment_id`; evidence rows must not point to a different provider connection than their concrete resource unless the implementation report records a provider-independent evidence rationale.
|
||||||
|
|
||||||
|
## No Legacy / No Backward Compatibility Constraint *(mandatory)*
|
||||||
|
|
||||||
|
TenantPilot is pre-production unless this spec explicitly records a compatibility exception.
|
||||||
|
|
||||||
|
- **Compatibility posture**: inactive kernel plus later hard cutover.
|
||||||
|
- **Legacy aliases, fallback readers, hidden routes, duplicate UI, old labels, or historical fixtures kept?**: no new compatibility behavior. Existing v1 runtime remains active only because this spec does not activate v2 yet.
|
||||||
|
- **Why clean replacement is safe now**: Spec 414 does not remove active v1 runtime. It prepares the kernel for a later explicit cutover spec that will remove/replace legacy behavior under its own validation scope.
|
||||||
|
|
||||||
|
Forbidden as Coverage v2 kernel concepts:
|
||||||
|
|
||||||
|
- `policy_record_missing`
|
||||||
|
- `foundation_not_policy_backed`
|
||||||
|
- `meta_fallback`
|
||||||
|
- `ambiguous_match`
|
||||||
|
- `raw_gap_count`
|
||||||
|
- `primary_gap_count`
|
||||||
|
- `tenant_id` as Coverage v2 internal ownership truth
|
||||||
|
|
||||||
|
## UI Surface Impact *(mandatory - UI-COV-001)*
|
||||||
|
|
||||||
|
Does this spec add, remove, rename, or materially change any reachable UI surface?
|
||||||
|
|
||||||
|
- [x] No UI surface impact
|
||||||
|
- [ ] Existing page changed
|
||||||
|
- [ ] New page/route added
|
||||||
|
- [ ] Navigation changed
|
||||||
|
- [ ] Filament panel/provider surface changed
|
||||||
|
- [ ] New modal/drawer/wizard/action added
|
||||||
|
- [ ] New table/form/state added
|
||||||
|
- [ ] Customer-facing surface changed
|
||||||
|
- [ ] Dangerous action changed
|
||||||
|
- [ ] Status/evidence/review presentation changed
|
||||||
|
- [ ] Workspace/environment context presentation changed
|
||||||
|
|
||||||
|
## UI/Productization Coverage
|
||||||
|
|
||||||
|
N/A - no reachable UI surface impact.
|
||||||
|
|
||||||
|
If any UI file is changed during implementation, implementation must stop and update this spec with Product Surface Impact, affected routes, page archetype, browser proof, and Human Product Sanity criteria before continuing.
|
||||||
|
|
||||||
|
## Product Surface Impact
|
||||||
|
|
||||||
|
- **Product Surface Contract applies?**: no runtime UI surface impact expected.
|
||||||
|
- **Page archetype**: N/A.
|
||||||
|
- **Primary user question**: N/A.
|
||||||
|
- **Primary action**: N/A.
|
||||||
|
- **Surface budget result**: N/A.
|
||||||
|
- **Technical Annex / deep-link demotion**: N/A.
|
||||||
|
- **Canonical status vocabulary**: N/A for rendered UI. Kernel claim states are domain values, not product-facing labels in this slice.
|
||||||
|
- **Visible complexity impact**: N/A - no rendered product surface changed.
|
||||||
|
- **Product Surface exceptions**: none.
|
||||||
|
|
||||||
|
## Browser Verification Plan *(mandatory)*
|
||||||
|
|
||||||
|
- **Browser proof required?**: no.
|
||||||
|
- **No-browser rationale**: N/A - no rendered UI surface changed.
|
||||||
|
- **Focused path when required**: N/A unless implementation changes a reachable UI surface after spec amendment.
|
||||||
|
- **Primary interaction to execute**: N/A.
|
||||||
|
- **Console, Livewire, Filament, network, and 500-error checks**: N/A.
|
||||||
|
- **Full-suite failure triage**: N/A.
|
||||||
|
|
||||||
|
## Human Product Sanity Check *(mandatory)*
|
||||||
|
|
||||||
|
- **Required?**: no.
|
||||||
|
- **No-human-sanity rationale**: N/A - no rendered product surface changed.
|
||||||
|
- **Reviewer questions**: workflow sanity only; confirm the slice stays inactive and does not create customer-facing dual truth.
|
||||||
|
- **Planned result location**: implementation report.
|
||||||
|
|
||||||
|
## Product Surface Merge Gate Checklist *(mandatory)*
|
||||||
|
|
||||||
|
- [x] No-legacy posture or approved exception recorded.
|
||||||
|
- [x] Product Surface Impact is completed or `N/A` is justified.
|
||||||
|
- [x] Browser proof is completed or `N/A - no rendered UI surface changed` is justified.
|
||||||
|
- [x] Human Product Sanity is completed or not applicable with rationale.
|
||||||
|
- [x] Product Surface exceptions are documented or `none`.
|
||||||
|
- [x] Implementation report will state Livewire v4 compliance, provider registration location, global search posture, destructive/high-impact action posture, asset strategy, tests/browser result, deployment impact, and visible complexity outcome.
|
||||||
|
|
||||||
|
## Cross-Cutting / Shared Pattern Reuse
|
||||||
|
|
||||||
|
- **Cross-cutting feature?**: yes, but kernel-only.
|
||||||
|
- **Interaction class(es)**: none rendered. Future specs will touch status messaging, evidence/report viewers, restore readiness, and operator surfaces.
|
||||||
|
- **Systems touched**: kernel persistence and domain services only.
|
||||||
|
- **Existing pattern(s) to extend**: existing Laravel models, migrations, services, factories, Pest tests, PostgreSQL schema conventions.
|
||||||
|
- **Shared contract / presenter / builder / renderer to reuse**: no UI renderer. If status-like badges are later introduced, use `BadgeCatalog` / `BadgeRenderer` in the activation spec.
|
||||||
|
- **Why the existing shared path is sufficient or insufficient**: Existing rendering paths are out of scope. Existing v1 coverage paths are insufficient as v2 truth, but they remain active until later cutover.
|
||||||
|
- **Allowed deviation and why**: new Coverage v2 kernel namespace is allowed because current-release claim safety needs a separate kernel before activation.
|
||||||
|
- **Consistency impact**: later specs must consume the kernel and must not invent parallel claim models.
|
||||||
|
- **Review focus**: prevent v2 from being wired into product surfaces, prevent `tenant_id`, prevent compatibility shims.
|
||||||
|
|
||||||
|
## OperationRun UX Impact
|
||||||
|
|
||||||
|
- **Touches OperationRun start/completion/link UX?**: no by default.
|
||||||
|
- **Shared OperationRun UX contract/layer reused**: N/A.
|
||||||
|
- **Delegated start/completion UX behaviors**: N/A.
|
||||||
|
- **Local surface-owned behavior that remains**: N/A.
|
||||||
|
- **Queued DB-notification policy**: N/A.
|
||||||
|
- **Terminal notification path**: N/A.
|
||||||
|
- **Exception required?**: none.
|
||||||
|
|
||||||
|
No remote TCM/Graph capture is implemented in Spec 414. No OperationRun-producing workflow is introduced unless a minimal registry/scope evaluation command is added after spec amendment. If any command/job/run is added and can be operationally relevant or asynchronous, it must follow the OperationRun contract. OperationRun-backed capture/evaluation is deferred to Generic Content-Backed Capture.
|
||||||
|
|
||||||
|
## Provider Boundary / Platform Core Check
|
||||||
|
|
||||||
|
- **Shared provider/platform boundary touched?**: yes.
|
||||||
|
- **Boundary classification**: mixed. Coverage v2 core terms are platform-core; Microsoft TCM/Graph source details are provider-owned source metadata.
|
||||||
|
- **Seams affected**: resource type taxonomy, source class, supported scope, provider provenance, claim labels.
|
||||||
|
- **Neutral platform terms preserved or introduced**: provider, source class, resource type, resource class, supported scope, coverage level, evidence state, identity state, claim state, managed environment.
|
||||||
|
- **Provider-specific semantics retained and why**: `tcm`, `graph_v1_fallback`, and `graph_beta_experimental` are retained because this kernel is TCM-first and must classify Microsoft fallback/beta source posture.
|
||||||
|
- **Why this does not deepen provider coupling accidentally**: source class is kernel metadata; provider-native tenant/directory/account IDs remain source metadata, not platform-core ownership keys.
|
||||||
|
- **Follow-up path**: document-in-feature for contained Microsoft source classes; follow-up specs for broader provider mapping.
|
||||||
|
|
||||||
|
## UI / Surface Guardrail Impact
|
||||||
|
|
||||||
|
N/A - no operator-facing surface change.
|
||||||
|
|
||||||
|
## Decision-First Surface Role
|
||||||
|
|
||||||
|
N/A - no operator-facing surface change.
|
||||||
|
|
||||||
|
## Audience-Aware Disclosure
|
||||||
|
|
||||||
|
N/A - no rendered audience surface change.
|
||||||
|
|
||||||
|
## UI/UX Surface Classification
|
||||||
|
|
||||||
|
N/A - no rendered UI surface change.
|
||||||
|
|
||||||
|
## Operator Surface Contract
|
||||||
|
|
||||||
|
N/A - no rendered operator surface change.
|
||||||
|
|
||||||
|
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||||
|
|
||||||
|
- **New source of truth?**: yes, but inactive until later cutover.
|
||||||
|
- **New persisted entity/table/artifact?**: yes. Required: `tenant_configuration_resource_types` and `tenant_configuration_supported_scopes`. Optional only if needed for tests or clean service boundaries: `tenant_configuration_resources` and `tenant_configuration_resource_evidence`.
|
||||||
|
- **New abstraction?**: yes. Resource type registry, supported-scope resolver, and claim guard.
|
||||||
|
- **New enum/state/reason family?**: yes. Source class, workload, resource class, support state, coverage level, evidence state, identity state, claim state, and restore tier only if needed by claim guard. Allowed values are fixed in this spec and must not be expanded during implementation without amending the artifacts first.
|
||||||
|
- **New cross-domain UI framework/taxonomy?**: no.
|
||||||
|
- **Current operator problem**: later customer claims cannot be safely activated without deterministic kernel boundaries.
|
||||||
|
- **Existing structure is insufficient because**: v1 coverage/gap semantics cannot express source class, supported-scope denominator, beta exclusion, fallback inclusion, provider provenance, or exact claim eligibility.
|
||||||
|
- **Narrowest correct implementation**: inactive value families, minimal persistence, initial registry, supported-scope contract, claim guard, unit/feature tests.
|
||||||
|
- **Ownership cost**: new domain namespace, persistence, factories, services, and test family.
|
||||||
|
- **Alternative intentionally rejected**: full cutover in one spec; v1-to-v2 compatibility mapping; UI-only relabeling.
|
||||||
|
- **Release truth**: current-release kernel needed before future activation.
|
||||||
|
|
||||||
|
### Compatibility posture
|
||||||
|
|
||||||
|
This feature assumes a pre-production environment.
|
||||||
|
|
||||||
|
Backward compatibility, legacy aliases, migration shims, historical fixtures, fallback readers, dual writes, and compatibility-specific tests are out of scope.
|
||||||
|
|
||||||
|
## Coverage v2 Kernel Persistence
|
||||||
|
|
||||||
|
Spec 414 must introduce only the minimum persistence required for the v2 kernel.
|
||||||
|
|
||||||
|
Required:
|
||||||
|
|
||||||
|
- `tenant_configuration_resource_types`
|
||||||
|
- `tenant_configuration_supported_scopes`
|
||||||
|
|
||||||
|
Optional only if needed for tests or clean service boundaries:
|
||||||
|
|
||||||
|
- `tenant_configuration_resources`
|
||||||
|
- `tenant_configuration_resource_evidence`
|
||||||
|
|
||||||
|
Deferred:
|
||||||
|
|
||||||
|
- `tenant_configuration_coverage_runs`
|
||||||
|
- full capture/evaluation run summaries
|
||||||
|
- append-only evidence history beyond minimal kernel needs
|
||||||
|
|
||||||
|
`tenant_configuration_resource_types` is a platform-seeded registry definition table. Required fields:
|
||||||
|
|
||||||
|
- `id`
|
||||||
|
- `canonical_type`
|
||||||
|
- `display_name`
|
||||||
|
- `description` nullable
|
||||||
|
- `source_class`
|
||||||
|
- `workload`
|
||||||
|
- `resource_class`
|
||||||
|
- `support_state`
|
||||||
|
- `default_coverage_level`
|
||||||
|
- `default_evidence_state`
|
||||||
|
- `default_identity_state`
|
||||||
|
- `default_claim_state`
|
||||||
|
- `restore_tier` nullable
|
||||||
|
- `allows_beta_claims` boolean default false
|
||||||
|
- `allows_graph_fallback_claims` boolean default false
|
||||||
|
- `allows_certified_claims` boolean default false
|
||||||
|
- `is_active` boolean default true
|
||||||
|
- `metadata` jsonb nullable
|
||||||
|
- `created_at`
|
||||||
|
- `updated_at`
|
||||||
|
|
||||||
|
`tenant_configuration_resource_types` uniqueness rules:
|
||||||
|
|
||||||
|
- `(canonical_type, source_class)` must be unique.
|
||||||
|
- Initial registry entries must be upsert-safe and deterministic; re-running seed/migration logic must not duplicate entries.
|
||||||
|
|
||||||
|
`tenant_configuration_supported_scopes` is a platform-seeded supported-scope definition table. Required fields:
|
||||||
|
|
||||||
|
- `id`
|
||||||
|
- `scope_key`
|
||||||
|
- `display_name`
|
||||||
|
- `description` nullable
|
||||||
|
- `minimum_coverage_level`
|
||||||
|
- `included_resource_types` jsonb
|
||||||
|
- `allow_beta` boolean default false
|
||||||
|
- `allow_graph_fallback` boolean default false
|
||||||
|
- `customer_claims_allowed` boolean default false
|
||||||
|
- `is_active` boolean default true
|
||||||
|
- `metadata` jsonb nullable
|
||||||
|
- `created_at`
|
||||||
|
- `updated_at`
|
||||||
|
|
||||||
|
`tenant_configuration_supported_scopes` uniqueness and denominator rules:
|
||||||
|
|
||||||
|
- `scope_key` must be unique.
|
||||||
|
- `included_resource_types` must contain canonical resource type identifiers from `tenant_configuration_resource_types`.
|
||||||
|
- Initial supported-scope definitions must be upsert-safe and deterministic.
|
||||||
|
- Workspace-specific or environment-specific scope overrides are out of scope for Spec 414.
|
||||||
|
|
||||||
|
If `tenant_configuration_resources` is kept in 414, required fields are:
|
||||||
|
|
||||||
|
- `id`
|
||||||
|
- `workspace_id`
|
||||||
|
- `managed_environment_id`
|
||||||
|
- `provider_connection_id` nullable/required according to source class and provenance
|
||||||
|
- `resource_type_id`
|
||||||
|
- `canonical_external_id` nullable
|
||||||
|
- `canonical_key`
|
||||||
|
- `canonical_key_kind`
|
||||||
|
- `source_key` nullable
|
||||||
|
- `display_name` nullable
|
||||||
|
- `source_class`
|
||||||
|
- `workload`
|
||||||
|
- `resource_class`
|
||||||
|
- `support_state`
|
||||||
|
- `latest_coverage_level`
|
||||||
|
- `latest_evidence_state`
|
||||||
|
- `identity_state`
|
||||||
|
- `claim_state`
|
||||||
|
- `first_seen_at` nullable
|
||||||
|
- `last_seen_at` nullable
|
||||||
|
- `tombstoned_at` nullable
|
||||||
|
- `created_at`
|
||||||
|
- `updated_at`
|
||||||
|
|
||||||
|
If `tenant_configuration_resource_evidence` is kept in 414, required fields are:
|
||||||
|
|
||||||
|
- `id`
|
||||||
|
- `tenant_configuration_resource_id`
|
||||||
|
- `provider_connection_id` nullable
|
||||||
|
- `source_class`
|
||||||
|
- `source_endpoint` nullable
|
||||||
|
- `source_version` nullable
|
||||||
|
- `schema_version` nullable
|
||||||
|
- `raw_payload` jsonb nullable
|
||||||
|
- `normalized_payload` jsonb nullable
|
||||||
|
- `payload_hash` nullable
|
||||||
|
- `permission_context` jsonb nullable
|
||||||
|
- `coverage_level`
|
||||||
|
- `evidence_state`
|
||||||
|
- `captured_at` nullable
|
||||||
|
- `created_at`
|
||||||
|
- `updated_at`
|
||||||
|
|
||||||
|
No Coverage v2 table may include `tenant_id`.
|
||||||
|
|
||||||
|
## Coverage v2 Kernel Value Families
|
||||||
|
|
||||||
|
Spec 414 fixes these allowed values for the inactive kernel. Implementation must not add extra values without amending spec, plan, and tasks first.
|
||||||
|
|
||||||
|
- **SourceClass**: `tcm`, `graph_v1_fallback`, `graph_beta_experimental`
|
||||||
|
- **Workload**: `intune`
|
||||||
|
- **ResourceClass**: `configuration`
|
||||||
|
- **SupportState**: `supported`, `fallback_supported`, `experimental`, `unsupported`, `out_of_scope`
|
||||||
|
- **CoverageLevel**: `detected`, `content_backed`, `comparable`, `renderable`, `restorable`, `certified`
|
||||||
|
- **EvidenceState**: `not_captured`, `captured`, `content_backed`, `permission_blocked`, `source_unavailable`, `schema_unknown`, `capture_failed`
|
||||||
|
- **IdentityState**: `stable`, `derived`, `identity_conflict`, `missing_external_id`, `unsupported_identity`
|
||||||
|
- **ClaimState**: `claim_allowed`, `claim_limited`, `claim_blocked`, `internal_only`
|
||||||
|
- **RestoreTier** *(only if implemented in this slice)*: `not_restorable`, `preview_only`, `restorable`
|
||||||
|
|
||||||
|
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||||
|
|
||||||
|
- **Test purpose / classification**: Unit for value families, registry, supported-scope resolver, claim guard; Feature for persistence seed/defaults, same-scope provider connection validation where applicable, and no-`tenant_id` schema proof.
|
||||||
|
- **Validation lane(s)**: fast-feedback, confidence, pgsql when migrations use JSONB/composite constraints.
|
||||||
|
- **Why this classification and these lanes are sufficient**: The narrowed spec changes kernel persistence and service behavior only. Browser and heavy UI lanes are not required unless a future amendment adds rendered UI.
|
||||||
|
- **New or expanded test families**: `tests/Unit/Support/TenantConfiguration` and `tests/Feature/TenantConfiguration`.
|
||||||
|
- **Fixture / helper cost impact**: New factories must stay explicit and opt-in.
|
||||||
|
- **Heavy-family visibility / justification**: no new heavy-governance family by default.
|
||||||
|
- **Special surface test profile**: `N/A - no rendered UI surface changed`.
|
||||||
|
- **Standard-native relief or required special coverage**: no browser proof required.
|
||||||
|
- **Reviewer handoff**: verify inactive kernel, no UI wiring, no compatibility shims, no `tenant_id`, correct source classes, beta claim blocking, Graph fallback classification, exact-scope claim guard, and provider connection same-scope rules.
|
||||||
|
- **Budget / baseline / trend impact**: document in implementation report if new tests materially increase runtime.
|
||||||
|
- **Escalation needed**: document-in-feature.
|
||||||
|
- **Active feature PR close-out entry**: Kernel / No UI / No dual truth.
|
||||||
|
- **Planned validation commands**:
|
||||||
|
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
|
||||||
|
- focused unit tests for `tests/Unit/Support/TenantConfiguration`
|
||||||
|
- focused feature tests for `tests/Feature/TenantConfiguration`
|
||||||
|
- PostgreSQL-focused tests when schema constraints or JSONB require PostgreSQL proof
|
||||||
|
- `git diff --check`
|
||||||
|
|
||||||
|
## User Scenarios & Testing *(mandatory)*
|
||||||
|
|
||||||
|
### User Story 1 - Establish Inactive Coverage v2 Registry (Priority: P1)
|
||||||
|
|
||||||
|
As a release reviewer, I need the initial Coverage v2 resource type registry to exist with correct source classes and support defaults so later activation starts from explicit supported-scope truth.
|
||||||
|
|
||||||
|
**Why this priority**: The registry is the denominator for all future claims.
|
||||||
|
|
||||||
|
**Independent Test**: Assert the required initial resource type entries exist and classify TCM, Graph fallback, and beta experimental resources correctly.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** the kernel registry is seeded, **When** entries are queried, **Then** all required initial resource types exist with expected source class and support state.
|
||||||
|
2. **Given** `notificationMessageTemplate`, **When** registry metadata is inspected, **Then** it is classified as `graph_v1_fallback`.
|
||||||
|
3. **Given** `roleScopeTag`, **When** registry metadata is inspected, **Then** it is classified as `graph_beta_experimental` and cannot be certified by default.
|
||||||
|
|
||||||
|
### User Story 2 - Define Supported Scope Contract (Priority: P1)
|
||||||
|
|
||||||
|
As a platform engineer, I need supported scopes to define explicit denominators and minimum coverage levels so claims cannot be made against ambiguous or unscoped sets.
|
||||||
|
|
||||||
|
**Why this priority**: Claim guard behavior depends on exact scope and level boundaries.
|
||||||
|
|
||||||
|
**Independent Test**: Resolve a supported scope and assert denominator membership, beta exclusion by default, Graph fallback inclusion only when allowed, and minimum coverage level enforcement.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a supported scope, **When** its denominator is resolved, **Then** only explicitly included resource types count toward the scope.
|
||||||
|
2. **Given** beta experimental resource types, **When** a default supported scope is resolved, **Then** those resource types are excluded unless the scope explicitly allows beta.
|
||||||
|
3. **Given** Graph fallback resource types, **When** a supported scope does not allow fallback, **Then** those resource types do not count toward allowed claims.
|
||||||
|
|
||||||
|
### User Story 3 - Block Unsafe Claims in Kernel (Priority: P1)
|
||||||
|
|
||||||
|
As a release reviewer, I need claim guard rules to block unsafe coverage statements before any UI activation occurs.
|
||||||
|
|
||||||
|
**Why this priority**: Customer-facing activation must not be possible without safe claim rules.
|
||||||
|
|
||||||
|
**Independent Test**: Exercise claim guard inputs and assert unsafe claims are blocked while exact scope + level claims are allowed.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** an unscoped 100% claim, **When** the claim guard evaluates it, **Then** the claim is blocked.
|
||||||
|
2. **Given** a beta resource type, **When** certified coverage is requested, **Then** the claim is blocked by default.
|
||||||
|
3. **Given** an incomplete supported scope, **When** a customer-facing claim is requested, **Then** the claim is blocked.
|
||||||
|
4. **Given** a complete exact scope at the required level, **When** the claim guard evaluates it, **Then** the exact claim may be allowed.
|
||||||
|
|
||||||
|
### User Story 4 - Preserve Inactive Kernel Boundary (Priority: P2)
|
||||||
|
|
||||||
|
As an implementation reviewer, I need proof that Coverage v2 was not wired into customer/operator product surfaces as a parallel truth.
|
||||||
|
|
||||||
|
**Why this priority**: The kernel fails if it becomes a partial second truth model before activation.
|
||||||
|
|
||||||
|
**Independent Test**: Review changed files and focused tests; assert no UI routes/resources/pages are changed and no v1-to-v2 adapter, dual-write, or fallback reader exists.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** Spec 414 implementation is complete, **When** changed files are reviewed, **Then** no reachable UI surface changed unless the spec was amended first.
|
||||||
|
2. **Given** v1 runtime still exists, **When** Coverage v2 services are inspected, **Then** no compatibility adapter translates old v1 gap taxonomy into v2 truth.
|
||||||
|
3. **Given** Coverage v2 persistence exists, **When** schema is inspected, **Then** no Coverage v2 ownership field uses `tenant_id`.
|
||||||
|
|
||||||
|
## Requirements *(mandatory)*
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
|
||||||
|
- **FR-414-001**: TenantPilot MUST introduce an inactive Coverage v2 kernel and MUST NOT make it the active customer-facing or operator-facing coverage truth in this spec.
|
||||||
|
- **FR-414-002**: The kernel MUST define `SourceClass`, `Workload`, `ResourceClass`, `SupportState`, `CoverageLevel`, `EvidenceState`, `IdentityState`, and `ClaimState`.
|
||||||
|
- **FR-414-003**: The kernel MAY define `RestoreTier` only if claim guard behavior requires restore-readiness boundaries in this slice.
|
||||||
|
- **FR-414-004**: The kernel MUST persist `tenant_configuration_resource_types` and `tenant_configuration_supported_scopes`.
|
||||||
|
- **FR-414-005**: The kernel MAY persist `tenant_configuration_resources` and `tenant_configuration_resource_evidence` only if needed for tests or clean service boundaries.
|
||||||
|
- **FR-414-006**: The kernel MUST NOT require `tenant_configuration_coverage_runs` in Spec 414.
|
||||||
|
- **FR-414-007**: The initial registry MUST include `deviceAndAppManagementAssignmentFilter`, `deviceEnrollmentLimitRestriction`, `deviceEnrollmentPlatformRestriction`, `deviceEnrollmentStatusPageWindows10`, `appProtectionPolicyAndroid`, `appProtectionPolicyiOS`, `notificationMessageTemplate`, and `roleScopeTag`.
|
||||||
|
- **FR-414-008**: TCM-aligned Intune entries MUST use `source_class = tcm`.
|
||||||
|
- **FR-414-009**: `notificationMessageTemplate` MUST use `source_class = graph_v1_fallback`.
|
||||||
|
- **FR-414-010**: `roleScopeTag` MUST use `source_class = graph_beta_experimental`.
|
||||||
|
- **FR-414-011**: Supported scopes MUST define explicit denominator membership and required minimum coverage level.
|
||||||
|
- **FR-414-012**: Supported scopes MUST exclude beta experimental resource types by default.
|
||||||
|
- **FR-414-013**: Supported scopes MUST include Graph fallback resource types only when the scope explicitly allows fallback.
|
||||||
|
- **FR-414-014**: Claim guard MUST block unscoped 100% claims.
|
||||||
|
- **FR-414-015**: Claim guard MUST block certified claims for beta experimental resource types by default.
|
||||||
|
- **FR-414-016**: Claim guard MUST block restore claims when the resource type is not restorable.
|
||||||
|
- **FR-414-017**: Claim guard MUST block customer-facing claims when supported scope is incomplete.
|
||||||
|
- **FR-414-018**: Claim guard MUST allow only exact scope + level claims.
|
||||||
|
- **FR-414-019**: Coverage v2 MUST NOT adapt, translate, dual-write, or fallback-read legacy v1 truth.
|
||||||
|
- **FR-414-020**: Coverage v2 MUST NOT use old v1 gap taxonomy as v2 runtime truth.
|
||||||
|
- **FR-414-021**: Coverage v2 MUST NOT introduce `tenant_id` in required ownership fields.
|
||||||
|
- **FR-414-022**: When `provider_connection_id` is stored, the provider connection MUST belong to the same `workspace_id` and `managed_environment_id` as the Coverage v2 record.
|
||||||
|
- **FR-414-023**: Provider-native tenant, directory, subscription, account, or provider tenant IDs MUST remain provider/source metadata only.
|
||||||
|
- **FR-414-024**: No reachable UI surface may change in this spec unless spec/plan/tasks are amended first with Product Surface Impact, browser proof, and Human Product Sanity criteria.
|
||||||
|
- **FR-414-025**: Required kernel definition tables MUST be platform-seeded, deterministic, upsert-safe definitions and MUST NOT include workspace, managed-environment, provider-connection, or tenant ownership columns.
|
||||||
|
- **FR-414-026**: Required kernel value families MUST use only the allowed values listed in `Coverage v2 Kernel Value Families`.
|
||||||
|
|
||||||
|
### Non-Functional Requirements
|
||||||
|
|
||||||
|
- **NFR-414-001**: Kernel persistence MUST follow PostgreSQL-first schema expectations, including JSONB only where JSON payload storage is actually introduced.
|
||||||
|
- **NFR-414-002**: New factories, helpers, seeds, and fixtures MUST stay opt-in and must not widen default test setup.
|
||||||
|
- **NFR-414-003**: No remote TCM, Graph, or provider calls may occur during this spec.
|
||||||
|
- **NFR-414-004**: No OperationRun-producing workflow is introduced unless this spec is amended first.
|
||||||
|
- **NFR-414-005**: The implementation report MUST record no-legacy confirmation, no-dual-truth confirmation, no-`tenant_id` proof, provider provenance proof, tests, browser N/A, Livewire v4 compliance, provider registration location, global search posture, destructive/high-impact action posture, asset strategy, and deployment impact.
|
||||||
|
|
||||||
|
### UI Action Matrix
|
||||||
|
|
||||||
|
N/A - no Filament UI surface is changed.
|
||||||
|
|
||||||
|
### Key Entities
|
||||||
|
|
||||||
|
- **Tenant Configuration Resource Type**: Kernel registry entry describing canonical resource type, source class, workload, resource class, support state, default coverage level, beta/fallback flags, restore tier if needed, and claim defaults.
|
||||||
|
- **Tenant Configuration Supported Scope**: Named claim denominator with explicit included resource types, minimum coverage level, beta/fallback inclusion rules, and customer-claim eligibility.
|
||||||
|
- **Tenant Configuration Resource** *(optional in Spec 414)*: Concrete observed resource scoped by workspace and managed environment, with provider connection provenance when required.
|
||||||
|
- **Tenant Configuration Resource Evidence** *(optional in Spec 414)*: Minimal evidence record for kernel tests only if needed; full append-only evidence history is deferred.
|
||||||
|
|
||||||
|
## Success Criteria *(mandatory)*
|
||||||
|
|
||||||
|
### Measurable Outcomes
|
||||||
|
|
||||||
|
- **SC-414-001**: Focused tests prove the initial required registry entries exist with correct source classes.
|
||||||
|
- **SC-414-002**: Focused tests prove beta experimental resources cannot produce certified claims by default.
|
||||||
|
- **SC-414-003**: Focused tests prove Graph fallback is represented through `graph_v1_fallback` source class.
|
||||||
|
- **SC-414-004**: Focused tests prove supported scopes use explicit denominators and minimum coverage levels.
|
||||||
|
- **SC-414-005**: Focused tests prove unscoped 100%, incomplete-scope, beta-certified, and non-restorable restore claims are blocked.
|
||||||
|
- **SC-414-006**: Schema or migration tests prove no Coverage v2 ownership field uses `tenant_id`.
|
||||||
|
- **SC-414-007**: Review of changed files proves no reachable UI surface was changed unless the spec was amended first.
|
||||||
|
- **SC-414-008**: Validation proves no compatibility adapter, dual-write target, fallback reader, or v1-to-v2 translator was added.
|
||||||
|
- **SC-414-009**: Unit/feature tests and `git diff --check` pass.
|
||||||
|
- **SC-414-010**: Focused tests or schema checks prove required kernel definition tables are deterministic, upsert-safe, globally seeded definitions and not workspace/environment/provider-connection-owned records.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
PASS requires:
|
||||||
|
|
||||||
|
- Coverage v2 kernel exists.
|
||||||
|
- Registry and supported scope persistence exist.
|
||||||
|
- Initial required resource types are registered.
|
||||||
|
- Source classes are correct.
|
||||||
|
- No `tenant_id` exists in Coverage v2 required ownership fields.
|
||||||
|
- Provider-native tenant IDs are metadata only.
|
||||||
|
- Claim Guard blocks unsafe/unscoped claims.
|
||||||
|
- Beta cannot be certified by default.
|
||||||
|
- Graph fallback is source-classed.
|
||||||
|
- No UI surface is changed, or if changed, Product Surface Contract is completed before continuing.
|
||||||
|
- No legacy compatibility shim is added.
|
||||||
|
- No v1-to-v2 translator is added.
|
||||||
|
- No customer-facing dual truth is introduced.
|
||||||
|
- Unit/feature tests pass.
|
||||||
|
- `git diff --check` passes.
|
||||||
|
|
||||||
|
FAIL if:
|
||||||
|
|
||||||
|
- Coverage v2 becomes a parallel customer-facing truth.
|
||||||
|
- `tenant_id` is reintroduced.
|
||||||
|
- old gap taxonomy is reintroduced as v2 logic.
|
||||||
|
- compatibility adapter, dual-write, or fallback reader is added.
|
||||||
|
- full runtime cutover is attempted inside Spec 414.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
| Risk | Severity | Mitigation |
|
||||||
|
|---|---:|---|
|
||||||
|
| Kernel becomes partial customer-facing truth | High | No UI changes; stop-and-amend rule for any rendered surface |
|
||||||
|
| Persistence grows back into full cutover | High | Only resource types and supported scopes are required; concrete resource/evidence tables are optional |
|
||||||
|
| `tenant_id` reappears through old terminology | High | Schema/test proof and implementation report ownership reconciliation |
|
||||||
|
| Claim guard becomes too broad | Medium | Unit-test exact scoped decisions and keep generic provider framework out of scope |
|
||||||
|
| Future activation lacks needed capture data | Medium | Defer content-backed capture and identity engine to explicit follow-up specs |
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
|
||||||
|
- TenantPilot remains pre-production under LEAN-001.
|
||||||
|
- Existing v1 coverage remains active until a later activation/cutover spec.
|
||||||
|
- No runtime UI, route, navigation, action, browser, OperationRun, or remote provider work is needed for the kernel.
|
||||||
|
- Supply-chain remediation, if still open, remains a release/pilot gate outside this spec.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- None blocking preparation.
|
||||||
|
|
||||||
|
## Out Of Scope
|
||||||
|
|
||||||
|
- Full UI cutover.
|
||||||
|
- Evidence Overview conversion.
|
||||||
|
- Customer Review Workspace conversion.
|
||||||
|
- Review Pack/report conversion.
|
||||||
|
- Restore readiness conversion.
|
||||||
|
- Full baseline/compare conversion.
|
||||||
|
- Legacy runtime deletion.
|
||||||
|
- Old test suite rewrite across all surfaces.
|
||||||
|
- OperationRun-backed capture/evaluation.
|
||||||
|
- TCM snapshot ingestion.
|
||||||
|
- Graph fallback capture.
|
||||||
|
- Generic content-backed evidence capture.
|
||||||
|
- Canonical identity engine implementation.
|
||||||
|
- Browser proof across customer surfaces.
|
||||||
|
- Full Microsoft TCM resource catalog import.
|
||||||
|
- Complete 249+ resource list.
|
||||||
|
- Certified Intune pack.
|
||||||
|
- Actual 100% coverage closure.
|
||||||
|
|
||||||
|
## Follow-Up Spec Candidates
|
||||||
|
|
||||||
|
- Spec 415 - Generic Content-Backed Capture.
|
||||||
|
- Spec 416 - Canonical Identity Engine.
|
||||||
|
- Spec 417 - Coverage v2 Operator Surface.
|
||||||
|
- Spec 418 - Legacy Coverage Cutover & Removal.
|
||||||
|
- Spec 419 - Intune Core Comparable/Renderable Pack.
|
||||||
|
- Spec 420 - Certified Intune Core Coverage Pack.
|
||||||
|
- Spec 421 - Pilot Readiness Gate.
|
||||||
164
specs/414-tcm-first-coverage-core-cutover/tasks.md
Normal file
164
specs/414-tcm-first-coverage-core-cutover/tasks.md
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
# Tasks: Spec 414 - TCM-First Coverage v2 Kernel
|
||||||
|
|
||||||
|
**Input**: Design documents from `/specs/414-tcm-first-coverage-core-cutover/`
|
||||||
|
**Prerequisites**: `spec.md`, `plan.md`, user-provided patch prompt narrowing Spec 414 to an inactive kernel
|
||||||
|
|
||||||
|
**Tests**: Runtime behavior changes require Pest unit and feature tests. Browser tests are not required because this narrowed spec has no rendered UI surface impact unless implementation stops and amends the spec first.
|
||||||
|
|
||||||
|
## Test Governance Checklist
|
||||||
|
|
||||||
|
- [x] TGC001 Lane assignment is named and is the narrowest sufficient proof for kernel behavior.
|
||||||
|
- [x] TGC002 New or changed tests stay in unit/feature/pgsql lanes; no browser/heavy-governance family is introduced unless scope is amended.
|
||||||
|
- [x] TGC003 Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default; Coverage v2 setup is opt-in.
|
||||||
|
- [x] TGC004 Planned validation commands cover kernel behavior without hiding unrelated lane cost.
|
||||||
|
- [x] TGC005 Browser proof is explicitly `N/A - no rendered UI surface changed`.
|
||||||
|
- [x] TGC006 Product Surface implementation-report close-out records no UI impact, no dual truth, and no browser requirement.
|
||||||
|
- [x] TGC007 Material budget, baseline, trend, or escalation notes are recorded in the implementation report.
|
||||||
|
|
||||||
|
## Phase 1: Preparation And Guardrails
|
||||||
|
|
||||||
|
**Purpose**: Protect repo state and keep Spec 414 bounded to the inactive kernel.
|
||||||
|
|
||||||
|
- [x] T001 Capture current branch, HEAD, and `git status --short` in `specs/414-tcm-first-coverage-core-cutover/implementation-report.md`.
|
||||||
|
- [x] T002 Confirm `.specify/memory/constitution.md` ownership alignment: Coverage v2 uses `workspace_id`, `managed_environment_id`, and same-scope `provider_connection_id` where provider provenance is stored.
|
||||||
|
- [x] T003 Confirm the patched `spec.md`, `plan.md`, `tasks.md`, and `checklists/requirements.md` remove full-cutover scope and frame Spec 414 as inactive kernel only.
|
||||||
|
- [x] T004 Confirm changed files before implementation do not include runtime code outside active spec artifacts; if unrelated dirty files appear, stop before application changes.
|
||||||
|
- [x] T005 Confirm no reachable UI surface, Filament resource/page, navigation entry, browser proof, OperationRun-backed capture, remote TCM/Graph ingestion, legacy runtime deletion, or broad v1 test rewrite remains required by Spec 414.
|
||||||
|
|
||||||
|
## Phase 2: Tests First - Kernel Semantics
|
||||||
|
|
||||||
|
**Purpose**: Lock the kernel contract before implementation.
|
||||||
|
|
||||||
|
- [x] T006 [P] [US1] Add `apps/platform/tests/Unit/Support/TenantConfiguration/ResourceTypeRegistryTest.php` covering the required initial resource type entries and source classes.
|
||||||
|
- [x] T007 [P] [US2] Add `apps/platform/tests/Unit/Support/TenantConfiguration/SupportedScopeResolverTest.php` covering explicit denominator membership, required minimum coverage level, beta exclusion by default, fallback inclusion only when allowed, and no unscoped 100% claims.
|
||||||
|
- [x] T008 [P] [US3] Add `apps/platform/tests/Unit/Support/TenantConfiguration/ClaimGuardTest.php` covering unscoped 100% blocks, beta certification blocks, non-restorable restore claim blocks, incomplete supported-scope blocks, and exact scope + level allowance.
|
||||||
|
- [x] T009 [P] [US1] Add `apps/platform/tests/Unit/Support/TenantConfiguration/CoverageKernelValueTest.php` covering the exact allowed values for all kernel value families and ordering where ordering affects claim behavior.
|
||||||
|
|
||||||
|
## Phase 3: Tests First - Persistence And Ownership
|
||||||
|
|
||||||
|
**Purpose**: Prove persistence and ownership rules without activating product surfaces.
|
||||||
|
|
||||||
|
- [x] T010 [P] [US1] Add `apps/platform/tests/Feature/TenantConfiguration/TenantConfigurationResourceTypeRegistryTest.php` covering persisted or seeded required registry entries.
|
||||||
|
- [x] T011 [P] [US2] Add `apps/platform/tests/Feature/TenantConfiguration/TenantConfigurationSupportedScopeTest.php` covering persisted supported-scope denominator rules and minimum coverage levels.
|
||||||
|
- [x] T012 [P] [US3] Add `apps/platform/tests/Feature/TenantConfiguration/TenantConfigurationClaimGuardFeatureTest.php` proving claim guard blocks unsafe customer-facing claims without wiring into rendered UI.
|
||||||
|
- [x] T013 [P] [US4] Add `apps/platform/tests/Feature/TenantConfiguration/TenantConfigurationKernelSchemaTest.php` proving Coverage v2 ownership fields do not include `tenant_id` and provider-native tenant IDs remain metadata only.
|
||||||
|
- [x] T014 [P] [US4] Add PostgreSQL-focused coverage for JSONB fields, composite foreign keys, partial unique indexes, or same-scope `provider_connection_id` constraints if the Coverage v2 schema uses any of those PostgreSQL-specific behaviors.
|
||||||
|
|
||||||
|
## Phase 4: Kernel Value Families
|
||||||
|
|
||||||
|
**Purpose**: Add the minimal domain vocabulary needed by registry, scope, and claim guard.
|
||||||
|
|
||||||
|
- [x] T015 [US1] Create `apps/platform/app/Support/TenantConfiguration/SourceClass.php` with exactly `tcm`, `graph_v1_fallback`, and `graph_beta_experimental`.
|
||||||
|
- [x] T016 [US1] Create `apps/platform/app/Support/TenantConfiguration/Workload.php` with exactly `intune`.
|
||||||
|
- [x] T017 [US1] Create `apps/platform/app/Support/TenantConfiguration/ResourceClass.php` with exactly `configuration`.
|
||||||
|
- [x] T018 [US1] Create `apps/platform/app/Support/TenantConfiguration/SupportState.php` with exactly `supported`, `fallback_supported`, `experimental`, `unsupported`, and `out_of_scope`.
|
||||||
|
- [x] T019 [US1] Create `apps/platform/app/Support/TenantConfiguration/CoverageLevel.php` with exactly `detected`, `content_backed`, `comparable`, `renderable`, `restorable`, and `certified`.
|
||||||
|
- [x] T020 [US1] Create `apps/platform/app/Support/TenantConfiguration/EvidenceState.php` with exactly `not_captured`, `captured`, `content_backed`, `permission_blocked`, `source_unavailable`, `schema_unknown`, and `capture_failed`.
|
||||||
|
- [x] T021 [US1] Create `apps/platform/app/Support/TenantConfiguration/IdentityState.php` with exactly `stable`, `derived`, `identity_conflict`, `missing_external_id`, and `unsupported_identity`.
|
||||||
|
- [x] T022 [US3] Create `apps/platform/app/Support/TenantConfiguration/ClaimState.php` with exactly `claim_allowed`, `claim_limited`, `claim_blocked`, and `internal_only`.
|
||||||
|
- [x] T023 [US3] Create `apps/platform/app/Support/TenantConfiguration/RestoreTier.php` with exactly `not_restorable`, `preview_only`, and `restorable` only if restore-claim blocking cannot stay local to `ClaimGuard`.
|
||||||
|
|
||||||
|
## Phase 5: Minimal Kernel Persistence
|
||||||
|
|
||||||
|
**Purpose**: Persist only the required kernel truth.
|
||||||
|
|
||||||
|
- [x] T024 [US1] Create migration(s) under `apps/platform/database/migrations/` for `tenant_configuration_resource_types` and `tenant_configuration_supported_scopes`.
|
||||||
|
- [x] T025 [US4] Ensure required Coverage v2 kernel tables do not include `tenant_id`.
|
||||||
|
- [x] T026 [US4] Ensure required Coverage v2 kernel definition tables do not include `workspace_id`, `managed_environment_id`, or `provider_connection_id`; they are platform-seeded definitions.
|
||||||
|
- [x] T027 [US4] Ensure environment-owned optional tables, if added, include non-null `workspace_id` and `managed_environment_id`, require `provider_connection_id` for provider-observed `tcm`, `graph_v1_fallback`, or `graph_beta_experimental` rows, and validate that any stored provider connection belongs to the same workspace and managed environment. Not applicable in this slice because optional environment-owned tables were deferred.
|
||||||
|
- [x] T028 [US1] Create `apps/platform/app/Models/TenantConfigurationResourceType.php` and `apps/platform/app/Models/TenantConfigurationSupportedScope.php` with casts and relationships following sibling model conventions.
|
||||||
|
- [x] T029 [US1] Create factories under `apps/platform/database/factories/` for required kernel models with explicit workspace/managed-environment setup only where required.
|
||||||
|
- [x] T030 [US1] Add optional `tenant_configuration_resources` and `tenant_configuration_resource_evidence` tables/models only if tests or clean service boundaries require them; otherwise record the deferral in `implementation-report.md`. Deferred in implementation report.
|
||||||
|
|
||||||
|
## Phase 6: Initial Registry And Supported Scope
|
||||||
|
|
||||||
|
**Purpose**: Seed the required initial source-class definitions and exact supported-scope contract.
|
||||||
|
|
||||||
|
- [x] T031 [US1] Create `apps/platform/app/Services/TenantConfiguration/ResourceTypeRegistry.php` to load active resource type definitions without Graph/TCM/provider calls.
|
||||||
|
- [x] T032 [US1] Seed or migrate TCM-aligned Intune types: `deviceAndAppManagementAssignmentFilter`, `deviceEnrollmentLimitRestriction`, `deviceEnrollmentPlatformRestriction`, `deviceEnrollmentStatusPageWindows10`, `appProtectionPolicyAndroid`, and `appProtectionPolicyiOS` with `source_class = tcm`.
|
||||||
|
- [x] T033 [US1] Seed or migrate `notificationMessageTemplate` with `source_class = graph_v1_fallback`.
|
||||||
|
- [x] T034 [US1] Seed or migrate `roleScopeTag` with `source_class = graph_beta_experimental` and default beta/certification-blocking posture.
|
||||||
|
- [x] T035 [US2] Create `apps/platform/app/Services/TenantConfiguration/SupportedScopeResolver.php` to resolve explicit denominators, minimum coverage levels, beta exclusion, and fallback inclusion rules.
|
||||||
|
- [x] T036 [US2] Add initial supported-scope definitions in `tenant_configuration_supported_scopes` using deterministic `scope_key` values, JSONB `included_resource_types`, minimum coverage level, beta/fallback flags, and no broad Microsoft 365 or 249-resource catalog labels.
|
||||||
|
|
||||||
|
## Phase 7: Claim Guard
|
||||||
|
|
||||||
|
**Purpose**: Block unsafe claims before any UI activation exists.
|
||||||
|
|
||||||
|
- [x] T037 [US3] Create `apps/platform/app/Services/TenantConfiguration/ClaimGuard.php`.
|
||||||
|
- [x] T038 [US3] Implement unscoped 100% claim blocking in `ClaimGuard`.
|
||||||
|
- [x] T039 [US3] Implement certified-claim blocking for beta experimental resource types in `ClaimGuard`.
|
||||||
|
- [x] T040 [US3] Implement restore-claim blocking when the resource type is not restorable in `ClaimGuard`.
|
||||||
|
- [x] T041 [US3] Implement customer-facing claim blocking when the supported scope is incomplete in `ClaimGuard`.
|
||||||
|
- [x] T042 [US3] Implement exact scope + level allowance in `ClaimGuard`.
|
||||||
|
- [x] T043 [US4] Confirm `ClaimGuard` does not adapt, translate, fallback-read, or dual-write legacy v1 truth.
|
||||||
|
|
||||||
|
## Phase 8: Boundary Guards And No-UI Proof
|
||||||
|
|
||||||
|
**Purpose**: Preserve the inactive kernel boundary.
|
||||||
|
|
||||||
|
- [x] T044 [US4] Confirm no Filament page/resource, Blade view, Livewire component, route, navigation entry, customer report, review pack, restore readiness, evidence overview, or baseline/compare surface is changed.
|
||||||
|
- [x] T045 [US4] Confirm no browser test is required because no rendered UI surface changed; if a UI file changed, stop and amend `spec.md`, `plan.md`, and `tasks.md`.
|
||||||
|
- [x] T046 [US4] Confirm no OperationRun-producing command/job/action is added; if one is required, stop and amend spec/plan/tasks with OperationRun UX impact.
|
||||||
|
- [x] T047 [US4] Confirm no remote TCM/Graph/provider call path is introduced.
|
||||||
|
- [x] T048 [US4] Confirm no v1-to-v2 compatibility adapter, dual-write target, fallback reader, or old snapshot promotion path was added.
|
||||||
|
- [x] T049 [US4] Confirm old v1 gap taxonomy is not used as Coverage v2 logic.
|
||||||
|
|
||||||
|
## Phase 9: Close-Out And Validation
|
||||||
|
|
||||||
|
**Purpose**: Run focused proof and document implementation readiness.
|
||||||
|
|
||||||
|
- [x] T050 Complete `specs/414-tcm-first-coverage-core-cutover/implementation-report.md` with branch, HEAD, dirty state, files changed, kernel tables/models/services, optional table deferrals, no-`tenant_id` proof, provider metadata/provenance proof, no-legacy/no-dual-truth confirmation, no-UI/browser N/A, tests, Livewire v4 compliance, provider registration location, global search posture, destructive/high-impact action posture, asset strategy, deployment impact, and follow-up candidates.
|
||||||
|
- [x] T051 Run `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`.
|
||||||
|
- [x] T052 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/TenantConfiguration`.
|
||||||
|
- [x] T053 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantConfiguration`.
|
||||||
|
- [x] T054 Run `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest -c phpunit.pgsql.xml tests/Feature/TenantConfiguration` because Coverage v2 migrations add JSONB fields and PostgreSQL check constraints. The original `--filter=TenantConfiguration` command matched no tests in this repo.
|
||||||
|
- [x] T055 Run `git diff --check`.
|
||||||
|
- [x] T056 Confirm final `git status --short` contains only intended Spec 414 and implementation files.
|
||||||
|
|
||||||
|
## Dependencies & Execution Order
|
||||||
|
|
||||||
|
- Phase 1 blocks all implementation.
|
||||||
|
- Phases 2 and 3 test tasks should be written before or alongside implementation.
|
||||||
|
- Phase 4 value families must precede registry/scope/claim services.
|
||||||
|
- Phase 5 required persistence must precede feature tests that query persisted kernel definitions.
|
||||||
|
- Phase 6 registry/scope must precede claim guard feature behavior.
|
||||||
|
- Phase 7 claim guard must precede boundary and close-out proof.
|
||||||
|
- Phase 8 must pass before validation close-out.
|
||||||
|
|
||||||
|
## Parallel Opportunities
|
||||||
|
|
||||||
|
- T006-T009 can run in parallel.
|
||||||
|
- T010-T014 can run in parallel after migration/model shape is agreed.
|
||||||
|
- T015-T023 can be split by value-family file.
|
||||||
|
- T031-T034 can run in parallel after persistence exists.
|
||||||
|
- T038-T042 can run in parallel after `ClaimGuard` shape is defined.
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
### MVP First
|
||||||
|
|
||||||
|
1. Complete preflight.
|
||||||
|
2. Add value-family tests and value families.
|
||||||
|
3. Add required kernel persistence.
|
||||||
|
4. Add registry and supported-scope resolver.
|
||||||
|
5. Add claim guard.
|
||||||
|
6. Prove no UI/dual-truth/legacy compatibility path exists.
|
||||||
|
|
||||||
|
### Incremental Delivery
|
||||||
|
|
||||||
|
1. Land kernel persistence and registry.
|
||||||
|
2. Land supported-scope contract.
|
||||||
|
3. Land claim guard.
|
||||||
|
4. Land ownership/no-UI/no-legacy proof.
|
||||||
|
5. Land implementation report and validation.
|
||||||
|
|
||||||
|
### Stop Conditions
|
||||||
|
|
||||||
|
- A UI, route, navigation, report, review, restore, evidence, baseline, or browser change becomes necessary.
|
||||||
|
- OperationRun-backed capture/evaluation becomes necessary.
|
||||||
|
- Remote TCM/Graph/provider calls become necessary.
|
||||||
|
- Concrete resource/evidence tables expand beyond minimal kernel needs.
|
||||||
|
- `tenant_id` appears in Coverage v2 ownership fields.
|
||||||
|
- A v1 compatibility adapter, dual write, fallback reader, or old snapshot promotion path is needed.
|
||||||
|
- Old gap taxonomy is required as Coverage v2 logic.
|
||||||
Loading…
Reference in New Issue
Block a user