Files
KArtSell.Aegis/docs/ARCHITECTURE_DETAILED.md
T
kjh2064 07ad98ec12 refactor(docs): Optimize CLAUDE.md structure (47KB→12KB) + expand AGENTS.md v16.0
## 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>
2026-08-16 14:47:41 +09:00

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 <= 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

// ✅ 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

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:

  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:

/* ✅ 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:

// ✅ 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.