# Property Match — Architecture Reference > This document describes the architecture of the Property Match platform. > For coding rules see [CLAUDE.md](./CLAUDE.md). > For state management patterns see [STATE_MANAGEMENT.md](./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, ItemResponse │ └── 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 (0–100 each) │ 3. Soft scoring │ ← Prestige, accessibility, ESG, footfall, etc. (0–100 each) │ 4. Modifiers │ ← confidenceModifier (-20→+5) + dataQualityModifier (-15→0) │ 5. totalScore │ ← Weighted average + modifiers (0–100) └──────────┬──────────┘ │ ▼ ┌─────────────────────┐ │ tradeOffAnalyzer │ ← TradeOff[], Risk[], MissingDataItem[] └──────────┬──────────┘ │ ▼ ┌─────────────────────┐ │ rankingEngine │ ← Builds Match[], ranks, adds NextBestAction[] └──────────┬──────────┘ │ ▼ ┌─────────────────────┐ │ matchCardAdapter │ ← Match → MatchCardViewModel (UI props) └──────────┬──────────┘ │ ▼ MatchCard / MatchDetail / Compare ``` **ScoreBreakdown shape:** ```ts { hardMatchScore: number // 0–100: area, location, budget, timing, usage softFactorScore: number // 0–100: prestige, accessibility, ESG, etc. confidenceModifier: number // –20 → +5 dataQualityModifier: number // –15 → 0 totalScore: number // 0–100 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, 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 (0–1) ├── 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 ``. 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 · 200–500 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 └── or ``` Every page handles three states: loading (``), error (``), and empty (``). 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 (0–100 scale) ≥ 80 → STRONG → DS_COLORS.confidence.high (#1a7a4a) ≥ 60 → MODERATE → DS_COLORS.confidence.medium (#d97706) < 60 → WEAK → DS_COLORS.confidence.low (#c0392b) Confidence (0–1 scale) ≥ 0.85 → HIGH → DS_COLORS.confidence.high ≥ 0.65 → MEDIUM → DS_COLORS.confidence.medium < 0.65 → LOW → DS_COLORS.confidence.low Data Quality (0–1 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`.