# Implementation Plan: Baseline Capture Truthful Outcomes and Upstream Guardrails **Branch**: `235-baseline-capture-truth` | **Date**: 2026-04-23 | **Spec**: [spec.md](./spec.md) **Input**: Feature specification from `/specs/235-baseline-capture-truth/spec.md` **Note**: This plan keeps the slice intentionally narrow. It reuses the existing `BaselineSnapshot` lifecycle/usability model and the existing Ops UX explanation path, then hardens only baseline-capture eligibility, outcome mapping, no-data artifact handling, and current-baseline promotion. ## Summary Harden baseline capture so it only succeeds when there is a credible inventory basis and at least one in-scope subject produces a consumable snapshot. The implementation will extend the existing capture reason-code family, make `BaselineCaptureService` evaluate the latest relevant inventory sync before enqueue, re-check the same prerequisite inside `CaptureBaselineSnapshotJob`, map zero-subject captures to `partially_succeeded` plus no-data artifact truth, keep `BaselineProfile.active_snapshot_id` anchored to the last consumable snapshot, and route operator messaging through the existing `ReasonTranslator`, `BaselineCompareStats`, and `GovernanceRunDiagnosticSummaryBuilder` paths instead of adding page-local copy branches. ## Technical Context **Language/Version**: PHP 8.4.15, Laravel 12, Filament v5, Livewire v4 **Primary Dependencies**: `BaselineCaptureService`, `CaptureBaselineSnapshotJob`, `BaselineReasonCodes`, `BaselineCompareStats`, `ReasonTranslator`, `GovernanceRunDiagnosticSummaryBuilder`, `OperationRunService`, `BaselineProfile`, `BaselineSnapshot`, `OperationRunOutcome`, existing Filament capture/compare surfaces **Storage**: Existing PostgreSQL tables only; no new table or schema migration is planned in the mainline slice **Testing**: Pest v4 feature tests through Laravel Sail **Validation Lanes**: `fast-feedback`, `confidence` **Target Platform**: Laravel admin web application in Sail containers with workspace-admin routes under `/admin` and tenant routes under `/admin/t/{tenant}` **Project Type**: Monorepo with one Laravel runtime in `apps/platform` and spec artifacts at repository root **Performance Goals**: Preserve current capture request and queued-job behavior; add at most one focused latest-inventory eligibility lookup per capture attempt and no new high-cardinality UI rendering path **Constraints**: No stale successful inventory fallback, no new persisted entity or lifecycle state, no new generic artifact-truth framework, no auth-plane expansion, and no drift of message semantics into page-local copy **Scale/Scope**: One existing queued workflow (`baseline_capture`), one reason-code family extension, two existing start surfaces, one snapshot detail surface, one Monitoring run-detail explanation path, and focused baseline/Monitoring test families ## Filament v5 Implementation Contract - **Livewire v4.0+ compliance**: Preserved. The plan changes existing Filament actions and shared presenters only; it introduces no legacy Livewire patterns. - **Provider registration location**: Unchanged. Panel providers remain registered in `apps/platform/bootstrap/providers.php`. - **Global search coverage**: `BaselineProfileResource` and `BaselineSnapshotResource` both keep global search disabled via `$isGloballySearchable = false`, so this slice adds no global-search exposure and no new view/edit requirement. - **Destructive actions**: No destructive action is added or changed. The existing `Archive baseline profile` action already uses `->requiresConfirmation()` and remains on its current path. - **Asset strategy**: No new assets are planned. Deployment expectations remain unchanged, including `cd apps/platform && php artisan filament:assets` only when future work introduces registered assets. - **Testing plan**: Prove the slice with focused Pest feature coverage for baseline capture service/start surfaces, retained consumable happy-path success, compare landing readiness, snapshot-detail no-data truth, Monitoring run summaries, and the existing audit/terminal-notification contract for `baseline_capture`. ## UI / Surface Guardrail Plan - **Guardrail scope**: changed surfaces - **Native vs custom classification summary**: native - **Shared-family relevance**: status messaging, header actions, run-detail explanations, audit-aligned summaries - **State layers in scope**: page, detail - **Handling modes by drift class or surface**: review-mandatory - **Repository-signal treatment**: review-mandatory - **Special surface test profiles**: `standard-native-filament`, `monitoring-state-page` - **Required tests or manual smoke**: `functional-core`, `state-contract` - **Exception path and spread control**: none planned; any unavoidable message deviation must stay bounded to the existing baseline shared presenter/translator path - **Active feature PR close-out entry**: `Guardrail` ## Shared Pattern & System Fit - **Cross-cutting feature marker**: yes - **Systems touched**: baseline capture start surfaces, compare availability/readiness surfaces, baseline snapshot truth presentation, Monitoring run detail, audit prose, canonical reason translation - **Shared abstractions reused**: `BaselineReasonCodes`, `BaselineCompareStats`, `ReasonTranslator`, `OperationRunService`, `OperationUxPresenter`, `GovernanceRunDiagnosticSummaryBuilder`, `OperatorExplanationBuilder` - **New abstraction introduced? why?**: none planned. If inventory-eligibility logic needs reuse across start-time and runtime recheck, keep it as a narrow `BaselineCaptureService`-owned method or tiny baseline-local helper rather than a new registry/resolver layer. - **Why the existing abstraction was sufficient or insufficient**: Existing abstractions are sufficient for translation, explanation, and compare-readiness messaging. The current gap is that capture eligibility and no-data truth do not yet feed those shared paths consistently. - **Bounded deviation / spread control**: none ## Constitution Check *GATE: Passed before Phase 0 research. Re-check after Phase 1 design: still passed with no new persistence, no new UI framework, and no auth-plane drift.* | Gate | Status | Plan Notes | |------|--------|------------| | Inventory-first / read-write separation | PASS | The slice makes capture depend on the latest credible inventory truth and does not introduce any new Graph write or preview path. | | RBAC, workspace isolation, tenant isolation | PASS | No new routes or capabilities are introduced; existing `/admin`, `/admin/t/{tenant}`, and canonical Monitoring entitlement rules remain authoritative. | | Run observability / Ops-UX lifecycle | PASS | Existing `baseline_capture` `OperationRun` remains the queued-work truth. Known start-surface preconditions may still short-circuit with no run, while queued runtime rechecks will resolve through `OperationRunService` only. | | Shared pattern first | PASS | The plan extends existing reason translation and run-summary builders instead of adding page-local message trees. | | Proportionality / no premature abstraction | PASS | No new persistence or subsystem is planned. The only structural addition is a bounded extension of existing capture reason codes plus reuse of current services/presenters. | | Persisted truth / behavioral state | PASS | No new table or snapshot lifecycle state is added. No-data capture uses existing snapshot lifecycle/usability semantics if an artifact row is kept. | | Badge semantics / Filament-native discipline | PASS | Existing badge/outcome semantics remain centralized; touched surfaces stay on native Filament actions and shared presenters. | | Filament v5 / Livewire v4 contract | PASS | Provider registration, global-search posture, and destructive-action discipline remain unchanged and compliant. | | Test governance | PASS | Proof stays in focused baseline and Monitoring feature lanes without heavy-governance or browser expansion. | ## Test Governance Check - **Test purpose / classification by changed surface**: `Feature` for service/start-surface, compare-readiness, retained consumable success, snapshot-detail truth, and Monitoring truth - **Affected validation lanes**: `fast-feedback`, `confidence` - **Why this lane mix is the narrowest sufficient proof**: The business truth lives in existing capture execution, existing Filament surfaces, and existing Monitoring detail. Focused feature tests prove the slice end-to-end without widening into browser or heavy-governance families. - **Narrowest proving command(s)**: - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Baselines/BaselineCaptureTest.php` - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php tests/Feature/Filament/BaselineCompareLandingStartSurfaceTest.php tests/Feature/Filament/BaselineCaptureResultExplanationSurfaceTest.php` - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/OperationRunBaselineTruthSurfaceTest.php tests/Feature/Monitoring/GovernanceOperationRunSummariesTest.php tests/Feature/Authorization/OperatorExplanationSurfaceAuthorizationTest.php tests/Feature/Monitoring/AuditCoverageGovernanceTest.php tests/Feature/Notifications/OperationRunNotificationTest.php` - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Baselines/BaselineSnapshotBackfillTest.php` only if legacy empty-snapshot classification changes prove necessary during implementation - **Fixture / helper / factory / seed / context cost risks**: Moderate. The slice needs explicit inventory-run outcome fixtures (`no inventory`, `blocked`, `failed`, `unusable coverage`, `after-enqueue drift to non-credible`, `credible but zero subjects`, `previous complete snapshot still current`) and must keep those scenarios opt-in rather than adding new default helpers. - **Expensive defaults or shared helper growth introduced?**: No. New inventory eligibility scenarios should stay local to baseline capture tests. - **Heavy-family additions, promotions, or visibility changes**: none - **Surface-class relief / special coverage rule**: `standard-native relief` for profile/compare surfaces, `monitoring-state-page` for run-detail explanation assertions - **Closing validation and reviewer handoff**: Reviewers should verify that no test still encodes empty capture as unconditional success, that unusable coverage and after-enqueue prerequisite drift are proved explicitly, that `active_snapshot_id` never advances on blocked/zero-subject capture paths, that compare landing still derives readiness from consumable baseline truth, that snapshot detail distinguishes no-data evidence from current baseline truth, and that Monitoring, audit prose, and terminal notification copy lead with the same dominant cause before diagnostics. - **Budget / baseline / trend follow-up**: none expected beyond a small increase in baseline and Monitoring feature assertions - **Review-stop questions**: Did any new helper start hiding expensive inventory/run setup? Did the plan accidentally widen into compare-engine or generic artifact-state work? Did any runtime branch bypass `OperationRunService`? Did any surface add local copy that duplicates the shared reason/summary path? - **Escalation path**: `document-in-feature` unless legacy empty-snapshot backfill proves structurally necessary, in which case reassess inside this feature before widening further - **Active feature PR close-out entry**: `Guardrail` - **Why no dedicated follow-up spec is needed**: The slice is a bounded hardening change on one existing workflow and one existing operator truth family. Only a forced legacy-row reclassification problem would justify widening further. ## Project Structure ### Documentation (this feature) ```text specs/235-baseline-capture-truth/ ├── spec.md ├── plan.md ├── research.md ├── data-model.md ├── quickstart.md ├── checklists/ │ └── requirements.md └── tasks.md ``` No contracts artifact is planned because this feature changes no external API, route contract, or standalone logical interaction contract. ### Source Code (repository root) ```text apps/platform/ ├── app/ │ ├── Filament/ │ │ ├── Pages/ │ │ │ └── BaselineCompareLanding.php │ │ └── Resources/ │ │ ├── BaselineSnapshotResource/ │ │ │ └── Pages/ │ │ │ └── ViewBaselineSnapshot.php │ │ └── BaselineProfileResource/ │ │ └── Pages/ │ │ └── ViewBaselineProfile.php │ ├── Jobs/ │ │ └── CaptureBaselineSnapshotJob.php │ ├── Models/ │ │ ├── BaselineProfile.php │ │ └── BaselineSnapshot.php │ ├── Notifications/ │ │ └── OperationRunCompleted.php │ ├── Services/ │ │ ├── OperationRunService.php │ │ └── Baselines/ │ │ └── BaselineCaptureService.php │ └── Support/ │ ├── Baselines/ │ │ ├── BaselineCompareStats.php │ │ └── BaselineReasonCodes.php │ ├── OpsUx/ │ │ ├── GovernanceRunDiagnosticSummaryBuilder.php │ │ └── OperationUxPresenter.php │ ├── ReasonTranslation/ │ │ └── ReasonTranslator.php │ └── Ui/ │ └── OperatorExplanation/ │ └── OperatorExplanationBuilder.php └── tests/ ├── Feature/ │ ├── Authorization/ │ │ └── OperatorExplanationSurfaceAuthorizationTest.php │ ├── Baselines/ │ │ ├── BaselineCaptureTest.php │ │ └── BaselineSnapshotBackfillTest.php │ ├── Filament/ │ │ ├── BaselineCaptureResultExplanationSurfaceTest.php │ │ ├── BaselineCompareLandingStartSurfaceTest.php │ │ ├── BaselineProfileCaptureStartSurfaceTest.php │ │ └── OperationRunBaselineTruthSurfaceTest.php │ ├── Notifications/ │ │ └── OperationRunNotificationTest.php │ └── Monitoring/ │ ├── AuditCoverageGovernanceTest.php │ └── GovernanceOperationRunSummariesTest.php ``` **Structure Decision**: Keep the work entirely inside the existing Laravel runtime in `apps/platform`. The slice changes one existing queued workflow, two existing Filament start surfaces, one immutable snapshot detail surface, shared compare-readiness and explanation helpers, the existing audit/notification composition path, and focused regression families. No new module or subsystem is introduced. ## Complexity Tracking No constitutional violation is planned. No complexity exception is currently required. | Violation | Why Needed | Simpler Alternative Rejected Because | |-----------|------------|-------------------------------------| | — | — | — | ## Proportionality Review - **Current operator problem**: Baseline capture can report success even when no trustworthy baseline exists, which directly misleads operators and auditors. - **Existing structure is insufficient because**: `BaselineCaptureService` currently validates only profile/tenant/scope preconditions, and `CaptureBaselineSnapshotJob` promotes `active_snapshot_id` whenever a consumable snapshot exists or can be reused, including all-zero paths that are not decision-grade. - **Narrowest correct implementation**: Extend the existing capture reason-code family, reuse the existing snapshot lifecycle/usability model, add one shared inventory-eligibility evaluation path for start-time and runtime recheck, and adapt existing translator/stats/run-summary surfaces. - **Ownership cost created**: A few new reason-code translations, one extra eligibility branch in capture service/job, a small amount of extra run-context metadata, and focused regression fixtures for inventory-run truth. - **Alternative intentionally rejected**: A generic artifact-no-data framework or stale-inventory fallback. The first imports too much structure; the second would preserve false reassurance. - **Release truth**: current-release truth ## Phase 0 Research Summary - `BaselineCaptureService` is the current start-time gate and can reject capture without creating an `OperationRun`; it is the right place for latest-inventory eligibility preflight. - `CaptureBaselineSnapshotJob` currently updates `active_snapshot_id` whenever the resulting snapshot is consumable and currently treats `expected_items === 0` as a valid complete capture. That is the concrete root of the false-green/no-data promotion problem. - `BaselineReasonCodes`, `ReasonTranslator`, `BaselineCompareStats`, and `GovernanceRunDiagnosticSummaryBuilder` already centralize the operator language for baseline truth and Monitoring explanations; they are the right shared paths to extend. - `BaselineProfile::resolveCurrentConsumableSnapshot()` already falls back to the latest complete snapshot when `active_snapshot_id` is unusable, so preserving the previous trustworthy baseline is already supported if the capture path stops advancing `active_snapshot_id` incorrectly. - `OperationRunOutcome::PartiallySucceeded` already exists and is already rendered consistently across Ops UX, badges, and Monitoring; no new run-outcome family is needed. - Legacy empty-snapshot backfill currently classifies proven empty captures as `complete`. The mainline plan does not widen into migration/backfill unless implementation proves that historical empty snapshots still act as current truth in active runtime paths. ## Phase 1 Design Summary - `research.md` records the product and architectural decisions: strict latest-inventory truth, no stale fallback, no new snapshot state, and reuse of shared reason/summary infrastructure. - `data-model.md` documents the touched existing truths: `BaselineProfile.active_snapshot_id`, `BaselineSnapshot.lifecycle_state` plus completion metadata, and `OperationRun.context` keys for inventory eligibility and current-baseline-change effect. - `quickstart.md` gives the narrow validation order for service preflight, queued runtime recheck, no-data capture, compare-readiness truth, snapshot-detail truth, Monitoring explanation, and audit/notification alignment. - No contracts artifact is planned because this slice changes no external API or logical interaction contract. ## Phase 1 — Agent Context Update Run after artifact generation: - `.specify/scripts/bash/update-agent-context.sh copilot` ## Implementation Strategy ### Phase A — Extend capture eligibility around the latest credible inventory run **Goal**: Make capture start and queued execution agree on whether the latest relevant inventory basis is trustworthy enough to build a baseline. | Step | File | Change | |------|------|--------| | A.1 | `apps/platform/app/Support/Baselines/BaselineReasonCodes.php` | Add the bounded capture reason codes for missing latest inventory, blocked latest inventory, failed latest inventory, unusable coverage, and zero-subject outcome. Keep them in the existing reason-code family. | | A.2 | `apps/platform/app/Services/Baselines/BaselineCaptureService.php` | Extend `validatePreconditions()` with a reusable latest-inventory eligibility decision that inspects the most recent relevant inventory sync and returns the new capture reason codes without creating an `OperationRun` when the block is already known at start time. | | A.3 | `apps/platform/app/Support/ReasonTranslation/ReasonTranslator.php` | Add operator-safe translations and next steps for the new baseline-capture reason codes so profile/start-surface, Monitoring, and audit-aligned prose stay consistent. | | A.4 | `apps/platform/app/Jobs/CaptureBaselineSnapshotJob.php` | Re-check the same eligibility after the run starts, so prerequisite drift between page load and execution resolves through `OperationRunService` with `completed + blocked` rather than a false green run. | ### Phase B — Stop no-data captures from becoming current baseline truth **Goal**: Treat zero-subject capture as real audit evidence with follow-up, not as a trustworthy baseline refresh, and keep compare readiness anchored to the same consumable-truth contract. | Step | File | Change | |------|------|--------| | B.1 | `apps/platform/app/Jobs/CaptureBaselineSnapshotJob.php` | Split the zero-subject path from the normal consumable-snapshot path before any existing consumable snapshot is reused or `active_snapshot_id` is advanced. | | B.2 | `apps/platform/app/Jobs/CaptureBaselineSnapshotJob.php` | Map zero-subject capture to `OperationRunOutcome::PartiallySucceeded`, record the new reason code in run context, keep numeric `summary_counts`, record `baseline_capture.subjects_total`, record `result.snapshot_lifecycle` when an artifact exists, and record whether current baseline truth changed. | | B.3 | `apps/platform/app/Models/BaselineSnapshot.php` and job call sites | Reuse the existing lifecycle/usability model if a no-data artifact row is retained: mark it non-consumable via existing incomplete semantics and store the finalization reason in `completion_meta_jsonb` rather than introducing a new snapshot state. | | B.4 | `apps/platform/app/Models/BaselineProfile.php` and job promotion path | Preserve the previously consumable snapshot by ensuring `active_snapshot_id` is updated only when the new capture result is actually consumable. | | B.5 | `apps/platform/app/Filament/Resources/BaselineSnapshotResource/Pages/ViewBaselineSnapshot.php` and `apps/platform/app/Filament/Resources/BaselineProfileResource/Pages/ViewBaselineProfile.php` | Distinguish current trustworthy baseline truth from no-data evidence on snapshot and profile detail surfaces so operators do not read a zero-subject artifact as a normal refresh. | | B.6 | `apps/platform/app/Support/Baselines/BaselineCompareStats.php` | Extend compare-readiness and missing-snapshot guidance so compare landing and profile-level compare affordances can explain why compare is unavailable after a blocked, failed, or zero-subject capture without inferring success from snapshot existence or the latest run alone. | | B.7 | `apps/platform/app/Filament/Pages/BaselineCompareLanding.php` | Keep compare availability derived from consumable baseline truth and show the updated explanation-first guidance when the latest capture failed, drifted to a non-credible prerequisite, or produced no usable baseline. | ### Phase C — Align Monitoring explanation and shared audit/notification copy with the hardened capture truth **Goal**: Make Monitoring and the existing completion summary path speak the same truthful baseline-capture language as the hardened capture and compare-readiness surfaces. | Step | File | Change | |------|------|--------| | C.1 | `apps/platform/app/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilder.php` | Teach baseline-capture summaries to distinguish blocked latest-inventory prerequisites, after-enqueue prerequisite drift, and zero-subject no-data captures from normal success before diagnostics are shown. | | C.2 | `apps/platform/app/Support/OpsUx/OperationUxPresenter.php`, `apps/platform/app/Support/Ui/OperatorExplanation/OperatorExplanationBuilder.php`, and `apps/platform/app/Notifications/OperationRunCompleted.php` | Keep Monitoring, audit prose, and terminal notification copy aligned to the same dominant baseline-capture reason, whether current baseline truth changed, and initiator-aware notification rules. | ### Phase D — Audit, test, and edge-condition follow-through **Goal**: Lock the hardened truth into the existing regression families and keep historical edge cases explicit. | Step | File | Change | |------|------|--------| | D.1 | `apps/platform/tests/Feature/Baselines/BaselineCaptureTest.php` | Replace the implicit “empty capture succeeds” assumption with explicit coverage for no inventory, blocked inventory, failed inventory, unusable coverage, after-enqueue prerequisite drift, zero subjects, and previous snapshot preservation. | | D.2 | `apps/platform/tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php`, `apps/platform/tests/Feature/Filament/BaselineCompareLandingStartSurfaceTest.php`, `apps/platform/tests/Feature/Filament/BaselineCaptureResultExplanationSurfaceTest.php` | Prove capture preflight messaging, compare readiness, snapshot-detail no-data truth, and no-data explanation on the affected Filament surfaces. | | D.3 | `apps/platform/tests/Feature/Filament/OperationRunBaselineTruthSurfaceTest.php`, `apps/platform/tests/Feature/Monitoring/GovernanceOperationRunSummariesTest.php`, `apps/platform/tests/Feature/Monitoring/AuditCoverageGovernanceTest.php`, and `apps/platform/tests/Feature/Notifications/OperationRunNotificationTest.php` | Prove Monitoring detail separates blocked/no-data capture truth from raw counts and generic success wording, and that audit summary plus terminal notification copy preserve the same dominant reason with initiator-aware delivery rules. | | D.4 | `apps/platform/tests/Feature/Authorization/OperatorExplanationSurfaceAuthorizationTest.php` | Keep the authorized happy-path surface access proof explicit and preserve 404 vs 403 semantics on the touched explanation-first surfaces. | | D.5 | `apps/platform/tests/Feature/Baselines/BaselineSnapshotBackfillTest.php` | Only if implementation proves legacy empty snapshots still participate in active runtime truth, adjust the legacy classification rule and regression accordingly inside this feature instead of adding a second follow-up spec. | ## Risks and Mitigations - **Local copy drift on capture surfaces**: Existing Filament actions currently branch on reason code locally. Mitigation: converge on `ReasonTranslator` instead of adding more local message cases. - **Zero-subject path still reuses a historical empty complete snapshot**: Current job flow can reuse an existing consumable snapshot before creating a new one. Mitigation: short-circuit zero-subject handling before `findExistingConsumableSnapshot()` or any `active_snapshot_id` promotion logic can make it authoritative. - **Queued runtime recheck bypasses Ops-UX rules**: It is easy to update context only and forget terminal run outcome. Mitigation: all blocked/partial terminal states remain service-owned through `OperationRunService` and keep numeric summary counts. - **Legacy empty backfill broadens the slice unexpectedly**: Historical classification may need adjustment if runtime truth still depends on it. Mitigation: treat it as a conditional step inside this feature, only if a focused regression proves it is necessary. ## Post-Design Re-check The package remains constitution-compliant, Livewire v4 / Filament v5 compliant, and narrow. It introduces no new persistence, no new UI framework, no new auth plane, and no new operation type. It reuses the existing baseline snapshot lifecycle/usability truth and the existing shared reason/Monitoring explanation paths, and the generated implementation artifacts are aligned for execution.