TenantAtlas/specs/119-baseline-drift-engine/tasks.md
ahmido da1adbdeb5 Spec 119: Drift cutover to Baseline Compare (golden master) (#144)
Implements Spec 119 (Drift Golden Master Cutover):

- Baseline Compare is the only drift writer (`source = baseline.compare`).
- Drift findings now store diff-compatible `evidence_jsonb` (summary.kind, baseline/current policy_version_id refs, fidelity + provenance).
- Findings UI renders one-sided diffs for `missing_policy`/`unexpected_policy` when a single ref exists; otherwise shows explicit “diff unavailable”.
- Removes legacy drift generator runtime (jobs/services/UI) and related tests.
- Adds one-time migration to delete legacy drift findings (`finding_type=drift` where source is null or != baseline.compare).
- Scopes baseline capture & landing duplicate warnings to latest completed inventory sync.
- Canonicalizes compliance `scheduledActionsForRule` drift signal and keeps legacy snapshots comparable.

Tests:
- `vendor/bin/sail artisan test --compact` (full suite per tasks)
- Focused pack: BaselinePolicyVersionResolverTest, BaselineCompareDriftEvidenceContractTest, DriftFindingDiffUnavailableTest, LegacyDriftFindingsCleanupMigrationTest, ComplianceNoncomplianceActionsDriftTest

Notes:
- Livewire v4+ / Filament v5 compatible (no legacy APIs).
- No new external dependencies.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #144
2026-03-06 14:30:49 +00:00

228 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
description: "Task list for Spec 119 implementation"
---
# Tasks: Drift Golden Master Cutover (Baseline Compare) (Spec 119)
**Input**: Design documents from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/119-baseline-drift-engine/`
**Prerequisites**:
- `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/119-baseline-drift-engine/plan.md` (required)
- `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/119-baseline-drift-engine/spec.md` (required)
- `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/119-baseline-drift-engine/research.md`
- `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/119-baseline-drift-engine/data-model.md`
- `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/119-baseline-drift-engine/contracts/drift.openapi.yaml`
- `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/119-baseline-drift-engine/quickstart.md`
**Tests**: REQUIRED (Pest) because this feature changes runtime behavior.
**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
---
## Phase 1: Setup (Shared Infrastructure)
**Purpose**: Local readiness + baseline validation before changing runtime behavior
- [X] T001 Start local stack and run migrations using `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/vendor/bin/sail` (up -d, artisan migrate)
- [X] T002 Run a baseline test subset using `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/vendor/bin/sail` to confirm a green starting point (artisan test --compact --filter=BaselineCompare)
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Shared building blocks used across user stories (evidence contract + resolvers)
**⚠️ CRITICAL**: Complete this phase before starting any user story work.
- [X] T003 Create baseline policy-version resolver in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Services/Baselines/Evidence/BaselinePolicyVersionResolver.php` (resolve baseline `policy_version_id` deterministically from baseline snapshot item identity `policy_type` + `subject_key` and baseline evidence provenance `observed_at`; return null when no match)
- [X] T004 [P] Add resolver unit tests in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Baselines/BaselinePolicyVersionResolverTest.php` (covers: found, not found, invalid observed_at, deterministic tie-breaker when multiple candidates exist)
- [X] T005 [P] Add drift evidence contract assertion helper in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Support/AssertsDriftEvidenceContract.php` (required keys, allowed `summary.kind`, provenance keys, fidelity algorithm, and diff-renderability rule)
**Checkpoint**: Contract helper + resolver exist; US1 tests can be written against them.
---
## Phase 3: User Story 1 — Understand drift with consistent diffs (Priority: P1) 🎯 MVP
**Goal**: Baseline Compare drift findings carry diff-compatible evidence so operators get consistent diffs when content evidence exists, and a clear “diff unavailable” explanation when it does not.
**Independent Test**: Run Baseline Compare to produce (a) a `different_version` drift finding with both refs, (b) a `missing_policy` or `unexpected_policy` finding with a single required ref, and (c) a meta-only finding, then verify the finding detail view renders the correct diff or an explicit “diff unavailable” explanation.
### Tests for User Story 1 (write first) ⚠️
- [X] T006 [P] [US1] Add Baseline Compare evidence-contract tests in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/Baselines/BaselineCompareDriftEvidenceContractTest.php` (different_version + missing_policy; asserts `source`, `change_type` semantics unchanged, `summary.kind`, baseline/current `policy_version_id`, fidelity algorithm, and `evidence_jsonb.provenance.*` keys including `compare_operation_run_id`)
- [X] T007 [P] [US1] Add FindingResource “diff unavailable” regression test in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/Drift/DriftFindingDiffUnavailableTest.php` (missing baseline/current refs → explicit message)
- [X] T008 [P] [US1] Update Baseline Compare findings tests for new evidence shape in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/Baselines/BaselineCompareFindingsTest.php` (replace `current_hash`/`baseline_hash` assertions with nested baseline/current evidence + `summary.kind`)
### Implementation for User Story 1
- [X] T009 [US1] Upgrade Baseline Compare drift evidence schema in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Jobs/CompareBaselineToTenantJob.php` (write `evidence_jsonb.summary.kind`, `evidence_jsonb.baseline.policy_version_id`, `evidence_jsonb.current.policy_version_id`, compute `evidence_jsonb.fidelity`, and write `evidence_jsonb.provenance.{baseline_profile_id,baseline_snapshot_id,compare_operation_run_id,inventory_sync_run_id}`)
- [X] T010 [US1] Populate `evidence_jsonb.current.policy_version_id` from content evidence meta in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Jobs/CompareBaselineToTenantJob.php` (use `ResolvedEvidence.meta.policy_version_id` when available; else null)
- [X] T011 [US1] Populate `evidence_jsonb.baseline.policy_version_id` via `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Services/Baselines/Evidence/BaselinePolicyVersionResolver.php` and integrate into `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Jobs/CompareBaselineToTenantJob.php` (when baseline snapshot provenance indicates content evidence + has `observed_at`, attempt resolve; otherwise null)
- [X] T012 [US1] Implement `summary.kind` selection in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Jobs/CompareBaselineToTenantJob.php` by “stealing” dimension detection logic from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Services/Drift/DriftFindingGenerator.php` (prefer settings snapshot changes; else assignments; else scope tags; fallback policy_snapshot)
- [X] T013 [US1] Ensure `findings.evidence_fidelity` and `evidence_jsonb.fidelity` stay aligned in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Jobs/CompareBaselineToTenantJob.php` (compute deterministically from policy-version refs: both present = `content`, exactly one = `mixed`, none = `meta`)
- [X] T014 [US1] Enforce diff-renderability rule in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Resources/FindingResource.php` (`different_version` requires both refs; `missing_policy`/`unexpected_policy` render against an empty side when their single required ref exists; otherwise show explicit “diff unavailable” explanation in the Diff section)
- [X] T015 [US1] Run US1-focused tests via `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/vendor/bin/sail` (artisan test --compact --filter=BaselineCompareDriftEvidenceContract; artisan test --compact --filter=DriftFindingDiffUnavailable)
**Checkpoint**: Baseline Compare findings show consistent diffs for content evidence; meta-only shows “diff unavailable” without errors.
---
## Phase 4: User Story 2 — Eliminate “two truths” for drift (Priority: P2)
**Goal**: Operators only see Baseline Compare as the drift engine; no legacy “Generate drift” UI or source switching remains.
**Independent Test**: After the cutover, verify all drift findings shown in the UI are Baseline Compare-origin and no UI surface offers a legacy “Generate drift” action or “source” switching.
### Tests for User Story 2 (write first) ⚠️
- [X] T016 [P] [US2] Remove/replace DriftLanding enforcement tests in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/Rbac/DriftLandingUiEnforcementTest.php` (assert drift entry point is Baseline Compare landing instead)
### Implementation for User Story 2
- [X] T017 [US2] Remove legacy Drift landing surface by deleting `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Pages/DriftLanding.php` and `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/resources/views/filament/pages/drift-landing.blade.php`
- [X] T018 [US2] Update related-run links to point drift to Baseline Compare landing in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/OperationRunLinks.php` (remove DriftLanding import; add BaselineCompareLanding link for baseline_compare runs)
- [X] T019 [US2] Update dashboard attention widget to use Baseline Compare runs (not drift_generate) in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Widgets/Dashboard/NeedsAttention.php` (stale/failed checks + URLs → Baseline Compare landing / Operations)
- [X] T020 [US2] Remove DriftLanding action-surface exemption in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php`
- [X] T021 [US2] Update Filament auth allowlist to remove DriftLanding in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/Guards/NoAdHocFilamentAuthPatternsTest.php`
- [X] T022 [US2] Run US2-focused tests via `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/vendor/bin/sail` (artisan test --compact --filter=BaselineCompareLanding; artisan test --compact --filter=NeedsAttention)
**Checkpoint**: No DriftLanding surface exists; “Drift” entry point routes to Baseline Compare landing.
---
## Phase 5: User Story 3 — Clean cutover & legacy removal (Priority: P3)
**Goal**: Hard cut: legacy run-to-run drift generator is removed end-to-end and legacy drift findings are deleted so operators never encounter mixed states.
**Independent Test**: After deployment and the one-time cleanup step, verify legacy drift findings no longer exist, legacy drift generation cannot be started, and Baseline Compare drift continues to work.
### Tests for User Story 3 (write first) ⚠️
- [X] T023 [P] [US3] Add cleanup migration test in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/Drift/LegacyDriftFindingsCleanupMigrationTest.php` (deletes `finding_type=drift` where `source` is null or != baseline.compare; keeps baseline.compare)
- [X] T024 [P] [US3] Update alert + monitoring tests to remove drift_generate_findings references in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/Alerts/BaselineCompareFailedAlertTest.php` and `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/RunAuthorizationTenantIsolationTest.php`
- [X] T025 [P] [US3] Remove legacy drift generator tests tied to DriftLanding/GenerateDriftFindingsJob/DriftFindingGenerator in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/Drift/` (delete or rewrite: DriftGenerationDispatchTest.php, DriftLandingCopyTest.php, DriftLandingShowsComparisonInfoTest.php, GenerateDriftFindingsJobNotificationTest.php, Drift*DriftDetectionTest.php, DriftGenerationDeterminismTest.php, DriftTenantIsolationTest.php)
### Implementation for User Story 3
- [X] T026 [US3] Delete legacy drift generation runtime code: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Jobs/GenerateDriftFindingsJob.php`, `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Services/Drift/DriftFindingGenerator.php`, `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Services/Drift/DriftRunSelector.php`, `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Services/Drift/DriftScopeKey.php`
- [X] T027 [US3] Remove legacy drift operation type from catalogs + triage: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/OperationRunType.php`, `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/OperationCatalog.php`, `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Services/SystemConsole/OperationRunTriageService.php` (no drift_generate_findings label/duration/retry/cancel)
- [X] T028 [US3] Remove legacy drift-generate alert event producer in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Jobs/Alerts/EvaluateAlertsJob.php` (drop compareFailedEvents() and its call site)
- [X] T029 [US3] Add one-time cleanup migration in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/database/migrations/2026_03_05_000001_delete_legacy_drift_findings.php` (delete `finding_type=drift` where `source` is null or <> baseline.compare)
- [X] T030 [US3] Remove remaining legacy drift references discovered by search in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/` and `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/resources/` (target: no DriftLanding / drift_generate_findings strings outside historical migrations/specs; explicitly audit Findings UI for legacy-source badges/filters/labels or “source switching” states)
- [X] T031 [US3] Run US3-focused tests via `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/vendor/bin/sail` (artisan test --compact --filter=Alerts; artisan test --compact --filter=OperationRun; artisan test --compact --filter=Drift)
**Checkpoint**: Legacy generator is gone; DB cleanup migration removes legacy drift findings; all tests green.
---
## Phase N: Polish & Cross-Cutting Concerns
**Purpose**: Final quality pass across all user stories
- [X] T032 [P] Format changed files using `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/vendor/bin/sail` (php vendor/bin/pint)
- [X] T033 Run full test suite using `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/vendor/bin/sail` (artisan test)
- [X] T034 [P] Validate manual smoke steps remain accurate in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/119-baseline-drift-engine/quickstart.md` (update only if behavior/screens changed)
- [X] T035 [P] Update Spec 119 docs for one-sided drift diff rendering in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/119-baseline-drift-engine/` (spec, plan, research, data-model, quickstart)
- [X] T036 [US1] Extend `FindingResource` one-sided diff rendering in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Resources/FindingResource.php` (`unexpected_policy` => added against empty baseline, `missing_policy` => removed against empty current)
- [X] T037 [P] Extend drift view regressions in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/Drift/DriftFindingDiffUnavailableTest.php` (cover one-sided empty-side rendering and explicit unavailable messaging)
- [X] T038 [US1] Scope baseline snapshot capture to the latest completed Inventory Sync in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Jobs/CaptureBaselineSnapshotJob.php` (ignore stale `inventory_items` rows from older sync runs when deriving subject-key matches; record `baseline_capture.inventory_sync_run_id` for operability)
- [X] T039 [P] Add baseline-capture stale-inventory regression coverage in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/Baselines/BaselineCaptureAmbiguousMatchGapTest.php` (duplicates in the same latest sync still gap; stale duplicates from older syncs no longer do)
- [X] T040 [P] Update Spec 119 docs for baseline-capture latest-sync scoping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/119-baseline-drift-engine/` (spec, plan, research, data-model, quickstart)
- [X] T041 [US1] Reuse same-run full-content capture evidence in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Jobs/CompareBaselineToTenantJob.php` (overlay compare-time captured/reused `policy_versions` before the `since`-based resolver so unchanged policies do not become `missing_current`)
- [X] T042 [P] Add full-content compare reuse regression in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/Baselines/BaselineCompareWhyNoFindingsReasonCodeTest.php` (reused identical compare-purpose version still yields `no_drift_detected`, not `evidence_capture_incomplete`)
- [X] T043 [P] Update Spec 119 docs for reused compare-evidence handling in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/119-baseline-drift-engine/` (spec, plan, research, quickstart)
- [X] T044 [US1] Scope Baseline Compare landing duplicate-name warnings to the latest completed Inventory Sync in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Baselines/BaselineCompareStats.php` (historical stale duplicates must not keep the warning banner visible)
- [X] T045 [P] Add landing-page stale-duplicate regression in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/Filament/BaselineCompareLandingDuplicateNamesBannerTest.php` (latest sync clean ⇒ no warning, latest sync duplicate ⇒ warning remains)
- [X] T046 [P] Update Spec 119 docs for landing-page duplicate warning scoping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/119-baseline-drift-engine/` (spec, research)
- [X] T047 [US1] Canonicalize compliance `scheduledActionsForRule` in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Services/Intune/CompliancePolicyNormalizer.php` (include semantic noncompliance action fields in the drift signal; ignore opaque IDs/order-only noise)
- [X] T048 [P] Add unit coverage for canonical compliance action drift normalization in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/CompliancePolicyNormalizerTest.php` (grace-period/template signal, stable ordering, ignored internal IDs)
- [X] T049 [US1] Recompute effective baseline content hashes from resolved baseline `policy_versions` in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Jobs/CompareBaselineToTenantJob.php` (keep existing content-backed baseline snapshots comparable when drift-signal semantics expand)
- [X] T050 [P] Add compare regressions + docs for compliance noncompliance action drift in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/BaselineDriftEngine/ComplianceNoncomplianceActionsDriftTest.php` and `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/119-baseline-drift-engine/` (unchanged legacy-hash snapshot stays quiet; changed grace/action semantics create drift)
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: No dependencies - can start immediately
- **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories
- **User Stories (Phase 3+)**: All depend on Foundational phase completion
- Stories can proceed in priority order (P1 → P2 → P3)
- Or in parallel after Phase 2 if staffed (be mindful of overlapping files)
- **Polish (Final Phase)**: Depends on all desired user stories being complete
### User Story Dependencies
- **US1 (P1)**: No dependencies after Phase 2; delivers the MVP (diff-compatible evidence + diff guardrails)
- **US2 (P2)**: Depends on US1 being stable in UI terms (so removing DriftLanding doesnt remove drift visibility)
- **US3 (P3)**: Can start after Phase 2, but recommended after US1/US2 so the cutover is clean and operators still have a drift workflow
### Parallel Opportunities (examples)
- Tests marked `[P]` can be written in parallel with implementation (and should fail before the fix lands).
- Deletions of legacy drift files in US3 can be parallelized with catalog/triage cleanup (different files).
---
## Parallel Example: US1
```bash
# In parallel (different files):
Task: "Add Baseline Compare evidence-contract tests in /Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/Baselines/BaselineCompareDriftEvidenceContractTest.php"
Task: "Implement evidence schema upgrade in /Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Jobs/CompareBaselineToTenantJob.php"
Task: "Add diff-unavailable regression test in /Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/Drift/DriftFindingDiffUnavailableTest.php"
```
---
## Parallel Example: US2
```bash
# In parallel (different files):
Task: "Delete Drift landing surface in /Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Pages/DriftLanding.php"
Task: "Update dashboard widget links in /Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Widgets/Dashboard/NeedsAttention.php"
Task: "Update run links in /Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/OperationRunLinks.php"
```
---
## Parallel Example: US3
```bash
# In parallel (different files):
Task: "Add cleanup migration in /Users/ahmeddarrazi/Documents/projects/TenantAtlas/database/migrations/2026_03_05_000001_delete_legacy_drift_findings.php"
Task: "Remove legacy drift code in /Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Jobs/GenerateDriftFindingsJob.php"
Task: "Update triage/catalog in /Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/OperationCatalog.php"
```
---
## Implementation Strategy
### MVP First (User Story 1 Only)
1. Complete Phase 1: Setup
2. Complete Phase 2: Foundational (CRITICAL - blocks all stories)
3. Complete Phase 3: User Story 1
4. **STOP and VALIDATE**: Test User Story 1 independently
5. Deploy/demo if ready
### Incremental Delivery
1. Complete Setup + Foundational → Foundation ready
2. Add User Story 1 → Test independently → Deploy/Demo (MVP!)
3. Add User Story 2 → Test independently → Deploy/Demo
4. Add User Story 3 → Test independently → Deploy/Demo
5. Each story adds value without breaking previous stories
---
## Notes
- `[P]` tasks = different files, no dependencies
- `[US#]` label maps task to specific user story for traceability
- Each user story should be independently completable and testable
- Prefer Sail for all local commands: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/vendor/bin/sail`