diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..185fc14 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,476 @@ +# 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`. diff --git a/CLAUDE.md b/CLAUDE.md index 01a1e3b..7a6cd10 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,40 +1,99 @@ -# property-match — Development Guidelines +# property-match — Engineering Guidelines -## Stack +> **This file is read by Claude Code on every session.** All rules here are binding. +> For architecture diagrams and data flows see [ARCHITECTURE.md](./ARCHITECTURE.md). +> For the pre-commit review checklist see [CODE_REVIEW_CHECKLIST.md](./CODE_REVIEW_CHECKLIST.md). +> For state management patterns see [STATE_MANAGEMENT.md](./STATE_MANAGEMENT.md). -- **Vite 8** + **React 19** + **TypeScript 6** -- **MUI v9** (`@mui/material`) — primary component library -- **Tailwind CSS v4** — utility classes via `@tailwindcss/vite` (no `tailwind.config.js`) -- **React Router v7** — import from `react-router`, not `react-router-dom` +--- -## Components +## 1. Project Vision -Always reach for an existing MUI component before writing a custom one. Check the [MUI component list](https://mui.com/material-ui/all-components/) first. Only build a custom component when MUI has no equivalent or the required behavior diverges significantly from what MUI provides. +Property Match is a **Decision Intelligence Platform** — not a real estate portal. -## Styling +The distinction matters for every design decision: -Use Tailwind utility classes for all layout and styling. Do not write plain CSS rules or add styles to `.css` files. The only CSS file is `src/index.css`, which holds the Tailwind layer imports — do not add project styles there. +| Classical Portal | Decision Intelligence | +|---|---| +| Show listings, let the user decide | Surface scored matches with explainability | +| Generic SaaS listing UI | Every screen is designed around one user decision | +| More filters = better | Fewer, smarter signals = better | +| Trust by volume | Trust by transparency | +**Three design principles that override everything else:** -## Providers +1. **Explainability-first.** Every score, badge, and recommendation must be traceable. If the user cannot understand *why* a match is strong, the feature is not done. Score breakdowns, trade-off panels, risk indicators, and data provenance are first-class UI elements — not tooltips added at the end. -All data access and data actions live in `src/provider/`. +2. **Trust-first.** Confidence levels, data freshness, and missing-data warnings are shown proactively. We never surface a result that looks more certain than the underlying data supports. When data is stale or incomplete, we say so. -### Naming +3. **Better-than-Google.** A property search result that just shows "here are things that match your query" fails the user. Property Match should rank, explain trade-offs, surface future signals, and give the user a clear recommended next action — like a trusted advisor, not a search engine. -| Rule | Example | -|------|---------| -| Every provider file/class is suffixed `Provider` | `PropertyProvider`, `UserProvider` | -| Every provider backed by mock data is also prefixed `Mockup` | `MockupPropertyProvider`, `MockupUserProvider` | +--- -### Interface pattern +## 2. Stack -Define a TypeScript interface for each provider so the mockup and the real implementation are interchangeable: -Every Interface should be prefixed with a capitalized I. +| Technology | Version | Role | +|---|---|---| +| Vite | 8 | Build tool & dev server | +| React | 19 | UI framework | +| TypeScript | 6 | Type safety | +| MUI | v9 (`@mui/material`) | Component library | +| Tailwind CSS | v4 | Utility classes (via `@tailwindcss/vite`) | +| React Router | v7 | Routing — import from `react-router`, not `react-router-dom` | +| TanStack Query | v5 | Server state & caching | +| Zustand | v5 | Global UI state | +| @dnd-kit | current | Drag-and-drop (Pipeline board) | +| Zod | current | Schema validation | +| Lucide React | current | Icon set (supplementary to MUI icons) | + +**There is no `tailwind.config.js`.** Tailwind v4 uses `@tailwindcss/vite` — config is inline. + +--- + +## 3. Three-Workspace Architecture + +The app is organized into three protected workspaces. Every route lives in exactly one workspace. + +| Workspace | Route prefix | Primary user | Core job-to-be-done | +|---|---|---|---| +| **SUPPLY** | `/supply/*` | Property manager / owner | Manage inventory, respond to demand, monitor market | +| **DEMAND** | `/demand/*` | Tenant / company | Find, compare, and pipeline commercial space | +| **OPERATIONS** | `/ops/*` | Internal staff | AI governance, market intelligence, audit logs | + +Route protection is enforced at the router level via ``. Never add workspace-specific logic inside shared components — pass `isStaff`, `isOwner`, etc. as props. + +--- + +## 4. Layered Architecture + +Data and logic flow through four explicit layers. **Never skip a layer.** + +``` +Provider → Service → Hook (React Query) → Component +``` + +### 4.1 Provider Layer (`src/provider/`) + +Providers are the **only** code that touches data storage. Everything else goes through them. + +**Rules:** +- Every entity has an interface: `I[Entity]Provider` (file: `I[Entity]Provider.ts`) +- Every interface is implemented by a mockup: `Mockup[Entity]Provider` (file: `Mockup[Entity]Provider.ts`) +- Every method is `async` and returns a `Promise` — even in the mockup +- Mockups hold in-memory seed data from `src/mock-data/` +- Real implementations swap in without changing any call sites + +**Naming:** + +| Pattern | Example | +|---|---| +| Interface | `IPropertyProvider` | +| Mock implementation | `MockupPropertyProvider` | +| Real implementation (future) | `RestPropertyProvider`, `SupabasePropertyProvider` | ```ts // src/provider/IPropertyProvider.ts -export interface PropertyProvider { +export interface IPropertyProvider { getAll(): Promise getById(id: string): Promise create(data: CreatePropertyInput): Promise @@ -43,38 +102,482 @@ export interface PropertyProvider { } ``` -### Async methods - -Every method in a provider must be `async` and return a `Promise`, even in the mockup. This ensures the real provider can be swapped in without changing any call sites. - ```ts // src/provider/MockupPropertyProvider.ts -import type { PropertyProvider } from './PropertyProvider' - -const properties: Property[] = [ /* seed data */ ] - -export const MockupPropertyProvider: PropertyProvider = { - async getAll() { - return [...properties] - }, - async getById(id) { - return properties.find(p => p.id === id) ?? null - }, - async create(data) { - const next: Property = { id: crypto.randomUUID(), ...data } - properties.push(next) - return next - }, - async update(id, data) { - const idx = properties.findIndex(p => p.id === id) - properties[idx] = { ...properties[idx], ...data } - return properties[idx] - }, - async remove(id) { - const idx = properties.findIndex(p => p.id === id) - properties.splice(idx, 1) - }, +export const MockupPropertyProvider: IPropertyProvider = { + async getAll() { return [...properties] }, + async getById(id) { return properties.find(p => p.id === id) ?? null }, + // ... } ``` -Swap to a real implementation by replacing `MockupPropertyProvider` with a provider that calls an API — no other code changes required. +**Anti-patterns:** +- Never call a provider directly from a component — use a service +- Never mutate provider state from outside the provider +- Never share state across providers (cross-provider coupling creates hidden dependencies) + +### 4.2 Service Layer (`src/services/`) + +Services wrap providers with business logic, error handling, and response standardization. + +**Rules:** +- Named `[entity]Service` (e.g., `matchService`, `needService`) +- Return standardized types: `ListResponse` or `ItemResponse` from `services/types.ts` +- Errors are thrown via `throwServiceError()` from `services/errors.ts` +- Services never import React hooks — they are plain TypeScript +- Services access Zustand stores via `getState()`, never via `useStore()` hooks + +```ts +// ✅ Service accessing auth — no hook +const user = useSessionStore.getState().currentUser +``` + +### 4.3 Hook Layer (`src/hooks/`) + +Hooks wrap React Query around services. + +**Rules:** +- One hook file per entity: `useProperties.ts`, `useMatches.ts`, etc. +- `useQuery` for reads, `useMutation` for writes +- Query keys follow the convention in STATE_MANAGEMENT.md +- Stale times are imported from `src/lib/constants.ts` — never defined locally in hooks +- Mutations call `queryClient.invalidateQueries` on success + +```ts +// ✅ Stale time from constants — never local +import { STALE_PROPERTIES } from '../lib/constants' +export function useProperties() { + return useQuery({ queryKey: ['properties'], queryFn: () => propertyService.getAll(), staleTime: STALE_PROPERTIES }) +} +``` + +### 4.4 Component Layer (`src/components/` + `src/pages/`) + +Components are **pure UI**. They render data, dispatch mutations, and show state. They do not contain business logic. + +--- + +## 5. Component Rules + +### 5.1 Size Limits + +| File type | Hard limit | Soft target | +|---|---|---| +| Page component (`src/pages/`) | 300 lines | 150–200 lines | +| Feature component | 250 lines | 100–150 lines | +| Atom/shared component | 150 lines | 50–100 lines | + +When a component exceeds its soft target, split it. Extract sub-components, move constants to a `*Constants.ts` file, move pure helpers to a `*Utils.ts` file (`.tsx` if it returns JSX). + +### 5.2 No Business Logic in JSX + +The render body is for presentation only. All logic belongs upstream. + +```tsx +// ❌ Business logic in JSX +{matches.filter(m => m.score > 70 && m.status !== 'DISMISSED').map(...)} + +// ✅ Logic in useMemo, computed before the return +const visibleMatches = useMemo( + () => matches.filter(m => m.score > 70 && m.status !== 'DISMISSED'), + [matches] +) +// then: +{visibleMatches.map(...)} +``` + +### 5.3 No Magic Strings + +Every label, status, color, and route string has a named constant. + +```ts +// ❌ +if (user.role === 'PROPERTY_MANAGER') { ... } +navigate('/supply/properties') + +// ✅ +import { UserRole } from '../domain/enums' +import { ROUTES } from '../lib/constants' +if (user.role === UserRole.PROPERTY_MANAGER) { ... } +navigate(ROUTES.SUPPLY.PROPERTIES) +``` + +All German UI labels for enums live in `src/lib/constants.ts` (e.g., `ASSET_TYPE_LABELS`, `AVAILABILITY_LABELS`). + +### 5.4 No `any` Casts + +`any` disables TypeScript. Use `unknown` with type guards, or fix the type properly. + +```ts +// ❌ +const result = response as any +const { data } = result + +// ✅ +const result: ItemResponse = response +const { data } = result +``` + +The one allowed exception: third-party library types that don't ship proper types. Wrap the cast in a type-safe adapter so the `any` is isolated. + +### 5.5 No Duplicated State + +If a value can be computed from existing state, compute it. Do not store it. + +```ts +// ❌ Duplicated — sync bugs guaranteed +const [overdueCount, setOverdueCount] = useState(0) +useEffect(() => setOverdueCount(reminders.filter(r => isPast(r.dueDate)).length), [reminders]) + +// ✅ Derived inline +const overdueCount = reminders.filter(r => isPast(r.dueDate)).length +``` + +See STATE_MANAGEMENT.md for the full decision tree. + +### 5.6 Component Library First + +Before writing a custom component, check the [MUI component list](https://mui.com/material-ui/all-components/). Only build custom when: +- MUI has no equivalent, or +- The required behavior diverges significantly from what MUI provides + +### 5.7 Styling + +Use Tailwind utility classes for all layout and spacing. Use MUI's `sx` prop for component-specific overrides and theme values. **Never add styles to `.css` files.** The only CSS file is `src/index.css` (Tailwind layer imports) — do not add project styles there. + +--- + +## 6. State Management + +Full rules in [STATE_MANAGEMENT.md](./STATE_MANAGEMENT.md). Summary: + +| Data type | Where it lives | +|---|---| +| Server data (fetched from provider) | React Query (`useQuery`) | +| Write operations | React Query (`useMutation`) | +| Global UI state (dialog open/close, compare tray, wizard steps) | Zustand store | +| Auth session | `sessionStore` (Zustand, also read by services via `getState()`) | +| Component-local state (form inputs, hover, toggle) | `useState` | +| Values derived from existing state | Computed inline — never stored | + +**Critical rules:** +- Never copy React Query data into a Zustand store +- Never call `useStore()` without a selector (use `s => s.field`) +- Services read Zustand via `getState()`, never via hooks +- Side effects (cache invalidation, analytics) go in `useEffect`, never in the render body + +--- + +## 7. Domain & Types + +### 7.1 Single Source of Truth + +All domain types live in `src/domain/`. Never duplicate a type or interface. + +| File | Contents | +|---|---| +| `enums.ts` | All enums: `AssetType`, `ResultType`, `MatchStatus`, `UserRole`, `WorkspaceType`, etc. | +| `property.ts` | `Property`, `Location`, `ContactPerson` | +| `match.ts` | `Match`, `ScoreBreakdown`, `ScoreFactor`, `TradeOff`, `Risk` | +| `need.ts` | `Need`, `AreaRange`, `BudgetRange`, `WeightingProfile` | +| `unifiedResult.ts` | `UnifiedMatchResult` (the discriminated union for result feeds) | +| `scoring.ts` | `ScoringWeightProfile`, `HardFilterResult`, `MatchEngineOutput` | +| `aiOutput.ts` | AI scoring, extraction, monitoring types | +| ... | See `/domain/index.ts` for full list | + +### 7.2 No Inline Type Duplication + +```ts +// ❌ Redefining a type locally +interface LocalProperty { id: string; title: string; ... } + +// ✅ Import from domain +import type { Property } from '../../domain/property' +``` + +### 7.3 Response Types + +All service responses use the standardized wrapper from `src/services/types.ts`: + +```ts +type ListResponse = { data: T[]; total: number } +type ItemResponse = { data: T } +``` + +--- + +## 8. Design System + +All visual constants are centralized. **Never hardcode a color, spacing multiplier, or border-radius in a component.** + +### 8.1 Color Tokens (`src/lib/ds.ts`) + +```ts +DS_COLORS.confidence.high // '#1a7a4a' +DS_COLORS.confidence.medium // '#d97706' +DS_COLORS.confidence.low // '#c0392b' +DS_COLORS.risk.critical // '#dc2626' +DS_COLORS.risk.warning // '#d97706' +DS_COLORS.freshness.stale // '#d97706' +DS_COLORS.freshness.outdated// '#dc2626' +``` + +### 8.2 Score → Color Helpers (`src/lib/utils.ts`) + +| Function | Input | Output | Use for | +|---|---|---|---| +| `matchScoreHex(score)` | 0–100 | hex string | Overall match score color | +| `confidenceHex(score)` | 0–1 | hex string | Confidence score color | +| `dataQualityColor(score)` | 0–1 | hex string | Data quality color | +| `criterionScoreColor(score)` | 0–100 | MUI color token | Individual criterion badge | +| `criterionScoreTextColor(score)` | 0–100 | hex string | Criterion text color | + +### 8.3 Result Type Metadata (`src/lib/ds.ts`) + +Use `RESULT_TYPE_META[resultType]` for label, color, and background. Never define these inline. + +```ts +// ❌ +label="Verified Portfolio" +bgcolor="#1e3a5f" + +// ✅ +import { RESULT_TYPE_META } from '../../lib/ds' +label={RESULT_TYPE_META[result.resultType]?.label} +bgcolor={RESULT_TYPE_META[result.resultType]?.color} +``` + +### 8.4 Thresholds (`src/lib/constants.ts`) + +```ts +SCORE_STRONG = 80 // match score considered "strong" +SCORE_MODERATE = 60 // match score considered "moderate" +CONF_HIGH = 0.85 // confidence considered "high" +CONF_MEDIUM = 0.65 // confidence considered "medium" +DQ_HIGH = 0.8 // data quality considered "high" +DQ_MEDIUM = 0.6 // data quality considered "medium" +``` + +These thresholds are used by `scoreToConfidenceLevel()`, `scoreToDataQualityLevel()`, and all badge logic. Never re-define them locally. + +--- + +## 9. AI Integration Rules + +Property Match integrates AI as a **service**, not as a feature bolted onto the UI. + +### 9.1 Model-Agnostic Interface + +AI is accessed exclusively through `IAIService` (`src/services/ai/IAIService.ts`). The interface defines capabilities (parseNeed, compareProperties, generateDecisionBrief, etc.). No component imports an LLM client directly. + +```ts +// ❌ Never in a component or hook +import OpenAI from 'openai' +const client = new OpenAI(...) + +// ✅ Always via service +import { aiService } from '../services/aiService' +const result = await aiService.parseNeed(text) +``` + +### 9.2 Implementations + +| Implementation | File | Used when | +|---|---|---| +| `MockAIService` | `src/services/ai/MockAIService.ts` | Dev / CI — deterministic, no API calls | +| `OpenRouterAIService` | `src/services/ai/OpenRouterAIService.ts` | Production — OpenRouter API | + +Swap implementations by changing the export in `src/services/aiService.ts`. No other file changes required. + +### 9.3 Structured Responses + +AI calls must return typed, structured objects — never raw strings passed into JSX. + +```ts +// ❌ Raw LLM output in JSX +{llmResponse} + +// ✅ Parsed, validated, typed output +const result: ParsedNeed = await aiService.parseNeed(text) + +``` + +All AI response schemas are validated with Zod before use. + +### 9.4 Explainable Outputs + +Every AI output that influences a user decision must be accompanied by an explanation. + +- Match scores include `ScoreFactor[]` (positive & negative) +- Trade-off analysis includes `TradeOff[]` with severity +- Future availability signals include `probabilityScore` + `signalBasis` +- AI-parsed needs include `confidence` + which fields were inferred vs. stated + +### 9.5 Human-in-the-Loop + +AI suggestions are always proposals — the user confirms or overrides. + +- Need parsing: user reviews extracted criteria before search runs +- AI recommendations: presented with confidence, not as commands +- Review queue (`/ops`): all AI-generated content can be reviewed and corrected +- Governance: `aiMonitoringService` logs every AI output with model version, prompt hash, and response + +### 9.6 Fallback Handling + +Every AI call has a defined fallback. If the AI service fails or returns a low-confidence result: +- The UI shows a manual entry path +- Confidence badges reflect uncertainty +- No AI failure should block a user workflow + +--- + +## 10. Performance Rules + +### 10.1 No Expensive Calculations in Render + +Any computation over a list, aggregation, or filter runs in `useMemo`, not in the render body. + +```tsx +// ❌ Runs on every render — expensive if list is large +return ( +
+ {properties + .filter(p => p.confidenceScore >= 0.7) + .sort((a, b) => b.areaSqm - a.areaSqm) + .map(p => )} +
+) + +// ✅ Runs only when dependencies change +const visible = useMemo( + () => properties.filter(p => p.confidenceScore >= 0.7).sort((a, b) => b.areaSqm - a.areaSqm), + [properties] +) +return
{visible.map(p => )}
+``` + +### 10.2 Memoize List Items + +Components rendered in lists (feeds, tables, grids) must be wrapped in `React.memo` to prevent cascade re-renders when parent state changes (e.g., a filter toggle or `selectedId` update). + +```ts +// ✅ List item — always memo +export const PropertyCard = memo(function PropertyCard({ property, onSelect }: Props) { ... }) +export const ReminderListRow = memo(function ReminderListRow({ reminder }: Props) { ... }) +``` + +### 10.3 Stable Callbacks for Memoized Children + +When passing callbacks as props to memoized children, wrap them in `useCallback`. An unstable function reference breaks `React.memo`. + +```ts +// ✅ Stable reference — memo children won't re-render +const handleSelect = useCallback((id: string) => setSelectedId(id), []) +``` + +### 10.4 Single-Pass Aggregations + +When computing multiple aggregates over the same list (e.g., count of matchReady + criticalGaps + lowConfidence), use a single `for` loop in one `useMemo` — not three separate `.filter()` calls. + +### 10.5 Virtualization + +The current page size (`DEFAULT_PAGE_SIZE = 25`) makes React virtualization unnecessary today. If paginated lists are removed or page size exceeds ~200 items, evaluate `@tanstack/virtual` for the result feed and property table. + +### 10.6 Bundle Splitting + +All routes are already lazy-loaded via `React.lazy()` in `App.tsx`. Keep it that way — do not import page components directly. + +--- + +## 11. Security & Governance + +### 11.1 Auth & Session + +- Auth state lives in `sessionStore` (Zustand) +- On logout, **clear the React Query cache**: `queryClient.clear()` +- Role checks use `src/lib/permissions.ts` — never inline `role === 'PROPERTY_MANAGER'` +- Workspace access is enforced by `` — never guard with `if` inside a component + +### 11.2 Role-Based Rendering + +```ts +// ❌ Magic string role check +if (user.role === 'PROPERTY_MANAGER') { ... } + +// ✅ Permission helper +import { canViewMatchCenter } from '../../lib/permissions' +if (canViewMatchCenter(user)) { ... } +``` + +### 11.3 No Sensitive Data in Client State + +- Do not store tokens, passwords, or PII in Zustand stores beyond what is strictly needed for session identity +- Do not log sensitive fields in console statements +- Do not pass sensitive data as URL params (use navigation state instead) + +### 11.4 AI Audit Logging + +Every AI output that influences a business decision is logged via `aiMonitoringService`. Logs include: +- Timestamp +- Model ID and version +- Prompt hash (not the full prompt — for privacy) +- Response summary +- User ID and workspace + +### 11.5 Data Lineage + +Every property record carries `DataSource` metadata (origin, freshness, last verified). Surface this data proactively — especially when `freshness === 'STALE'` or `freshness === 'OUTDATED'`. + +--- + +## 12. Adding a New Feature — Checklist + +Before writing any code, answer these questions: + +1. **Which workspace does this belong to?** (SUPPLY / DEMAND / OPS) +2. **What decision does this help the user make?** (If you can't answer this, reconsider the feature) +3. **Does a provider already exist for this entity?** If not, create `I[Entity]Provider` + `Mockup[Entity]Provider` first +4. **Does a service already exist?** If not, create `[entity]Service` that wraps the provider +5. **Does a hook already exist?** If not, create `use[Entity]` in `src/hooks/` +6. **Is this server state or UI state?** (see STATE_MANAGEMENT.md) +7. **Does this show a score or confidence value?** Use `DS_COLORS` + helper functions from `lib/utils.ts` +8. **Does this involve AI?** Go through `IAIService` — never call LLM APIs directly +9. **Does this need explainability?** Add score factors, provenance, or reasoning before calling it done + +**Do not** add business logic to components. **Do not** duplicate types from `/domain/`. **Do not** hardcode colors, labels, or thresholds. + +--- + +## 13. File Naming & Organization + +| Location | What goes there | +|---|---| +| `src/pages/[workspace]/` | Route-level page components | +| `src/components/[feature]/` | Feature-specific components | +| `src/components/shared/` | Components used across features | +| `src/components/ui/` | Atomic UI: ErrorBoundary, EmptyState, Toast, DecisionContextPanel | +| `src/components/layout/` | AppShell, PageHeader, RightContextPanel | +| `src/hooks/` | React Query hooks | +| `src/services/` | Business logic services | +| `src/services/ai/` | AI service interface + implementations + prompts | +| `src/provider/` | Data access interfaces + mock implementations | +| `src/domain/` | TypeScript types and enums | +| `src/features/matching/` | Scoring and ranking algorithms | +| `src/stores/` | Zustand stores | +| `src/lib/` | Shared utilities, theme, constants, design system | +| `src/mock-data/` | Seed data for mock providers | + +Each component folder has an `index.ts` barrel export. New components must be added to the barrel before use. + +**File extension rule:** Any file that contains JSX must use `.tsx`. Plain TypeScript (no JSX) uses `.ts`. Violating this causes Vite parse errors. + +--- + +## 14. TypeScript Rules + +- `strict: true` is enforced — do not disable strict mode or individual checks +- Prefer `interface` for object shapes (can be extended); use `type` for unions and aliases +- Props interfaces are named `Props` (local) or `[ComponentName]Props` (exported) +- Never use `@ts-ignore` — fix the type instead +- `as unknown as T` is acceptable only when bridging genuinely untyped third-party code; wrap it in a named adapter function +- Enums from `src/domain/enums.ts` are the source of truth — never redefine values locally diff --git a/CODE_REVIEW_CHECKLIST.md b/CODE_REVIEW_CHECKLIST.md new file mode 100644 index 0000000..3a8873f --- /dev/null +++ b/CODE_REVIEW_CHECKLIST.md @@ -0,0 +1,226 @@ +# Code Review Checklist — Property Match + +> Use this checklist before every PR review and before considering any implementation "done". +> For architecture context see [ARCHITECTURE.md](./ARCHITECTURE.md). +> For coding rules see [CLAUDE.md](./CLAUDE.md). + +--- + +## A. Component Size & Structure + +- [ ] **Is the component too large?** + - Page component: hard limit 300 lines, target 150–200 + - Feature component: hard limit 250 lines, target 100–150 + - Atom/shared component: hard limit 150 lines + - If over limit: extract sub-components, move constants to `*Constants.ts`, move helpers to `*Utils.ts` (`.tsx` if JSX) + +- [ ] **Is business logic inside JSX?** + - `.filter()`, `.sort()`, `.reduce()`, conditional rendering based on computed values — these belong in `useMemo`, not inline in the return + - The render body should read computed values, not compute them + +- [ ] **Are hardcoded strings present?** + - Status labels (`'ACTIVE'`, `'Aktiv'`, `'PROPERTY_MANAGER'`) must come from `enums.ts` or `constants.ts` + - Route paths must come from `ROUTES` constants, not string literals + - German UI labels for enums live in `constants.ts` (e.g., `ASSET_TYPE_LABELS`) + +- [ ] **Are hardcoded colors or thresholds present?** + - Colors like `'#1a7a4a'`, `'#dc2626'` directly in `sx` props → use `DS_COLORS` from `lib/ds.ts` + - Score thresholds like `score >= 80` → use `SCORE_STRONG` from `constants.ts` + - Confidence thresholds like `conf >= 0.75` → use `CONF_HIGH` from `constants.ts` + +- [ ] **Are duplicate result type labels/colors defined locally?** + - `RESULT_TYPE_META` in `lib/ds.ts` is the single source of truth + - `RESULT_TYPE_LABEL` / `RESULT_TYPE_COLOR` inline objects anywhere else → delete them + +--- + +## B. State & Data + +- [ ] **Is server data stored in Zustand?** + - Any `useEffect(() => setFoo(data), [data])` pattern that mirrors React Query data into local/Zustand state → remove it, read directly from the query + - Server data lives exclusively in the React Query cache + +- [ ] **Is derived state stored in state?** + - `const [count, setCount] = useState(0)` + `useEffect` to keep it in sync → compute inline or use `useMemo` + - Exception: expensive computations may use `useMemo` + +- [ ] **Is the full Zustand store subscribed to without a selector?** + - `const { a, b, c } = useMyStore()` — re-renders on every store mutation + - Use `useMyStore(s => s.a)` for single fields, `useShallow` for multiple fields + +- [ ] **Is there a side effect in the render body?** + - `queryClient.invalidateQueries(...)` called during render → must be in `useEffect` + - Any `console.log`, analytics call, or mutation triggered directly in render body → `useEffect` + +- [ ] **Are all hooks called before conditional early returns?** + - React rules of hooks: all `useState`, `useMemo`, `useCallback`, `useQuery` calls must appear before any `if (isLoading) return ...` + +--- + +## C. Type Safety + +- [ ] **Are there `any` casts?** + - `as any`, `: any`, `// @ts-ignore` — fix the type or use `unknown` with a type guard + - One exception: bridging untyped third-party code — must be isolated in a named adapter + +- [ ] **Are domain types re-defined locally?** + - Any interface or type that duplicates something in `src/domain/` → import from domain instead + +- [ ] **Are response types used?** + - Service functions should return `ListResponse` or `ItemResponse` from `services/types.ts` + +- [ ] **Do `.tsx` files contain JSX and `.ts` files not?** + - Any file with JSX (``, ``, ``) must have `.tsx` extension + - Plain TypeScript utilities use `.ts` + +--- + +## D. Layering & Architecture + +- [ ] **Is business logic in a component?** + - Scoring calculations, ranking, filtering rules, AI calls, data transformation → move to service or `features/` layer + - Components only render and dispatch + +- [ ] **Is a provider called directly from a component or hook?** + - `MockupPropertyProvider.getAll()` called in a hook → must go through `propertyService` + - All provider access is mediated by a service + +- [ ] **Is an LLM API called directly?** + - Any `fetch` to an AI API, or direct `OpenAI` / `openrouter` SDK calls outside `src/services/ai/` → move behind `IAIService` + +- [ ] **Are there cross-workspace component imports?** + - A supply component importing from `components/demand/` or vice versa → use `components/shared/` or `components/ui/` instead + +- [ ] **Are the import direction rules respected?** + - `services/` must not import React hooks + - `domain/` must not import anything from `src/` + - `lib/constants.ts` must not import from components or services + - No circular imports + +--- + +## E. Loading, Error & Empty States + +- [ ] **Does every data-dependent view handle loading?** + - Every `useQuery` that drives a list or table must show a skeleton or spinner while `isLoading` + - Use the appropriate skeleton component (`FeedSkeleton`, `ReminderSkeleton`, etc.) + +- [ ] **Does every data-dependent view handle error?** + - `isError` from `useQuery` must show `` + - Never leave an error state blank or silently swallowed + +- [ ] **Does every list handle the empty state?** + - Empty results after filtering → `` with a reset action + - Empty because no data exists → `` + +--- + +## F. Performance + +- [ ] **Are list items wrapped in `React.memo`?** + - Components rendered inside `.map()` in feeds, tables, or grids → should be `memo()` + - Without memo, every parent state change (e.g., filter toggle, `selectedId`) re-renders the entire list + +- [ ] **Are callbacks to memoized children wrapped in `useCallback`?** + - Passing `() => setSelectedId(id)` inline as a prop to a `memo()`-wrapped child → the memo is broken + - Wrap with `useCallback` and explicit dependency array + +- [ ] **Are multiple aggregations over the same list collapsed into one pass?** + - Three separate `useMemo` calls that each `.filter()` the same array → collapse into one `useMemo` with a `for` loop + +- [ ] **Is `useMemo` used for expensive filters and sorts?** + - `.filter()` + `.sort()` over 25+ items in the render body → `useMemo` + - Decision aggregates (matchReady count, criticalGaps count, etc.) in `Properties.tsx` → already in `useMemo` + +- [ ] **Are routes lazy-loaded?** + - All page imports in `App.tsx` must use `React.lazy()` — never static imports for route components + +--- + +## G. Explainability & Trust + +- [ ] **Does every score shown have a breakdown path?** + - A match score badge with no way to see *why* it's that score → add `ScoreBreakdownPanel` or link to MatchDetail + +- [ ] **Are confidence levels shown proactively?** + - Data with `confidenceScore < CONF_MEDIUM` or `freshness === 'STALE'` → must be visually flagged + - `ConfidenceBadge`, `FreshnessIndicator`, `DataQualityBar` exist for this purpose + +- [ ] **Are Future Availability signals clearly labeled as probabilistic?** + - `FUTURE_AVAILABILITY` results must always show `probabilityScore` and `signalBasis` + - They must never look identical to confirmed availability listings + +- [ ] **Does AI output show its confidence?** + - AI-extracted need criteria → shown with confidence indicator + - AI match suggestions → shown with `MAISON_WORK` badge and probability + +--- + +## H. Security + +- [ ] **Is the React Query cache cleared on logout?** + - Logout flow must call `queryClient.clear()` — otherwise stale data from previous user persists + +- [ ] **Are role checks using `permissions.ts` helpers?** + - `user.role === 'PROPERTY_MANAGER'` inline → use `canViewMatchCenter(user)` from `lib/permissions.ts` + +- [ ] **Are workspace route guards in place?** + - Every supply/demand/ops route wrapped in `` + - Never guard with `if (user.workspace !== ...)` inside a component body + +- [ ] **Is sensitive data stored in Zustand?** + - PII, tokens, and sensitive fields beyond current-user identity should not persist in client state + +--- + +## I. Backend-Readiness + +- [ ] **Is the provider interface complete?** + - Any new entity that creates a mock shortcut (array mutation inline, no interface) → add `I[Entity]Provider` and `Mockup[Entity]Provider` + +- [ ] **Are async patterns consistent?** + - All provider methods are `async` — if a new provider method is synchronous, fix it before it gets called from a service + +- [ ] **Are query keys stable and specific?** + - Query key `['properties']` is fine for list; `['properties', id]` for single item + - Keys that include derived or computed objects (not primitives) will break cache lookup + +- [ ] **Are stale times centralized?** + - No `staleTime: 60_000` or `staleTime: 5 * 60 * 1000` inline in hook files + - Use `STALE_PROPERTIES`, `STALE_MATCHES`, etc. from `src/lib/constants.ts` + +--- + +## J. AI & Model Governance + +- [ ] **Is the AI call going through `IAIService`?** + - Direct fetch to AI API anywhere outside `src/services/ai/` → move it + +- [ ] **Is the AI response validated with Zod before use?** + - Raw LLM string parsed inline with `JSON.parse()` without a schema → add Zod validation + +- [ ] **Is the AI output logged via `aiMonitoringService`?** + - Any AI call that influences a business decision (need parsing, match recommendation, market signal) → log it + +- [ ] **Does the AI flow have a fallback?** + - If `aiService` throws or returns `confidence < threshold`, is there a manual entry path? + - AI failures must never block the user workflow + +--- + +## Quick Checklist — Pre-Commit + +Before every commit, verify: + +``` +[ ] npx tsc --noEmit → zero errors +[ ] No new hardcoded colors, routes, or role strings +[ ] No business logic added to a JSX return block +[ ] No React Query data mirrored into Zustand or useState +[ ] All hooks called before early returns +[ ] New list-item components wrapped in React.memo +[ ] New provider methods are async and match the interface +[ ] New feature goes through Provider → Service → Hook → Component +[ ] Loading, error, and empty states handled for any new data-fetching view +[ ] File with JSX has .tsx extension +```