# 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
