# Architecture Deep Dive

**Reference:** For quick overview, see CLAUDE.md "Architecture" section.  
**Governance:** All decisions follow AGENTS.md v16.0 and VIBE Coding Guardrails.

## Backend: Modular Monolith + Vertical Slices

### Module Structure
```
src/
  KArtSell.Host/                    # Main ASP.NET Core app
  KArtSell.BuildingBlocks/          # Shared infrastructure (logging, serialization, extensions)
  KArtSell.DbMigrator/              # DbUp migrations
  KArtSell.Modules.ModelOperations/ # Model lifecycle, validation, activation
  KArtSell.Modules.SignalEngine/    # Trading signal generation
```

### Vertical Slice Template
Each feature is a complete, self-contained slice from HTTP endpoint to database:

```
Features/<SliceName>/
  Endpoint.cs         # FastEndpoints route handler (HTTP/contract/status codes)
  Request.cs          # Input model with validation
  Response.cs         # Output model (DTO)
  Validator.cs        # Fluent/Policy validation rules
  Handler.cs          # Use case orchestration (Application layer)
  Policy.cs           # Pure business decision logic (Domain layer)
  Sql.cs              # Dapper queries (Data layer)
  Mapper.cs           # Entity ↔ DTO mapping
  Jobs/               # Related Hangfire jobs
  Contracts/          # Event/Job contract definitions
  Tests/              # Unit/integration tests specific to this slice
  README.md           # Traceability: requirements, ADRs, assumptions
```

**Key rule:** Endpoint handles HTTP concerns; Handler handles transaction boundaries; Policy makes decisions; Sql uses Dapper for explicit, schema-qualified queries.

### Design Principles
- **No Generic Repository:** Each slice writes its own Dapper queries; promotes clarity.
- **No Service Layer:** Handler + Policy + Sql replaces it; keeps flow visible.
- **Module Isolation:** Modules do not query each other's source tables directly.
  - Synchronous: Use narrow Read Port services.
  - Asynchronous: Use Outbox/Inbox event patterns.
- **PIT (Point-in-Time) Queries:** Must include `WHERE published_at <= cutoff` and revision resolver.
- **Evidence & Audit:** Update/delete are blocked; new state appended as new revision.
- **Migrations:** `src/KArtSell.DbMigrator` uses DbUp; file naming: `NNNN_description.sql`.

### Query Patterns
```csharp
// ✅ DO: Schema-qualified, explicit columns, cancellation token
const string sql = """
  SELECT id, name, created_at
  FROM model_operations.signals
  WHERE published_at <= @cutoff
    AND status = @status
  ORDER BY created_at DESC
""";

// ❌ DON'T: SELECT *, generic repository, no token
const string sql = "SELECT * FROM signals WHERE status = @status";
```

### Async Coupling: Outbox/Inbox
- **Outbox:** When a command succeeds, events are inserted into `outbox` in the same transaction.
- **Inbox:** A Hangfire job polls the outbox, publishes events, and marks them as processed.
- **Idempotency:** Each inbox handler is idempotent; replayed events are no-ops.

## Database & Migrations

### DbUp
- **Run at startup:** `KArtSell.DbMigrator` is the single source of truth.
- **Schema ownership:** Each module owns its schema (e.g., `model_operations.*`, `signal_engine.*`).
- **Safety:** Migrations are idempotent and checksummed; failed migration rolls back and waits for manual intervention.
- **Test:** Each migration has fresh/upgrade/re-run/failure-recovery tests in CI.

## Hangfire (Background Jobs & Scheduling)

### Job Design
- **Not a business decision maker:** Hangfire executes approved Application Commands, not policies.
- **Idempotency key:** Each job must be replayable without side effects.
- **Watermark & version set:** Track input/output state across retries.
- **Queue isolation:** `q-customer-sla` (business SLA) is separate from `q-research` (non-critical).
- **Retry classification:**
  - `transient` (network glitch, retry immediately)
  - `permanent` (bad input, log & alert)
  - `dq` (data quality issue, quarantine for manual review)
  - `business-hold` (awaiting approval or external event)

### Example Job Structure
```csharp
public class MyJobCommand : ICommand
{
    public string IdempotencyKey { get; set; }
    public Guid JobRunId { get; set; }
    public Guid CorrelationId { get; set; }
}
```

Jobs do not call other jobs directly; instead, they emit events or check readiness gates.

## SignalR (Real-Time Push)

Used for live notifications (model activation events, approval notifications). Follows Hub/Group pattern with correlation to `CorrelationId` for traceability.

---

## Frontend: Vue 3 + Vite + KBX Foundation v4 (Operational Navigation)

### Directory Layout (Registry-Driven)
```
frontend/src/
  app/
    router.ts               # Vue Router setup (page-level only)
    installKbx.ts           # KBX system initialization (registry, contracts, permissions)
  features/
    <feature>/
      routes.ts             # Feature route definitions (lazy-loaded)
      registry.ts           # Screen registry entry (@kbx/contracts.ScreenDefinition)
      pages/
        <Screen>.vue        # Page component (matches registry.screenId)
      components/           # Feature-scoped components (not shared)
      stores/               # Pinia stores (feature state)
      composables/          # Reusable hooks (feature logic)
      types/                # TS interfaces for this feature
  shared/
    ui/
      adapter/              # MANDATORY boundary: PrimeVue/AG Grid wrappers
      components/           # Cross-feature components (shared contracts)
      layouts/              # Page layout templates (header, sidebar, footer)
      tokens/               # Design tokens (compact, comfortable, touch density)
    composables/
    types/
    stores/
  registry/                 # Central screen definition registry
    index.ts                # Import all feature registries, export merged ScreenRegistry
    ui-context.ts           # UI adapter context provider
  design-system/            # Design tokens (NOT arbitrary page CSS)
    tokens.css              # CSS custom properties (34px, 44px, 52px, etc.)
    density/                # compact, comfortable, touch variants
```

### KBX Contracts (@kbx/contracts)

```typescript
// ScreenDefinition (required in all feature registries)
export interface ScreenDefinition {
  screenId: string              // e.g., "oms.orders.list"
  title: string                 // Display name (localized)
  module: "OMS" | "WMS" | "ERP" // Functional area
  path: string                  // Vue Router path
  component: () => Promise<any>  // Lazy-loaded page component
  permissions: string[]         // Required roles (e.g., ["order.view"])
  help?: HelpDefinition         // Contextual help (registry-driven)
  grid?: GridDefinition         // AG Grid config (shared theme)
  shortcut?: string             // Keyboard shortcut (help searchable)
}

// PermissionDefinition (centralized RBAC)
export interface PermissionDefinition {
  permissionId: string   // e.g., "order.create"
  label: string          // Human-readable (for audit/help)
  screens: string[]      // Which screens require this permission
  forms: string[]        // Which forms check this permission
}

// HelpDefinition (context-aware, registry-indexed)
export interface HelpDefinition {
  title: string          // Panel title (screen context)
  sections: HelpSection[]
  relatedScreens: string[] // Cross-screen navigation
  externalUrl?: string   // Knowledge base link
}
```

### App Initialization (@kbx Lifecycle)

```typescript
// frontend/src/app/installKbx.ts
// 1. Load screen registry (all feature registries merged)
const registry = await loadScreenRegistry()

// 2. Install permission context (RBAC decision engine)
app.use(createPermissionContext(registry))

// 3. Install router with lazy-loaded pages
const router = createRouter({
  routes: buildRouterFromRegistry(registry) // Page routes only
})

// 4. Install KBX global components (adapter-wrapped UI)
app.use(KbxUiPlugin)

// 5. Populate stores (registry cache for help, permissions, status)
useRegistryStore().setRegistry(registry)
```

### UI Adapter Pattern (Mandatory Boundary)

All UI framework usage must go through `@kbx/ui/adapter`:

```typescript
// ❌ DON'T: Use PrimeVue directly in screens
<PButton label="Save" @click="save" />

// ✅ DO: Use KBX adapter (framework-agnostic)
<KbxButton label="Save" @click="save" />

// Adapter handles:
// - Theme switching (dark/light/system)
// - Density token application (compact/comfortable/touch)
// - Accessibility (ARIA, focus management)
// - Keyboard shortcuts (Ctrl+S, etc.)
```

### State Management (Registry-Driven, Contract-Based)

| State | Owner | Tool | Registry Link |
|-------|-------|------|---|
| API responses, cache, stale, retry | TanStack Query | @tanstack/vue-query | → API contracts (OpenAPI) |
| Session, role, UI preferences | Global Pinia | `authStore`, `registryStore` | → PermissionDefinition |
| Form values, errors, touched | Form library | vee-validate + Zod schema | → Screen.forms contract |
| URL filters, pagination, sorting | Router | vue-router query/params | → ScreenDefinition.grid |
| Large data tables, virtual scroll | Server-side row model | AG Grid server mode (adapter) | → GridDefinition contract |

**Anti-patterns:**
- ❌ Do NOT duplicate API responses in Pinia (use TanStack Query cache).
- ❌ Do NOT write 401/409/422/429/503 error handling in every screen (use ErrorBoundary + QueryStateBoundary).
- ❌ Do NOT manage query cache manually.
- ❌ Do NOT define routes outside registry (route table is generated from registry).
- ❌ Do NOT bypass PermissionGuard for conditional rendering (use registry-driven rendering).

### Screen Component Example

```vue
<!-- features/orders/pages/OrdersList.vue -->
<template>
  <div>
    <!-- Header: registry-driven title, help, export -->
    <ScreenHeader :screenId="screenId" />
    
    <!-- Content: data grid with server-side row model -->
    <QueryStateBoundary :query="ordersQuery">
      <AgGridShell
        :gridOptions="gridConfig"
        :rows="ordersQuery.data"
        :loading="ordersQuery.isPending"
      />
    </QueryStateBoundary>
  </div>
</template>

<script setup>
// Registry access (read-only, cached)
const registry = useRegistry()
const screenDef = registry.screens.get('oms.orders.list')
const screenId = screenDef.screenId

// Permission check (registry-driven)
const can = usePermission()
const canCreate = can('order.create')  // Registry permission ID

// Data fetching (TanStack Query, no Pinia duplication)
const ordersQuery = useQuery({
  queryKey: ['orders', filters],
  queryFn: () => api.orders.list(filters)
})

// Grid config (adapter-wrapped, density-aware)
const gridConfig = computed(() => ({
  columnDefs: screenDef.grid.columnDefs,
  rowHeight: tokens.gridRowHeight,
  ...defaultGridOptions
}))
</script>
```

### Screen Registry Entry

```typescript
export const ordersListScreen: ScreenDefinition = {
  screenId: "oms.orders.list",
  title: "Orders",
  module: "OMS",
  path: "/oms/orders",
  component: () => import("./pages/OrdersList.vue"),
  permissions: ["order.view"],
  help: {
    title: "Order Search & Management",
    sections: [{
      title: "How to search",
      content: "Use filters at the top to search by date, customer, or status"
    }],
    relatedScreens: ["oms.orders.detail", "oms.orders.register"]
  },
  grid: {
    columnDefs: [
      { field: "orderId", headerName: "Order ID", width: 120 },
      { field: "customerName", headerName: "Customer", width: 200 }
    ],
    rowHeight: "auto",
    serverSideDatasource: true
  },
  shortcut: "Ctrl+Shift+O"
}
```

### Component Elevation Criteria

Promote to `shared/ui/components/` only when:
1. **Same business meaning & permissions** (check registry.screens[].permissions).
2. **Repeated state/error handling logic** across 3+ consumers.
3. **Accessibility & testing** fully implemented.
4. **Contract-driven** (implements @kbx/contracts interface).

**Always-shared components (KBX system):**
- `QueryStateBoundary` (loading/error/empty, registry context-aware)
- `PermissionGuard` (RBAC via registry.permissions)
- `ScreenHeader` (title, help trigger, export buttons from registry)
- `AgGridShell` (AG Grid adapter with density tokens)
- `KbxStatus` (status display per StatusDefinition contract)
- `KbxHelpPanel` (registry-driven help, contextual)

### Design Token Density

Screen density (compact/comfortable/touch) is applied globally via tokens, NOT per-screen CSS:

```css
/* ✅ DO: Define tokens, let screens inherit */
:root {
  --kbx-density: compact;  /* or 'comfortable', 'touch' */
  --kbx-input-height: 34px;
  --kbx-grid-row-height: 34px;
  --kbx-touch-target: 44px;
}

:root[data-density="comfortable"] {
  --kbx-input-height: 36px;
  --kbx-grid-row-height: 36px;
  --kbx-touch-target: 48px;
}

:root[data-density="touch"] {
  --kbx-input-height: 52px;
  --kbx-grid-row-height: 48px;
  --kbx-touch-target: 52px;
}
```

### Route Registration Flow

1. **Feature Registry** (`features/<feature>/registry.ts`): Define ScreenDefinition(s)
2. **Central Registry** (`frontend/src/registry/index.ts`): Import and merge all feature registries
3. **Router Build** (`app/installKbx.ts`): Generate Vue Router routes from registry
4. **Page-Level Routes Only**: No nested routing; each screen is a top-level route

### Permission & Help Enforcement

**Registry-driven RBAC:**
```typescript
// ✅ DO: Registry-driven permission checks
const canEdit = computed(() => {
  const screen = registry.screens.get('oms.orders.detail')
  return permissions.hasAll(screen.permissions)
})

// ❌ DON'T: Hard-coded permission strings
const canEdit = permissions.has('order.edit')  // WRONG: no registry reference
```

**Registry-driven Help:**
```typescript
// ✅ DO: Help from registry
const { openHelp } = useHelpPanel()
openHelp('oms.orders.list')

// ❌ DON'T: Hard-coded help text
const helpText = "Use filters to search..."  // WRONG: duplicates registry
```

### Contract Enforcement (CI/CD Gate)

Build-time validation ensures all screens comply with contracts:

```bash
# .gitea/workflows/quality-gate.yml
- name: Validate screen contracts
  run: |
    # 1. Check: All files in features/*/pages/*.vue match registry entries
    # 2. Check: All ScreenDefinition.permissions exist in permissionRegistry
    # 3. Check: Grid configs use adapter tokens, not inline CSS
    # 4. Check: No PrimeVue/AG Grid imports outside adapter/
    # 5. Generate: ScreenManifest.json for help/telemetry indexing
```

---

## FastEndpoints

- Docs: [FastEndpoints GitHub](https://github.com/FastEndpoints/FastEndpoints)
- Pattern: Each endpoint maps to a Vertical Slice; routes are discovered automatically.
