# Implementation Report: Spec 417 - Canonical Identity Engine ## Preflight - Branch: `417-canonical-identity-engine` - Starting HEAD: `332f6325 feat: add tenantpilot agent skill layer v1 (#483)` - Starting dirty state: `specs/417-canonical-identity-engine/` untracked as the active spec artifact set. - Dirty-state assessment: active Spec 417 preparation artifacts only; no runtime code was dirty before implementation. - Activated skills: `spec-kit-implementation-loop`, `pest-testing`, `tenantpilot-spec-readiness-gate`, `tenantpilot-workspace-scope-safety`, `tenantpilot-evidence-anchor-contract`, `tenantpilot-tcm-cutover-guard`. - Hard-gate stop conditions before implementation: none observed. Coverage v2 remains inactive; no UI/customer proof surface is in scope. ## Dependency Context - Spec 414: completed/validated context only; not modified. - Spec 415: completed/validated context only; not modified. - Existing canonical identity storage: `tenant_configuration_resources.canonical_resource_key` remains the single persisted canonical key truth. - Browser proof decision: `N/A - no rendered UI surface changed`. ## Implementation Summary - Added a bounded canonical identity engine for the initial eight Coverage v2 resource types. - Added `CanonicalKeyKind` values without display-name/name-only stable key kinds. - Added identity strategy, resolver, secondary-key, diagnostics, and same-scope conflict evaluation services under `App\Services\TenantConfiguration`. - Extended `tenant_configuration_resources` with additive identity metadata: `canonical_key_kind`, `identity_strategy`, `source_identity`, `secondary_identity_keys`, `identity_diagnostics`, and `identity_evaluated_at`. - Kept `canonical_resource_key` as the single persisted canonical key truth; no `canonical_key` column or duplicate key truth was added. - Updated `CoverageResourceUpserter` to consume resolver/evaluator output instead of hardcoding `id`/`sourceId`. - Updated `CoverageEvidenceWriter` to preserve resource identity/claim state instead of resetting to resource-type defaults. - Updated `ClaimGuard` to block `identity_conflict`, `missing_external_id`, and `unsupported_identity`, and to limit/block `derived` identity unless explicitly allowed. - Kept Coverage v2 inactive; no UI, route, navigation, report, review, restore, customer-output, or browser-visible activation was added. ## Identity Strategy Matrix | Canonical type | Strategy | Stable ID posture | Derived/experimental posture | Default claim consequence | | --- | --- | --- | --- | --- | | `deviceAndAppManagementAssignmentFilter` | `tcm.assignment_filter.v1` | TCM/provider IDs stable | source/derived composite allowed | derived limited | | `deviceEnrollmentLimitRestriction` | `tcm.device_enrollment_limit_restriction.v1` | TCM/provider IDs stable | source/derived composite allowed | derived limited | | `deviceEnrollmentPlatformRestriction` | `tcm.device_enrollment_platform_restriction.v1` | TCM/provider IDs stable | source/derived composite allowed | derived limited | | `deviceEnrollmentStatusPageWindows10` | `tcm.device_enrollment_status_page_windows10.v1` | TCM/provider IDs stable | source/derived composite allowed | derived limited | | `appProtectionPolicyAndroid` | `tcm.app_protection_policy_android.v1` | TCM/provider IDs stable | source/derived composite allowed | derived limited | | `appProtectionPolicyiOS` | `tcm.app_protection_policy_ios.v1` | TCM/provider IDs stable | source/derived composite allowed | derived limited | | `notificationMessageTemplate` | `graph.notification_message_template.v1` | Graph/template IDs stable | source/derived composite allowed | no certification by default | | `roleScopeTag` | `graph_beta.role_scope_tag.v1` | beta IDs are experimental, not stable proof | experimental source key resolves as derived | internal-only by default | ## Schema And Persistence - Migration added: `apps/platform/database/migrations/2026_06_26_000417_extend_tenant_configuration_resource_identity.php`. - Added PostgreSQL check constraint for `canonical_key_kind`. - Added PostgreSQL JSONB object constraints for `source_identity`, `secondary_identity_keys`, and `identity_diagnostics`. - Added targeted indexes for scope/type/identity-state and resource-type/key-kind lookup paths. - No speculative JSONB GIN index was added. - Tombstone behavior is deferred; no tombstone field or active delete/drift workflow is implemented in this slice. ## Scope, Claim, And Evidence Safety - Scope tuple remains `workspace_id`, `managed_environment_id`, `provider_connection_id`, `resource_type_id`, and `canonical_resource_key`. - Provider connections are still validated against the same workspace and managed environment before upsert/capture. - Stable ID rename updates the same same-scope resource row. - Duplicate display names with different stable IDs remain separate rows. - Display-name-only payloads resolve to `missing_external_id`, not `stable`; repeated same-scope display-only observations are promoted to `identity_conflict` instead of merging by secondary/display fingerprint. - Unsupported identity observations are promoted to `identity_conflict` on same-scope fallback-key collision instead of merging by unsupported fallback key. - Same-scope unsafe derived identity collisions mark existing/incoming resources as `identity_conflict` and `claim_blocked`. - Cross-workspace, cross-managed-environment, and cross-provider resources do not merge because the existing scope tuple remains part of identity uniqueness. - Diagnostics and secondary keys are redacted and bounded; no raw provider payloads, tokens, secrets, cookies, authorization headers, private keys, certificates, or full provider response dumps are persisted in identity diagnostics. - No fallback-to-latest evidence path, v1-to-v2 adapter, dual-write path, old snapshot promotion, or old v1 gap taxonomy was introduced. ## Product Surface Close-Out - UI Surface Impact: none. - Product Surface Impact: `N/A - no rendered product surface changed`. - Browser smoke result: `N/A - no rendered UI surface changed`. - Human Product Sanity: `N/A - no product surface changed`; visible complexity outcome is neutral. - Livewire v4 compliance: Livewire v4 baseline unchanged; 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/global search change. - Destructive/high-impact actions: none added. - Asset strategy: no assets registered; `filament:assets` is not required by this spec. - No completed historical spec was rewritten or stripped of validation, task, smoke, browser, or review history. ## Validation - PASS: `php -l` syntax sweep for tracked and untracked changed PHP files. - PASS: `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec417` (24 passed, 1 PostgreSQL-only skipped, 162 assertions). - PASS: `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/TenantConfiguration` (35 passed, 170 assertions). - PASS: `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantConfiguration` (35 passed, 8 PostgreSQL-only skipped, 190 assertions). - PASS: `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest -c phpunit.pgsql.xml tests/Feature/TenantConfiguration` (43 passed, 203 assertions). - PASS: `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`. - PASS: `git diff --check`. - PASS: untracked-file trailing-whitespace scan. ## Analysis And Fix Loop - Iteration 1 finding: secondary-key and diagnostics builders redacted nested values without preserving key context, so sensitive scalar fields were not redacted. Fixed by redacting with field-name context; regression tests pass. - Iteration 2 finding: PostgreSQL JSONB object constraints rejected empty PHP arrays encoded as `[]`. Fixed by normalizing empty metadata/defaults to JSON objects; PostgreSQL lane passes. - Post-implementation scope finding: optional tombstone timestamp was unnecessary without tombstone behavior. Fixed by removing the unused field and documenting tombstone deferral. - Manual final-review finding: repeated same-scope display-name-only payloads and unsupported identity observations could merge by fallback candidate keys even though they were not marked stable. Fixed by treating repeated `missing_external_id` and `unsupported_identity` candidate collisions as `identity_conflict`, marking existing candidates `claim_blocked`, generating a separate conflict key for the new unsafe observation, normalizing empty `source_metadata` to JSON object form, and adding feature regressions for both paths. - Remaining confirmed in-scope findings: none. ## Files Changed - `apps/platform/app/Models/TenantConfigurationResource.php` - `apps/platform/app/Services/TenantConfiguration/CanonicalIdentityResolver.php` - `apps/platform/app/Services/TenantConfiguration/CanonicalIdentityResult.php` - `apps/platform/app/Services/TenantConfiguration/ClaimGuard.php` - `apps/platform/app/Services/TenantConfiguration/CoverageEvidenceWriter.php` - `apps/platform/app/Services/TenantConfiguration/CoverageIdentityStrategyRegistry.php` - `apps/platform/app/Services/TenantConfiguration/CoverageResourceIdentityEvaluator.php` - `apps/platform/app/Services/TenantConfiguration/CoverageResourceUpserter.php` - `apps/platform/app/Services/TenantConfiguration/CoverageSecondaryKeyBuilder.php` - `apps/platform/app/Services/TenantConfiguration/IdentityConflictDiagnosticsBuilder.php` - `apps/platform/app/Support/TenantConfiguration/CanonicalKeyKind.php` - `apps/platform/database/factories/TenantConfigurationResourceFactory.php` - `apps/platform/database/migrations/2026_06_26_000417_extend_tenant_configuration_resource_identity.php` - `apps/platform/tests/Feature/TenantConfiguration/Spec417CanonicalIdentityPersistenceTest.php` - `apps/platform/tests/Feature/TenantConfiguration/Spec417CoverageResourceIdentityUpsertTest.php` - `apps/platform/tests/Feature/TenantConfiguration/Spec417IdentityClaimGuardFeatureTest.php` - `apps/platform/tests/Feature/TenantConfiguration/Spec417IdentityConflictScopeTest.php` - `apps/platform/tests/Feature/TenantConfiguration/Spec417IdentityNoLegacyNoUiActivationTest.php` - `apps/platform/tests/Unit/Support/TenantConfiguration/ClaimGuardTest.php` - `apps/platform/tests/Unit/Support/TenantConfiguration/Spec417CanonicalIdentityResolverTest.php` - `apps/platform/tests/Unit/Support/TenantConfiguration/Spec417CoverageIdentityStrategyRegistryTest.php` - `apps/platform/tests/Unit/Support/TenantConfiguration/Spec417CoverageSecondaryKeyBuilderTest.php` - `apps/platform/tests/Unit/Support/TenantConfiguration/Spec417IdentityConflictDiagnosticsTest.php` - `specs/417-canonical-identity-engine/implementation-report.md` - `specs/417-canonical-identity-engine/tasks.md` ## Deployment Impact - Migrations: yes, additive identity metadata/check constraints/indexes on `tenant_configuration_resources`. - Environment variables: none. - Queues/workers: no new queue or OperationRun path. - Scheduler: no change. - Storage/volumes: no change. - Assets: no change. - Staging/production: run migrations before any later Coverage v2 activation; validate PostgreSQL migration in staging. ## Deferred Work - Tombstone/delete workflow remains out of scope. - Customer/operator identity diagnostics UI remains out of scope. - Coverage v2 activation, legacy cutover/removal, compare/render/restore/report/customer claims remain future specs. ## Final Gate Result - Spec Readiness Gate: PASS. - Implementation Scope Gate: PASS. - Test Gate: PASS. - Browser Smoke Test Gate: PASS as not applicable, `N/A - no rendered UI surface changed`. - Post-Implementation Analysis Gate: PASS. - Merge Readiness Gate: PASS; ready for manual review/merge.