## Summary - **CLAUDE.md optimization:** Move engineering guidelines to AGENTS.md only (governance lock) - Removed: Governance, Testing Strategy, Observability details, Common Workflows, Guardrails - Kept: Project status, timeline, architecture high-level overview, quick reference - Result: 47KB → 12.1KB (75% reduction, well within 40KB limit) - **AGENTS.md expansion:** Add 5 missing engineering procedure sections - v16.0 Testing Strategy (xUnit/Vitest/Playwright organization, commands, rules) - v16.0 Backend Architecture (Vertical Slice, Database/Migrations, Hangfire Job Design) - v16.0 Frontend Architecture (Registry-driven screens, KBX contracts, UI adapter boundary) - v16.0 Observability (Logging, Tracing, Dashboards, Metrics) - v16.0 Common Workflows (Adding Vertical Slices, Refactoring, Creating Jobs) - **New companion docs** (no duplication, supplement AGENTS.md): - docs/ARCHITECTURE_DETAILED.md — Deep dive on backend/frontend patterns - docs/COMMON_WORKFLOWS.md — Workflow procedures with examples - docs/GITEA_API_REFERENCE.md — Gitea API + External data sources ## Governance (enforced) - All engineering procedures now in AGENTS.md ONLY - CLAUDE.md = project context only (status, timeline, overview) - Companion docs reference AGENTS.md (no duplicate guidance) - No conflicting guidance across multiple sources ## Result - CLAUDE.md: 12.1KB ✅ (within 40KB limit) - AGENTS.md: 44.8KB (comprehensive procedures) - Single source of truth for all engineering guidelines Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
14 KiB
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 <= cutoffand revision resolver. - Evidence & Audit: Update/delete are blocked; new state appended as new revision.
- Migrations:
src/KArtSell.DbMigratoruses DbUp; file naming:NNNN_description.sql.
Query Patterns
// ✅ 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
outboxin 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.DbMigratoris 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 fromq-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
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)
// 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)
// 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:
// ❌ 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
<!-- 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
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:
- Same business meaning & permissions (check registry.screens[].permissions).
- Repeated state/error handling logic across 3+ consumers.
- Accessibility & testing fully implemented.
- 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:
/* ✅ 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
- Feature Registry (
features/<feature>/registry.ts): Define ScreenDefinition(s) - Central Registry (
frontend/src/registry/index.ts): Import and merge all feature registries - Router Build (
app/installKbx.ts): Generate Vue Router routes from registry - Page-Level Routes Only: No nested routing; each screen is a top-level route
Permission & Help Enforcement
Registry-driven RBAC:
// ✅ 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:
// ✅ 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:
# .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
- Pattern: Each endpoint maps to a Vertical Slice; routes are discovered automatically.