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>
24 KiB
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 (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:
{
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<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:
IAIServiceinterface method- Zod validation
- Typed domain object
- 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 <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 · 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
└── <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 hooksdomain/must not import anything fromsrc/lib/constants.tsmust 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.