TenantAtlas/specs/159-baseline-snapshot-truth/plan.md
ahmido 8426741068 feat: add baseline snapshot truth guards (#189)
## Summary
- add explicit BaselineSnapshot lifecycle truth with conservative backfill and a shared truth resolver
- block baseline compare from building, incomplete, or superseded snapshots and align workspace/tenant UI truth surfaces with effective snapshot state
- surface artifact truth separately from operation outcome across baseline profile, snapshot, compare, and operation run pages

## Testing
- integrated browser smoke test on the active feature surfaces
- `vendor/bin/sail artisan test --compact tests/Feature/Filament/BaselineSnapshotTruthSurfaceTest.php tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php`
- targeted baseline lifecycle and compare guard coverage added in Pest
- `vendor/bin/sail bin pint --dirty --format agent`

## Notes
- Livewire v4 compliance preserved
- no panel provider registration changes were needed; Laravel 12 providers remain in `bootstrap/providers.php`
- global search remains disabled for the affected baseline resources by design
- destructive actions remain confirmation-gated; capture and compare actions keep their existing authorization and confirmation behavior
- no new panel assets were added; existing deploy flow for `filament:assets` is unchanged

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #189
2026-03-23 11:32:00 +00:00

487 lines
22 KiB
Markdown

# Implementation Plan: 159 — BaselineSnapshot Artifact Truth & Downstream Consumption Guards
**Branch**: `159-baseline-snapshot-truth` | **Date**: 2026-03-23 | **Spec**: `specs/159-baseline-snapshot-truth/spec.md`
**Input**: Feature specification from `specs/159-baseline-snapshot-truth/spec.md`
## Summary
Make BaselineSnapshot explicitly authoritative for its own usability by (1) adding a first-class lifecycle state and completion metadata to the artifact, (2) finalizing capture with `building -> complete|incomplete` semantics while deriving historical `superseded` presentation status without mutating immutable snapshot records, (3) centralizing consumability and effective-current-snapshot resolution so compare and profile truth only use complete snapshots, and (4) reusing the existing artifact-truth, badge, and Filament surface patterns so operators can distinguish run outcome from artifact usability.
## Technical Context
**Language/Version**: PHP 8.4
**Primary Dependencies**: Laravel 12, Filament v5, Livewire v4
**Storage**: PostgreSQL (via Sail)
**Testing**: Pest v4 (PHPUnit 12)
**Target Platform**: Docker (Laravel Sail), deployed containerized on Dokploy
**Project Type**: Web application (Laravel)
**Performance Goals**: Preserve current chunked snapshot-item persistence characteristics and reject non-consumable snapshots before expensive compare/drift work
**Constraints**:
- `OperationRun` lifecycle and outcomes remain service-owned via `OperationRunService`
- Capture and compare keep the existing Ops-UX 3-surface feedback contract
- Workspace/tenant isolation and 404/403 semantics remain unchanged
- No new Microsoft Graph contracts or synchronous render-time remote calls
- Legacy snapshots must be backfilled conservatively; ambiguous history must fail safe
**Scale/Scope**: Workspace-owned baseline profiles may accumulate many snapshots and each snapshot may hold many items via chunked inserts; this feature touches capture jobs, compare services/jobs, artifact-truth presentation, Filament admin/tenant surfaces, migrations, and focused baseline test suites
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- Inventory-first: PASS — Inventory remains the last-observed truth. BaselineSnapshot remains an explicit immutable capture artifact whose lifecycle becomes explicit without rewriting prior terminal states.
- Read/write separation: PASS — this feature mutates TenantPilot-owned baseline records only; capture/compare start surfaces already require confirmation and auditability remains intact.
- Graph contract path: PASS — no new Graph endpoints or contract-registry changes are introduced.
- Deterministic capabilities: PASS — capability checks remain in existing registries/resolvers; no new raw capability strings.
- Workspace + tenant isolation: PASS — BaselineProfile/BaselineSnapshot remain workspace-owned, compare execution remains tenant-owned, and current cross-plane rules remain unchanged.
- Canonical-view scope: PASS — Monitoring run detail stays canonical-view scoped, defaults to active tenant context when present, and requires workspace plus referenced-tenant entitlement before revealing tenant-owned baseline runs.
- Run observability (`OperationRun`): PASS — capture/compare continue using canonical queued jobs and `OperationRun` records.
- Ops-UX 3-surface feedback: PASS — no additional toast/notification surfaces are introduced.
- Ops-UX lifecycle: PASS — run transitions remain through `OperationRunService`; artifact lifecycle is added on BaselineSnapshot, not as a substitute for run lifecycle.
- Ops-UX summary counts: PASS — artifact completeness moves to BaselineSnapshot fields; `summary_counts` remain numeric-only execution metrics.
- Ops-UX guards: PASS — focused regression tests will cover that capture finalization and compare blocking do not regress service-owned run transitions.
- Badge semantics (BADGE-001): PASS — lifecycle/usability presentation will extend the centralized badge/artifact-truth system instead of creating ad-hoc mappings.
- UI naming (UI-NAMING-001): PASS — operator wording stays aligned to `Capture baseline`, `Compare now`, `Building`, `Complete`, `Incomplete`, `Superseded`, and `Not usable for compare`.
- Operator surfaces (OPSURF-001): PASS — run outcome, snapshot lifecycle, snapshot usability, and compare readiness will be shown as distinct dimensions where relevant.
- Filament UI Action Surface Contract: PASS — no new resource topology is introduced; existing immutable-snapshot exemptions remain valid.
- Filament UX-001 layout: PASS — this feature changes labels, badges, and enablement logic on existing pages/resources rather than introducing new form layouts.
- UI-STD-001: PASS — modified Baseline Profiles and Baseline Snapshots list surfaces will be reviewed against `docs/product/standards/list-surface-review-checklist.md`.
## Project Structure
### Documentation (this feature)
```text
specs/159-baseline-snapshot-truth/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│ └── openapi.yaml
└── checklists/
└── requirements.md
```
### Source Code (repository root)
```text
app/
├── Filament/
│ ├── Pages/
│ │ └── BaselineCompareLanding.php
│ └── Resources/
│ ├── BaselineProfileResource.php
│ ├── BaselineSnapshotResource.php
│ ├── BaselineProfileResource/Pages/ViewBaselineProfile.php
│ └── BaselineSnapshotResource/Pages/ViewBaselineSnapshot.php
├── Jobs/
│ ├── CaptureBaselineSnapshotJob.php
│ └── CompareBaselineToTenantJob.php
├── Models/
│ ├── BaselineProfile.php
│ ├── BaselineSnapshot.php
│ ├── BaselineSnapshotItem.php
│ └── OperationRun.php
├── Services/
│ └── Baselines/
│ ├── BaselineCaptureService.php
│ ├── BaselineCompareService.php
│ ├── BaselineSnapshotIdentity.php
│ └── SnapshotRendering/
├── Support/
│ ├── Baselines/
│ ├── Badges/
│ └── Ui/GovernanceArtifactTruth/
database/
├── migrations/
└── factories/
tests/
├── Feature/Baselines/
├── Feature/Filament/
└── Unit/Badges/
```
**Structure Decision**: Web application (Laravel 12). All work stays within the existing baseline jobs/services/models, centralized support layers for badges and artifact truth, the existing Filament resources/pages, one schema migration, and focused Pest coverage.
## Complexity Tracking
No constitution violations are required for this feature.
## Phase 0 — Outline & Research (DONE)
Outputs:
- `specs/159-baseline-snapshot-truth/research.md`
Key decisions captured:
- Use a single BaselineSnapshot lifecycle enum in V1 (`building`, `complete`, `incomplete`) and derive historical `superseded` presentation status from effective-truth resolution instead of mutating immutable snapshots.
- Keep the existing chunked snapshot-item persistence strategy, but add explicit finalization and completion proof rather than attempting a large transactional rewrite.
- Treat `baseline_profiles.active_snapshot_id` as a cached pointer to the latest complete snapshot only; effective truth resolves from complete snapshots, not from the latest attempted snapshot.
- Backfill legacy snapshots conservatively using persisted item counts, summary metadata, and producing-run context where available; ambiguous snapshots default to non-consumable.
- Reuse the existing `ArtifactTruthPresenter` and badge domains rather than inventing a parallel UI truth system.
## Phase 1 — Design & Contracts (DONE)
Outputs:
- `specs/159-baseline-snapshot-truth/data-model.md`
- `specs/159-baseline-snapshot-truth/contracts/openapi.yaml`
- `specs/159-baseline-snapshot-truth/quickstart.md`
Design highlights:
- BaselineSnapshot becomes the authoritative artifact-truth model with lifecycle state, completion timestamps, failure timestamps, and completion metadata sufficient to justify a `complete` transition.
- `active_snapshot_id` remains the operator-facing current-snapshot pointer but only for complete snapshots; compare and UI consumers must use a shared effective-snapshot resolution helper.
- Historical `superseded` status is derived in presentation and truth resolvers from the existence of a newer effective complete snapshot, not persisted as a mutable lifecycle transition.
- Compare blocking happens both before enqueue and at job execution time so an explicit override or stale context cannot bypass the consumability rules.
- Explicit compare overrides to older complete snapshots that are no longer the effective current truth are blocked in V1 and return a dedicated operator-safe reason.
- Snapshot lifecycle display reuses centralized badge and artifact-truth presentation infrastructure so run outcome and artifact usability remain separate but consistent across list, detail, compare, and monitoring surfaces.
## Phase 1 — Agent Context Update (REQUIRED)
Run:
- `.specify/scripts/bash/update-agent-context.sh copilot`
## Constitution Check — Post-Design Re-evaluation
- PASS — Design keeps snapshot truth local to BaselineSnapshot and does not expand into a generic artifact framework.
- PASS — No new Graph calls or contract-registry changes.
- PASS — Existing run observability patterns remain intact and artifact state does not bypass `OperationRunService`.
- PASS — Authorization, destructive confirmation, and operator-surface contracts remain within existing patterns.
- PASS — Badge and artifact-truth semantics remain centralized.
## Phase 2 — Implementation Plan
### Step 1 — Schema and domain lifecycle semantics
Goal: implement FR-001 through FR-006, FR-015, and FR-021.
Changes:
- Add BaselineSnapshot lifecycle/completion columns in a new migration:
- lifecycle state
- `completed_at`
- `failed_at`
- completion metadata JSON for integrity proof and failure reason
- Add BaselineSnapshot status enum/support type and model helpers/scopes:
- lifecycle label helpers
- `isConsumable()`
- transition helpers or a dedicated domain service
- scopes for complete/current-eligible snapshots
- Add centralized effective-snapshot and consumability resolution in the baselines domain layer.
Tests:
- Add unit/domain tests for lifecycle-to-consumability rules.
- Add badge/artifact-truth tests for the new snapshot lifecycle states.
### Step 2 — Capture finalization and current-snapshot rollover
Goal: implement FR-002 through FR-005, FR-009 through FR-014, and the primary P1 regression path.
Changes:
- Update capture flow so snapshot creation begins in `building` before item persistence.
- Keep the existing deterministic deduplication/chunked insert strategy, but add explicit finalization after item persistence completes.
- Only mark the snapshot `complete` after the completion proof passes:
- persisted item count matches expected deduplicated item count
- no unresolved assembly error occurred
- finalization step completed successfully
- Mark the snapshot `incomplete` when capture fails after row creation, including partial-item scenarios.
- Persist completion-proof metadata and lifecycle context needed for later operator truth and legacy backfill decisions, including producing-run linkage and best-available incomplete reason data.
- Advance `active_snapshot_id` only when the new snapshot is complete.
- Derive the previously effective complete snapshot as historical or superseded in truth presentation only after the new snapshot becomes complete.
Tests:
- Update `tests/Feature/Baselines/BaselineCaptureTest.php` for `building -> complete` semantics.
- Add a partial-write failure regression test where a snapshot row exists but never becomes consumable.
- Add retry/idempotency tests ensuring duplicate logical subjects do not produce a falsely complete snapshot.
### Step 3 — Effective snapshot resolution and compare guards
Goal: implement FR-007 through FR-010 and FR-018.
Changes:
- Update compare start service to resolve the effective snapshot from the latest complete snapshot when no explicit override is supplied.
- Validate explicit snapshot overrides against the same consumability rules.
- Re-check snapshot consumability inside `CompareBaselineToTenantJob` before any normal compare work starts.
- Introduce clear reason codes/messages for:
- no consumable snapshot available
- snapshot still building
- snapshot incomplete
- snapshot no longer effective for current truth because a newer complete snapshot is active
- Ensure compare stats and related widgets derive availability from effective consumable truth, not raw snapshot existence.
Tests:
- Update `tests/Feature/Baselines/BaselineComparePreconditionsTest.php` for blocked building/incomplete snapshots.
- Add tests covering fallback to the last complete snapshot when the latest attempt is incomplete.
- Add execution-path tests proving the compare job refuses non-consumable snapshots even if queued context contains a snapshot id.
- Add regression coverage proving modified compare and capture entry points preserve confirmation and 404 or 403 authorization behavior while lifecycle messaging changes.
### Step 4 — UI semantic corrections and centralized presentation
Goal: implement FR-017, FR-019, and FR-020.
Changes:
- Update `ArtifactTruthPresenter` and the relevant badge domain registrations for BaselineSnapshot lifecycle truth.
- Update Baseline Profile detail and list surfaces to distinguish:
- latest complete snapshot
- latest attempted snapshot when different
- compare availability and current baseline availability
- Update Baseline Snapshot list/detail to show explicit lifecycle/usability semantics.
- Update Baseline Compare landing and related widgets so compare unavailability explains the operator next step.
- Update Monitoring/Operation Run detail presentation for baseline capture/compare so run outcome and produced snapshot lifecycle are clearly separate.
Tests:
- Update or add Filament page/resource tests for operator-safe messaging and state display across baseline profile, baseline snapshot, and compare landing surfaces.
- Add dedicated Monitoring run-detail truth-surface coverage so run outcome and artifact truth remain separately verifiable.
- Extend artifact-truth presenter tests for historical/superseded presentation and incomplete snapshot rendering.
### Step 5 — Conservative historical backfill
Goal: implement FR-016 without overstating legacy trust.
Changes:
- Backfill existing snapshots as `complete` only when the first matching proof rule in the spec decision table succeeds:
- count proof: `summary_jsonb.total_items` or equivalent expected-item metadata matches persisted item count
- producing-run success proof: producing-run outcome proves successful finalization and expected-item and persisted-item counts reconcile
- proven empty capture proof: producing-run outcome proves successful finalization for the recorded scope and zero items were expected and persisted
- Classify contradictory, partial, or null evidence as `incomplete` with no tie-breaker that can elevate it to `complete`.
- Leave operator guidance pointing to recapture when legacy truth is unavailable.
Tests:
- Add migration/backfill tests for count-proof complete, proven-empty complete, and ambiguous legacy rows.
### Step 6 — Focused regression and guard coverage
Goal: satisfy the spec test acceptance while keeping CI scope targeted.
Changes:
- Update the most relevant existing baseline suites instead of creating broad duplicate coverage.
- Add new focused tests in:
- `tests/Feature/Baselines/`
- `tests/Feature/Filament/`
- `tests/Unit/Badges/`
- Add focused lifecycle-auditability coverage proving producing-run linkage and best-available incomplete reasons remain queryable after complete and incomplete transitions, and that historical/superseded presentation is derived from effective-truth resolution.
- Add a dedicated Ops-UX guard proving baseline capture/compare code does not bypass `OperationRunService` for status/outcome transitions and does not emit non-canonical terminal operation notifications.
- Preserve existing guard tests for Filament action surfaces and Ops-UX patterns.
Tests:
- Minimum focused run set:
- baseline capture lifecycle tests
- compare preconditions/guard tests
- compare stats tests
- artifact-truth/badge tests
- affected Filament surface tests
snapshot:
$ref: '#/components/schemas/BaselineSnapshotTruth'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
/workspaces/{workspaceId}/baseline-profiles/{profileId}/effective-snapshot:
get:
tags: [BaselineProfiles]
summary: Resolve effective current baseline snapshot
description: Returns the latest complete snapshot that is valid as current baseline truth for the profile.
parameters:
- $ref: '#/components/parameters/WorkspaceId'
- $ref: '#/components/parameters/ProfileId'
responses:
'200':
description: Effective current baseline truth resolved
content:
application/json:
schema:
type: object
required: [profileId, effectiveSnapshot]
properties:
profileId:
type: integer
effectiveSnapshot:
oneOf:
- $ref: '#/components/schemas/BaselineSnapshotTruth'
- type: 'null'
latestAttemptedSnapshot:
oneOf:
- $ref: '#/components/schemas/BaselineSnapshotTruth'
- type: 'null'
compareAvailability:
$ref: '#/components/schemas/CompareAvailability'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
/workspaces/{workspaceId}/baseline-snapshots/{snapshotId}:
get:
tags: [BaselineSnapshots]
summary: Read baseline snapshot truth
parameters:
- $ref: '#/components/parameters/WorkspaceId'
- $ref: '#/components/parameters/SnapshotId'
responses:
'200':
description: Snapshot truth returned
content:
application/json:
schema:
$ref: '#/components/schemas/BaselineSnapshotTruth'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
/tenants/{tenantId}/baseline-compares:
post:
tags: [BaselineCompare]
summary: Start baseline compare
description: Starts compare only when the resolved or explicitly selected snapshot is consumable.
parameters:
- $ref: '#/components/parameters/TenantId'
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
baselineSnapshotId:
type: integer
nullable: true
responses:
'202':
description: Compare accepted and queued
content:
application/json:
schema:
type: object
required: [operationRunId, snapshot]
properties:
operationRunId:
type: integer
snapshot:
$ref: '#/components/schemas/BaselineSnapshotTruth'
'409':
description: Compare blocked because no consumable snapshot is available
content:
application/json:
schema:
$ref: '#/components/schemas/CompareAvailability'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
components:
parameters:
WorkspaceId:
name: workspaceId
in: path
required: true
schema:
type: integer
ProfileId:
name: profileId
in: path
required: true
schema:
type: integer
SnapshotId:
name: snapshotId
in: path
required: true
schema:
type: integer
TenantId:
name: tenantId
in: path
required: true
schema:
type: integer
responses:
Forbidden:
description: Member lacks the required capability
NotFound:
description: Workspace or tenant scope is not entitled, or the record is not visible in that scope
schemas:
BaselineSnapshotTruth:
type: object
required:
- id
- workspaceId
- baselineProfileId
- lifecycleState
- consumable
- capturedAt
properties:
id:
type: integer
workspaceId:
type: integer
baselineProfileId:
type: integer
lifecycleState:
type: string
enum: [building, complete, incomplete, superseded]
consumable:
type: boolean
capturedAt:
type: string
format: date-time
completedAt:
type: string
format: date-time
nullable: true
failedAt:
type: string
format: date-time
nullable: true
supersededAt:
type: string
format: date-time
nullable: true
completionMeta:
type: object
additionalProperties: true
nullable: true
usabilityLabel:
type: string
examples: [Complete, Incomplete, Building, Superseded, Not usable for compare]
reasonCode:
type: string
nullable: true
reasonMessage:
type: string
nullable: true
CompareAvailability:
type: object
required:
- allowed
properties:
allowed:
type: boolean
effectiveSnapshotId:
type: integer
nullable: true
latestAttemptedSnapshotId:
type: integer
nullable: true
reasonCode:
type: string
nullable: true
enum:
- no_consumable_snapshot
- snapshot_building
- snapshot_incomplete
- snapshot_superseded
- invalid_snapshot_selection
reasonMessage:
type: string
nullable: true
nextAction:
type: string
nullable: true
examples:
- Wait for capture completion
- Re-run baseline capture
- Review failed capture