# 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