Files
property-match/ARCHITECTURE.md
Benjamin Sutter 713ef3ec08 docs: engineering governance — CLAUDE.md, ARCHITECTURE.md, CODE_REVIEW_CHECKLIST.md
Complete rewrite of CLAUDE.md with full binding rules (vision, architecture layers,
component limits, state management, design system, AI integration, performance,
security). New ARCHITECTURE.md covers folder structure, all data flows with ASCII
diagrams, scoring pipeline, AI flow, unified result feed, and workspace routing.
New CODE_REVIEW_CHECKLIST.md provides a 10-section review guide and pre-commit
quick checklist.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 02:10:30 +02:00

24 KiB
Raw Permalink Blame History

Property Match — Architecture Reference

This document describes the architecture of the Property Match platform. For coding rules see CLAUDE.md. For state management patterns see STATE_MANAGEMENT.md.


1. Folder Structure

src/
├── assets/                      Static images, icons
│
├── domain/                      TypeScript types & enums (single source of truth)
│   ├── enums.ts                 AssetType, ResultType, MatchStatus, UserRole, WorkspaceType
│   ├── match.ts                 Match, ScoreBreakdown, ScoreFactor, TradeOff, Risk
│   ├── property.ts              Property, Location, ContactPerson
│   ├── need.ts                  Need, AreaRange, BudgetRange, WeightingProfile
│   ├── unifiedResult.ts         UnifiedMatchResult (discriminated union)
│   ├── scoring.ts               ScoringWeightProfile, MatchEngineOutput
│   ├── aiOutput.ts              AI scoring, extraction, monitoring types
│   ├── futureSignal.ts          Future availability signals
│   ├── pipeline.ts              Pipeline stages & items
│   ├── inquiry.ts               Demand-side inquiry records
│   ├── reminder.ts              Reminder & DueDate
│   └── index.ts                 Barrel re-exports
│
├── features/                    Business logic algorithms (pure TypeScript)
│   └── matching/
│       ├── scoreCalculator.ts   Hard/soft scoring, confidence & DQ modifiers
│       ├── rankingEngine.ts     Match ranking, unified result assembly
│       ├── mustHaveScorer.ts    Must-have criteria evaluation
│       ├── tradeOffAnalyzer.ts  Tradeoff, risk, and missing-data analysis
│       └── matchCardAdapter.ts  Match → MatchCardViewModel adapter
│
├── provider/                    Data access layer (interface + mock per entity)
│   ├── IPropertyProvider.ts
│   ├── MockupPropertyProvider.ts
│   ├── INeedProvider.ts
│   ├── MockupNeedProvider.ts
│   └── ... (one pair per entity)
│
├── services/                    Business logic services
│   ├── ai/
│   │   ├── IAIService.ts        AI capability interface
│   │   ├── MockAIService.ts     Deterministic mock (dev/CI)
│   │   ├── OpenRouterAIService.ts  Real LLM calls (production)
│   │   └── prompts/             Structured prompt templates
│   ├── matchService.ts
│   ├── needService.ts
│   ├── propertyService.ts
│   ├── aiMonitoringService.ts
│   ├── governanceService.ts
│   ├── types.ts                 ListResponse<T>, ItemResponse<T>
│   └── errors.ts                throwServiceError()
│
├── hooks/                       React Query wrappers
│   ├── useProperties.ts
│   ├── useMatches.ts
│   ├── useUnifiedResults.ts     Aggregates all result types for demand feed
│   └── ... (one file per entity)
│
├── stores/                      Zustand UI stores (global UI state only)
│   ├── sessionStore.ts          Auth, currentUser, workspace
│   ├── compareStore.ts          Compare tray items (max 4)
│   ├── pipelineStore.ts         Pipeline items + add-dialog
│   ├── assistantStore.ts        AI assistant drawer conversation
│   ├── offerWizardStore.ts      Multi-step offer wizard
│   ├── layoutStore.ts           Sidebar, right panel, active workspace
│   ├── matchCenterStore.ts      Supply match center selection
│   ├── reminderStore.ts         Reminder filters + drawer
│   ├── shortlistStore.ts        Shortlist selection + add-dialog
│   └── toastStore.ts            Toast notification queue
│
├── lib/                         Shared utilities & design system
│   ├── constants.ts             All thresholds, labels, stale times, routes
│   ├── ds.ts                    DS_COLORS, RESULT_TYPE_META, score → level converters
│   ├── utils.ts                 matchScoreHex, confidenceHex, dataQualityColor, formatters
│   ├── theme.ts                 MUI theme
│   ├── queryClient.ts           TanStack Query client config
│   ├── permissions.ts           Role-based access helpers
│   ├── locationIntelligence.ts  Location scoring
│   └── propertyHeat.ts          Market heat analysis
│
├── components/                  UI components (organized by feature)
│   ├── layout/                  AppShell, PageHeader, RightContextPanel
│   ├── ui/                      ErrorBoundary, EmptyState, Toast, DecisionContextPanel
│   ├── shared/                  LocationPreview, ViewToggle (cross-feature)
│   ├── match-card/              MatchCard variants (compact/expanded/review/compare-mini)
│   ├── match-detail/            MatchDetailHero, ScoreBreakdownPanel, CriterionRow
│   ├── compare/                 CompareMetricRow, CompareColumn, compareUtils
│   ├── pipeline/                PipelineCard, PipelineColumn, pipelineConstants, pipelineUtils
│   ├── results/                 UnifiedResultFeed, ResultFilterBar, ResultTypeBadge
│   ├── supply/                  PropertyIntelligenceCard, ReminderFeed, ReminderListRow, ...
│   ├── demand/                  NeedInput, WeightingEditor, CriteriaReviewPanel, ...
│   ├── future-signals/          SignalCard, SignalFilterBar, FutureAvailabilityPanel, ...
│   ├── data-quality/            DataQualityPanel, FreshnessIndicator, ProvenancePanel, ...
│   ├── badges/                  ConfidenceBadge, RiskBadge, FreshnessBadge, ...
│   ├── ai-monitoring/           AI audit log UI
│   └── ...
│
├── pages/                       Route-level page components
│   ├── auth/LoginScreen.tsx
│   ├── supply/                  SupplyDashboard, Properties, MatchCenter, ...
│   ├── demand/                  AISearch, Results, MatchDetail, Compare, Pipeline, ...
│   └── ops/                     MarketIntelligence
│
├── mock-data/                   Seed data for mock providers
│
├── App.tsx                      React Router setup (all routes, lazy-loaded)
└── index.css                    Tailwind layer imports (no project styles here)

2. Data Flow Overview

The core data flow is unidirectional across four layers.

┌─────────────────────────────────────────────────────────────────────────┐
│                          Component / Page                                │
│                                                                          │
│   Reads: useQuery hook         Writes: useMutation hook                 │
│   UI state: Zustand selector   Side effects: useEffect                  │
└────────────────┬──────────────────────────┬────────────────────────────┘
                 │ calls                     │ calls
                 ▼                           ▼
┌────────────────────────────────────────────────────────────────────────┐
│                             Hook Layer                                  │
│                        src/hooks/*.ts                                   │
│                                                                         │
│   useQuery({ queryFn: () => service.getAll() })                        │
│   useMutation({ mutationFn: (data) => service.create(data) })          │
└────────────────────────────────┬───────────────────────────────────────┘
                                 │ calls
                                 ▼
┌────────────────────────────────────────────────────────────────────────┐
│                           Service Layer                                 │
│                        src/services/*.ts                                │
│                                                                         │
│   Business logic, error handling, response standardization             │
│   Reads session via sessionStore.getState()                            │
│   Calls scoring engine (features/matching/) as needed                  │
└────────────────────────────────┬───────────────────────────────────────┘
                                 │ calls
                                 ▼
┌────────────────────────────────────────────────────────────────────────┐
│                           Provider Layer                                │
│                        src/provider/*.ts                                │
│                                                                         │
│   IPropertyProvider (interface)                                        │
│   MockupPropertyProvider (in-memory, dev)  ← swap →  RestPropertyProvider (prod)
└────────────────────────────────────────────────────────────────────────┘

3. Provider Swap Flow

Swapping from mock to real requires one file change — the service import. No hooks, components, or tests change.

Dev / CI                                Production
─────────────────────────────────────   ─────────────────────────────────────
propertyService.ts                      propertyService.ts
  import provider from                    import provider from
    './MockupPropertyProvider'              './RestPropertyProvider'
                                                  ↑
                                         One line change — nothing else

Provider interfaces enforce the contract. All methods are async. Mock providers hold in-memory seed data from src/mock-data/.


4. State Architecture

                  ┌──────────────────────────────────────────┐
                  │              React Query Cache            │
                  │                                          │
                  │   ['properties']  ──  5 min stale        │
                  │   ['matches', needId]  ──  2 min stale   │
                  │   ['signals']  ──  5 min stale           │
                  │   ['review-queue']  ──  30 s stale       │
                  │                                          │
                  │   ← server data, cached, invalidated     │
                  │     on mutation success →                │
                  └──────────────────────────────────────────┘

                  ┌──────────────────────────────────────────┐
                  │              Zustand Stores              │
                  │                                          │
                  │   sessionStore    ← auth, currentUser    │
                  │   layoutStore     ← sidebar, workspace   │
                  │   compareStore    ← compare tray items   │
                  │   pipelineStore   ← pipeline + dialog    │
                  │   assistantStore  ← AI chat conversation │
                  │   offerWizardStore← multi-step wizard    │
                  │   reminderStore   ← filter + drawer      │
                  │   toastStore      ← notification queue   │
                  │                                          │
                  │   ← UI state only, no server data →      │
                  └──────────────────────────────────────────┘

                  ┌──────────────────────────────────────────┐
                  │           Component Local State          │
                  │                                          │
                  │   useState  ← form inputs, toggles       │
                  │   useMemo   ← derived / filtered lists   │
                  │   useCallback ← stable callbacks         │
                  └──────────────────────────────────────────┘

5. Matching & Scoring Flow

The matching engine (src/features/matching/) produces all scores. Components never compute scores — they display them.

Need (criteria)    +    Property (attributes)
       │                        │
       └───────────┬────────────┘
                   ▼
        ┌─────────────────────┐
        │   scoreCalculator   │
        │                     │
        │  1. Hard filters    │  ← Exclusions (wrong type, out of area, etc.)
        │  2. Hard scoring    │  ← Area, location, budget, timing, asset type (0100 each)
        │  3. Soft scoring    │  ← Prestige, accessibility, ESG, footfall, etc. (0100 each)
        │  4. Modifiers       │  ← confidenceModifier (-20→+5) + dataQualityModifier (-15→0)
        │  5. totalScore      │  ← Weighted average + modifiers (0100)
        └──────────┬──────────┘
                   │
                   ▼
        ┌─────────────────────┐
        │   tradeOffAnalyzer  │  ← TradeOff[], Risk[], MissingDataItem[]
        └──────────┬──────────┘
                   │
                   ▼
        ┌─────────────────────┐
        │   rankingEngine     │  ← Builds Match[], ranks, adds NextBestAction[]
        └──────────┬──────────┘
                   │
                   ▼
        ┌─────────────────────┐
        │  matchCardAdapter   │  ← Match → MatchCardViewModel (UI props)
        └──────────┬──────────┘
                   │
                   ▼
           MatchCard / MatchDetail / Compare

ScoreBreakdown shape:

{
  hardMatchScore: number       // 0100: area, location, budget, timing, usage
  softFactorScore: number      // 0100: prestige, accessibility, ESG, etc.
  confidenceModifier: number   // 20 → +5
  dataQualityModifier: number  // 15 → 0
  totalScore: number           // 0100 final score
  positiveFactors: ScoreFactor[]   // top reasons this is a good match
  negativeFactors: ScoreFactor[]   // top concerns
  tradeOffs: TradeOff[]            // explicit trade-off explanations
  risks: Risk[]                    // risk categories
  missingData: MissingDataItem[]   // fields that would improve the score
}

6. AI Flow

User input (text / voice)
         │
         ▼
  NeedInput component
         │  calls
         ▼
  aiService.parseNeed(text)
         │  routes to
         ├── MockAIService (dev) ──────────────► deterministic ParsedNeed
         └── OpenRouterAIService (prod) ────────► LLM API call
                    │
                    │  validates with Zod
                    ▼
             ParseNeedResult
             { need: Partial<Need>, confidence: number, followUpQuestions: string[] }
                    │
                    ▼
       CriteriaReviewPanel
       (user confirms / overrides AI-extracted criteria)
                    │
                    ▼
          Need saved → Results feed runs

AI Governance loop:

Every AI call
     │
     ├─► aiMonitoringService.log({ modelId, promptHash, responseDigest, userId, ... })
     │
     └─► Visible in /ops/ai-monitoring (review queue for ops team)

AI never speaks directly to the UI. All AI outputs go through:

  1. IAIService interface method
  2. Zod validation
  3. Typed domain object
  4. Component that renders it with explainability metadata

7. Unified Result Feed Flow (Demand)

The demand workspace shows a unified feed that merges three result types.

useUnifiedResults(needId)
         │
         ├─► matchService.getForNeed(needId)
         │        └─► VERIFIED_PORTFOLIO matches (own properties)
         │        └─► EXTERNAL_MARKET matches (partner/scraped)
         │
         ├─► aiService.getMaisonWorkResults(needId)
         │        └─► MAISON_WORK matches (AI-suggested)
         │
         └─► futureSignalService.getForNeed(needId)
                  └─► FUTURE_AVAILABILITY signals (AI-predicted)
                           ├── probabilityScore (01)
                           ├── signalBasis (string)
                           └── expectedAvailabilityDate

All merged into UnifiedMatchResult[]
Sorted by matchScore desc (default)
         │
         ▼
UnifiedResultFeed
  ├── MatchCard (VERIFIED_PORTFOLIO, EXTERNAL_MARKET, MAISON_WORK)
  └── FutureAvailabilityCard (FUTURE_AVAILABILITY — different visual treatment)

Future Availability signals are probabilistic — they must always show probabilityScore and a basis explanation. They are never presented as confirmed availability.


8. Three-Workspace Architecture

App.tsx
  │
  ├── /auth/login  → LoginScreen
  │
  ├── /supply/*  ──────────────────────────────────► SUPPLY workspace
  │     Protected: WorkspaceType.SUPPLY             (PropertyManager, OrgAdmin)
  │     Pages: SupplyDashboard, Properties,
  │            MatchCenter, Anfragencenter,
  │            FutureAvailability, DataQuality,
  │            ReminderManager, MarketLeads,
  │            NewListing, MyListings
  │
  ├── /demand/*  ──────────────────────────────────► DEMAND workspace
  │     Protected: WorkspaceType.DEMAND             (Tenant, CompanyAdmin)
  │     Pages: AISearch, Results, MatchDetail,
  │            Compare, Pipeline, Anfragen,
  │            PropertyDetail, Shortlists
  │
  └── /ops/*  ────────────────────────────────────► OPERATIONS workspace
        Protected: WorkspaceType.OPERATIONS         (Internal staff)
        Pages: MarketIntelligence

Route protection enforced by <ProtectedRoute workspace={...} />. Each workspace has distinct data flows and does not share components with other workspaces unless through src/components/shared/ or src/components/ui/.


9. Decision Screen Design Pattern

Every page is built around one user decision. The DecisionContextPanel component surfaces this explicitly at the top of each page.

┌─────────────────────────────────────────────────────────────┐
│ PageHeader — title, subtitle, secondary actions (ViewToggle) │
├─────────────────────────────────────────────────────────────┤
│ DecisionContextPanel                                         │
│   decision: "Which matches are ready to shortlist?"         │
│   context:  "Search: OFFICE · 200500 m² · Zürich"         │
│   metrics:  [strongMatches: 3 ✓] [withGaps: 2 ⚠]          │
│   risks:    ["2 results have missing floor plan data"]      │
│   actions:  [Open Compare] [Refine Search]                  │
├─────────────────────────────────────────────────────────────┤
│ FilterBar / Controls                                         │
├─────────────────────────────────────────────────────────────┤
│ Content (list / grid / table / kanban)                       │
└─────────────────────────────────────────────────────────────┘

If a screen cannot answer "what decision does this help the user make?", the design is wrong.


10. Error Boundary Architecture

App.tsx
└── AppErrorBoundary (top-level catch-all)
    └── Route components
        └── Feature-level try/catch in services
            └── Error state passed via useQuery error prop
                └── <ErrorState message={...} /> or <UnauthorizedState />

Every page handles three states: loading (<FeedSkeleton />), error (<ErrorState />), and empty (<FeedEmptyState />). These are never omitted.


11. Component Dependency Rules

pages/          can import from   components/, hooks/, stores/, lib/, domain/
components/     can import from   components/, hooks/, stores/, lib/, domain/
hooks/          can import from   services/, lib/, domain/
services/       can import from   provider/, features/, lib/, domain/
features/       can import from   lib/, domain/
provider/       can import from   domain/, mock-data/
lib/            can import from   (nothing in src — pure utilities)
domain/         can import from   (nothing — types only)
stores/         can import from   domain/, lib/

Forbidden imports:

  • services/ must not import React hooks
  • domain/ must not import anything from src/
  • lib/constants.ts must not import from components or services
  • Circular imports of any kind

12. Key Design System Tokens

Scores (0100 scale)
  ≥ 80  →  STRONG   →  DS_COLORS.confidence.high   (#1a7a4a)
  ≥ 60  →  MODERATE →  DS_COLORS.confidence.medium (#d97706)
  < 60  →  WEAK     →  DS_COLORS.confidence.low    (#c0392b)

Confidence (01 scale)
  ≥ 0.85  →  HIGH    →  DS_COLORS.confidence.high
  ≥ 0.65  →  MEDIUM  →  DS_COLORS.confidence.medium
  < 0.65  →  LOW     →  DS_COLORS.confidence.low

Data Quality (01 scale)
  ≥ 0.80  →  HIGH
  ≥ 0.60  →  MEDIUM
  < 0.60  →  LOW

Result Types
  VERIFIED_PORTFOLIO  →  label 'Plattform'      color #1e3a5f
  EXTERNAL_MARKET     →  label 'Plattform'      color #1e3a5f
  MAISON_WORK         →  label 'Maison Work'    color #0369a1
  FUTURE_AVAILABILITY →  label 'Zukunftssignal' color #7c3aed

Freshness
  FRESH    →  DS_COLORS.freshness.fresh    (#1a7a4a)
  STALE    →  DS_COLORS.freshness.stale    (#d97706)
  OUTDATED →  DS_COLORS.freshness.outdated (#dc2626)

All tokens from src/lib/ds.ts. All thresholds from src/lib/constants.ts.