# `Profile` Business-Object Permission Reconciliation

This document records the fourth `Profile` validation track: the control-plane fix for partially seeded business-object `ObjectPermissions` / `FieldPermissions`.

## Scope

This pass addresses the remaining business-object drift identified in:

- `roadmap-profile-convergence.md`
- `validation-03-full-profile-gap-inventory.md`

One-sentence mechanism:

> `InitializePermissions` treated a profile as “already seeded” as soon as it had any `ObjectPermissions` rows, so partially seeded business-object coverage escaped both normal setup and the old half-seeded repair path.

## What Was Broken

Before this change, the control-plane initialization path had two blind spots:

1. `InitializePermissions` only seeded profiles where `ObjectPermissions` count was exactly zero.
2. `reconcileHalfSeededProfiles` only repaired the special case:
   - `ObjectPermissions > 0`
   - `FieldPermissions = 0`

That left these states untreated:

- some business objects missing `ObjectPermissions`
- some business objects missing `FieldPermissions`
- profiles that already had many `FieldPermissions`, but still not full business-object coverage

This matched the `validation-03` inventory much more closely than a profile-creation or runtime object/field creation bug.

## Repair Strategy

The fix keeps the existing seeding model, but changes the convergence rule:

- continue to seed truly unseeded profiles through the normal full-schema path
- add a dedicated business-object reconciliation step that:
  - computes the expected business-object coverage from metadata
  - counts current business-object `ObjectPermissions` / `FieldPermissions` per profile
  - detects profiles whose coverage is still partial
  - reuses the shared permission-seeding helpers to upsert the missing business-object rows

Key design choices:

- no tenant hardcoding
- metadata-driven object scope
- reuses `BuildPermissionsForProfileAndSchema`
- idempotent via existing batch upsert behavior
- runs even when the hot path sees “no completely unseeded profiles”

## Code Changes

Primary implementation:

- `backend/internal/controlplane/packs/permissions.go`

Supporting plumbing:

- `backend/internal/infrastructure/persistence/perm_sets.go`
- `backend/internal/features/permissions/service.go`
- `backend/internal/domain/ports/repositories.go`

New behavior introduced in this pass:

1. build a business-object seed scope from metadata
2. count existing business-object rows per profile
3. reconcile profiles whose business-object counts are below expected coverage
4. keep the original “seed brand-new profiles” behavior
5. fail fast if an unseeded-profile seed is attempted without a metadata service

## Local Verification Completed

The following commands were run after the implementation:

```bash
go test ./internal/controlplane/...
go test ./internal/features/permissions/...
go test ./internal/infrastructure/persistence/...
```

Observed result:

- all targeted packages passed

This validates:

- the control-plane package still builds and tests cleanly
- the new repository interface methods compile across consumers
- the permission package mocks and service layer are consistent with the new reconciliation path

## What This Validation Does Not Yet Claim

This document does **not** yet claim that the business-object rows were already repaired in `ci-test` or `staging`.

At this stage, the validated facts are:

- the code change compiles
- the relevant test suites pass
- the control-plane logic now has an explicit partial-seeded reconciliation path

The database-level convergence still needs to be executed and observed in environment.

## Environment Validation Checklist

When this branch is applied to `ci-test` or `staging`, validate the repair in this order:

### 1. Capture pre-repair counts

For profile-owned permission sets, record:

- business-object `ObjectPermissions` count per profile
- business-object `FieldPermissions` count per profile
- sample missing objects from `validation-03`

Recommended sample objects:

- `campaignmember`
- `contentversion`
- `opportunitylineitem`
- `quotelineitem`
- `orderitem`

### 2. Run the repaired control-plane flow

Run the normal convergence entrypoint intended for this environment:

- preferred: the standard setup/apply path used by the environment
- acceptable for historical recovery validation: `setup repair` if that is the selected execution path

### 3. Capture post-repair counts

Confirm that the same profiles now have:

- full business-object `ObjectPermissions` coverage for the sampled objects
- full business-object `FieldPermissions` coverage for the sampled objects

### 4. Confirm idempotence

Re-run the same convergence entrypoint and confirm:

- no regressions
- no duplicate-row side effects
- counts stay stable after the second run

## Expected User Impact

If this pass works as intended, the user-visible impact should be:

- migrated profiles recover missing business-object permission coverage that should already have existed
- downstream describe / form / CRUD behavior becomes consistent with the intended profile rule set
- the environment no longer depends on a “fully zero rows” condition before profile permission convergence can happen

This pass is primarily a **forward-correctness + partial-state convergence** fix, not only a one-time historical backfill.

## Out of Scope

Still not closed by this pass:

- final staging evidence screenshots
- audit / verify guardrails for future drift detection
- any unrelated non-profile permission bugs
- any symptom that turns out to be caused by layout / schema / frontend logic instead of profile permission coverage
