TenantAtlas/specs/405-json-to-jsonb-data-layer-hardening/implementation-report.md
ahmido 686947d26c feat: harden json to jsonb data layer for trust payloads (#476)
Automated PR provided by Codex via Gitea API.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #476
2026-06-23 21:36:35 +00:00

332 lines
27 KiB
Markdown

# Spec 405 Implementation Report - JSON-to-JSONB Data-layer Hardening
## 1. Candidate Gate Result
**Current result**: `PASS WITH CONDITIONS`.
Local PostgreSQL migration/type proof, rollback/down-up proof, schema-attribute proof, and focused model/query regression tests pass. The remaining condition is external staging/Dokploy validation; it is not accessible from this agent session, so the gate cannot honestly be stronger than `PASS WITH CONDITIONS`.
## 2. Scope Confirmation
In scope:
- Inventory every live PostgreSQL `json` and `jsonb` column.
- Classify every live `json` column.
- Convert only reviewed `CONVERT` columns to `jsonb`.
- Preserve current payload semantics, casts, scoping, and query behavior.
- Add local PostgreSQL tests and focused regression proof.
Out of scope and not changed:
- UI surfaces, routes, navigation, Filament resources, panels, actions, forms, tables, widgets, or customer output.
- Authorization model, roles, capabilities, provider semantics, lifecycle semantics, normalized replacement tables, and new product concepts.
- Completed historical specs and their validation/task/smoke/browser history.
- Speculative JSONB indexes.
Historical context reviewed as read-only:
- Spec 400: no `implementation-report.md` present; spec/plan/tasks were treated as historical audit context only.
- Spec 401: backup confirmation and provider residual context reviewed; left unchanged.
- Spec 402: provider action residual closure context reviewed; left unchanged.
- Spec 403: evidence/currentness runtime closure context reviewed; left unchanged.
- Spec 404: management report PDF staging validation and `PASS WITH CONDITIONS` staging caveat reviewed; left unchanged.
## 3. Dirty State
Initial repository state:
- Branch: `405-json-to-jsonb-data-layer-hardening`
- HEAD: `8918b357 feat: finish management report PDF staging validation (#475)`
- Initial dirty state: untracked `specs/405-json-to-jsonb-data-layer-hardening/`
- Session branch not created because the active spec package was already untracked in the working tree; work continued cautiously on the current feature branch.
- Initial `git diff --check`: passed.
Final `git status --short --untracked-files=all`:
```text
M apps/platform/app/Models/BackupItem.php
?? apps/platform/database/migrations/2026_06_23_000405_convert_trust_payload_json_columns_to_jsonb.php
?? apps/platform/tests/Feature/Database/JsonbDataLayerHardeningTest.php
?? specs/405-json-to-jsonb-data-layer-hardening/checklists/requirements.md
?? specs/405-json-to-jsonb-data-layer-hardening/implementation-report.md
?? specs/405-json-to-jsonb-data-layer-hardening/plan.md
?? specs/405-json-to-jsonb-data-layer-hardening/spec.md
?? specs/405-json-to-jsonb-data-layer-hardening/tasks.md
```
The spec package was untracked at session start. This implementation added the migration, database test, implementation report, task completion updates, and the `BackupItem` runtime adjustment.
## 4. JSON/JSONB Inventory Matrix
Inventory source: PostgreSQL `information_schema.columns`, row/null counts from the live local database, 2026-06-23. Converted-column migration decision is direct `ALTER COLUMN ... TYPE jsonb USING ...::jsonb`, grouped per table when more than one column is converted on the same table. Local converted tables have small row counts and no JSON-column index rebuild requirement; staging must still validate actual lock duration and migration runtime before production promotion.
| Column | Type | Rows | Nulls | Nullable | Default | Indexes | Classification | Decision |
|---|---:|---:|---:|---|---|---|---|---|
| alert_deliveries.payload | json | 0 | 0 | YES | none | none | CONVERT P2 | Alert delivery payload; direct conversion. |
| alert_rules.tenant_allowlist | json | 0 | 0 | YES | none | none | CONVERT P2 | Alert scoping list; direct conversion. |
| audit_logs.metadata | json | 1320 | 0 | YES | none | none | CONVERT P1 | Audit context queried by metadata key; direct conversion. |
| backup_items.payload | json | 246 | 0 | NO | none | none | CONVERT P1 | Critical backup snapshot payload; direct conversion. |
| backup_items.metadata | json | 246 | 0 | YES | none | none | CONVERT P1 | Backup metadata and warnings; direct conversion. |
| backup_items.assignments | json | 246 | 146 | YES | none | none | CONVERT P1 | Assignment proof payload; direct conversion and query function update. |
| backup_schedules.days_of_week | json | 0 | 0 | YES | none | none | CONVERT P2 | Schedule structured config; direct conversion. |
| backup_schedules.policy_types | json | 0 | 0 | NO | none | none | CONVERT P2 | Schedule policy-type filter; direct conversion. |
| backup_sets.metadata | json | 25 | 0 | YES | none | none | CONVERT P1 | Backup-set provenance metadata; direct conversion. |
| baseline_profiles.scope_jsonb | jsonb | 4 | 0 | NO | none | none | ALREADY_JSONB | No migration. |
| baseline_snapshot_items.meta_jsonb | jsonb | 102 | 0 | YES | none | none | ALREADY_JSONB | No migration. |
| baseline_snapshots.summary_jsonb | jsonb | 4 | 0 | YES | none | none | ALREADY_JSONB | No migration. |
| baseline_snapshots.completion_meta_jsonb | jsonb | 4 | 0 | YES | none | none | ALREADY_JSONB | No migration. |
| baseline_tenant_assignments.override_scope_jsonb | jsonb | 3 | 3 | YES | none | none | ALREADY_JSONB | No migration. |
| entra_groups.group_types | jsonb | 574 | 0 | YES | none | none | ALREADY_JSONB | No migration. |
| environment_review_sections.summary_payload | jsonb | 153 | 0 | NO | `{}` | none | ALREADY_JSONB | No migration. |
| environment_review_sections.render_payload | jsonb | 153 | 0 | NO | `{}` | none | ALREADY_JSONB | No migration. |
| environment_reviews.summary | jsonb | 28 | 0 | NO | `{}` | none | ALREADY_JSONB | No migration. |
| evidence_snapshot_items.summary_payload | jsonb | 98 | 0 | NO | `{}` | `evidence_snapshot_items_payload_gin` | ALREADY_JSONB | Existing justified GIN retained. |
| evidence_snapshots.summary | jsonb | 24 | 0 | NO | `{}` | none | ALREADY_JSONB | No migration. |
| finding_exception_decisions.metadata | jsonb | 7 | 0 | NO | `{}` | `finding_exception_decisions_metadata_gin` | ALREADY_JSONB | Existing justified GIN retained. |
| finding_exception_evidence_references.summary_payload | jsonb | 0 | 0 | NO | `{}` | `finding_exception_evidence_refs_payload_gin` | ALREADY_JSONB | Existing justified GIN retained. |
| finding_exceptions.evidence_summary | jsonb | 9 | 0 | NO | `{}` | `finding_exceptions_evidence_summary_gin` | ALREADY_JSONB | Existing justified GIN retained. |
| findings.evidence_jsonb | jsonb | 254 | 0 | YES | none | none | ALREADY_JSONB | No migration. |
| inventory_items.meta_jsonb | jsonb | 229 | 0 | YES | none | none | ALREADY_JSONB | No migration. |
| inventory_links.metadata | jsonb | 251 | 0 | YES | none | none | ALREADY_JSONB | No migration. |
| managed_environment_onboarding_sessions.state | json | 2 | 0 | YES | none | none | CONVERT P1 | Onboarding state payload; direct conversion. |
| managed_environment_permissions.details | json | 135 | 0 | YES | none | none | CONVERT P1 | Provider permission details; direct conversion. |
| managed_environment_triage_reviews.review_snapshot | jsonb | 0 | 0 | NO | `{}` | none | ALREADY_JSONB | No migration. |
| managed_environments.metadata | json | 54 | 2 | YES | none | none | CONVERT P1 | Provider/environment metadata; direct conversion. |
| managed_environments.rbac_canary_results | json | 54 | 54 | YES | none | none | CONVERT P1 | RBAC readiness proof; direct conversion. |
| managed_environments.rbac_last_warnings | json | 54 | 54 | YES | none | none | CONVERT P1 | RBAC warning proof; direct conversion. |
| notifications.data | jsonb | 311 | 0 | NO | none | none | ALREADY_JSONB | No migration. |
| operation_runs.summary_counts | jsonb | 96 | 0 | NO | `{}` | none | ALREADY_JSONB | No migration. |
| operation_runs.failure_summary | jsonb | 96 | 0 | NO | `[]` | none | ALREADY_JSONB | No migration. |
| operation_runs.context | jsonb | 96 | 0 | NO | `{}` | none | ALREADY_JSONB | No migration. |
| platform_users.capabilities | jsonb | 1 | 0 | NO | `[]` | none | ALREADY_JSONB | No migration. |
| policies.metadata | json | 208 | 3 | YES | none | none | CONVERT P1 | Policy inventory metadata; direct conversion. |
| policy_versions.snapshot | json | 422 | 0 | NO | none | none | CONVERT P1 | Immutable policy snapshot; direct conversion. |
| policy_versions.metadata | json | 422 | 0 | YES | none | none | CONVERT P1 | Version metadata; direct conversion. |
| policy_versions.assignments | json | 422 | 216 | YES | none | none | CONVERT P1 | Version assignment snapshot; direct conversion. |
| policy_versions.scope_tags | json | 422 | 14 | YES | none | none | CONVERT P1 | Scope tag snapshot; direct conversion. |
| policy_versions.secret_fingerprints | json | 422 | 2 | YES | none | none | CONVERT P1 | Redaction integrity metadata; direct conversion. |
| product_usage_events.metadata | jsonb | 100 | 0 | NO | `{}` | none | ALREADY_JSONB | No migration. |
| provider_connections.scopes_granted | jsonb | 16 | 0 | NO | `{}` | none | ALREADY_JSONB | No migration. |
| provider_connections.metadata | jsonb | 16 | 0 | NO | `{}` | none | ALREADY_JSONB | No migration. |
| restore_runs.requested_items | json | 4 | 2 | YES | none | none | CONVERT P1 | Restore request payload; direct conversion. |
| restore_runs.preview | json | 4 | 1 | YES | none | none | CONVERT P1 | Restore preview payload; direct conversion. |
| restore_runs.results | json | 4 | 0 | YES | none | none | CONVERT P1 | Restore execution result payload; direct conversion. |
| restore_runs.metadata | json | 4 | 0 | YES | none | none | CONVERT P1 | Restore metadata; direct conversion. |
| restore_runs.group_mapping | json | 4 | 4 | YES | none | none | CONVERT P1 | Restore group mapping; direct conversion. |
| review_packs.summary | jsonb | 23 | 0 | NO | `{}` | none | ALREADY_JSONB | No migration. |
| review_packs.options | jsonb | 23 | 0 | NO | `{}` | none | ALREADY_JSONB | No migration. |
| review_publication_resolution_cases.summary | jsonb | 3 | 0 | NO | `{}` | `review_publication_resolution_cases_summary_gin` | ALREADY_JSONB | Existing justified GIN retained. |
| review_publication_resolution_cases.metadata | jsonb | 3 | 0 | NO | `{}` | none | ALREADY_JSONB | No migration. |
| review_publication_resolution_steps.summary | jsonb | 17 | 0 | NO | `{}` | `review_publication_resolution_steps_summary_gin` | ALREADY_JSONB | Existing justified GIN retained. |
| review_publication_resolution_steps.metadata | jsonb | 17 | 0 | NO | `{}` | none | ALREADY_JSONB | No migration. |
| settings_catalog_definitions.raw | jsonb | 0 | 0 | NO | none | `idx_settings_catalog_definitions_raw_gin` | ALREADY_JSONB | Existing justified GIN retained. |
| stored_reports.payload | jsonb | 35 | 0 | NO | none | `stored_reports_payload_gin` | ALREADY_JSONB | Existing justified GIN retained. |
| support_requests.context_envelope | jsonb | 0 | 0 | NO | `{}` | none | ALREADY_JSONB | No migration. |
| tenant_settings.value | json | 0 | 0 | NO | none | none | CONVERT P1 | Tenant-scoped setting value; direct conversion. |
| workspace_settings.value | json | 0 | 0 | NO | none | none | CONVERT P1 | Workspace-scoped setting value; direct conversion. |
No live `json` column was left as `KEEP_JSON`, `DEPRECATED`, or `DECISION_REQUIRED`: every live `json` column had current model ownership, bounded row counts, existing cast/read semantics, and a trust-layer storage reason to align with the existing `jsonb` baseline.
Inventory ownership, cast, and query continuation for converted columns:
| Column | Model / owner | Existing cast or accessor | Existing query/rendered usage | Constraint/FK context |
|---|---|---|---|---|
| alert_deliveries.payload | `App\Models\AlertDelivery` | `array` | Alert delivery rendering/notification payload; no JSON predicate. | Workspace/environment/rule/destination FKs unchanged. |
| alert_rules.tenant_allowlist | `App\Models\AlertRule` | `array` | Alert tenant scoping list; no JSON predicate. | Workspace FK unchanged. |
| audit_logs.metadata | `App\Models\AuditLog` / `AuditRecorder` | `array` | `metadata ->> '_dedupe_key'`, `metadata->...` audit filters/tests. | Workspace/environment/operation FKs and audit scope check unchanged. |
| backup_items.payload | `App\Models\BackupItem` | `array` | Backup set detail, restore readiness, included item rendering. | Workspace/environment/backup/policy FKs unchanged. |
| backup_items.metadata | `App\Models\BackupItem` | `array` | Backup quality/source/warning helpers and rendered backup detail. | Workspace/environment/backup/policy FKs unchanged. |
| backup_items.assignments | `App\Models\BackupItem` | `array` | `scopeWithAssignments()`; assignment count/group helper rendering. | Workspace/environment/backup/policy FKs unchanged. |
| backup_schedules.days_of_week | `App\Models\BackupSchedule` | `array` | Schedule form/detail semantics; no JSON predicate. | Workspace/environment FKs and frequency check unchanged. |
| backup_schedules.policy_types | `App\Models\BackupSchedule` | `array` | Schedule policy-type selection; no JSON predicate. | Workspace/environment FKs and frequency check unchanged. |
| backup_sets.metadata | `App\Models\BackupSet` | `array` | Backup provenance/source helpers; `metadata->source` test path. | Workspace/environment FK unchanged. |
| managed_environment_onboarding_sessions.state | `App\Models\ManagedEnvironmentOnboardingSession` | `array` with allowed-key mutator | Onboarding resume/state rendering and historical migration reads. | Workspace/environment/user FKs and lifecycle constraints unchanged. |
| managed_environment_permissions.details | `App\Models\ManagedEnvironmentPermission` | `array` | Provider permission/readiness detail rendering; no JSON predicate. | Workspace/environment FK and permission unique key unchanged. |
| managed_environments.metadata | `App\Models\ManagedEnvironment` | `array` | Environment/provider metadata accessors and rendered environment context. | Workspace FK and tenant lifecycle constraints unchanged. |
| managed_environments.rbac_canary_results | `App\Models\ManagedEnvironment` | JSON decode accessor | RBAC readiness proof rendering. | Workspace FK and tenant lifecycle constraints unchanged. |
| managed_environments.rbac_last_warnings | `App\Models\ManagedEnvironment` | JSON decode accessor | RBAC warning rendering. | Workspace FK and tenant lifecycle constraints unchanged. |
| policies.metadata | `App\Models\Policy` | `array` | Policy inventory metadata rendering and helper access. | Workspace/environment FK unchanged. |
| policy_versions.snapshot | `App\Models\PolicyVersion` | `array` | Immutable version snapshot rendering/diff/backup provenance. | Workspace/environment/policy FKs unchanged. |
| policy_versions.metadata | `App\Models\PolicyVersion` | `array` | Version source/warning/integrity helpers. | Workspace/environment/policy FKs unchanged. |
| policy_versions.assignments | `App\Models\PolicyVersion` | `array` | Version assignment snapshot rendering; sibling hash index unchanged. | Workspace/environment/policy FKs unchanged. |
| policy_versions.scope_tags | `App\Models\PolicyVersion` | `array` | Scope tag snapshot rendering; sibling hash index unchanged. | Workspace/environment/policy FKs unchanged. |
| policy_versions.secret_fingerprints | `App\Models\PolicyVersion` | `array` | Redaction integrity helper. | Workspace/environment/policy FKs unchanged. |
| restore_runs.requested_items | `App\Models\RestoreRun` | `array` | Restore request/preview detail rendering. | Workspace/environment/backup FKs unchanged. |
| restore_runs.preview | `App\Models\RestoreRun` | `array` | Restore preview wizard/detail rendering. | Workspace/environment/backup FKs unchanged. |
| restore_runs.results | `App\Models\RestoreRun` | `array` | Restore result proof/detail rendering. | Workspace/environment/backup FKs unchanged. |
| restore_runs.metadata | `App\Models\RestoreRun` | `array` | Restore result/safety metadata and reconciliation helpers; no direct JSON predicate on this table. | Workspace/environment/backup FKs unchanged. |
| restore_runs.group_mapping | `App\Models\RestoreRun` | `array` | Restore group-mapping preview/detail rendering. | Workspace/environment/backup FKs unchanged. |
| tenant_settings.value | `App\Models\TenantSetting` | `array` | Tenant settings read/write; no JSON predicate. | Workspace/environment/user FKs unchanged. |
| workspace_settings.value | `App\Models\WorkspaceSetting` | `array` | Workspace settings read/write; no JSON predicate. | Workspace/user FKs unchanged. |
Already-JSONB ownership groups were also reviewed and left unchanged:
- Baseline/inventory/finding/evidence/review rows: existing `*_jsonb`, `summary`, `summary_payload`, `render_payload`, `meta_jsonb`, and evidence payload columns with current model/service ownership; existing GIN indexes retained where already present.
- OperationRun/provider/notification/support rows: existing `operation_runs.context`, summary/failure payloads, provider connection metadata/scopes, notification data, product usage metadata, and support request context already use `jsonb`.
- Stored report and review publication rows: `stored_reports.payload`, review pack summaries/options, and review publication resolution summaries/metadata already use `jsonb`; browser proof confirms rendered output remains stable.
## 5. Migrations Added
Added `apps/platform/database/migrations/2026_06_23_000405_convert_trust_payload_json_columns_to_jsonb.php`.
- Converts the 27 reviewed `CONVERT` columns with `ALTER TABLE ... ALTER COLUMN ... TYPE jsonb USING column::jsonb`.
- Groups multiple converted columns on the same table into a single `ALTER TABLE` statement to avoid repeated per-column table rewrites/lock windows where PostgreSQL can satisfy the changes together.
- Runs only when `DB::getDriverName() === 'pgsql'`.
- Preserves nullability, defaults, constraints, and existing non-JSON indexes by altering only the column type.
- Rollback converts the same columns back to `json` with `USING column::json`.
- Rollback limitation: `jsonb` canonicalizes object key ordering and duplicate JSON object keys. Semantic content is preserved, but byte-for-byte textual JSON representation is not guaranteed.
- Local rollback/down-up proof passed in the PostgreSQL lane, including `BackupItem::withAssignments()` while the column is temporarily `json`.
## 6. Index Justification
No new JSONB indexes were added.
Usage scan found one conversion-affected raw JSON function path, `BackupItem::scopeWithAssignments()`, which filters by array length but does not justify a GIN or expression index at current row counts. Existing hash/sibling indexes and existing GIN indexes on already-JSONB tables remain unchanged.
The new PostgreSQL test asserts no speculative GIN index exists on converted-column tables.
## 7. Runtime Changes Made
Changed `apps/platform/app/Models/BackupItem.php`:
- `scopeWithAssignments()` now uses `jsonb_array_length(assignments::jsonb)` on PostgreSQL.
- The explicit cast keeps the scope compatible during pre-migration deploy windows and after a database-only rollback to `json`.
- Non-PostgreSQL test lanes keep `json_array_length(assignments)` to avoid breaking SQLite-style JSON test support.
No model casts required changes. Laravel array casts continue to read/write `jsonb` columns correctly.
## 8. Tests Added or Updated
Added `apps/platform/tests/Feature/Database/JsonbDataLayerHardeningTest.php` with PostgreSQL-only coverage:
- All reviewed legacy `json` columns are `jsonb` after migrations.
- The 405 migration can run `down()` to `json` and back `up()` to `jsonb` while preserving representative existing rows.
- Nullability, defaults, table indexes, and constraints remain unchanged across the 405 down/up cycle.
- No remaining live public-schema `json` columns remain after the conversion migration.
- No speculative GIN index was introduced for converted columns.
- Representative read/write/cast behavior for alert, audit, backup, restore, policy, provider/environment, settings, and onboarding payload categories.
- Audit metadata key query using `metadata ->> '_dedupe_key'`.
- `BackupItem::withAssignments()` query path against both rollback-state `json` and migrated `jsonb`.
Latest result: 5 tests passed, 260 assertions.
Existing test run:
- `apps/platform/tests/Unit/BackupItemTest.php` still passes after the driver-aware scope change.
- Latest result: 20 tests passed, 31 assertions.
## 9. Data Validation
Local PostgreSQL validation:
- The migration/type test ran migrations on the PostgreSQL testing database and asserted every converted column is `jsonb`.
- The new down/up test temporarily rolled the 405 migration back to `json`, inserted representative rows across every converted column category, verified `BackupItem::withAssignments()` works in the rollback-state schema, migrated forward to `jsonb`, and compared decoded payload semantics.
- The same down/up test asserted nullability, defaults, table index definitions, and table constraint definitions remain unchanged after conversion.
- The same test asserted there are no remaining public-schema columns with `data_type = 'json'`.
- Representative non-sensitive samples were written and read back through Eloquent casts and direct JSONB key predicates.
- Row/null counts were recorded before conversion from the local development database; converted tables are small enough for direct `ALTER COLUMN` locally. Staging/Dokploy must still validate real runtime and lock behavior.
Not performed in this session:
- Production-sized timing.
- Staging/Dokploy migration runtime.
- Full byte-level dump comparison. This is not required because `jsonb` canonicalizes textual representation; semantic preservation was tested instead.
## 10. Browser Proof
Focused browser proof passed using existing payload-backed smoke tests. No UI files were changed; this proof is regression-only.
Command:
```bash
cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec403EvidenceCurrentnessRuntimeClosureSmokeTest.php tests/Browser/Spec394ProviderFreshnessPermissionSmokeTest.php tests/Browser/Spec371BackupSetProductizationSmokeTest.php tests/Browser/Spec335RestoreRunDetailProductizationSmokeTest.php tests/Browser/Spec379ManagementReportPdfSmokeTest.php
```
Result:
```text
5 browser tests passed, 176 assertions, 19.87s
```
Covered surfaces:
- Evidence Overview and OperationRun proof currentness: `Spec403EvidenceCurrentnessRuntimeClosureSmokeTest.php`
- Provider freshness and required-permission readiness: `Spec394ProviderFreshnessPermissionSmokeTest.php`
- Backup set list/detail decision hierarchy and included backup items: `Spec371BackupSetProductizationSmokeTest.php`
- Restore run detail post-execution proof/results rendering: `Spec335RestoreRunDetailProductizationSmokeTest.php`
- Review Pack management PDF / Stored Report output state: `Spec379ManagementReportPdfSmokeTest.php`
Browser tests asserted no JavaScript errors and no unexpected console logs on the tested surfaces.
## 11. Regression Proof
Completed:
- PostgreSQL schema/type/index/model/query proof passed.
- PostgreSQL rollback/down-up preservation proof passed.
- Existing `BackupItem` unit regression suite passed.
- Focused browser proof passed across evidence, operations, provider, backup, restore, review pack, stored report, and management PDF surfaces.
- No UI/runtime surface files were edited.
- No authorization, global search, provider registration, destructive/high-impact action, or asset behavior was changed.
## 12. Staging Validation
Staging/Dokploy validation is not accessible from this agent session.
Required staging release checks before production:
- Run the migration on staging PostgreSQL.
- Confirm app boot.
- Run the focused PostgreSQL Spec405 test or equivalent staging validation.
- Smoke the representative payload-backed surfaces.
- Confirm no secrets or raw provider credential payloads appear in logs/screenshots.
Because staging is unavailable here, final readiness remains `PASS WITH CONDITIONS`.
## 13. Remaining Findings
- No P0 findings.
- No unresolved `json` column classification.
- No in-scope schema or application regression currently known after fixing rollback-compatible query handling and grouped table conversion.
- Remaining P1 condition: staging/Dokploy validation unavailable in this session.
- Browser proof passed locally.
## 14. Deferred Items
- Staging/Dokploy validation and migration runtime observation.
- Optional future online migration strategy if a production-sized table proves too large for direct `ALTER COLUMN`.
- Full browser/runtime audit remains out of scope.
- Governance artifact lifecycle and retention remain separate follow-up scope.
## 15. Validation Commands
Passed:
```bash
git diff --check
cd apps/platform && ./vendor/bin/sail php vendor/bin/pest -c phpunit.pgsql.xml tests/Feature/Database/JsonbDataLayerHardeningTest.php
cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Unit/BackupItemTest.php
cd apps/platform && ./vendor/bin/sail pint app/Models/BackupItem.php database/migrations/2026_06_23_000405_convert_trust_payload_json_columns_to_jsonb.php tests/Feature/Database/JsonbDataLayerHardeningTest.php
cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec403EvidenceCurrentnessRuntimeClosureSmokeTest.php tests/Browser/Spec394ProviderFreshnessPermissionSmokeTest.php tests/Browser/Spec371BackupSetProductizationSmokeTest.php tests/Browser/Spec335RestoreRunDetailProductizationSmokeTest.php tests/Browser/Spec379ManagementReportPdfSmokeTest.php
```
## 16. Product Surface, Filament, Deployment, and Recommended Next Step Close-Out
- **Application implementation status**: implemented locally with focused PostgreSQL rollback/down-up, unit, formatter, diff, and browser proof passing; staging validation still conditions the gate.
- **Livewire v4 compliance**: Livewire 4.1.4 confirmed by Laravel Boost; no Livewire code changed.
- **Provider registration location**: Laravel 12 panel providers remain in `apps/platform/bootstrap/providers.php`; no provider registration changed.
- **Global search posture**: unchanged. No globally searchable resource was added or modified.
- **Destructive/high-impact actions**: none added or changed; no confirmation/authorization behavior changed.
- **Asset strategy**: no frontend assets added, no `FilamentAsset` registration, no new `filament:assets` deploy requirement beyond the existing deployment baseline.
- **Product Surface Impact**: no runtime UI surface changed.
- **UI Surface Impact**: none; focused browser proof is regression-only.
- **No-legacy posture**: canonical data-layer conversion; no compatibility shim or dual-write path introduced.
- **Page archetype / surface budgets / Technical Annex / deep-link demotion / canonical status vocabulary**: N/A for changed code; existing surfaces remain unchanged.
- **Product Surface exceptions**: none.
- **Focused browser proof**: passed across evidence/currentness, operations, provider readiness, backup, restore, review pack, stored report, and management PDF surfaces.
- **Human Product Sanity**: visible complexity unchanged; no raw payload exposure added; current/released/failed/partial semantics unchanged.
- **Implementation-report fields**: Livewire v4, provider registration, global search, destructive/high-impact actions, asset strategy, deployment impact, tests/browser result, and visible complexity are recorded here.
- **Deployment impact**: database migration only. No env vars, queues, scheduler, storage, routes, provider scopes, panels, assets, or worker changes. Staging validation is required before production promotion.
- **No completed-spec rewrite assertion**: Specs 400-404 were read-only context; their files were not rewritten.
- **Recommended next step**: run staging PostgreSQL migration validation before production promotion.