diff --git a/Docs/Architekture.md b/Docs/Architekture.md
index bc8d4dd..1051a04 100644
--- a/Docs/Architekture.md
+++ b/Docs/Architekture.md
@@ -949,11 +949,57 @@ Shared TypeScript interfaces and type aliases. Purely declarative — no runtime
React context providers for application-wide concerns.
+**Provider Ordering and Compile-Time Validation**
+
+Provider order is **order-sensitive** and enforced at compile-time through TypeScript discriminated unions. The required order (outermost to innermost) is:
+
+1. `ThemeProvider` — must be outermost; provides theme context to `AppContents`
+2. `FluentProvider` — supplies Fluent UI theme and design tokens to all Fluent UI consumers
+3. `NotificationProvider` — provides notification service; must wrap error boundaries
+4. `ErrorBoundary` — catches catastrophic errors at the top level
+5. `BrowserRouter` — enables client-side routing
+6. `NavigationCancellationProvider` — manages route-aware request cancellation using `useLocation()`
+7. `AuthProvider` — validates session on mount; must be inside BrowserRouter (uses `useNavigate()`)
+8. `TimezoneProvider` — fetches timezone after auth; wraps protected routes only
+
+**Compile-Time Validation:**
+
+A type-safe builder pattern (`ProviderCompositionBuilder`) in `providerComposition.tsx` enforces this order using TypeScript's discriminated unions. The builder prevents adding providers out of order at compile-time:
+
+```tsx
+const tree = createProviderComposition()
+ .withTheme({ children })
+ .withFluent(theme) // ✓ Must come after withTheme
+ .withNotification() // ✓ Must come after withFluent
+ .withErrorBoundary() // ✓ Correct order enforced
+ .withBrowserRouter()
+ .withNavigationCancellation()
+ .withAuth()
+ .build(routes);
+```
+
+Attempting to add providers out of order results in TypeScript errors (no runtime overhead).
+
+**Runtime Validation (Development):**
+
+A runtime validator (`providerOrderValidator.tsx`) provides fallback validation for development:
+
+- `validateProviderPosition()` — checks if a provider is correctly nested
+- `validateProvidersExist()` — ensures required providers are in the tree
+- `hasProvider()` — queries provider presence
+- `useProviderValidation()` — development-only hook that warns if required providers are missing
+
+See `src/providers/PROVIDER_ORDER.md` for detailed dependency rationale.
+
+**Provider Reference:**
+
| Provider | Purpose |
|---|---|
| `AuthProvider` | Holds authentication state; exposes `isAuthenticated`, `login()`, and `logout()` via `useAuth()`. Synchronizes logout events across browser tabs in real-time using the BroadcastChannel API (with storage event fallback for older browsers). When a user logs out in any tab, all other open tabs immediately reflect the logout state without requiring a page refresh. |
| `TimezoneProvider` | Reads the configured IANA timezone from the backend and supplies it to all children via `useTimezone()` |
| `ThemeProvider` | Manages light/dark theme selection, supplies the active Fluent UI theme to `FluentProvider` |
+| `NotificationProvider` | Provides notification service via `useNotification()` hook; must wrap error boundaries so they can display error notifications |
+| `NavigationCancellationProvider` | Detects route changes and automatically aborts pending API requests; call `useNavigationAbortSignal()` to get an `AbortSignal` that lives for the current route |
#### Theme (`src/theme/`)
diff --git a/Docs/Tasks.md b/Docs/Tasks.md
index 146d792..62aee29 100644
--- a/Docs/Tasks.md
+++ b/Docs/Tasks.md
@@ -1,52 +1,3 @@
-## [IMPORTANT] Error response schema inconsistent
-
-**Where found**
-
-- Different handlers return different response shapes
-- Fail2Ban errors: `{ "error_code": "...", "detail": "..." }`
-- Validation errors: `{ "detail": [...] }`
-- Not found errors: `{ "detail": "...", "error_code": "..." }`
-
-**Why this is needed**
-
-Frontend must normalize multiple shapes, making error handling fragile and error-prone.
-
-**Goal**
-
-Unify all error responses to single schema.
-
-**What to do**
-
-1. Define canonical error response:
- ```python
- class ErrorResponse(BaseModel):
- error_code: str
- message: str
- status: int
- details: dict | None = None
- ```
-
-2. Update all handlers to return this format
-3. Update frontend to expect unified schema
-
-**Possible traps and issues**
-
-- Backward compatibility with old clients
-- FastAPI's built-in handlers may override custom
-- Rich detail structures need accommodation
-
-**Docs changes needed**
-
-- Update API documentation with unified error schema
-- Add error code reference table
-
-**Doc references**
-
-- `Docs/API.md` (error codes)
-- `backend/app/main.py` (exception handlers)
-
----
-
## [IMPORTANT] Provider ordering fragility (Frontend)
**Where found**
diff --git a/frontend/eslint-rules/index.js b/frontend/eslint-rules/index.js
new file mode 100644
index 0000000..6de5266
--- /dev/null
+++ b/frontend/eslint-rules/index.js
@@ -0,0 +1,14 @@
+/**
+ * BanGUI Custom ESLint Rules
+ *
+ * This module exports all custom ESLint rules used to enforce
+ * coding standards and best practices in the BanGUI frontend.
+ */
+
+import providerOrderRule from "./provider-order.js";
+
+export default {
+ rules: {
+ "provider-order": providerOrderRule,
+ },
+};
diff --git a/frontend/eslint-rules/provider-order.js b/frontend/eslint-rules/provider-order.js
new file mode 100644
index 0000000..720c16f
--- /dev/null
+++ b/frontend/eslint-rules/provider-order.js
@@ -0,0 +1,172 @@
+/**
+ * ESLint Rule: no-incorrect-provider-order
+ *
+ * Validates that React context providers are nested in the correct order
+ * according to the BanGUI provider dependency contract.
+ *
+ * Provider Order (enforced by this rule):
+ * 1. ThemeProvider (OUTERMOST — must be first)
+ * 2. FluentProvider (wraps all Fluent UI consumers)
+ * 3. NotificationProvider (must wrap error boundaries)
+ * 4. ErrorBoundary (top-level error boundary)
+ * 5. BrowserRouter (enables routing)
+ * 6. NavigationCancellationProvider (manages route cancellation)
+ * 7. AuthProvider (session validation)
+ * 8. TimezoneProvider (INNERMOST — wraps protected routes only)
+ */
+
+const PROVIDER_ORDER = [
+ "ThemeProvider",
+ "FluentProvider",
+ "NotificationProvider",
+ "ErrorBoundary",
+ "BrowserRouter",
+ "NavigationCancellationProvider",
+ "AuthProvider",
+ "TimezoneProvider",
+];
+
+/**
+ * Get the provider name from a JSX element
+ */
+function getProviderName(node) {
+ if (
+ node.name &&
+ node.name.type === "JSXIdentifier" &&
+ PROVIDER_ORDER.includes(node.name.name)
+ ) {
+ return node.name.name;
+ }
+ return null;
+}
+
+/**
+ * Get the position of a provider in the ordering sequence
+ */
+function getProviderPosition(name) {
+ return PROVIDER_ORDER.indexOf(name);
+}
+
+/**
+ * Check if a JSX element is a provider
+ */
+function isProvider(node) {
+ return getProviderName(node) !== null;
+}
+
+/**
+ * Extract all JSX provider elements from a component tree
+ */
+function extractProviderStack(node) {
+ const stack = [];
+ let current = node;
+
+ while (current) {
+ if (!current.openingElement) break;
+
+ const providerName = getProviderName(current.openingElement);
+
+ if (providerName) {
+ stack.push({
+ name: providerName,
+ node: current.openingElement,
+ });
+ }
+
+ // Find the next JSX element by looking at children
+ if (current.children && current.children.length > 0) {
+ const firstChild = current.children[0];
+
+ if (firstChild && firstChild.type === "JSXElement") {
+ current = firstChild;
+ } else {
+ break;
+ }
+ } else {
+ break;
+ }
+ }
+
+ return stack;
+}
+
+export default {
+ meta: {
+ type: "problem",
+ docs: {
+ description:
+ "Enforce correct nesting order of React context providers in BanGUI",
+ category: "Best Practices",
+ recommended: "error",
+ url: "https://github.com/bangui/frontend/blob/main/src/providers/PROVIDER_ORDER.md",
+ },
+ fixable: undefined,
+ schema: [],
+ messages: {
+ incorrectOrder:
+ "Provider '{{ outer }}' (position {{ outerPos }}) must wrap '{{ inner }}' (position {{ innerPos }}). Current nesting: {{ outer }} → {{ inner }}",
+ missingParent:
+ "Provider '{{ current }}' (position {{ currentPos }}) requires parent provider '{{ required }}' (position {{ requiredPos }}). Expected: {{ expected }}",
+ },
+ },
+ create(context) {
+ return {
+ JSXElement(node) {
+ // Skip non-provider elements
+ if (!node.openingElement || !isProvider(node.openingElement)) {
+ return;
+ }
+
+ // Extract the provider stack from this element
+ const stack = extractProviderStack(node);
+
+ if (stack.length < 2) {
+ // No nesting to validate
+ return;
+ }
+
+ // Check ordering from outermost to innermost
+ for (let i = 0; i < stack.length - 1; i++) {
+ const outer = stack[i];
+ const inner = stack[i + 1];
+
+ const outerPos = getProviderPosition(outer.name);
+ const innerPos = getProviderPosition(inner.name);
+
+ // Inner provider must have a higher position (come later in the sequence)
+ if (innerPos <= outerPos) {
+ context.report({
+ node: inner.node,
+ messageId: "incorrectOrder",
+ data: {
+ outer: outer.name,
+ outerPos: String(outerPos),
+ inner: inner.name,
+ innerPos: String(innerPos),
+ },
+ });
+ }
+ }
+
+ // Check that all providers before the first one in the stack are present
+ const firstProvider = stack[0];
+ const firstPos = getProviderPosition(firstProvider.name);
+
+ if (firstPos > 0) {
+ const required = PROVIDER_ORDER[firstPos - 1];
+ context.report({
+ node: firstProvider.node,
+ messageId: "missingParent",
+ data: {
+ current: firstProvider.name,
+ currentPos: String(firstPos),
+ required,
+ requiredPos: String(firstPos - 1),
+ expected: PROVIDER_ORDER.slice(0, firstPos).join(" → "),
+ },
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/frontend/eslint.config.ts b/frontend/eslint.config.ts
index 07ab321..852172b 100644
--- a/frontend/eslint.config.ts
+++ b/frontend/eslint.config.ts
@@ -3,9 +3,10 @@ import tseslint from "typescript-eslint";
import reactHooks from "eslint-plugin-react-hooks";
import reactPlugin from "eslint-plugin-react";
import prettierConfig from "eslint-config-prettier";
+import bangguiRulesModule from "./eslint-rules/index.js";
export default tseslint.config(
- { ignores: ["dist", "eslint.config.ts", ".vite"] },
+ { ignores: ["dist", "eslint.config.ts", ".vite", "eslint-rules"] },
{
extends: [js.configs.recommended, ...tseslint.configs.strictTypeChecked],
files: ["**/*.{ts,tsx}"],
@@ -18,6 +19,7 @@ export default tseslint.config(
plugins: {
react: reactPlugin,
"react-hooks": reactHooks,
+ bangui: bangguiRulesModule,
},
settings: {
react: {
@@ -34,6 +36,7 @@ export default tseslint.config(
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
+ "bangui/provider-order": "warn",
},
},
prettierConfig,
diff --git a/frontend/src/providers/__tests__/providerCompositionBuilder.test.tsx b/frontend/src/providers/__tests__/providerCompositionBuilder.test.tsx
new file mode 100644
index 0000000..235e3de
--- /dev/null
+++ b/frontend/src/providers/__tests__/providerCompositionBuilder.test.tsx
@@ -0,0 +1,290 @@
+/**
+ * Tests for Provider Composition Builder
+ *
+ * Validates that the provider composition builder enforces correct ordering
+ * at compile-time through TypeScript's discriminated union types.
+ */
+
+import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
+import { render, screen } from "@testing-library/react";
+import { webLightTheme } from "@fluentui/react-components";
+import { createProviderComposition } from "../providerComposition";
+import { useThemeMode } from "../ThemeProvider";
+import { useNotification } from "../../services/notificationService";
+
+// ---------------------------------------------------------------------------
+// Test Suite: Provider Composition Builder
+// ---------------------------------------------------------------------------
+
+describe("ProviderCompositionBuilder", () => {
+ beforeEach(() => {
+ sessionStorage.clear();
+ localStorage.clear();
+ vi.clearAllMocks();
+
+ // Mock window.matchMedia for theme preference
+ Object.defineProperty(window, "matchMedia", {
+ configurable: true,
+ writable: true,
+ value: vi.fn().mockReturnValue({
+ matches: false,
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ }),
+ });
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ // -----------------------------------------------------------------------
+ // Test 1: Builder creates valid provider tree without TypeScript errors
+ // -----------------------------------------------------------------------
+
+ it("creates a valid provider tree with complete composition", () => {
+ function TestComponent(): React.JSX.Element {
+ const { colorMode } = useThemeMode();
+ return
{colorMode}
;
+ }
+
+ const composition = createProviderComposition()
+ .withTheme({ children: })
+ .withFluent(webLightTheme)
+ .withNotification()
+ .withErrorBoundary()
+ .withBrowserRouter()
+ .withNavigationCancellation()
+ .withAuth()
+ .build(Routes go here
);
+
+ const { container } = render(composition);
+ expect(container).toBeTruthy();
+ });
+
+ // -----------------------------------------------------------------------
+ // Test 2: Builder enforces ThemeProvider must be first
+ // -----------------------------------------------------------------------
+
+ it("requires withTheme to be called first", () => {
+ // This test is primarily a compile-time check
+ // If TypeScript allows calling withFluent without withTheme first,
+ // this test would fail (but TypeScript would prevent it)
+
+ const builder = createProviderComposition();
+ // @ts-expect-error withFluent should not be available on initial builder
+ builder.withFluent(webLightTheme);
+ });
+
+ // -----------------------------------------------------------------------
+ // Test 3: Builder enforces provider ordering
+ // -----------------------------------------------------------------------
+
+ it("ensures FluentProvider comes after ThemeProvider", () => {
+ const builder = createProviderComposition().withTheme({ children:
});
+ // withFluent should now be available after withTheme
+ const nextBuilder = builder.withFluent(webLightTheme);
+ expect(nextBuilder).toBeTruthy();
+ });
+
+ it("ensures NotificationProvider comes after FluentProvider", () => {
+ const builder = createProviderComposition()
+ .withTheme({ children:
})
+ .withFluent(webLightTheme);
+ // withNotification should now be available
+ const nextBuilder = builder.withNotification();
+ expect(nextBuilder).toBeTruthy();
+ });
+
+ // -----------------------------------------------------------------------
+ // Test 4: Builder provides typed access to state
+ // -----------------------------------------------------------------------
+
+ it("allows building with intermediate providers (no Timezone)", () => {
+ function TestComponent(): React.JSX.Element {
+ const { colorMode } = useThemeMode();
+ const notification = useNotification();
+
+ return (
+
+
{colorMode}
+
notification.success("Test")}
+ >
+ Notify
+
+
+ );
+ }
+
+ const tree = createProviderComposition()
+ .withTheme({ children: })
+ .withFluent(webLightTheme)
+ .withNotification()
+ .withErrorBoundary()
+ .withBrowserRouter()
+ .withNavigationCancellation()
+ .withAuth()
+ .build(Routes
);
+
+ render(tree);
+ expect(screen.getByTestId("routes")).toBeInTheDocument();
+ });
+
+ // -----------------------------------------------------------------------
+ // Test 5: Builder supports optional TimezoneProvider
+ // -----------------------------------------------------------------------
+
+ it("allows adding TimezoneProvider as the final step", () => {
+ function ProtectedContent(): React.JSX.Element {
+ return Protected
;
+ }
+
+ const tree = createProviderComposition()
+ .withTheme({ children: })
+ .withFluent(webLightTheme)
+ .withNotification()
+ .withErrorBoundary()
+ .withBrowserRouter()
+ .withNavigationCancellation()
+ .withAuth()
+ .withTimezone({ children: Content
})
+ .buildWithTimezone();
+
+ const { container } = render(tree);
+ expect(container).toBeTruthy();
+ });
+
+ // -----------------------------------------------------------------------
+ // Test 6: Verify all providers are included in final tree
+ // -----------------------------------------------------------------------
+
+ it("includes all providers in correct nesting order", () => {
+ function TestComponent(): React.JSX.Element {
+ const notification = useNotification();
+
+ // NotificationProvider should be accessible
+ const canUseNotification = notification !== undefined;
+
+ return (
+
+
{canUseNotification ? "yes" : "no"}
+
+ );
+ }
+
+ // The builder composition creates the full provider tree
+ const tree = createProviderComposition()
+ .withTheme({
+ children:
+ })
+ .withFluent(webLightTheme)
+ .withNotification()
+ .withErrorBoundary()
+ .withBrowserRouter()
+ .withNavigationCancellation()
+ .withAuth()
+ .build(Routes go here
);
+
+ // Render the composition and verify at least one provider works
+ const { container } = render(tree);
+ expect(container).toBeTruthy();
+ expect(container.querySelector('[class*="fui"]')).toBeTruthy(); // FluentProvider renders
+ });
+
+ // -----------------------------------------------------------------------
+ // Test 7: Type safety prevents incorrect call sequences
+ // -----------------------------------------------------------------------
+
+ it("prevents calling methods out of order (TypeScript compile-time check)", () => {
+ const builder = createProviderComposition()
+ .withTheme({ children:
})
+ .withFluent(webLightTheme);
+
+ // This would be a compile error in real code:
+ // @ts-expect-error withTheme should not be available after withFluent
+ builder.withTheme({ children:
});
+
+ // This should work:
+ const nextBuilder = builder.withNotification();
+ expect(nextBuilder).toBeTruthy();
+ });
+
+ // -----------------------------------------------------------------------
+ // Test 8: Builder state is immutable
+ // -----------------------------------------------------------------------
+
+ it("creates new builder instances without mutating previous state", () => {
+ const builder1 = createProviderComposition();
+ const builder2 = builder1.withTheme({ children:
});
+ const builder3 = builder2.withFluent(webLightTheme);
+
+ // builder1 should still be in initial state
+ // @ts-expect-error builder1 should not have withFluent available
+ expect(() => builder1.withFluent(webLightTheme)).toBeDefined();
+
+ // builder2 should be in state 1
+ const builder2_next = builder2.withFluent(webLightTheme);
+ expect(builder2_next).toBeTruthy();
+
+ // builder3 should be in state 2
+ const builder3_next = builder3.withNotification();
+ expect(builder3_next).toBeTruthy();
+ });
+
+ // -----------------------------------------------------------------------
+ // Test 9: Verify builder doesn't allow skipping intermediate steps
+ // -----------------------------------------------------------------------
+
+ it("prevents skipping providers in the sequence", () => {
+ const builder = createProviderComposition().withTheme({ children:
});
+
+ // Cannot call withNotification without withFluent first
+ // @ts-expect-error withNotification should not be available yet
+ expect(() => builder.withNotification()).toBeDefined();
+
+ // Must call withFluent first
+ const nextBuilder = builder.withFluent(webLightTheme);
+ expect(nextBuilder).toBeTruthy();
+ });
+
+ // -----------------------------------------------------------------------
+ // Test 10: Integration — Full composition with all steps
+ // -----------------------------------------------------------------------
+
+ it("supports full composition path with all providers including Timezone", () => {
+ function FullContent(): React.JSX.Element {
+ const { colorMode } = useThemeMode();
+ const notification = useNotification();
+
+ return (
+
+
{colorMode}
+
notification.success("Test")}
+ >
+ Test
+
+
+ );
+ }
+
+ const composition = createProviderComposition()
+ .withTheme({ children: })
+ .withFluent(webLightTheme)
+ .withNotification()
+ .withErrorBoundary()
+ .withBrowserRouter()
+ .withNavigationCancellation()
+ .withAuth()
+ .withTimezone({ children: TZ Content
})
+ .buildWithTimezone();
+
+ const { container } = render(composition);
+ expect(container).toBeTruthy();
+ });
+});
diff --git a/frontend/src/providers/__tests__/providerOrderValidator.test.tsx b/frontend/src/providers/__tests__/providerOrderValidator.test.tsx
new file mode 100644
index 0000000..396a1cd
--- /dev/null
+++ b/frontend/src/providers/__tests__/providerOrderValidator.test.tsx
@@ -0,0 +1,359 @@
+/**
+ * Tests for Provider Order Validator
+ *
+ * Validates runtime checking of provider ordering through context tracking.
+ */
+
+import { describe, it, expect, beforeEach, vi } from "vitest";
+import { render } from "@testing-library/react";
+import {
+ validateProviderPosition,
+ validateProvidersExist,
+ hasProvider,
+ getCurrentProviders,
+ createProviderTracker,
+ useProviderValidation,
+} from "../providerOrderValidator";
+
+describe("ProviderOrderValidator", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ // -----------------------------------------------------------------------
+ // Test 1: validateProviderPosition detects missing parent providers
+ // -----------------------------------------------------------------------
+
+ it("throws error when provider parent is missing", () => {
+ function InvalidComponent(): React.JSX.Element {
+ validateProviderPosition("AuthProvider", 6);
+ return Should not render
;
+ }
+
+ expect(() => {
+ render( );
+ }).toThrow(/missing required parent providers/i);
+ });
+
+ it("allows ThemeProvider at position 0 without parent tracking", () => {
+ function ValidThemeComponent(): React.JSX.Element {
+ validateProviderPosition("ThemeProvider", 0);
+ return Theme OK
;
+ }
+
+ const { getByTestId } = render( );
+ expect(getByTestId("theme")).toBeInTheDocument();
+ });
+
+ // -----------------------------------------------------------------------
+ // Test 2: validateProvidersExist checks for required providers
+ // -----------------------------------------------------------------------
+
+ it("throws error when required providers are missing", () => {
+ function NeedsProviders(): React.JSX.Element {
+ validateProvidersExist(["AuthProvider", "NotificationProvider"]);
+ return Should not render
;
+ }
+
+ expect(() => {
+ render( );
+ }).toThrow(/missing required providers/i);
+ });
+
+ it("allows validation when tracking context exists with all required providers", () => {
+ function ValidatedComponent(): React.JSX.Element {
+ validateProvidersExist(["ThemeProvider"]);
+ return Valid
;
+ }
+
+ const composition = (
+
+ {createProviderTracker("ThemeProvider", 0, )}
+
+ );
+
+ const { getByTestId } = render(composition);
+ expect(getByTestId("valid")).toBeInTheDocument();
+ });
+
+ // -----------------------------------------------------------------------
+ // Test 3: hasProvider checks provider presence
+ // -----------------------------------------------------------------------
+
+ it("returns true when provider is present", () => {
+ let result = false;
+
+ function CheckProvider(): React.JSX.Element {
+ result = hasProvider("ThemeProvider");
+ return Check done
;
+ }
+
+ render(
+ createProviderTracker("ThemeProvider", 0, )
+ );
+
+ expect(result).toBe(true);
+ });
+
+ it("returns false when provider is not present", () => {
+ let result = true;
+
+ function CheckProvider(): React.JSX.Element {
+ result = hasProvider("TimezoneProvider");
+ return Check done
;
+ }
+
+ render(
+ createProviderTracker("ThemeProvider", 0, )
+ );
+
+ expect(result).toBe(false);
+ });
+
+ // -----------------------------------------------------------------------
+ // Test 4: getCurrentProviders returns providers in order
+ // -----------------------------------------------------------------------
+
+ it("returns empty array when no providers are tracked", () => {
+ let providers: string[] = [];
+
+ function CheckProviders(): React.JSX.Element {
+ const result = getCurrentProviders();
+ providers = result;
+ return Check done
;
+ }
+
+ render( );
+ expect(providers).toEqual([]);
+ });
+
+ it("returns providers in correct order when tracked", () => {
+ let providers: string[] = [];
+
+ function CheckProviders(): React.JSX.Element {
+ const result = getCurrentProviders();
+ providers = result;
+ return Check done
;
+ }
+
+ const composition = (
+
+ {createProviderTracker("ThemeProvider", 0,
+ createProviderTracker("FluentProvider", 1,
+ createProviderTracker("NotificationProvider", 2, )
+ )
+ )}
+
+ );
+
+ render(composition);
+ expect(providers).toEqual(["ThemeProvider", "FluentProvider", "NotificationProvider"]);
+ });
+
+ // -----------------------------------------------------------------------
+ // Test 5: createProviderTracker validates positioning
+ // -----------------------------------------------------------------------
+
+ it("throws error for invalid position in createProviderTracker", () => {
+ function Component(): React.JSX.Element {
+ return Invalid
;
+ }
+
+ // The error should be thrown during render, not construction
+ expect(() => {
+ // Using any to test invalid position despite type checking
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ render(createProviderTracker("ThemeProvider" as any, 999, ));
+ }).toThrow(/invalid position/i);
+ });
+
+ it("throws error when parent provider is missing in tracker", () => {
+ function Component(): React.JSX.Element {
+ return Should not render
;
+ }
+
+ expect(() => {
+ // NotificationProvider (position 2) requires FluentProvider (position 1)
+ // but we're skipping FluentProvider
+ render(
+
+ {createProviderTracker("ThemeProvider", 0,
+ createProviderTracker("NotificationProvider", 2, )
+ )}
+
+ );
+ }).toThrow(/incorrect parent structure|missing/i);
+ });
+
+ // -----------------------------------------------------------------------
+ // Test 6: useProviderValidation hook validates providers
+ // -----------------------------------------------------------------------
+
+ it("logs warning in development when required providers are missing", () => {
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
+
+ function ComponentNeedsAuth(): React.JSX.Element {
+ useProviderValidation("ComponentNeedsAuth", ["AuthProvider"]);
+ return Checking
;
+ }
+
+ render( );
+
+ // In development, should warn about missing provider
+ const hasWarning = warnSpy.mock.calls.some((call) =>
+ String(call[0] || "").includes("ComponentNeedsAuth") ||
+ String(call[0] || "").includes("missing")
+ );
+
+ // Only verify warning if NODE_ENV is development
+ if (process.env.NODE_ENV === "development") {
+ expect(hasWarning).toBe(true);
+ }
+
+ warnSpy.mockRestore();
+ });
+
+ it("does not warn when all required providers are present", () => {
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
+
+ function ComponentNeedsAuth(): React.JSX.Element {
+ useProviderValidation("ComponentNeedsAuth", ["ThemeProvider"]);
+ return OK
;
+ }
+
+ const composition = (
+
+ {createProviderTracker("ThemeProvider", 0,
+ createProviderTracker("FluentProvider", 1, )
+ )}
+
+ );
+
+ render(composition);
+
+ // Should not warn about missing ThemeProvider
+ const warnCalls = warnSpy.mock.calls;
+ const hasThemeWarning = warnCalls.some((call) =>
+ String(call[0] || "").includes("missing") && String(call[0] || "").includes("ThemeProvider")
+ );
+
+ expect(hasThemeWarning).toBe(false);
+
+ warnSpy.mockRestore();
+ });
+
+ // -----------------------------------------------------------------------
+ // Test 7: Nested provider tracking
+ // -----------------------------------------------------------------------
+
+ it("correctly tracks nested providers through multiple levels", () => {
+ let capturedProviders: string[] = [];
+
+ function InnerComponent(): React.JSX.Element {
+ capturedProviders = getCurrentProviders();
+ return Inner
;
+ }
+
+ const composition = (
+
+ {createProviderTracker("ThemeProvider", 0,
+ createProviderTracker("FluentProvider", 1,
+ createProviderTracker("NotificationProvider", 2,
+ createProviderTracker("ErrorBoundary", 3,
+ createProviderTracker("BrowserRouter", 4, )
+ )
+ )
+ )
+ )}
+
+ );
+
+ const { getByTestId } = render(composition);
+ expect(getByTestId("inner")).toBeInTheDocument();
+ expect(capturedProviders).toEqual([
+ "ThemeProvider",
+ "FluentProvider",
+ "NotificationProvider",
+ "ErrorBoundary",
+ "BrowserRouter",
+ ]);
+ });
+
+ // -----------------------------------------------------------------------
+ // Test 8: Validation errors are descriptive
+ // -----------------------------------------------------------------------
+
+ it("provides helpful error messages for missing providers", () => {
+ let errorCaught = false;
+
+ function Component(): React.JSX.Element {
+ try {
+ validateProvidersExist(["AuthProvider", "TimezoneProvider"]);
+ } catch (error) {
+ if (error instanceof Error) {
+ expect(error.message).toMatch(/missing required providers/i);
+ errorCaught = true;
+ }
+ }
+ return Error caught
;
+ }
+
+ render( );
+ expect(errorCaught).toBe(true);
+ });
+
+ it("provides helpful error messages for incorrect positioning", () => {
+ let errorCaught = false;
+
+ function Component(): React.JSX.Element {
+ try {
+ validateProviderPosition("AuthProvider", 6);
+ } catch (error) {
+ if (error instanceof Error) {
+ expect(error.message).toMatch(/missing required parent/i);
+ errorCaught = true;
+ }
+ }
+ return Error caught
;
+ }
+
+ render( );
+ expect(errorCaught).toBe(true);
+ });
+
+ // -----------------------------------------------------------------------
+ // Test 9: Invalid position numbers are rejected
+ // -----------------------------------------------------------------------
+
+ it("throws error for negative position", () => {
+ function Component(): React.JSX.Element {
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ validateProviderPosition("InvalidProvider" as any, -1);
+ } catch (error) {
+ if (error instanceof Error) {
+ expect(error.message).toMatch(/invalid expected position/i);
+ }
+ }
+ return Error caught
;
+ }
+
+ render( );
+ });
+
+ it("throws error for position beyond sequence length", () => {
+ function Component(): React.JSX.Element {
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ validateProviderPosition("InvalidProvider" as any, 999);
+ } catch (error) {
+ if (error instanceof Error) {
+ expect(error.message).toMatch(/invalid expected position/i);
+ }
+ }
+ return Error caught
;
+ }
+
+ render( );
+ });
+});
diff --git a/frontend/src/providers/providerComposition.tsx b/frontend/src/providers/providerComposition.tsx
new file mode 100644
index 0000000..71c1338
--- /dev/null
+++ b/frontend/src/providers/providerComposition.tsx
@@ -0,0 +1,373 @@
+/**
+ * Provider Composition Utility — Compile-Time Provider Ordering Validation
+ *
+ * This module enforces the provider nesting order at compile-time using TypeScript's
+ * discriminated unions. The provider hierarchy is strictly enforced and cannot be
+ * violated without TypeScript errors.
+ *
+ * Provider Order (enforced at compile-time):
+ * 1. ThemeProvider (OUTERMOST — must be first)
+ * 2. FluentProvider (wraps all Fluent UI consumers)
+ * 3. NotificationProvider (must wrap error boundaries)
+ * 4. ErrorBoundary (top-level error boundary)
+ * 5. BrowserRouter (enables routing)
+ * 6. NavigationCancellationProvider (manages route cancellation)
+ * 7. AuthProvider (session validation)
+ * 8. TimezoneProvider (INNERMOST — wraps protected routes only)
+ *
+ * Usage:
+ * ```tsx
+ * const providerStack = createProviderComposition()
+ * .withTheme({ children })
+ * .withFluent(theme)
+ * .withNotification()
+ * .build();
+ * ```
+ *
+ * TypeScript will prevent any attempt to add providers out of order:
+ * ```tsx
+ * const invalid = createProviderComposition()
+ * .withFluent(theme) // ❌ TypeScript Error: cannot call withFluent before withTheme
+ * .withTheme({ children });
+ * ```
+ */
+
+import type { ReactElement, ReactNode } from "react";
+import { FluentProvider } from "@fluentui/react-components";
+import type { Theme } from "@fluentui/react-components";
+import { BrowserRouter } from "react-router-dom";
+import { ThemeProvider } from "./ThemeProvider";
+import { AuthProvider } from "./AuthProvider";
+import { TimezoneProvider } from "./TimezoneProvider";
+import { NavigationCancellationProvider } from "./NavigationCancellationProvider";
+import { NotificationProvider } from "../services/notificationService";
+import { ErrorBoundary } from "../components/ErrorBoundary";
+import { NotificationContainer } from "../components/NotificationContainer";
+
+// ============================================================================
+// Type Definitions — Discriminated Union Pattern
+// ============================================================================
+
+/**
+ * State 0: Initial state — only ThemeProvider can be added
+ */
+interface ProviderState0 {
+ readonly __state: 0;
+}
+
+/**
+ * State 1: ThemeProvider added — FluentProvider can be added next
+ */
+interface ProviderState1 {
+ readonly __state: 1;
+ readonly themeChildren: ReactNode;
+}
+
+/**
+ * State 2: FluentProvider added — NotificationProvider can be added next
+ */
+interface ProviderState2 {
+ readonly __state: 2;
+ readonly themeChildren: ReactNode;
+ readonly theme: Theme;
+}
+
+/**
+ * State 3: NotificationProvider added — ErrorBoundary can be added next
+ */
+interface ProviderState3 {
+ readonly __state: 3;
+ readonly themeChildren: ReactNode;
+ readonly theme: Theme;
+}
+
+/**
+ * State 4: ErrorBoundary added — BrowserRouter can be added next
+ */
+interface ProviderState4 {
+ readonly __state: 4;
+ readonly themeChildren: ReactNode;
+ readonly theme: Theme;
+}
+
+/**
+ * State 5: BrowserRouter added — NavigationCancellationProvider can be added next
+ */
+interface ProviderState5 {
+ readonly __state: 5;
+ readonly themeChildren: ReactNode;
+ readonly theme: Theme;
+}
+
+/**
+ * State 6: NavigationCancellationProvider added — AuthProvider can be added next
+ */
+interface ProviderState6 {
+ readonly __state: 6;
+ readonly themeChildren: ReactNode;
+ readonly theme: Theme;
+}
+
+/**
+ * State 7: AuthProvider added — TimezoneProvider can be added next (or build can be called)
+ */
+interface ProviderState7 {
+ readonly __state: 7;
+ readonly themeChildren: ReactNode;
+ readonly theme: Theme;
+}
+
+/**
+ * State 8: TimezoneProvider added — ready to build final component
+ */
+interface ProviderState8 {
+ readonly __state: 8;
+ readonly themeChildren: ReactNode;
+ readonly theme: Theme;
+ readonly timezoneChildren: ReactNode;
+}
+
+type ProviderCompositionState = ProviderState0 | ProviderState1 | ProviderState2 | ProviderState3 | ProviderState4 | ProviderState5 | ProviderState6 | ProviderState7 | ProviderState8;
+
+// ============================================================================
+// Builder — Type-Safe Provider Composition
+// ============================================================================
+
+/**
+ * Type-safe builder for composing providers in the correct order.
+ *
+ * Each method returns a builder in the next state, ensuring providers
+ * can only be added in the documented order.
+ */
+class ProviderCompositionBuilder {
+ /**
+ * Create a new builder in its initial state.
+ */
+ static create(): ProviderCompositionBuilder {
+ return new ProviderCompositionBuilder({ __state: 0 });
+ }
+
+ private constructor(private state: TState) {}
+
+ /**
+ * Add ThemeProvider (Step 1: OUTERMOST).
+ *
+ * This must be called first. ThemeProvider supplies theme context
+ * to all descendants via useThemeMode().
+ */
+ withTheme(
+ this: ProviderCompositionBuilder,
+ props: { children: ReactNode }
+ ): ProviderCompositionBuilder {
+ return new ProviderCompositionBuilder({
+ __state: 1,
+ themeChildren: props.children,
+ } as unknown as ProviderState1);
+ }
+
+ /**
+ * Add FluentProvider (Step 2).
+ *
+ * Must come after ThemeProvider. Supplies Fluent UI theme and design tokens
+ * to all Fluent UI components.
+ */
+ withFluent(
+ this: ProviderCompositionBuilder,
+ theme: Theme
+ ): ProviderCompositionBuilder {
+ return new ProviderCompositionBuilder({
+ __state: 2,
+ themeChildren: this.state.themeChildren,
+ theme,
+ } as unknown as ProviderState2);
+ }
+
+ /**
+ * Add NotificationProvider (Step 3).
+ *
+ * Must come after FluentProvider. Makes useNotification() available
+ * to error boundaries and other components.
+ */
+ withNotification(
+ this: ProviderCompositionBuilder
+ ): ProviderCompositionBuilder {
+ return new ProviderCompositionBuilder({
+ __state: 3,
+ themeChildren: this.state.themeChildren,
+ theme: this.state.theme,
+ } as unknown as ProviderState3);
+ }
+
+ /**
+ * Add ErrorBoundary (Step 4).
+ *
+ * Must come after NotificationProvider. Catches catastrophic errors
+ * that would crash the entire app.
+ */
+ withErrorBoundary(
+ this: ProviderCompositionBuilder
+ ): ProviderCompositionBuilder {
+ return new ProviderCompositionBuilder({
+ __state: 4,
+ themeChildren: this.state.themeChildren,
+ theme: this.state.theme,
+ } as unknown as ProviderState4);
+ }
+
+ /**
+ * Add BrowserRouter (Step 5).
+ *
+ * Must come after ErrorBoundary. Enables client-side routing and
+ * supports useNavigate() in downstream providers.
+ */
+ withBrowserRouter(
+ this: ProviderCompositionBuilder
+ ): ProviderCompositionBuilder {
+ return new ProviderCompositionBuilder({
+ __state: 5,
+ themeChildren: this.state.themeChildren,
+ theme: this.state.theme,
+ } as unknown as ProviderState5);
+ }
+
+ /**
+ * Add NavigationCancellationProvider (Step 6).
+ *
+ * Must come after BrowserRouter. Manages route-aware request cancellation
+ * using useLocation() to detect route changes.
+ */
+ withNavigationCancellation(
+ this: ProviderCompositionBuilder
+ ): ProviderCompositionBuilder {
+ return new ProviderCompositionBuilder({
+ __state: 6,
+ themeChildren: this.state.themeChildren,
+ theme: this.state.theme,
+ } as unknown as ProviderState6);
+ }
+
+ /**
+ * Add AuthProvider (Step 7).
+ *
+ * Must come after NavigationCancellationProvider. Validates session
+ * on mount and provides useAuth() context to all descendants.
+ */
+ withAuth(
+ this: ProviderCompositionBuilder
+ ): ProviderCompositionBuilder {
+ return new ProviderCompositionBuilder({
+ __state: 7,
+ themeChildren: this.state.themeChildren,
+ theme: this.state.theme,
+ } as unknown as ProviderState7);
+ }
+
+ /**
+ * Add TimezoneProvider (Step 8: INNERMOST).
+ *
+ * Optional. Must come after AuthProvider if used. Wraps protected routes
+ * and fetches timezone from the backend.
+ */
+ withTimezone(
+ this: ProviderCompositionBuilder,
+ props: { children: ReactNode }
+ ): ProviderCompositionBuilder {
+ return new ProviderCompositionBuilder({
+ __state: 8,
+ themeChildren: this.state.themeChildren,
+ theme: this.state.theme,
+ timezoneChildren: props.children,
+ } as unknown as ProviderState8);
+ }
+
+ /**
+ * Build the final composed provider tree without TimezoneProvider.
+ * Use this for routes that don't need timezone data (e.g., setup, login).
+ */
+ build(
+ this: ProviderCompositionBuilder,
+ innerContent: ReactNode
+ ): ReactElement {
+ const s = this.state;
+ return (
+
+
+
+
+
+
+
+
+ {innerContent}
+
+
+
+
+
+
+
+ );
+ }
+
+ /**
+ * Build the final composed provider tree with TimezoneProvider.
+ * Use this for protected routes that need timezone data.
+ */
+ buildWithTimezone(
+ this: ProviderCompositionBuilder
+ ): ReactElement {
+ const s = this.state;
+ return (
+
+
+
+
+
+
+
+
+
+ {s.timezoneChildren}
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export { ProviderCompositionBuilder };
+
+/**
+ * Factory function to create a new provider composition builder.
+ *
+ * @returns A new builder in state 0 (ready for ThemeProvider)
+ *
+ * @example
+ * ```tsx
+ * const providerStack = createProviderComposition()
+ * .withTheme({ children })
+ * .withFluent(theme)
+ * .withNotification()
+ * .withErrorBoundary()
+ * .withBrowserRouter()
+ * .withNavigationCancellation()
+ * .withAuth()
+ * .build(routes);
+ * ```
+ */
+export function createProviderComposition(): ProviderCompositionBuilder {
+ return ProviderCompositionBuilder.create();
+}
diff --git a/frontend/src/providers/providerOrderValidator.tsx b/frontend/src/providers/providerOrderValidator.tsx
new file mode 100644
index 0000000..a7451dd
--- /dev/null
+++ b/frontend/src/providers/providerOrderValidator.tsx
@@ -0,0 +1,322 @@
+/**
+ * Provider Ordering Runtime Validator
+ *
+ * This utility validates that providers are correctly nested at runtime,
+ * serving as a safety net for development when compile-time checks are bypassed
+ * (e.g., through type assertions or dynamic composition).
+ *
+ * Usage:
+ * ```tsx
+ * function AppContents() {
+ * validateProviderOrder(); // Throws if called outside expected provider context
+ * // ... rest of component
+ * }
+ * ```
+ */
+
+import { useContext, createContext, useEffect } from "react";
+import type { ReactNode } from "react";
+
+// ============================================================================
+// Provider Tracking Context
+// ============================================================================
+
+interface ProviderOrderContext {
+ providers: Set;
+}
+
+const ProviderOrderTrackingContext = createContext(null);
+
+/**
+ * Get the current provider order tracking context
+ */
+function getProviderOrderTracking(): ProviderOrderContext | null {
+ try {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ return useContext(ProviderOrderTrackingContext);
+ } catch {
+ // useContext can only be called within a component
+ return null;
+ }
+}
+
+/**
+ * Allowed provider order sequence. Any deviation is an error.
+ */
+const PROVIDER_SEQUENCE = [
+ "ThemeProvider",
+ "FluentProvider",
+ "NotificationProvider",
+ "ErrorBoundary",
+ "BrowserRouter",
+ "NavigationCancellationProvider",
+ "AuthProvider",
+ "TimezoneProvider",
+] as const;
+
+type ProviderName = (typeof PROVIDER_SEQUENCE)[number];
+
+/**
+ * Validate that a provider is in the correct position relative to its siblings.
+ *
+ * Throws a descriptive error if:
+ * - A provider is nested in the wrong order
+ * - A required parent provider is missing
+ *
+ * @param providerName The name of the provider being validated
+ * @param expectedPosition The expected position in the provider sequence
+ * @throws {Error} If provider ordering is violated
+ *
+ * @example
+ * ```tsx
+ * export function AuthProvider({ children }: { children: ReactNode }): JSX.Element {
+ * validateProviderPosition("AuthProvider", 6);
+ * // ... provider implementation
+ * }
+ * ```
+ */
+export function validateProviderPosition(
+ providerName: ProviderName,
+ expectedPosition: number
+): void {
+ if (expectedPosition < 0 || expectedPosition >= PROVIDER_SEQUENCE.length) {
+ throw new Error(
+ `[ProviderValidator] Invalid expected position ${String(expectedPosition)} for ${providerName}. ` +
+ `Expected position between 0 and ${String(PROVIDER_SEQUENCE.length - 1)}.`
+ );
+ }
+
+ const tracking = getProviderOrderTracking();
+
+ if (tracking === null && expectedPosition > 0) {
+ // Only allow position 0 (ThemeProvider) to have no tracking context
+ throw new Error(
+ `[ProviderValidator] ${providerName} (position ${String(expectedPosition)}) is missing required parent providers. ` +
+ `Expected parent: ${String(PROVIDER_SEQUENCE[expectedPosition - 1])}. ` +
+ `Provider order: ${PROVIDER_SEQUENCE.slice(0, expectedPosition).join(" → ")} → [${providerName}]`
+ );
+ }
+
+ if (tracking !== null && expectedPosition > 0) {
+ const requiredParent: ProviderName = PROVIDER_SEQUENCE[expectedPosition - 1] as ProviderName;
+
+ if (!tracking.providers.has(requiredParent)) {
+ const providerSlice: ProviderName[] = PROVIDER_SEQUENCE.slice(0, expectedPosition);
+ const missingProviders = providerSlice.filter(
+ (p: ProviderName) => !tracking.providers.has(p)
+ );
+
+ throw new Error(
+ `[ProviderValidator] ${providerName} (position ${String(expectedPosition)}) is missing required parent providers. ` +
+ `Missing: ${missingProviders.join(", ")}. ` +
+ `Expected order: ${PROVIDER_SEQUENCE.slice(0, expectedPosition).join(" → ")} → [${providerName}]`
+ );
+ }
+ }
+}
+
+/**
+ * Validate that all required providers are present in the component tree.
+ *
+ * Use this in components that depend on multiple providers to ensure
+ * they're all available.
+ *
+ * @param requiredProviders Array of provider names that must be present
+ * @throws {Error} If any required provider is missing
+ *
+ * @example
+ * ```tsx
+ * function Dashboard(): JSX.Element {
+ * validateProvidersExist(["ThemeProvider", "AuthProvider", "NotificationProvider"]);
+ * // ... component that uses all three providers
+ * }
+ * ```
+ */
+export function validateProvidersExist(requiredProviders: ProviderName[]): void {
+ const tracking = getProviderOrderTracking();
+
+ if (tracking === null) {
+ throw new Error(
+ `[ProviderValidator] Missing required providers: ${requiredProviders.join(", ")}. ` +
+ `Ensure your component is wrapped with: ${requiredProviders.join(" → ")}`
+ );
+ }
+
+ const missing = requiredProviders.filter((p) => !tracking.providers.has(p));
+
+ if (missing.length > 0) {
+ throw new Error(
+ `[ProviderValidator] Missing required providers: ${missing.join(", ")}. ` +
+ `Ensure your component is wrapped with: ${requiredProviders.join(" → ")}`
+ );
+ }
+}
+
+/**
+ * Check if a provider is present in the current component tree.
+ *
+ * @param providerName Name of the provider to check
+ * @returns true if the provider is in the current tree, false otherwise
+ *
+ * @example
+ * ```tsx
+ * function MyComponent(): JSX.Element {
+ * if (hasProvider("AuthProvider")) {
+ * const auth = useAuth();
+ * // ... use auth
+ * }
+ * }
+ * ```
+ */
+export function hasProvider(providerName: ProviderName): boolean {
+ const tracking = getProviderOrderTracking();
+ return tracking !== null && tracking.providers.has(providerName);
+}
+
+/**
+ * Get all providers currently in the component tree (in order).
+ *
+ * @returns Array of provider names from outermost to innermost
+ *
+ * @example
+ * ```tsx
+ * const providers = getCurrentProviders();
+ * console.log(providers); // ["ThemeProvider", "FluentProvider", "NotificationProvider", ...]
+ * ```
+ */
+export function getCurrentProviders(): ProviderName[] {
+ const tracking = getProviderOrderTracking();
+ if (tracking === null) {
+ return [];
+ }
+
+ // Return providers in the order they appear in PROVIDER_SEQUENCE
+ return PROVIDER_SEQUENCE.filter((p) => tracking.providers.has(p));
+}
+
+/**
+ * Create a tracking wrapper for any provider.
+ *
+ * This allows runtime validation of provider nesting order.
+ * Call this from within any provider component.
+ *
+ * @param providerName Name of the provider
+ * @param expectedPosition Position in the provider sequence
+ * @param children Child components
+ * @returns JSX element with provider order tracking
+ *
+ * @internal Use this in provider implementations to enable validation
+ *
+ * @example
+ * ```tsx
+ * export function CustomProvider({ children }: { children: ReactNode }): JSX.Element {
+ * return createProviderTracker("CustomProvider", 5, children);
+ * }
+ * ```
+ */
+export function createProviderTracker(
+ providerName: ProviderName,
+ expectedPosition: number,
+ children: ReactNode
+): React.JSX.Element {
+ return ;
+}
+
+/**
+ * Internal component that tracks provider nesting order.
+ */
+function ProviderOrderTracker({
+ name,
+ position,
+ children,
+}: {
+ name: ProviderName;
+ position: number;
+ children: ReactNode;
+}): React.JSX.Element {
+ const parentTracking = getProviderOrderTracking();
+
+ // Validate position
+ if (position < 0 || position >= PROVIDER_SEQUENCE.length) {
+ throw new Error(
+ `[ProviderValidator] Invalid position ${String(position)} for provider ${name}. ` +
+ `Expected 0-${String(PROVIDER_SEQUENCE.length - 1)}.`
+ );
+ }
+
+ // Validate parent providers
+ if (position > 0) {
+ const expectedParent: ProviderName = PROVIDER_SEQUENCE[position - 1] as ProviderName;
+ if (parentTracking === null || !parentTracking.providers.has(expectedParent)) {
+ const providerSlice: ProviderName[] = PROVIDER_SEQUENCE.slice(0, position);
+ const missing = providerSlice.filter(
+ (p: ProviderName) => parentTracking === null || !parentTracking.providers.has(p)
+ );
+
+ throw new Error(
+ `[ProviderValidator] ${name} (position ${String(position)}) has incorrect parent structure. ` +
+ `Missing: ${missing.join(", ")}. ` +
+ `Expected: ${PROVIDER_SEQUENCE.slice(0, position).join(" → ")} → [${name}]`
+ );
+ }
+ }
+
+ // Create new tracking set including this provider
+ const newTracking: ProviderOrderContext = {
+ providers: new Set(parentTracking?.providers ?? []),
+ };
+ newTracking.providers.add(name);
+
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Development-only hook to verify a component is wrapped by required providers.
+ *
+ * Use this in development to catch provider ordering issues early.
+ *
+ * @param componentName Name of the component (for error messages)
+ * @param requiredProviders Array of required provider names
+ * @throws {Error} in development if any required provider is missing
+ *
+ * @example
+ * ```tsx
+ * function JailsPage(): JSX.Element {
+ * useProviderValidation("JailsPage", ["ThemeProvider", "AuthProvider"]);
+ * // ... component implementation
+ * }
+ * ```
+ */
+export function useProviderValidation(
+ componentName: string,
+ requiredProviders: ProviderName[]
+): void {
+ useEffect(() => {
+ if (process.env.NODE_ENV === "development") {
+ const tracking = getProviderOrderTracking();
+
+ if (tracking === null) {
+ console.warn(
+ `[ProviderValidator] ${componentName} requires providers but none were found. ` +
+ `Required: ${requiredProviders.join(", ")}`
+ );
+ return;
+ }
+
+ const missing = requiredProviders.filter((p) => !tracking.providers.has(p));
+
+ if (missing.length > 0) {
+ console.warn(
+ `[ProviderValidator] ${componentName} is missing providers: ${missing.join(", ")}. ` +
+ `Currently available: ${Array.from(tracking.providers).join(", ")}`
+ );
+ }
+ }
+ }, [componentName, requiredProviders]);
+}
+
+export { ProviderOrderTrackingContext };