Compare commits
1 Commits
dev
...
212-test-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9fdd847a6 |
8
.github/agents/copilot-instructions.md
vendored
8
.github/agents/copilot-instructions.md
vendored
@ -7,7 +7,6 @@ ## Relocation override
|
||||
- Human-facing commands should use `cd apps/platform && ...`.
|
||||
- Repo-root tooling may delegate via `./scripts/platform-sail` when it cannot set a nested working directory.
|
||||
- Repo-root JavaScript orchestration uses `corepack pnpm install`, `corepack pnpm dev:platform`, `corepack pnpm dev:website`, `corepack pnpm dev`, `corepack pnpm build:website`, and `corepack pnpm build:platform`.
|
||||
- `corepack pnpm dev:platform` starts the platform Sail stack and the Laravel panel Vite watcher. `corepack pnpm dev` starts that platform watcher plus the website dev server.
|
||||
- `apps/website` is a standalone Astro app, not a second Laravel runtime, so Boost MCP remains platform-only.
|
||||
- If any generated technology note below conflicts with the current repo, trust `apps/platform/composer.json`, `apps/platform/package.json`, and the live Laravel application metadata over stale generated entries.
|
||||
|
||||
@ -203,9 +202,6 @@ ## Active Technologies
|
||||
- SQLite `:memory:` for lane execution, filesystem artifacts under `apps/platform/storage/logs/test-lanes`, staged CI bundles under `.gitea-artifacts/<workflow-profile>`, bounded derived trend/history artifacts adjacent to current lane artifacts, and no new product database persistence (211-runtime-trend-recalibration)
|
||||
- Markdown for repository governance artifacts, JSON Schema plus logical OpenAPI for planning contracts, and Bash-backed SpecKit scripts already present in the repo + `.specify/memory/constitution.md`, `.specify/templates/spec-template.md`, `.specify/templates/plan-template.md`, `.specify/templates/tasks-template.md`, `.specify/templates/checklist-template.md`, `.specify/README.md`, `README.md`, and the existing Specs 206 through 211 governance vocabulary (212-test-authoring-guardrails)
|
||||
- Repository-owned markdown and contract artifacts under `.specify/`, `specs/212-test-authoring-guardrails/`, and root documentation files; no product database persistence (212-test-authoring-guardrails)
|
||||
- PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `WorkspaceContext`, `OperateHubShell`, `EnsureFilamentTenantSelected`, `WorkspaceRedirectResolver`, `WorkspaceIntendedUrl`, `TenantPageCategory`, and `ResolvesPanelTenantContext` (199-global-context-shell-contract)
|
||||
- PostgreSQL unchanged plus existing Laravel session keys `current_workspace_id`, `workspace_intended_url`, and `workspace_last_tenant_ids`; no schema change planned (199-global-context-shell-contract)
|
||||
- Markdown governance artifacts in a PHP 8.4.15 / Laravel 12 / Filament v5 / Livewire v4 repository + `.specify/memory/constitution.md`, `docs/ui/operator-ux-surface-standards.md`, adjacent Specs 196 through 199, existing UI rule IDs `UI-SURF-001`, `ACTSURF-001`, `UI-HARD-001`, `UI-EX-001`, `UI-FIL-001`, `DECIDE-001`, and `UX-001` (200-filament-surface-rules)
|
||||
|
||||
- PHP 8.4.15 (feat/005-bulk-operations)
|
||||
|
||||
@ -240,8 +236,8 @@ ## Code Style
|
||||
PHP 8.4.15: Follow standard conventions
|
||||
|
||||
## Recent Changes
|
||||
- 200-filament-surface-rules: Added Markdown governance artifacts in a PHP 8.4.15 / Laravel 12 / Filament v5 / Livewire v4 repository + `.specify/memory/constitution.md`, `docs/ui/operator-ux-surface-standards.md`, adjacent Specs 196 through 199, existing UI rule IDs `UI-SURF-001`, `ACTSURF-001`, `UI-HARD-001`, `UI-EX-001`, `UI-FIL-001`, `DECIDE-001`, and `UX-001`
|
||||
- 199-global-context-shell-contract: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `WorkspaceContext`, `OperateHubShell`, `EnsureFilamentTenantSelected`, `WorkspaceRedirectResolver`, `WorkspaceIntendedUrl`, `TenantPageCategory`, and `ResolvesPanelTenantContext`
|
||||
- 212-test-authoring-guardrails: Added Markdown for repository governance artifacts, JSON Schema plus logical OpenAPI for planning contracts, and Bash-backed SpecKit scripts already present in the repo + `.specify/memory/constitution.md`, `.specify/templates/spec-template.md`, `.specify/templates/plan-template.md`, `.specify/templates/tasks-template.md`, `.specify/templates/checklist-template.md`, `.specify/README.md`, `README.md`, and the existing Specs 206 through 211 governance vocabulary
|
||||
- 211-runtime-trend-recalibration: Added PHP 8.4.15 for repo-truth governance logic, Bash for repo-root wrappers, GitHub-compatible Gitea Actions workflow YAML under `.gitea/workflows/`, plus JSON Schema and logical OpenAPI for repository contracts + Laravel 12, Pest v4, PHPUnit 12, Filament v5, Livewire v4, Laravel Sail, Gitea Actions backed by `act_runner`, uploaded artifact bundles, and the existing `Tests\Support\TestLaneManifest`, `TestLaneBudget`, and `TestLaneReport` seams
|
||||
- 210-ci-matrix-budget-enforcement: Added PHP 8.4.15 for repo-truth test governance, Bash for repo-root wrappers, and GitHub-compatible Gitea Actions workflow YAML under `.gitea/workflows/` + Laravel 12, Pest v4, PHPUnit 12, Filament v5, Livewire v4, Laravel Sail, Gitea Actions backed by `act_runner`, and the existing `Tests\Support\TestLaneManifest`, `TestLaneBudget`, and `TestLaneReport` seams
|
||||
<!-- MANUAL ADDITIONS START -->
|
||||
<!-- MANUAL ADDITIONS END -->
|
||||
|
||||
1
.github/copilot-instructions.md
vendored
1
.github/copilot-instructions.md
vendored
@ -293,7 +293,6 @@ ## Application Structure & Architecture
|
||||
|
||||
## Workspace Commands
|
||||
- Repo-root JavaScript orchestration now uses `corepack pnpm install`, `corepack pnpm dev:platform`, `corepack pnpm dev:website`, `corepack pnpm dev`, `corepack pnpm build:website`, and `corepack pnpm build:platform`.
|
||||
- `corepack pnpm dev:platform` starts the platform Sail stack and the Laravel panel Vite watcher. `corepack pnpm dev` starts that platform watcher plus the website dev server.
|
||||
- `apps/website` is a standalone Astro app, not a second Laravel runtime, so Boost MCP remains platform-only.
|
||||
|
||||
## Frontend Bundling
|
||||
|
||||
@ -1,19 +1,33 @@
|
||||
<!--
|
||||
Sync Impact Report
|
||||
|
||||
- Version change: 2.5.0 -> 2.6.0
|
||||
- Version change: 2.4.0 -> 2.5.0
|
||||
- Modified principles:
|
||||
- UI surface taxonomy and review expectations: expanded with native
|
||||
vs custom classification, shared-detail host ownership, named
|
||||
anti-patterns, and shell/page/detail state ownership review
|
||||
- Filament Native First / No Ad-hoc Styling (UI-FIL-001): expanded
|
||||
into explicit native-by-default, fake-native, shared-family, and
|
||||
exception-boundary language
|
||||
- Test Suite Governance Must Live In The Delivery Workflow
|
||||
(TEST-GOV-001): expanded into explicit test-impact disclosure,
|
||||
lane discipline, minimal-fixture defaults, heavy-family visibility,
|
||||
expensive-default bans, runtime-budget stewardship, review-stop
|
||||
rules, and escalation triggers
|
||||
- Governance review expectations: expanded to require test-purpose
|
||||
classification, explicit runtime-cost review, and visible review
|
||||
routine coverage in delivery artifacts
|
||||
- Added sections: None
|
||||
- Removed sections: None
|
||||
- Templates requiring updates:
|
||||
- None in this docs-only constitution slice; enforcement remains
|
||||
deferred to Spec 201
|
||||
- ✅ .specify/memory/constitution.md
|
||||
- ✅ .specify/templates/plan-template.md (lane-discipline and
|
||||
escalation-planning checks expanded)
|
||||
- ✅ .specify/templates/spec-template.md (test-purpose,
|
||||
lane-discipline, heavy-family, and escalation prompts expanded)
|
||||
- ✅ .specify/templates/tasks-template.md (task obligations expanded for
|
||||
classification, cheap defaults, review-stop rules, and runtime
|
||||
stewardship)
|
||||
- ✅ .specify/templates/checklist-template.md (review checklist guidance
|
||||
expanded for lane fit, heavy risk, and escalation)
|
||||
- ✅ .specify/README.md (SpecKit workflow expectations expanded for
|
||||
visible test-governance coverage)
|
||||
- ✅ README.md (developer workflow guidance expanded for lane
|
||||
discipline and runtime stewardship)
|
||||
- Commands checked:
|
||||
- N/A `.specify/templates/commands/*.md` directory is not present in this repo
|
||||
- Follow-up TODOs: None
|
||||
@ -565,24 +579,6 @@ ##### Detail-first Operational Surface
|
||||
- Destructive actions: detail header or grouped header actions only, always with confirmation.
|
||||
- Row click and explicit View/Inspect: not applicable.
|
||||
|
||||
##### Native vs custom and shared-family classification
|
||||
- Every operator-facing surface MUST also classify whether it is a
|
||||
`Native Surface`, a `Custom Surface`, or a `Shared Detail Micro-UI`
|
||||
embedded inside a `Host`.
|
||||
- `Native Surface` means the primary interaction contract is expressed
|
||||
through Filament-native components or approved shared primitives.
|
||||
- `Custom Surface` means the operator need is materially richer than
|
||||
standard CRUD, overview, or report semantics and the deviation is
|
||||
justified through UI-EX-001.
|
||||
- `Shared Detail Micro-UI` means a repeated embedded review, evidence,
|
||||
or detail surface that appears in more than one host and must read as
|
||||
the same family wherever it appears.
|
||||
- `Host` means the page, resource, workbench, or detail surface that
|
||||
embeds a shared detail micro-UI and owns routing, authorization, the
|
||||
outer inspect/open model, and host-only actions.
|
||||
- A `Fake-Native Surface` is never an allowed classification. It is a
|
||||
violation class defined by UI-HARD-001 and UI-FIL-001.
|
||||
|
||||
#### Action Surface Discipline (ACTSURF-001)
|
||||
|
||||
Goal: actions across all surfaces MUST make the next sensible operator
|
||||
@ -653,22 +649,6 @@ ##### Utility / System surfaces
|
||||
- System or recovery status does not justify casual placement of
|
||||
destructive or governance-changing actions.
|
||||
|
||||
##### Shared detail families and one primary interaction model
|
||||
|
||||
- A shared detail micro-UI MUST define one family-level core
|
||||
interaction model before a second host extends it.
|
||||
- Hosts MAY vary framing, assist entry, surrounding navigation, or
|
||||
optional diagnostics only when the shared core remains recognizable
|
||||
and the variation is explicit.
|
||||
- The host owns page-level navigation, authorization, surrounding
|
||||
mutations, and dangerous actions. The shared family owns only the
|
||||
repeated read/inspect/view semantics that are intentionally common.
|
||||
- One user concern MUST NOT be split across two peer interaction
|
||||
models on the same page.
|
||||
- `Parallel Inspect Worlds` means the same concern is driven by two
|
||||
competing inspect, selected-record, or view-state owners. It is
|
||||
forbidden.
|
||||
|
||||
##### Action grouping and order
|
||||
|
||||
- Actions MUST be ordered by meaning, frequency, and risk.
|
||||
@ -738,12 +718,7 @@ ##### Review gate
|
||||
3. Is navigation cleanly separated from mutation?
|
||||
4. Are rare or risky actions removed from the primary plane?
|
||||
5. Is the hierarchy scanable in a few seconds?
|
||||
6. If this is a repeated detail family, what is shared core vs
|
||||
host-owned variation?
|
||||
7. Does one concern still have exactly one primary interaction model?
|
||||
8. Which layer owns the relevant truth: shell, page, or detail?
|
||||
9. Is any exception real, bounded, and named, or is it a hidden
|
||||
exception?
|
||||
6. Is this a real special type or just an unordered exception?
|
||||
|
||||
If those answers are not clear, the surface is non-conformant.
|
||||
|
||||
@ -762,11 +737,6 @@ ##### Primary inspect model
|
||||
- A surface MUST NOT offer row click, identifier click, and explicit View/Inspect for the same destination as parallel primary models.
|
||||
- CRUD / List-first and Read-only Registry / Report surfaces MUST provide an obvious one-click open path.
|
||||
- Queue / Review and History / Audit surfaces MUST use explicit Inspect rather than row-click navigation.
|
||||
- Inline detail, summary, or sidebar inspect MAY exist only as
|
||||
subordinate presentations of the same selected-record truth, not as a
|
||||
second inspect contract.
|
||||
- `Parallel Inspect Worlds` are forbidden even when each local variant
|
||||
looks individually reasonable.
|
||||
|
||||
##### Row-click semantics
|
||||
- Full-row click is the default for CRUD / List-first and Read-only Registry / Report surfaces.
|
||||
@ -785,29 +755,6 @@ ##### Action hierarchy
|
||||
- All other secondary actions MUST move to overflow.
|
||||
- Long-running workflow launches such as sync, compare, verify, generate, consent, setup, or retry SHOULD live in list headers or detail headers rather than in every row.
|
||||
|
||||
##### Native-by-default and fake-native drift
|
||||
- Standard form, filter, table, action, tab, badge, link, and overview
|
||||
work is `Native Surface` work by default when Filament or an existing
|
||||
shared primitive can express it.
|
||||
- A `Fake-Native Surface` is any surface that visually lives inside
|
||||
Filament but keeps a second HTML, GET, query, or Blade-request
|
||||
interaction contract for its primary behavior.
|
||||
- Simple report or overview pages with ordinary columns, filters, empty
|
||||
states, and navigation default to native table semantics.
|
||||
- `Filament Costume` means locally assembled markup imitates native
|
||||
Filament controls, badges, or actions even though native or shared
|
||||
primitives fit. It is forbidden.
|
||||
- `Blade Request UI` means the primary body-state contract depends on
|
||||
`request()`, GET forms, or manual query parsing inside an active
|
||||
Filament surface. It is forbidden unless a documented exception limits
|
||||
request input to initialization-only behavior.
|
||||
- `Hand-Rolled Simple Overview` means a simple report or overview is
|
||||
rebuilt as bespoke markup where native table/list/report semantics
|
||||
fit. It is forbidden.
|
||||
- `Hidden Exception` means a surface behaves like a custom or special
|
||||
case without naming an exception type and reason block. It is
|
||||
forbidden.
|
||||
|
||||
##### Destructive actions
|
||||
- Destructive actions MUST NOT appear inline beside the primary inspect interaction on standard CRUD, Config-lite, or Read-only Registry surfaces.
|
||||
- Destructive actions MUST live in overflow or the detail header.
|
||||
@ -855,16 +802,6 @@ ##### Row density and scanability
|
||||
- Standard CRUD rows MUST NOT carry more than one sentence of flowing prose.
|
||||
- Next-step prose belongs in detail, inspect, or queue surfaces, not in ordinary CRUD rows.
|
||||
|
||||
##### Shared-family and state-layer violations
|
||||
- `Host Drift` occurs when a host silently redefines a shared detail
|
||||
micro-UI's core zones, diagnostics contract, or primary view/inspect
|
||||
model. Host Drift is forbidden.
|
||||
- `State Layer Collapse` occurs when shell, page, or detail layers each
|
||||
claim the same active truth or restoration responsibility. It is
|
||||
forbidden.
|
||||
- A lower layer MAY format or reveal a higher-layer truth, but it MUST
|
||||
NOT quietly become the higher layer's authority.
|
||||
|
||||
##### Custom abstractions
|
||||
- Custom UI abstractions MAY document and validate, but they MUST NOT create declaration-only safety that diverges from real behavior.
|
||||
- Contract systems MUST NOT force placeholder UI.
|
||||
@ -875,11 +812,6 @@ #### Exception Model (UI-EX-001)
|
||||
|
||||
Only catalogued exception types are allowed. Every exception MUST be named in the spec, reference its exception type, include a reason block, be called out explicitly in the PR, and carry at least one dedicated test.
|
||||
|
||||
- A `Legitimate Exception` is a named, bounded deviation that states the
|
||||
product reason, the smallest custom behavior required, what remains
|
||||
standardized, which layer owns the relevant state, and what proof or
|
||||
review evidence keeps it from turning into a general permission slip.
|
||||
|
||||
##### Queue Decision Exception
|
||||
- Allowed when per-item decision-making is the real queue work.
|
||||
- Guardrails: Inspect remains available unless detail is already inline; irreversible decisions require confirmation; unrelated maintenance actions do not join the row.
|
||||
@ -900,38 +832,6 @@ ##### Cross-panel Canonical Route Exception
|
||||
- Allowed when only one canonical surface makes sense.
|
||||
- Guardrails: nouns stay stable; shell transition is explicit; back navigation is clear; scope signals remain truthful.
|
||||
|
||||
##### Legitimate Custom Surface Exception
|
||||
- Allowed when the operator need is materially richer than ordinary
|
||||
CRUD, overview, or simple report semantics, such as richer
|
||||
visualization, high-value diagnostic or review work, multi-zone shared
|
||||
detail micro-UI, shell-context-specific UI, or domain presentation
|
||||
that native primitives do not express cleanly.
|
||||
- Guardrails: the spec MUST state the product reason, the smallest
|
||||
custom behavior required, which native/shared primitives still apply,
|
||||
which layer owns the relevant state, and what remains standardized.
|
||||
|
||||
##### Nativity Exception
|
||||
- Allowed only when Filament-native or shared primitives cannot express
|
||||
the required semantics cleanly.
|
||||
- Guardrails: the exception MUST name the missing semantic, reject
|
||||
`Filament Costume`, `Blade Request UI`, and `Hand-Rolled Simple
|
||||
Overview` shortcuts, keep native/shared surrounding controls where
|
||||
they still fit, and MUST NOT invent a local status language.
|
||||
|
||||
##### Shared Detail Host Variation Exception
|
||||
- Allowed when a known shared detail micro-UI needs bounded host
|
||||
framing, assist entry, or optional-zone variation.
|
||||
- Guardrails: the host MUST NOT redefine the family core zones,
|
||||
next-step contract, diagnostics contract, or primary view/inspect
|
||||
model. Differences stay visibly host-scoped.
|
||||
|
||||
##### State-Layer Special-case Exception
|
||||
- Allowed when a page legitimately needs explicit requested, active,
|
||||
draft, inspect, or restorable roles beyond the simple default.
|
||||
- Guardrails: the owner layer MUST be explicit, the restorable subset
|
||||
MUST be explicit, any query role MUST be documented, and no lower
|
||||
layer may silently take over shell or page truth.
|
||||
|
||||
#### Filament UI — Action Surface Contract (NON-NEGOTIABLE)
|
||||
|
||||
For every new or modified Filament Resource, RelationManager, or Page:
|
||||
@ -942,10 +842,6 @@ #### Filament UI — Action Surface Contract (NON-NEGOTIABLE)
|
||||
- Accepted forms are `recordUrl()` row click, a primary linked column, or an explicit row action when the taxonomy requires Inspect.
|
||||
- CRUD / List-first, Config-lite, and Read-only Registry surfaces MUST NOT render a redundant View action when the same destination is already available through row click or identifier click.
|
||||
- Queue / Review and History / Audit surfaces MAY use a lone explicit Inspect action because context-preserving inspect is the primary interaction.
|
||||
- Simple report or overview pages with ordinary columns, filters,
|
||||
empty-state behavior, and navigation MUST be implemented as native
|
||||
table surfaces unless UI-EX-001 documents why a richer custom surface
|
||||
is required.
|
||||
- View/Detail MUST define header actions and MUST keep destructive actions grouped and confirmed.
|
||||
- View/Detail MUST be sectioned using Infolists, Sections, Cards, Tabs, or equivalent composable structure.
|
||||
- Create/Edit MUST provide consistent Save and Cancel UX.
|
||||
@ -954,9 +850,6 @@ #### Filament UI — Action Surface Contract (NON-NEGOTIABLE)
|
||||
- Standard CRUD and Read-only Registry rows MUST NOT exceed inspect/open plus one inline safe shortcut.
|
||||
- Queue / Review rows MAY expose inline decision actions only when allowed by UI-EX-001.
|
||||
- Everything else MUST move to `ActionGroup::make()` or the detail header.
|
||||
- Repeated embedded detail/evidence families MUST declare one shared
|
||||
core contract. Host-specific navigation, mutations, and destructive
|
||||
actions stay outside that shared core.
|
||||
- Bulk actions MUST be grouped via `BulkActionGroup` only when the surface has a real bulk use case.
|
||||
- Empty `ActionGroup` and `BulkActionGroup` are forbidden.
|
||||
- Destructive actions MUST NOT be primary and MUST require confirmation; typed confirmation MAY be required for large or high-risk bulk changes.
|
||||
@ -971,12 +864,6 @@ #### Filament UI — Action Surface Contract (NON-NEGOTIABLE)
|
||||
- Every spec MUST include both a UI/UX Surface Classification and a UI Action Matrix.
|
||||
- Every changed operator-facing surface MUST declare its broad
|
||||
action-surface class and the one most likely next operator action.
|
||||
- Every changed operator-facing surface MUST also declare whether it is
|
||||
a `Native Surface`, `Custom Surface`, or `Shared Detail Micro-UI`, and
|
||||
MUST name any exception type it relies on.
|
||||
- If a surface uses shell, page, or detail state beyond simple static
|
||||
rendering, the governing spec MUST name which layer owns the relevant
|
||||
requested, active, draft, inspect, and restorable state.
|
||||
- Custom action-surface contracts are legitimate only when they validate rendered behavior, not only declarations or slot counts.
|
||||
- A change is not Done unless the implemented interaction semantics conform to the declared surface type or an approved exception documents and tests the deviation.
|
||||
|
||||
@ -1012,26 +899,6 @@ #### Filament UI — Layout & Information Architecture Standards (UX-001)
|
||||
- Standard CRUD tables MUST stay scanable and MUST NOT rely on row prose to communicate next steps.
|
||||
- Critical operational truth that informs list decisions MUST be default-visible.
|
||||
|
||||
State ownership
|
||||
- `Global Context State` is shell-owned workspace, tenant, or tenantless
|
||||
truth. Context bars and shell partials may display it, but they MUST
|
||||
NOT invent second precedence or fallback logic.
|
||||
- `Page State` is page-owned filter, tab, mode, or selected-record truth
|
||||
that changes the current page result or workflow.
|
||||
- `Detail State` is embedded viewer or shared-family state inside one
|
||||
detail surface and remains subordinate to shell and page truth unless
|
||||
an approved exception says otherwise.
|
||||
- `Requested State` is route, query, or upstream input before validation.
|
||||
- `Active State` is the currently governing validated state.
|
||||
- `Draft State` is local pending state that is intentionally separate
|
||||
from the currently applied result state.
|
||||
- `Inspect State` is the selected-record or selected-detail focus that
|
||||
drives inline or same-page inspect.
|
||||
- `Restorable State` is the subset intentionally recreated by refresh,
|
||||
back, bookmark, or shared link.
|
||||
- `State Layer Collapse` is forbidden. Shell, page, and detail layers
|
||||
MUST NOT silently overwrite one another's authority.
|
||||
|
||||
Enforcement
|
||||
- Shared layout builders such as `MainAsideForm`, `MainAsideInfolist`, and `StandardTableDefaults` SHOULD be reused where available.
|
||||
- A change is not Done unless UX-001 is satisfied or an approved exception documents why not.
|
||||
@ -1053,16 +920,6 @@ ##### Core rule
|
||||
contextual references do not belong in the header; they belong directly
|
||||
at the affected field, status indicator, or relation.
|
||||
|
||||
##### Shared-family host discipline
|
||||
|
||||
- If a record or detail page embeds a shared detail micro-UI, the
|
||||
header remains host-owned.
|
||||
- Family-level view switches, tabs, diagnostics reveals, and inner
|
||||
assist controls belong inside the family, not as copied header
|
||||
buttons.
|
||||
- Host-specific navigation or mutations MAY appear in the header only
|
||||
when they are truly host-critical and do not redefine the family core.
|
||||
|
||||
##### Maximum one primary visible header action
|
||||
|
||||
- Each record/detail page MUST expose at most one clearly prioritized
|
||||
@ -1165,9 +1022,6 @@ ##### Reviewer heuristics
|
||||
- Pure navigation buttons in the header.
|
||||
- Danger actions beside normal actions without clear separation.
|
||||
- Rarely used administrative actions as visible standard buttons.
|
||||
- Shared-family view switches or host-only forks are exported into the
|
||||
main header instead of staying inside the family or contextual
|
||||
placement.
|
||||
- The header resembles an action stockpile instead of a focused
|
||||
workflow entry point.
|
||||
|
||||
@ -1266,12 +1120,8 @@ #### Enforcement Model (UI-REVIEW-001)
|
||||
actions are ordered, canonical collection route, canonical detail
|
||||
route, scope signals and their exact meaning, canonical noun,
|
||||
critical truth visible by default, workflow-vs-storage IA
|
||||
justification, attention-load reduction, whether the surface is
|
||||
native, custom, or a shared detail family, what shared core vs host
|
||||
variation exists if relevant, which layer owns the relevant shell,
|
||||
page, and detail truth, which requested/active/draft/inspect/
|
||||
restorable roles exist, whether any fake-native or host-drift risk is
|
||||
present, and whether an exception type is used.
|
||||
justification, attention-load reduction, and whether an exception
|
||||
type is used.
|
||||
- Missing any of those answers makes the spec incomplete.
|
||||
|
||||
PR review requirements
|
||||
@ -1286,10 +1136,8 @@ #### Enforcement Model (UI-REVIEW-001)
|
||||
promoted into primary navigation without justification, one case
|
||||
fragmented across multiple equal-rank pages, new automation that adds
|
||||
attention surfaces without reducing operator work, noisy default
|
||||
surfaces with no action/watch/reference hierarchy, `Filament Costume`,
|
||||
`Blade Request UI`, `Hand-Rolled Simple Overview`, `Hidden Exception`,
|
||||
`Host Drift`, `State Layer Collapse`, `Parallel Inspect Worlds`, or
|
||||
undocumented exceptions without dedicated tests.
|
||||
surfaces with no action/watch/reference hierarchy, or undocumented
|
||||
exceptions without dedicated tests.
|
||||
|
||||
Guard tests
|
||||
- Repository guards SHOULD validate: declared surface type, declared
|
||||
@ -1298,11 +1146,8 @@ #### Enforcement Model (UI-REVIEW-001)
|
||||
presence of explicit Inspect on Queue / Review and History / Audit
|
||||
surfaces, absence of empty `ActionGroup` or `BulkActionGroup`,
|
||||
correct placement of destructive actions, truthful scope signals,
|
||||
stable canonical nouns across shells, absence of fake-native primary
|
||||
controls where metadata says the surface is native, bounded shared
|
||||
family contracts where metadata says a family is reused, explicit
|
||||
state ownership where specs or metadata expose it, and dedicated
|
||||
tests for every approved exception.
|
||||
stable canonical nouns across shells, and dedicated tests for every
|
||||
approved exception.
|
||||
|
||||
#### Immediate Retrofit Priorities
|
||||
|
||||
@ -1355,13 +1200,6 @@ #### Appendix A - One-page Condensed Constitution
|
||||
- Destructive actions never sit openly beside inspect on standard lists.
|
||||
- Overflow is standardized per surface class and is never empty.
|
||||
- Bulk exists only when it is genuinely useful.
|
||||
- Standard forms, filters, tables, tabs, badges, links, and simple
|
||||
overviews are native-by-default.
|
||||
- Fake-native surfaces, hidden exceptions, host drift, and state-layer
|
||||
collapse do not ship.
|
||||
- Repeated detail micro-UIs define shared core and bounded host
|
||||
variation before a second host forks them.
|
||||
- Shell, page, and detail truth each have one owner.
|
||||
- Navigation and mutation do not share equal visual weight without
|
||||
explicit hierarchy.
|
||||
- Monitoring and workbench surfaces separate scope/context, selection,
|
||||
@ -1390,14 +1228,10 @@ #### Appendix B - Feature Review Checklist
|
||||
- Broad action-surface class is declared.
|
||||
- Detailed surface type is declared.
|
||||
- The one most likely next operator action is explicit.
|
||||
- The surface is classified correctly as native, custom, or shared
|
||||
family.
|
||||
- Primary inspect/open model is defined.
|
||||
- Row-click rule is decided.
|
||||
- View/Inspect is correctly present or correctly forbidden.
|
||||
- Edit-as-inspect is used only when allowed.
|
||||
- Fake-native shortcuts are absent or explicitly exception-gated.
|
||||
- Shared-family core vs host-owned variation is explicit where relevant.
|
||||
- Navigation and mutation are separated intentionally.
|
||||
- Secondary actions are grouped correctly.
|
||||
- Destructive actions are placed correctly.
|
||||
@ -1408,9 +1242,6 @@ #### Appendix B - Feature Review Checklist
|
||||
- Canonical nouns stay consistent.
|
||||
- Critical truth is visible.
|
||||
- Scanability is preserved.
|
||||
- Shell, page, and detail state owners are explicit.
|
||||
- Requested, active, draft, inspect, and restorable roles are explicit
|
||||
when the surface uses them.
|
||||
- Exceptions are documented and tested.
|
||||
- Header passes the 5-second scan rule (HDR-001).
|
||||
- No pure navigation in the header.
|
||||
@ -1430,11 +1261,6 @@ #### Appendix C - Red Flags for Future PRs
|
||||
attention load.
|
||||
- The surface creates more noise than priority.
|
||||
- Row click and View open the same destination.
|
||||
- A Filament-looking surface keeps its real primary contract in raw HTML,
|
||||
GET, or Blade request state.
|
||||
- A simple overview is rebuilt as bespoke markup without a real product
|
||||
reason.
|
||||
- A special surface exists only by history and lacks a named exception.
|
||||
- A row becomes a control center.
|
||||
- Archive or Delete sits openly beside View or Inspect on a standard list.
|
||||
- More menus or bulk menus are empty.
|
||||
@ -1448,10 +1274,6 @@ #### Appendix C - Red Flags for Future PRs
|
||||
actions as one flat header rail.
|
||||
- Critical health or operability truth is hidden by default.
|
||||
- A contract claims conformance while the rendered UI behaves differently.
|
||||
- A repeated detail surface quietly changes family core structure from
|
||||
one host to another.
|
||||
- Shell, page, and detail layers each claim the same truth.
|
||||
- One concern has two competing inspect or view-state owners.
|
||||
- Header has multiple equally weighted buttons without clear prioritization.
|
||||
- "Open X" navigation links placed in the header instead of at the related field.
|
||||
- Governance-changing actions sit casually beside the primary action without friction.
|
||||
@ -1472,36 +1294,14 @@ ### Filament Native First / No Ad-hoc Styling (UI-FIL-001)
|
||||
- If Filament already provides the required semantic element, feature code MUST use the Filament-native component instead of a locally assembled replacement.
|
||||
- Preferred native elements include `x-filament::badge`, `x-filament::button`, `x-filament::icon`, and Filament Forms, Infolists, Tables, Sections, Tabs, Grids, and Actions.
|
||||
|
||||
Native-by-default classification
|
||||
- `Native Surface` means the primary interaction contract is built from
|
||||
Filament-native components or approved shared primitives.
|
||||
- Standard forms, filters, tables, tabs, badges, links, and simple
|
||||
overviews default to `Native Surface` status.
|
||||
- `Custom Surface` is allowed only through UI-EX-001 when the operator
|
||||
need is richer than standard CRUD, overview, or report semantics.
|
||||
- `Fake-Native Surface` is forbidden: a surface that looks native but
|
||||
keeps a second HTML, GET, query, or Blade-request contract for the
|
||||
same primary interaction.
|
||||
|
||||
Forbidden local replacements
|
||||
- Feature code MUST NOT hand-build badges, pills, status chips, alert cards, or action buttons from raw `<span>`, `<div>`, or `<button>` markup plus Tailwind classes when Filament-native or shared project primitives can express the same meaning.
|
||||
- Feature code MUST NOT introduce page-local visual status languages for status, risk, outcome, drift, trust, importance, or severity.
|
||||
- Feature code MUST NOT make local color, border, rounding, or emphasis decisions for semantic UI states using ad-hoc classes such as `bg-danger-100`, `text-warning-900`, `border-dashed`, or `rounded-full` when the same state can be expressed through Filament props or shared primitives.
|
||||
- `Filament Costume` is forbidden: locally assembled markup that merely
|
||||
imitates native Filament controls, badges, or actions.
|
||||
- `Blade Request UI` is forbidden: request-driven body state or GET-form
|
||||
control rails as the primary interaction contract inside an active
|
||||
Filament surface.
|
||||
- `Hand-Rolled Simple Overview` is forbidden: bespoke overview/report
|
||||
shells where a native table/list surface fits the job.
|
||||
|
||||
Shared primitive before local override
|
||||
- If the same UI pattern can recur, it MUST use an existing shared primitive or introduce a new central primitive instead of reassembling the pattern inside a Blade view.
|
||||
- Central badge and status catalogs remain the canonical source for status semantics; local views MUST consume them rather than re-map them.
|
||||
- If the same custom detail, evidence, or review surface appears in
|
||||
more than one host, it becomes a `Shared Detail Micro-UI` and MUST
|
||||
define shared core vs host variation before another host reassembles
|
||||
it locally.
|
||||
|
||||
Upgrade-safe preference
|
||||
- Update-safe, framework-native implementations take priority over page-local styling shortcuts.
|
||||
@ -1513,19 +1313,13 @@ ### Filament Native First / No Ad-hoc Styling (UI-FIL-001)
|
||||
- native Filament components cannot express the required semantics,
|
||||
- no suitable shared primitive exists,
|
||||
- and the deviation is justified briefly in code and in the governing spec or PR.
|
||||
- Approved exceptions MUST stay layout-neutral, use the minimum local
|
||||
classes necessary, MUST NOT invent a new page-local status language,
|
||||
and MUST say what remains standardized.
|
||||
- `Hidden Exception` is forbidden. Historical accident or local
|
||||
implementation convenience is not a valid substitute for UI-EX-001.
|
||||
- Approved exceptions MUST stay layout-neutral, use the minimum local classes necessary, and MUST NOT invent a new page-local status language.
|
||||
|
||||
Review and enforcement
|
||||
- Every UI review MUST answer:
|
||||
- which native Filament element or shared primitive was used,
|
||||
- why an existing component was insufficient if an exception was taken,
|
||||
- whether the surface is native, custom, or a shared detail family,
|
||||
- and whether any ad-hoc status, emphasis styling, or fake-native
|
||||
contract was introduced.
|
||||
- and whether any ad-hoc status or emphasis styling was introduced.
|
||||
- UI work is not Done if it introduces ad-hoc status styling or framework-foreign replacement components where a native Filament or shared UI solution was viable.
|
||||
|
||||
### Incremental UI Standards Enforcement (UI-STD-001)
|
||||
@ -1573,4 +1367,4 @@ ### Versioning Policy (SemVer)
|
||||
- **MINOR**: new principle/section or materially expanded guidance.
|
||||
- **MAJOR**: removing/redefining principles in a backward-incompatible way.
|
||||
|
||||
**Version**: 2.6.0 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-04-18
|
||||
**Version**: 2.5.0 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-04-18
|
||||
|
||||
@ -722,7 +722,6 @@ ## Application Structure & Architecture
|
||||
## Frontend Bundling
|
||||
|
||||
- Repo-root JavaScript orchestration now uses `corepack pnpm install`, `corepack pnpm dev:platform`, `corepack pnpm dev:website`, `corepack pnpm dev`, `corepack pnpm build:website`, and `corepack pnpm build:platform`.
|
||||
- `corepack pnpm dev:platform` starts the platform Sail stack and the Laravel panel Vite watcher. `corepack pnpm dev` starts that platform watcher plus the website dev server.
|
||||
- `apps/website` is a standalone Astro app, not a second Laravel runtime, so Boost MCP remains platform-only.
|
||||
- If the user doesn't see a platform frontend change reflected in the UI, it could mean they need to run `cd apps/platform && ./vendor/bin/sail pnpm build`, `cd apps/platform && ./vendor/bin/sail pnpm dev`, or `cd apps/platform && ./vendor/bin/sail composer run dev`. Ask them.
|
||||
|
||||
|
||||
@ -560,7 +560,6 @@ ## Application Structure & Architecture
|
||||
## Frontend Bundling
|
||||
|
||||
- Repo-root JavaScript orchestration now uses `corepack pnpm install`, `corepack pnpm dev:platform`, `corepack pnpm dev:website`, `corepack pnpm dev`, `corepack pnpm build:website`, and `corepack pnpm build:platform`.
|
||||
- `corepack pnpm dev:platform` starts the platform Sail stack and the Laravel panel Vite watcher. `corepack pnpm dev` starts that platform watcher plus the website dev server.
|
||||
- `apps/website` is a standalone Astro app, not a second Laravel runtime, so Boost MCP remains platform-only.
|
||||
- If the user doesn't see a platform frontend change reflected in the UI, it could mean they need to run `cd apps/platform && ./vendor/bin/sail pnpm build`, `cd apps/platform && ./vendor/bin/sail pnpm dev`, or `cd apps/platform && ./vendor/bin/sail composer run dev`. Ask them.
|
||||
|
||||
|
||||
11
README.md
11
README.md
@ -12,17 +12,14 @@ ## Multi-App Topology
|
||||
- repo root: workspace manifests, documentation, scripts, editor tooling, and `docker-compose.yml`
|
||||
- `./scripts/platform-sail`: platform-only compatibility helper for tooling that cannot set `cwd`
|
||||
|
||||
Website-track guardrails for independent evolution live in
|
||||
[`docs/strategy/website-working-contract.md`](docs/strategy/website-working-contract.md).
|
||||
|
||||
## Official Root Commands
|
||||
|
||||
- Install workspace-managed JavaScript dependencies: `corepack pnpm install`
|
||||
- Start the platform stack and Laravel panel Vite watcher: `corepack pnpm dev:platform`
|
||||
- Start the platform stack: `corepack pnpm dev:platform`
|
||||
- Start the website dev server: `corepack pnpm dev:website`
|
||||
- Start platform Vite + website together: `corepack pnpm dev`
|
||||
- Start platform + website together: `corepack pnpm dev`
|
||||
- Build the website: `corepack pnpm build:website`
|
||||
- Build platform frontend assets inside Sail: `corepack pnpm build:platform`
|
||||
- Build platform frontend assets: `corepack pnpm build:platform`
|
||||
|
||||
## App-Local Commands
|
||||
|
||||
@ -32,7 +29,7 @@ ### Platform
|
||||
- Start Sail: `cd apps/platform && ./vendor/bin/sail up -d`
|
||||
- Generate the app key: `cd apps/platform && ./vendor/bin/sail artisan key:generate`
|
||||
- Run migrations and seeders: `cd apps/platform && ./vendor/bin/sail artisan migrate --seed`
|
||||
- Run frontend watch/build inside Sail: `corepack pnpm dev:platform`, `cd apps/platform && ./vendor/bin/sail pnpm dev`, or `cd apps/platform && ./vendor/bin/sail pnpm build`
|
||||
- Run frontend watch/build inside Sail: `cd apps/platform && ./vendor/bin/sail pnpm dev` or `cd apps/platform && ./vendor/bin/sail pnpm build`
|
||||
- Run tests: `cd apps/platform && ./vendor/bin/sail artisan test --compact`
|
||||
|
||||
### Website
|
||||
|
||||
@ -127,14 +127,12 @@ public function selectWorkspace(int $workspaceId): void
|
||||
resourceId: (string) $workspace->getKey(),
|
||||
);
|
||||
|
||||
$intendedUrl = WorkspaceIntendedUrl::consume(request());
|
||||
|
||||
/** @var WorkspaceRedirectResolver $resolver */
|
||||
$resolver = app(WorkspaceRedirectResolver::class);
|
||||
|
||||
$redirectTarget = $resolver->resolve(
|
||||
$workspace,
|
||||
$user,
|
||||
WorkspaceIntendedUrl::consume(request()),
|
||||
);
|
||||
$redirectTarget = $intendedUrl ?: $resolver->resolve($workspace, $user);
|
||||
|
||||
$this->redirect($redirectTarget);
|
||||
}
|
||||
@ -172,14 +170,12 @@ public function createWorkspace(array $data): void
|
||||
->success()
|
||||
->send();
|
||||
|
||||
$intendedUrl = WorkspaceIntendedUrl::consume(request());
|
||||
|
||||
/** @var WorkspaceRedirectResolver $resolver */
|
||||
$resolver = app(WorkspaceRedirectResolver::class);
|
||||
|
||||
$redirectTarget = $resolver->resolve(
|
||||
$workspace,
|
||||
$user,
|
||||
WorkspaceIntendedUrl::consume(request()),
|
||||
);
|
||||
$redirectTarget = $intendedUrl ?: $resolver->resolve($workspace, $user);
|
||||
|
||||
$this->redirect($redirectTarget);
|
||||
}
|
||||
|
||||
@ -69,13 +69,15 @@ public function __invoke(Request $request): RedirectResponse
|
||||
resourceId: (string) $workspace->getKey(),
|
||||
);
|
||||
|
||||
$intendedUrl = WorkspaceIntendedUrl::consume($request);
|
||||
|
||||
if ($intendedUrl !== null) {
|
||||
return redirect()->to($intendedUrl);
|
||||
}
|
||||
|
||||
/** @var WorkspaceRedirectResolver $resolver */
|
||||
$resolver = app(WorkspaceRedirectResolver::class);
|
||||
|
||||
return redirect()->to($resolver->resolve(
|
||||
$workspace,
|
||||
$user,
|
||||
WorkspaceIntendedUrl::consume($request),
|
||||
));
|
||||
return redirect()->to($resolver->resolve($workspace, $user));
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,14 +8,18 @@
|
||||
use App\Filament\Resources\AlertRuleResource;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Models\WorkspaceMembership;
|
||||
use App\Services\Auth\WorkspaceRoleCapabilityMap;
|
||||
use App\Services\Tenants\TenantOperabilityService;
|
||||
use App\Support\Auth\Capabilities;
|
||||
use App\Support\OperateHub\OperateHubShell;
|
||||
use App\Support\Tenants\TenantOperabilityQuestion;
|
||||
use App\Support\Tenants\TenantPageCategory;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Closure;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Models\Contracts\HasTenants;
|
||||
use Filament\Navigation\NavigationBuilder;
|
||||
use Filament\Navigation\NavigationItem;
|
||||
use Illuminate\Http\Request;
|
||||
@ -29,7 +33,6 @@ class EnsureFilamentTenantSelected
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$panel = Filament::getCurrentOrDefaultPanel();
|
||||
$resolvedContext = app(OperateHubShell::class)->resolvedContext($request);
|
||||
|
||||
$path = '/'.ltrim($request->path(), '/');
|
||||
|
||||
@ -82,27 +85,75 @@ public function handle(Request $request, Closure $next): Response
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$tenantParameter = null;
|
||||
if ($request->route()?->hasParameter('tenant')) {
|
||||
$tenantParameter = $request->route()->parameter('tenant');
|
||||
} elseif (filled($request->query('tenant'))) {
|
||||
$tenantParameter = $request->query('tenant');
|
||||
}
|
||||
|
||||
if (
|
||||
! $resolvedContext->hasTenant()
|
||||
$tenantParameter === null
|
||||
&& ! $this->hasCanonicalTenantSelection($request)
|
||||
&& $this->adminPathRequiresTenantSelection($path)
|
||||
) {
|
||||
return redirect()->route('filament.admin.pages.choose-tenant');
|
||||
}
|
||||
|
||||
if ($resolvedContext->pageCategory === TenantPageCategory::TenantBound && ! $resolvedContext->hasTenant()) {
|
||||
abort(404);
|
||||
}
|
||||
if ($tenantParameter !== null) {
|
||||
$user = $request->user();
|
||||
|
||||
if (
|
||||
$resolvedContext->hasTenant()
|
||||
&& (
|
||||
$panel?->getId() === 'tenant'
|
||||
|| (! $this->isWorkspaceScopedPageWithTenant($path) && $resolvedContext->pageCategory === TenantPageCategory::TenantBound)
|
||||
)
|
||||
) {
|
||||
Filament::setTenant($resolvedContext->tenant, true);
|
||||
} elseif (! $resolvedContext->hasTenant()) {
|
||||
Filament::setTenant(null, true);
|
||||
if ($user === null) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if (! $user instanceof HasTenants) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$tenant = $tenantParameter instanceof Tenant
|
||||
? $tenantParameter
|
||||
: Tenant::query()->withTrashed()->where('external_id', (string) $tenantParameter)->first();
|
||||
|
||||
if (! $tenant instanceof Tenant) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
if ($workspaceId === null) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
if ((int) $tenant->workspace_id !== (int) $workspaceId) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$workspace = Workspace::query()->whereKey($workspaceId)->first();
|
||||
|
||||
if (! $workspace instanceof Workspace) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
if (! $user instanceof User || ! $workspaceContext->isMember($user, $workspace)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
if (! $this->routeTenantIsAuthorized($tenant, $user, $workspaceId, $path)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
if ($this->isWorkspaceScopedPageWithTenant($path)) {
|
||||
$this->configureNavigationForRequest($panel);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
app(WorkspaceContext::class)->rememberTenantContext($tenant, $request);
|
||||
|
||||
$this->configureNavigationForRequest($panel);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if (
|
||||
@ -240,4 +291,27 @@ private function adminPathRequiresTenantSelection(string $path): bool
|
||||
|
||||
return preg_match('#^/admin/(inventory|policies|policy-versions|backup-sets)(/|$)#', $path) === 1;
|
||||
}
|
||||
|
||||
private function hasCanonicalTenantSelection(Request $request): bool
|
||||
{
|
||||
return app(OperateHubShell::class)->activeEntitledTenant($request) instanceof Tenant;
|
||||
}
|
||||
|
||||
private function routeTenantIsAuthorized(Tenant $tenant, User $user, int $workspaceId, string $path): bool
|
||||
{
|
||||
$pageCategory = TenantPageCategory::fromPath($path);
|
||||
$question = match ($pageCategory) {
|
||||
TenantPageCategory::TenantBound => TenantOperabilityQuestion::TenantBoundViewability,
|
||||
default => TenantOperabilityQuestion::AdministrativeDiscoverability,
|
||||
};
|
||||
|
||||
return app(TenantOperabilityService::class)->outcomeFor(
|
||||
tenant: $tenant,
|
||||
question: $question,
|
||||
actor: $user,
|
||||
workspaceId: $workspaceId,
|
||||
lane: $pageCategory->lane(),
|
||||
selectedTenant: Filament::getTenant() instanceof Tenant ? Filament::getTenant() : null,
|
||||
)->allowed;
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,9 +7,9 @@
|
||||
use App\Filament\Pages\TenantDashboard;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Services\Auth\CapabilityResolver;
|
||||
use App\Services\Tenants\TenantOperabilityService;
|
||||
use App\Support\Tenants\TenantInteractionLane;
|
||||
use App\Support\Tenants\TenantOperabilityQuestion;
|
||||
use App\Support\Tenants\TenantPageCategory;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
@ -19,8 +19,6 @@
|
||||
|
||||
final class OperateHubShell
|
||||
{
|
||||
private const string REQUEST_ATTRIBUTE = 'tenantpilot.resolved_shell_context';
|
||||
|
||||
public function __construct(
|
||||
private WorkspaceContext $workspaceContext,
|
||||
private CapabilityResolver $capabilityResolver,
|
||||
@ -85,7 +83,7 @@ public function headerActions(
|
||||
|
||||
public function activeEntitledTenant(?Request $request = null): ?Tenant
|
||||
{
|
||||
return $this->resolvedContext($request)->tenant;
|
||||
return $this->resolveActiveTenant($request);
|
||||
}
|
||||
|
||||
public function tenantOwnedPanelContext(?Request $request = null): ?Tenant
|
||||
@ -93,162 +91,42 @@ public function tenantOwnedPanelContext(?Request $request = null): ?Tenant
|
||||
return $this->activeEntitledTenant($request);
|
||||
}
|
||||
|
||||
public function resolvedContext(?Request $request = null): ResolvedShellContext
|
||||
{
|
||||
$request ??= request();
|
||||
|
||||
if ($request instanceof Request) {
|
||||
$cached = $request->attributes->get(self::REQUEST_ATTRIBUTE);
|
||||
|
||||
if ($cached instanceof ResolvedShellContext) {
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
|
||||
$resolved = $this->buildResolvedContext($request);
|
||||
|
||||
if ($request instanceof Request) {
|
||||
$request->attributes->set(self::REQUEST_ATTRIBUTE, $resolved);
|
||||
}
|
||||
|
||||
return $resolved;
|
||||
}
|
||||
|
||||
private function buildResolvedContext(?Request $request = null): ResolvedShellContext
|
||||
private function resolveActiveTenant(?Request $request = null): ?Tenant
|
||||
{
|
||||
$pageCategory = $this->pageCategory($request);
|
||||
$routeTenantCandidate = $this->resolveRouteTenantCandidate($request, $pageCategory);
|
||||
$sessionWorkspace = $this->workspaceContext->currentWorkspace($request);
|
||||
$workspace = $this->workspaceContext->currentWorkspaceOrTenantWorkspace($routeTenantCandidate, $request);
|
||||
$routeTenant = $this->resolveRouteTenant($request, $pageCategory);
|
||||
|
||||
$workspaceSource = match (true) {
|
||||
$workspace instanceof Workspace && $sessionWorkspace instanceof Workspace && $workspace->is($sessionWorkspace) => 'session_workspace',
|
||||
$workspace instanceof Workspace && $routeTenantCandidate instanceof Tenant && (int) $routeTenantCandidate->workspace_id === (int) $workspace->getKey() => 'route',
|
||||
default => 'none',
|
||||
};
|
||||
|
||||
if (! $workspace instanceof Workspace) {
|
||||
return new ResolvedShellContext(
|
||||
workspace: null,
|
||||
tenant: null,
|
||||
pageCategory: $pageCategory,
|
||||
state: 'missing_workspace',
|
||||
displayMode: 'recovery',
|
||||
recoveryAction: $pageCategory === TenantPageCategory::WorkspaceChooserException ? 'none' : 'redirect_choose_workspace',
|
||||
recoveryDestination: '/admin/choose-workspace',
|
||||
recoveryReason: 'missing_workspace',
|
||||
);
|
||||
if ($routeTenant instanceof Tenant) {
|
||||
return $routeTenant;
|
||||
}
|
||||
|
||||
$routeTenant = $this->resolveValidatedRouteTenant($routeTenantCandidate, $workspace, $request, $pageCategory);
|
||||
|
||||
if ($routeTenant['tenant'] instanceof Tenant) {
|
||||
return new ResolvedShellContext(
|
||||
workspace: $workspace,
|
||||
tenant: $routeTenant['tenant'],
|
||||
pageCategory: $pageCategory,
|
||||
state: 'tenant_scoped',
|
||||
displayMode: 'tenant_scoped',
|
||||
workspaceSource: $workspaceSource,
|
||||
tenantSource: 'route',
|
||||
);
|
||||
}
|
||||
|
||||
$recoveryReason = $routeTenant['reason'];
|
||||
|
||||
if ($pageCategory === TenantPageCategory::TenantBound && $recoveryReason !== null) {
|
||||
return new ResolvedShellContext(
|
||||
workspace: $workspace,
|
||||
tenant: null,
|
||||
pageCategory: $pageCategory,
|
||||
state: 'invalid_tenant',
|
||||
displayMode: 'recovery',
|
||||
workspaceSource: $workspaceSource,
|
||||
recoveryAction: 'abort_not_found',
|
||||
recoveryReason: $recoveryReason,
|
||||
);
|
||||
}
|
||||
|
||||
$queryHintTenant = $this->resolveValidatedQueryHintTenant($request, $workspace, $pageCategory);
|
||||
|
||||
if ($queryHintTenant['tenant'] instanceof Tenant) {
|
||||
return new ResolvedShellContext(
|
||||
workspace: $workspace,
|
||||
tenant: $queryHintTenant['tenant'],
|
||||
pageCategory: $pageCategory,
|
||||
state: 'tenant_scoped',
|
||||
displayMode: 'tenant_scoped',
|
||||
workspaceSource: $workspaceSource,
|
||||
tenantSource: 'query_hint',
|
||||
);
|
||||
}
|
||||
|
||||
$recoveryReason ??= $queryHintTenant['reason'];
|
||||
|
||||
$tenant = $this->resolveValidatedFilamentTenant($request, $pageCategory, $workspace);
|
||||
$tenant = $this->resolveValidatedFilamentTenant($request, $pageCategory);
|
||||
|
||||
if ($tenant instanceof Tenant) {
|
||||
return new ResolvedShellContext(
|
||||
workspace: $workspace,
|
||||
tenant: $tenant,
|
||||
pageCategory: $pageCategory,
|
||||
state: 'tenant_scoped',
|
||||
displayMode: 'tenant_scoped',
|
||||
workspaceSource: $workspaceSource,
|
||||
tenantSource: 'filament_tenant',
|
||||
);
|
||||
return $tenant;
|
||||
}
|
||||
|
||||
if ($pageCategory->allowsRememberedTenantRestore()) {
|
||||
$rememberedTenant = $this->workspaceContext->rememberedTenant($request);
|
||||
|
||||
if ($rememberedTenant instanceof Tenant) {
|
||||
return new ResolvedShellContext(
|
||||
workspace: $workspace,
|
||||
tenant: $rememberedTenant,
|
||||
pageCategory: $pageCategory,
|
||||
state: 'tenant_scoped',
|
||||
displayMode: 'tenant_scoped',
|
||||
workspaceSource: $workspaceSource,
|
||||
tenantSource: 'remembered',
|
||||
);
|
||||
}
|
||||
if ($pageCategory === TenantPageCategory::TenantBound) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($pageCategory->requiresExplicitTenant()) {
|
||||
return new ResolvedShellContext(
|
||||
workspace: $workspace,
|
||||
tenant: null,
|
||||
pageCategory: $pageCategory,
|
||||
state: 'missing_tenant',
|
||||
displayMode: 'recovery',
|
||||
workspaceSource: $workspaceSource,
|
||||
recoveryAction: $pageCategory === TenantPageCategory::TenantScopedEvidence
|
||||
? 'redirect_evidence_overview'
|
||||
: 'abort_not_found',
|
||||
recoveryDestination: $pageCategory === TenantPageCategory::TenantScopedEvidence
|
||||
? '/admin/evidence/overview'
|
||||
: null,
|
||||
recoveryReason: $recoveryReason ?? 'missing_tenant',
|
||||
);
|
||||
$rememberedTenant = $this->workspaceContext->rememberedTenant($request);
|
||||
|
||||
if (! $rememberedTenant instanceof Tenant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ResolvedShellContext(
|
||||
workspace: $workspace,
|
||||
tenant: null,
|
||||
pageCategory: $pageCategory,
|
||||
state: 'tenantless_workspace',
|
||||
displayMode: 'tenantless',
|
||||
workspaceSource: $workspaceSource,
|
||||
recoveryReason: $recoveryReason,
|
||||
);
|
||||
if (! $this->isRememberedTenantValid($rememberedTenant, $request)) {
|
||||
$this->workspaceContext->clearRememberedTenantContext($request);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $rememberedTenant;
|
||||
}
|
||||
|
||||
private function resolveValidatedFilamentTenant(
|
||||
?Request $request = null,
|
||||
?TenantPageCategory $pageCategory = null,
|
||||
?Workspace $workspace = null,
|
||||
): ?Tenant {
|
||||
private function resolveValidatedFilamentTenant(?Request $request = null, ?TenantPageCategory $pageCategory = null): ?Tenant
|
||||
{
|
||||
$tenant = Filament::getTenant();
|
||||
|
||||
if (! $tenant instanceof Tenant) {
|
||||
@ -256,9 +134,8 @@ private function resolveValidatedFilamentTenant(
|
||||
}
|
||||
|
||||
$pageCategory ??= $this->pageCategory($request);
|
||||
$workspace ??= $this->workspaceContext->currentWorkspaceOrTenantWorkspace($tenant, $request);
|
||||
|
||||
if ($workspace instanceof Workspace && $this->tenantValidationReason($tenant, $workspace, $request, $pageCategory) === null) {
|
||||
if ($this->isContextTenantEntitled($tenant, $request, $pageCategory)) {
|
||||
return $tenant;
|
||||
}
|
||||
|
||||
@ -267,58 +144,19 @@ private function resolveValidatedFilamentTenant(
|
||||
return null;
|
||||
}
|
||||
|
||||
private function resolveValidatedRouteTenant(
|
||||
?Tenant $tenant,
|
||||
Workspace $workspace,
|
||||
?Request $request = null,
|
||||
?TenantPageCategory $pageCategory = null,
|
||||
): array {
|
||||
$pageCategory ??= $this->pageCategory($request);
|
||||
|
||||
if (! $tenant instanceof Tenant) {
|
||||
return ['tenant' => null, 'reason' => null];
|
||||
}
|
||||
|
||||
$reason = $this->tenantValidationReason($tenant, $workspace, $request, $pageCategory);
|
||||
|
||||
if ($reason !== null) {
|
||||
return ['tenant' => null, 'reason' => $reason];
|
||||
}
|
||||
|
||||
return ['tenant' => $tenant, 'reason' => null];
|
||||
}
|
||||
|
||||
private function resolveValidatedQueryHintTenant(
|
||||
?Request $request,
|
||||
Workspace $workspace,
|
||||
TenantPageCategory $pageCategory,
|
||||
): array {
|
||||
if (! $pageCategory->allowsQueryTenantHints()) {
|
||||
return ['tenant' => null, 'reason' => null];
|
||||
}
|
||||
|
||||
$queryTenant = $this->resolveQueryTenantHint($request);
|
||||
|
||||
if (! $queryTenant instanceof Tenant) {
|
||||
return ['tenant' => null, 'reason' => null];
|
||||
}
|
||||
|
||||
$reason = $this->tenantValidationReason($queryTenant, $workspace, $request, $pageCategory);
|
||||
|
||||
if ($reason !== null) {
|
||||
return ['tenant' => null, 'reason' => $reason];
|
||||
}
|
||||
|
||||
return ['tenant' => $queryTenant, 'reason' => null];
|
||||
}
|
||||
|
||||
private function resolveRouteTenantCandidate(?Request $request = null, ?TenantPageCategory $pageCategory = null): ?Tenant
|
||||
private function resolveRouteTenant(?Request $request = null, ?TenantPageCategory $pageCategory = null): ?Tenant
|
||||
{
|
||||
$route = $request?->route();
|
||||
$pageCategory ??= $this->pageCategory($request);
|
||||
|
||||
if ($route?->hasParameter('tenant')) {
|
||||
return $this->resolveTenantIdentifier($route->parameter('tenant'));
|
||||
$tenant = $this->resolveTenantRouteParameter($route->parameter('tenant'));
|
||||
|
||||
if (! $tenant instanceof Tenant || ! $this->isRouteTenantEntitled($tenant, $request, $pageCategory)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $tenant;
|
||||
}
|
||||
|
||||
if (
|
||||
@ -329,35 +167,24 @@ private function resolveRouteTenantCandidate(?Request $request = null, ?TenantPa
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->resolveTenantIdentifier($route->parameter('record'));
|
||||
$tenant = $this->resolveTenantRouteParameter($route->parameter('record'));
|
||||
|
||||
if (! $tenant instanceof Tenant || ! $this->isRouteTenantEntitled($tenant, $request, $pageCategory)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $tenant;
|
||||
}
|
||||
|
||||
private function resolveQueryTenantHint(?Request $request = null): ?Tenant
|
||||
private function resolveTenantRouteParameter(mixed $routeTenant): ?Tenant
|
||||
{
|
||||
$queryTenant = $request?->query('tenant');
|
||||
|
||||
if (filled($queryTenant)) {
|
||||
return $this->resolveTenantIdentifier($queryTenant);
|
||||
if ($routeTenant instanceof Tenant) {
|
||||
return $routeTenant;
|
||||
}
|
||||
|
||||
$queryTenantId = $request?->query('tenant_id');
|
||||
$routeTenant = trim((string) $routeTenant);
|
||||
|
||||
if (filled($queryTenantId)) {
|
||||
return $this->resolveTenantIdentifier($queryTenantId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function resolveTenantIdentifier(mixed $tenantIdentifier): ?Tenant
|
||||
{
|
||||
if ($tenantIdentifier instanceof Tenant) {
|
||||
return $tenantIdentifier;
|
||||
}
|
||||
|
||||
$tenantIdentifier = trim((string) $tenantIdentifier);
|
||||
|
||||
if ($tenantIdentifier === '') {
|
||||
if ($routeTenant === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -365,58 +192,94 @@ private function resolveTenantIdentifier(mixed $tenantIdentifier): ?Tenant
|
||||
|
||||
return Tenant::query()
|
||||
->withTrashed()
|
||||
->where(static function ($query) use ($tenantIdentifier, $tenantKeyColumn): void {
|
||||
$query->where('external_id', $tenantIdentifier);
|
||||
->where(static function ($query) use ($routeTenant, $tenantKeyColumn): void {
|
||||
$query->where('external_id', $routeTenant);
|
||||
|
||||
if (ctype_digit($tenantIdentifier)) {
|
||||
$query->orWhere($tenantKeyColumn, (int) $tenantIdentifier);
|
||||
if (ctype_digit($routeTenant)) {
|
||||
$query->orWhere($tenantKeyColumn, (int) $routeTenant);
|
||||
}
|
||||
})
|
||||
->first();
|
||||
}
|
||||
|
||||
private function tenantValidationReason(
|
||||
Tenant $tenant,
|
||||
Workspace $workspace,
|
||||
?Request $request = null,
|
||||
?TenantPageCategory $pageCategory = null,
|
||||
): ?string {
|
||||
$pageCategory ??= $this->pageCategory($request);
|
||||
private function isRouteTenantEntitled(Tenant $tenant, ?Request $request = null, ?TenantPageCategory $pageCategory = null): bool
|
||||
{
|
||||
$pageCategory ??= TenantPageCategory::fromRequest($request);
|
||||
|
||||
if ((int) $tenant->workspace_id !== (int) $workspace->getKey()) {
|
||||
return 'mismatched_workspace';
|
||||
if ($pageCategory !== TenantPageCategory::TenantBound) {
|
||||
return $this->isContextTenantEntitled($tenant, $request, $pageCategory);
|
||||
}
|
||||
|
||||
return $this->evaluateOutcome(
|
||||
tenant: $tenant,
|
||||
request: $request,
|
||||
question: TenantOperabilityQuestion::TenantBoundViewability,
|
||||
lane: TenantInteractionLane::AdministrativeManagement,
|
||||
);
|
||||
}
|
||||
|
||||
private function isContextTenantEntitled(Tenant $tenant, ?Request $request = null, ?TenantPageCategory $pageCategory = null): bool
|
||||
{
|
||||
$pageCategory ??= TenantPageCategory::fromRequest($request);
|
||||
|
||||
return match ($pageCategory) {
|
||||
TenantPageCategory::TenantBound => $this->evaluateOutcome(
|
||||
tenant: $tenant,
|
||||
request: $request,
|
||||
question: TenantOperabilityQuestion::TenantBoundViewability,
|
||||
lane: TenantInteractionLane::AdministrativeManagement,
|
||||
),
|
||||
TenantPageCategory::CanonicalWorkspaceRecordViewer,
|
||||
TenantPageCategory::OnboardingWorkflow,
|
||||
TenantPageCategory::WorkspaceScoped => $this->evaluateOutcome(
|
||||
tenant: $tenant,
|
||||
request: $request,
|
||||
question: TenantOperabilityQuestion::AdministrativeDiscoverability,
|
||||
lane: TenantInteractionLane::AdministrativeManagement,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private function isRememberedTenantValid(Tenant $tenant, ?Request $request = null): bool
|
||||
{
|
||||
return $this->evaluateOutcome(
|
||||
tenant: $tenant,
|
||||
request: $request,
|
||||
question: TenantOperabilityQuestion::RememberedContextValidity,
|
||||
lane: TenantInteractionLane::StandardActiveOperating,
|
||||
);
|
||||
}
|
||||
|
||||
private function evaluateOutcome(
|
||||
Tenant $tenant,
|
||||
?Request $request,
|
||||
TenantOperabilityQuestion $question,
|
||||
TenantInteractionLane $lane,
|
||||
): bool {
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $user instanceof User) {
|
||||
return 'not_member';
|
||||
return false;
|
||||
}
|
||||
|
||||
$workspaceId = $this->workspaceContext->currentWorkspaceId($request);
|
||||
|
||||
if ($workspaceId !== null && (int) $tenant->workspace_id !== (int) $workspaceId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $this->capabilityResolver->isMember($user, $tenant)) {
|
||||
return 'not_member';
|
||||
return false;
|
||||
}
|
||||
|
||||
$question = $pageCategory === TenantPageCategory::TenantBound
|
||||
? TenantOperabilityQuestion::TenantBoundViewability
|
||||
: TenantOperabilityQuestion::AdministrativeDiscoverability;
|
||||
|
||||
$allowed = $this->tenantOperabilityService->outcomeFor(
|
||||
return $this->tenantOperabilityService->outcomeFor(
|
||||
tenant: $tenant,
|
||||
question: $question,
|
||||
actor: $user,
|
||||
workspaceId: (int) $workspace->getKey(),
|
||||
lane: $pageCategory->lane(),
|
||||
workspaceId: $workspaceId,
|
||||
lane: $lane,
|
||||
selectedTenant: Filament::getTenant() instanceof Tenant ? Filament::getTenant() : null,
|
||||
)->allowed;
|
||||
|
||||
if ($allowed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $pageCategory === TenantPageCategory::TenantBound
|
||||
? 'inaccessible'
|
||||
: 'not_operable';
|
||||
}
|
||||
|
||||
private function pageCategory(?Request $request = null): TenantPageCategory
|
||||
|
||||
@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support\OperateHub;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Models\Workspace;
|
||||
use App\Support\Tenants\TenantPageCategory;
|
||||
|
||||
final readonly class ResolvedShellContext
|
||||
{
|
||||
public function __construct(
|
||||
public ?Workspace $workspace,
|
||||
public ?Tenant $tenant,
|
||||
public TenantPageCategory $pageCategory,
|
||||
public string $state,
|
||||
public string $displayMode,
|
||||
public string $workspaceSource = 'none',
|
||||
public string $tenantSource = 'none',
|
||||
public string $recoveryAction = 'none',
|
||||
public ?string $recoveryDestination = null,
|
||||
public ?string $recoveryReason = null,
|
||||
) {}
|
||||
|
||||
public function hasWorkspace(): bool
|
||||
{
|
||||
return $this->workspace instanceof Workspace;
|
||||
}
|
||||
|
||||
public function hasTenant(): bool
|
||||
{
|
||||
return $this->tenant instanceof Tenant;
|
||||
}
|
||||
|
||||
public function showsRecoveryNotice(): bool
|
||||
{
|
||||
return $this->displayMode === 'recovery' || $this->recoveryReason !== null;
|
||||
}
|
||||
}
|
||||
@ -15,11 +15,9 @@ public static function fromPageCategory(TenantPageCategory $pageCategory): self
|
||||
{
|
||||
return match ($pageCategory) {
|
||||
TenantPageCategory::OnboardingWorkflow => self::OnboardingWorkflow,
|
||||
TenantPageCategory::TenantBound,
|
||||
TenantPageCategory::TenantScopedEvidence => self::AdministrativeManagement,
|
||||
TenantPageCategory::TenantBound => self::AdministrativeManagement,
|
||||
TenantPageCategory::CanonicalWorkspaceRecordViewer => self::CanonicalWorkspaceRecord,
|
||||
TenantPageCategory::WorkspaceScoped,
|
||||
TenantPageCategory::WorkspaceChooserException => self::StandardActiveOperating,
|
||||
TenantPageCategory::WorkspaceScoped => self::StandardActiveOperating,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,9 +9,7 @@
|
||||
enum TenantPageCategory: string
|
||||
{
|
||||
case WorkspaceScoped = 'workspace_scoped';
|
||||
case WorkspaceChooserException = 'workspace_chooser_exception';
|
||||
case TenantBound = 'tenant_bound';
|
||||
case TenantScopedEvidence = 'tenant_scoped_evidence';
|
||||
case OnboardingWorkflow = 'onboarding_workflow';
|
||||
case CanonicalWorkspaceRecordViewer = 'canonical_workspace_record_viewer';
|
||||
|
||||
@ -28,21 +26,10 @@ public static function fromPath(string $path): self
|
||||
{
|
||||
$normalizedPath = '/'.ltrim($path, '/');
|
||||
|
||||
if ($normalizedPath === '/admin/choose-workspace') {
|
||||
return self::WorkspaceChooserException;
|
||||
}
|
||||
|
||||
if (preg_match('#^/admin/operations/[^/]+$#', $normalizedPath) === 1) {
|
||||
return self::CanonicalWorkspaceRecordViewer;
|
||||
}
|
||||
|
||||
if (
|
||||
str_starts_with($normalizedPath, '/admin/evidence/')
|
||||
&& ! str_starts_with($normalizedPath, '/admin/evidence/overview')
|
||||
) {
|
||||
return self::TenantScopedEvidence;
|
||||
}
|
||||
|
||||
if (preg_match('#^/admin/onboarding(?:/[^/]+)?$#', $normalizedPath) === 1) {
|
||||
return self::OnboardingWorkflow;
|
||||
}
|
||||
@ -57,41 +44,6 @@ public static function fromPath(string $path): self
|
||||
return self::WorkspaceScoped;
|
||||
}
|
||||
|
||||
public function allowsQueryTenantHints(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::WorkspaceScoped, self::OnboardingWorkflow => true,
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
public function allowsRememberedTenantRestore(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::WorkspaceScoped, self::OnboardingWorkflow, self::CanonicalWorkspaceRecordViewer => true,
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
public function allowsTenantlessState(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::WorkspaceScoped,
|
||||
self::WorkspaceChooserException,
|
||||
self::OnboardingWorkflow,
|
||||
self::CanonicalWorkspaceRecordViewer => true,
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
public function requiresExplicitTenant(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::TenantBound, self::TenantScopedEvidence => true,
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
public function lane(): TenantInteractionLane
|
||||
{
|
||||
return TenantInteractionLane::fromPageCategory($this);
|
||||
|
||||
@ -55,33 +55,6 @@ public function currentWorkspace(?Request $request = null): ?Workspace
|
||||
return $workspace;
|
||||
}
|
||||
|
||||
public function currentWorkspaceOrTenantWorkspace(?Tenant $tenant = null, ?Request $request = null): ?Workspace
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
|
||||
if ($workspace instanceof Workspace) {
|
||||
return $workspace;
|
||||
}
|
||||
|
||||
if (! $tenant instanceof Tenant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$workspace = $tenant->workspace()->first();
|
||||
|
||||
if (! $workspace instanceof Workspace || ! $this->isWorkspaceSelectable($workspace)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$user = $request?->user() instanceof User ? $request->user() : auth()->user();
|
||||
|
||||
if (! $user instanceof User || ! $this->isMember($user, $workspace) || ! $this->userCanAccessTenant($tenant, $request)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $workspace;
|
||||
}
|
||||
|
||||
public function setCurrentWorkspace(Workspace $workspace, ?User $user = null, ?Request $request = null): void
|
||||
{
|
||||
$session = ($request && $request->hasSession()) ? $request->session() : session();
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
use App\Filament\Pages\ChooseTenant;
|
||||
use App\Filament\Pages\ChooseWorkspace;
|
||||
use App\Filament\Pages\TenantDashboard;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Services\Tenants\TenantOperabilityService;
|
||||
@ -31,12 +30,8 @@ public function __construct(
|
||||
*
|
||||
* Returns a fully qualified URL string.
|
||||
*/
|
||||
public function resolve(Workspace $workspace, User $user, ?string $intendedUrl = null): string
|
||||
public function resolve(Workspace $workspace, User $user): string
|
||||
{
|
||||
if (is_string($intendedUrl) && $this->intendedUrlMatchesWorkspace($intendedUrl, $workspace, $user)) {
|
||||
return $intendedUrl;
|
||||
}
|
||||
|
||||
$selectableTenants = $this->tenantOperabilityService->filterSelectable($user->tenants()
|
||||
->where('workspace_id', $workspace->getKey())
|
||||
->orderBy('name')
|
||||
@ -76,45 +71,4 @@ public function resolveFromId(int $workspaceId, User $user): string
|
||||
|
||||
return $this->resolve($workspace, $user);
|
||||
}
|
||||
|
||||
private function intendedUrlMatchesWorkspace(string $intendedUrl, Workspace $workspace, User $user): bool
|
||||
{
|
||||
$path = '/'.ltrim((string) (parse_url($intendedUrl, PHP_URL_PATH) ?? ''), '/');
|
||||
|
||||
if (! str_starts_with($path, '/admin')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (preg_match('#^/admin/(?:t|tenants)/([^/]+)(?:/|$)#', $path, $matches) === 1) {
|
||||
return $this->tenantIdentifierMatchesWorkspace($matches[1], $workspace, $user);
|
||||
}
|
||||
|
||||
parse_str((string) (parse_url($intendedUrl, PHP_URL_QUERY) ?? ''), $query);
|
||||
|
||||
$tenantIdentifier = $query['tenant'] ?? $query['tenant_id'] ?? null;
|
||||
|
||||
if ($tenantIdentifier !== null && ! $this->tenantIdentifierMatchesWorkspace((string) $tenantIdentifier, $workspace, $user)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function tenantIdentifierMatchesWorkspace(string $identifier, Workspace $workspace, User $user): bool
|
||||
{
|
||||
$tenant = Tenant::query()
|
||||
->withTrashed()
|
||||
->where(static function ($query) use ($identifier): void {
|
||||
$query->where('external_id', $identifier);
|
||||
|
||||
if (ctype_digit($identifier)) {
|
||||
$query->orWhereKey((int) $identifier);
|
||||
}
|
||||
})
|
||||
->first();
|
||||
|
||||
return $tenant instanceof Tenant
|
||||
&& (int) $tenant->workspace_id === (int) $workspace->getKey()
|
||||
&& $user->canAccessTenant($tenant);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,14 +4,14 @@
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Support\OperateHub\OperateHubShell;
|
||||
use App\Support\Tenants\TenantPageCategory;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Filament\Facades\Filament;
|
||||
|
||||
/** @var WorkspaceContext $workspaceContext */
|
||||
$workspaceContext = app(WorkspaceContext::class);
|
||||
$resolvedContext = app(OperateHubShell::class)->resolvedContext(request());
|
||||
|
||||
$workspace = $resolvedContext->workspace;
|
||||
$workspace = $workspaceContext->currentWorkspace(request());
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
@ -22,17 +22,29 @@
|
||||
->values();
|
||||
}
|
||||
|
||||
$currentTenant = $resolvedContext->tenant;
|
||||
$operateHubShell = app(OperateHubShell::class);
|
||||
$currentTenant = $operateHubShell->activeEntitledTenant(request());
|
||||
$currentTenantId = $currentTenant instanceof Tenant ? (int) $currentTenant->getKey() : null;
|
||||
$currentTenantName = $currentTenant instanceof Tenant ? $currentTenant->getFilamentName() : null;
|
||||
|
||||
$hasAnyFilamentTenantContext = Filament::getTenant() instanceof Tenant;
|
||||
|
||||
$route = request()->route();
|
||||
$routeName = (string) ($route?->getName() ?? '');
|
||||
$pageCategory = TenantPageCategory::fromRequest(request());
|
||||
$tenantQuery = request()->query('tenant');
|
||||
$hasTenantQuery = is_string($tenantQuery) && trim($tenantQuery) !== '';
|
||||
|
||||
$isTenantScopedRoute = $pageCategory === TenantPageCategory::TenantBound
|
||||
|| ($hasTenantQuery && str_starts_with($routeName, 'filament.admin.'));
|
||||
|
||||
$lastTenantId = $workspaceContext->lastTenantId(request());
|
||||
$canClearTenantContext = $currentTenant instanceof Tenant || $lastTenantId !== null;
|
||||
$canClearTenantContext = $hasAnyFilamentTenantContext || $lastTenantId !== null;
|
||||
@endphp
|
||||
|
||||
@php
|
||||
$tenantLabel = $currentTenantName ?? 'No tenant selected';
|
||||
$workspaceLabel = $workspace?->name ?? 'Choose workspace';
|
||||
$tenantLabel = $currentTenantName ?? 'All tenants';
|
||||
$workspaceLabel = $workspace?->name ?? 'Select workspace';
|
||||
$hasActiveTenant = $currentTenantName !== null;
|
||||
$managedTenantsUrl = $workspace
|
||||
? route('admin.workspace.managed-tenants.index', ['workspace' => $workspace])
|
||||
@ -40,7 +52,7 @@
|
||||
$workspaceUrl = $workspace
|
||||
? route('admin.home')
|
||||
: ChooseWorkspace::getUrl(panel: 'admin');
|
||||
$tenantTriggerLabel = $workspace ? $tenantLabel : 'Choose workspace';
|
||||
$tenantTriggerLabel = $workspace ? ($hasActiveTenant ? $tenantLabel : 'No tenant selected') : 'Select tenant';
|
||||
@endphp
|
||||
|
||||
<div class="inline-flex items-center gap-0 rounded-lg border border-gray-200 bg-white text-sm dark:border-white/10 dark:bg-white/5">
|
||||
@ -76,18 +88,6 @@ class="inline-flex items-center gap-1.5 rounded-r-lg px-2 py-1.5 transition hove
|
||||
|
||||
<x-filament::dropdown.list>
|
||||
<div class="space-y-3 px-3 py-2" x-data="{ query: '' }">
|
||||
@if ($resolvedContext->showsRecoveryNotice())
|
||||
<div class="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-200">
|
||||
<div class="font-semibold">Context unavailable</div>
|
||||
|
||||
@if ($workspace)
|
||||
<div>The requested scope could not be restored. The shell is showing a valid workspace state instead.</div>
|
||||
@else
|
||||
<div>Choose a workspace to continue with a valid admin context.</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Workspace section --}}
|
||||
<div class="space-y-1">
|
||||
<div class="text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
||||
@ -128,28 +128,16 @@ class="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-gray-700 transi
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($resolvedContext->pageCategory->requiresExplicitTenant() && $hasActiveTenant)
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2 rounded-lg bg-primary-50 px-3 py-2 dark:bg-primary-950/30">
|
||||
<x-filament::icon icon="heroicon-o-building-office-2" class="h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||||
<span class="text-sm font-medium text-primary-700 dark:text-primary-300">{{ $currentTenantName }}</span>
|
||||
<a
|
||||
href="{{ ChooseTenant::getUrl(panel: 'admin') }}"
|
||||
class="ml-auto text-xs font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300"
|
||||
>
|
||||
Switch tenant
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if ($canClearTenantContext)
|
||||
<form method="POST" action="{{ route('admin.clear-tenant-context') }}">
|
||||
@csrf
|
||||
|
||||
<button type="submit" class="w-full rounded-lg px-3 py-1.5 text-left text-xs font-medium text-gray-500 transition hover:bg-gray-50 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-200">
|
||||
Clear tenant scope
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
@if ($isTenantScopedRoute)
|
||||
<div class="flex items-center gap-2 rounded-lg bg-primary-50 px-3 py-2 dark:bg-primary-950/30">
|
||||
<x-filament::icon icon="heroicon-o-building-office-2" class="h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||||
<span class="text-sm font-medium text-primary-700 dark:text-primary-300">{{ $currentTenantName ?? 'Tenant' }}</span>
|
||||
<a
|
||||
href="{{ ChooseTenant::getUrl(panel: 'admin') }}"
|
||||
class="ml-auto text-xs font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300"
|
||||
>
|
||||
Switch tenant
|
||||
</a>
|
||||
</div>
|
||||
@else
|
||||
@if ($tenants->isEmpty())
|
||||
|
||||
@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('shows a recovery label when workspace-scoped surfaces render without an active workspace', function (): void {
|
||||
$tenant = Tenant::factory()->active()->create(['name' => 'Recovery Tenant']);
|
||||
[$user] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||
|
||||
session()->forget(WorkspaceContext::SESSION_KEY);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get('/admin/workspaces')
|
||||
->assertOk()
|
||||
->assertSee('Context unavailable')
|
||||
->assertSee('Choose workspace');
|
||||
});
|
||||
|
||||
it('shows explicit recovery wording when an invalid tenant hint is discarded on a workspace route', function (): void {
|
||||
$validTenant = Tenant::factory()->active()->create(['name' => 'Valid Workspace Tenant']);
|
||||
[$user, $validTenant] = createUserWithTenant(tenant: $validTenant, role: 'owner');
|
||||
|
||||
$foreignTenant = Tenant::factory()->active()->create(['name' => 'Foreign Workspace Tenant']);
|
||||
createUserWithTenant(tenant: $foreignTenant, user: User::factory()->create(), role: 'owner');
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([
|
||||
WorkspaceContext::SESSION_KEY => (int) $validTenant->workspace_id,
|
||||
])
|
||||
->get(route('admin.operations.index', ['tenant' => $foreignTenant->external_id]))
|
||||
->assertOk()
|
||||
->assertSee('Context unavailable')
|
||||
->assertSee('No tenant selected')
|
||||
->assertDontSee('Tenant scope: '.$foreignTenant->name);
|
||||
});
|
||||
@ -5,7 +5,6 @@
|
||||
use App\Models\Workspace;
|
||||
use App\Models\WorkspaceMembership;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
@ -64,36 +63,3 @@
|
||||
->assertSee($workspace->name)
|
||||
->assertDontSee('name="workspace_id"', escape: false);
|
||||
});
|
||||
|
||||
test('workspace-scoped operations honor a valid tenant query hint over remembered tenant context', function () {
|
||||
$rememberedTenant = Tenant::factory()->create([
|
||||
'workspace_id' => null,
|
||||
'status' => 'active',
|
||||
'name' => 'Remembered Topbar Tenant',
|
||||
]);
|
||||
[$user, $rememberedTenant] = createUserWithTenant(tenant: $rememberedTenant, role: 'owner');
|
||||
|
||||
$hintedTenant = Tenant::factory()->create([
|
||||
'workspace_id' => (int) $rememberedTenant->workspace_id,
|
||||
'status' => 'active',
|
||||
'name' => 'Hinted Topbar Tenant',
|
||||
]);
|
||||
|
||||
createUserWithTenant(tenant: $hintedTenant, user: $user, role: 'owner');
|
||||
|
||||
Filament::setTenant(null, true);
|
||||
|
||||
$workspaceId = (int) $rememberedTenant->workspace_id;
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([
|
||||
WorkspaceContext::SESSION_KEY => $workspaceId,
|
||||
WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [
|
||||
(string) $workspaceId => (int) $rememberedTenant->getKey(),
|
||||
],
|
||||
])
|
||||
->get(route('admin.operations.index', ['tenant' => $hintedTenant->external_id]))
|
||||
->assertOk()
|
||||
->assertSee('Tenant scope: Hinted Topbar Tenant')
|
||||
->assertDontSee('Tenant scope: Remembered Topbar Tenant');
|
||||
});
|
||||
|
||||
@ -65,11 +65,13 @@
|
||||
$this->actingAs($user)
|
||||
->get('/admin/workspaces')
|
||||
->assertOk()
|
||||
->assertSee('Select workspace')
|
||||
->assertSee('Select tenant')
|
||||
->assertSee('Choose a workspace first.')
|
||||
->assertDontSee('Search tenants…');
|
||||
});
|
||||
|
||||
it('keeps tenant-scoped pages selector read-only while exposing the clear tenant scope action', function (): void {
|
||||
it('renders the tenant indicator read-only on tenant-scoped pages (Filament tenant menu is primary)', function (): void {
|
||||
$tenant = Tenant::factory()->create(['status' => 'active']);
|
||||
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner');
|
||||
|
||||
@ -80,10 +82,12 @@
|
||||
->get(route('filament.admin.resources.tenants.index', filamentTenantRouteParams($tenant)))
|
||||
->assertOk()
|
||||
->assertSee($tenant->getFilamentName())
|
||||
->assertSee('Clear tenant scope');
|
||||
->assertDontSee('Search tenants…')
|
||||
->assertDontSee('admin/select-tenant')
|
||||
->assertDontSee('Clear tenant scope');
|
||||
});
|
||||
|
||||
it('renders routed tenant resource view pages with a clear tenant scope action but no inline selector list', function (): void {
|
||||
it('renders the routed tenant as read-only context on tenant resource view pages', function (): void {
|
||||
$currentTenant = Tenant::factory()->create([
|
||||
'status' => 'active',
|
||||
'name' => 'YPTW2',
|
||||
@ -113,7 +117,9 @@
|
||||
->assertOk()
|
||||
->assertSee($routedTenant->getFilamentName())
|
||||
->assertSee('Switch tenant')
|
||||
->assertSee('Clear tenant scope');
|
||||
->assertDontSee('Search tenants…')
|
||||
->assertDontSee('admin/select-tenant')
|
||||
->assertDontSee('Clear tenant scope');
|
||||
});
|
||||
|
||||
it('filters the header tenant picker to tenants the user can access', function (): void {
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Pages\TenantDashboard;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('shows the routed workspace and tenant truth on tenant-panel entry without relying on session workspace state', function (): void {
|
||||
$tenant = Tenant::factory()->active()->create(['name' => 'Tenant Panel Entry']);
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||
|
||||
session()->forget(WorkspaceContext::SESSION_KEY);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant))
|
||||
->assertOk()
|
||||
->assertSee($tenant->workspace()->firstOrFail()->name)
|
||||
->assertSee('Tenant Panel Entry')
|
||||
->assertSee('Switch tenant')
|
||||
->assertSee('Clear tenant scope')
|
||||
->assertDontSee('Search tenants…')
|
||||
->assertDontSee('admin/select-tenant');
|
||||
});
|
||||
|
||||
it('keeps workspace-scoped routes tenantless when a cross-workspace tenant hint is rejected', function (): void {
|
||||
$workspaceTenant = Tenant::factory()->active()->create(['name' => 'Workspace Tenant']);
|
||||
[$user, $workspaceTenant] = createUserWithTenant(tenant: $workspaceTenant, role: 'owner');
|
||||
|
||||
$foreignTenant = Tenant::factory()->active()->create(['name' => 'Rejected Foreign Tenant']);
|
||||
createUserWithTenant(tenant: $foreignTenant, user: User::factory()->create(), role: 'owner');
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspaceTenant->workspace_id])
|
||||
->get(route('admin.operations.index', ['tenant' => $foreignTenant->external_id]))
|
||||
->assertOk()
|
||||
->assertSee('No tenant selected')
|
||||
->assertDontSee('Tenant scope: Rejected Foreign Tenant');
|
||||
});
|
||||
@ -87,43 +87,3 @@
|
||||
])
|
||||
->assertRedirect('/admin/choose-workspace');
|
||||
});
|
||||
|
||||
it('returns 404 when selecting a tenant from another workspace', function (): void {
|
||||
$activeTenant = Tenant::factory()->active()->create(['name' => 'Current Workspace Tenant']);
|
||||
[$user, $activeTenant] = createUserWithTenant(tenant: $activeTenant, role: 'owner');
|
||||
|
||||
$foreignTenant = Tenant::factory()->active()->create(['name' => 'Foreign Workspace Tenant']);
|
||||
createUserWithTenant(tenant: $foreignTenant, user: $user, role: 'owner');
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $activeTenant->workspace_id])
|
||||
->post(route('admin.select-tenant'), [
|
||||
'tenant_id' => (int) $foreignTenant->getKey(),
|
||||
])
|
||||
->assertNotFound();
|
||||
|
||||
expect(session()->get(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, []))
|
||||
->not->toHaveKey((string) $activeTenant->workspace_id);
|
||||
});
|
||||
|
||||
it('returns 404 when selecting a tenant the user cannot access', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
|
||||
WorkspaceMembership::factory()->create([
|
||||
'workspace_id' => (int) $workspace->getKey(),
|
||||
'user_id' => (int) $user->getKey(),
|
||||
'role' => 'owner',
|
||||
]);
|
||||
|
||||
$tenant = Tenant::factory()->active()->create([
|
||||
'workspace_id' => (int) $workspace->getKey(),
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
||||
->post(route('admin.select-tenant'), [
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
])
|
||||
->assertNotFound();
|
||||
});
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Pages\TenantDashboard;
|
||||
use App\Models\Tenant;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('ignores an intended tenant route that is not valid in the target workspace', function (): void {
|
||||
$sourceTenant = Tenant::factory()->active()->create(['name' => 'Source Tenant']);
|
||||
[$user, $sourceTenant] = createUserWithTenant(tenant: $sourceTenant, role: 'owner');
|
||||
|
||||
$targetWorkspaceTenant = Tenant::factory()->active()->create([
|
||||
'name' => 'Target Tenant',
|
||||
]);
|
||||
createUserWithTenant(tenant: $targetWorkspaceTenant, user: $user, role: 'owner');
|
||||
|
||||
$targetWorkspace = $targetWorkspaceTenant->workspace()->firstOrFail();
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->withSession([
|
||||
WorkspaceContext::SESSION_KEY => (int) $sourceTenant->workspace_id,
|
||||
WorkspaceContext::INTENDED_URL_SESSION_KEY => "/admin/t/{$sourceTenant->external_id}",
|
||||
])
|
||||
->post(route('admin.switch-workspace'), [
|
||||
'workspace_id' => (int) $targetWorkspace->getKey(),
|
||||
]);
|
||||
|
||||
$response->assertRedirect(TenantDashboard::getUrl(panel: 'tenant', tenant: $targetWorkspaceTenant));
|
||||
expect(session(WorkspaceContext::SESSION_KEY))->toBe((int) $targetWorkspace->getKey());
|
||||
});
|
||||
@ -119,59 +119,3 @@
|
||||
|
||||
expect($url)->toBe($expectedRoute);
|
||||
});
|
||||
|
||||
it('preserves a safe intended admin url that targets a tenant in the selected workspace', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
|
||||
WorkspaceMembership::factory()->create([
|
||||
'workspace_id' => $workspace->getKey(),
|
||||
'user_id' => $user->getKey(),
|
||||
'role' => 'owner',
|
||||
]);
|
||||
|
||||
$tenant = Tenant::factory()->create([
|
||||
'workspace_id' => $workspace->getKey(),
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$intendedUrl = route('admin.operations.index', ['tenant' => $tenant->external_id]);
|
||||
|
||||
$url = $this->resolver->resolve($workspace, $user, $intendedUrl);
|
||||
|
||||
expect($url)->toBe($intendedUrl);
|
||||
});
|
||||
|
||||
it('rejects an unsafe intended admin url when its tenant hint targets another workspace', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
|
||||
WorkspaceMembership::factory()->create([
|
||||
'workspace_id' => $workspace->getKey(),
|
||||
'user_id' => $user->getKey(),
|
||||
'role' => 'owner',
|
||||
]);
|
||||
|
||||
$tenant = Tenant::factory()->create([
|
||||
'workspace_id' => $workspace->getKey(),
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$foreignTenant = Tenant::factory()->create([
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
$intendedUrl = route('admin.operations.index', ['tenant' => $foreignTenant->external_id]);
|
||||
|
||||
$url = $this->resolver->resolve($workspace, $user, $intendedUrl);
|
||||
|
||||
expect($url)->toBe(TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant));
|
||||
});
|
||||
|
||||
@ -45,7 +45,7 @@
|
||||
$this->actingAs($user)
|
||||
->get('/admin/workspaces')
|
||||
->assertOk()
|
||||
->assertSee('Choose workspace')
|
||||
->assertSee('Select workspace')
|
||||
->assertSee('Choose a workspace first.');
|
||||
});
|
||||
|
||||
|
||||
@ -1,102 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Support\OperateHub\OperateHubShell;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('prefers a valid tenant query hint over remembered tenant state on workspace-scoped admin routes', function (): void {
|
||||
$rememberedTenant = Tenant::factory()->active()->create(['name' => 'Remembered Tenant']);
|
||||
[$user, $rememberedTenant] = createUserWithTenant(tenant: $rememberedTenant, role: 'owner');
|
||||
|
||||
$hintedTenant = Tenant::factory()->active()->create([
|
||||
'workspace_id' => (int) $rememberedTenant->workspace_id,
|
||||
'name' => 'Hinted Tenant',
|
||||
]);
|
||||
|
||||
createUserWithTenant(tenant: $hintedTenant, user: $user, role: 'owner');
|
||||
|
||||
$this->actingAs($user);
|
||||
Filament::setTenant(null, true);
|
||||
|
||||
$workspaceId = (int) $rememberedTenant->workspace_id;
|
||||
|
||||
session()->put(WorkspaceContext::SESSION_KEY, $workspaceId);
|
||||
session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [
|
||||
(string) $workspaceId => (int) $rememberedTenant->getKey(),
|
||||
]);
|
||||
|
||||
$request = Request::create(route('admin.operations.index', ['tenant' => $hintedTenant->external_id]));
|
||||
$request->setLaravelSession(app('session.store'));
|
||||
$request->setUserResolver(static fn () => $user);
|
||||
|
||||
$route = app('router')->getRoutes()->match($request);
|
||||
$request->setRouteResolver(static fn () => $route);
|
||||
|
||||
$resolved = app(OperateHubShell::class)->resolvedContext($request);
|
||||
|
||||
expect($resolved->workspace?->getKey())->toBe($workspaceId)
|
||||
->and($resolved->tenant?->is($hintedTenant))->toBeTrue()
|
||||
->and($resolved->tenantSource)->toBe('query_hint')
|
||||
->and($resolved->state)->toBe('tenant_scoped');
|
||||
});
|
||||
|
||||
it('falls back to a tenantless workspace state when a tenant query hint targets another workspace', function (): void {
|
||||
$workspaceTenant = Tenant::factory()->active()->create(['name' => 'Current Workspace Tenant']);
|
||||
[$user, $workspaceTenant] = createUserWithTenant(tenant: $workspaceTenant, role: 'owner');
|
||||
|
||||
$foreignTenant = Tenant::factory()->active()->create(['name' => 'Foreign Tenant']);
|
||||
createUserWithTenant(tenant: $foreignTenant, user: User::factory()->create(), role: 'owner');
|
||||
|
||||
$this->actingAs($user);
|
||||
Filament::setTenant(null, true);
|
||||
|
||||
$workspaceId = (int) $workspaceTenant->workspace_id;
|
||||
|
||||
session()->put(WorkspaceContext::SESSION_KEY, $workspaceId);
|
||||
|
||||
$request = Request::create(route('admin.operations.index', ['tenant' => $foreignTenant->external_id]));
|
||||
$request->setLaravelSession(app('session.store'));
|
||||
$request->setUserResolver(static fn () => $user);
|
||||
|
||||
$route = app('router')->getRoutes()->match($request);
|
||||
$request->setRouteResolver(static fn () => $route);
|
||||
|
||||
$resolved = app(OperateHubShell::class)->resolvedContext($request);
|
||||
|
||||
expect($resolved->workspace?->getKey())->toBe($workspaceId)
|
||||
->and($resolved->tenant)->toBeNull()
|
||||
->and($resolved->state)->toBe('tenantless_workspace')
|
||||
->and($resolved->recoveryReason)->toBe('mismatched_workspace');
|
||||
});
|
||||
|
||||
it('uses the routed tenant workspace when the tenant panel is entered without a selected workspace session', function (): void {
|
||||
$tenant = Tenant::factory()->active()->create(['name' => 'Tenant Panel Scope']);
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||
|
||||
$this->actingAs($user);
|
||||
Filament::setTenant(null, true);
|
||||
|
||||
session()->forget(WorkspaceContext::SESSION_KEY);
|
||||
|
||||
$request = Request::create("/admin/t/{$tenant->external_id}");
|
||||
$request->setLaravelSession(app('session.store'));
|
||||
$request->setUserResolver(static fn () => $user);
|
||||
|
||||
$route = app('router')->getRoutes()->match($request);
|
||||
$request->setRouteResolver(static fn () => $route);
|
||||
|
||||
$resolved = app(OperateHubShell::class)->resolvedContext($request);
|
||||
|
||||
expect($resolved->workspace?->getKey())->toBe((int) $tenant->workspace_id)
|
||||
->and($resolved->tenant?->is($tenant))->toBeTrue()
|
||||
->and($resolved->workspaceSource)->toBe('route')
|
||||
->and($resolved->tenantSource)->toBe('route');
|
||||
});
|
||||
@ -11,12 +11,9 @@
|
||||
expect(TenantPageCategory::fromPath($path))->toBe($expected);
|
||||
})->with([
|
||||
'workspace overview' => ['/admin', TenantPageCategory::WorkspaceScoped],
|
||||
'workspace chooser exception' => ['/admin/choose-workspace', TenantPageCategory::WorkspaceChooserException],
|
||||
'tenant chooser' => ['/admin/choose-tenant', TenantPageCategory::WorkspaceScoped],
|
||||
'tenant detail' => ['/admin/tenants/tenant-123', TenantPageCategory::TenantBound],
|
||||
'tenant panel route' => ['/admin/t/tenant-123', TenantPageCategory::TenantBound],
|
||||
'tenant scoped evidence detail' => ['/admin/evidence/123', TenantPageCategory::TenantScopedEvidence],
|
||||
'evidence overview' => ['/admin/evidence/overview', TenantPageCategory::WorkspaceScoped],
|
||||
'onboarding index' => ['/admin/onboarding', TenantPageCategory::OnboardingWorkflow],
|
||||
'onboarding draft' => ['/admin/onboarding/42', TenantPageCategory::OnboardingWorkflow],
|
||||
'operations index' => ['/admin/operations', TenantPageCategory::WorkspaceScoped],
|
||||
|
||||
@ -635,11 +635,10 @@ ### Key Commands
|
||||
```bash
|
||||
# Local dev
|
||||
corepack pnpm install # Install workspace JS dependencies
|
||||
corepack pnpm dev:platform # Start the platform stack + panel Vite watcher
|
||||
corepack pnpm dev:platform # Start the platform stack
|
||||
corepack pnpm dev:website # Start the website
|
||||
corepack pnpm dev # Start platform Vite + website together
|
||||
corepack pnpm build:website # Build the website
|
||||
corepack pnpm build:platform # Build platform frontend inside Sail
|
||||
cd apps/platform && ./vendor/bin/sail pnpm build # Build platform frontend
|
||||
|
||||
# Testing
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact # Full suite
|
||||
|
||||
@ -1,237 +0,0 @@
|
||||
# Website Working Contract
|
||||
|
||||
> Guardrails for evolving `apps/website` as an independently evolvable track in the current repository.
|
||||
> This document is repo-truth-based and describes the currently verified state, not a speculative future architecture.
|
||||
|
||||
**Last reviewed**: 2026-04-18
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
`apps/website` is currently treated as an independently evolvable website track.
|
||||
|
||||
This contract is based on the current repository state:
|
||||
|
||||
- no verified runtime coupling to `apps/platform`
|
||||
- no verified shared DTO, enum, API, or auth contracts
|
||||
- current relevant coupling is primarily through root scripts, the pnpm workspace, the website package name, `WEBSITE_PORT`, and `apps/platform/tests/Feature/WorkspaceFoundation/PlatformWorkspaceCompatibilityTest.php`
|
||||
|
||||
Therefore, website work should be planned and implemented as technically independent by default unless a known minimal contract is being changed.
|
||||
|
||||
## 2. Scope
|
||||
|
||||
This working contract applies to:
|
||||
|
||||
- `apps/website/**`
|
||||
- root files only insofar as they directly define or preserve website-facing workspace contracts
|
||||
|
||||
This document does not grant implicit approval to introduce new coupling to:
|
||||
|
||||
- `apps/platform`
|
||||
- shared runtime APIs
|
||||
- shared auth or session behavior
|
||||
- shared domain models
|
||||
- shared packages
|
||||
|
||||
Such coupling must be introduced deliberately and treated as a new explicit contract.
|
||||
|
||||
## 3. Current Repo-Based Statement
|
||||
|
||||
At the current verified repo state, `apps/website` is:
|
||||
|
||||
- a small static Astro application
|
||||
- built from local layout, local page, and local CSS files
|
||||
- without verified platform runtime dependency
|
||||
- without verified shared workspace packages
|
||||
- without verified platform API usage
|
||||
- without verified auth or session sharing
|
||||
|
||||
The current real technical coupling is concentrated in:
|
||||
|
||||
- `package.json`
|
||||
- `pnpm-workspace.yaml`
|
||||
- `apps/website/package.json`
|
||||
- `README.md`
|
||||
- `apps/platform/tests/Feature/WorkspaceFoundation/PlatformWorkspaceCompatibilityTest.php`
|
||||
|
||||
## 4. Freely Changeable Without Special Coordination
|
||||
|
||||
The following changes are currently treated as website-internal and are generally free to make as long as they do not create new outward-facing contracts.
|
||||
|
||||
### Content and Structure
|
||||
|
||||
- copy
|
||||
- headlines
|
||||
- messaging
|
||||
- CTA text
|
||||
- page sections
|
||||
- HTML structure inside the website
|
||||
|
||||
### UI and Frontend
|
||||
|
||||
- styling in `src/styles/global.css`
|
||||
- layout changes in `src/layouts/BaseLayout.astro`
|
||||
- additional components inside `apps/website`
|
||||
- additional pages under `src/pages`
|
||||
- additional layouts
|
||||
- additional local assets
|
||||
|
||||
### Astro-Local Evolution
|
||||
|
||||
- expanding page structure
|
||||
- refactoring inline markup into components
|
||||
- app-local reorganization inside `apps/website`
|
||||
- local SEO, meta, robots, and favicon updates
|
||||
|
||||
### Usually Non-Critical When Kept Local
|
||||
|
||||
- new website-only dependencies
|
||||
- app-local build or styling improvements
|
||||
- app-local content structures
|
||||
|
||||
## 5. Changes Requiring Coordination
|
||||
|
||||
The following changes must not happen silently because they touch real existing contracts in the current repository.
|
||||
|
||||
### Hard Minimal Contracts
|
||||
|
||||
- changing the package name `@tenantatlas/website`
|
||||
- changing the root script names for website flows
|
||||
- changing the `WEBSITE_PORT` convention
|
||||
- changing workspace membership via `apps/*`
|
||||
- moving `apps/website` out of the current workspace pattern
|
||||
|
||||
### Repo and Tooling Contracts
|
||||
|
||||
- changes that break `pnpm --filter @tenantatlas/website ...`
|
||||
- changes that break the root `dev:website` or `build:website` workflows
|
||||
- changes that break `PlatformWorkspaceCompatibilityTest.php`
|
||||
- changes to root scripts when the website is expected to remain reachable through root commands
|
||||
|
||||
### New Technical Couplings
|
||||
|
||||
- introducing API calls to `apps/platform`
|
||||
- introducing shared DTOs, enums, or payload contracts
|
||||
- introducing shared auth or session mechanics
|
||||
- introducing shared packages as a required dependency
|
||||
- introducing shared content or asset sources
|
||||
- introducing a shared runtime deployment model
|
||||
|
||||
## 6. Silent Assumptions That Are Not Allowed
|
||||
|
||||
For website work, the following assumptions are explicitly disallowed:
|
||||
|
||||
- monorepo membership does not automatically mean technical coupling
|
||||
- `apps/platform` is not automatically part of website frontend scope
|
||||
- Filament or Livewire changes are not automatically website-relevant
|
||||
- shared product domain does not automatically imply a shared technical contract
|
||||
- future integrations must not be pre-assumed when they do not exist in the repo
|
||||
|
||||
## 7. Rule for New Couplings
|
||||
|
||||
New coupling between website and platform is allowed only when it is introduced as an explicit new contract.
|
||||
|
||||
Before implementation, the following must be made clear:
|
||||
|
||||
- which coupling is being introduced
|
||||
- where it is defined in the repo
|
||||
- whether it is runtime, build, deploy, or convention coupling
|
||||
- what impact it has on independent evolution of `apps/website`
|
||||
- what stability guarantee is expected going forward
|
||||
|
||||
Without that explicit decision, the default rule is:
|
||||
|
||||
**Do not introduce a new coupling.**
|
||||
|
||||
## 8. Review Rule for Website PRs
|
||||
|
||||
A change in `apps/website` is uncritical by default when all of the following can be answered with `Yes`:
|
||||
|
||||
- Does the package name `@tenantatlas/website` remain unchanged?
|
||||
- Do the root scripts remain functionally compatible?
|
||||
- Does `WEBSITE_PORT` remain unchanged?
|
||||
- Does workspace membership through `apps/*` remain intact?
|
||||
- Is no new dependency on `apps/platform` introduced?
|
||||
- Is no new API, auth, DTO, or shared-package contract introduced?
|
||||
- Does the change stay entirely inside `apps/website`?
|
||||
|
||||
If any answer is `No`, the change is coordination-required.
|
||||
|
||||
## 9. Change Classes
|
||||
|
||||
### Class A — Free
|
||||
|
||||
Pure website change with no new outward-facing contract.
|
||||
|
||||
Examples:
|
||||
|
||||
- new landing page
|
||||
- refactor of `index.astro`
|
||||
- new components
|
||||
- CSS restructuring
|
||||
- local SEO changes
|
||||
|
||||
### Class B — Cautious
|
||||
|
||||
Change likely remains local but touches website build or dev behavior.
|
||||
|
||||
Examples:
|
||||
|
||||
- adjusting Astro configuration
|
||||
- adding website dependencies
|
||||
- larger structural changes in `src/`
|
||||
|
||||
Normal website review is usually sufficient.
|
||||
|
||||
### Class C — Coordination-Required
|
||||
|
||||
Change touches root, workspace, or test contracts, or introduces new coupling.
|
||||
|
||||
Examples:
|
||||
|
||||
- renaming the package
|
||||
- changing scripts
|
||||
- changing the port convention
|
||||
- changing the workspace pattern
|
||||
- integrating a platform API
|
||||
- introducing shared types
|
||||
|
||||
These changes require explicit coordination.
|
||||
|
||||
## 10. Operational Summary
|
||||
|
||||
Yes: `apps/website` may currently be developed as its own frontend and website track.
|
||||
|
||||
Condition: the known minimal contracts stay stable:
|
||||
|
||||
- `@tenantatlas/website`
|
||||
- root scripts
|
||||
- `WEBSITE_PORT`
|
||||
- `apps/*`
|
||||
- compatibility with `PlatformWorkspaceCompatibilityTest.php`
|
||||
|
||||
Not automatically part of website scope:
|
||||
|
||||
- changes in `apps/platform`
|
||||
|
||||
Introduce only deliberately:
|
||||
|
||||
- any new runtime, API, auth, shared-package, or DTO coupling
|
||||
|
||||
## 11. Evidence Anchors
|
||||
|
||||
This contract is grounded in the current repo state represented by these files:
|
||||
|
||||
- `apps/website/package.json`
|
||||
- `apps/website/astro.config.mjs`
|
||||
- `apps/website/src/pages/index.astro`
|
||||
- `apps/website/src/layouts/BaseLayout.astro`
|
||||
- `apps/website/src/styles/global.css`
|
||||
- `package.json`
|
||||
- `pnpm-workspace.yaml`
|
||||
- `README.md`
|
||||
- `apps/platform/tests/Feature/WorkspaceFoundation/PlatformWorkspaceCompatibilityTest.php`
|
||||
- `docker-compose.yml`
|
||||
|
||||
If these files change materially, this contract should be revalidated against the current checkout.
|
||||
@ -11,11 +11,6 @@ # Operator UX & Surface Standards
|
||||
|
||||
This document is normative for new operator-facing UI work and for major UI refactors.
|
||||
|
||||
It follows the constitution vocabulary for `Native Surface`, `Custom Surface`,
|
||||
`Shared Detail Micro-UI`, and shell/page/detail state ownership. This
|
||||
document complements the constitution and MUST NOT be used as a parallel
|
||||
rulebook that redefines those terms.
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
TenantPilot is not a generic admin UI. It is an enterprise operator product for managed Microsoft tenant governance, backup, restore, monitoring, drift detection, and review workflows.
|
||||
|
||||
@ -6,10 +6,10 @@
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "bash ./scripts/dev-workspace",
|
||||
"dev:platform": "bash ./scripts/dev-platform",
|
||||
"dev": "./scripts/platform-sail up -d && corepack pnpm dev:website",
|
||||
"dev:platform": "./scripts/platform-sail up -d",
|
||||
"dev:website": "WEBSITE_PORT=${WEBSITE_PORT:-4321} corepack pnpm --filter @tenantatlas/website dev",
|
||||
"build:platform": "./scripts/platform-sail pnpm build",
|
||||
"build:platform": "corepack pnpm --filter @tenantatlas/platform build",
|
||||
"build:website": "corepack pnpm --filter @tenantatlas/website build"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
"${SCRIPT_DIR}/platform-sail" up -d
|
||||
|
||||
if curl --silent --fail --max-time 2 http://127.0.0.1:5173/@vite/client >/dev/null; then
|
||||
echo "Platform Vite dev server already running at http://localhost:5173"
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exec bash "${SCRIPT_DIR}/platform-vite-dev"
|
||||
@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
platform_pid=''
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "${platform_pid}" ]]; then
|
||||
kill "${platform_pid}" 2>/dev/null || true
|
||||
wait "${platform_pid}" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
"${SCRIPT_DIR}/platform-sail" up -d
|
||||
|
||||
if curl --silent --fail --max-time 2 http://127.0.0.1:5173/@vite/client >/dev/null; then
|
||||
echo "Platform Vite dev server already running at http://localhost:5173"
|
||||
else
|
||||
bash "${SCRIPT_DIR}/platform-vite-dev" &
|
||||
platform_pid=$!
|
||||
fi
|
||||
|
||||
cd "${ROOT_DIR}"
|
||||
WEBSITE_PORT="${WEBSITE_PORT:-4321}" corepack pnpm --filter @tenantatlas/website dev
|
||||
@ -11,6 +11,4 @@ APP_DIR="${SCRIPT_DIR}/../apps/platform"
|
||||
|
||||
cd "${APP_DIR}"
|
||||
|
||||
export COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-tenantatlas}"
|
||||
|
||||
exec ./vendor/bin/sail "$@"
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
exec "${SCRIPT_DIR}/platform-sail" pnpm dev
|
||||
175
specs/001-dashboard-recovery-honesty/spec.md
Normal file
175
specs/001-dashboard-recovery-honesty/spec.md
Normal file
@ -0,0 +1,175 @@
|
||||
# Feature Specification: Dashboard Recovery Posture Honesty
|
||||
|
||||
**Feature Branch**: `[001-dashboard-recovery-honesty]`
|
||||
**Created**: 2026-04-08
|
||||
**Status**: Draft
|
||||
**Input**: User description: "Spec 184 — Dashboard Recovery Posture Honesty"
|
||||
|
||||
## Spec Scope Fields *(mandatory)*
|
||||
|
||||
- **Scope**: tenant
|
||||
- **Primary Routes**: `/admin/t/{tenant}`, `/admin/t/{tenant}/restore-runs`, `/admin/t/{tenant}/restore-runs/{record}`, `/admin/t/{tenant}/backup-sets`, `/admin/t/{tenant}/backup-sets/{record}`
|
||||
- **Data Ownership**: Tenant-owned `BackupSet`, `RestoreRun`, and linked `OperationRun` outcome context are read within the active workspace and tenant scope to derive a more honest overview statement. No new persisted recovery-confidence state is introduced.
|
||||
- **RBAC**: Workspace plus tenant membership remains required on every affected surface. Members who can open the tenant dashboard must see honest summary boundaries even when they cannot start or manage restore runs. Existing restore-run creation and mutation actions remain under current restore permissions. Non-members continue to receive deny-as-not-found semantics.
|
||||
|
||||
## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)*
|
||||
|
||||
| Surface | Surface Type | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Tenant dashboard KPI strip | Dashboard / stats overview | Explicit stat click per signal | forbidden | Supporting text inside the stat description | none | `/admin/t/{tenant}` | Signal-specific drill-through to `/admin/t/{tenant}/restore-runs` or `/admin/t/{tenant}/restore-runs/{record}` | Workspace context plus tenant context | Dashboard KPIs / Backup posture | Backup health is separate from restore evidence | existing widget pattern |
|
||||
| Needs Attention / Healthy Checks panel | Dashboard / attention summary | Explicit card CTA per attention item; healthy state is read-only | forbidden | Card CTA and helper copy only | none | `/admin/t/{tenant}` | `/admin/t/{tenant}/restore-runs`, `/admin/t/{tenant}/restore-runs/{record}` | Workspace context plus tenant context | Needs attention / Healthy checks | Unknown and weakened recovery confidence are visible before drilldown | existing widget pattern |
|
||||
| Restore runs page | CRUD / list-first resource | Full-row click to restore-run detail | required | Existing header action plus More menu | Existing More and bulk More groups | `/admin/t/{tenant}/restore-runs` | `/admin/t/{tenant}/restore-runs/{record}` | Tenant context plus restore-run identity | Restore runs / Restore run | Recent restore outcome and follow-up reason confirm the overview claim | none |
|
||||
|
||||
## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)*
|
||||
|
||||
| Surface | Primary Persona | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| Tenant dashboard KPI strip | Tenant operator | Dashboard summary | Do healthy backups also have supporting restore evidence, or is that still unknown? | Backup posture, recovery-confidence qualifier, visible claim boundary, next step | Per-run causes, raw backup metadata, deeper restore evidence | backup health, recovery evidence availability, recent restore attention | None; read-only summary | Open restore history, open supporting backup context when backup health itself needs follow-up | none |
|
||||
| Needs Attention / Healthy Checks panel | Tenant operator | Dashboard attention and healthy-boundary surface | What recovery-confidence issue needs action now, and why? | No restore history, weakened recent restore history, boundary copy, concrete next action | Full restore results, preview or check details, low-level run metadata | backup health, recovery evidence availability, restore result attention, recency | None; read-only summary | Open restore history, open latest problematic restore run | none |
|
||||
| Restore runs page | Tenant operator | List and detail | Which restore runs explain the dashboard signal? | Recent restore status, result-attention reason, completed timing, related backup context | Assignment-level failures, preview detail, low-level result payloads | execution lifecycle, result attention, follow-up state | Existing restore-run maintenance actions only | Inspect restore run, create restore run | Existing rerun, archive, restore archived, and force-delete actions |
|
||||
|
||||
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||
|
||||
- **New source of truth?**: no
|
||||
- **New persisted entity/table/artifact?**: no
|
||||
- **New abstraction?**: no
|
||||
- **New enum/state/reason family?**: no
|
||||
- **New cross-domain UI framework/taxonomy?**: no
|
||||
- **Current operator problem**: A tenant dashboard can currently look calm or healthy even when restore history is absent or recent restore results weaken confidence, so operators can overread backup health as recovery posture.
|
||||
- **Existing structure is insufficient because**: Backup health, restore history, and restore result attention already exist as separate truths, but the summary surfaces do not yet combine them with an honest claim boundary. Operators must manually cross-check multiple pages to avoid an overclaim.
|
||||
- **Narrowest correct implementation**: Derive a small set of overview honesty signals from existing backup health assessment, restore history presence, and per-run restore result attention, then show them on the existing dashboard widgets and existing restore-run drilldowns.
|
||||
- **Ownership cost**: Additional widget copy, narrow derived-summary logic, and focused feature plus RBAC regression tests that keep overview language and drilldown continuity aligned.
|
||||
- **Alternative intentionally rejected**: A new recovery-confidence score, enum, page, or persisted posture state was rejected because it would introduce new truth and new ownership cost before the current overview surfaces tell the existing truth accurately.
|
||||
- **Release truth**: current-release truth hardening
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - See Unvalidated Recovery Confidence Early (Priority: P1)
|
||||
|
||||
A tenant operator opens the tenant dashboard and needs to know within seconds whether healthy-looking backups are backed by any relevant restore evidence or whether recovery confidence is still unvalidated.
|
||||
|
||||
**Why this priority**: This is the highest-risk trust gap. If the first overview screen quietly converts healthy backups into a healthy recovery impression, later detail truth arrives too late.
|
||||
|
||||
**Independent Test**: Can be fully tested by rendering the tenant dashboard with healthy backup fixtures and no relevant restore history, then verifying that the overview shows an explicit unvalidated or unknown recovery-confidence signal instead of an all-clear.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a tenant has healthy backup posture and no relevant restore history, **When** the operator opens the tenant dashboard, **Then** the summary shows healthy backups plus an explicit unvalidated or unknown recovery-confidence message and a next action.
|
||||
2. **Given** the same tenant has no other attention items, **When** the healthy-check state renders, **Then** the widget does not show an unqualified all-good message and instead keeps the recovery-confidence boundary visible.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Escalate Weak Restore History on Overview (Priority: P2)
|
||||
|
||||
A tenant operator reviewing the dashboard needs recent failed, partial, or follow-up restore results to affect the overview immediately instead of hiding inside restore history details.
|
||||
|
||||
**Why this priority**: Weak restore history is evidence that directly changes how much trust the operator should place in recovery posture. It cannot remain a drilldown-only fact.
|
||||
|
||||
**Independent Test**: Can be fully tested by rendering overview surfaces with recent failed, partial, and follow-up restore fixtures and verifying that each case creates a visible confidence-related attention signal with matching drilldown behavior.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a tenant has healthy backups but a recent failed or partial restore run, **When** the operator opens the dashboard, **Then** Needs Attention shows a recovery-confidence issue that links to restore history explaining the same failure state.
|
||||
2. **Given** a tenant has a recent restore run that completed with follow-up required, **When** the operator opens the dashboard, **Then** the overview shows weakened confidence rather than a neutral or healthy-only message.
|
||||
3. **Given** recent restore history exists without a current confidence-weakening attention state, **When** the operator opens the dashboard, **Then** the overview may say that no recent restore issues are visible but does not claim that recovery is proven.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Preserve Honest Drilldowns and RBAC Boundaries (Priority: P3)
|
||||
|
||||
A tenant operator or read-only member needs the dashboard signal and the destination surface to tell the same story, while RBAC limits must never make the summary look stronger than the accessible evidence.
|
||||
|
||||
**Why this priority**: Overview honesty fails if the next click contradicts the dashboard or if authorization gaps hide weakness by omission.
|
||||
|
||||
**Independent Test**: Can be fully tested by opening overview signals as different tenant members, verifying that the linked restore-history surface confirms the same reason, and ensuring restricted users still see cautious summary language.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** the dashboard says recovery confidence is unvalidated because no relevant restore history exists, **When** the operator follows the dashboard action, **Then** the destination surface confirms that the tenant lacks relevant restore history.
|
||||
2. **Given** the dashboard says recovery confidence is weakened by a recent problematic restore, **When** the operator follows the dashboard action, **Then** the destination surface confirms the same failed, partial, or follow-up reason.
|
||||
3. **Given** a tenant member can see the dashboard but cannot open deeper restore evidence, **When** the dashboard renders, **Then** the summary remains cautious and truthful and does not replace missing evidence with a stronger claim.
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- A tenant has only draft, preview-only, or dry-run restore history; the overview treats recovery confidence as unvalidated rather than positive.
|
||||
- A tenant has both an older successful restore and a more recent failed or follow-up restore; the weakened signal takes precedence on the summary surface.
|
||||
- A summary signal points to a restore run that is no longer directly openable; the drilldown falls back to tenant-scoped restore history rather than a dead end.
|
||||
- A user can see the dashboard but lacks permission to inspect restore runs; the summary still states unknown or weakened confidence without suggesting that everything is healthy.
|
||||
- Healthy backup posture and backup-automation follow-up can coexist with unvalidated recovery confidence; the overview must not let one healthy-sounding statement erase the other caution.
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
This feature introduces no new Microsoft Graph calls, no new background work, no new `OperationRun`, and no new persistence. It is a read-first truth-hardening slice that makes existing backup and restore evidence visible more honestly on tenant overview surfaces.
|
||||
|
||||
Authorization remains in the tenant/admin plane under `/admin/t/{tenant}/...`. Non-members must continue to receive 404 responses. Established members missing deeper restore capabilities must continue to receive 403 on execution paths, but summary visibility must not depend on restore-mutation rights.
|
||||
|
||||
This slice reuses existing Filament dashboard widgets, stat descriptions, attention cards, and existing restore-run resource surfaces. No new local badge framework, page-local status language, or extra action surface is introduced. UI-FIL-001 is satisfied by continuing to use existing Filament widget primitives and shared status language. UX-001 create, edit, and detail-form rules are not materially changed; the dashboard keeps its existing layout, and the restore-run resource keeps its existing list-and-view contract.
|
||||
|
||||
The affected Filament surfaces keep exactly one primary inspect or open model, add no redundant View actions, and introduce no new destructive actions. Existing destructive restore-run actions continue to follow the current placement and confirmation rules. Action Surface Contract expectations therefore remain satisfied.
|
||||
|
||||
Existing per-run restore result attention remains the authoritative signal for restore outcome quality. This feature may summarize or elevate that truth, but it must not duplicate it with a second scoring or status system.
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-184-001**: The system MUST present tenant backup health and tenant recovery-confidence evidence as separate truths on tenant dashboard summary surfaces.
|
||||
- **FR-184-002**: When backup health is healthy but no relevant restore history exists, the system MUST display an explicit unknown or unvalidated recovery-confidence state and MUST NOT present an all-clear summary.
|
||||
- **FR-184-003**: When the system cannot determine recovery confidence from the available restore history, the system MUST say that limitation directly instead of inferring a positive recovery claim from backup health alone.
|
||||
- **FR-184-004**: Needs Attention or the healthy-boundary surface MUST surface absence of restore history as an overview-relevant condition with a clear next action.
|
||||
- **FR-184-005**: Recent restore history with `failed`, `partial`, `completed_with_follow_up`, or an equivalent confidence-weakening attention state MUST appear on overview surfaces as a recovery-confidence issue.
|
||||
- **FR-184-006**: Overview surfaces MUST distinguish unknown or unvalidated confidence from weakened confidence and MUST NOT collapse both states into one ambiguous bucket.
|
||||
- **FR-184-007**: Any positive backup-health summary on the dashboard MUST show a visible claim boundary that healthy backups reflect backup inputs only and do not prove restore success.
|
||||
- **FR-184-008**: Healthy checks MUST NOT render an unqualified healthy or all-clear state when recovery confidence is unknown, weakened, or not evaluated.
|
||||
- **FR-184-009**: When recovery confidence is unknown or weakened, overview copy MUST explain what is missing or concerning, why that affects confidence, and what the operator should do next.
|
||||
- **FR-184-010**: Overview signals about missing restore history MUST drill into a tenant-scoped restore-history surface that confirms the absence or insufficiency of relevant restore evidence.
|
||||
- **FR-184-011**: Overview signals about weakened restore history MUST drill into a tenant-scoped restore-history surface or restore-run detail that confirms the same failed, partial, or follow-up reason shown on the summary surface.
|
||||
- **FR-184-012**: The feature MUST reuse existing per-run restore result attention as the authoritative quality signal for restore outcomes and MUST NOT introduce a parallel positive-scoring or reason system.
|
||||
- **FR-184-013**: The feature MUST NOT introduce a new state or message that claims recovery is proven, guaranteed, or strongly confirmed beyond the evidence the current system already has.
|
||||
- **FR-184-014**: RBAC limits on restore history visibility MUST NOT cause summary surfaces to make stronger recovery claims than the visible evidence supports; when detailed restore evidence cannot be opened, the summary must remain cautious and truthful.
|
||||
- **FR-184-015**: Tenant-linked summaries shown outside the tenant dashboard, if they reuse this posture signal, MUST preserve the same meaning for unknown, weakened, and backup-only-positive states.
|
||||
- **FR-184-016**: The feature MUST derive its summary state from existing tenant backup health, restore history, and restore result attention records and MUST NOT add a new persisted recovery-confidence field, table, or scoring artifact.
|
||||
- **FR-184-017**: When recent restore history exists without a current confidence-weakening attention state, overview surfaces MAY state that no recent restore issues are visible, but MUST stop short of claiming recovery proof.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Relevant restore history means tenant-scoped restore runs that have reached an executed result state or another existing result-attention state that the current system can classify. Draft-only, preview-only, or dry-run-only history does not count as proven recovery evidence.
|
||||
- Existing restore history surfaces already show enough result detail to confirm failed, partial, and follow-up reasons once the operator drills down from the overview.
|
||||
- Workspace-level surfaces that later reuse this posture language should consume the same tenant-level semantics rather than creating a separate recovery-confidence vocabulary.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Existing tenant dashboard surfaces remain the operator entry point for this slice.
|
||||
- Existing `TenantBackupHealthAssessment` and `TenantBackupHealthResolver` remain the source of backup-input truth.
|
||||
- Existing `RestoreRun` history surfaces and `RestoreSafetyResolver::resultAttentionForRun(...)` remain the source of restore-outcome truth.
|
||||
- Existing RBAC helper-text and disabled-link patterns remain the fallback behavior when the operator cannot open deeper restore evidence.
|
||||
|
||||
## Out of Scope and Follow-up
|
||||
|
||||
- No new recovery-confidence engine, score, enum, or dedicated posture page.
|
||||
- No automatic restore validation, scheduled restore probes, or restore execution changes.
|
||||
- No new backup-health rules, restore-result-attention taxonomy changes, or restore-safety model redesign.
|
||||
- No new claim that a tenant is recovery-proven.
|
||||
- Reasonable follow-up work includes broader workspace-level recovery rollups after tenant-level overview honesty is stable.
|
||||
|
||||
## UI Action Matrix *(mandatory when Filament is changed)*
|
||||
|
||||
| Surface | Location | Header Actions | Inspect Affordance (List/Table) | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Tenant dashboard summary widgets | `app/Filament/Pages/TenantDashboard.php`, `app/Filament/Widgets/Dashboard/DashboardKpis.php`, `app/Filament/Widgets/Dashboard/NeedsAttention.php` | none added | Explicit stat and card CTA only; no row click | none | n/a | none | n/a | n/a | no new audit event | Action Surface Contract stays satisfied because the dashboard remains read-only. UI-FIL-001 stays satisfied through existing Filament widget primitives. UX-001 create and edit form rules are not applicable to this dashboard slice. |
|
||||
| RestoreRunResource list and detail | `app/Filament/Resources/RestoreRunResource.php`, `app/Filament/Resources/RestoreRunResource/Pages/ListRestoreRuns.php`, `app/Filament/Resources/RestoreRunResource/Pages/ViewRestoreRun.php` | Existing `New restore run` action remains | `recordUrl()` clickable row to restore-run detail | Existing More-menu maintenance actions remain unchanged | Existing grouped bulk actions remain unchanged | Existing `New restore run` empty-state CTA remains | none added | Existing restore-run create flow remains unchanged | existing restore-run mutation audit behavior only | This spec reuses restore-run list and detail as canonical drilldowns and adds no new destructive action or placement exception. |
|
||||
|
||||
## Key Entities *(include if feature involves data)*
|
||||
|
||||
- **Backup health assessment**: Tenant-level summary of backup freshness and input health that is useful but not sufficient to prove recovery success.
|
||||
- **Restore history**: Tenant-scoped record of restore runs whose presence, absence, and recent outcomes affect how strongly the product can speak about recovery confidence.
|
||||
- **Restore result attention**: Per-run classification that distinguishes completed, failed, partial, follow-up, and not-executed outcome states that matter for operator trust.
|
||||
- **Recovery posture summary**: Non-persisted dashboard statement that combines backup health, restore history presence, and restore-result attention without becoming a new score or stored state.
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: In acceptance testing, operators can identify within 10 seconds whether a tenant has healthy backups plus unvalidated or weakened recovery evidence from `/admin/t/{tenant}` without opening raw details.
|
||||
- **SC-002**: In 100% of tested tenants with no relevant restore history, the dashboard or healthy-boundary surface shows an explicit unvalidated or unknown recovery-confidence signal and never shows a healthy-only all-clear.
|
||||
- **SC-003**: In 100% of tested tenants with recent failed, partial, or follow-up restore runs, the overview shows a confidence-related attention item with a drilldown that confirms the same reason.
|
||||
- **SC-004**: In 100% of tested positive backup-health scenarios, summary-level copy includes the claim boundary that healthy backups do not prove restore success.
|
||||
- **SC-005**: In 100% of tested RBAC-restricted scenarios, summary surfaces remain cautious and truthful even when the user cannot open deeper restore evidence pages.
|
||||
126
specs/001-finding-risk-acceptance/plan.md
Normal file
126
specs/001-finding-risk-acceptance/plan.md
Normal file
@ -0,0 +1,126 @@
|
||||
# Implementation Plan: Finding Risk Acceptance Lifecycle
|
||||
|
||||
**Branch**: `001-finding-risk-acceptance` | **Date**: 2026-03-19 | **Spec**: [/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/001-finding-risk-acceptance/spec.md](/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/001-finding-risk-acceptance/spec.md)
|
||||
**Input**: Feature specification from `/specs/001-finding-risk-acceptance/spec.md`
|
||||
|
||||
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/scripts/` for helper scripts.
|
||||
|
||||
## Summary
|
||||
|
||||
Introduce a first-class tenant-owned Finding Exception domain that governs formal risk acceptance for findings instead of relying on a bare `risk_accepted` status and freeform reason field. The implementation adds dedicated exception and exception-decision records, tenant-scoped request and detail surfaces, a canonical workspace approval queue, centralized validity semantics, audit coverage for every lifecycle mutation, and explicit downstream contracts so evidence and reporting flows can distinguish valid governed exceptions from expired, revoked, rejected, or missing ones.
|
||||
|
||||
The implementation keeps Findings as the system of record for the underlying issue, uses the existing `FindingWorkflowService` as the only path that can transition a finding into or out of `risk_accepted`, stores governance history in append-only decision records, and uses DB-backed tenant/workspace queries rather than a new `OperationRun` workflow for normal approval actions.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: PHP 8.4.15
|
||||
**Primary Dependencies**: Laravel 12, Filament v5, Livewire v4, Pest v4, existing Finding, AuditLog, EvidenceSnapshot, CapabilityResolver, WorkspaceCapabilityResolver, and UiEnforcement patterns
|
||||
**Storage**: PostgreSQL with new tenant-owned exception tables and JSONB-backed supporting metadata
|
||||
**Testing**: Pest feature tests, Pest unit tests, and Livewire/Filament component tests
|
||||
**Target Platform**: Laravel Sail web application on PostgreSQL
|
||||
**Project Type**: Web application monolith
|
||||
**Performance Goals**: Exception request, approval, rejection, renewal, and revocation remain synchronous DB-backed actions under 2 seconds; tenant and canonical exception lists remain DB-only at render time; expiring queue filters remain index-backed
|
||||
**Constraints**: No Microsoft Graph calls; no new public API; one current valid active exception per finding at a time; approval history must remain append-only; normal workflow stays outside `OperationRun`; status-like UI uses centralized badge semantics
|
||||
**Scale/Scope**: First rollout covers finding-specific exceptions only, tenant detail plus workspace approval queue, linked evidence references, validity-state evaluation, and downstream reuse by evidence/reporting consumers
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
- **Pre-Phase-0 Gate: PASS**
|
||||
- Inventory-first: PASS. The feature governs findings and linked evidence already present in the product; it does not recollect or redefine source inventory.
|
||||
- Read/write separation: PASS. Exception request, approval, rejection, renewal, and revocation are explicit governance writes with confirmation, audit coverage, and focused tests.
|
||||
- Graph contract path: PASS. No Graph calls are introduced.
|
||||
- Deterministic capabilities: PASS. New capabilities are added to the canonical registry and role maps and tested through existing capability resolver patterns.
|
||||
- RBAC-UX / workspace / tenant isolation: PASS. Tenant exception records stay tenant-owned; the canonical workspace queue is query-only and entitlement-filtered; non-members remain 404 and in-scope capability denials remain 403.
|
||||
- Global search: PASS. The first rollout does not require global-search exposure for exception records.
|
||||
- Run observability: PASS with explicit exemption. Normal exception decisions are DB-only and expected to complete under 2 seconds, so they intentionally skip `OperationRun` and rely on audit history and surface state changes. No remote or long-running work is introduced.
|
||||
- Ops-UX 3-surface feedback: PASS by non-applicability. No new `OperationRun`-driven operator workflow is introduced in v1.
|
||||
- Ops-UX lifecycle / summary counts / system runs: PASS by non-applicability for the core decision paths.
|
||||
- Data minimization: PASS. Exception records store bounded justification, structured evidence references, and sanitized audit context; no raw payloads or secrets are persisted.
|
||||
- BADGE-001: PASS. New exception-state and validity-state badges are introduced via centralized badge domain entries and covered by tests.
|
||||
- UI-NAMING-001: PASS. Operator-facing vocabulary remains `Request exception`, `Approve exception`, `Reject exception`, `Renew exception`, and `Revoke exception` with `risk acceptance` used for the governed outcome.
|
||||
- Filament UI Action Surface Contract: PASS. Tenant finding detail, tenant exception register, canonical approval queue, and exception detail all use explicit inspection affordances, grouped actions, and confirmed destructive-like mutations.
|
||||
- Filament UI UX-001: PASS. Detail surfaces are inspection-first Infolists; list surfaces expose search, sort, and filters; exception request and renewal use structured sections in modals or dedicated forms.
|
||||
|
||||
**Post-Phase-1 Re-check: PASS**
|
||||
- The design keeps Findings as the underlying domain record, adds a tenant-owned governance layer without cross-tenant duplication, routes all status mutations through the existing workflow service, avoids unnecessary `OperationRun` usage, and preserves audit-first history for every decision path.
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/001-finding-risk-acceptance/
|
||||
├── plan.md # This file (/speckit.plan command output)
|
||||
├── research.md # Phase 0 output (/speckit.plan command)
|
||||
├── data-model.md # Phase 1 output (/speckit.plan command)
|
||||
├── quickstart.md # Phase 1 output (/speckit.plan command)
|
||||
├── contracts/ # Phase 1 output (/speckit.plan command)
|
||||
└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
```text
|
||||
app/
|
||||
├── Filament/
|
||||
│ ├── Pages/
|
||||
│ │ └── Monitoring/
|
||||
│ └── Resources/
|
||||
├── Models/
|
||||
├── Policies/
|
||||
├── Services/
|
||||
│ ├── Audit/
|
||||
│ ├── Auth/
|
||||
│ ├── Evidence/
|
||||
│ └── Findings/
|
||||
└── Support/
|
||||
├── Audit/
|
||||
├── Auth/
|
||||
├── Badges/
|
||||
└── Rbac/
|
||||
|
||||
database/
|
||||
└── migrations/
|
||||
|
||||
tests/
|
||||
├── Feature/
|
||||
│ ├── Findings/
|
||||
│ ├── Monitoring/
|
||||
│ └── Guards/
|
||||
└── Unit/
|
||||
├── Findings/
|
||||
└── Support/
|
||||
```
|
||||
|
||||
**Structure Decision**: Keep the existing Laravel monolith structure. Add new exception models and decision-history tables under `app/Models`, lifecycle orchestration under `app/Services/Findings`, authorization under `app/Policies`, and tenant/canonical Filament surfaces under `app/Filament`. Persist schema in `database/migrations` and cover behavior with focused Pest feature/unit tests in existing Findings, Monitoring, and guard suites.
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
> **Fill ONLY if Constitution Check has violations that must be justified**
|
||||
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
|-----------|------------|-------------------------------------|
|
||||
|
||||
## Phase 0 — Research Output
|
||||
|
||||
- [research.md](/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/001-finding-risk-acceptance/research.md)
|
||||
|
||||
## Phase 1 — Design Output
|
||||
|
||||
- [data-model.md](/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/001-finding-risk-acceptance/data-model.md)
|
||||
- [quickstart.md](/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/001-finding-risk-acceptance/quickstart.md)
|
||||
- [contracts/finding-risk-acceptance.openapi.yaml](/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/001-finding-risk-acceptance/contracts/finding-risk-acceptance.openapi.yaml)
|
||||
|
||||
## Phase 2 — Implementation Planning
|
||||
|
||||
`tasks.md` should cover:
|
||||
|
||||
1. Schema creation for `finding_exceptions` and `finding_exception_decisions` with tenant/workspace ownership constraints, validity indexes, and evidence-reference metadata.
|
||||
2. Capability registry and role-map updates for `finding_exception.view`, `finding_exception.manage`, and `finding_exception.approve` plus authorization policies for tenant and canonical views.
|
||||
3. Service-layer orchestration that routes all accepted-risk status mutations through a new exception lifecycle service plus the existing `FindingWorkflowService`.
|
||||
4. Filament tenant finding-detail, tenant exception register, canonical approval queue, and exception detail surfaces aligned with Action Surface and UX-001 rules.
|
||||
5. Audit-log integration, badge-domain additions, and canonical related-navigation support.
|
||||
6. Downstream validity-resolution hooks for evidence and reporting consumers that must distinguish valid governed exceptions from expired, revoked, rejected, or missing ones.
|
||||
7. Focused Pest coverage for positive and negative authorization, invalid transitions, renewal/revocation history, wrong-tenant behavior, and canonical queue filtering.
|
||||
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
|
||||
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
|
||||
203
specs/001-finding-risk-acceptance/spec.md
Normal file
203
specs/001-finding-risk-acceptance/spec.md
Normal file
@ -0,0 +1,203 @@
|
||||
# Feature Specification: Finding Risk Acceptance Lifecycle
|
||||
|
||||
**Feature Branch**: `001-finding-risk-acceptance`
|
||||
**Created**: 2026-03-19
|
||||
**Status**: Draft
|
||||
**Input**: User description: "Create a formal exception and risk acceptance workflow for findings with approval, expiry, renewal, audit trail, and evidence linkage."
|
||||
|
||||
## Spec Scope Fields *(mandatory)*
|
||||
|
||||
- **Scope**: tenant + canonical-view
|
||||
- **Primary Routes**:
|
||||
- `/admin/t/{tenant}/findings/{finding}` as the tenant-context finding inspection surface where operators can review and initiate risk-acceptance requests
|
||||
- `/admin/t/{tenant}/exceptions` as the tenant-scoped exception register for active, pending, expiring, expired, rejected, and revoked finding exceptions
|
||||
- `/admin/exceptions` as the canonical workspace review and governance queue for authorized approvers and auditors
|
||||
- Existing evidence and audit destinations remain drill-down targets from exception detail when the operator is entitled to inspect them
|
||||
- **Data Ownership**:
|
||||
- Tenant-owned: finding exception records, approval decisions, renewal decisions, expiry state, revocation state, and linked evidence references for one tenant's findings
|
||||
- Workspace-owned but tenant-filtered: canonical review queue state, approval workload filters, and workspace-level summaries for expiring or overdue exceptions without changing tenant ownership of the exception itself
|
||||
- Existing findings, evidence snapshots, review packs, and audit events remain separate systems of record and are referenced rather than duplicated
|
||||
- **RBAC**:
|
||||
- Workspace membership remains required for every exception workflow surface
|
||||
- Tenant entitlement remains required to inspect or mutate tenant-scoped exception records
|
||||
- `finding_exception.view` permits reviewing exception details within authorized scope
|
||||
- `finding_exception.manage` permits creating requests, renewing requests, attaching justification and evidence references, and revoking exceptions where policy allows
|
||||
- `finding_exception.approve` permits approving or rejecting requests and renewals within authorized scope
|
||||
- Non-members or users outside the relevant workspace or tenant scope remain deny-as-not-found, while in-scope members lacking the required capability remain forbidden
|
||||
|
||||
For canonical-view specs, the spec MUST define:
|
||||
|
||||
- **Default filter behavior when tenant-context is active**: When an operator navigates from a tenant finding into the shared exceptions queue, the canonical workspace view opens with that tenant prefiltered. The operator may clear or change the filter only within their authorized tenant set.
|
||||
- **Explicit entitlement checks preventing cross-tenant leakage**: Exception queries, counts, approver queues, filter options, related finding labels, and linked evidence references must be assembled only after workspace and tenant entitlement checks. Unauthorized users must not learn whether another tenant has pending, active, expiring, or expired exceptions.
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Propose and approve a time-bounded risk acceptance (Priority: P1)
|
||||
|
||||
As a tenant manager, I want to request a formal risk acceptance for a finding and route it for approval, so that a risk decision becomes explicit, reviewable, and time-bounded instead of being hidden behind a status flag.
|
||||
|
||||
**Why this priority**: This is the core governance gap. Without a first-class request and approval flow, the product still cannot answer who accepted a risk, why, and until when.
|
||||
|
||||
**Independent Test**: Can be fully tested by creating a finding, submitting a risk-acceptance request with justification and review date, approving it as an authorized approver, and verifying that the finding becomes governed by a valid active exception.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a finding is open and no active exception exists, **When** an authorized operator submits a risk-acceptance request with justification, owner, and review deadline, **Then** the system creates a pending exception request linked to that finding.
|
||||
2. **Given** a pending exception request exists, **When** an authorized approver approves it, **Then** the exception becomes active with a recorded approver, decision time, and expiry date.
|
||||
3. **Given** a pending exception request exists, **When** an authorized approver rejects it, **Then** the request records the rejection outcome and reason without changing the finding into an accepted-risk state.
|
||||
4. **Given** a user lacks the relevant capability or tenant entitlement, **When** they attempt to create or approve an exception request, **Then** the server denies the action with the correct 404 or 403 behavior.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - See whether accepted risk is still valid (Priority: P1)
|
||||
|
||||
As an auditor or workspace approver, I want a clear register of pending, active, expiring, expired, rejected, and revoked exceptions, so that I can tell which accepted risks are still valid and which require action.
|
||||
|
||||
**Why this priority**: A risk-acceptance workflow is only governable if operators can review its current state without reconstructing history from comments and status changes.
|
||||
|
||||
**Independent Test**: Can be fully tested by creating exception records in several lifecycle states and verifying that tenant and canonical views expose the correct state, dates, owners, and next-action cues without leaking unauthorized tenant data.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a tenant has pending, active, and expired exceptions, **When** an authorized operator opens the tenant exception register, **Then** each exception clearly shows its lifecycle state, finding, owner, approver context, and review timing.
|
||||
2. **Given** an approver is responsible for multiple tenants, **When** they open the canonical exceptions queue, **Then** they can filter by tenant, state, and due timing without seeing unauthorized tenants.
|
||||
3. **Given** an active exception is nearing expiry, **When** an authorized operator inspects the register, **Then** the exception is visibly distinguished from long-valid exceptions.
|
||||
4. **Given** no exception matches the current filters, **When** the operator opens the register, **Then** the empty state explains that no governed exceptions match and offers exactly one clear next action.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Renew or revoke an accepted risk with audit evidence (Priority: P2)
|
||||
|
||||
As a governance operator, I want to renew or revoke an existing accepted risk with a durable decision trail and linked evidence, so that exceptions stay current rather than becoming permanent silent waivers.
|
||||
|
||||
**Why this priority**: Time-bounded approval loses value if the product cannot handle renewal and revocation as first-class governance decisions.
|
||||
|
||||
**Independent Test**: Can be fully tested by renewing an active exception with new justification and evidence references, revoking another one, and verifying that lifecycle history, current validity, and audit trail remain intelligible.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** an active exception is approaching expiry, **When** an authorized operator submits a renewal request with updated justification and supporting evidence references, **Then** the system records a new renewal decision path without rewriting the earlier decision.
|
||||
2. **Given** a renewal request exists, **When** an authorized approver approves it, **Then** the active-validity window extends and the prior decision history remains visible.
|
||||
3. **Given** an active exception is no longer acceptable, **When** an authorized operator revokes it with a reason, **Then** the exception becomes revoked and no longer counts as valid risk acceptance.
|
||||
4. **Given** a linked evidence snapshot or supporting artifact later disappears from active views, **When** an operator reviews the exception history, **Then** the exception remains understandable from stored reference metadata.
|
||||
|
||||
---
|
||||
|
||||
### User Story 4 - Detect governance drift in accepted-risk findings (Priority: P2)
|
||||
|
||||
As a compliance-focused operator, I want the system to surface findings marked as accepted risk without a currently valid exception, so that governance drift is visible instead of silently undermining auditability.
|
||||
|
||||
**Why this priority**: The business risk is not just missing workflow. It is false confidence when a finding looks accepted even though its approval expired, was revoked, or never existed.
|
||||
|
||||
**Independent Test**: Can be fully tested by creating findings in accepted-risk status with valid, expired, revoked, and missing exception records and verifying that only truly valid exceptions count as accepted governance state.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a finding is marked as accepted risk and has a valid active exception, **When** the operator inspects it, **Then** the finding shows that the acceptance is governed and time-bounded.
|
||||
2. **Given** a finding is marked as accepted risk but the linked exception is expired, revoked, or absent, **When** the operator inspects it or opens the exception queue, **Then** the system surfaces it as a governance warning rather than a valid accepted risk.
|
||||
3. **Given** a downstream review or evidence workflow summarizes accepted risks, **When** it evaluates findings, **Then** only findings backed by a currently valid exception count as active risk acceptance.
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- A finding is resolved or closed while an exception request is still pending; the request must not silently convert into an active accepted risk without an explicit decision.
|
||||
- A finding remains in `risk_accepted` status after the governing exception expires or is revoked; the system must show that the risk state is no longer valid.
|
||||
- An operator attempts to renew an exception that is already expired; the renewal path must remain explicit and must not overwrite the expired decision history.
|
||||
- The same person requests and approves an exception; the system must either block self-approval in normal flow or record an explicit elevated-policy override when self-approval is allowed.
|
||||
- A finding reopens through detection recurrence while a previous exception exists; the system must make it clear whether the earlier exception still governs the re-opened risk or whether a fresh decision is required.
|
||||
- Evidence linked to an exception may be partial, stale, or later removed from active surfaces; the exception history must preserve enough reference context for review.
|
||||
- A workspace approver can review multiple tenants, but must not see queue counts, labels, or filter values for unauthorized tenants.
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
**Constitution alignment (required):** This feature introduces a new governance data model and new user-driven write behavior, but no new Microsoft Graph calls. Exception request, approval, renewal, rejection, expiry, and revocation are security-relevant DB-backed governance mutations and must be explicitly audited. The feature must define tenant isolation, approval safety, validity semantics, linked-evidence semantics, and tests for valid, expired, revoked, missing, and unauthorized paths. If scheduled reminder or expiry evaluation is introduced, it must describe how that work is observable and how it avoids cross-tenant leakage.
|
||||
|
||||
**Constitution alignment (OPS-UX):** The primary workflow is synchronous governance mutation and does not require a dedicated long-running `OperationRun` for request, approval, rejection, renewal, or revocation. These decisions must therefore be observable through audit history, surface state changes, and user notifications instead of an operation progress surface. If the product later adds scheduled reminder or expiry evaluation, that work may integrate with existing monitoring or alerting patterns, but the first release of this feature does not rely on a new operator-facing progress workflow.
|
||||
|
||||
**Constitution alignment (RBAC-UX):** This feature operates in the tenant/admin plane for tenant-scoped finding and exception surfaces and in the workspace-admin canonical view for the approval queue. Cross-plane access remains deny-as-not-found. Non-members or users outside workspace or tenant scope receive `404`. In-scope users lacking `finding_exception.view`, `finding_exception.manage`, or `finding_exception.approve` receive `403` according to the attempted action. Authorization must be enforced server-side for request creation, approval, rejection, renewal, revocation, and any canonical queue action. The canonical capability registry remains the only capability source. Destructive-like actions such as revoke and reject require confirmation.
|
||||
|
||||
**Constitution alignment (OPS-EX-AUTH-001):** Not applicable. No authentication handshake behavior is changed.
|
||||
|
||||
**Constitution alignment (BADGE-001):** Exception lifecycle state, risk-governance validity, and due-timing indicators are status-like values and must use centralized badge semantics rather than per-page color choices. Tests must cover all introduced states such as pending, active, expiring, expired, rejected, and revoked.
|
||||
|
||||
**Constitution alignment (UI-NAMING-001):** The target object is the finding exception. Operator-facing verbs are `Request exception`, `Approve exception`, `Reject exception`, `Renew exception`, and `Revoke exception`. The term `risk acceptance` describes the governance outcome, while `exception` names the governed record. The same vocabulary must be preserved across finding detail, exception register, approval queue, audit prose, and notifications. Implementation-first terms such as `waiver row`, `approval token`, or `state machine` must not become primary labels.
|
||||
|
||||
**Constitution alignment (Filament Action Surfaces):** This feature modifies tenant finding detail and introduces exception list and detail inspection surfaces plus approval actions. The Action Surface Contract is satisfied if request and review actions are explicit, destructive-like actions require confirmation, list inspection uses a canonical inspect affordance, and every mutation is authorization-gated and audited.
|
||||
|
||||
**Constitution alignment (UX-001 — Layout & Information Architecture):** Exception list screens must provide search, sort, and filters for state, tenant, owner, approver, and expiry timing. Exception detail must be an inspection surface using Infolist-style composition rather than a disabled edit form. Creation and renewal may use a structured modal or dedicated form surface, but must keep justification, owner, timing, and evidence references grouped inside sections. Empty states must include a specific title, explanation, and exactly one CTA.
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: The system MUST provide a first-class finding exception record that governs formal risk acceptance for a specific finding.
|
||||
- **FR-002**: A finding exception MUST capture at minimum the target finding, requester, accountable owner, requested justification, requested decision time, and the bounded validity window for accepted risk.
|
||||
- **FR-003**: The system MUST support an exception lifecycle that distinguishes at least pending, active, expiring, expired, rejected, revoked, and superseded or renewed states.
|
||||
- **FR-004**: An operator MUST be able to request risk acceptance for a finding without directly bypassing the approval lifecycle.
|
||||
- **FR-005**: The system MUST support explicit approval and explicit rejection of pending exception requests, with durable decision reason and actor history.
|
||||
- **FR-006**: The system MUST support renewal of an existing exception as a new governance decision that preserves earlier request and approval history.
|
||||
- **FR-007**: The system MUST support explicit revocation of an active exception, with recorded actor, time, and revocation reason.
|
||||
- **FR-008**: The system MUST treat a finding as having valid accepted risk only while a currently valid active exception exists for that finding.
|
||||
- **FR-009**: A finding in `risk_accepted` status without a currently valid exception MUST be surfaced as a governance warning rather than a fully governed accepted risk.
|
||||
- **FR-010**: The feature MUST define whether one finding may have multiple historical exception records over time, while ensuring that only one current exception can govern the finding for a given validity window.
|
||||
- **FR-011**: Exception requests and renewals MUST support structured supporting context, including freeform justification and one or more linked evidence references when available.
|
||||
- **FR-012**: Evidence references linked to an exception MUST remain intelligible even if the live evidence artifact later expires, is superseded, or becomes inaccessible from normal active views.
|
||||
- **FR-013**: The system MUST provide a tenant-scoped exception register that allows authorized operators to review current and historical exception records for that tenant.
|
||||
- **FR-014**: The system MUST provide a canonical workspace approval and governance queue that allows authorized viewers to review pending, expiring, expired, rejected, and revoked exceptions across entitled tenants.
|
||||
- **FR-015**: Tenant and canonical views MUST provide filters for lifecycle state, due timing, requester, owner, approver, and finding severity or type where relevant.
|
||||
- **FR-016**: The system MUST make upcoming expiry and already-expired exceptions clearly visible so that time-bounded risk acceptance does not silently lapse.
|
||||
- **FR-017**: The system MUST define reminder semantics for exceptions nearing expiry, including who needs visibility when action is required.
|
||||
- **FR-018**: All exception lifecycle mutations must be recorded in audit history with workspace scope, tenant scope, actor, target finding context, action, outcome, and readable supporting context.
|
||||
- **FR-019**: Exception audit records MUST be summary-first and MUST NOT store secrets, raw evidence payloads, or arbitrary oversized snapshots.
|
||||
- **FR-020**: The system MUST enforce 404 deny-as-not-found behavior for non-members and out-of-scope users, and 403 behavior for in-scope users lacking the required capability.
|
||||
- **FR-021**: The feature MUST define approval separation rules, including whether normal self-approval is blocked and how any exceptional override path is governed and auditable.
|
||||
- **FR-022**: The feature MUST preserve intelligible history when a finding later resolves, closes, reopens, or changes severity after an exception decision.
|
||||
- **FR-023**: Downstream review, evidence, and reporting workflows that summarize accepted risk MUST distinguish valid governed exceptions from expired, revoked, rejected, or missing ones.
|
||||
- **FR-024**: The feature MUST introduce at least one positive and one negative authorization test for tenant-context request flows and canonical approval-queue flows.
|
||||
- **FR-025**: The feature MUST introduce regression tests for pending, approved, rejected, renewed, revoked, expired, and missing-exception states, plus wrong-tenant and invalid-transition paths.
|
||||
|
||||
## UI Action Matrix *(mandatory when Filament is changed)*
|
||||
|
||||
| Surface | Location | Header Actions | Inspect Affordance (List/Table) | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Finding Detail Risk Panel | Tenant-context finding inspection under `/admin/t/{tenant}/findings/{finding}` | `Request exception` (`finding_exception.manage`) when no valid exception exists | Linked exception summary card or explicit `View exception` affordance | `View exception`, `Request exception` or `Renew exception` depending on state | None | `Request first exception` when no governance record exists | `Renew exception`, `Revoke exception` when authorized | N/A | Yes | Action labels must describe the governance object, not just the finding status |
|
||||
| Tenant Exception Register | Tenant-context list under `/admin/t/{tenant}/exceptions` | Contextual filters only | Clickable row to exception detail | `View exception`, `Renew exception` or `Revoke exception` depending on state | None in v1 | `Request first exception` | None | N/A | Yes | Inspection-first surface; no bulk approval in first slice |
|
||||
| Canonical Exceptions Queue | Workspace canonical view at `/admin/exceptions` | Contextual filters only | Clickable row to exception detail | `Approve exception`, `Reject exception` for pending items | None in v1 | `Clear filters` | None | N/A | Yes | Queue must remain tenant-safe and only show entitled tenants |
|
||||
| Exception Detail | Tenant or canonical detail inspection surface | None | N/A | None | None | N/A | `Approve exception`, `Reject exception`, `Renew exception`, `Revoke exception` depending on state and capability | N/A | Yes | Detail is an inspection surface, not a disabled edit form |
|
||||
|
||||
### Key Entities *(include if feature involves data)*
|
||||
|
||||
- **Finding Exception**: A governed risk-acceptance record for one finding, including request context, decision state, validity timing, and current governance outcome.
|
||||
- **Exception Decision**: A durable approval, rejection, renewal, or revocation record that explains who made the decision, when, and why.
|
||||
- **Exception Evidence Reference**: A structured pointer to supporting evidence used to justify or review an exception, preserved as intelligible reference metadata.
|
||||
- **Risk Governance Validity**: The normalized truth of whether a finding's accepted-risk posture is currently valid, expiring soon, expired, revoked, rejected, or unsupported.
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: An authorized operator can request and route a formal finding exception in under 3 minutes without leaving the product.
|
||||
- **SC-002**: In automated tests, 100% of findings counted as valid accepted risk are backed by a currently valid active exception.
|
||||
- **SC-003**: In acceptance review, an authorized auditor can answer who requested, who approved, why it was accepted, and until when it remains valid within 2 minutes using the product alone.
|
||||
- **SC-004**: Expired, revoked, rejected, and missing-governance accepted-risk states are all distinguishable in automated regression coverage with no false classification as valid active acceptance.
|
||||
- **SC-005**: Negative authorization tests prove that non-members or wrong-tenant users receive deny-as-not-found behavior and in-scope users without the required capability cannot request, approve, renew, or revoke exceptions.
|
||||
- **SC-006**: Renewal and revocation flows preserve prior decision history in automated tests rather than overwriting the previous governance record.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Spec 111 remains the product source of truth for finding lifecycle and status semantics, including the existing `risk_accepted` status.
|
||||
- Spec 134 remains the source of truth for canonical audit readability and event history behavior.
|
||||
- Evidence linkage may reference evidence snapshots, review artifacts, or other governance evidence when available, but the exception lifecycle must not be blocked merely because evidence is partial.
|
||||
- Normal approval flow should not rely on silent self-approval; any permitted override path must be explicit and auditable.
|
||||
- The first rollout focuses on finding-specific exceptions, not a generic cross-domain waiver engine.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Replacing the existing findings workflow with a different status model
|
||||
- Creating a generic exception platform for every future domain in the first slice
|
||||
- Suppressing or deleting findings automatically when risk is accepted
|
||||
- Making legal or certification claims about compliance acceptance
|
||||
- Replacing evidence snapshots, review packs, or the broader audit foundation with exception-owned storage
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Findings workflow semantics and lifecycle rules from `specs/111-findings-workflow-sla/spec.md`
|
||||
- Audit history foundation and event readability rules from `specs/134-audit-log-foundation/spec.md`
|
||||
- Evidence-domain linkage patterns from `specs/153-evidence-domain-foundation/spec.md` when evidence snapshots are available
|
||||
170
specs/001-tenant-review-layer/spec.md
Normal file
170
specs/001-tenant-review-layer/spec.md
Normal file
@ -0,0 +1,170 @@
|
||||
# Feature Specification: Tenant Review Layer
|
||||
|
||||
**Feature Branch**: `001-tenant-review-layer`
|
||||
**Created**: 2026-03-20
|
||||
**Status**: Draft
|
||||
**Input**: User description: "Executive Review Packs / Tenant Review Layer"
|
||||
|
||||
## Spec Scope Fields *(mandatory)*
|
||||
|
||||
- **Scope**: workspace + tenant + canonical-view
|
||||
- **Primary Routes**:
|
||||
- `/admin/t/{tenant}/reviews` as the tenant-scoped review library and entry point for recurring review preparation
|
||||
- `/admin/t/{tenant}/reviews/{review}` as the canonical tenant review inspection surface
|
||||
- `/admin/reviews` as the workspace-scoped canonical review register for entitled operators who manage recurring reviews across multiple tenants
|
||||
- Existing evidence, findings, baseline, and permissions surfaces remain linked drill-down destinations from the review detail when the operator is entitled to inspect them
|
||||
- **Data Ownership**:
|
||||
- Tenant-owned: tenant review records, review composition metadata, review lifecycle state, executive summary content, and stakeholder-facing review-pack references for one tenant
|
||||
- Tenant-owned inputs: evidence snapshots, accepted-risk summaries, findings summaries, baseline or drift posture, permission posture, and operational health summaries that are consumed but not re-owned by the review layer
|
||||
- Workspace-owned but tenant-filtered: canonical review library filters, review schedule summaries, and cross-tenant list presentation state without changing tenant ownership of the review itself
|
||||
- Compliance or framework readiness interpretations remain outside this feature and are not stored as first-class review truth in this slice
|
||||
- **RBAC**:
|
||||
- Workspace membership remains required for every review surface
|
||||
- Tenant entitlement remains required to inspect or mutate tenant-scoped review records
|
||||
- `tenant_review.view` permits listing and inspecting reviews within authorized scope
|
||||
- `tenant_review.manage` permits creating, refreshing, publishing, archiving, and exporting review packs within authorized scope
|
||||
- Non-members or users outside the relevant workspace or tenant scope remain deny-as-not-found, while in-scope members lacking the required capability remain forbidden
|
||||
|
||||
For canonical-view specs, the spec MUST define:
|
||||
|
||||
- **Default filter behavior when tenant-context is active**: When an operator navigates from a tenant into the shared review register, the canonical workspace view opens prefiltered to that tenant. The operator may clear or change the filter only within their authorized tenant set.
|
||||
- **Explicit entitlement checks preventing cross-tenant leakage**: Review queries, counts, tenant labels, filter options, executive summaries, and exported review-pack references must be assembled only after workspace and tenant entitlement checks. Unauthorized users must not learn whether another tenant has review history, stakeholder packs, or upcoming review work.
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Prepare one tenant review from curated evidence (Priority: P1)
|
||||
|
||||
As a governance operator, I want to create a tenant review from an evidence snapshot and related governance signals, so that quarterly or ad hoc tenant reviews start from one stable, curated review record instead of manual page-by-page assembly.
|
||||
|
||||
**Why this priority**: This is the core product workflow. Without a first-class tenant review record, executive review packs are still ad hoc exports rather than a repeatable review motion.
|
||||
|
||||
**Independent Test**: Can be fully tested by selecting an eligible tenant evidence snapshot, creating a tenant review, and verifying that the resulting review preserves the chosen evidence basis, key governance sections, and summary state even if live source data changes later.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a tenant has at least one eligible evidence snapshot, findings summary, and posture inputs, **When** an authorized operator creates a tenant review, **Then** the system creates one review record that captures the selected evidence basis and generated review sections for that tenant.
|
||||
2. **Given** a tenant review has been created from a specific evidence snapshot, **When** live findings or posture data later change, **Then** the existing review remains tied to its original evidence basis until the operator explicitly refreshes or creates a new review.
|
||||
3. **Given** the chosen evidence basis is partial, **When** the operator creates the review, **Then** the review clearly records which sections are complete, partial, or unavailable rather than implying a fully complete review.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Present an executive-ready tenant review pack (Priority: P1)
|
||||
|
||||
As an MSP account manager or governance lead, I want a concise executive review surface and exportable review pack for one tenant, so that I can lead customer or management conversations with a stakeholder-ready output rather than raw operational artifacts.
|
||||
|
||||
**Why this priority**: This is the commercial value layer. The product stops being only an operator console when it can produce a readable, stakeholder-facing review output.
|
||||
|
||||
**Independent Test**: Can be fully tested by opening a prepared tenant review, confirming that it presents executive summary sections and drill-down links coherently, and generating a stakeholder-ready review pack from that review without rebuilding the evidence manually.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** an authorized operator opens a prepared tenant review, **When** the review detail loads, **Then** it shows an executive summary, key risks, accepted-risk summary, posture highlights, and recommended next actions in one coherent inspection surface.
|
||||
2. **Given** a tenant review is ready for stakeholder delivery, **When** the operator publishes or exports the executive review pack, **Then** the pack is generated from that review record and reflects the same section ordering and summary truth shown in the product.
|
||||
3. **Given** a stakeholder-facing review pack omits one or more dimensions because the underlying evidence was partial, **When** the operator inspects or exports it, **Then** the omission is explained clearly instead of being silently hidden.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Manage recurring tenant reviews over time (Priority: P2)
|
||||
|
||||
As a workspace operator, I want a canonical review library across the tenants I manage, so that I can see which tenants were reviewed, which reviews are draft or published, and which tenants need the next review cycle.
|
||||
|
||||
**Why this priority**: Once the first tenant review exists, the product needs a repeatable operating model rather than one-off packs. This enables recurring review discipline and prepares the ground for the later portfolio dashboard.
|
||||
|
||||
**Independent Test**: Can be fully tested by creating reviews for multiple tenants, opening the workspace review register, and verifying that the register shows only entitled tenants with correct lifecycle, publish status, and recency signals.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** an operator is entitled to multiple tenants with review history, **When** they open the workspace review register, **Then** they can filter by tenant, review state, publish status, and review date without seeing unauthorized tenant rows.
|
||||
2. **Given** a tenant already has a published review, **When** the operator starts the next review cycle, **Then** the system creates a new draft review instead of mutating the historical published review.
|
||||
3. **Given** no review matches the current filters, **When** the operator opens the canonical review register, **Then** the empty state explains that no review records match and offers exactly one clear next action.
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- A tenant has eligible evidence snapshots but no valid accepted-risk records; the review must still generate and explicitly show that no governed accepted risks are currently active.
|
||||
- A previously published review pack is revisited after the underlying evidence snapshot expires or is superseded; the historical review must remain intelligible from stored review metadata.
|
||||
- A tenant has multiple evidence snapshots available; the operator must choose which one anchors the review rather than the system silently picking a different basis.
|
||||
- An operator tries to publish or export a review that is still missing required summary sections; the product must fail with a clear readiness reason instead of producing a misleading finished pack.
|
||||
- A workspace operator is entitled to some, but not all, tenants in a workspace; the canonical review register must suppress unauthorized tenant labels, counts, and filter values.
|
||||
- A tenant review is created twice from the same evidence basis without meaningful changes; the system must prevent accidental duplicate published reviews while still allowing a deliberate new draft when needed.
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
**Constitution alignment (required):** This feature introduces a new review-domain data model, user-driven write behavior, and optional long-running generation work for stakeholder-facing review packs, but it does not introduce new Microsoft Graph collection. It must describe the review contract with evidence snapshots, explicit publish/export safety gates, tenant isolation, run observability for any generated pack artifact, and tests. Security-relevant DB-only review lifecycle changes such as publish, archive, and unpublish equivalents must always emit audit history.
|
||||
|
||||
**Constitution alignment (OPS-UX):** If review-pack generation is asynchronous, this feature creates or reuses a dedicated `OperationRun` family for tenant review pack generation and must comply with the Ops-UX 3-surface feedback contract. Start actions may show intent-only feedback. Progress belongs only in the active-ops widget and Monitoring run detail. Review detail may link to the canonical run detail but must not create a parallel progress tracker. `OperationRun.status` and `OperationRun.outcome` remain service-owned through `OperationRunService`. Any `summary_counts` must use allowed numeric-only keys and values. Scheduled or system-initiated review generation must not create initiator-only terminal DB notifications.
|
||||
|
||||
**Constitution alignment (RBAC-UX):** This feature operates in the tenant/admin plane for tenant review detail and mutation surfaces and in the workspace-admin canonical view for the shared review register. Cross-plane access remains deny-as-not-found. Non-members or users outside workspace or tenant scope receive `404`. In-scope users lacking `tenant_review.view` or `tenant_review.manage` receive `403` according to the attempted action. Authorization must be enforced server-side for create, refresh, publish, archive, and export actions. The canonical capability registry remains the only capability source. Destructive-like actions such as archive require confirmation.
|
||||
|
||||
**Constitution alignment (OPS-EX-AUTH-001):** Not applicable. No authentication handshake behavior is changed.
|
||||
|
||||
**Constitution alignment (BADGE-001):** Review lifecycle state, publication state, completeness state, and export readiness are status-like values and must use centralized badge semantics rather than local page-specific mappings. Tests must cover all newly introduced values.
|
||||
|
||||
**Constitution alignment (UI-NAMING-001):** The target object is the tenant review. Operator-facing verbs are `Create review`, `Refresh review`, `Publish review`, `Export executive pack`, and `Archive review`. Source/domain disambiguation is needed only where the review references evidence dimensions such as findings, baseline posture, permissions, or operations health. The same review vocabulary must be preserved across action labels, modal titles, run titles, notifications, and audit prose. Implementation-first terms such as `render package`, `materialize review`, or `hydrate sections` must not become primary operator-facing labels.
|
||||
|
||||
**Constitution alignment (Filament Action Surfaces):** This feature adds or modifies Filament pages/resources for tenant review list and detail plus workspace canonical review register. The Action Surface Contract is satisfied if list inspection uses a canonical inspect affordance, pack generation remains an explicit action, destructive lifecycle actions require confirmation, and all mutations are capability-gated and auditable.
|
||||
|
||||
**Constitution alignment (UX-001 — Layout & Information Architecture):** Review list screens must provide search, sort, and filters for tenant, review state, publication state, evidence basis, and review date. Review detail must use an Infolist-style inspection surface rather than a disabled edit form. Any review-creation form or action must keep inputs inside sections. Empty states must include a specific title, explanation, and exactly one CTA.
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: The system MUST provide a first-class tenant review record that represents one curated governance review for one tenant and one chosen evidence basis.
|
||||
- **FR-002**: A tenant review MUST reference exactly one anchored evidence basis at creation time, with enough stored metadata to remain intelligible if downstream source artifacts later change, expire, or are superseded.
|
||||
- **FR-003**: The first implementation slice MUST support review sections for executive summary, open-risk highlights, accepted-risk summary, permission posture summary, baseline or drift posture summary, and operational health summary.
|
||||
- **FR-004**: The system MUST allow an authorized operator to create a tenant review from an eligible evidence snapshot without manually rebuilding each section from live source pages.
|
||||
- **FR-005**: The system MUST preserve review immutability for published reviews. Refreshing a published review MUST create a new draft review or explicit successor review instead of mutating the published historical record.
|
||||
- **FR-006**: The system MUST distinguish at least draft, ready, published, archived, and superseded review lifecycle states.
|
||||
- **FR-007**: The system MUST record review completeness and section availability explicitly, including when a review is based on partial evidence.
|
||||
- **FR-008**: The system MUST make it clear which evidence dimensions were included, omitted, partial, or stale in each review.
|
||||
- **FR-009**: The system MUST provide one tenant-scoped review library where authorized operators can list, inspect, refresh, publish, archive, and export review records for the active tenant.
|
||||
- **FR-010**: The system MUST provide one workspace-scoped canonical review register where authorized operators can review tenant review history across entitled tenants without leaking unauthorized tenant detail.
|
||||
- **FR-011**: The system MUST provide one stakeholder-facing executive review surface for a prepared tenant review that presents summary content and recommended next steps without forcing the operator into raw source artifacts.
|
||||
- **FR-012**: The system MUST support an exportable executive review pack derived from one prepared tenant review record rather than from ad hoc live assembly.
|
||||
- **FR-013**: Exporting an executive review pack MUST use the selected tenant review as the source of truth for section ordering, summary content, and included dimensions.
|
||||
- **FR-014**: The system MUST block publish or export actions when the review lacks required summary sections or required completeness thresholds for this slice, and it MUST explain the blocking reason clearly.
|
||||
- **FR-015**: The system MUST define duplicate-prevention semantics so that accidental repeated publish or export attempts from the same unchanged review do not create duplicate final artifacts unintentionally.
|
||||
- **FR-016**: The system MUST preserve historical published review records and exported pack references so prior reviews remain auditable and comparable over time.
|
||||
- **FR-017**: Creating, refreshing, publishing, archiving, and exporting a review MUST be recorded in audit history with workspace scope, tenant scope, actor, action, and outcome.
|
||||
- **FR-018**: The feature MUST explicitly exclude framework-oriented compliance scoring, certification claims, and BSI, NIS2, or CIS mapping from the first slice. Those remain a downstream Compliance Readiness feature.
|
||||
- **FR-019**: The feature MUST introduce at least one positive and one negative authorization test for tenant-scoped review management and workspace-scoped canonical review visibility.
|
||||
- **FR-020**: The feature MUST introduce regression tests proving evidence-basis anchoring, published-review immutability, executive-pack consistency, and cross-tenant isolation.
|
||||
|
||||
## UI Action Matrix *(mandatory when Filament is changed)*
|
||||
|
||||
| Surface | Location | Header Actions | Inspect Affordance (List/Table) | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Tenant Review Library | Tenant-context review list under `/admin/t/{tenant}/reviews` | `Create review` (`tenant_review.manage`) | Clickable row to review detail | `View review`, `Export executive pack` when ready | None in v1 | `Create first review` | None | N/A | Yes | Create may use an action modal because it selects an evidence basis and starts a review composition workflow |
|
||||
| Tenant Review Detail | Canonical detail route under `/admin/t/{tenant}/reviews/{review}` | None | N/A | None | None | N/A | `Refresh review`, `Publish review`, `Export executive pack`, `Archive review` | N/A | Yes | Inspection surface only; no disabled edit form |
|
||||
| Workspace Review Register | Workspace canonical view at `/admin/reviews` | `Clear filters` | Clickable row to review detail | `View review`, `Export executive pack` when authorized | None in v1 | `Clear filters` | None | N/A | Export yes | Must suppress unauthorized tenant rows and filter values |
|
||||
|
||||
### Key Entities *(include if feature involves data)*
|
||||
|
||||
- **Tenant Review**: A curated review record for one tenant anchored to a chosen evidence basis and used for recurring governance conversations.
|
||||
- **Review Section**: One named portion of the tenant review, such as executive summary, risk highlights, posture summary, or operational health summary, including its completeness and source references.
|
||||
- **Executive Review Pack**: A stakeholder-facing deliverable derived from one tenant review and preserving that review's section ordering, summary truth, and completeness disclosures.
|
||||
- **Review Lifecycle State**: The normalized state of a tenant review, including draft, ready, published, archived, and superseded.
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: An authorized operator can create a tenant review from an eligible evidence basis and open its executive summary in under 3 minutes without leaving the product.
|
||||
- **SC-002**: Published tenant reviews remain unchanged in 100% of automated immutability tests after underlying live source records are modified.
|
||||
- **SC-003**: In manual review flow validation, an operator can answer the tenant's top risks, current posture highlights, and next actions from one review detail surface without opening more than one optional drill-down page.
|
||||
- **SC-004**: Exported executive review packs match their source tenant review's included dimensions and summary ordering in 100% of automated integration tests for the covered first-slice review sections.
|
||||
- **SC-005**: Negative authorization tests prove that non-members or wrong-tenant users receive deny-as-not-found behavior and in-scope users without the required capability cannot create, publish, archive, or export tenant reviews.
|
||||
- **SC-006**: Operators can distinguish draft, ready, published, archived, and superseded review states in one inspection step from list or detail surfaces.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Evidence snapshots are the primary source-of-truth input for review creation in the first slice.
|
||||
- Findings summaries, accepted-risk lifecycle data, permission posture, and baseline or drift posture are mature enough to populate first-slice review sections.
|
||||
- The first slice optimizes for tenant-by-tenant recurring reviews and executive packs, not for framework-oriented compliance mapping.
|
||||
- Workspace-level review visibility is a register and management surface, not yet a portfolio dashboard with SLA analytics.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Building a framework-oriented Compliance Readiness layer with BSI, NIS2, or CIS mapping
|
||||
- Creating tenant portfolio rollups, SLA health dashboards, or fleet ranking views across tenants
|
||||
- Implementing cross-tenant compare or promotion workflows
|
||||
- Turning the tenant review layer into a generic BI reporting system
|
||||
- Triggering new Microsoft Graph collection during review preparation
|
||||
@ -1,36 +0,0 @@
|
||||
# Specification Quality Checklist: Global Context Shell Contract
|
||||
|
||||
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||
**Created**: 2026-04-18
|
||||
**Feature**: [spec.md](../spec.md)
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] No implementation details (languages, frameworks, APIs)
|
||||
- [x] Focused on user value and business needs
|
||||
- [x] Written for non-technical stakeholders
|
||||
- [x] All mandatory sections completed
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||
- [x] Requirements are testable and unambiguous
|
||||
- [x] Success criteria are measurable
|
||||
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||
- [x] All acceptance scenarios are defined
|
||||
- [x] Edge cases are identified
|
||||
- [x] Scope is clearly bounded
|
||||
- [x] Dependencies and assumptions identified
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [x] All functional requirements have clear acceptance criteria
|
||||
- [x] User scenarios cover primary flows
|
||||
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||
- [x] No implementation details leak into specification
|
||||
|
||||
## Notes
|
||||
|
||||
- Specification reviewed against the repo spec template and constitution prompts on 2026-04-18.
|
||||
- No `[NEEDS CLARIFICATION]` markers remain.
|
||||
- Route and shell-surface references are included only to bound the affected product surfaces and do not prescribe implementation structure.
|
||||
@ -1,467 +0,0 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Global Context Shell Logical Contract
|
||||
version: 0.1.0
|
||||
summary: Logical HTTP contract for workspace and tenant shell context resolution and mutation flows
|
||||
description: >-
|
||||
This is a logical contract for Spec 199. The real routes render HTML and redirects,
|
||||
but the schemas below define the canonical request-scoped shell context and the
|
||||
expected redirect or recovery outcomes for shared workspace and tenant shell flows.
|
||||
|
||||
servers:
|
||||
- url: /
|
||||
description: Application root for admin and tenant shell entry surfaces
|
||||
|
||||
tags:
|
||||
- name: shell-context
|
||||
- name: workspace-switch
|
||||
- name: tenant-select
|
||||
- name: tenant-clear
|
||||
|
||||
paths:
|
||||
/admin:
|
||||
get:
|
||||
tags: [shell-context]
|
||||
summary: Resolve workspace-scoped shell entry
|
||||
description: Resolve the active workspace and optional tenant context for a workspace-scoped admin route, including query-backed tenant hints only where the contract explicitly allows them.
|
||||
parameters:
|
||||
- name: tenant
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
description: Optional tenant external-ID hint on routes that explicitly allow query-backed shell resolution.
|
||||
- name: tenant_id
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
oneOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
description: Optional tenant identifier hint on workspace-scoped routes that explicitly allow query-backed context hints.
|
||||
responses:
|
||||
'200':
|
||||
description: Workspace-scoped shell entry resolved successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ResolvedShellContextEnvelope'
|
||||
examples:
|
||||
tenantless:
|
||||
value:
|
||||
resolvedContext:
|
||||
state: tenantless_workspace
|
||||
displayMode: tenantless
|
||||
pageCategory: workspace_scoped
|
||||
workspaceSource: session_workspace
|
||||
tenantSource: none
|
||||
workspace:
|
||||
id: 42
|
||||
slug: alpha-workspace
|
||||
name: Alpha Workspace
|
||||
tenant: null
|
||||
recoveryDirective:
|
||||
action: none
|
||||
reason: null
|
||||
destination: null
|
||||
preserveIntendedUrl: false
|
||||
rememberedTenant:
|
||||
value:
|
||||
resolvedContext:
|
||||
state: tenant_scoped
|
||||
displayMode: tenant_scoped
|
||||
pageCategory: workspace_scoped
|
||||
workspaceSource: session_workspace
|
||||
tenantSource: remembered
|
||||
workspace:
|
||||
id: 42
|
||||
slug: alpha-workspace
|
||||
name: Alpha Workspace
|
||||
tenant:
|
||||
id: 7
|
||||
externalId: tenant-7
|
||||
name: Tenant Seven
|
||||
recoveryDirective:
|
||||
action: none
|
||||
reason: null
|
||||
destination: null
|
||||
preserveIntendedUrl: false
|
||||
queryHintTenant:
|
||||
value:
|
||||
resolvedContext:
|
||||
state: tenant_scoped
|
||||
displayMode: tenant_scoped
|
||||
pageCategory: workspace_scoped
|
||||
workspaceSource: session_workspace
|
||||
tenantSource: query_hint
|
||||
workspace:
|
||||
id: 42
|
||||
slug: alpha-workspace
|
||||
name: Alpha Workspace
|
||||
tenant:
|
||||
id: 7
|
||||
externalId: tenant-7
|
||||
name: Tenant Seven
|
||||
requestedContext:
|
||||
workspaceIdentifier: null
|
||||
tenantIdentifier: tenant-7
|
||||
source: query_hint
|
||||
pageCategory: workspace_scoped
|
||||
recoveryDirective:
|
||||
action: none
|
||||
reason: null
|
||||
destination: null
|
||||
preserveIntendedUrl: false
|
||||
'302':
|
||||
description: No valid workspace could be resolved and the user must be redirected to a chooser or safe fallback.
|
||||
'404':
|
||||
description: The requested context implies inaccessible or invalid workspace-bound data that cannot be widened safely.
|
||||
|
||||
/admin/choose-workspace:
|
||||
get:
|
||||
tags: [shell-context]
|
||||
summary: Resolve the explicit workspace chooser exception route
|
||||
description: Render the explicit workspace chooser exception route used when no workspace truth can be recovered or when the operator must select a workspace directly.
|
||||
responses:
|
||||
'200':
|
||||
description: Workspace chooser rendered as the explicit recovery route.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ResolvedShellContextEnvelope'
|
||||
examples:
|
||||
chooser:
|
||||
value:
|
||||
resolvedContext:
|
||||
state: missing_workspace
|
||||
displayMode: recovery
|
||||
pageCategory: workspace_chooser_exception
|
||||
workspaceSource: none
|
||||
tenantSource: none
|
||||
workspace: null
|
||||
tenant: null
|
||||
recoveryDirective:
|
||||
action: none
|
||||
reason: missing_workspace
|
||||
destination: /admin/choose-workspace
|
||||
preserveIntendedUrl: true
|
||||
|
||||
/admin/choose-tenant:
|
||||
get:
|
||||
tags: [shell-context]
|
||||
summary: Resolve the explicit choose-tenant route after workspace selection
|
||||
description: Render the explicit choose-tenant route used when a resolved workspace has multiple selectable tenants.
|
||||
responses:
|
||||
'200':
|
||||
description: Choose-tenant route rendered successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ResolvedShellContextEnvelope'
|
||||
examples:
|
||||
chooseTenant:
|
||||
value:
|
||||
resolvedContext:
|
||||
state: tenantless_workspace
|
||||
displayMode: tenantless
|
||||
pageCategory: workspace_scoped
|
||||
workspaceSource: session_workspace
|
||||
tenantSource: none
|
||||
workspace:
|
||||
id: 42
|
||||
slug: alpha-workspace
|
||||
name: Alpha Workspace
|
||||
tenant: null
|
||||
recoveryDirective:
|
||||
action: none
|
||||
reason: null
|
||||
destination: /admin/choose-tenant
|
||||
preserveIntendedUrl: false
|
||||
|
||||
/admin/t/{external_id}:
|
||||
get:
|
||||
tags: [shell-context]
|
||||
summary: Resolve tenant-bound shell entry
|
||||
description: Resolve tenant context for a tenant-bound route where explicit tenant routing is required.
|
||||
parameters:
|
||||
- name: external_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Tenant-bound shell entry resolved successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ResolvedShellContextEnvelope'
|
||||
examples:
|
||||
tenantBound:
|
||||
value:
|
||||
resolvedContext:
|
||||
state: tenant_scoped
|
||||
displayMode: tenant_scoped
|
||||
pageCategory: tenant_bound
|
||||
workspaceSource: session_workspace
|
||||
tenantSource: route
|
||||
workspace:
|
||||
id: 42
|
||||
slug: alpha-workspace
|
||||
name: Alpha Workspace
|
||||
tenant:
|
||||
id: 7
|
||||
externalId: tenant-7
|
||||
name: Tenant Seven
|
||||
recoveryDirective:
|
||||
action: none
|
||||
reason: null
|
||||
destination: null
|
||||
preserveIntendedUrl: false
|
||||
'404':
|
||||
description: The route tenant is invalid, inaccessible, or incompatible with the active workspace.
|
||||
|
||||
/admin/switch-workspace:
|
||||
post:
|
||||
tags: [workspace-switch]
|
||||
summary: Switch the active workspace
|
||||
description: Set the active workspace, re-evaluate tenant compatibility, and redirect to a safe concrete destination such as an intended `/admin...` URL, `admin.workspace.managed-tenants.index`, `/admin/choose-tenant`, or the tenant dashboard.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
type: object
|
||||
required: [workspace_id]
|
||||
properties:
|
||||
workspace_id:
|
||||
type: integer
|
||||
responses:
|
||||
'302':
|
||||
description: Workspace switch accepted and redirected to intended URL or resolver destination.
|
||||
headers:
|
||||
Location:
|
||||
schema:
|
||||
type: string
|
||||
description: Safe destination for the resolved workspace and resulting tenant state, currently an intended `/admin...` URL, `admin.workspace.managed-tenants.index`, `/admin/choose-tenant`, or a tenant dashboard route.
|
||||
'404':
|
||||
description: Workspace does not exist, is archived, or is not accessible to the current user.
|
||||
'422':
|
||||
description: Request body failed validation.
|
||||
|
||||
/admin/select-tenant:
|
||||
post:
|
||||
tags: [tenant-select]
|
||||
summary: Select the active tenant inside the resolved workspace
|
||||
description: Explicitly activate a tenant that belongs to the current workspace and passes entitlement and operability checks, then redirect to the deterministic tenant entry route for that tenant.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
type: object
|
||||
required: [tenant_id]
|
||||
properties:
|
||||
tenant_id:
|
||||
type: integer
|
||||
responses:
|
||||
'302':
|
||||
description: Tenant selection accepted and redirected to the deterministic tenant entry route for the selected tenant.
|
||||
headers:
|
||||
Location:
|
||||
schema:
|
||||
type: string
|
||||
'404':
|
||||
description: Tenant is missing, inaccessible, incompatible with the active workspace, or fails operability rules.
|
||||
'422':
|
||||
description: Request body failed validation.
|
||||
|
||||
/admin/clear-tenant-context:
|
||||
post:
|
||||
tags: [tenant-clear]
|
||||
summary: Clear active tenant context
|
||||
description: Remove remembered and panel tenant state, then resolve according to page category and route compatibility to either same-route tenantless workspace state or one of the documented concrete destinations: `admin.operations.index`, `admin.evidence.overview`, `admin.workspace.managed-tenants.index`, `admin.operations.view`, or `admin.home`.
|
||||
responses:
|
||||
'302':
|
||||
description: Tenant context cleared and request resolved to tenantless workspace state on the current route or redirected to one of the documented concrete workspace-safe fallbacks.
|
||||
headers:
|
||||
Location:
|
||||
schema:
|
||||
type: string
|
||||
'404':
|
||||
description: The current route cannot recover safely because scope is no longer accessible.
|
||||
|
||||
components:
|
||||
schemas:
|
||||
RequestedContext:
|
||||
type: object
|
||||
properties:
|
||||
workspaceIdentifier:
|
||||
oneOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
- type: 'null'
|
||||
tenantIdentifier:
|
||||
oneOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
- type: 'null'
|
||||
source:
|
||||
$ref: '#/components/schemas/ContextSource'
|
||||
pageCategory:
|
||||
$ref: '#/components/schemas/PageCategory'
|
||||
|
||||
RememberedContextCandidate:
|
||||
type: object
|
||||
properties:
|
||||
workspaceId:
|
||||
type: integer
|
||||
tenantId:
|
||||
type:
|
||||
- integer
|
||||
- 'null'
|
||||
source:
|
||||
$ref: '#/components/schemas/ContextSource'
|
||||
eligible:
|
||||
type: boolean
|
||||
invalidReason:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
|
||||
ResolvedShellContextEnvelope:
|
||||
type: object
|
||||
required: [resolvedContext]
|
||||
properties:
|
||||
resolvedContext:
|
||||
$ref: '#/components/schemas/ResolvedShellContext'
|
||||
|
||||
ResolvedShellContext:
|
||||
type: object
|
||||
required:
|
||||
- state
|
||||
- displayMode
|
||||
- pageCategory
|
||||
- workspaceSource
|
||||
- tenantSource
|
||||
- workspace
|
||||
- tenant
|
||||
- recoveryDirective
|
||||
properties:
|
||||
state:
|
||||
$ref: '#/components/schemas/ShellState'
|
||||
displayMode:
|
||||
type: string
|
||||
enum:
|
||||
- tenant_scoped
|
||||
- tenantless
|
||||
- recovery
|
||||
pageCategory:
|
||||
$ref: '#/components/schemas/PageCategory'
|
||||
workspaceSource:
|
||||
$ref: '#/components/schemas/ContextSource'
|
||||
tenantSource:
|
||||
$ref: '#/components/schemas/ContextSource'
|
||||
workspace:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/WorkspaceReference'
|
||||
- type: 'null'
|
||||
tenant:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/TenantReference'
|
||||
- type: 'null'
|
||||
requestedContext:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/RequestedContext'
|
||||
- type: 'null'
|
||||
rememberedContext:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/RememberedContextCandidate'
|
||||
- type: 'null'
|
||||
recoveryDirective:
|
||||
$ref: '#/components/schemas/RecoveryDirective'
|
||||
|
||||
RecoveryDirective:
|
||||
type: object
|
||||
required: [action, reason, destination, preserveIntendedUrl]
|
||||
properties:
|
||||
action:
|
||||
$ref: '#/components/schemas/RecoveryAction'
|
||||
reason:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
destination:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
preserveIntendedUrl:
|
||||
type: boolean
|
||||
|
||||
WorkspaceReference:
|
||||
type: object
|
||||
required: [id, slug, name]
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
slug:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
|
||||
TenantReference:
|
||||
type: object
|
||||
required: [id, externalId, name]
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
externalId:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
|
||||
ContextSource:
|
||||
type: string
|
||||
enum:
|
||||
- route
|
||||
- explicit_switch
|
||||
- explicit_select
|
||||
- session_workspace
|
||||
- filament_tenant
|
||||
- remembered
|
||||
- query_hint
|
||||
- none
|
||||
|
||||
PageCategory:
|
||||
type: string
|
||||
enum:
|
||||
- workspace_scoped
|
||||
- workspace_chooser_exception
|
||||
- tenant_bound
|
||||
- tenant_scoped_evidence
|
||||
- canonical_workspace_record_viewer
|
||||
|
||||
ShellState:
|
||||
type: string
|
||||
enum:
|
||||
- tenant_scoped
|
||||
- tenantless_workspace
|
||||
- missing_workspace
|
||||
- invalid_workspace
|
||||
- missing_tenant
|
||||
- invalid_tenant
|
||||
- inaccessible_tenant
|
||||
- incompatible_tenant
|
||||
|
||||
RecoveryAction:
|
||||
type: string
|
||||
enum:
|
||||
- none
|
||||
- render_tenantless_workspace
|
||||
- redirect_choose_workspace
|
||||
- redirect_operations_index
|
||||
- redirect_evidence_overview
|
||||
- redirect_workspace_home
|
||||
- redirect_workspace_managed_tenants
|
||||
- redirect_workspace_record_fallback
|
||||
- abort_not_found
|
||||
@ -1,227 +0,0 @@
|
||||
# Data Model: Global Context Shell Contract
|
||||
|
||||
## Persistence Impact
|
||||
|
||||
- No new database tables, columns, or persisted shell artifacts are introduced.
|
||||
- Existing session-backed fields remain the only durable support state used by the contract:
|
||||
- `current_workspace_id`
|
||||
- `workspace_intended_url`
|
||||
- `workspace_last_tenant_ids`
|
||||
- Existing user-level fields such as `users.last_workspace_id` and `users.last_tenant_id` or `user_tenant_preferences.last_used_at` remain support inputs only and do not become active shell truth.
|
||||
|
||||
## Context Source Inventory
|
||||
|
||||
| Source | Context facet | Source role | Owning seam | Validation / notes |
|
||||
|---|---|---|---|---|
|
||||
| Explicit workspace switch request | Workspace | leading | `SwitchWorkspaceController` + `WorkspaceContext` | Must point to an accessible, selectable workspace before it can replace current workspace truth. |
|
||||
| Current session workspace | Workspace | leading | `WorkspaceContext` | Remains the default current-workspace truth when no stronger explicit request exists and membership is still valid. |
|
||||
| Remembered last workspace | Workspace | supporting | `WorkspaceContext` | Restore-only candidate used when no valid current session workspace exists and the entry flow allows restore. |
|
||||
| Route tenant parameter | Tenant | leading | Route + `OperateHubShell` | Strongest tenant source on tenant-bound routes. Must belong to the resolved workspace and remain entitled. |
|
||||
| Explicit tenant selection request | Tenant | leading | `SelectTenantController` + `OperateHubShell` | Can activate tenant scope only inside an already resolved workspace. |
|
||||
| Filament panel tenant state | Tenant | supporting | `ResolvesPanelTenantContext` | May support resolution only after workspace compatibility and entitlement checks succeed. |
|
||||
| Remembered tenant for resolved workspace | Tenant | supporting | `WorkspaceContext` | Restore-only candidate on tenantless-capable workspace pages. Never valid on its own for tenant-bound routes. |
|
||||
| Query hint | Workspace or Tenant | supporting only when contract explicitly allows it | `OperateHubShell` | Must never become effective truth unless the contract explicitly names the query-backed flow. |
|
||||
| View-local shell inference | Workspace or Tenant | never-leading | Shared shell partials and page-local views | Rendering surfaces may display resolved truth only. They cannot evaluate precedence or recovery. |
|
||||
|
||||
## Runtime Entities
|
||||
|
||||
| Entity | Kind | Fields | Validation / Notes |
|
||||
|---|---|---|---|
|
||||
| RequestedWorkspaceContext | Derived runtime input | `workspaceIdentifier`, `source`, `intendedUrl`, `pageCategory` | Represents a workspace request from route, explicit switch flow, or initial restore path before validation. |
|
||||
| RequestedTenantContext | Derived runtime input | `tenantIdentifier`, `source`, `pageCategory`, `requiresExplicitTenant` | Represents route tenant, explicit tenant select, query hint, Filament tenant, or remembered tenant before validation. |
|
||||
| RememberedContextCandidate | Derived support state | `workspaceId`, `tenantId`, `source`, `eligible` | Represents stored last-used tenant for the active workspace or last-used workspace during initial resolution. Never leading by itself. |
|
||||
| ResolvedShellContext | Canonical request-scoped truth | `workspace`, `tenant`, `pageCategory`, `workspaceSource`, `tenantSource`, `state`, `recoveryDirective`, `displayMode` | The only context object shell UI and server-side consumers should trust for the current request. |
|
||||
| RecoveryDirective | Derived outcome | `action`, `destination`, `reason`, `preserveIntendedUrl` | Encodes whether the request renders tenantless state, redirects, or aborts. |
|
||||
| InvalidContext | Derived runtime outcome | `kind`, `source`, `reason`, `requestedWorkspaceIdentifier`, `requestedTenantIdentifier` | Captures why a requested or remembered context could not become active truth. |
|
||||
|
||||
## Supporting Enums / Value Domains
|
||||
|
||||
### ContextSource
|
||||
|
||||
- `route`
|
||||
- `explicit_switch`
|
||||
- `explicit_select`
|
||||
- `session_workspace`
|
||||
- `filament_tenant`
|
||||
- `remembered`
|
||||
- `query_hint`
|
||||
- `none`
|
||||
|
||||
### ShellState
|
||||
|
||||
- `tenant_scoped`
|
||||
- `tenantless_workspace`
|
||||
- `missing_workspace`
|
||||
- `invalid_workspace`
|
||||
- `missing_tenant`
|
||||
- `invalid_tenant`
|
||||
- `inaccessible_tenant`
|
||||
- `incompatible_tenant`
|
||||
|
||||
### RecoveryAction
|
||||
|
||||
- `none`
|
||||
- `render_tenantless_workspace`
|
||||
- `redirect_choose_workspace`
|
||||
- `redirect_operations_index`
|
||||
- `redirect_evidence_overview`
|
||||
- `redirect_workspace_home`
|
||||
- `redirect_workspace_managed_tenants`
|
||||
- `redirect_workspace_record_fallback`
|
||||
- `abort_not_found`
|
||||
|
||||
### PageCategory
|
||||
|
||||
- `workspace_scoped`
|
||||
- `workspace_chooser_exception`
|
||||
- `tenant_bound`
|
||||
- `tenant_scoped_evidence`
|
||||
- `canonical_workspace_record_viewer`
|
||||
|
||||
## Entity Details
|
||||
|
||||
### RequestedWorkspaceContext
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| `workspaceIdentifier` | string or int | yes | May come from explicit workspace switch flow or a safe intended URL restore path. |
|
||||
| `source` | `ContextSource` | yes | `explicit_switch`, `session_workspace`, or `remembered` are the main inputs today. |
|
||||
| `intendedUrl` | string or null | no | Safe `/admin...` path captured via `WorkspaceIntendedUrl`. |
|
||||
| `pageCategory` | `PageCategory` | yes | Needed to determine if a tenantless fallback is valid after workspace resolution. |
|
||||
|
||||
**Validation rules**:
|
||||
|
||||
- Workspace must exist.
|
||||
- Workspace must not be archived or otherwise unselectable.
|
||||
- User must be a member of the workspace.
|
||||
- Cross-plane routes remain out of scope; only `web`-guarded admin and tenant routes participate.
|
||||
|
||||
### RequestedTenantContext
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| `tenantIdentifier` | string or int | yes | May come from route param, explicit tenant selection, query hint, Filament panel state, or remembered session state. |
|
||||
| `source` | `ContextSource` | yes | Route and explicit selection are strongest; remembered is weakest. |
|
||||
| `pageCategory` | `PageCategory` | yes | Determines whether tenant fallback is valid, optional, or forbidden. |
|
||||
| `requiresExplicitTenant` | bool | yes | `true` for tenant-bound pages; `false` for workspace-scoped pages that can remain tenantless. |
|
||||
|
||||
**Validation rules**:
|
||||
|
||||
- Tenant must exist.
|
||||
- Tenant must belong to the resolved workspace.
|
||||
- User must be entitled to the tenant.
|
||||
- Tenant must satisfy the relevant operability question for the current lane.
|
||||
- Tenant must be compatible with the current route type.
|
||||
|
||||
### RememberedContextCandidate
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| `workspaceId` | int | yes | Key for the remembered tenant map. |
|
||||
| `tenantId` | int or null | no | Candidate tenant for restore. |
|
||||
| `source` | `ContextSource` | yes | Today this is `remembered`, with supporting user-level last-used values. |
|
||||
| `eligible` | bool | yes | `false` once access, operability, or workspace match fails. |
|
||||
| `invalidReason` | string or null | no | Captures why the remembered candidate became ineligible during validation or cleanup. |
|
||||
|
||||
**Validation rules**:
|
||||
|
||||
- Candidate tenant must still exist.
|
||||
- Candidate tenant must still belong to the active workspace.
|
||||
- Candidate tenant must still be accessible to the user.
|
||||
- Candidate tenant must still pass `RememberedContextValidity` for the current lane.
|
||||
- Ineligible remembered context is cleared immediately and cannot survive as visible shell truth.
|
||||
|
||||
### ResolvedShellContext
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| `workspace` | Workspace or null | yes | Null only when recovery requires chooser or not-found handling. |
|
||||
| `tenant` | Tenant or null | yes | Null is valid only in tenantless workspace state or before a hard recovery redirect. |
|
||||
| `pageCategory` | `PageCategory` | yes | Controls whether tenantless state is valid. |
|
||||
| `workspaceSource` | `ContextSource` | yes | Records which source actually won for workspace resolution. |
|
||||
| `tenantSource` | `ContextSource` | yes | Records which source actually won for tenant resolution, or `none`. |
|
||||
| `state` | `ShellState` | yes | The user-visible shell state. |
|
||||
| `recoveryDirective` | `RecoveryDirective` | yes | Defines what to render or where to redirect if the request cannot continue as requested. |
|
||||
| `displayMode` | string | yes | `tenant_scoped`, `tenantless`, or `recovery`. |
|
||||
|
||||
**Invariants**:
|
||||
|
||||
- A resolved tenant cannot exist without a resolved workspace.
|
||||
- Remembered context cannot become active if a stronger valid source exists.
|
||||
- The shell display must derive only from `ResolvedShellContext`.
|
||||
- Tenant-bound pages cannot render a remembered-tenant fallback as though it were an explicit route tenant.
|
||||
|
||||
### InvalidContext
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| `kind` | string | yes | `workspace` or `tenant` |
|
||||
| `source` | `ContextSource` | yes | Identifies whether route, panel, remembered, or query input failed. |
|
||||
| `reason` | string | yes | `missing`, `inaccessible`, `incompatible`, `not_operable`, `not_member`, `archived`, or `mismatched_workspace` |
|
||||
| `requestedWorkspaceIdentifier` | string or int or null | no | Included for diagnostics and testing only. |
|
||||
| `requestedTenantIdentifier` | string or int or null | no | Included for diagnostics and testing only. |
|
||||
|
||||
## Relationships
|
||||
|
||||
- `ResolvedShellContext` is composed from zero or one `RequestedWorkspaceContext`, zero or one `RequestedTenantContext`, zero or one `RememberedContextCandidate`, and zero or one `InvalidContext` plus `RecoveryDirective`.
|
||||
- `RecoveryDirective` is downstream of `ResolvedShellContext.state` and `PageCategory`.
|
||||
- `RememberedContextCandidate.workspaceId` is always keyed to the resolved workspace candidate; it is not global across workspaces.
|
||||
|
||||
## Resolution Rules
|
||||
|
||||
### Workspace Resolution
|
||||
|
||||
1. Try a valid explicit workspace request when the current entry flow provides one.
|
||||
2. Otherwise use the current session workspace if it remains valid.
|
||||
3. Otherwise allow a valid last-used workspace restore only during initial resolution.
|
||||
4. Otherwise emit `missing_workspace` or `invalid_workspace` with a chooser-oriented recovery directive.
|
||||
|
||||
### Tenant Resolution
|
||||
|
||||
1. On tenant-bound pages, validate route tenant first and fail if it is missing, inaccessible, or incompatible.
|
||||
2. On workspace-scoped pages, accept a valid route tenant or explicit tenant-selection request first.
|
||||
3. Next accept a validated query-backed tenant hint only on routes where the contract explicitly allows query-backed shell resolution.
|
||||
4. Next accept validated `Filament::getTenant()` only if it matches the resolved workspace and remains entitled.
|
||||
5. Next accept remembered tenant only when the page category permits tenantless fallback and no stronger valid tenant source exists.
|
||||
6. Otherwise resolve to tenantless workspace state or to an explicit recovery directive depending on page category.
|
||||
|
||||
## Recovery Matrix
|
||||
|
||||
| Page category | Invalid workspace | Invalid explicit tenant | Missing tenant after clear | Invalid remembered tenant |
|
||||
|---|---|---|---|---|
|
||||
| `workspace_scoped` | redirect to chooser, or to `admin.operations.index` when cleanup is referrer-free or sentinel-driven | render tenantless workspace or clear request | render tenantless workspace on the current route, or use `admin.operations.index` when no safe prior route exists | clear remembered tenant and remain tenantless |
|
||||
| `workspace_chooser_exception` | remain on `/admin/choose-workspace` | not applicable | remain on `/admin/choose-workspace` until the user selects a workspace | not applicable |
|
||||
| `tenant_bound` | 404 or redirect to chooser if no workspace can be re-established | 404 when route tenant is invalid or inaccessible | redirect to `admin.workspace.managed-tenants.index` for the current workspace, else `admin.home` | ignored as active truth; route still governs |
|
||||
| `tenant_scoped_evidence` | redirect to chooser if workspace truth cannot be re-established | redirect to `admin.evidence.overview` when tenant detail context is no longer valid | redirect to `admin.evidence.overview` | clear remembered tenant and return to `admin.evidence.overview` |
|
||||
| `canonical_workspace_record_viewer` | 404 if the record itself is no longer entitled | 404 if tenant-scoped record access fails | remain on `admin.operations.view` when it is still workspace-safe, otherwise use the documented workspace fallback | clear remembered tenant and keep record-only rules |
|
||||
|
||||
## Documented Recovery Destinations
|
||||
|
||||
| RecoveryAction | Route target | When it applies |
|
||||
|---|---|---|
|
||||
| `redirect_choose_workspace` | `/admin/choose-workspace` | Missing or unrecoverable workspace truth at shell entry or restore time |
|
||||
| `redirect_operations_index` | `admin.operations.index` | External or missing referrer, clear-flow sentinel path, and generic workspace-safe fallback for tenantless monitoring entry |
|
||||
| `redirect_evidence_overview` | `admin.evidence.overview` | Tenant-scoped evidence paths that must return to a workspace-safe evidence landing |
|
||||
| `redirect_workspace_managed_tenants` | `admin.workspace.managed-tenants.index` | Tenant-bound cleanup or workspace switch flows that must return to tenant selection inside the resolved workspace |
|
||||
| `redirect_workspace_home` | `admin.home` | Tenant-bound cleanup when no current workspace truth remains available |
|
||||
| `redirect_workspace_record_fallback` | `admin.operations.view` in the current-release scope | Canonical workspace record viewers that stay in workspace scope without reviving tenant truth |
|
||||
|
||||
## State Transitions
|
||||
|
||||
| Trigger | From | To | Notes |
|
||||
|---|---|---|---|
|
||||
| Workspace switch | `tenant_scoped` | `tenant_scoped` or `tenantless_workspace` | Existing tenant survives only after compatibility re-check in target workspace. |
|
||||
| Tenant select | `tenantless_workspace` | `tenant_scoped` | Explicit user action; requires entitlement and operability validation. |
|
||||
| Tenant clear on workspace page | `tenant_scoped` | `tenantless_workspace` | Valid only on workspace-scoped pages. |
|
||||
| Tenant clear on tenant-bound page | `tenant_scoped` | `tenantless_workspace` plus redirect | Redirect destination depends on page category and route family. |
|
||||
| Remembered tenant invalidation | `tenant_scoped` or restore candidate | `tenantless_workspace` | Candidate is cleared and cannot stay visible. |
|
||||
| Workspace invalidation | any | `missing_workspace` or `invalid_workspace` | Recovery goes to chooser or not-found depending on entry path. |
|
||||
|
||||
## Display Semantics
|
||||
|
||||
| Resolved state | Workspace label | Tenant label | Action affordances |
|
||||
|---|---|---|---|
|
||||
| `tenant_scoped` | Active workspace name | Active tenant name | Switch workspace, Select tenant, Clear tenant context |
|
||||
| `tenantless_workspace` | Active workspace name | `No tenant selected` | Switch workspace, Select tenant |
|
||||
| `missing_workspace` / `invalid_workspace` | `Choose workspace` or recovery label | hidden or disabled | Choose workspace, optional safe return |
|
||||
| `invalid_tenant` / `inaccessible_tenant` / `incompatible_tenant` | Active workspace name if valid | no stale tenant name shown | Recovery action only; no stale tenant truth |
|
||||
@ -1,323 +0,0 @@
|
||||
# Implementation Plan: Global Context Shell Contract
|
||||
|
||||
**Branch**: `199-global-context-shell-contract` | **Date**: 2026-04-18 | **Spec**: `specs/199-global-context-shell-contract/spec.md`
|
||||
**Input**: Feature specification from `specs/199-global-context-shell-contract/spec.md`
|
||||
|
||||
**Note**: This plan keeps the work inside the existing Filament v5 / Livewire v4 admin and tenant panels, reuses `WorkspaceContext` as the session-backed storage owner, promotes one request-scoped shell resolution contract over the current split logic, and explicitly avoids new persistence, panel proliferation, or a generic context framework.
|
||||
|
||||
## Summary
|
||||
|
||||
Cut one explicit workspace-first global shell contract for `/admin` and `/admin/t/{external_id}` by keeping workspace session ownership in `WorkspaceContext`, consolidating active workspace and tenant resolution behind one request-scoped shell contract consumed by `OperateHubShell`, aligning switch/select/clear controllers and `EnsureFilamentTenantSelected` to the same precedence and fallback rules, and reducing the shared `context-bar` partial to a pure consumer and dispatcher of resolved context. Preserve tenant-safe global search, deny-as-not-found isolation, intended-URL handling, and existing Filament panel topology while replacing scattered partial, controller, middleware, and panel-state heuristics with one documented source-of-truth hierarchy and one explicit invalid-context recovery model.
|
||||
|
||||
## Contract Ownership
|
||||
|
||||
- **Source inventory owner**: `specs/199-global-context-shell-contract/data-model.md` contains the canonical `Context Source Inventory` for every in-scope workspace and tenant context source.
|
||||
- **Documented recovery destinations**:
|
||||
- missing or unrecoverable workspace truth falls back to `/admin/choose-workspace`
|
||||
- generic referrer-free or sentinel cleanup falls back to `admin.operations.index`
|
||||
- tenant-scoped evidence cleanup falls back to `admin.evidence.overview`
|
||||
- tenant-bound cleanup with a valid workspace falls back to `admin.workspace.managed-tenants.index`
|
||||
- tenant-bound cleanup without recoverable workspace falls back to `admin.home`
|
||||
- tenantless-capable workspace routes and canonical workspace record viewers remain on their current route when entitlement remains valid
|
||||
- **Workspace switch destination set**:
|
||||
- safe intended `/admin...` URL when present and still valid
|
||||
- `admin.workspace.managed-tenants.index` when the resolved workspace has zero selectable tenants
|
||||
- `/admin/choose-tenant` when the resolved workspace has multiple selectable tenants
|
||||
- tenant dashboard route under `/admin/t/{external_id}` when the resolved workspace has exactly one selectable tenant
|
||||
- **Explicit page-category exceptions**:
|
||||
- `/admin/choose-workspace` is the `workspace_chooser_exception` route
|
||||
- `/admin/evidence/...` except `/admin/evidence/overview` is treated as `tenant_scoped_evidence` for recovery behavior
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: PHP 8.4.15
|
||||
**Primary Dependencies**: Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `WorkspaceContext`, `OperateHubShell`, `EnsureFilamentTenantSelected`, `WorkspaceRedirectResolver`, `WorkspaceIntendedUrl`, `TenantPageCategory`, and `ResolvesPanelTenantContext`
|
||||
**Storage**: PostgreSQL unchanged plus existing Laravel session keys `current_workspace_id`, `workspace_intended_url`, and `workspace_last_tenant_ids`; no schema change planned
|
||||
**Testing**: Pest unit and feature tests, existing Filament or Livewire page tests, and manual shell smoke validation through Laravel Sail; browser automation remains optional and not the proving default
|
||||
**Validation Lanes**: fast-feedback, confidence
|
||||
**Target Platform**: Laravel monolith web application under `apps/platform`, running via Sail locally, with shared operator shells on `/admin` and `/admin/t/{external_id}` and isolated `/system` remaining out of scope
|
||||
**Project Type**: web application
|
||||
**Performance Goals**: Keep shell context resolution request-scoped, DB-and-session-only at render time, avoid any outbound HTTP or queued work during shell hydration, avoid additional N+1 tenant lookups in the topbar, and keep context-bar rendering within existing operator-page latency expectations
|
||||
**Constraints**: No new persisted truth, no generic context engine, no cross-plane auth redesign, no hidden page-state ownership inside the shell contract, no global navigation rewrite, no dependency changes, and no new asset pipeline requirements
|
||||
**Scale/Scope**: 2 shared operator panels, 1 shared context-bar partial, 3 context mutation endpoints, 5 existing core support classes or middleware seams, targeted updates across existing workspace, monitoring, RBAC, and tenant-RBAC feature seams, and 2 to 4 new narrow regression files for shell resolution and recovery
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Passed before Phase 0 research. Re-checked after Phase 1 design and still passing.*
|
||||
|
||||
| Principle | Pre-Research | Post-Design | Notes |
|
||||
|-----------|--------------|-------------|-------|
|
||||
| Inventory-first / snapshots-second | PASS | PASS | The feature changes shell context truth only. It does not alter inventory, snapshot, or backup product truth. |
|
||||
| Read/write separation | PASS | PASS | The work changes session-backed scope selection and recovery behavior only. No new Microsoft tenant write, queued work, or operational mutation is introduced. |
|
||||
| Graph contract path | N/A | N/A | No Microsoft Graph call path is added or modified. |
|
||||
| Deterministic capabilities | PASS | PASS | Existing workspace and tenant entitlement checks remain authoritative. No new capability strings or auth planes are introduced. |
|
||||
| Workspace + tenant isolation | PASS | PASS | The design strengthens workspace-first isolation by preventing tenant truth from surviving outside a valid workspace and by standardizing 404 versus tenantless fallback rules. |
|
||||
| RBAC-UX authorization semantics | PASS | PASS | Non-members remain 404, members without capability remain 403 only after scope is established, and shell context cleanup must not surface inaccessible tenant or workspace truth. |
|
||||
| Run observability / Ops-UX | PASS | PASS | No `OperationRun` is introduced or changed. Shell resolution and display remain synchronous request work. |
|
||||
| Data minimization | PASS | PASS | No new persistence, cache mirror, or derived artifact is added; remembered values remain support state only. |
|
||||
| Proportionality / anti-bloat | PASS WITH JUSTIFIED SOURCE CONTRACT | PASS WITH JUSTIFIED SOURCE CONTRACT | The feature introduces one bounded source-of-truth hierarchy and one bounded runtime state vocabulary because multiple existing classes already resolve the same context with competing rules. It explicitly avoids a generic engine or persisted context model. |
|
||||
| UI semantics / few layers | PASS | PASS | The plan keeps one thin request-scoped contract and removes partial-owned context logic instead of adding a presenter or UI framework layer. |
|
||||
| Filament-native UI | PASS | PASS | Existing Filament panels, routes, middleware, and shared partials remain the implementation path. No hand-built alternate shell system is introduced. |
|
||||
| Filament v5 / Livewire v4 compliance | PASS | PASS | All touched surfaces remain on Filament v5 and Livewire v4 semantics only. |
|
||||
| Provider registration location | PASS | PASS | No provider registration change is required. Laravel 11+ provider registration remains in `bootstrap/providers.php`. |
|
||||
| Global search hard rule | PASS | PASS | No new globally searchable resource is introduced. Existing searchable resources keep their View or Edit pages, and the contract explicitly preserves tenant-safe global search behavior under resolved shell scope. |
|
||||
| Destructive action safety | PASS | PASS | No new destructive Filament action is added. Context reset via `clear-tenant-context` remains a scope action, not a record-destruction action. |
|
||||
| Asset strategy | PASS | PASS | No new assets or build steps are planned. Existing `cd apps/platform && php artisan filament:assets` deployment handling remains sufficient. |
|
||||
|
||||
## Filament-Specific Compliance Notes
|
||||
|
||||
- **Livewire v4.0+ compliance**: The implementation stays on Filament v5 and Livewire v4 pages, middleware, support classes, and shared Blade partials. No legacy Livewire or Filament APIs are introduced.
|
||||
- **Provider registration location**: No panel or provider registration changes are planned. Provider registration remains in `bootstrap/providers.php`.
|
||||
- **Global search**: No new searchable resources are added. Existing searchable resources remain `TenantResource` and `PolicyResource`, both of which already have View pages, and the shell contract must preserve existing tenant-safe and workspace-safe global search semantics.
|
||||
- **Destructive actions**: The feature introduces no destructive record actions. Existing shell actions `Switch workspace`, `Select tenant`, and `Clear tenant context` remain scope-setting flows only. Any existing destructive actions elsewhere remain unchanged and continue to require confirmation and authorization under current resource contracts.
|
||||
- **Asset strategy**: No new JS or CSS assets are planned. Existing deployment handling of `cd apps/platform && php artisan filament:assets` remains unchanged.
|
||||
- **Testing plan**: Extend or reuse `WorkspaceContextRememberedTenantTest`, `WorkspaceContextTopbarAndTenantSelectionTest`, `WorkspaceContextRecoveryDisplayTest`, `SelectTenantControllerTest`, `ChooseTenantPageTest`, `ChooseWorkspacePageTest`, `ChooseWorkspaceRedirectsToChooseTenantTest`, `WorkspaceRedirectResolverTest`, `WorkspaceSwitchUserMenuTest`, `SwitchWorkspaceRedirectsToTenantRegistrationWhenNoTenantsTest`, `SwitchWorkspaceControllerTest`, `GlobalContextShellContractTest`, `EnsureWorkspaceSelectedMiddlewareTest`, `WorkspacesResourceIsTenantlessTest`, `OperationsIndexHeaderTest`, `AdminGlobalSearchContextSafetyTest`, `TenantSwitcherScopeTest`, `TenantActionSurfaceConsistencyTest`, `OperationsDbOnlyRenderTest`, and `OperationsActionsEnqueueRunTest` so the contract is proven without introducing a new browser or heavy-governance family.
|
||||
|
||||
## Test Governance Check
|
||||
|
||||
- **Test purpose / classification by changed surface**: Unit for runtime context resolution and invalidation rules in support classes; Feature for controller flows, middleware behavior, panel entry routes, and shared shell rendering.
|
||||
- **Affected validation lanes**: fast-feedback, confidence.
|
||||
- **Why this lane mix is the narrowest sufficient proof**: The feature changes request-time scope resolution, redirects, session mutation, and rendered shell truth. These are best proven with unit tests around the support layer plus feature tests over routes and rendered pages. A browser lane is not required unless later implementation introduces client-only shell behavior that feature tests cannot observe.
|
||||
- **Narrowest proving command(s)**:
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/Workspaces/WorkspaceContextRememberedTenantTest.php`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/OperateHub/OperateHubShellResolutionTest.php`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/WorkspaceContextTopbarAndTenantSelectionTest.php`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Workspaces/SelectTenantControllerTest.php`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Workspaces/ChooseTenantPageTest.php`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Workspaces/ChooseWorkspacePageTest.php`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Workspaces/WorkspaceRedirectResolverTest.php`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Spec085/OperationsIndexHeaderTest.php`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Monitoring/OperationsDbOnlyRenderTest.php`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Monitoring/OperationsActionsEnqueueRunTest.php`
|
||||
- **Fixture / helper / factory / seed / context cost risks**: Moderate but bounded. Tests need workspace membership, tenant membership, workspace session state, remembered tenant session state, and selective Filament tenant seeding. No new provider bootstraps, seeds, queues, or browser fixtures are expected.
|
||||
- **Expensive defaults or shared helper growth introduced?**: No. Existing helper seams such as `createUserWithTenant()` and targeted session seeding stay opt-in. The plan should avoid introducing a global full-shell fixture as the new default.
|
||||
- **Heavy-family additions, promotions, or visibility changes**: none.
|
||||
- **Non-functional shell-render proof**: Reuse `OperationsDbOnlyRenderTest` and `OperationsActionsEnqueueRunTest` so the shell-anchor workspace surfaces stay DB-only and non-enqueuing during render.
|
||||
- **Closing validation and reviewer handoff**: Reviewers should confirm that new tests stay in unit and feature lanes, that invalid-context recovery is proven without browser escalation, that shell display and actual resolved context match, and that remembered-tenant invalidation remains explicit in test names and assertions.
|
||||
- **Budget / baseline / trend follow-up**: none beyond a small increase in focused unit and feature runtime.
|
||||
- **Review-stop questions**: Is the new proof actually feature-level? Did any helper make full workspace or tenant context implicit by default? Did the implementation create a second source of truth in tests or UI? Did any browser-only assertion sneak in without necessity?
|
||||
- **Escalation path**: document-in-feature.
|
||||
- **Why no dedicated follow-up spec is needed**: Test cost remains feature-local as long as the work stays in the existing support layer, controllers, middleware, and shared shell partials without introducing a new test harness or a new browser family.
|
||||
|
||||
## Phase 0 Research
|
||||
|
||||
Research outcomes are captured in `specs/199-global-context-shell-contract/research.md`.
|
||||
|
||||
Key decisions:
|
||||
|
||||
- Keep `WorkspaceContext` as the session-backed owner of workspace selection and remembered tenant storage, but stop treating it and `OperateHubShell` as parallel visible truths.
|
||||
- Use one request-scoped resolved shell contract to unify route tenant, Filament tenant, remembered tenant, and tenantless fallback semantics instead of repeating those rules in controllers, partials, and middleware.
|
||||
- Preserve the existing effective precedence for active tenant on admin surfaces: valid route tenant first, then explicit tenant selection, then validated query-backed tenant hints only on explicitly allowed workspace-scoped routes, then validated Filament tenant, then remembered tenant only on workspace-scoped pages, with tenant-bound pages rejecting query-hint and remembered fallback.
|
||||
- Reduce `context-bar.blade.php` to a pure consumer and dispatcher of resolved context instead of letting it re-discover state on its own.
|
||||
- Make invalid-context recovery explicit and page-category-aware so missing workspace, missing tenant, incompatible tenant, and inaccessible tenant produce deterministic fallback rather than mixed 404, silent clear, or stale shell display behavior.
|
||||
- Extend existing unit and feature seams instead of introducing a browser-first shell test family.
|
||||
|
||||
## Phase 1 Design
|
||||
|
||||
Design artifacts are created under `specs/199-global-context-shell-contract/`:
|
||||
|
||||
- `research.md`: shell source-of-truth decisions, risks, and rejected alternatives
|
||||
- `data-model.md`: runtime shell-context entities, validation rules, and state transitions
|
||||
- `contracts/global-context-shell.logical.openapi.yaml`: internal logical HTTP contract for shell context entry and mutation flows
|
||||
- `quickstart.md`: implementation and verification workflow for Spec 199
|
||||
|
||||
Design highlights:
|
||||
|
||||
- Keep the contract derived and request-scoped. No new table or persisted shell artifact is introduced.
|
||||
- Preserve `WorkspaceContext` as the storage owner and existing route controllers as explicit mutation entry points, but make them consume one resolved shell contract instead of each re-defining fallback behavior.
|
||||
- Treat `OperateHubShell` as the canonical shared shell resolver for admin-facing context, with tenant-panel-native semantics remaining route-bound and panel-native where appropriate.
|
||||
- Keep the workspace chooser flow as the explicit current-release `workspace_chooser_exception` instead of letting missing-workspace handling stay implicit.
|
||||
- Encode invalid-context recovery as explicit outcome types tied to route requirements and `TenantPageCategory`, instead of leaving recovery scattered across `context-bar.blade.php`, `ClearTenantContextController`, and middleware heuristics.
|
||||
- Keep page-local filters, tabs, inspect state, and other page-state concerns out of the shell contract so tenant-prefilter behavior remains explicit and opt-in.
|
||||
- Keep `EnsureFilamentTenantSelected` and `ResolvesPanelTenantContext` as consumers of the contract so shared panel behavior does not drift.
|
||||
|
||||
## Phase 1 - Agent Context Update
|
||||
|
||||
Executed command:
|
||||
|
||||
- `.specify/scripts/bash/update-agent-context.sh copilot`
|
||||
|
||||
This feature does not add a new language or framework, but the agent-context refresh still runs after design artifacts are complete so the current feature context is recorded in the agent guidance files.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/199-global-context-shell-contract/
|
||||
├── spec.md
|
||||
├── plan.md
|
||||
├── research.md
|
||||
├── data-model.md
|
||||
├── quickstart.md
|
||||
├── contracts/
|
||||
│ └── global-context-shell.logical.openapi.yaml
|
||||
├── checklists/
|
||||
│ └── requirements.md
|
||||
└── tasks.md
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
|
||||
```text
|
||||
apps/platform/
|
||||
├── app/
|
||||
│ ├── Filament/
|
||||
│ │ └── Concerns/
|
||||
│ │ ├── ResolvesPanelTenantContext.php # MODIFY
|
||||
│ │ └── ScopesGlobalSearchToTenant.php # REUSE / possible small adjust
|
||||
│ ├── Http/
|
||||
│ │ └── Controllers/
|
||||
│ │ ├── SwitchWorkspaceController.php # MODIFY
|
||||
│ │ ├── SelectTenantController.php # MODIFY
|
||||
│ │ └── ClearTenantContextController.php # MODIFY
|
||||
│ ├── Providers/
|
||||
│ │ └── Filament/
|
||||
│ │ ├── AdminPanelProvider.php # REUSE / possible small adjust
|
||||
│ │ └── TenantPanelProvider.php # REUSE / possible small adjust
|
||||
│ └── Support/
|
||||
│ ├── Middleware/
|
||||
│ │ └── EnsureFilamentTenantSelected.php # MODIFY
|
||||
│ ├── OperateHub/
|
||||
│ │ └── OperateHubShell.php # MODIFY
|
||||
│ ├── Tenants/
|
||||
│ │ └── TenantPageCategory.php # MODIFY
|
||||
│ └── Workspaces/
|
||||
│ ├── WorkspaceContext.php # MODIFY
|
||||
│ ├── WorkspaceIntendedUrl.php # REUSE / possible small adjust
|
||||
│ └── WorkspaceRedirectResolver.php # MODIFY
|
||||
├── resources/
|
||||
│ └── views/
|
||||
│ └── filament/
|
||||
│ └── partials/
|
||||
│ └── context-bar.blade.php # MODIFY
|
||||
└── tests/
|
||||
├── Unit/
|
||||
│ └── Support/
|
||||
│ ├── OperateHub/
|
||||
│ │ └── OperateHubShellResolutionTest.php # NEW
|
||||
│ └── Workspaces/
|
||||
│ └── WorkspaceContextRememberedTenantTest.php # MODIFY
|
||||
└── Feature/
|
||||
├── Filament/
|
||||
│ ├── WorkspaceContextTopbarAndTenantSelectionTest.php # MODIFY
|
||||
│ └── WorkspaceContextRecoveryDisplayTest.php # NEW
|
||||
├── Monitoring/
|
||||
│ ├── OperationsActionsEnqueueRunTest.php # MODIFY / VERIFY
|
||||
│ └── OperationsDbOnlyRenderTest.php # MODIFY / VERIFY
|
||||
├── Rbac/
|
||||
│ ├── AdminGlobalSearchContextSafetyTest.php # MODIFY
|
||||
│ └── TenantActionSurfaceConsistencyTest.php # MODIFY
|
||||
├── Spec085/
|
||||
│ └── OperationsIndexHeaderTest.php # MODIFY
|
||||
├── TenantRBAC/
|
||||
│ └── TenantSwitcherScopeTest.php # MODIFY
|
||||
└── Workspaces/
|
||||
├── ChooseTenantPageTest.php # MODIFY
|
||||
├── ChooseWorkspacePageTest.php # MODIFY
|
||||
├── ChooseWorkspaceRedirectsToChooseTenantTest.php # MODIFY
|
||||
├── EnsureWorkspaceSelectedMiddlewareTest.php # MODIFY
|
||||
├── GlobalContextShellContractTest.php # NEW
|
||||
├── SelectTenantControllerTest.php # MODIFY
|
||||
├── SwitchWorkspaceControllerTest.php # NEW
|
||||
├── WorkspaceRedirectResolverTest.php # MODIFY
|
||||
├── SwitchWorkspaceRedirectsToTenantRegistrationWhenNoTenantsTest.php # MODIFY
|
||||
├── WorkspaceSwitchUserMenuTest.php # MODIFY
|
||||
└── WorkspacesResourceIsTenantlessTest.php # MODIFY
|
||||
```
|
||||
|
||||
**Structure Decision**: Keep the work entirely inside the existing Laravel and Filament monolith under `apps/platform`. Reuse current support classes, controllers, middleware, panel providers, and the shared `context-bar` partial. Add only narrow new tests and, if the implementation proves it necessary, a tiny request-scoped result structure inside the existing support layer rather than a new framework directory.
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
|-----------|------------|-------------------------------------|
|
||||
| Bounded new source-of-truth contract and runtime shell-context taxonomy | Multiple existing classes and the shared shell partial already resolve active workspace and tenant context with competing precedence and recovery rules. The feature needs one explicit request-scoped contract to stop drift now. | Pure controller or partial cleanup would keep resolution logic split across `WorkspaceContext`, `OperateHubShell`, middleware, panel state, and the Blade partial, which would preserve the exact ambiguity Spec 199 exists to remove. |
|
||||
|
||||
## Proportionality Review
|
||||
|
||||
- **Current operator problem**: Operators and reviewers cannot reliably trust the shell to answer where they are operating, which tenant is active, or what a switch or clear action will do.
|
||||
- **Existing structure is insufficient because**: The current rules live across `WorkspaceContext`, `OperateHubShell`, controllers, middleware, panel tenancy, and `context-bar.blade.php`, so local cleanup in one place cannot produce one shared source of truth.
|
||||
- **Narrowest correct implementation**: Keep workspace and remembered-tenant storage in the current session-backed support layer, introduce one explicit resolved shell contract for the request, and make existing shell consumers use it. Do not add persistence or a generic engine.
|
||||
- **Ownership cost created**: Reviewers must maintain one shared shell source hierarchy, one invalid-context taxonomy, and focused unit and feature regression coverage for switch, select, clear, restore, and recovery.
|
||||
- **Alternative intentionally rejected**: A generic multi-panel context framework was rejected as overproduction, and a partial-only cleanup was rejected as insufficient.
|
||||
- **Release truth**: current-release operator trust and scope clarity
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### Phase A - Canonicalize one resolved shell context
|
||||
|
||||
**Goal**: Replace competing runtime context truths with one request-scoped resolved contract while keeping existing session-backed storage ownership intact.
|
||||
|
||||
| Step | File | Change |
|
||||
|------|------|--------|
|
||||
| A.0 | `specs/199-global-context-shell-contract/data-model.md` | Maintain the canonical `Context Source Inventory` so every in-scope source has one declared role, one owner, and one validation note. |
|
||||
| A.1 | `apps/platform/app/Support/OperateHub/OperateHubShell.php` | Expand the shell support seam so it can resolve one canonical shell context for the current request, including workspace, tenant, page category, source precedence, tenantless validity, and invalid-context recovery metadata. |
|
||||
| A.2 | `apps/platform/app/Support/Workspaces/WorkspaceContext.php` | Keep workspace and remembered-tenant session ownership here, but align helper methods to the canonical contract by making remembered values restore-only and by making invalidation rules explicit and reusable. |
|
||||
| A.3 | `apps/platform/app/Filament/Concerns/ResolvesPanelTenantContext.php` | Make admin-panel tenant context consumption route through the canonical resolved shell contract instead of ad hoc tenant lookup. |
|
||||
| A.4 | `apps/platform/tests/Unit/Support/OperateHub/OperateHubShellResolutionTest.php` and `apps/platform/tests/Unit/Support/Workspaces/WorkspaceContextRememberedTenantTest.php` | Add or extend unit coverage for route-first, Filament-tenant, remembered-tenant, tenantless, and invalid remembered-context branches. |
|
||||
|
||||
### Phase B - Align explicit scope mutation flows
|
||||
|
||||
**Goal**: Make switch, select, clear, restore, and intended-return behavior follow the same source hierarchy and fallback matrix.
|
||||
|
||||
| Step | File | Change |
|
||||
|------|------|--------|
|
||||
| B.1 | `apps/platform/app/Http/Controllers/SwitchWorkspaceController.php` and `apps/platform/app/Support/Workspaces/WorkspaceRedirectResolver.php` | Ensure workspace switching re-evaluates tenant compatibility deterministically, clears incompatible tenant state, and uses one redirect strategy after intended-URL consumption. |
|
||||
| B.2 | `apps/platform/app/Http/Controllers/SelectTenantController.php` | Keep explicit tenant selection as the only user-driven tenant activation flow on workspace-level pages, but align it with the canonical shell contract, selector-operability rules, and recovery semantics. |
|
||||
| B.3 | `apps/platform/app/Http/Controllers/ClearTenantContextController.php` and `apps/platform/app/Support/Tenants/TenantPageCategory.php` | Standardize tenant-clear recovery and route compatibility rules so tenant-required pages, workspace pages, evidence paths, and canonical record viewers resolve either to same-route tenantless workspace state or to the documented destinations `admin.workspace.managed-tenants.index`, `admin.evidence.overview`, `admin.operations.index`, `admin.operations.view`, or `admin.home` as appropriate. |
|
||||
| B.4 | `apps/platform/app/Support/Workspaces/WorkspaceIntendedUrl.php` | Reuse or slightly refine intended-URL handling so switch and recovery flows return to safe shell-compatible destinations only. |
|
||||
| B.5 | `apps/platform/tests/Feature/Workspaces/SwitchWorkspaceControllerTest.php`, `SwitchWorkspaceRedirectsToTenantRegistrationWhenNoTenantsTest.php`, `WorkspaceRedirectResolverTest.php`, `SelectTenantControllerTest.php`, and `ChooseWorkspacePageTest.php` | Cover workspace switch redirects, explicit tenant selection, workspace-independent chooser exceptions, tenant compatibility, and invalid or inaccessible context requests. |
|
||||
|
||||
### Phase C - Make the shell surfaces consume the contract
|
||||
|
||||
**Goal**: Reduce the shared shell UI to one truthful display and action entry point.
|
||||
|
||||
| Step | File | Change |
|
||||
|------|------|--------|
|
||||
| C.1 | `apps/platform/resources/views/filament/partials/context-bar.blade.php` | Remove partial-owned source precedence and make the topbar display and controls derive only from the canonical resolved shell context and explicit available actions. |
|
||||
| C.2 | `apps/platform/app/Providers/Filament/AdminPanelProvider.php` and `TenantPanelProvider.php` | Keep the shared render-hook strategy, but adjust only if needed so both panels consume the same shared shell contract without panel-specific truth drift. |
|
||||
| C.3 | `apps/platform/tests/Feature/Filament/WorkspaceContextTopbarAndTenantSelectionTest.php`, `OperationsIndexHeaderTest.php`, and `WorkspaceContextRecoveryDisplayTest.php` | Cover active tenant display, explicit tenantless display, stale or inaccessible remembered context clearing, and panel-consistent shell output. |
|
||||
|
||||
### Phase D - Harden page-category, middleware, and scope-safety behavior
|
||||
|
||||
**Goal**: Ensure that route type and access boundaries determine whether tenantless fallback, redirect, or 404 is correct.
|
||||
|
||||
| Step | File | Change |
|
||||
|------|------|--------|
|
||||
| D.1 | `apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php` | Replace ad hoc tenant-selection and navigation heuristics with canonical shell-context checks while preserving tenant-bound route enforcement and workspace isolation. |
|
||||
| D.2 | `apps/platform/app/Support/Tenants/TenantPageCategory.php` | Tighten route categorization only where current path-pattern rules are too implicit for the new invalid-context matrix, including the explicit `workspace_chooser_exception` and `tenant_scoped_evidence` cases. |
|
||||
| D.3 | `apps/platform/tests/Feature/Workspaces/GlobalContextShellContractTest.php` | Add focused feature coverage for missing workspace, missing tenant, invalid tenant, inaccessible tenant, tenant-bound route fallback, and workspace-scoped tenantless behavior. |
|
||||
|
||||
### Phase E - Close with regression protection and operator verification
|
||||
|
||||
**Goal**: Leave the repo with one documented shell contract, narrow regression coverage, and a clear manual validation path.
|
||||
|
||||
| Step | File | Change |
|
||||
|------|------|--------|
|
||||
| E.1 | Existing unit and feature suites listed above | Extend current tests instead of creating a browser-heavy new family. Keep resolution, redirect, and display assertions explicit in names and expectations. |
|
||||
| E.2 | `apps/platform/tests/Feature/Monitoring/OperationsDbOnlyRenderTest.php` and `apps/platform/tests/Feature/Monitoring/OperationsActionsEnqueueRunTest.php` | Keep shell-anchor workspace rendering DB-only and non-enqueuing so the contract does not widen runtime behavior accidentally. |
|
||||
| E.3 | `specs/199-global-context-shell-contract/quickstart.md` | Record implementation order, manual smoke steps, documented fallback targets, and exact verification commands for workspace switch, tenant select, tenant clear, invalid recovery, panel parity, and non-functional render proof. |
|
||||
| E.4 | `specs/199-global-context-shell-contract/tasks.md` | Break work into dependency-ordered tasks after this plan is accepted. |
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### D-001 - `WorkspaceContext` remains the storage owner, not the visible shell owner
|
||||
|
||||
The session-backed workspace and remembered-tenant state already live in `WorkspaceContext`. The right move is to keep it as the storage owner and validation seam, not replace it with persistence or a new framework.
|
||||
|
||||
### D-002 - One request-scoped shell contract must replace partial-owned precedence
|
||||
|
||||
The shell currently re-discovers scope inside Blade, middleware, and controllers. The plan centralizes that into one request-scoped resolved contract consumed everywhere else.
|
||||
|
||||
### D-003 - Route-bound tenant remains strongest on tenant-required surfaces
|
||||
|
||||
The existing effective precedence already treats route tenant as strongest, then validated panel tenant, then remembered tenant only on workspace-scoped pages. The plan keeps that precedence but makes it explicit and testable.
|
||||
|
||||
### D-004 - Tenant clear and invalid-context recovery need the same route-compatibility matrix
|
||||
|
||||
The product currently mixes previous-URL redirecting, silent remembered-context clearing, and 404 behavior. The plan aligns tenant clear and invalid recovery under one page-category-aware outcome matrix.
|
||||
|
||||
### D-005 - The context bar becomes a consumer and dispatcher only
|
||||
|
||||
The shared shell partial should show the resolved contract and expose explicit switch, select, and clear actions, but it should not be allowed to own a second context truth.
|
||||
@ -1,173 +0,0 @@
|
||||
# Quickstart: Global Context Shell Contract
|
||||
|
||||
## Goal
|
||||
|
||||
Implement Spec 199 by making workspace and tenant shell context resolve from one request-scoped contract, then verify that switch, select, clear, restore, and invalid-context flows all produce the same truth the shell displays.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Start the app stack:
|
||||
|
||||
```bash
|
||||
cd apps/platform && ./vendor/bin/sail up -d
|
||||
```
|
||||
|
||||
2. Confirm the working branch:
|
||||
|
||||
```bash
|
||||
git branch --show-current
|
||||
```
|
||||
|
||||
3. Keep the current scope of work bounded to the existing Laravel and Filament monolith under `apps/platform`.
|
||||
|
||||
## Recommended Implementation Order
|
||||
|
||||
1. **Canonicalize resolution first**
|
||||
|
||||
- Align `WorkspaceContext` and `OperateHubShell` so they can produce one resolved shell contract.
|
||||
- Make remembered context restore-only and remove any equal-ranking shell truth outside the resolved contract.
|
||||
|
||||
2. **Align explicit mutation flows second**
|
||||
|
||||
- Update `SwitchWorkspaceController`, `SelectTenantController`, `ClearTenantContextController`, and `WorkspaceRedirectResolver` to consume the same contract rules.
|
||||
- Keep safe intended-URL behavior via `WorkspaceIntendedUrl`.
|
||||
|
||||
3. **Convert shell surfaces third**
|
||||
|
||||
- Update `context-bar.blade.php` and any panel concern or middleware consumer to render only the resolved contract.
|
||||
- Preserve tenantless workspace behavior on routes that support it.
|
||||
|
||||
4. **Close with regression coverage**
|
||||
|
||||
- Extend current unit and feature seams before adding any new test family.
|
||||
- Use browser testing only if a client-only shell behavior appears that feature tests cannot observe.
|
||||
|
||||
## Context Source Inventory Owner
|
||||
|
||||
Keep the canonical source inventory in `specs/199-global-context-shell-contract/data-model.md` under `Context Source Inventory`. Any new source or fallback seam added during implementation must be recorded there before tasks are considered complete.
|
||||
|
||||
## Documented Recovery Destinations
|
||||
|
||||
- Missing or unrecoverable workspace truth goes to `/admin/choose-workspace`.
|
||||
- Generic workspace-safe recovery with no trustworthy prior route goes to `admin.operations.index`.
|
||||
- Tenant-scoped evidence cleanup goes to `admin.evidence.overview`.
|
||||
- Tenant-bound cleanup with a valid workspace goes to `admin.workspace.managed-tenants.index`.
|
||||
- Tenant-bound cleanup with no recoverable workspace goes to `admin.home`.
|
||||
- Tenantless-capable workspace routes and canonical workspace record viewers stay on their current route when entitlement remains valid.
|
||||
|
||||
## Explicit Page-Category Exceptions
|
||||
|
||||
- `/admin/choose-workspace` is the explicit `workspace_chooser_exception` route.
|
||||
- Tenant-scoped evidence paths under `/admin/evidence/...` except `/admin/evidence/overview` are explicit `tenant_scoped_evidence` routes for recovery purposes.
|
||||
|
||||
## Documented Workspace Switch Destinations
|
||||
|
||||
- A safe intended `/admin...` URL wins when it is still valid.
|
||||
- Workspaces with zero selectable tenants land on `admin.workspace.managed-tenants.index`.
|
||||
- Workspaces with multiple selectable tenants land on `/admin/choose-tenant`.
|
||||
- Workspaces with exactly one selectable tenant land on the tenant dashboard route under `/admin/t/{external_id}`.
|
||||
|
||||
## Focused Validation Commands
|
||||
|
||||
Run the narrowest commands that prove the contract:
|
||||
|
||||
```bash
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/OperateHub/OperateHubShellResolutionTest.php
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/Workspaces/WorkspaceContextRememberedTenantTest.php
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/WorkspaceContextTopbarAndTenantSelectionTest.php
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/WorkspaceContextRecoveryDisplayTest.php
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Workspaces/SelectTenantControllerTest.php
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Workspaces/ChooseTenantPageTest.php
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Workspaces/ChooseWorkspacePageTest.php
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Workspaces/ChooseWorkspaceRedirectsToChooseTenantTest.php
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Workspaces/SwitchWorkspaceControllerTest.php
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Workspaces/SwitchWorkspaceRedirectsToTenantRegistrationWhenNoTenantsTest.php
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Workspaces/WorkspaceSwitchUserMenuTest.php
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Workspaces/WorkspaceRedirectResolverTest.php
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Workspaces/GlobalContextShellContractTest.php
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Workspaces/WorkspacesResourceIsTenantlessTest.php
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Spec085/OperationsIndexHeaderTest.php
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Rbac/AdminGlobalSearchContextSafetyTest.php
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Rbac/TenantActionSurfaceConsistencyTest.php
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantRBAC/TenantSwitcherScopeTest.php
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Monitoring/OperationsDbOnlyRenderTest.php
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Monitoring/OperationsActionsEnqueueRunTest.php
|
||||
```
|
||||
|
||||
If new focused tests are added for Spec 199, run them directly as well. The two Monitoring commands above are the non-functional proof that the shell-anchor workspace surfaces remain DB-only and do not enqueue work while rendering.
|
||||
|
||||
## Formatting
|
||||
|
||||
Before closing the feature work:
|
||||
|
||||
```bash
|
||||
cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent
|
||||
```
|
||||
|
||||
## Manual Smoke Checklist
|
||||
|
||||
Use a simple 3-second timer for each first-look shell scan on the in-scope entry paths so the SC-001 review stays measurable.
|
||||
|
||||
### 1. Workspace-scoped tenantless entry
|
||||
|
||||
- Enter `/admin/operations` with a valid workspace and no active tenant.
|
||||
- Confirm the shell shows the workspace name and `No tenant selected` or the approved tenantless wording.
|
||||
- Confirm no stale tenant label appears.
|
||||
|
||||
### 2. Explicit tenant selection
|
||||
|
||||
- Select a valid active tenant from the shared shell surface.
|
||||
- Confirm the destination route and shell both show the same tenant.
|
||||
- Confirm the tenant belongs to the active workspace only.
|
||||
|
||||
### 3. Tenant clear from a workspace-scoped page
|
||||
|
||||
- Clear tenant context while on `/admin/operations` or another workspace-scoped page.
|
||||
- Confirm the shell becomes tenantless and the page remains valid.
|
||||
|
||||
### 4. Tenant clear from a tenant-bound page
|
||||
|
||||
- Clear tenant context while on a tenant-bound route.
|
||||
- Confirm the request does not leave the user in a half-valid tenant-bound route.
|
||||
- Confirm the redirect lands in `admin.workspace.managed-tenants.index` for the current workspace, or `admin.home` when no workspace truth remains.
|
||||
|
||||
### 4a. Tenant clear from a tenant-scoped evidence path
|
||||
|
||||
- Clear tenant context while on a tenant-scoped evidence path under `/admin/evidence/...`.
|
||||
- Confirm the redirect lands on `admin.evidence.overview` and no stale tenant label remains in the shell.
|
||||
|
||||
### 4b. Tenant clear from a canonical workspace record viewer
|
||||
|
||||
- Clear tenant context while on `/admin/operations/{run}`.
|
||||
- Confirm the request stays on `admin.operations.view` when entitlement remains valid and does not widen into a different route unnecessarily.
|
||||
|
||||
### 5. Invalid remembered tenant
|
||||
|
||||
- Seed a remembered tenant that is inaccessible, missing, or incompatible.
|
||||
- Confirm the remembered tenant is cleared automatically and does not reappear in the shell.
|
||||
|
||||
### 6. Workspace switch with stale tenant context
|
||||
|
||||
- Switch from one workspace to another where the prior tenant is not valid.
|
||||
- Confirm the shell clears tenant context or replaces it only after validation in the new workspace.
|
||||
|
||||
### 7. Workspace-independent chooser route
|
||||
|
||||
- Enter the workspace chooser flow without an active workspace.
|
||||
- Confirm the route remains available as an explicit exception and is not treated as a generic missing-workspace failure.
|
||||
|
||||
### 8. Admin versus tenant panel parity
|
||||
|
||||
- Resolve the same valid tenant scenario through `/admin` and `/admin/t/{external_id}`.
|
||||
- Confirm the shared shell displays the same active truth and does not expose a competing panel-owned context label.
|
||||
|
||||
## Done Signal
|
||||
|
||||
Spec 199 is implementation-ready when:
|
||||
|
||||
- one resolved shell contract governs display and route behavior,
|
||||
- switch, select, clear, restore, and invalid recovery follow one shared rule set,
|
||||
- the shared shell renders only the resolved truth,
|
||||
- targeted unit and feature tests pass,
|
||||
- timed manual smoke checks confirm tenantless and tenant-scoped behavior are both explicit and understandable.
|
||||
@ -1,81 +0,0 @@
|
||||
# Research: Global Context Shell Contract
|
||||
|
||||
## Decision 1 - Keep `WorkspaceContext` as the session-backed storage owner, but not as a competing visible shell truth
|
||||
|
||||
- **Decision**: `WorkspaceContext` remains the owner of `current_workspace_id`, `workspace_intended_url`, and `workspace_last_tenant_ids`, while one request-scoped shell contract becomes the only visible and consumable truth for the active workspace and tenant.
|
||||
- **Rationale**: The current repo already uses `WorkspaceContext` correctly as the place where session-backed workspace and remembered-tenant state live. The ambiguity comes from visible shell truth being recomputed separately in `OperateHubShell`, middleware, controllers, and `context-bar.blade.php`, not from the storage location itself.
|
||||
- **Alternatives considered**:
|
||||
- Replace `WorkspaceContext` with a new persistence model: rejected because the spec explicitly disallows new persisted truth.
|
||||
- Leave `WorkspaceContext` and `OperateHubShell` as parallel visible truths: rejected because that preserves the ambiguity Spec 199 exists to remove.
|
||||
|
||||
## Decision 2 - Preserve the current effective tenant precedence, but make it explicit and shared
|
||||
|
||||
- **Decision**: On admin-shell surfaces, active tenant resolution remains: valid route tenant first, then explicit tenant selection, then a validated query-backed tenant hint only on workspace-scoped routes that explicitly allow it, then validated `Filament::getTenant()`, then remembered tenant only on workspace-scoped pages, then explicit tenantless fallback. Tenant-bound pages do not accept query hints or remembered tenant fallback as normal active truth.
|
||||
- **Rationale**: This is already the effective behavior in `OperateHubShell::resolveActiveTenant()`, and it matches the repo's workspace-first intent while keeping route-bound tenant requirements strongest where the route semantics demand them.
|
||||
- **Alternatives considered**:
|
||||
- Make remembered tenant equal to route or panel tenant: rejected because remembered context must remain support-only.
|
||||
- Make Filament tenant always win even when route tenant is explicit: rejected because tenant-bound routes need explicit route authority.
|
||||
|
||||
## Decision 3 - Convert the context bar into a pure consumer and dispatcher of the resolved contract
|
||||
|
||||
- **Decision**: `context-bar.blade.php` should stop owning resolution rules and should render only the already resolved workspace and tenant state plus the explicit switch, select, and clear actions that mutate shell scope.
|
||||
- **Rationale**: The current partial queries `WorkspaceContext`, `OperateHubShell`, `Filament::getTenant()`, route name, query tenant, and `TenantPageCategory` in one Blade file. That makes the shell UI a second source of truth and hides business rules in a rendering surface.
|
||||
- **Alternatives considered**:
|
||||
- Keep the partial as-is and only tweak labels: rejected because the issue is structural, not cosmetic.
|
||||
- Move all logic into Alpine or client-side state: rejected because the authoritative context is server-side and request-scoped.
|
||||
|
||||
## Decision 4 - Invalid-context recovery must be explicit and page-category-aware
|
||||
|
||||
- **Decision**: Missing workspace, missing tenant, incompatible tenant, inaccessible tenant, and invalid remembered context should map to explicit recovery outcomes that depend on route type and page category rather than on ad hoc previous-URL heuristics.
|
||||
- **Rationale**: The current repo mixes silent remembered-context clearing, page-category redirect logic in `ClearTenantContextController`, and 404 behavior in middleware and route guards. That inconsistency is the operator-facing confusion Spec 199 is meant to remove.
|
||||
- **Alternatives considered**:
|
||||
- Always 404 on any invalid tenant state: rejected because workspace-scoped pages intentionally support a valid tenantless state.
|
||||
- Always redirect to `/admin/operations`: rejected because tenant-bound routes and canonical record viewers need different recovery behavior.
|
||||
|
||||
## Decision 5 - Keep the solution in the current support layer instead of introducing a generic context engine
|
||||
|
||||
- **Decision**: The implementation should stay inside the existing support layer around `WorkspaceContext`, `OperateHubShell`, middleware, and controllers. If a new runtime object is needed, it should be a narrow request-scoped result structure only.
|
||||
- **Rationale**: The repo already has the needed building blocks. The problem is coordination and precedence, not lack of extension points.
|
||||
- **Alternatives considered**:
|
||||
- New multi-panel context framework with registries, strategies, or factories: rejected under ABSTR-001 and PROP-001.
|
||||
- Panel-specific duplicated fixes: rejected because the same shell contract spans both admin and tenant panels.
|
||||
|
||||
## Decision 6 - Reuse existing unit and feature seams as the primary proof strategy
|
||||
|
||||
- **Decision**: The proving default for Spec 199 is unit coverage around runtime resolution and feature coverage around controllers, middleware, and rendered shell surfaces. Browser automation stays optional and secondary.
|
||||
- **Rationale**: The current repo already has targeted tests for remembered-tenant invalidation, context-bar display, choose-tenant behavior, redirect resolution, and clear-tenant fallbacks. Extending these seams is cheaper and more precise than introducing a new browser-heavy shell suite.
|
||||
- **Alternatives considered**:
|
||||
- Browser-first shell regression family: rejected because the contract is server-driven and the narrowest sufficient proof already exists in unit and feature seams.
|
||||
- Manual-only verification: rejected because Spec 199 changes request-time contract behavior that should be regression-protected.
|
||||
|
||||
## Decision 7 - Preserve panel topology and cross-plane boundaries unchanged
|
||||
|
||||
- **Decision**: `/admin` and `/admin/t/{external_id}` continue to share the `web` guard and shared shell contract, while `/system` remains out of scope and isolated under the `platform` guard.
|
||||
- **Rationale**: The spec is about global admin and tenant shell truth, not about re-cutting panel or guard boundaries.
|
||||
- **Alternatives considered**:
|
||||
- Fold tenant-panel behavior into `/admin` only: rejected because tenant-panel-native routing and tenancy already exist and remain valid.
|
||||
- Expand the feature to `/system`: rejected because cross-plane behavior is a different product boundary.
|
||||
|
||||
## Decision 8 - Keep global search tenant-safe under the new shell contract without changing searchable resources
|
||||
|
||||
- **Decision**: The shell contract must preserve existing tenant-safe and workspace-safe global search behavior, but it does not introduce or remove searchable resources.
|
||||
- **Rationale**: A context contract that widens tenant-owned global search results on workspace-scoped surfaces would violate the existing RBAC and tenant isolation guarantees.
|
||||
- **Alternatives considered**:
|
||||
- Ignore global search as unrelated: rejected because the resolved shell context influences whether tenant-owned search results are safe to return.
|
||||
- Expand searchable resource scope during shell cleanup: rejected because the spec is about context truth, not search surface growth.
|
||||
|
||||
## Decision 9 - Keep the explicit source inventory in the feature data model artifact
|
||||
|
||||
- **Decision**: The canonical source inventory for Spec 199 lives in `data-model.md` under `Context Source Inventory`, not in controller comments or scattered plan prose.
|
||||
- **Rationale**: The feature needs one maintained place that lists every in-scope source, its source role, the seam that owns it, and the validation boundary it must pass.
|
||||
- **Alternatives considered**:
|
||||
- Keep the source inventory implicit in the plan only: rejected because task generation and implementation reviews need a concrete artifact that can be updated without re-reading the entire plan narrative.
|
||||
- Split the inventory across multiple code comments: rejected because that would recreate the same drift problem inside the documentation layer.
|
||||
|
||||
## Decision 10 - Document fallback destinations from the current route families instead of inventing abstract recovery targets
|
||||
|
||||
- **Decision**: The contract documents the existing workspace-safe fallback routes used by the product today: `admin.operations.index`, `admin.evidence.overview`, `admin.workspace.managed-tenants.index`, `admin.home`, and route-stable workspace record viewers where they remain valid.
|
||||
- **Rationale**: The repo already has concrete fallback behavior in `ClearTenantContextController`, `WorkspaceRedirectResolver`, and route families around monitoring, evidence, and tenant selection. The contract should reflect that real product behavior instead of describing a generic fallback abstraction.
|
||||
- **Alternatives considered**:
|
||||
- Keep fallback wording abstract as “workspace-level fallback”: rejected because that leaves implementers and reviewers guessing about the actual destination.
|
||||
- Collapse all recovery into `/admin/operations`: rejected because tenant-bound and evidence-specific flows already use more precise workspace-safe landings.
|
||||
@ -1,356 +0,0 @@
|
||||
# Feature Specification: Global Context Shell Contract
|
||||
|
||||
**Feature Branch**: `199-global-context-shell-contract`
|
||||
**Created**: 2026-04-18
|
||||
**Status**: Proposed
|
||||
**Input**: User description: "Spec 199 — Global Context Shell Contract"
|
||||
|
||||
## Spec Candidate Check *(mandatory — SPEC-GATE-001)*
|
||||
|
||||
- **Problem**: The admin and tenant shell currently derive active workspace and tenant scope from a mix of route input, query hints, panel tenant state, session-backed workspace context, remembered tenant values, and shell-local rendering logic instead of one explicit product contract.
|
||||
- **Today's failure**: Operators and developers cannot reliably answer the same basic question across the shell: what scope is actually active, what a switch or clear flow will do, whether a deeplink is authoritative, and which fallback wins when context inputs disagree or become invalid.
|
||||
- **User-visible improvement**: Operators see one clear workspace-first shell truth, one predictable tenant truth inside that workspace, explicit tenantless states, and consistent switch, clear, restore, and fallback behavior across shared shell surfaces.
|
||||
- **Smallest enterprise-capable version**: Map the current context sources and shell entry points, define one resolved context contract for workspace and tenant, align the context bar and switch/clear flows to that contract, add focused regression coverage for resolution and fallback, and document what remains page-state or constitution work.
|
||||
- **Explicit non-goals**: No page-level tab/filter/inspect-state contract, no generic Filament nativity cleanup, no global navigation or IA rewrite, no detail micro-UI redesign, no new generic context platform engine, and no product-wide authorization redesign beyond shell context visibility and enforcement boundaries.
|
||||
- **Permanent complexity imported**: A bounded shell-context taxonomy, an explicit source-of-truth hierarchy, documented switch/select/clear/fallback rules, a shared shell vocabulary for workspace and tenant state, and focused regression tests around those rules.
|
||||
- **Why now**: The repo already has multiple context sources and shared shell surfaces across `/admin` and `/admin/t/{external_id}`. Adjacent specs intentionally leave this global shell context layer unresolved, so additional work will keep reintroducing drift unless the contract is cut now.
|
||||
- **Why not local**: Local fixes inside one partial, one controller, or one panel would reduce a symptom, but would keep competing truths alive and would not give operators or reviewers a single product contract for scope resolution.
|
||||
- **Approval class**: Core Enterprise
|
||||
- **Red flags triggered**: Cross-panel contract breadth and source-of-truth consolidation. Defense: the scope is tightly bounded to workspace and tenant shell truth, introduces no new persistence, and explicitly excludes broader navigation, page-state, and micro-UI cleanup work.
|
||||
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexität: 1 | Produktnähe: 2 | Wiederverwendung: 2 | **Gesamt: 11/12**
|
||||
- **Decision**: approve
|
||||
|
||||
## Spec Scope Fields *(mandatory)*
|
||||
|
||||
- **Scope**: workspace
|
||||
- **Primary Routes**:
|
||||
- `/admin`
|
||||
- `/admin/choose-workspace`
|
||||
- `/admin/choose-tenant`
|
||||
- `/admin/switch-workspace`
|
||||
- `/admin/select-tenant`
|
||||
- `/admin/clear-tenant-context`
|
||||
- `/admin/t/{external_id}/...`
|
||||
- Shared shell surfaces rendered on the admin and tenant panels
|
||||
- **Data Ownership**:
|
||||
- This feature introduces no new tables, persisted entities, or storage truth.
|
||||
- It standardizes the shell contract that governs how workspace-owned context and tenant-owned scope visibility are resolved on existing surfaces.
|
||||
- Remembered workspace and tenant values remain convenience state only; they do not become independent product records.
|
||||
- **RBAC**:
|
||||
- Workspace membership is required before a workspace can become the active shell context.
|
||||
- Tenant membership is required before a tenant can become the active tenant context.
|
||||
- Non-membership remains deny-as-not-found and capability checks inside an established scope remain server-side authorization concerns.
|
||||
|
||||
For canonical-view specs, the spec MUST define:
|
||||
|
||||
- **Default filter behavior when tenant-context is active**: Workspace-level pages may opt into an explicit tenant prefilter, but the shell contract itself must never inject hidden page-local filters. Tenant-bound routes remain explicitly tenant-scoped.
|
||||
- **Explicit entitlement checks preventing cross-tenant leakage**: Route, query, remembered, and switch requests must pass workspace and tenant entitlement checks before they become active shell context. Invalid or inaccessible context requests must be discarded without leaking unavailable tenant truth.
|
||||
|
||||
## Decision-First Surface Role *(mandatory when operator-facing surfaces are changed)*
|
||||
|
||||
| Surface | Decision Role | Human-in-the-loop Moment | Immediately Visible for First Decision | On-Demand Detail / Evidence | Why This Is Primary or Why Not | Workflow Alignment | Attention-load Reduction |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Global context bar / shell header | Secondary Context Surface | Confirm or change current scope before navigating, reviewing, or mutating anything else | Active workspace, active tenant or explicit tenantless state, current shell scope, available switch affordance | Available workspaces, available tenants, and any intentionally surfaced recovery hint | Not primary because it frames decisions across the product rather than owning a domain-specific decision queue | Aligns `/admin` and `/admin/t/{external_id}` entry points around one scope model | Removes repeated “where am I?” reconstruction and hidden scope drift between pages |
|
||||
| Context recovery shell state | Secondary Context Surface | Recover from missing, invalid, inaccessible, or incompatible context before work continues | Missing or invalid scope state, actual fallback scope, and the next required action | Optional explanation of why a requested or remembered context was discarded | Not primary because it is a recovery state of the shell contract, not a business workbench | Aligns missing-context behavior instead of letting each page improvise its own fallback | Prevents confusing half-active scope states and reduces retry-based navigation |
|
||||
|
||||
## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)*
|
||||
|
||||
| Surface | Action Surface Class | Surface Type | Likely Next Operator Action | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type / Justification |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Global context bar / shell header | Navigation / Context / Shell | Global scope switcher | Confirm current scope or intentionally switch workspace or tenant | One inline shell context strip with one menu-driven switch model | forbidden | Inside the context controls only | none; tenant clear is a scope-reset action, not a data-destructive action | `/admin` and `/admin/t/{external_id}` shared shell | none | Active workspace, active tenant or `No tenant selected`, current panel scope | Context / workspace / tenant | The single resolved workspace and tenant truth governing the shell right now | none |
|
||||
| Context recovery shell state | Navigation / Context / Recovery | Missing or invalid scope recovery | Choose a valid workspace, return to a workspace-level route, or clear invalid tenant context | One inline recovery state in the same shell contract | forbidden | Recovery controls inside the shell state only | none | `/admin` and `/admin/t/{external_id}` shared shell | none | Missing workspace, missing tenant, invalid request, incompatible tenant, inaccessible tenant | Context recovery | Why the requested scope cannot be honored and what valid scope remains | none |
|
||||
|
||||
## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)*
|
||||
|
||||
| Surface | Primary Persona | Decision / Operator Action Supported | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Global context bar / shell header | Workspace operator, tenant operator | Confirm current scope and intentionally switch workspace or tenant | Shell context strip | Where am I operating right now, and can I safely change scope? | Active workspace, active tenant or explicit tenantless state, current panel scope, switch affordances | Why a requested context was ignored, which remembered value was eligible, and any compatibility note that must stay secondary | workspace presence, tenant presence, validity, source resolution | context only | Switch workspace, Select tenant, Clear tenant context | none |
|
||||
| Context recovery shell state | Workspace operator, tenant operator | Recover from missing, invalid, inaccessible, or incompatible context | Shell recovery prompt | Why is this scope unavailable, and what is the valid next step? | Missing or invalid state, current fallback scope, next valid action | Discarded request reason and any intentionally surfaced restore candidate | missing, invalid, inaccessible, incompatible, tenantless | context only | Choose workspace, Return to workspace-level page, Clear tenant request | none |
|
||||
|
||||
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||
|
||||
- **New source of truth?**: yes
|
||||
- **New persisted entity/table/artifact?**: no
|
||||
- **New abstraction?**: no
|
||||
- **New enum/state/reason family?**: yes
|
||||
- **New cross-domain UI framework/taxonomy?**: no
|
||||
- **Current operator problem**: Operators and reviewers face multiple competing answers for current scope, fallback, and restore behavior, so shell context cannot be trusted as a single product truth.
|
||||
- **Existing structure is insufficient because**: The current shell can derive active context from route input, query hints, session state, panel state, and remembered values. Local cleanup in one place cannot define one shared switch, clear, restore, and fallback contract across panels.
|
||||
- **Narrowest correct implementation**: Define one resolved shell-context contract over existing workspace and tenant context behavior, explicitly classify requested, remembered, resolved, tenantless, and invalid states, and align current shell surfaces and entry flows to it without new persistence or a generic engine.
|
||||
- **Ownership cost**: The repo gains one source hierarchy to maintain, one shared shell vocabulary to preserve, and focused feature tests for context resolution, switch, clear, restore, and invalid-state handling.
|
||||
- **Alternative intentionally rejected**: Local partial-only cleanup was rejected because it would preserve competing truths. A broader generic multi-panel context framework was rejected because it would import unnecessary abstraction for a bounded shell problem.
|
||||
- **Release truth**: current-release operator truth and shell reliability
|
||||
|
||||
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||
|
||||
- **Test purpose / classification**: Unit + Feature
|
||||
- **Validation lane(s)**: fast-feedback, confidence
|
||||
- **Why this classification and these lanes are sufficient**: The change spans two proving purposes: unit tests for canonical source precedence, remembered-context invalidation, and page-category decisions in the support layer, plus feature tests for controller flows, middleware behavior, route recovery, and shared shell rendering. Together they prove runtime behavior without escalating to browser or heavy-governance lanes by default.
|
||||
- **New or expanded test families**: Focused unit shell-context resolution tests, workspace-switch and tenant-selection controller tests, explicit workspace-chooser exception tests, and shared-shell display tests for admin and tenant panel entry paths.
|
||||
- **Fixture / helper cost impact**: Minimal workspace, tenant, membership, entitlement, session, and remembered-context setup. No new providers, seeds, heavy browser harness, or long-running runtime fixtures are required.
|
||||
- **Heavy-family visibility / justification**: none
|
||||
- **Reviewer handoff**: Reviewers must confirm that coverage remains feature-level, that shell rendering is not accidentally pushed into a broad browser family, that invalid-context fallbacks are proven, and that the minimal commands below are enough to verify the contract.
|
||||
- **Budget / baseline / trend impact**: Minor increase in focused feature-test runtime only.
|
||||
- **Escalation needed**: none
|
||||
- **Planned validation commands**:
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=GlobalContext`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=WorkspaceSwitch`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=TenantContext`
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - See The True Current Scope (Priority: P1)
|
||||
|
||||
As an operator, I want every shared shell surface to show the real active workspace and tenant state so I can trust where my next action will apply.
|
||||
|
||||
**Why this priority**: If the shell cannot answer the active scope question clearly, every downstream page remains harder to trust.
|
||||
|
||||
**Independent Test**: Open workspace-level and tenant-bound entry paths with valid and tenantless scenarios, then verify that the shell always shows the same resolved scope truth the target page is using.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a valid workspace and tenant context are active, **When** the shell renders on a shared admin or tenant panel surface, **Then** it shows that resolved workspace and tenant consistently.
|
||||
2. **Given** a workspace-level page is active with no tenant selected, **When** the shell renders, **Then** it shows an explicit tenantless state instead of implying that a hidden tenant is still active.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Switch Workspace Without Stale Tenant Truth (Priority: P1)
|
||||
|
||||
As an operator, I want workspace switching to deterministically resolve what happens to tenant context so I do not carry a stale tenant into the wrong workspace or page.
|
||||
|
||||
**Why this priority**: Workspace is the primary scope. If workspace switching is ambiguous, the entire workspace-first model breaks down.
|
||||
|
||||
**Independent Test**: Start from a valid workspace and tenant, switch to a different workspace with compatible and incompatible tenant conditions, and verify the resulting scope, fallback, and destination page.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** an operator switches to a different workspace and the prior tenant is not valid in that workspace, **When** the switch resolves, **Then** the shell clears tenant context and lands in a valid workspace-scoped state.
|
||||
2. **Given** an operator switches to a workspace where a requested or remembered tenant is valid and allowed for the target surface, **When** the switch resolves, **Then** that tenant becomes active only after validation within the new workspace.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Select Or Clear Tenant Intentionally (Priority: P1)
|
||||
|
||||
As an operator, I want tenant selection and tenant clearing to behave like explicit scope decisions so I always understand whether I am entering tenant scope or returning to workspace-only scope.
|
||||
|
||||
**Why this priority**: Tenant is secondary but operationally critical. The select and clear flows are the most visible scope-changing actions in the shell.
|
||||
|
||||
**Independent Test**: Select a tenant from the shell, clear it from a workspace-level page, and clear it from a tenant-bound route to verify that the resulting shell truth and destination are deterministic.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** an operator selects a valid tenant inside the active workspace, **When** the shell resolves the selection, **Then** the active tenant becomes that tenant and the shell shows the same tenant on the target page.
|
||||
2. **Given** an operator clears tenant context while on a tenant-required route, **When** the clear flow resolves, **Then** the system redirects to the documented workspace-level fallback and the shell shows no active tenant.
|
||||
|
||||
---
|
||||
|
||||
### User Story 4 - Reject Invalid Or Stale Context Cleanly (Priority: P1)
|
||||
|
||||
As an operator, I want invalid, inaccessible, or stale requested and remembered context to fail cleanly so I do not operate under a false scope illusion.
|
||||
|
||||
**Why this priority**: Invalid context handling is where competing truths most often become visible and dangerous.
|
||||
|
||||
**Independent Test**: Enter the shell with invalid route, query, and remembered context combinations, then verify that the shell discards invalid inputs, lands in a valid fallback state, and never leaves stale scope indicators behind.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a route or query requests a tenant that does not belong to the resolved workspace, **When** the shell resolves context, **Then** that tenant request is rejected and the shell falls back to a valid workspace-scoped state.
|
||||
2. **Given** a remembered tenant is no longer accessible or compatible, **When** the shell restores context, **Then** the remembered tenant is ignored and the operator sees an explicit valid fallback state.
|
||||
|
||||
---
|
||||
|
||||
### User Story 5 - Keep Shared Shell Logic Consistent Across Panels (Priority: P2)
|
||||
|
||||
As a reviewer, I want admin and tenant panel entry paths that share the shell contract to resolve context by the same rules so extensions do not create panel-specific truths.
|
||||
|
||||
**Why this priority**: The main risk is drift between shared shell surfaces and panel-specific context assumptions.
|
||||
|
||||
**Independent Test**: Resolve the same entitled workspace and tenant scenario through the workspace-level shell and tenant-bound shell entry paths, then verify that both surfaces display the same active truth and compatible fallback behavior.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** the same workspace and tenant are active through different valid entry paths, **When** the shared shell renders, **Then** both panels show the same resolved scope truth.
|
||||
2. **Given** a panel-specific context source disagrees with the resolved shell contract, **When** the shell renders, **Then** the panel-specific source is treated as supporting data only and does not become a competing visible truth.
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- A route or query requests a tenant that belongs to a different workspace than the resolved workspace.
|
||||
- Session-backed workspace state and requested workspace state disagree on first load.
|
||||
- A remembered tenant exists for the prior workspace but not for the newly selected workspace.
|
||||
- A tenant is cleared while the current page requires tenant scope.
|
||||
- The explicit workspace chooser route is entered without workspace context and must not be mistaken for a generic missing-workspace error.
|
||||
- A tenant becomes inaccessible between selection and the next request.
|
||||
- A shell recovery state must distinguish between missing workspace, missing tenant, invalid tenant, and inaccessible tenant.
|
||||
- Shared shell display and underlying page behavior disagree unless one explicit contract prevents the drift.
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
**Constitution alignment (required):** This feature introduces no new Microsoft Graph calls, no new persistent storage, and no new queued or scheduled operational workflow. It standardizes shell context resolution, display, and context-changing flows on existing surfaces.
|
||||
|
||||
**Constitution alignment (PROP-001 / ABSTR-001 / PERSIST-001 / STATE-001 / BLOAT-001):** This feature introduces one bounded source-of-truth hierarchy and one bounded state taxonomy for global shell context. It does not introduce new persistence or a generic context framework. The proportionality review above explains why local cleanup is insufficient and why broader abstraction is rejected.
|
||||
|
||||
**Constitution alignment (TEST-GOV-001):** This feature changes runtime behavior and targeted tests. The proving purpose is feature-level validation of shell resolution, switch, clear, restore, and fallback behavior. The narrowest sufficient lanes are fast-feedback and confidence. No new browser or heavy-governance family is justified, fixture cost remains limited to workspace, tenant, membership, and session state, and reviewers must treat accidental escalation beyond those bounds as a merge blocker.
|
||||
|
||||
**Constitution alignment (OPS-UX):** Existing `OperationRun` behavior remains unchanged. The feature does not create or rename run types, change service-owned run transitions, or alter progress or notification contracts.
|
||||
|
||||
**Constitution alignment (RBAC-UX):** The feature affects workspace-context routes on `/admin`, tenant-context routes on `/admin/t/{external_id}`, and shared shell context display. Non-members remain deny-as-not-found. Members without required capability remain authorization failures only after workspace and tenant entitlement are established. Context resolution must never surface inaccessible workspace or tenant truth, and global search must remain tenant-safe under the resolved shell contract. Positive and negative authorization regression coverage must prove that shell cleanup does not relax these rules.
|
||||
|
||||
**Constitution alignment (OPS-EX-AUTH-001):** Not applicable. The feature does not add synchronous authentication-handshake behavior.
|
||||
|
||||
**Constitution alignment (BADGE-001):** The feature does not introduce a new badge language. Existing status and severity badges remain centrally defined and must not be remapped ad hoc as part of the shell-context work.
|
||||
|
||||
**Constitution alignment (UI-FIL-001):** The feature must rely on existing panel shell surfaces, native Filament controls, and shared shell primitives already used by the repo. It must not introduce a new local status language or a second context widget family beside the shared shell contract.
|
||||
|
||||
**Constitution alignment (UI-NAMING-001):** Operator vocabulary must stay domain-first and consistent across shell labels, action labels, notifications, and recovery copy. Canonical terms include `Workspace`, `Tenant`, `No tenant selected`, `Switch workspace`, `Select tenant`, `Clear tenant context`, and `Context unavailable`.
|
||||
|
||||
**Constitution alignment (DECIDE-001):** The affected shell surfaces are secondary context surfaces. They exist to make the next operator decision trustworthy by showing current scope and offering explicit scope changes, not by becoming a separate workbench.
|
||||
|
||||
**Constitution alignment (UI-CONST-001 / UI-SURF-001 / ACTSURF-001 / UI-HARD-001 / UI-EX-001 / UI-REVIEW-001 / HDR-001):** The shared shell must expose one primary context display and switch model, one recovery model, explicit placement of scope-reset actions, and one canonical vocabulary for workspace and tenant truth. It must avoid competing header, modal, or partial-owned context truths.
|
||||
|
||||
**Constitution alignment (ACTSURF-001 - action hierarchy):** Context display, scope selection, and scope recovery must remain separated from destructive data actions and from unrelated page-local actions. Tenant clear remains a context-reset action grouped with tenant controls, not a destructive record action.
|
||||
|
||||
**Constitution alignment (OPSURF-001):** Default-visible shell content must stay operator-first: active workspace, tenant state, validity, and the next valid context action. Any diagnostic explanation of discarded requests or remembered candidates must stay secondary and only appear where necessary.
|
||||
|
||||
**Constitution alignment (UI-SEM-001 / LAYER-001 / TEST-TRUTH-001):** The feature replaces competing shell truth with one resolved context model and a bounded state vocabulary. It must not add redundant presenter or explanation layers, and tests must focus on visible outcomes such as displayed scope, redirects, clears, and invalid-state fallback.
|
||||
|
||||
**Constitution alignment (Filament Action Surfaces):** The Action Surface Contract is satisfied when the shared shell uses exactly one resolved context source for display, uses the same contract for switch, select, and clear flows, and avoids redundant alternate context widgets or empty grouped action placeholders. The UI Action Matrix below documents the shell surfaces.
|
||||
|
||||
**Constitution alignment (UX-001 — Layout & Information Architecture):** This feature changes shell context behavior, not page form architecture. Shared shell surfaces must remain calm, concise, and explicit, and missing-context recovery must not overload the header with unrelated information.
|
||||
|
||||
### Global Context Taxonomy
|
||||
|
||||
- **Requested Context**: Workspace or tenant input requested by route, query, explicit switch/select action, or other documented entry path before validation.
|
||||
- **Resolved Workspace Context**: The one validated workspace scope that the shell and workspace-bound pages consume.
|
||||
- **Resolved Tenant Context**: The one validated tenant scope inside the resolved workspace, or the explicit tenantless state when no tenant is active.
|
||||
- **Remembered Context**: Non-leading convenience state used only as a restore candidate after validation.
|
||||
- **Invalid / Unavailable Context**: Requested or remembered workspace or tenant input that cannot be honored because it is missing, inaccessible, incompatible, or no longer valid.
|
||||
- **Tenantless Workspace State**: A valid workspace-scoped state where no tenant is active and the shell must say so explicitly.
|
||||
|
||||
### Context Source Hierarchy
|
||||
|
||||
| Context facet | Leading source | Supporting sources | Never-leading sources |
|
||||
|---|---|---|---|
|
||||
| Active workspace | Valid requested workspace for the current entry flow, otherwise the validated current workspace state | Remembered last workspace only when there is no active session workspace and the restore rule explicitly allows it | View-local fallbacks, raw query hints outside documented flows, raw tenant-panel state |
|
||||
| Active tenant | Valid tenant required by the current route or explicitly selected by the operator inside the resolved workspace | Query-backed tenant hints only on explicitly allowed workspace-scoped routes after validation, then remembered tenant for the resolved workspace only when no higher-priority tenant is active and the target surface allows restore | Tenant from a different workspace, stale remembered tenant, partial-local shell state, raw panel tenant state treated as an independent truth |
|
||||
| Shell display | The resolved context model only | none | Any partial-owned inference, convenience hint, or stale page-local context that is not part of the resolved context |
|
||||
|
||||
### Context Source Inventory Ownership
|
||||
|
||||
- The canonical inventory of in-scope workspace and tenant context sources is maintained in `data-model.md` under `Context Source Inventory`.
|
||||
- Any new source that can influence shell context must be added there with its source role, owning seam, and validation note before implementation or cleanup work lands.
|
||||
|
||||
### Context Flow Summary
|
||||
|
||||
| Flow | Trigger | Required validation | Allowed outcome | Forbidden outcome |
|
||||
|---|---|---|---|---|
|
||||
| Workspace switch | Explicit shell action | Workspace entitlement, route compatibility, tenant compatibility re-evaluation | Resolved target workspace with valid tenant or explicit tenantless state | Carrying an incompatible tenant silently into the new workspace |
|
||||
| Tenant select | Explicit shell action or required tenant entry route | Tenant entitlement, membership in resolved workspace, route compatibility | Resolved tenant inside the active workspace | Tenant becoming active without a valid workspace or despite incompatibility |
|
||||
| Tenant clear | Explicit shell action | Current route compatibility with tenantless state | Valid tenantless workspace state or redirect to workspace-level fallback | Remaining on a tenant-required route with no valid tenant |
|
||||
| Restore | First load or eligible return flow | Restore rule, workspace compatibility, tenant compatibility, entitlement | Valid restored workspace or tenant context | Remembered context overriding an explicit current truth or reviving invalid scope |
|
||||
| Invalid-context recovery | Any invalid request or stale remembered state | Missing, inaccessible, incompatible, or unauthorized determination | Clear fallback state and explicit recovery path | Hidden failure or stale shell scope display |
|
||||
|
||||
### Documented Workspace-Safe Fallbacks
|
||||
|
||||
| Situation | Required fallback |
|
||||
|---|---|
|
||||
| External previous URL, missing referrer, or clear-flow sentinel path | `/admin/operations` via `admin.operations.index` |
|
||||
| Missing or unrecoverable workspace truth at shell entry or restore time | `/admin/choose-workspace` |
|
||||
| Tenant-bound route under `/admin/t/{external_id}/...` or `/admin/tenants/...` with a valid current workspace | `admin.workspace.managed-tenants.index` for the current workspace |
|
||||
| Tenant-bound route cleanup after workspace truth is no longer available | `/admin` via `admin.home` |
|
||||
| Tenant-scoped evidence path under `/admin/evidence/...` except `/admin/evidence/overview` | `/admin/evidence/overview` via `admin.evidence.overview` |
|
||||
| Workspace-scoped route that is tenantless-capable | Remain on the same route in explicit tenantless state |
|
||||
| Canonical workspace record viewer under `/admin/operations/{run}` with valid entitlement | Remain on the same viewer route in explicit tenantless or tenant-scoped state, whichever the resolved contract allows |
|
||||
|
||||
### Explicit Page-Category Exceptions
|
||||
|
||||
- `/admin/choose-workspace` is the explicit `workspace_chooser_exception` route. It remains reachable without an established workspace and must not be inferred from generic missing-workspace behavior.
|
||||
- Tenant-scoped evidence paths under `/admin/evidence/...` except `/admin/evidence/overview` are explicit `tenant_scoped_evidence` routes for recovery purposes and must fall back to `admin.evidence.overview`.
|
||||
|
||||
### Documented Workspace Switch Destinations
|
||||
|
||||
| Switch outcome | Destination |
|
||||
|---|---|
|
||||
| Safe intended return inside `/admin...` | Intended URL wins when still valid for the resolved workspace |
|
||||
| Resolved workspace with zero selectable tenants | `admin.workspace.managed-tenants.index` |
|
||||
| Resolved workspace with multiple selectable tenants | `/admin/choose-tenant` |
|
||||
| Resolved workspace with exactly one selectable tenant | Tenant dashboard route under `/admin/t/{external_id}` |
|
||||
|
||||
### Assumptions and Dependencies
|
||||
|
||||
- The product remains workspace-first: workspace context is the primary shell scope and tenant context stays subordinate to it.
|
||||
- Existing admin and tenant panels continue to share shell-level context surfaces rather than splitting into unrelated context systems.
|
||||
- Existing entitlement checks for workspace membership and tenant membership remain the security boundary the shell contract must respect.
|
||||
- Current-release workspace-independent exception coverage is limited to the workspace chooser flow; it must stay explicit and must not be inferred from a generic missing-workspace failure path.
|
||||
- Page-level tab, filter, inspect, and draft/apply semantics remain governed by the separate monitoring page-state contract unless this spec explicitly takes ownership.
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-199-001 Source inventory**: The product MUST maintain one explicit inventory of all workspace and tenant context sources that can affect the shared shell contract, and that inventory MUST remain the canonical feature artifact for source roles and ownership.
|
||||
- **FR-199-002 Single resolved truth**: The shell MUST expose exactly one resolved workspace truth and exactly one resolved tenant truth for each request.
|
||||
- **FR-199-003 Workspace primacy**: Workspace MUST remain the primary shell scope for the product.
|
||||
- **FR-199-004 Tenant dependency**: Tenant context MUST never remain active without a valid resolved workspace.
|
||||
- **FR-199-005 Source-role declaration**: Every context source in scope MUST be classified as leading, supporting, or never-leading.
|
||||
- **FR-199-006 Requested-context validation**: Requested context from route, query, or explicit switch/select flows MUST be validated before it becomes active shell context.
|
||||
- **FR-199-007 Query-role discipline**: Query-based context hints MUST only participate when their role is explicitly defined by the contract.
|
||||
- **FR-199-008 Remembered-context role**: Remembered or last-used values MUST remain supporting restore candidates only.
|
||||
- **FR-199-009 Remembered-context precedence**: Remembered context MUST NEVER outrank a valid route request, explicit operator selection, or already resolved current-request truth.
|
||||
- **FR-199-010 Workspace switch contract**: Workspace switching MUST define the resulting tenant outcome, route compatibility outcome, and fallback behavior.
|
||||
- **FR-199-011 Tenant re-evaluation on workspace switch**: When workspace changes, existing tenant context MUST be re-evaluated against the target workspace before it may remain active.
|
||||
- **FR-199-012 Tenant select contract**: Tenant selection MUST only activate a tenant inside the currently resolved workspace and only after the route's selector-operability check succeeds.
|
||||
- **FR-199-013 Tenant clear contract**: Tenant clear MUST be an explicit operator flow with deterministic resulting scope and redirect behavior.
|
||||
- **FR-199-014 Tenant-required fallback**: Clearing or losing tenant context on a tenant-required route MUST redirect to a documented workspace-level fallback.
|
||||
- **FR-199-015 Tenantless validity**: Workspace-level pages may operate without an active tenant only when they are explicitly defined as tenantless-capable.
|
||||
- **FR-199-016 Workspace-required validity**: Workspace-bound pages MUST NOT behave as valid without a resolved workspace.
|
||||
- **FR-199-017 Workspace-independent exception discipline**: If a route is workspace-independent, that exception MUST be explicit and MUST NOT be inferred from missing context.
|
||||
- **FR-199-018 Distinct invalid-state handling**: Missing workspace, missing tenant, invalid tenant, inaccessible tenant, and incompatible tenant MUST be distinguishable in contract behavior where they imply different operator recovery paths.
|
||||
- **FR-199-019 Visible scope parity**: Any workspace or tenant scope shown in the shell MUST match the scope actually governing the current request.
|
||||
- **FR-199-020 Context-bar derivation**: The context bar MUST derive its display and affordances only from resolved shell context and MUST NOT keep a second implicit context model.
|
||||
- **FR-199-021 Shared entry-rule consistency**: Shell entry through direct navigation, route-bound tenant context, query-backed request context, switch flows, and restore flows MUST use the same resolution rules.
|
||||
- **FR-199-022 Page-state separation**: Global shell context MUST remain distinct from local page-state such as tabs, filters, inspect state, and draft/apply behavior.
|
||||
- **FR-199-023 Redirect consistency**: Redirect and return behavior after switch, clear, or invalid-context recovery MUST be deterministic and documented.
|
||||
- **FR-199-024 Restore entry discipline**: Restore behavior MUST explicitly define which entry flows may consult remembered workspace and tenant values and when restore is skipped.
|
||||
- **FR-199-025 Invalid remembered-state cleanup**: Invalid remembered workspace or tenant context MUST be discarded from support state cleanly and MUST NOT revive stale shell truth.
|
||||
- **FR-199-026 Panel consistency**: Relevant admin and tenant panel shell surfaces that share the contract MUST resolve and display context by the same rules.
|
||||
- **FR-199-027 Supporting-state boundaries**: Raw panel tenant state, session convenience data, and view-local shell logic MAY support resolution but MUST NOT become independent active truth.
|
||||
- **FR-199-028 Workspace and tenant compatibility**: Tenant context MUST always remain compatible with the resolved workspace and the current route type.
|
||||
- **FR-199-029 Explicit tenantless display**: When no tenant is active, the shell MUST show an explicit tenantless state rather than implying remembered or hidden tenant scope.
|
||||
- **FR-199-030 Recovery visibility**: Invalid or missing context recovery MUST provide a clear next action and MUST not strand operators in an ambiguous half-context state.
|
||||
- **FR-199-031 Global search safety**: Shell-context resolution MUST preserve tenant-safe and workspace-safe search behavior so inaccessible tenant-owned results never become visible through context drift.
|
||||
- **FR-199-032 No partial-owned authority**: Partial-local rendering logic MAY format resolved context, but it MUST NOT own precedence, validation, or recovery decisions.
|
||||
- **FR-199-033 Regression coverage**: Automated tests MUST cover context resolution, workspace switch, tenant select, tenant clear, restore behavior, invalid-context handling, and shell display consistency.
|
||||
- **FR-199-034 Manual shell smoke checks**: Manual smoke checks MUST confirm that operators can understand scope, switch scope, clear tenant context, and recover from invalid context without hidden state.
|
||||
- **FR-199-035 Closure documentation**: Final documentation MUST record the source hierarchy, allowed switch/select/clear/restore rules, fallback behavior, and what remains out of scope for page-state or constitution specs.
|
||||
- **FR-199-036 No navigation rewrite**: The implementation MUST standardize shell context truth without turning this feature into a general navigation or information-architecture rewrite.
|
||||
|
||||
### Non-Goals
|
||||
|
||||
- Standardizing page-level tabs, filters, inspect state, or draft/apply semantics that belong to the page-state contract.
|
||||
- Redesigning shared detail micro-UI families or generic header action patterns outside shell-context needs.
|
||||
- Rewriting product information architecture beyond what is required to make shell context truthful.
|
||||
- Creating a generic shell platform, universal context engine, or speculative cross-product abstraction.
|
||||
- Treating tenant as a globally independent truth outside the workspace-first model.
|
||||
|
||||
## UI Action Matrix *(mandatory when Filament is changed)*
|
||||
|
||||
| Surface | Location | Header Actions | Inspect Affordance (List/Table) | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Global context bar / shell hook | Shared topbar shell surface on admin and tenant panels | `Switch workspace`, `Select tenant`, `Clear tenant context` | none | none | none | `Choose workspace` or `Select tenant` only when the contract requires recovery | none | n/a | no | Context actions change shell scope only. They must use one resolved context contract and must not introduce a second widget-owned truth. |
|
||||
| Context recovery shell state | Shared shell recovery state | `Choose workspace`, `Return to workspace scope`, `Clear invalid tenant request` | none | none | none | Same recovery actions only | none | n/a | no | Recovery is a shell-state correction, not a record workflow. No destructive record action is introduced. |
|
||||
|
||||
### Key Entities *(include if feature involves data)*
|
||||
|
||||
- **Requested Context**: A requested workspace or tenant scope supplied by route, query, explicit switch/select action, or another documented entry path before validation.
|
||||
- **Resolved Context**: The one validated shell truth consumed by the shared shell and the current page, composed of a resolved workspace and a resolved tenant or explicit tenantless state.
|
||||
- **Remembered Context**: A non-leading convenience candidate representing last-used workspace or tenant values that may be considered for restore under documented rules.
|
||||
- **Invalid / Unavailable Context**: Requested or remembered context that cannot be honored because it is missing, inaccessible, incompatible, or no longer valid.
|
||||
- **Tenantless Workspace State**: A valid shell state in which a workspace is active but no tenant is active.
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: In validation scenarios, operators can identify the active workspace and tenant or tenantless state from the shared shell within 3 seconds on all in-scope entry paths.
|
||||
- **SC-002**: 100% of documented workspace-switch, tenant-select, tenant-clear, and invalid-context scenarios land in one valid resolved scope that matches what the shell displays.
|
||||
- **SC-003**: 100% of tested invalid, inaccessible, or incompatible requested and remembered context scenarios fall back without leaving stale workspace or tenant indicators behind.
|
||||
- **SC-004**: Shared admin-panel and tenant-panel shell entry paths show the same resolved scope truth for the same entitled scenario during validation.
|
||||
- **SC-005**: Manual smoke validation confirms that operators never need trial-and-error navigation to understand whether they are in workspace-only or tenant scope on the covered shell flows.
|
||||
@ -1,297 +0,0 @@
|
||||
# Tasks: Global Context Shell Contract
|
||||
|
||||
**Input**: Design documents from `/specs/199-global-context-shell-contract/`
|
||||
**Prerequisites**: `plan.md` (required), `spec.md` (required for user stories), `research.md`, `data-model.md`, `contracts/`, `quickstart.md`
|
||||
|
||||
**Tests**: Tests are REQUIRED for this feature because it changes runtime shell resolution, session-backed workspace and tenant context behavior, redirect and recovery rules, shared Filament shell rendering, and authorization-sensitive scope fallbacks in a Laravel/Pest codebase.
|
||||
**Operations**: This feature does not create a new `OperationRun`, background workflow, or audit-only DB mutation path. The work is limited to request-scoped shell context resolution, redirects, and shared shell rendering.
|
||||
**RBAC**: Existing workspace membership, tenant entitlement, and 404 vs 403 semantics remain authoritative. Tasks must preserve deny-as-not-found for non-members or non-entitled scope, keep capability failures server-side after scope is established, and keep global search tenant-safe under the canonical shell contract.
|
||||
**Operator Surfaces**: The shared `context-bar` shell surface and the shell recovery state remain secondary context surfaces. Tasks must keep them operator-first, truthful, and free of competing widget-owned scope state.
|
||||
**Filament UI Action Surfaces**: No new destructive actions, Resources, or alternate shell widgets are introduced. `Switch workspace`, `Select tenant`, `Clear tenant context`, and recovery actions remain the only in-scope operator actions.
|
||||
**Filament UI UX-001**: No new create, edit, or view page layout work is introduced. The feature is limited to shared shell rendering, route behavior, and context recovery.
|
||||
**Badges**: No new badge language or badge mapping is introduced.
|
||||
|
||||
**Organization**: Tasks are grouped by user story so each story can be implemented and verified as an independent increment.
|
||||
|
||||
## Test Governance Checklist
|
||||
|
||||
- Lane assignment is named and is the narrowest sufficient proof for the changed behavior.
|
||||
- New or changed tests stay in the smallest honest family, and any heavy-governance or browser addition is explicit.
|
||||
- Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default; any widening is isolated or documented.
|
||||
- Planned validation commands cover the change without pulling in unrelated lane cost.
|
||||
- Any material budget, baseline, trend, or escalation note is recorded in the active spec or PR.
|
||||
|
||||
## Phase 1: Setup (Shell Contract Regression Scaffolding)
|
||||
|
||||
**Purpose**: Create the focused regression files, source-inventory baseline, and verification baseline needed to implement Spec 199 safely.
|
||||
|
||||
- [X] T001 Create shell-contract regression scaffolding in `apps/platform/tests/Unit/Support/OperateHub/OperateHubShellResolutionTest.php`, `apps/platform/tests/Feature/Workspaces/GlobalContextShellContractTest.php`, and `apps/platform/tests/Feature/Filament/WorkspaceContextRecoveryDisplayTest.php`
|
||||
- [X] T002 [P] Create mutation-flow regression scaffolding in `apps/platform/tests/Feature/Workspaces/SwitchWorkspaceControllerTest.php` and extend `apps/platform/tests/Feature/Workspaces/SelectTenantControllerTest.php`
|
||||
- [X] T003 [P] Confirm lane assignment, source-inventory ownership, performance-proof commands, and timed manual smoke coverage in `specs/199-global-context-shell-contract/plan.md`, `specs/199-global-context-shell-contract/data-model.md`, and `specs/199-global-context-shell-contract/quickstart.md`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Foundational (Blocking Canonical Resolver Seams)
|
||||
|
||||
**Purpose**: Put the canonical shell-resolution seams in place before any story-level behavior is changed.
|
||||
|
||||
**CRITICAL**: No user story work should begin until this phase is complete.
|
||||
|
||||
- [X] T004 Implement canonical resolved shell-context precedence and recovery metadata in `apps/platform/app/Support/OperateHub/OperateHubShell.php`
|
||||
- [X] T005 [P] Align session-backed workspace, remembered-tenant, and safe intended-url helpers with restore-only semantics in `apps/platform/app/Support/Workspaces/WorkspaceContext.php` and `apps/platform/app/Support/Workspaces/WorkspaceIntendedUrl.php`
|
||||
- [X] T006 [P] Route admin-panel tenant consumption through the canonical shell contract in `apps/platform/app/Filament/Concerns/ResolvesPanelTenantContext.php`
|
||||
- [X] T007 Update unit coverage for route-first, Filament-tenant, remembered-tenant, tenantless, and invalid remembered-context branches in `apps/platform/tests/Unit/Support/OperateHub/OperateHubShellResolutionTest.php` and `apps/platform/tests/Unit/Support/Workspaces/WorkspaceContextRememberedTenantTest.php`
|
||||
|
||||
**Checkpoint**: The shared shell resolver, storage semantics, and panel-consumption seam exist, so story work can proceed independently.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: User Story 1 - See The True Current Scope (Priority: P1)
|
||||
|
||||
**Goal**: Make every shared shell surface display the same truthful workspace and tenant state the request is actually using.
|
||||
|
||||
**Independent Test**: Open workspace-scoped and tenant-bound entry paths with tenant-scoped and tenantless states, then verify the shared shell displays the same resolved truth the page is operating under.
|
||||
|
||||
### Tests for User Story 1
|
||||
|
||||
- [X] T008 [P] [US1] Extend shared-shell truth display and no-hidden-page-state coverage for tenant-scoped and tenantless routes in `apps/platform/tests/Feature/Filament/WorkspaceContextTopbarAndTenantSelectionTest.php` and `apps/platform/tests/Feature/Spec085/OperationsIndexHeaderTest.php`
|
||||
- [X] T009 [P] [US1] Add recovery-shell display assertions for missing workspace, missing tenant, and explicit tenantless states in `apps/platform/tests/Feature/Filament/WorkspaceContextRecoveryDisplayTest.php`
|
||||
|
||||
### Implementation for User Story 1
|
||||
|
||||
- [X] T010 [US1] Reduce the shared shell to a consumer-only resolved-context display and keep page-local filters, tabs, and inspect state out of the shell contract in `apps/platform/resources/views/filament/partials/context-bar.blade.php`
|
||||
- [X] T011 [US1] Keep both panels rendering the same shared shell contract in `apps/platform/app/Providers/Filament/AdminPanelProvider.php` and `apps/platform/app/Providers/Filament/TenantPanelProvider.php`
|
||||
- [X] T012 [US1] Run focused US1 verification against `apps/platform/tests/Feature/Filament/WorkspaceContextTopbarAndTenantSelectionTest.php`, `apps/platform/tests/Feature/Filament/WorkspaceContextRecoveryDisplayTest.php`, and `apps/platform/tests/Feature/Spec085/OperationsIndexHeaderTest.php`
|
||||
|
||||
**Checkpoint**: Shared shell surfaces now show one truthful scope model instead of competing display logic.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: User Story 2 - Switch Workspace Without Stale Tenant Truth (Priority: P1)
|
||||
|
||||
**Goal**: Make workspace switching deterministically re-evaluate tenant compatibility, fallback, and redirect behavior.
|
||||
|
||||
**Independent Test**: Start from a valid workspace and tenant, switch to compatible and incompatible target workspaces, and verify the resulting tenant state, redirect destination, and authorization behavior.
|
||||
|
||||
### Tests for User Story 2
|
||||
|
||||
- [X] T013 [P] [US2] Add switch regression coverage for compatible, incompatible, archived, and non-member target workspaces in `apps/platform/tests/Feature/Workspaces/SwitchWorkspaceControllerTest.php`, `apps/platform/tests/Feature/Workspaces/WorkspaceRedirectResolverTest.php`, and `apps/platform/tests/Feature/Workspaces/SwitchWorkspaceRedirectsToTenantRegistrationWhenNoTenantsTest.php`
|
||||
- [X] T014 [P] [US2] Extend positive and negative workspace-switch affordance coverage in `apps/platform/tests/Feature/Workspaces/WorkspaceSwitchUserMenuTest.php` and `apps/platform/tests/Feature/Workspaces/ChooseWorkspaceRedirectsToChooseTenantTest.php`
|
||||
|
||||
### Implementation for User Story 2
|
||||
|
||||
- [X] T015 [US2] Make workspace switching re-evaluate tenant compatibility and clear incompatible tenant state in `apps/platform/app/Http/Controllers/SwitchWorkspaceController.php` and `apps/platform/app/Support/Workspaces/WorkspaceContext.php`
|
||||
- [X] T016 [US2] Canonicalize post-switch destination rules and safe intended-url consumption in `apps/platform/app/Support/Workspaces/WorkspaceRedirectResolver.php` and `apps/platform/app/Support/Workspaces/WorkspaceIntendedUrl.php`
|
||||
- [X] T017 [US2] Run focused US2 verification against `apps/platform/tests/Feature/Workspaces/SwitchWorkspaceControllerTest.php`, `apps/platform/tests/Feature/Workspaces/WorkspaceRedirectResolverTest.php`, `apps/platform/tests/Feature/Workspaces/SwitchWorkspaceRedirectsToTenantRegistrationWhenNoTenantsTest.php`, `apps/platform/tests/Feature/Workspaces/WorkspaceSwitchUserMenuTest.php`, and `apps/platform/tests/Feature/Workspaces/ChooseWorkspaceRedirectsToChooseTenantTest.php`
|
||||
|
||||
**Checkpoint**: Workspace switching can no longer carry stale tenant truth into the next workspace or route.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: User Story 3 - Select Or Clear Tenant Intentionally (Priority: P1)
|
||||
|
||||
**Goal**: Make explicit tenant selection and tenant clear flows behave like deterministic scope decisions instead of partial-local heuristics.
|
||||
|
||||
**Independent Test**: Select a tenant from the shared shell, clear tenant context from a workspace page, and clear it from a tenant-bound route to verify predictable scope and redirect outcomes.
|
||||
|
||||
### Tests for User Story 3
|
||||
|
||||
- [X] T018 [P] [US3] Extend explicit tenant-selection coverage for happy-path, non-operable, wrong-workspace, and unauthorized tenant requests in `apps/platform/tests/Feature/Workspaces/SelectTenantControllerTest.php` and `apps/platform/tests/Feature/Filament/WorkspaceContextTopbarAndTenantSelectionTest.php`
|
||||
- [X] T019 [P] [US3] Extend clear-tenant route-compatibility coverage for workspace-scoped, tenant-bound, tenant-scoped evidence, and canonical workspace record viewer pages in `apps/platform/tests/Feature/Spec085/OperationsIndexHeaderTest.php`, `apps/platform/tests/Feature/Workspaces/ChooseTenantPageTest.php`, and `apps/platform/tests/Feature/Workspaces/GlobalContextShellContractTest.php`
|
||||
|
||||
### Implementation for User Story 3
|
||||
|
||||
- [X] T020 [US3] Align explicit tenant selection with the canonical shell contract, selector-operability rules, and remembered-context rules in `apps/platform/app/Http/Controllers/SelectTenantController.php` and `apps/platform/app/Support/OperateHub/OperateHubShell.php`
|
||||
- [X] T021 [US3] Standardize clear-tenant recovery outcomes (same-route tenantless workspace state, `admin.operations.index`, `admin.evidence.overview`, `admin.workspace.managed-tenants.index`, `admin.operations.view`, `admin.home`) and route compatibility in `apps/platform/app/Http/Controllers/ClearTenantContextController.php` and `apps/platform/app/Support/Tenants/TenantPageCategory.php`
|
||||
- [X] T022 [US3] Keep shell action labels and tenantless wording aligned to the approved vocabulary in `apps/platform/resources/views/filament/partials/context-bar.blade.php`
|
||||
- [X] T023 [US3] Run focused US3 verification against `apps/platform/tests/Feature/Workspaces/SelectTenantControllerTest.php`, `apps/platform/tests/Feature/Spec085/OperationsIndexHeaderTest.php`, `apps/platform/tests/Feature/Workspaces/ChooseTenantPageTest.php`, and `apps/platform/tests/Feature/Workspaces/GlobalContextShellContractTest.php`
|
||||
|
||||
**Checkpoint**: Tenant selection and clear behavior now act as explicit scope changes with stable wording and recovery.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: User Story 4 - Reject Invalid Or Stale Context Cleanly (Priority: P1)
|
||||
|
||||
**Goal**: Make invalid route, query, and remembered context fail cleanly without leaving stale scope visible or widening access.
|
||||
|
||||
**Independent Test**: Enter the shell with invalid route, query-hint, and remembered context combinations, then verify the request falls back to a valid scope or 404 path with no stale shell truth left behind.
|
||||
|
||||
### Tests for User Story 4
|
||||
|
||||
- [X] T024 [P] [US4] Add valid and invalid query-hint coverage plus stale remembered-context coverage in `apps/platform/tests/Feature/Workspaces/GlobalContextShellContractTest.php` and `apps/platform/tests/Unit/Support/Workspaces/WorkspaceContextRememberedTenantTest.php`
|
||||
- [X] T025 [P] [US4] Extend tenant-required fallback, workspace-required recovery, and explicit chooser-route exception coverage in `apps/platform/tests/Feature/Workspaces/ChooseTenantPageTest.php`, `apps/platform/tests/Feature/Workspaces/ChooseWorkspacePageTest.php`, and `apps/platform/tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php`
|
||||
|
||||
### Implementation for User Story 4
|
||||
|
||||
- [X] T026 [US4] Replace ad hoc tenant-selection heuristics with canonical invalid-context checks in `apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php`
|
||||
- [X] T027 [US4] Tighten page-category classification and invalid-context fallback mapping, including the explicit workspace-independent chooser-route exception, in `apps/platform/app/Support/Tenants/TenantPageCategory.php` and `apps/platform/app/Support/OperateHub/OperateHubShell.php`
|
||||
- [X] T028 [US4] Preserve deny-as-not-found, forbidden, and no-stale-scope recovery semantics across `/admin` and `/admin/t/{external_id}` in `apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php`, `apps/platform/app/Http/Controllers/ClearTenantContextController.php`, and `apps/platform/tests/Feature/Workspaces/GlobalContextShellContractTest.php`
|
||||
- [X] T029 [US4] Run focused US4 verification against `apps/platform/tests/Feature/Workspaces/GlobalContextShellContractTest.php`, `apps/platform/tests/Feature/Workspaces/ChooseTenantPageTest.php`, `apps/platform/tests/Feature/Workspaces/ChooseWorkspacePageTest.php`, `apps/platform/tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php`, and `apps/platform/tests/Unit/Support/Workspaces/WorkspaceContextRememberedTenantTest.php`
|
||||
|
||||
**Checkpoint**: Invalid or stale context now recovers explicitly and never survives as a false active scope.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: User Story 5 - Keep Shared Shell Logic Consistent Across Panels (Priority: P2)
|
||||
|
||||
**Goal**: Keep admin and tenant panel entry paths, supporting panel state, and global search safety aligned to the same shell contract.
|
||||
|
||||
**Independent Test**: Resolve the same entitled workspace and tenant through admin and tenant panel entry paths, then verify both panels show the same active truth and preserve tenant-safe search behavior.
|
||||
|
||||
### Tests for User Story 5
|
||||
|
||||
- [X] T030 [P] [US5] Add admin-versus-tenant panel parity coverage for the same entitled workspace and tenant scenario in `apps/platform/tests/Feature/Filament/WorkspaceContextTopbarAndTenantSelectionTest.php` and `apps/platform/tests/Feature/Workspaces/WorkspacesResourceIsTenantlessTest.php`
|
||||
- [X] T031 [P] [US5] Extend global-search context-safety coverage so tenant-owned results stay scoped under the canonical shell contract in `apps/platform/tests/Feature/Rbac/AdminGlobalSearchContextSafetyTest.php`, `apps/platform/tests/Feature/TenantRBAC/TenantSwitcherScopeTest.php`, and `apps/platform/tests/Feature/Rbac/TenantActionSurfaceConsistencyTest.php`
|
||||
|
||||
### Implementation for User Story 5
|
||||
|
||||
- [X] T032 [US5] Keep panel-specific context sources subordinate to the canonical shell contract in `apps/platform/app/Filament/Concerns/ResolvesPanelTenantContext.php`, `apps/platform/app/Providers/Filament/AdminPanelProvider.php`, and `apps/platform/app/Providers/Filament/TenantPanelProvider.php`
|
||||
- [X] T033 [US5] Preserve tenant-safe global search scoping while the shell contract is consolidated in `apps/platform/app/Filament/Concerns/ScopesGlobalSearchToTenant.php`, `apps/platform/app/Filament/Resources/TenantResource.php`, and `apps/platform/app/Filament/Resources/PolicyResource.php`
|
||||
- [X] T034 [US5] Run focused US5 verification against `apps/platform/tests/Feature/Filament/WorkspaceContextTopbarAndTenantSelectionTest.php`, `apps/platform/tests/Feature/Workspaces/WorkspacesResourceIsTenantlessTest.php`, `apps/platform/tests/Feature/Rbac/AdminGlobalSearchContextSafetyTest.php`, `apps/platform/tests/Feature/Rbac/TenantActionSurfaceConsistencyTest.php`, and `apps/platform/tests/Feature/TenantRBAC/TenantSwitcherScopeTest.php`
|
||||
|
||||
**Checkpoint**: Shared shell logic, panel state, and search safety remain aligned across admin and tenant entry paths.
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Polish & Cross-Cutting Concerns
|
||||
|
||||
**Purpose**: Finish validation, documentation parity, non-functional render proof, and operator smoke coverage across all stories.
|
||||
|
||||
- [X] T035 [P] Reconcile final source inventory, source hierarchy, recovery vocabulary, fallback matrix, and verification commands in `specs/199-global-context-shell-contract/plan.md`, `specs/199-global-context-shell-contract/research.md`, `specs/199-global-context-shell-contract/data-model.md`, `specs/199-global-context-shell-contract/contracts/global-context-shell.logical.openapi.yaml`, and `specs/199-global-context-shell-contract/quickstart.md`
|
||||
- [X] T036 [P] Run the focused Pest validation pack from `specs/199-global-context-shell-contract/quickstart.md`, including DB-only render and no-enqueue shell proof
|
||||
- [X] T037 Run formatting with `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
|
||||
- [X] T038 [P] Execute the timed 3-second manual smoke checklist from `specs/199-global-context-shell-contract/quickstart.md` for tenantless entry, workspace switch, tenant select, tenant clear, evidence fallback, canonical workspace record viewer fallback, invalid remembered tenant, explicit chooser-route exception handling, and panel parity
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Execution Order
|
||||
|
||||
### Phase Dependencies
|
||||
|
||||
- **Setup (Phase 1)**: Starts immediately and creates the focused regression scaffolding and verification baseline.
|
||||
- **Foundational (Phase 2)**: Depends on Setup and blocks all story work until the canonical resolver seams are in place.
|
||||
- **User Stories (Phase 3+)**: All depend on Foundational completion.
|
||||
- **Polish (Phase 8)**: Depends on the desired user stories being complete.
|
||||
|
||||
### User Story Dependencies
|
||||
|
||||
- **US1**: Depends only on the foundational resolver seam and is the recommended MVP slice.
|
||||
- **US2**: Depends on the foundational seam and can proceed independently of US1 once canonical workspace and tenant precedence exist.
|
||||
- **US3**: Depends on the foundational seam and can proceed independently of US1 and US2, though it benefits from the shared shell display already being consumer-only.
|
||||
- **US4**: Depends on the foundational seam and should land after the invalid-context matrix is stable, but it does not require US2 or US3 to be complete.
|
||||
- **US5**: Depends on the foundational seam and benefits from at least one earlier story landing first so panel parity and search safety are verified against the implemented contract.
|
||||
|
||||
### Within Each User Story
|
||||
|
||||
- Story tests should be written before or alongside implementation and should fail before the story is considered complete.
|
||||
- Resolver and storage seam updates must land before controller, middleware, or shell display changes are considered finished.
|
||||
- Authorization-sensitive regressions must stay in Unit or Feature lanes only; no browser family should be added for this feature.
|
||||
- Each story-level verification task should run after the story's implementation tasks are complete.
|
||||
|
||||
### Parallel Opportunities
|
||||
|
||||
- `T001`, `T002`, and `T003` can run in parallel during Setup.
|
||||
- `T005` and `T006` can run in parallel during Foundational work.
|
||||
- `T008` and `T009` can run in parallel for User Story 1.
|
||||
- `T013` and `T014` can run in parallel for User Story 2.
|
||||
- `T018` and `T019` can run in parallel for User Story 3.
|
||||
- `T024` and `T025` can run in parallel for User Story 4.
|
||||
- `T030` and `T031` can run in parallel for User Story 5.
|
||||
- `T035`, `T036`, and `T038` can run in parallel after implementation is complete.
|
||||
|
||||
---
|
||||
|
||||
## Parallel Example: User Story 1
|
||||
|
||||
```bash
|
||||
# User Story 1 tests in parallel:
|
||||
Task: "T008 Extend shared-shell truth display and no-hidden-page-state coverage in apps/platform/tests/Feature/Filament/WorkspaceContextTopbarAndTenantSelectionTest.php and apps/platform/tests/Feature/Spec085/OperationsIndexHeaderTest.php"
|
||||
Task: "T009 Add recovery-shell display assertions in apps/platform/tests/Feature/Filament/WorkspaceContextRecoveryDisplayTest.php"
|
||||
|
||||
# Then land the shared shell implementation:
|
||||
Task: "T010 Reduce the shared shell to a consumer-only resolved-context display and keep page-local filters, tabs, and inspect state out of the shell contract in apps/platform/resources/views/filament/partials/context-bar.blade.php"
|
||||
Task: "T011 Keep both panels rendering the same shared shell contract in apps/platform/app/Providers/Filament/AdminPanelProvider.php and apps/platform/app/Providers/Filament/TenantPanelProvider.php"
|
||||
```
|
||||
|
||||
## Parallel Example: User Story 2
|
||||
|
||||
```bash
|
||||
# User Story 2 tests in parallel:
|
||||
Task: "T013 Add switch regression coverage in apps/platform/tests/Feature/Workspaces/SwitchWorkspaceControllerTest.php, apps/platform/tests/Feature/Workspaces/WorkspaceRedirectResolverTest.php, and apps/platform/tests/Feature/Workspaces/SwitchWorkspaceRedirectsToTenantRegistrationWhenNoTenantsTest.php"
|
||||
Task: "T014 Extend workspace-switch affordance coverage in apps/platform/tests/Feature/Workspaces/WorkspaceSwitchUserMenuTest.php and apps/platform/tests/Feature/Workspaces/ChooseWorkspaceRedirectsToChooseTenantTest.php"
|
||||
|
||||
# Then land controller and redirect behavior:
|
||||
Task: "T015 Make workspace switching re-evaluate tenant compatibility in apps/platform/app/Http/Controllers/SwitchWorkspaceController.php and apps/platform/app/Support/Workspaces/WorkspaceContext.php"
|
||||
Task: "T016 Canonicalize post-switch destination rules in apps/platform/app/Support/Workspaces/WorkspaceRedirectResolver.php and apps/platform/app/Support/Workspaces/WorkspaceIntendedUrl.php"
|
||||
```
|
||||
|
||||
## Parallel Example: User Story 3
|
||||
|
||||
```bash
|
||||
# User Story 3 tests in parallel:
|
||||
Task: "T018 Extend explicit tenant-selection coverage in apps/platform/tests/Feature/Workspaces/SelectTenantControllerTest.php and apps/platform/tests/Feature/Filament/WorkspaceContextTopbarAndTenantSelectionTest.php"
|
||||
Task: "T019 Extend clear-tenant route-compatibility coverage in apps/platform/tests/Feature/Spec085/OperationsIndexHeaderTest.php and apps/platform/tests/Feature/Workspaces/ChooseTenantPageTest.php"
|
||||
|
||||
# Then land explicit scope-mutation behavior:
|
||||
Task: "T020 Align explicit tenant selection with the canonical shell contract in apps/platform/app/Http/Controllers/SelectTenantController.php and apps/platform/app/Support/OperateHub/OperateHubShell.php"
|
||||
Task: "T021 Standardize clear-tenant recovery destinations in apps/platform/app/Http/Controllers/ClearTenantContextController.php and apps/platform/app/Support/Tenants/TenantPageCategory.php"
|
||||
```
|
||||
|
||||
## Parallel Example: User Story 4
|
||||
|
||||
```bash
|
||||
# User Story 4 tests in parallel:
|
||||
Task: "T024 Add invalid route, query-hint, and stale remembered-context coverage in apps/platform/tests/Feature/Workspaces/GlobalContextShellContractTest.php and apps/platform/tests/Unit/Support/Workspaces/WorkspaceContextRememberedTenantTest.php"
|
||||
Task: "T025 Extend tenant-required fallback, workspace-required recovery, and explicit chooser-route exception coverage in apps/platform/tests/Feature/Workspaces/ChooseTenantPageTest.php, apps/platform/tests/Feature/Workspaces/ChooseWorkspacePageTest.php, and apps/platform/tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php"
|
||||
|
||||
# Then land middleware and fallback behavior:
|
||||
Task: "T026 Replace ad hoc tenant-selection heuristics in apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php"
|
||||
Task: "T027 Tighten page-category classification and invalid-context fallback mapping, including the explicit workspace-independent chooser-route exception, in apps/platform/app/Support/Tenants/TenantPageCategory.php and apps/platform/app/Support/OperateHub/OperateHubShell.php"
|
||||
```
|
||||
|
||||
## Parallel Example: User Story 5
|
||||
|
||||
```bash
|
||||
# User Story 5 tests in parallel:
|
||||
Task: "T030 Add admin-versus-tenant panel parity coverage in apps/platform/tests/Feature/Filament/WorkspaceContextTopbarAndTenantSelectionTest.php and apps/platform/tests/Feature/Workspaces/WorkspacesResourceIsTenantlessTest.php"
|
||||
Task: "T031 Extend global-search context-safety coverage in apps/platform/tests/Feature/Rbac/AdminGlobalSearchContextSafetyTest.php, apps/platform/tests/Feature/TenantRBAC/TenantSwitcherScopeTest.php, and apps/platform/tests/Feature/Rbac/TenantActionSurfaceConsistencyTest.php"
|
||||
|
||||
# Then land panel-parity and search-scope behavior:
|
||||
Task: "T032 Keep panel-specific context sources subordinate to the canonical shell contract in apps/platform/app/Filament/Concerns/ResolvesPanelTenantContext.php, apps/platform/app/Providers/Filament/AdminPanelProvider.php, and apps/platform/app/Providers/Filament/TenantPanelProvider.php"
|
||||
Task: "T033 Preserve tenant-safe global search scoping in apps/platform/app/Filament/Concerns/ScopesGlobalSearchToTenant.php, apps/platform/app/Filament/Resources/TenantResource.php, and apps/platform/app/Filament/Resources/PolicyResource.php"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### MVP First (User Story 1 Only)
|
||||
|
||||
1. Complete Phase 1: Setup.
|
||||
2. Complete Phase 2: Foundational.
|
||||
3. Complete Phase 3: User Story 1.
|
||||
4. Validate that the shared shell shows one truthful tenant-scoped and tenantless model before moving on.
|
||||
|
||||
### Incremental Delivery
|
||||
|
||||
1. Establish the canonical shell resolver and storage semantics.
|
||||
2. Deliver truthful shared-shell display as the MVP.
|
||||
3. Add deterministic workspace switching.
|
||||
4. Add deterministic tenant select and clear flows.
|
||||
5. Harden invalid-context recovery.
|
||||
6. Close with cross-panel parity, search safety, and final validation.
|
||||
|
||||
### Parallel Team Strategy
|
||||
|
||||
1. One developer can land Setup plus Foundational resolver seams.
|
||||
2. After Foundational work is complete, one developer can take US1 or US2 while another works on US3 or US4 because the primary file overlap is limited.
|
||||
3. US5 should land after at least one earlier story so panel parity and global-search safety verify the real implemented contract.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- `[P]` tasks are limited to work on different files or isolated test files with no incomplete dependency overlap.
|
||||
- `[US1]` through `[US5]` map directly to the user stories in `spec.md`.
|
||||
- The suggested MVP scope is Phase 1 through Phase 3 only.
|
||||
- This task list preserves Filament v5 and Livewire v4 compliance, keeps provider registration unchanged in `bootstrap/providers.php`, keeps destructive-action rules unchanged because no destructive record action is introduced, and preserves existing tenant-safe global search behavior while the shell contract is consolidated.
|
||||
@ -1,36 +0,0 @@
|
||||
# Specification Quality Checklist: UI/UX Constitution Extension: Filament Nativity & Custom Surface Rules
|
||||
|
||||
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||
**Created**: 2026-04-18
|
||||
**Feature**: [spec.md](../spec.md)
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] No implementation details (languages, frameworks, APIs)
|
||||
- [x] Focused on user value and business needs
|
||||
- [x] Written for non-technical stakeholders
|
||||
- [x] All mandatory sections completed
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||
- [x] Requirements are testable and unambiguous
|
||||
- [x] Success criteria are measurable
|
||||
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||
- [x] All acceptance scenarios are defined
|
||||
- [x] Edge cases are identified
|
||||
- [x] Scope is clearly bounded
|
||||
- [x] Dependencies and assumptions identified
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [x] All functional requirements have clear acceptance criteria
|
||||
- [x] User scenarios cover primary flows
|
||||
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||
- [x] No implementation details leak into specification
|
||||
|
||||
## Notes
|
||||
|
||||
- Optional operator-surface classification tables are intentionally omitted because this feature amends repository governance rather than changing a concrete runtime surface.
|
||||
- Filament and Livewire terminology is intentional product vocabulary for this constitution slice, not code-level implementation guidance.
|
||||
- Validation cross-checked the spec against adjacent Specs 196 through 199 and the existing constitution anchors UI-FIL-001, UI-EX-001, DECIDE-001, UI-REVIEW-001, and HDR-001.
|
||||
@ -1,103 +0,0 @@
|
||||
# Constitution Governance Contract
|
||||
|
||||
## Contract Type
|
||||
|
||||
Docs-only governance contract. This feature introduces no runtime HTTP, GraphQL, CLI, or queue API surface.
|
||||
|
||||
## Governing Scope
|
||||
|
||||
Spec 200 must amend the existing UI constitution so the following rule families become explicit, reviewable, and bounded:
|
||||
|
||||
- Filament-native by default
|
||||
- fake-native prohibitions
|
||||
- legitimate custom-surface allowance
|
||||
- shared detail micro-UI family rules
|
||||
- shell/page/detail state ownership rules
|
||||
- reviewer-facing guidance and classification questions
|
||||
- explicit exception handling
|
||||
- named anti-pattern catalog
|
||||
|
||||
## Required Amendment Targets
|
||||
|
||||
The implementation must update the existing constitution in `.specify/memory/constitution.md` rather than create a separate rulebook.
|
||||
|
||||
Expected amendment targets:
|
||||
|
||||
- `UI-SURF-001`
|
||||
- `ACTSURF-001`
|
||||
- `HDR-001`
|
||||
- `UI-HARD-001`
|
||||
- `UI-EX-001`
|
||||
- `UI-REVIEW-001`
|
||||
- `Filament UI — Action Surface Contract`
|
||||
- `UX-001`
|
||||
- `UI-FIL-001` where native-first wording needs sharpening
|
||||
|
||||
## Final Amendment Mapping
|
||||
|
||||
| Source spec | Problem class absorbed by Spec 200 | Final constitution targets |
|
||||
|---|---|---|
|
||||
| Spec 196 | Native-by-default clarity, fake-native drift, request-driven body-state misuse, simple-overview drift | `UI-FIL-001`, `UI-HARD-001`, `UI-EX-001`, `UI-REVIEW-001` |
|
||||
| Spec 197 | Shared detail micro-UI families, host/core boundaries, bounded host variation | `UI-SURF-001`, `ACTSURF-001`, `HDR-001`, `Filament UI — Action Surface Contract`, `UI-REVIEW-001` |
|
||||
| Spec 198 | Requested vs active vs draft vs inspect vs restorable page-state ownership | `UI-HARD-001`, `UX-001`, `UI-REVIEW-001` |
|
||||
| Spec 199 | Workspace-first shell truth, tenantless state, shell/page separation, fallback clarity | `ACTSURF-001`, `UX-001`, `UI-REVIEW-001` |
|
||||
| Spec 201 | Enforcement follow-up only | deferred consumer; no constitution wording invented there |
|
||||
|
||||
## Final Exception Boundary
|
||||
|
||||
Spec 200 leaves four bounded exception families available inside `UI-EX-001`:
|
||||
|
||||
- `Legitimate Custom Surface Exception`
|
||||
- `Nativity Exception`
|
||||
- `Shared Detail Host Variation Exception`
|
||||
- `State-Layer Special-case Exception`
|
||||
|
||||
Each exception must stay inside the existing exception model and must state:
|
||||
|
||||
1. the product reason
|
||||
2. the smallest custom behavior required
|
||||
3. what remains standardized
|
||||
4. which layer owns the relevant state
|
||||
5. what Spec 201 may later enforce
|
||||
|
||||
## Acceptance Contract
|
||||
|
||||
The finished constitution amendment must satisfy all of the following:
|
||||
|
||||
1. A reviewer can classify the representative cases from Specs 196, 197, 198, and 199 using the amended constitution alone.
|
||||
2. The amendment does not create a parallel top-level UI rulebook.
|
||||
3. Legitimate custom surfaces remain possible through an explicit exception path.
|
||||
4. The anti-pattern catalog names at least the recurring drift classes identified in the feature spec.
|
||||
5. Reviewer-facing guidance and classification questions are explicit enough to classify the representative cases from Specs 196 through 199.
|
||||
6. The cross-spec mapping to Specs 196 through 199 and the deferral to Spec 201 are explicit.
|
||||
|
||||
The acceptance contract is not satisfied unless the constitution now names all of the following review classes directly: `Native Surface`, `Fake-Native Surface`, `Custom Surface`, `Shared Detail Micro-UI`, `Host`, `Global Context State`, `Page State`, `Detail State`, `Legitimate Exception`, `Host Drift`, `State Layer Collapse`, and `Parallel Inspect Worlds`.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
This contract explicitly excludes:
|
||||
|
||||
- `app/` runtime code changes
|
||||
- route, controller, or Livewire behavior changes
|
||||
- CI/grep/lint/test enforcement
|
||||
- checklist-template operationalization beyond optional wording-only references
|
||||
- fabricated REST or GraphQL endpoints
|
||||
|
||||
## Deferred Enforcement Boundary
|
||||
|
||||
Spec 201 is responsible for operationalization work such as:
|
||||
|
||||
- review checklist changes
|
||||
- grep/lint guards
|
||||
- CI enforcement
|
||||
- runtime or test-backed regression guards for the anti-pattern catalog
|
||||
|
||||
Spec 200 must define the rule language cleanly enough that Spec 201 can consume it without inventing a new vocabulary.
|
||||
|
||||
## Close-out Contract
|
||||
|
||||
The final artifact set must leave one explicit close-out note that separates:
|
||||
|
||||
- newly added clauses and vocabulary
|
||||
- tightened existing clauses
|
||||
- enforcement work intentionally deferred to Spec 201
|
||||
@ -1,253 +0,0 @@
|
||||
# Data Model: UI/UX Constitution Extension: Filament Nativity & Custom Surface Rules
|
||||
|
||||
## Overview
|
||||
|
||||
Spec 200 is a docs-only governance feature. Its data model is conceptual rather than persisted: it describes the rule objects, vocabulary, and mappings that the constitution amendment must carry so reviewers can classify future UI work consistently.
|
||||
|
||||
No application database schema, runtime DTO, or transport contract is introduced by this feature.
|
||||
|
||||
## Entities
|
||||
|
||||
### 1. ConstitutionAmendmentTarget
|
||||
|
||||
Represents one existing constitution section that Spec 200 extends.
|
||||
|
||||
**Fields**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `section_id` | string | Existing rule ID or section anchor, for example `UI-SURF-001` or `UI-EX-001` |
|
||||
| `target_file` | string | File path, always `.specify/memory/constitution.md` for this feature |
|
||||
| `amendment_type` | enum | `clarification`, `extension`, or `new-clause` |
|
||||
| `problem_classes` | list<string> | The drift classes this amendment addresses |
|
||||
| `source_specs` | list<string> | Evidence specs feeding the amendment, limited here to Specs 196 through 199 |
|
||||
| `expected_outputs` | list<string> | Vocabulary, anti-pattern, exception, or state-layer additions contributed by this target |
|
||||
| `deferred_enforcement` | list<string> | What remains reserved for Spec 201 |
|
||||
|
||||
**Validation rules**
|
||||
|
||||
- `section_id` must reference an already existing constitution section.
|
||||
- `amendment_type` must not imply a new parallel rulebook.
|
||||
- Every target must cite at least one source spec or one direct repo problem class.
|
||||
|
||||
### 2. SurfaceVocabularyTerm
|
||||
|
||||
Represents a named concept that future specs and reviews must use consistently.
|
||||
|
||||
**Fields**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `name` | string | Canonical term, such as `Native Surface` or `Fake-Native Surface` |
|
||||
| `definition` | text | Reviewable product definition |
|
||||
| `positive_examples` | list<string> | Example surfaces or surface families that fit the term |
|
||||
| `negative_examples` | list<string> | Nearby cases that must not be misclassified under the term |
|
||||
| `governing_sections` | list<string> | Constitution sections that must mention or support the term |
|
||||
| `source_specs` | list<string> | Adjacent specs that proved the need for the term |
|
||||
|
||||
**Validation rules**
|
||||
|
||||
- Every term must map to at least one amended constitution section.
|
||||
- Terms must describe product-review concepts, not implementation jargon.
|
||||
- Terms must be classifiable from real repo cases.
|
||||
|
||||
### 3. AntiPattern
|
||||
|
||||
Represents a named failure mode the constitution must forbid or flag.
|
||||
|
||||
**Fields**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `name` | string | Canonical anti-pattern name, such as `Filament Costume` |
|
||||
| `definition` | text | What the anti-pattern looks like in practice |
|
||||
| `trigger_signals` | list<string> | Observable characteristics that let reviewers identify it |
|
||||
| `default_review_outcome` | enum | `reject`, `document-exception`, or `defer-to-follow-up-spec` |
|
||||
| `allowed_exception_path` | string | Exception type or `none` when no escape is expected |
|
||||
| `source_specs` | list<string> | Spec evidence behind the anti-pattern |
|
||||
|
||||
**Validation rules**
|
||||
|
||||
- Every anti-pattern must have at least one trigger signal.
|
||||
- Every anti-pattern must either forbid the pattern directly or point to a bounded exception path.
|
||||
- Anti-pattern names must remain stable enough for future guardrails in Spec 201.
|
||||
|
||||
### 4. ExceptionType
|
||||
|
||||
Represents a bounded, legitimate deviation from the default rules.
|
||||
|
||||
**Fields**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `name` | string | Canonical exception name |
|
||||
| `allowed_when` | text | Product reason that makes the exception legitimate |
|
||||
| `required_justification` | list<string> | Mandatory explanation points in the governing spec or PR |
|
||||
| `boundaries` | list<string> | What the exception does not allow |
|
||||
| `standardized_parts` | list<string> | What must remain consistent despite the exception |
|
||||
| `governing_section` | string | Expected home in `UI-EX-001` |
|
||||
| `deferred_enforcement` | list<string> | Which parts Spec 201 may later operationalize |
|
||||
|
||||
**Validation rules**
|
||||
|
||||
- Every exception type must stay inside the existing exception model.
|
||||
- `allowed_when` must describe product need, not convenience.
|
||||
- Every exception must state what stays standardized.
|
||||
|
||||
### 5. StateOwnershipRule
|
||||
|
||||
Represents the review contract for shell, page, or detail state.
|
||||
|
||||
**Fields**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `layer` | enum | `shell`, `page`, or `detail` |
|
||||
| `owns_truth` | text | What this layer is authoritative for |
|
||||
| `allowed_inputs` | list<string> | Inputs the layer may consume, such as route state, query seed, or local viewer state |
|
||||
| `query_role` | enum | `initialization-only`, `durable`, `deeplink-only`, or `unsupported` |
|
||||
| `forbidden_overlaps` | list<string> | Competing truths or cross-layer conflicts that are not allowed |
|
||||
| `source_specs` | list<string> | Spec 198 and/or 199 evidence |
|
||||
|
||||
**Validation rules**
|
||||
|
||||
- Each rule must assign exactly one owner layer.
|
||||
- A rule may describe inputs from other layers, but it may not assign equal authority to multiple layers.
|
||||
- Query role must be explicit whenever the layer can be initialized from URL or remembered state.
|
||||
|
||||
### 6. CrossSpecMapping
|
||||
|
||||
Represents how one adjacent spec feeds the constitution amendment and where follow-up work goes.
|
||||
|
||||
**Fields**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `source_spec` | string | `196`, `197`, `198`, `199`, or `201` |
|
||||
| `problem_class` | string | Problem family contributed by that spec |
|
||||
| `constitution_targets` | list<string> | Which amendment targets consume that evidence |
|
||||
| `status` | enum | `evidence-source`, `consumed-by-spec-200`, or `deferred-to-201` |
|
||||
| `notes` | text | Short mapping summary |
|
||||
|
||||
**Validation rules**
|
||||
|
||||
- Specs 196 through 199 must map into at least one amendment target.
|
||||
- Spec 201 must appear only as a deferred enforcement consumer, not as an implementation dependency within Spec 200.
|
||||
|
||||
## Relationships
|
||||
|
||||
- A `ConstitutionAmendmentTarget` produces zero or more `SurfaceVocabularyTerm` entries.
|
||||
- A `ConstitutionAmendmentTarget` may define zero or more `AntiPattern` and `ExceptionType` entries.
|
||||
- `StateOwnershipRule` entries are a specialized conceptual rule family that feed both vocabulary terms and amendment targets.
|
||||
- `CrossSpecMapping` connects the adjacent evidence specs to the exact constitution targets that absorb them.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
### Amendment Lifecycle
|
||||
|
||||
1. **Identified**: A rule gap is proven by a current repo case.
|
||||
2. **Mapped**: The gap is assigned to an existing constitution section.
|
||||
3. **Integrated**: The constitution text is amended with vocabulary, anti-patterns, or exceptions.
|
||||
4. **Reviewable**: Representative cases from Specs 196 through 199 can be classified through the amended language.
|
||||
5. **Operationalized later**: Spec 201 may add checklist, grep, lint, or test enforcement based on the integrated rule.
|
||||
|
||||
### Exception Lifecycle
|
||||
|
||||
1. **Requested**: A surface claims that native-by-default or shared-contract rules do not fit.
|
||||
2. **Justified**: The spec states the product reason, boundaries, and standardized parts.
|
||||
3. **Accepted or rejected**: Review uses the exception type and anti-pattern catalog.
|
||||
4. **Potentially enforceable later**: Spec 201 may formalize recurring exception checks.
|
||||
|
||||
## Derived Outputs
|
||||
|
||||
The conceptual data model must support these concrete outputs in the constitution amendment:
|
||||
|
||||
- glossary terms for the required vocabulary
|
||||
- named anti-pattern entries
|
||||
- explicit exception types or exception clauses
|
||||
- state ownership guidance for shell/page/detail separation
|
||||
- a mapping note connecting Specs 196 through 199 to the amended rule family
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- database tables
|
||||
- Eloquent models
|
||||
- runtime registries or service abstractions
|
||||
- REST or GraphQL transport schemas
|
||||
- executable state machines
|
||||
- template or CI enforcement logic in this spec
|
||||
|
||||
## Amendment Target Inventory
|
||||
|
||||
| Amendment target | Amendment type | Problem classes absorbed | Source specs | Expected outputs | Deferred enforcement |
|
||||
|---|---|---|---|---|---|
|
||||
| `UI-FIL-001` | extension | native-by-default, fake-native drift, simple-overview nativity | 196 | `Native Surface`, `Custom Surface`, `Fake-Native Surface`, anti-pattern anchors | Spec 201 guardrails for fake-native patterns |
|
||||
| `UI-HARD-001` | extension | forbidden fake-native behavior, one-primary-interaction-model, state/inspect conflict classes | 196, 198 | anti-pattern catalog, hard prohibitions, state-layer violation names | Spec 201 grep/test checks for forbidden patterns |
|
||||
| `UI-EX-001` | extension | bounded legitimate custom surfaces, nativity exceptions, host variation, state special cases | 196, 197, 198 | finalized exception families and guardrails | Spec 201 checklist and exception review enforcement |
|
||||
| `UI-SURF-001` | extension | shared detail family taxonomy, host terminology | 197 | `Shared Detail Micro-UI`, `Host` | Spec 201 metadata/review guardrails |
|
||||
| `ACTSURF-001` | extension | one primary interaction model, host/family action ownership, shell/page separation pressure | 197, 199 | host/core action rules, `Parallel Inspect Worlds` review questions | Spec 201 action-surface enforcement |
|
||||
| `HDR-001` | clarification | host-owned header discipline for embedded shared families | 197 | header-specific host boundary language | Spec 201 header review prompts |
|
||||
| `Filament UI — Action Surface Contract` | extension | native simple-overview rule, shared-family contract ownership, explicit state ownership disclosure | 196, 197, 198 | Filament-facing execution rules | Spec 201 checklist/guard uptake |
|
||||
| `UX-001` | extension | shell/page/detail ownership, requested/active/draft/inspect/restorable roles | 198, 199 | state vocabulary and owner-layer rules | Spec 201 review and doc enforcement |
|
||||
| `UI-REVIEW-001` | extension | reviewer-facing classification, anti-pattern checks, state-owner questions | 196, 197, 198, 199 | explicit review questions and red flags | Spec 201 operationalization |
|
||||
|
||||
## Final Vocabulary Inventory
|
||||
|
||||
| Term | Governing sections | Source specs | Role in the amendment |
|
||||
|---|---|---|---|
|
||||
| `Native Surface` | `UI-SURF-001`, `UI-FIL-001`, `UI-HARD-001` | 196 | Default classification for standard Filament/shared-primitives work |
|
||||
| `Fake-Native Surface` | `UI-HARD-001`, `UI-FIL-001`, `UI-REVIEW-001` | 196 | Forbidden violation class for Filament-looking but contract-foreign UI |
|
||||
| `Custom Surface` | `UI-SURF-001`, `UI-EX-001`, `UI-FIL-001` | 196 | Legitimate non-native surface only with bounded product reason |
|
||||
| `Legitimate Exception` | `UI-EX-001`, `UI-REVIEW-001` | 196 | Shared language for approved deviations |
|
||||
| `Shared Detail Micro-UI` | `UI-SURF-001`, `ACTSURF-001`, `Filament UI — Action Surface Contract` | 197 | Repeated embedded family that must keep one shared contract |
|
||||
| `Host` | `UI-SURF-001`, `ACTSURF-001`, `HDR-001` | 197 | Parent page/resource/workbench owning route, auth, and host-only actions |
|
||||
| `Global Context State` | `UX-001`, `UI-REVIEW-001` | 199 | Shell-owned workspace/tenant truth |
|
||||
| `Page State` | `UX-001`, `UI-REVIEW-001` | 198 | Page-owned filter/tab/mode/selection truth |
|
||||
| `Detail State` | `UX-001`, `UI-REVIEW-001` | 197, 198 | Embedded viewer or inner inspect truth subordinate to page/shell |
|
||||
| `Requested State` | `UX-001`, `UI-REVIEW-001` | 198, 199 | Input state before validation or hydration |
|
||||
| `Active State` | `UX-001`, `UI-REVIEW-001` | 198, 199 | Current governing validated state |
|
||||
| `Draft State` | `UX-001`, `UI-REVIEW-001` | 198 | Pending local state not yet applied |
|
||||
| `Inspect State` | `UI-HARD-001`, `UX-001`, `UI-REVIEW-001` | 198 | Selected-record/detail focus truth |
|
||||
| `Restorable State` | `UX-001`, `UI-REVIEW-001` | 198 | The shareable subset intentionally recreated |
|
||||
| `Host Drift` | `UI-HARD-001`, `UI-REVIEW-001` | 197 | Forbidden host-side rewrite of shared-family core semantics |
|
||||
| `State Layer Collapse` | `UI-HARD-001`, `UX-001`, `UI-REVIEW-001` | 198, 199 | Forbidden multi-layer ownership of the same truth |
|
||||
| `Parallel Inspect Worlds` | `ACTSURF-001`, `UI-HARD-001`, `UI-REVIEW-001` | 198 | Forbidden competing inspect/view models for one concern |
|
||||
|
||||
## Final Anti-pattern Catalog
|
||||
|
||||
| Anti-pattern | Trigger signals | Default review outcome | Allowed exception path | Source specs |
|
||||
|---|---|---|---|---|
|
||||
| `Filament Costume` | Raw HTML/Tailwind controls mimic Filament semantics that native/shared primitives already provide | reject | `Nativity Exception` only when the semantic gap is explicit and narrow | 196 |
|
||||
| `Blade Request UI` | Primary body-state contract depends on `request()`, GET forms, or manual query parsing inside an active Filament surface | reject | `Nativity Exception` only for initialization-only request input | 196 |
|
||||
| `Hand-Rolled Simple Overview` | Simple report/overview with ordinary columns, filters, empty states, and navigation is rebuilt as bespoke markup | reject | `Legitimate Custom Surface Exception` only when the product need is materially richer than a table/list | 196 |
|
||||
| `Hidden Exception` | Custom behavior survives through history or convenience without a named exception block | reject | none | 196 |
|
||||
| `Host Drift` | One host changes core family zones, view semantics, or diagnostics contract without declaring host-scoped variation | reject | `Shared Detail Host Variation Exception` | 197 |
|
||||
| `State Layer Collapse` | Shell, page, or detail state each claim the same active truth or restoration role | reject | `State-Layer Special-case Exception` only when the owner hierarchy stays explicit | 198, 199 |
|
||||
| `Parallel Inspect Worlds` | Two same-concern inspect/open/select/view contracts coexist as peers | reject | none | 198 |
|
||||
|
||||
## Final Exception Relationships
|
||||
|
||||
| Exception type | Solves | Governing section | Standardized parts that must remain intact | Related anti-patterns |
|
||||
|---|---|---|---|---|
|
||||
| `Legitimate Custom Surface Exception` | Rich visualization, diagnostic/review work, multi-zone evidence, or shell-context-specific UI that does not fit standard CRUD/overview semantics | `UI-EX-001` | canonical nouns, scope clarity, explicit action hierarchy, explicit state ownership | `Hand-Rolled Simple Overview`, `Hidden Exception` |
|
||||
| `Nativity Exception` | Filament/shared primitives cannot express the required semantics cleanly | `UI-EX-001` | native/shared surrounding controls, no local status language, no request-owned primary body state | `Filament Costume`, `Blade Request UI`, `Hidden Exception` |
|
||||
| `Shared Detail Host Variation Exception` | A known shared family needs bounded host framing, assist entry, or optional-zone variation | `UI-EX-001` | family core zones, next-step contract, diagnostics contract, primary view/inspect model | `Host Drift` |
|
||||
| `State-Layer Special-case Exception` | A page legitimately needs explicit requested/active/draft/inspect/restorable distinctions beyond the simple default | `UI-EX-001` | one owner per layer, explicit restorable subset, explicit query role, no silent shell/page overlap | `State Layer Collapse`, `Parallel Inspect Worlds` |
|
||||
|
||||
## Final State Ownership Rules
|
||||
|
||||
| Layer | Owns truth | Allowed inputs | Query role | Forbidden overlaps | Source specs |
|
||||
|---|---|---|---|---|---|
|
||||
| `shell` | workspace/tenant context, tenantless state, shell recovery state | route context, explicit switch/select/clear flows, valid restore candidates | durable only when the shell contract explicitly allows restore | page tabs/filters owning workspace truth, detail viewers owning tenant scope | 199 |
|
||||
| `page` | filters, tabs, active modes, selected-record/page-level inspect state, applied analysis state | shell context, deeplink/init state, local interactions, explicit draft/apply actions | initialization-only, durable, or unsupported exactly as declared by the page contract | shell precedence logic, detail-local state redefining page truth | 198 |
|
||||
| `detail` | embedded viewer state, inner section/tab choice, family-local assist or reveal state | host/page state, local viewer controls | usually unsupported or local-only unless an exception documents otherwise | shell or page ownership of the same active inspect/view truth | 197, 198 |
|
||||
|
||||
## Final Cross-Spec Mapping
|
||||
|
||||
| Source spec | Problem class | Constitution targets | Status | Notes |
|
||||
|---|---|---|---|---|
|
||||
| `196` | Fake-native drift and simple-overview nativity | `UI-FIL-001`, `UI-HARD-001`, `UI-EX-001`, `UI-REVIEW-001` | consumed-by-spec-200 | Produces native/custom/fake-native language and the anti-pattern anchors |
|
||||
| `197` | Shared detail micro-UI host/core boundaries | `UI-SURF-001`, `ACTSURF-001`, `HDR-001`, `Filament UI — Action Surface Contract`, `UI-REVIEW-001` | consumed-by-spec-200 | Produces shared-family, host, host-drift, and header-boundary language |
|
||||
| `198` | Page-state ownership and inspect/restoration semantics | `UI-HARD-001`, `UX-001`, `UI-REVIEW-001` | consumed-by-spec-200 | Produces requested/active/draft/inspect/restorable vocabulary and inspect-conflict rules |
|
||||
| `199` | Shell-context truth and shell/page separation | `ACTSURF-001`, `UX-001`, `UI-REVIEW-001` | consumed-by-spec-200 | Produces global-context/page/detail ownership clarity |
|
||||
| `201` | Review, grep, lint, and regression enforcement | all of the above as downstream consumers | deferred-to-201 | Spec 201 consumes the final vocabulary directly and must not invent replacement categories |
|
||||
@ -1,171 +0,0 @@
|
||||
# Implementation Plan: UI/UX Constitution Extension: Filament Nativity & Custom Surface Rules
|
||||
|
||||
**Branch**: `200-filament-surface-rules` | **Date**: 2026-04-18 | **Spec**: `specs/200-filament-surface-rules/spec.md`
|
||||
**Input**: Feature specification from `specs/200-filament-surface-rules/spec.md`
|
||||
|
||||
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/scripts/` for helper scripts.
|
||||
|
||||
## Summary
|
||||
|
||||
Extend the existing UI constitution so Filament-native defaults, fake-native drift, legitimate custom surfaces, shared detail micro-UI families, and shell/page/detail state ownership are all reviewable through one integrated rule set. The implementation approach is docs-only: amend existing constitution sections rather than create a parallel UI rulebook, add the shared vocabulary and anti-pattern catalog grounded in Specs 196 through 199, define the exception model boundaries, and record a clean handoff to Spec 201 for enforcement.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: Markdown governance artifacts in a PHP 8.4.15 / Laravel 12 / Filament v5 / Livewire v4 repository
|
||||
**Primary Dependencies**: `.specify/memory/constitution.md`, `docs/ui/operator-ux-surface-standards.md`, adjacent Specs 196 through 199, existing UI rule IDs `UI-SURF-001`, `ACTSURF-001`, `HDR-001`, `UI-HARD-001`, `UI-EX-001`, `UI-REVIEW-001`, `UI-FIL-001`, `DECIDE-001`, and `UX-001`
|
||||
**Storage**: N/A
|
||||
**Testing**: Document review, representative-case validation, and checklist verification only
|
||||
**Validation Lanes**: N/A (docs-only governance change)
|
||||
**Target Platform**: Repository governance for the Laravel web application and its Filament admin/operator surfaces
|
||||
**Project Type**: Laravel monolith with docs-only planning artifacts
|
||||
**Performance Goals**: N/A; the success target is review clarity, constitution precision, and explicit scope boundaries rather than runtime performance
|
||||
**Constraints**: No parallel rulebook; no runtime routes or API contracts; no CI/grep/lint/test enforcement in this spec; legitimate custom surfaces must remain possible; Spec 201 remains the enforcement boundary
|
||||
**Scale/Scope**: One constitution amendment path, one supporting operator-UX standards reference, one spec artifact set, and cross-spec mapping across Specs 196 through 199 plus Spec 201
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
- Inventory-first: N/A (docs-only governance feature; no inventory or snapshot behavior changes)
|
||||
- Read/write separation: PASS (no product write path is introduced)
|
||||
- Graph contract path: PASS (no Graph calls or contract registry changes)
|
||||
- Deterministic capabilities: PASS (no capability derivation changes)
|
||||
- RBAC-UX: PASS (no authorization behavior changes; planned rules only reinforce existing boundaries)
|
||||
- Workspace isolation: PASS (no new workspace context path; shell/page/detail layering rules must preserve established workspace truth)
|
||||
- Tenant isolation: PASS (no new tenant access path; future rules explicitly forbid silent scope broadening)
|
||||
- Run observability: PASS (no `OperationRun` creation or lifecycle changes)
|
||||
- Ops-UX 3-surface feedback: N/A (no run feedback surfaces)
|
||||
- Ops-UX lifecycle + summary counts + guards: N/A (no run lifecycle change)
|
||||
- Ops-UX system runs: N/A
|
||||
- Automation: N/A (no queued/scheduled flow)
|
||||
- Data minimization: PASS (no runtime or persistence impact)
|
||||
- Test governance (TEST-GOV-001): PASS (`N/A` lanes and no runtime impact are explicit; enforcement-oriented follow-up is deferred to Spec 201)
|
||||
- Proportionality (PROP-001): PASS but triggered; the new vocabulary and taxonomy are bounded to already proven repo problem classes and do not create runtime architecture
|
||||
- No premature abstraction (ABSTR-001): PASS (no new runtime factory, registry, resolver, or service layer is introduced)
|
||||
- Persisted truth (PERSIST-001): PASS (repository docs only; no product truth added)
|
||||
- Behavioral state (STATE-001): PASS (state-layer terms are governance vocabulary, not new persisted or executable state families)
|
||||
- UI semantics (UI-SEM-001): PASS (the plan strengthens direct, reviewable domain-to-UI rules and rejects local semantic frameworks)
|
||||
- V1 explicitness / few layers (V1-EXP-001, LAYER-001): PASS (existing constitution sections are amended in place rather than layered with a second framework)
|
||||
- Spec discipline / bloat check (SPEC-DISC-001, BLOAT-001): PASS (all related semantic additions stay in one rule spec; enforcement stays separate in Spec 201)
|
||||
- Badge semantics (BADGE-001): PASS (no new badge taxonomy; the plan only clarifies when local status language is forbidden)
|
||||
- Filament-native UI (UI-FIL-001): PASS (this feature extends the rule instead of introducing alternative UI semantics)
|
||||
- UI/UX surface taxonomy (UI-CONST-001 / UI-SURF-001): PASS (the plan tightens existing taxonomy and exception logic)
|
||||
- Decision-first operating model (DECIDE-001): PASS (no new primary surface; the plan strengthens review vocabulary for future surfaces)
|
||||
- UI/UX inspect model and hard rules (UI-HARD-001): PASS (the plan adds fake-native and state-layer clarity without changing runtime inspect models)
|
||||
- Action-surface discipline (ACTSURF-001 / HDR-001 / UI-EX-001): PASS (the plan extends existing action and exception rules in place)
|
||||
|
||||
Gate status before Phase 0 research: PASS
|
||||
|
||||
## Test Governance Check
|
||||
|
||||
> **Fill for any runtime-changing or test-affecting feature. Docs-only or template-only work may state concise `N/A` or `none`.**
|
||||
|
||||
- **Test purpose / classification by changed surface**: N/A
|
||||
- **Affected validation lanes**: N/A
|
||||
- **Why this lane mix is the narrowest sufficient proof**: This feature is a docs-only governance and planning slice. Validation is review-oriented and does not require runtime or test-lane execution.
|
||||
- **Narrowest proving command(s)**: N/A
|
||||
- **Fixture / helper / factory / seed / context cost risks**: none
|
||||
- **Expensive defaults or shared helper growth introduced?**: no
|
||||
- **Heavy-family additions, promotions, or visibility changes**: none
|
||||
- **Closing validation and reviewer handoff**: Reviewers should verify the amended constitution against the representative cases from Specs 196 through 199, confirm that no runtime enforcement is claimed prematurely, and ensure the handoff to Spec 201 is explicit.
|
||||
- **Budget / baseline / trend follow-up**: none
|
||||
- **Review-stop questions**: Does any rule create a parallel rulebook? Does any clause imply runtime enforcement that belongs to Spec 201? Does the taxonomy overreach beyond proven repo cases? Does the docs-only contract remain clearly bounded?
|
||||
- **Escalation path**: none
|
||||
- **Why no dedicated follow-up spec is needed**: A dedicated follow-up already exists as Spec 201; this plan only prepares the vocabulary and constitution amendments that Spec 201 will later operationalize.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/200-filament-surface-rules/
|
||||
├── plan.md
|
||||
├── checklists/
|
||||
│ └── requirements.md
|
||||
├── research.md
|
||||
├── data-model.md
|
||||
├── quickstart.md
|
||||
├── contracts/
|
||||
│ └── constitution-governance-contract.md
|
||||
└── tasks.md
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
|
||||
```text
|
||||
.specify/
|
||||
├── memory/
|
||||
│ └── constitution.md
|
||||
└── templates/
|
||||
└── checklist-template.md # referenced only; enforcement edits remain deferred to Spec 201
|
||||
|
||||
docs/
|
||||
└── ui/
|
||||
└── operator-ux-surface-standards.md
|
||||
|
||||
specs/
|
||||
├── 196-hard-filament-nativity-cleanup/
|
||||
├── 197-shared-detail-contract/
|
||||
├── 198-monitoring-page-state/
|
||||
├── 199-global-context-shell-contract/
|
||||
└── 200-filament-surface-rules/
|
||||
```
|
||||
|
||||
**Structure Decision**: This is a docs-only governance feature. The implementation centers on `.specify/memory/constitution.md` plus the feature artifacts in `specs/200-filament-surface-rules/`. `docs/ui/operator-ux-surface-standards.md` is a reference alignment target only if wording drift is discovered during implementation. Checklist or CI operationalization remains explicitly deferred to Spec 201.
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
|-----------|------------|-------------------------------------|
|
||||
| None | N/A | N/A |
|
||||
|
||||
## Proportionality Review
|
||||
|
||||
- **Current operator problem**: Reviewers lack one stable language for classifying fake-native drift, bounded custom surfaces, shared-family host drift, and shell/page/detail state ownership. That makes future UI work inconsistent even when local specs identify specific defects.
|
||||
- **Existing structure is insufficient because**: The existing constitution has strong surface and action rules, but it does not yet state native-by-default, fake-native, shared-family, and explicit state-layer ownership as one integrated set grounded in the proven repo cases from Specs 196 through 199.
|
||||
- **Narrowest correct implementation**: Amend the existing constitution sections in place, add the minimal new vocabulary and anti-pattern catalog, define the exception boundary, and leave template/checklist/grep/test operationalization to Spec 201.
|
||||
- **Ownership cost created**: The repo gains a tighter review vocabulary and more precise constitutional language that maintainers must preserve in future UI specs and PRs.
|
||||
- **Alternative intentionally rejected**: A separate standalone UI constitution document, broad template enforcement in this spec, or a generic design-system framework detached from the existing constitution.
|
||||
- **Release truth**: current-release review truth and governance clarity
|
||||
|
||||
## Phase 0 — Outline & Research (Complete)
|
||||
|
||||
Outputs:
|
||||
- `specs/200-filament-surface-rules/research.md`
|
||||
|
||||
Unknowns resolved:
|
||||
- Which existing constitution sections should absorb the new rules so the repo avoids a parallel rulebook.
|
||||
- Which parts of Spec 200 are genuinely new versus clarifications or extensions of existing rule IDs.
|
||||
- How to model a contracts artifact for a docs-only governance slice without inventing runtime API endpoints.
|
||||
- Which adjacent documentation artifacts should be referenced for operator-language and progressive-disclosure alignment.
|
||||
- Which parts of review/checklist operationalization must stay deferred to Spec 201 to honor scope.
|
||||
|
||||
## Phase 1 — Design & Contracts (Complete)
|
||||
|
||||
Outputs:
|
||||
- `specs/200-filament-surface-rules/data-model.md`
|
||||
- `specs/200-filament-surface-rules/contracts/constitution-governance-contract.md`
|
||||
- `specs/200-filament-surface-rules/quickstart.md`
|
||||
|
||||
Design highlights:
|
||||
- The feature uses a conceptual data model only: amendment targets, vocabulary terms, anti-patterns, exception types, state-ownership rules, and cross-spec mappings.
|
||||
- No runtime API contract is introduced; the contract artifact explicitly records the docs-only governance scope, amendment targets, acceptance conditions, and deferred enforcement boundary.
|
||||
- The quickstart centers on amending existing constitution sections, validating representative cases, and leaving automation to Spec 201.
|
||||
|
||||
## Constitution Re-check (Post-Design)
|
||||
|
||||
Result: PASS
|
||||
|
||||
- The design remains docs-only and introduces no Graph calls, authorization changes, runtime state, or `OperationRun` behavior.
|
||||
- The proportionality trigger stays justified because the new taxonomy is bounded to proven repo problem classes and integrated into existing rule IDs.
|
||||
- No template, checklist, grep, lint, or runtime enforcement is pulled into this spec; those concerns remain explicitly deferred to Spec 201.
|
||||
- Legitimate custom surfaces remain available through the documented exception path; the design does not collapse all operator-facing UI into one Filament-only rule.
|
||||
|
||||
## Implementation Sequencing
|
||||
|
||||
1. Amend `.specify/memory/constitution.md` in place, targeting the existing sections `UI-FIL-001`, `UI-SURF-001`, `ACTSURF-001`, `HDR-001`, `UI-HARD-001`, `UI-EX-001`, `UI-REVIEW-001`, `Filament UI — Action Surface Contract`, and `UX-001` with the native-by-default, fake-native, shared-family, state-layer, reviewer-guidance, and exception rules.
|
||||
2. Add the glossary and anti-pattern catalog inside the amended constitution language so the new vocabulary is reviewable without creating a second rule source.
|
||||
3. Add the cross-spec mapping and close-out note in the Spec 200 artifact set, showing how Specs 196 through 199 feed the rules and what remains reserved for Spec 201.
|
||||
4. Validate the amended constitution against the representative cases from Specs 196 through 199 to confirm that each case can be classified through the new rule language alone.
|
||||
5. If wording drift is discovered, align `docs/ui/operator-ux-surface-standards.md` with the amended constitution without creating a competing standard or adding enforcement mechanics.
|
||||
6. Record explicit deferrals to Spec 201 for review checklist changes, grep/lint guards, test enforcement, or CI automation.
|
||||
@ -1,89 +0,0 @@
|
||||
# Quickstart: Implementing Spec 200
|
||||
|
||||
## Purpose
|
||||
|
||||
Use this sequence when turning Spec 200 into the actual constitution amendment.
|
||||
|
||||
This is a docs-only governance feature. Do not add runtime behavior, transport contracts, CI rules, or enforcement automation here.
|
||||
|
||||
## Inputs
|
||||
|
||||
- `specs/200-filament-surface-rules/spec.md`
|
||||
- `specs/200-filament-surface-rules/research.md`
|
||||
- `specs/200-filament-surface-rules/data-model.md`
|
||||
- `.specify/memory/constitution.md`
|
||||
- `docs/ui/operator-ux-surface-standards.md`
|
||||
- `specs/196-hard-filament-nativity-cleanup/spec.md`
|
||||
- `specs/197-shared-detail-contract/spec.md`
|
||||
- `specs/198-monitoring-page-state/spec.md`
|
||||
- `specs/199-global-context-shell-contract/spec.md`
|
||||
|
||||
## Steps
|
||||
|
||||
1. Start in `.specify/memory/constitution.md` and locate the existing sections targeted by the plan: `UI-SURF-001`, `ACTSURF-001`, `UI-HARD-001`, `UI-EX-001`, `Filament UI — Action Surface Contract`, `UX-001`, and `UI-FIL-001`.
|
||||
2. Add the minimum rule language needed to make native-by-default, fake-native, legitimate custom surfaces, shared detail families, and shell/page/detail state ownership explicit.
|
||||
3. Add the required vocabulary terms and anti-pattern names inside the amended constitution language instead of in a separate standalone document.
|
||||
4. Extend the exception model so legitimate deviations are named, bounded, and forced to state what remains standardized.
|
||||
5. Validate the amendment against the representative cases from Specs 196 through 199. Each case must be classifiable from the constitution text alone.
|
||||
6. Write the close-out note for Spec 200, clearly separating:
|
||||
- newly added or tightened constitution rules
|
||||
- clarifications to existing rules
|
||||
- items intentionally deferred to Spec 201
|
||||
7. Only if wording drift is discovered, align `docs/ui/operator-ux-surface-standards.md` with the amended constitution. Do not create a competing standards document.
|
||||
|
||||
## Representative Walkthrough
|
||||
|
||||
Use this exact walkthrough to validate that the amendment classifies the already proven repo cases without inventing new rule families during review.
|
||||
|
||||
| Source spec | Case to walk through | Constitution language that must classify it | Expected result |
|
||||
|---|---|---|---|
|
||||
| Spec 196 | Dependency edges, required-permissions filters, and evidence overview nativity cleanup | `UI-FIL-001`, `UI-HARD-001`, `UI-EX-001`, `UI-REVIEW-001` | Fake-native and simple-overview drift are rejected unless a bounded custom or nativity exception is explicit |
|
||||
| Spec 197 | Verification report and normalized diff/settings hosts | `UI-SURF-001`, `ACTSURF-001`, `HDR-001`, `Filament UI — Action Surface Contract`, `UI-REVIEW-001` | Shared-family core and host-owned variation are distinguishable; host drift is rejectable |
|
||||
| Spec 198 | Monitoring and compare state ownership | `UI-HARD-001`, `UX-001`, `UI-REVIEW-001` | Requested, active, draft, inspect, and restorable state roles are classifiable without treating them as one blur |
|
||||
| Spec 199 | Global context bar, tenantless shell, fallback behavior | `ACTSURF-001`, `UX-001`, `UI-REVIEW-001` | Global context state is clearly shell-owned and not silently re-owned by a page or partial |
|
||||
|
||||
### Walkthrough output checklist
|
||||
|
||||
1. Name the surface class and decide whether the surface is `Native Surface`, `Custom Surface`, or a `Shared Detail Micro-UI`.
|
||||
2. Decide whether any named anti-pattern appears: `Filament Costume`, `Blade Request UI`, `Hand-Rolled Simple Overview`, `Hidden Exception`, `Host Drift`, `State Layer Collapse`, or `Parallel Inspect Worlds`.
|
||||
3. If the case is still allowed, identify the exact exception type and the standardized parts that remain intact.
|
||||
4. Name which layer owns the relevant truth: `Global Context State`, `Page State`, or `Detail State`.
|
||||
5. Name which state roles matter: `Requested`, `Active`, `Draft`, `Inspect`, or `Restorable`.
|
||||
6. Stop if the review needs a new term. Spec 200 is only complete when the constitution text already contains the needed category.
|
||||
|
||||
## Cross-Spec Mapping
|
||||
|
||||
| Input spec | What Spec 200 absorbs | What remains outside Spec 200 |
|
||||
|---|---|---|
|
||||
| `196-hard-filament-nativity-cleanup` | native-by-default language, fake-native prohibitions, simple-overview default-to-native rule | runtime cleanup work and regression tests |
|
||||
| `197-shared-detail-contract` | shared detail micro-UI and host/core vocabulary, host-drift review gates | runtime host consolidation and regression tests |
|
||||
| `198-monitoring-page-state` | shell/page/detail ownership vocabulary and explicit state-role language | runtime page-state contract implementation and regression tests |
|
||||
| `199-global-context-shell-contract` | workspace-first shell ownership vocabulary and shell/page separation | runtime shell resolution and fallback behavior |
|
||||
| `201-*` | no new concepts; only enforcement consumers | checklist, grep, lint, CI, and regression operationalization |
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
- The constitution is still the single source of truth.
|
||||
- No new runtime route, API, database, or enforcement code has been introduced.
|
||||
- Legitimate custom surfaces are still possible through the exception model.
|
||||
- Fake-native drift is named clearly enough that reviewers can identify it quickly.
|
||||
- Shared detail family and state-layer rules are grounded in the already proven cases from Specs 197, 198, and 199.
|
||||
- The handoff to Spec 201 is explicit and does not require new conceptual categories.
|
||||
|
||||
## Close-out Summary
|
||||
|
||||
When the amendment is finished, leave one concise close-out note that separates:
|
||||
|
||||
- **New clauses and vocabulary**:
|
||||
native/custom/fake-native classification, shared detail micro-UI and host language, state-layer ownership terms, and the named anti-pattern catalog.
|
||||
- **Tightened existing clauses**:
|
||||
native-by-default expectations, one-primary-interaction-model discipline, shared-family host ownership, record-header discipline for embedded families, and explicit review questions.
|
||||
- **Deferred enforcement**:
|
||||
checklist operationalization, grep/lint guards, CI checks, and runtime or test-backed regression enforcement in Spec 201.
|
||||
|
||||
## Not In Scope Here
|
||||
|
||||
- changing `.specify/templates/checklist-template.md` for enforcement behavior
|
||||
- adding grep or lint guards
|
||||
- adding CI checks
|
||||
- inventing sample runtime endpoints or implementation code snippets to simulate enforcement
|
||||
@ -1,85 +0,0 @@
|
||||
# Research: UI/UX Constitution Extension: Filament Nativity & Custom Surface Rules
|
||||
|
||||
## Decision 1: Amend existing constitution sections instead of creating a parallel UI rulebook
|
||||
|
||||
- **Decision**: Integrate Spec 200 into the existing constitution sections `UI-SURF-001`, `ACTSURF-001`, `UI-HARD-001`, `UI-EX-001`, `Filament UI — Action Surface Contract`, and `UX-001`.
|
||||
- **Rationale**: The repo already has a binding UI constitution. Extending those sections keeps one authority path for surface taxonomy, action discipline, hard rules, exceptions, Filament-specific contracts, and layout or IA guidance. This directly satisfies the spec's requirement to avoid a parallel rule hierarchy.
|
||||
- **Alternatives considered**:
|
||||
- Create a new top-level “Filament Nativity Constitution” section: rejected because it would force reviewers to reconcile two overlapping rulebooks.
|
||||
- Keep the rules distributed only across Specs 196 through 199: rejected because reviewers still need one durable product-language source rather than four adjacent historical specs.
|
||||
|
||||
## Decision 2: Treat native-by-default and fake-native as explicit extensions of existing UI-FIL-001 and UI-HARD-001 behavior
|
||||
|
||||
- **Decision**: Add an explicit native-by-default rule and a named fake-native anti-pattern to the existing Filament-native and hard-rule sections instead of inventing new standalone IDs.
|
||||
- **Rationale**: `UI-FIL-001` already says native Filament components come first, but it does not yet give reviewers a sharp fake-native vocabulary or clearly outlaw GET-form/request-driven body state for standard interactions. Spec 196 proved that gap.
|
||||
- **Alternatives considered**:
|
||||
- Leave native-by-default implicit in existing Filament guidance: rejected because implicit guidance did not prevent the repo's fake-native drift cases.
|
||||
- Introduce a new `UI-FIL-NATIVE-001` rule ID: rejected because the problem belongs inside the existing Filament-native section and hard-rule family.
|
||||
|
||||
## Decision 3: Model shared detail micro-UI and host variation as an extension of the current action-surface and exception framework
|
||||
|
||||
- **Decision**: Add shared-family and host-variation rules beneath the existing Filament UI contract and exception model instead of describing them as an unrelated custom-UI system.
|
||||
- **Rationale**: Spec 197 proved that the repeated problem is not “custom UI is bad,” but that repeated detail surfaces need one shared contract before host-specific variation is allowed. The existing action-surface and exception sections already govern how surfaces, actions, and deviations are classified.
|
||||
- **Alternatives considered**:
|
||||
- Create a separate “shared micro-UI framework” document: rejected because it would overproduce structure for two proven families and separate the rule from the main constitution.
|
||||
- Treat each host difference as a local PR concern: rejected because that is what allowed host drift to accumulate in the first place.
|
||||
|
||||
## Decision 4: Reuse the state-language proven in Specs 198 and 199 rather than invent a new runtime state taxonomy
|
||||
|
||||
- **Decision**: Carry forward shell/page/detail ownership and requested/active/draft/inspect/restorable distinctions as constitution vocabulary only, with no new runtime state framework in this spec.
|
||||
- **Rationale**: Specs 198 and 199 already proved the important state distinctions. Spec 200's job is to make those distinctions reviewable across future UI work, not to create a new implementation layer.
|
||||
- **Alternatives considered**:
|
||||
- Invent a fresh cross-product state taxonomy here: rejected because the repo already has adjacent specs proving the needed terms.
|
||||
- Limit the spec to visual nativity only: rejected because state-layer collapse is one of the core drift classes this spec must cover.
|
||||
|
||||
## Decision 5: Extend the existing exception model instead of normalizing hidden exceptions
|
||||
|
||||
- **Decision**: Keep `UI-EX-001` as the home for bounded exceptions and extend it with Spec 200's legitimate custom-surface and host-variation needs.
|
||||
- **Rationale**: The repo already recognizes that exceptions must be named, justified, and tested. Spec 200 broadens that discipline to fake-native escapes, legitimate custom surfaces, shared-family host variation, and state-related special cases without weakening the existing exception posture.
|
||||
- **Alternatives considered**:
|
||||
- Let individual specs define their own exception vocabulary: rejected because that recreates local drift.
|
||||
- Ban all custom surfaces to avoid exceptions entirely: rejected because the product legitimately includes richer diagnostic, review, and visualization surfaces.
|
||||
|
||||
## Decision 6: Treat the contracts artifact as an explicit no-runtime governance contract
|
||||
|
||||
- **Decision**: Create a docs-only contract note under `contracts/` that records amendment targets, acceptance conditions, and deferrals instead of inventing REST or GraphQL endpoints.
|
||||
- **Rationale**: Spec 200 introduces no user-facing API, route, or transport contract. The contract surface is the constitution amendment itself, so the artifact should make that boundary explicit rather than fabricate runtime endpoints that the spec forbids.
|
||||
- **Alternatives considered**:
|
||||
- Create a fake OpenAPI file: rejected because it would imply runtime behavior the feature does not add.
|
||||
- Omit `contracts/` entirely: rejected because the planning workflow expects an artifact and the no-runtime boundary should be made explicit.
|
||||
|
||||
## Decision 7: Reference operator-UX standards for language and disclosure, but defer review-checklist operationalization to Spec 201
|
||||
|
||||
- **Decision**: Use `docs/ui/operator-ux-surface-standards.md` as the supporting operator-language and progressive-disclosure reference, but keep checklist-template and enforcement work out of Spec 200 unless a wording-only cross-reference becomes unavoidable.
|
||||
- **Rationale**: Spec 200 is the rule and vocabulary slice. Spec 201 is the enforcement slice. That mirrors the repo's existing TEST-GOV-001 pattern, where standing governance rules live in the constitution and later specs operationalize them in templates and CI.
|
||||
- **Alternatives considered**:
|
||||
- Update review checklists now: rejected because it would blur the line between constitution definition and enforcement.
|
||||
- Ignore operator-UX standards docs completely: rejected because the constitution language should remain aligned with the repo's normative operator-facing UI guidance.
|
||||
|
||||
## Decision 8: Keep the implementation footprint intentionally small
|
||||
|
||||
- **Decision**: Plan for constitution changes in `.specify/memory/constitution.md`, feature-local artifacts in `specs/200-filament-surface-rules/`, and at most wording alignment in `docs/ui/operator-ux-surface-standards.md`.
|
||||
- **Rationale**: This keeps the scope proportional to a docs-only governance feature and avoids importing template, CI, or application-code changes that the spec explicitly defers.
|
||||
- **Alternatives considered**:
|
||||
- Widen the plan to include `.specify/templates/checklist-template.md`: rejected because template operationalization is enforcement work for Spec 201.
|
||||
- Widen the plan to include runtime examples in `app/` or `tests/`: rejected because Spec 200 is not an implementation spec.
|
||||
|
||||
## Decision 9: Put the new vocabulary into the existing rule families and appendices instead of creating a separate glossary section
|
||||
|
||||
- **Decision**: Introduce the new terms and anti-patterns inside the already binding rule families (`UI-SURF-001`, `UI-HARD-001`, `UI-EX-001`, `UX-001`, `UI-REVIEW-001`, `UI-FIL-001`) plus the condensed appendix/checklist/red-flag appendices, rather than creating a standalone vocabulary chapter.
|
||||
- **Rationale**: Reviewers need the terms exactly where they classify behavior. A standalone glossary would recreate the split-rulebook problem this spec is supposed to remove.
|
||||
- **Alternatives considered**:
|
||||
- Add a new glossary-only top-level constitution section: rejected because the review language would become detached from the operative rules.
|
||||
- Keep the vocabulary only in the Spec 200 artifact set: rejected because the constitution would still be missing the durable review language.
|
||||
|
||||
## Decision 10: Review questions should absorb nativity, shared-family, and state-layer checks directly
|
||||
|
||||
- **Decision**: Expand `UI-REVIEW-001`, Appendix B, and Appendix C with the new classification and anti-pattern checks rather than invent a second reviewer rubric for Filament nativity or state ownership.
|
||||
- **Rationale**: The existing enforcement model and appendices are already the review intake for operator-facing UI changes. Extending that surface is the narrowest way to make the new rules usable.
|
||||
- **Alternatives considered**:
|
||||
- Create a dedicated nativity/state checklist: rejected because it would fragment one review routine into multiple parallel rubrics.
|
||||
- Leave the new language implicit and rely on reviewer judgment: rejected because that is the current failure mode.
|
||||
|
||||
## Implementation Adjustment Note
|
||||
|
||||
- During implementation, the repo-level operator UX standards document only needs wording alignment that points back to the constitution vocabulary. A second standards track for native/custom/shared/state terminology would be overproduction and is intentionally avoided.
|
||||
@ -1,314 +0,0 @@
|
||||
# Feature Specification: UI/UX Constitution Extension: Filament Nativity & Custom Surface Rules
|
||||
|
||||
**Feature Branch**: `[200-filament-surface-rules]`
|
||||
**Created**: 2026-04-18
|
||||
**Status**: Proposed
|
||||
**Input**: User description: "Spec 200 - extend the UI/UX constitution with Filament nativity rules, custom surface allowance, shared detail micro-UI rules, state layering rules, and a documented exception model."
|
||||
|
||||
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
|
||||
|
||||
- **Problem**: The product still lacks one explicit constitution that answers when an admin surface must stay Filament-native, when a custom surface is legitimate, how repeated detail micro-UIs should be cut, and where shell, page, and detail state belong.
|
||||
- **Today's failure**: Reviewers and implementers can spot local defects, but they still lack one shared rule language for deciding whether a surface is fake-native drift, a bounded custom exception, host drift inside a shared family, or state-layer collapse.
|
||||
- **User-visible improvement**: Operators get more consistent, predictable admin UX over time because future surface work is reviewed against one product rule set instead of personal style, delivery pressure, or historical accident.
|
||||
- **Smallest enterprise-capable version**: Extend the existing UI constitution with bounded rules and vocabulary grounded in Specs 196 through 199, document the exception model, and define the handoff to Spec 201. Do not absorb cleanup sweeps, runtime refactors, or enforcement automation.
|
||||
- **Explicit non-goals**: No repo-wide cleanup pass, no CI or lint enforcement, no new runtime architecture, no new product surfaces, no separate design-system handbook, no page-state or shell-state implementation rewrite, and no attempt to forbid all custom surfaces.
|
||||
- **Permanent complexity imported**: A bounded review vocabulary, additional constitution clauses, an explicit anti-pattern catalog, a documented exception model, and a mapping from Specs 196 through 199 into one enduring rule set that Spec 201 can later enforce.
|
||||
- **Why now**: Specs 196 through 199 already exposed the repeated problem classes, and Spec 201 cannot operationalize guardrails cleanly unless those rules are first named, bounded, and integrated into the existing constitution.
|
||||
- **Why not local**: Local cleanup specs fix instances, but they do not settle the recurring review questions that keep resurfacing across adjacent UI work and future operator-facing surfaces.
|
||||
- **Approval class**: Core Enterprise
|
||||
- **Red flags triggered**: Two red flags are present: foundation-sounding constitution work and new cross-surface vocabulary. Defense: the scope is tightly bounded to already proven repo problem classes, integrates into existing constitution sections instead of inventing a parallel system, and explicitly defers runtime enforcement and automation to Spec 201.
|
||||
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 1 | Komplexität: 1 | Produktnähe: 2 | Wiederverwendung: 2 | **Gesamt: 10/12**
|
||||
- **Decision**: approve
|
||||
|
||||
## Spec Scope Fields *(mandatory)*
|
||||
|
||||
- **Scope**: canonical-view
|
||||
- **Primary Routes**: No new runtime route is introduced. The governed surfaces are the existing operator-facing admin surfaces under `/admin/**` and `/admin/t/{tenant}/...`, plus the workspace-owned constitution artifact that defines how those surfaces must be reviewed and extended.
|
||||
- **Data Ownership**: Workspace-owned constitution text, review vocabulary, anti-pattern definitions, and handoff notes only. No tenant-owned records, runtime tables, or persisted product entities are introduced.
|
||||
- **RBAC**: This feature does not change product authorization behavior. Future implementing work remains bound to the existing workspace membership, tenant entitlement, capability, and deny-as-not-found rules already enforced elsewhere in the product.
|
||||
|
||||
For canonical-view specs, the spec MUST define:
|
||||
|
||||
- **Default filter behavior when tenant-context is active**: This feature defines no new page or route filter behavior. It requires future specs and reviews to state explicitly whether tenant-context is shell scope, page prefilter, detail context, or unsupported state, and to keep those layers separate.
|
||||
- **Explicit entitlement checks preventing cross-tenant leakage**: This feature introduces no new access path. Any future application of these rules must preserve the existing workspace and tenant boundaries and must not let shell, page, or detail state silently broaden tenant scope.
|
||||
|
||||
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||
|
||||
- **New source of truth?**: yes, but only as additional clauses inside the existing workspace-owned constitution rather than a new product runtime truth
|
||||
- **New persisted entity/table/artifact?**: yes, but only repository-owned artifacts such as constitution text, mapping notes, and checklist-level governance content
|
||||
- **New abstraction?**: yes, a bounded review vocabulary and exception model for UI surface classification
|
||||
- **New enum/state/reason family?**: yes, because the feature formalizes surface classes, state-layer distinctions, and named anti-pattern categories
|
||||
- **New cross-domain UI framework/taxonomy?**: yes, but only a bounded constitution taxonomy for already proven Filament-adjacent surface problems
|
||||
- **Current operator problem**: Surface quality still depends too much on who built the page and under what pressure. The same repo now contains fake-native controls, request-driven page bodies, shared-family drift, and mixed shell or page state with no single rule language that reviewers can apply consistently.
|
||||
- **Existing structure is insufficient because**: Specs 196 through 199 each proved one part of the problem, but without a shared constitution extension the repo still lacks one stable answer to whether a surface should be native, custom, shared-family, shell-owned, page-owned, or a documented exception.
|
||||
- **Narrowest correct implementation**: Extend the existing UI constitution sections with explicit definitions, native-by-default rules, custom-surface allowance, shared-family rules, state-layer rules, anti-patterns, and an exception model grounded in existing repo cases. Do not add runtime machinery, a new design system, or enforcement automation.
|
||||
- **Ownership cost**: Maintainers must preserve the vocabulary, keep the amended constitution coherent with future UI work, and ensure later specs and PRs use the same rule language instead of rephrasing it ad hoc.
|
||||
- **Alternative intentionally rejected**: Leave the topic split across adjacent specs, document local exceptions only in PRs, or create a separate parallel UI-rule document detached from the existing constitution.
|
||||
- **Release truth**: current-release review truth needed now to keep future UI work and Spec 201 aligned
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The repo audit did not show that the product is fundamentally misbuilt. It showed that UI and surface quality still depends too much on local authorship and delivery pressure.
|
||||
|
||||
The repeated drift patterns are already visible:
|
||||
|
||||
- plain HTML controls wearing Filament styling
|
||||
- Blade-body micro-UIs with their own interaction contract
|
||||
- shared detail families growing by include or fork instead of by one contract
|
||||
- page-level state that is sometimes local, sometimes query-driven, and sometimes hybrid without a declared hierarchy
|
||||
- shell or context logic that still leaks into partials instead of one explicit resolver contract
|
||||
- legitimate custom surfaces that remain undocumented exceptions and therefore look indistinguishable from accidental drift
|
||||
|
||||
The missing ingredient is not another local cleanup. It is a constitution extension that answers the recurring product questions directly:
|
||||
|
||||
- What must be native?
|
||||
- When is custom legitimate?
|
||||
- How is a special case justified?
|
||||
- Which state belongs to shell, page, or detail?
|
||||
- How does a reviewer identify a regression into the old drift patterns early?
|
||||
|
||||
Spec 200 exists to establish those answers as durable product rules.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Depends on Spec 196 - Hard Filament Nativity Cleanup for the proven fake-native and request-driven UI cases that motivate the native-by-default rules.
|
||||
- Depends on Spec 197 - Shared Detail Micro-UI Contract for the proven repeated-host problem and the need for shared-core versus host-variation rules.
|
||||
- Depends on Spec 198 - Monitoring Page-State Contract for the proven page-state taxonomy and the need to distinguish requested, active, draft, inspect, and restorable state.
|
||||
- Depends on Spec 199 - Global Context Shell Contract for the proven workspace-first shell truth and the need to separate shell context from page and detail state.
|
||||
- Feeds Spec 201 - Enforcement and Guardrails, which will operationalize the rules from this spec in review, grep, lint, tests, or other automation.
|
||||
- Does not absorb cleanup implementation, runtime state rewrites, or enforcement machinery from any adjacent spec.
|
||||
|
||||
## Goals
|
||||
|
||||
- Make it explicit when a surface must use Filament-native primitives.
|
||||
- Make it explicit when a custom surface is legitimate and how narrow that exception must stay.
|
||||
- Define how shared detail micro-UI families are cut into shared contract versus host-specific variation.
|
||||
- Define how shell, page, and detail state are separated and reviewed.
|
||||
- Establish a named anti-pattern catalog so reviews describe the same problem classes with the same words.
|
||||
- Give Spec 201 a stable rule set to enforce instead of asking enforcement work to invent the rules later.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Cleaning up all existing violations immediately.
|
||||
- Enabling CI, lint, grep, or test enforcement in this spec.
|
||||
- Reorganizing navigation or inventing new product surfaces.
|
||||
- Creating a generic Filament theory document detached from repo reality.
|
||||
- Reimplementing page-state or shell-state runtime behavior beyond what adjacent specs already cover.
|
||||
- Prohibiting legitimate custom visualizations, diagnostic viewers, or multi-zone evidence surfaces.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- The existing UI constitution sections, especially UI-CONST-001, DECIDE-001, UI-EX-001, UI-REVIEW-001, HDR-001, and UI-FIL-001, remain the binding base that this spec extends rather than replaces.
|
||||
- Filament-native admin surfaces remain the normal product shell for standard forms, filters, tables, badges, tabs, and actions.
|
||||
- Legitimate custom surfaces will continue to exist for richer visualization, diagnostics, diff, review, and embedded evidence work.
|
||||
- Enforcement is intentionally deferred to Spec 201, not partially implemented here.
|
||||
- Review usefulness matters more than architectural purity; if a rule does not help a reviewer classify a real repo case, it is too abstract.
|
||||
|
||||
## Key Decisions
|
||||
|
||||
- **Native by default**: Standard form, filter, table, action, and overview work belongs on native Filament or Livewire primitives when an appropriate primitive already exists.
|
||||
- **Fake-native is a first-class violation**: A surface that only looks native, but keeps a separate HTML, GET, or Blade-request contract for the same core interaction, is drift.
|
||||
- **Custom requires product reason**: Custom surfaces are legitimate only when the product need is genuinely richer than standard CRUD or overview semantics and the exception is bounded explicitly.
|
||||
- **Shared contract before host fork**: Repeated detail families must be described once as a shared family before host-specific variation is allowed.
|
||||
- **State belongs to the right layer**: Workspace and tenant truth belong to the shell, page interaction belongs to the page, and viewer micro-state belongs to the detail surface.
|
||||
- **One primary interaction model per concern**: Surfaces may not run two competing inspect, tab, filter, or selection contracts for the same user concern.
|
||||
- **Explicit exceptions beat implicit drift**: Legitimate special cases must be named, bounded, and reviewable instead of surviving through historical accident.
|
||||
|
||||
## Required Outcomes
|
||||
|
||||
### Constitution Integration
|
||||
|
||||
The existing constitution must gain explicit rule language for Filament nativity, fake-native prohibitions, custom-surface allowance, shared detail micro-UI families, shell or page or detail state layering, and exception handling without creating a second parallel rulebook.
|
||||
|
||||
### Shared Review Vocabulary
|
||||
|
||||
The product must gain one stable vocabulary for the following concepts:
|
||||
|
||||
- Native Surface
|
||||
- Fake-Native Surface
|
||||
- Custom Surface
|
||||
- Shared Detail Micro-UI
|
||||
- Host
|
||||
- Global Context State
|
||||
- Page State
|
||||
- Detail State
|
||||
- Legitimate Exception
|
||||
|
||||
### Anti-Pattern Catalog
|
||||
|
||||
The constitution must catalog the recurring failure modes that the repo audit already exposed, so reviewers can call them by name and reject them consistently.
|
||||
|
||||
### Exception Model
|
||||
|
||||
The constitution must define how a legitimate deviation from native-by-default or shared-contract rules is justified, bounded, and kept from turning into a general permission slip.
|
||||
|
||||
### Cross-Spec Mapping
|
||||
|
||||
The constitution extension must explain how Specs 196 through 199 feed the new rules and which part of the work remains reserved for Spec 201.
|
||||
|
||||
### Close-Out Note
|
||||
|
||||
The finished spec must leave one explicit summary of what constitution rules were added or amended, which existing rules were tightened or clarified, and which topics remain enforcement follow-up rather than implementation scope.
|
||||
|
||||
## Representative Validation Cases
|
||||
|
||||
- **Dependency edges detail surface**: A reviewer must be able to classify a dependency-edge surface as a native-surface violation when standard controls are replaced by GET-form or Blade-request state without a true product reason.
|
||||
- **Verification report host family**: A reviewer must be able to classify host-specific structural drift inside repeated verification-report surfaces as a shared-family violation unless a clearly justified subtype exists.
|
||||
- **Monitoring page inspect and filter state**: A reviewer must be able to classify mixed requested, active, draft, or inspect state as page-state drift rather than as a harmless local implementation detail.
|
||||
- **Global context shell behavior**: A reviewer must be able to classify shell partials that quietly own scope truth or remembered context as shell-contract violations rather than neutral presentation code.
|
||||
- **Legitimate special visualization**: A reviewer must still be able to approve a truly custom visualization or rich diagnostic viewer when the product reason is real and the exception remains narrow and explicit.
|
||||
|
||||
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||
|
||||
- **Test purpose / classification**: N/A
|
||||
- **Validation lane(s)**: N/A
|
||||
- **Why this classification and these lanes are sufficient**: This feature changes repository governance text and specification artifacts, not product runtime behavior. Validation is document-based and depends on completeness, clarity, and traceability to real repo cases.
|
||||
- **New or expanded test families**: none
|
||||
- **Fixture / helper cost impact**: none
|
||||
- **Heavy-family visibility / justification**: none
|
||||
- **Reviewer handoff**: Reviewers must confirm that the new rules are grounded in Specs 196 through 199, that no enforcement or runtime behavior is claimed without a follow-up implementation spec, that no parallel rulebook is created, and that the resulting vocabulary is sufficient to classify the representative cases above.
|
||||
- **Budget / baseline / trend impact**: none
|
||||
- **Escalation needed**: none
|
||||
- **Planned validation commands**: N/A
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Classify Nativity And Exceptions Consistently (Priority: P1)
|
||||
|
||||
As a reviewer, I want one constitution vocabulary for native, fake-native, and legitimate exception decisions so I can classify a surface without inventing local review language every time.
|
||||
|
||||
**Why this priority**: The main current pain is review inconsistency. If the spec cannot standardize the classification language, the rest of the rule set will remain too soft to matter.
|
||||
|
||||
**Independent Test**: Can be fully tested by reviewing a representative fake-native case and a representative legitimate custom case and verifying that both can be classified through the constitution alone.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a Filament-looking surface that still uses plain HTML controls and request-driven body state, **When** a reviewer applies the amended constitution, **Then** the reviewer can classify it as fake-native drift without adding new terminology.
|
||||
2. **Given** a rich diagnostic or visualization surface that does not fit standard CRUD or overview primitives, **When** a reviewer applies the amended constitution, **Then** the reviewer can approve it only through the documented exception model and product reason.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Cut Shared Detail Families And State Layers Correctly (Priority: P1)
|
||||
|
||||
As an author or reviewer of operator-facing UI, I want one rule set for shared detail families and shell, page, and detail state so that repeated hosts and complex monitoring pages do not quietly re-fork the product contract.
|
||||
|
||||
**Why this priority**: Shared-family drift and state-layer collapse were both already proven by adjacent specs and are the largest remaining sources of UI inconsistency after obvious fake-native cleanup.
|
||||
|
||||
**Independent Test**: Can be fully tested by evaluating one shared detail family and one monitoring or shell-context case and confirming that the constitution states where the shared contract ends, where host variation begins, and where each state class belongs.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** the same detail micro-UI appears in multiple hosts, **When** the reviewer applies the amended constitution, **Then** the reviewer can decide whether the hosts share one family contract or have drifted into unapproved forks.
|
||||
2. **Given** a page mixes query state, local state, inspect state, and shell context, **When** the reviewer applies the amended constitution, **Then** the reviewer can identify which state belongs to shell, page, or detail instead of treating the mixture as harmless implementation detail.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Prepare Enforcement Without Re-Inventing Rules (Priority: P2)
|
||||
|
||||
As the author of follow-up guardrails, I want the constitution to state the reviewable rule classes explicitly so that enforcement can be derived from stable product language rather than from one-off heuristics.
|
||||
|
||||
**Why this priority**: Spec 201 should operationalize this rule set, not rediscover it. Without an explicit handoff, enforcement work will either be too weak or will invent a different vocabulary.
|
||||
|
||||
**Independent Test**: Can be fully tested by mapping the amended constitution to a future guardrail backlog and verifying that the enforcement targets follow directly from the named rule and anti-pattern classes.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** the amended constitution is complete, **When** a maintainer defines follow-up enforcement targets, **Then** the maintainer can identify which rule classes are enforceable without needing new conceptual categories.
|
||||
2. **Given** a future UI change violates one of the named anti-patterns, **When** Spec 201 is planned, **Then** the follow-up spec can reference the same vocabulary and representative cases instead of restating the rule family from scratch.
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- A surface visually matches Filament but keeps its real behavior in GET forms, query parsing, or Blade-body request state.
|
||||
- A custom surface is genuinely justified but risks becoming a broad precedent for unrelated ad hoc markup.
|
||||
- A repeated detail surface appears in only one current host but is about to appear in another and must not be prematurely over-generalized.
|
||||
- Remembered shell context, requested page state, and local viewer state disagree on first load.
|
||||
- A historically grown special surface has no explicit exception record and must be classified as either an allowed exception to document or a drift case to reject.
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
**Constitution alignment (required):** This feature changes repository-owned constitution and review guidance only. It introduces no Microsoft Graph calls, no write workflow, no long-running job, and no new runtime route. Existing operator-facing pages remain governed by their current implementation and authorization contracts until follow-up specs or code changes adopt these rules.
|
||||
|
||||
**Constitution alignment (PROP-001 / ABSTR-001 / PERSIST-001 / STATE-001 / BLOAT-001):** This feature introduces a bounded new vocabulary and rule taxonomy inside the existing constitution. It does not add product persistence, runtime abstractions, or execution-state machinery. The proportionality review above explains why the new language is justified now and how the scope remains tighter than a generic UI framework.
|
||||
|
||||
**Constitution alignment (TEST-GOV-001):** This feature does not change runtime behavior or test families. Validation is document-based. Any future automation, linting, grep rules, or regression tests that enforce this constitution are deferred to Spec 201 and must carry their own lane and runtime impact disclosure.
|
||||
|
||||
**Constitution alignment (OPS-UX):** Not applicable. This feature creates no `OperationRun`, changes no run status transitions, and does not alter toast, progress, or terminal-notification rules.
|
||||
|
||||
**Constitution alignment (RBAC-UX):** This feature does not change authorization behavior. It reinforces that shell, page, and detail state rules must not bypass workspace membership, tenant entitlement, capability checks, or deny-as-not-found semantics in future implementations.
|
||||
|
||||
**Constitution alignment (OPS-EX-AUTH-001):** Not applicable. Authentication handshake behavior is unchanged.
|
||||
|
||||
**Constitution alignment (BADGE-001):** The feature must not create a new page-local badge or severity language. It extends existing UI rules so future surfaces know when native or shared semantics are required and when local status language is forbidden.
|
||||
|
||||
**Constitution alignment (UI-FIL-001):** This feature explicitly extends UI-FIL-001 by clarifying when Filament-native primitives are mandatory, which fake-native substitutes are prohibited, and when a product-grounded exception is legitimate.
|
||||
|
||||
**Constitution alignment (UI-NAMING-001):** The new vocabulary is review-facing and architectural, not operator-facing action copy. It must remain concise, stable, and tied to repeated repo problem classes rather than implementation jargon.
|
||||
|
||||
**Constitution alignment (DECIDE-001):** This feature does not create a new operator surface. It strengthens the review language that future specs and PRs must use when classifying surfaces, exceptions, and the human-in-the-loop role of a page.
|
||||
|
||||
**Constitution alignment (UI-CONST-001 / UI-SURF-001 / ACTSURF-001 / UI-HARD-001 / UI-EX-001 / UI-REVIEW-001 / HDR-001):** The feature must integrate into the existing UI constitution family, not sit beside it. Any new rule must either extend an existing section cleanly or add a narrowly scoped adjacent clause that still participates in the same review model.
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-200-001**: The product constitution MUST be extended inside the existing UI constitution structure rather than through a separate standalone rule document.
|
||||
- **FR-200-002**: The constitution MUST define the following terms explicitly: Native Surface, Fake-Native Surface, Custom Surface, Shared Detail Micro-UI, Host, Global Context State, Page State, Detail State, and Legitimate Exception.
|
||||
- **FR-200-003**: The constitution MUST state that standard form, filter, table, action, tab, badge, link, and overview work is native-by-default when suitable Filament or existing shared primitives are available.
|
||||
- **FR-200-004**: The constitution MUST prohibit plain HTML or Filament-costume markup as the primary interaction contract for standard controls when native primitives fit the job.
|
||||
- **FR-200-005**: The constitution MUST classify GET forms and Blade `request()` body-state as forbidden fake-native patterns when they act as the primary interaction contract inside a running Filament surface, unless an explicit and bounded exception is documented.
|
||||
- **FR-200-006**: The constitution MUST state that simple report or overview surfaces with ordinary columns, filters, empty states, and navigation default to native table semantics unless a real product reason requires a custom surface.
|
||||
- **FR-200-007**: The constitution MUST define the legitimate reasons for a custom surface, including richer visualization, high-value diagnostic or review work, multi-zone shared detail micro-UIs, shell-context-specific UI, and domain presentation that does not fit standard CRUD.
|
||||
- **FR-200-008**: The constitution MUST require every legitimate custom surface to declare its state layers, expected inputs, host-owned actions, shared-core versus host-specific variation, and any shareable or restorable state that matters to the operator.
|
||||
- **FR-200-009**: The constitution MUST state that speed of implementation, historical growth, or local convenience are not sufficient reasons for a custom-surface or fake-native exception.
|
||||
- **FR-200-010**: The constitution MUST define shared detail family rules that require a common core contract, mandatory zones, optional zones, host extension points, and explicit state responsibilities before host-specific variation is allowed.
|
||||
- **FR-200-011**: The constitution MUST limit host variation to contextual framing, allowed actions, and approved optional zones, and MUST classify structural host forks for the same family as drift unless a new subtype is explicitly justified.
|
||||
- **FR-200-012**: The constitution MUST define page-state rules that distinguish initial requested state, active page state, optional draft state, inspect state, and shareable or restorable state, and MUST require each state class to declare any query or URL role explicitly.
|
||||
- **FR-200-013**: The constitution MUST define global shell rules that keep workspace as the primary context, tenant as the secondary context, remembered context as subordinate convenience state, and shell partials as renderers of the context contract rather than owners of it.
|
||||
- **FR-200-014**: The constitution MUST require one primary interaction model per concern and MUST classify competing inspect, tab, filter, or selection contracts on the same surface as a violation unless a documented exception defines their hierarchy.
|
||||
- **FR-200-015**: The constitution MUST define an explicit exception model that requires each deviation from native-by-default or shared-family rules to state why native or default behavior does not fit, why custom behavior is necessary, what remains standardized, and how the exception stays narrow.
|
||||
- **FR-200-016**: The constitution MUST catalog at least the following anti-patterns explicitly: Filament Costume, Blade Request UI, Hand-Rolled Simple Overview, Host Drift, State Layer Collapse, Parallel Inspect Worlds, and Hidden Exception.
|
||||
- **FR-200-017**: The constitution MUST add reviewer-facing guidance and questions that let maintainers classify representative cases from Specs 196 through 199 without creating new local vocabulary during review.
|
||||
- **FR-200-018**: The feature MUST document how Specs 196 through 199 feed the new rules and which part of the problem remains delegated to Spec 201 for enforcement.
|
||||
- **FR-200-019**: The feature MUST produce a close-out summary describing which constitution rules were added, which existing rules were tightened or clarified, and which future enforcement topics remain intentionally deferred.
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- **NFR-200-001**: The amended rules MUST stay grounded in already observed repo problem classes and be precise enough that a reviewer can classify the representative cases without supplementary verbal explanation.
|
||||
- **NFR-200-002**: The feature MUST remain docs-only and governance-only. It MUST NOT introduce runtime routes, provider registration changes, assets, persistence, or Graph behavior.
|
||||
- **NFR-200-003**: The resulting constitution MUST not accidentally ban all custom surfaces. Legitimate rich visualization, diagnostics, evidence, and multi-zone detail work must remain available through the documented exception path.
|
||||
- **NFR-200-004**: The extension MUST integrate into the existing constitution and review model rather than creating a second manual or rule hierarchy that reviewers must reconcile separately.
|
||||
- **NFR-200-005**: The wording MUST be strong enough for future enforcement work to derive guardrails from it, but not so dogmatic that it overrides real product reasons for bounded custom behavior.
|
||||
|
||||
### Key Entities *(include if feature involves data)*
|
||||
|
||||
- **Native Surface Classification**: The constitution-level classification for surfaces whose core interaction is carried by Filament-native or existing shared primitives without a competing local contract.
|
||||
- **Fake-Native Surface**: A surface that visually imitates native admin UI while keeping its actual interaction contract in plain HTML, GET forms, ad hoc request parsing, or other unnecessary local behavior.
|
||||
- **Custom Surface Allowance**: The bounded rule that permits truly product-specific surfaces when standard CRUD or overview primitives are not sufficient.
|
||||
- **Shared Detail Micro-UI Family**: A repeated domain detail surface that must define shared core zones, optional zones, and host variation before multiple hosts can evolve safely.
|
||||
- **State Layer Classification**: The distinction between global shell context, page interaction state, and detail micro-state, including how requested, active, draft, inspect, and restorable state are separated.
|
||||
- **Legitimate Exception Record**: The explicit rule-level justification that allows a bounded deviation from native-by-default or shared-family rules.
|
||||
- **Anti-Pattern Class**: A named review category for recurring UI drift that should be rejected or remediated consistently.
|
||||
|
||||
## Deliverables
|
||||
|
||||
- **D-200-001**: A constitution amendment that integrates the new Filament nativity, custom-surface, shared-family, state-layer, and exception rules into the existing UI constitution.
|
||||
- **D-200-002**: A stable vocabulary and anti-pattern catalog covering native, fake-native, custom, shared detail micro-UI, shell or page or detail state, and legitimate exception.
|
||||
- **D-200-003**: An explicit exception model for bounded custom surfaces and deliberate deviations from native-by-default rules.
|
||||
- **D-200-004**: A mapping note that explains how Specs 196 through 199 feed the new constitution rules and how Spec 201 will consume them for enforcement.
|
||||
- **D-200-005**: A close-out note that lists new clauses, amended clauses, and intentionally deferred enforcement follow-up.
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-200-001**: The amended constitution explicitly defines all nine required vocabulary terms from FR-200-002 with no unresolved placeholder or clarification marker.
|
||||
- **SC-200-002**: During document review, a reviewer can classify one representative case from each of Specs 196, 197, 198, and 199 using only the amended constitution and the named anti-pattern or exception vocabulary.
|
||||
- **SC-200-003**: The amended constitution contains explicit rule language for native-by-default behavior, fake-native prohibitions, shared-family contracts, page-state layering, shell-context layering, and legitimate exceptions.
|
||||
- **SC-200-004**: The result integrates into the existing constitution model and does not create a parallel standalone rulebook for Filament-adjacent UI development.
|
||||
- **SC-200-005**: The handoff to Spec 201 identifies the enforceable rule classes without requiring Spec 201 to invent new conceptual categories.
|
||||
- **SC-200-006**: In acceptance review, a team member can explain when native is required, when custom is legitimate, how shared families are cut, and where state belongs after reading the amended constitution and this spec.
|
||||
|
||||
## Follow-up Assumptions
|
||||
|
||||
- Adjacent specs remain the authoritative evidence base for the specific repo cases this constitution extension names.
|
||||
- Future code changes will update the constitution and follow-up specs rather than treating this vocabulary as optional review prose.
|
||||
- Spec 201 will remain a separate enforcement slice instead of being partially absorbed into implementation PRs.
|
||||
@ -1,174 +0,0 @@
|
||||
# Tasks: UI/UX Constitution Extension: Filament Nativity & Custom Surface Rules
|
||||
|
||||
**Input**: Design documents from `/specs/200-filament-surface-rules/`
|
||||
**Prerequisites**: plan.md, spec.md, research.md, data-model.md, quickstart.md, contracts/constitution-governance-contract.md
|
||||
|
||||
**Tests**: N/A. This is a docs-only governance feature; no runtime or test-surface changes are planned in Spec 200.
|
||||
|
||||
## Test Governance Checklist
|
||||
|
||||
N/A for this docs-only governance feature. Keep the feature bounded to documentation and constitution artifacts only.
|
||||
|
||||
## Phase 1: Setup (Shared Context)
|
||||
|
||||
**Purpose**: Lock the implementation scope, amendment targets, and supporting source material before editing the constitution.
|
||||
|
||||
- [x] T001 Review `specs/200-filament-surface-rules/spec.md`, `specs/200-filament-surface-rules/plan.md`, and `specs/200-filament-surface-rules/research.md` to lock the amendment scope and Spec 201 deferrals.
|
||||
- [x] T002 [P] Inspect `.specify/memory/constitution.md` and `docs/ui/operator-ux-surface-standards.md` to confirm the exact insertion points for `UI-FIL-001`, `UI-SURF-001`, `ACTSURF-001`, `HDR-001`, `UI-HARD-001`, `UI-EX-001`, `UI-REVIEW-001`, `Filament UI — Action Surface Contract`, and `UX-001`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Foundational (Blocking Prerequisites)
|
||||
|
||||
**Purpose**: Prepare the shared governance artifacts and validation scaffolding that all story work depends on.
|
||||
|
||||
**⚠️ CRITICAL**: No user story work should begin until this phase is complete.
|
||||
|
||||
- [x] T003 Align `specs/200-filament-surface-rules/contracts/constitution-governance-contract.md` with the agreed amendment targets, acceptance contract, and explicit no-runtime boundary before constitution edits begin.
|
||||
- [x] T004 [P] Normalize the baseline amendment-target, vocabulary, anti-pattern, exception, and state-ownership definitions in `specs/200-filament-surface-rules/data-model.md`.
|
||||
- [x] T005 [P] Expand `specs/200-filament-surface-rules/quickstart.md` with the baseline representative validation flow for Specs 196 through 199 and the explicit Spec 201 handoff.
|
||||
|
||||
**Checkpoint**: Amendment scope, supporting vocabulary, and deferred enforcement boundary are locked.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: User Story 1 - Classify Nativity And Exceptions Consistently (Priority: P1) 🎯 MVP
|
||||
|
||||
**Goal**: Make native-by-default, fake-native drift, and legitimate custom-surface exceptions reviewable through the constitution alone.
|
||||
|
||||
**Independent Test**: A reviewer can classify a fake-native dependency surface and a legitimate custom visualization using only the amended constitution and the representative cases from Spec 196.
|
||||
|
||||
- [x] T006 [US1] Amend `.specify/memory/constitution.md` in `UI-FIL-001` and `UI-HARD-001` to add explicit native-by-default language and fake-native prohibitions.
|
||||
- [x] T007 [US1] Amend `.specify/memory/constitution.md` in `UI-EX-001` to define bounded legitimate custom-surface and nativity-exception rules.
|
||||
- [x] T008 [US1] Add `Native Surface`, `Fake-Native Surface`, `Custom Surface`, `Legitimate Exception`, plus the `Filament Costume`, `Blade Request UI`, `Hand-Rolled Simple Overview`, and `Hidden Exception` catalog entries in `.specify/memory/constitution.md`.
|
||||
- [x] T009 [US1] Amend `.specify/memory/constitution.md` in `UI-REVIEW-001` to add reviewer-facing nativity and exception-classification questions, then validate the amended language against `specs/196-hard-filament-nativity-cleanup/spec.md` and record any wording refinements in `specs/200-filament-surface-rules/quickstart.md`.
|
||||
|
||||
**Checkpoint**: Native and exception cases are classifiable without local review vocabulary.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: User Story 2 - Cut Shared Detail Families And State Layers Correctly (Priority: P1)
|
||||
|
||||
**Goal**: Make shared detail micro-UI families, host variation, and shell/page/detail state ownership explicit in the constitution.
|
||||
|
||||
**Independent Test**: A reviewer can classify shared-family drift and shell/page/detail state confusion using only the amended constitution and the representative cases from Specs 197 through 199.
|
||||
|
||||
- [x] T010 [US2] Amend `.specify/memory/constitution.md` in `UI-SURF-001`, `ACTSURF-001`, `HDR-001`, and `Filament UI — Action Surface Contract` to define shared detail micro-UI families, one-primary-interaction-model guidance, shared core zones, bounded host variation, and any related header-discipline clarifications.
|
||||
- [x] T011 [US2] Amend `.specify/memory/constitution.md` in `UX-001` and adjacent UI sections to define shell/page/detail ownership plus requested, active, draft, inspect, and restorable state roles.
|
||||
- [x] T012 [US2] Add `Shared Detail Micro-UI`, `Host`, `Global Context State`, `Page State`, `Detail State`, `Host Drift`, `State Layer Collapse`, and `Parallel Inspect Worlds` entries in `.specify/memory/constitution.md`.
|
||||
- [x] T013 [US2] Amend `.specify/memory/constitution.md` in `UI-REVIEW-001`, `ACTSURF-001`, `HDR-001`, and `UI-HARD-001` review gates to add reviewer-facing shared-family and state-layer questions, then validate the amended language against `specs/197-shared-detail-contract/spec.md`, `specs/198-monitoring-page-state/spec.md`, and `specs/199-global-context-shell-contract/spec.md`, and record any refinements in `specs/200-filament-surface-rules/quickstart.md`.
|
||||
|
||||
**Checkpoint**: Shared-family and state-layer cases are classifiable without inventing a second taxonomy.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: User Story 3 - Prepare Enforcement Without Re-Inventing Rules (Priority: P2)
|
||||
|
||||
**Goal**: Leave a clean cross-spec mapping and enforcement handoff so Spec 201 can operationalize the rules directly.
|
||||
|
||||
**Independent Test**: A maintainer can derive Spec 201 enforcement targets from the amended constitution and feature artifacts without introducing new rule categories.
|
||||
|
||||
- [x] T014 [US3] Update `specs/200-filament-surface-rules/contracts/constitution-governance-contract.md` with the finalized cross-spec mapping and the deferred enforcement boundary for Spec 201.
|
||||
- [x] T015 [P] [US3] Update `specs/200-filament-surface-rules/data-model.md` with the finalized `CrossSpecMapping` and exception-type relationships produced by the amended constitution.
|
||||
- [x] T016 [P] [US3] Add the close-out summary of new clauses, tightened clauses, and intentionally deferred enforcement work to `specs/200-filament-surface-rules/quickstart.md`.
|
||||
- [x] T017 [US3] Review `.specify/memory/constitution.md`, `specs/200-filament-surface-rules/contracts/constitution-governance-contract.md`, and `specs/200-filament-surface-rules/quickstart.md` together to verify that Spec 201 can consume the final vocabulary directly.
|
||||
|
||||
**Checkpoint**: The constitution amendment and Spec 201 handoff are aligned and explicit.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Polish & Cross-Cutting Concerns
|
||||
|
||||
**Purpose**: Final alignment, wording cleanup, and end-to-end validation across all stories.
|
||||
|
||||
- [x] T018 [P] Align any remaining wording drift between `.specify/memory/constitution.md` and `docs/ui/operator-ux-surface-standards.md`.
|
||||
- [x] T019 [P] Refresh `specs/200-filament-surface-rules/research.md` with any final amendment-target or rejected-alternative adjustments discovered during implementation.
|
||||
- [x] T020 Run the representative-case walkthrough in `specs/200-filament-surface-rules/quickstart.md` against `.specify/memory/constitution.md` plus `specs/196-hard-filament-nativity-cleanup/spec.md`, `specs/197-shared-detail-contract/spec.md`, `specs/198-monitoring-page-state/spec.md`, and `specs/199-global-context-shell-contract/spec.md`.
|
||||
- [x] T021 Clean up and cross-check `specs/200-filament-surface-rules/plan.md`, `specs/200-filament-surface-rules/contracts/constitution-governance-contract.md`, and `specs/200-filament-surface-rules/quickstart.md` so the final artifact set tells one consistent story.
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Execution Order
|
||||
|
||||
### Phase Dependencies
|
||||
|
||||
- **Setup (Phase 1)**: No dependencies.
|
||||
- **Foundational (Phase 2)**: Depends on Setup completion and blocks all story work.
|
||||
- **User Story 1 (Phase 3)**: Depends on Foundational completion.
|
||||
- **User Story 2 (Phase 4)**: Depends on Foundational completion.
|
||||
- **User Story 3 (Phase 5)**: Depends on User Stories 1 and 2 because the enforcement handoff must reflect the final amended rule language.
|
||||
- **Polish (Phase 6)**: Depends on all user stories being complete.
|
||||
|
||||
### User Story Dependencies
|
||||
|
||||
- **US1 (P1)**: Can start immediately after Foundational and serves as the MVP slice.
|
||||
- **US2 (P1)**: Can start after Foundational, but it edits the same constitution file as US1, so one implementer should do the core `.specify/memory/constitution.md` edits sequentially or split them by section ownership deliberately.
|
||||
- **US3 (P2)**: Starts after US1 and US2 stabilize because it documents the final mapping and Spec 201 handoff.
|
||||
|
||||
### Within Each User Story
|
||||
|
||||
- Core `.specify/memory/constitution.md` edits should be completed before validation tasks for that story.
|
||||
- Story validation tasks should feed any refinement updates into `specs/200-filament-surface-rules/quickstart.md`.
|
||||
- Close-out and handoff tasks should only run after the governing constitution language is stable.
|
||||
|
||||
### Parallel Opportunities
|
||||
|
||||
- `T002` can run in parallel with `T001` after the feature scope is understood.
|
||||
- `T004` and `T005` can run in parallel after `T003` establishes the final contract boundary.
|
||||
- `T015` and `T016` can run in parallel once the final constitution wording from US1 and US2 is stable.
|
||||
- `T018` and `T019` can run in parallel during Polish because they touch different files.
|
||||
|
||||
---
|
||||
|
||||
## Parallel Example: Foundational Work
|
||||
|
||||
```bash
|
||||
# After T003 locks the final contract boundary:
|
||||
Task: "Normalize the amendment-target, vocabulary, anti-pattern, exception, and state-ownership definitions in specs/200-filament-surface-rules/data-model.md"
|
||||
Task: "Expand specs/200-filament-surface-rules/quickstart.md with the representative validation flow for Specs 196 through 199 and the explicit Spec 201 handoff"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Parallel Example: User Story 3
|
||||
|
||||
```bash
|
||||
# After US1 and US2 stabilize the final constitution wording:
|
||||
Task: "Update specs/200-filament-surface-rules/data-model.md with the finalized CrossSpecMapping and exception-type relationships"
|
||||
Task: "Add the close-out summary of new clauses, tightened clauses, and intentionally deferred enforcement work to specs/200-filament-surface-rules/quickstart.md"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### MVP First (User Story 1 Only)
|
||||
|
||||
1. Complete Phase 1: Setup.
|
||||
2. Complete Phase 2: Foundational.
|
||||
3. Complete Phase 3: User Story 1.
|
||||
4. **STOP and VALIDATE**: Confirm a reviewer can classify a fake-native case and a legitimate custom case from the amended constitution alone.
|
||||
|
||||
### Incremental Delivery
|
||||
|
||||
1. Complete Setup + Foundational.
|
||||
2. Add User Story 1 and validate nativity/exception classification.
|
||||
3. Add User Story 2 and validate shared-family/state-layer classification.
|
||||
4. Add User Story 3 and validate the Spec 201 handoff.
|
||||
5. Finish with Polish and representative-case walkthrough.
|
||||
|
||||
### Parallel Team Strategy
|
||||
|
||||
With multiple contributors:
|
||||
|
||||
1. One contributor completes the contract-boundary task `T003`.
|
||||
2. A second contributor can prepare `T004` while another prepares `T005`.
|
||||
3. Once the constitution wording stabilizes, one contributor can finalize the contract note while another finalizes the close-out summary and data-model mapping.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- `[P]` tasks are parallelizable because they touch different files and do not depend on unfinished work in the same artifact.
|
||||
- User-story phases remain independently valuable, but US1 and US2 share `.specify/memory/constitution.md`, so section-level coordination matters.
|
||||
- No runtime tests, CI rules, or lint/grep enforcement should be added under this task list; those remain reserved for Spec 201.
|
||||
Loading…
Reference in New Issue
Block a user