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>
This commit is contained in:
Benjamin Sutter
2026-05-24 02:10:30 +02:00
parent ad84fe8289
commit 713ef3ec08
3 changed files with 1256 additions and 51 deletions
+476
View File
@@ -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<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:**
```ts
{
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`.
+554 -51
View File
@@ -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: | Technology | Version | Role |
Every Interface should be prefixed with a capitalized I. |---|---|---|
| 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 `<ProtectedRoute workspace={WorkspaceType.X} />`. 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 ```ts
// src/provider/IPropertyProvider.ts // src/provider/IPropertyProvider.ts
export interface PropertyProvider { export interface IPropertyProvider {
getAll(): Promise<Property[]> getAll(): Promise<Property[]>
getById(id: string): Promise<Property | null> getById(id: string): Promise<Property | null>
create(data: CreatePropertyInput): Promise<Property> create(data: CreatePropertyInput): Promise<Property>
@@ -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 ```ts
// src/provider/MockupPropertyProvider.ts // src/provider/MockupPropertyProvider.ts
import type { PropertyProvider } from './PropertyProvider' export const MockupPropertyProvider: IPropertyProvider = {
async getAll() { return [...properties] },
const properties: Property[] = [ /* seed data */ ] async getById(id) { return properties.find(p => p.id === id) ?? null },
// ...
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)
},
} }
``` ```
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<T>` or `ItemResponse<T>` 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 | 150200 lines |
| Feature component | 250 lines | 100150 lines |
| Atom/shared component | 150 lines | 50100 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<Property> = 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<T> = { data: T[]; total: number }
type ItemResponse<T> = { 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)` | 0100 | hex string | Overall match score color |
| `confidenceHex(score)` | 01 | hex string | Confidence score color |
| `dataQualityColor(score)` | 01 | hex string | Data quality color |
| `criterionScoreColor(score)` | 0100 | MUI color token | Individual criterion badge |
| `criterionScoreTextColor(score)` | 0100 | 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
<Typography>{llmResponse}</Typography>
// ✅ Parsed, validated, typed output
const result: ParsedNeed = await aiService.parseNeed(text)
<NeedCardPreview need={result.need} confidence={result.confidence} />
```
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 (
<div>
{properties
.filter(p => p.confidenceScore >= 0.7)
.sort((a, b) => b.areaSqm - a.areaSqm)
.map(p => <PropertyCard key={p.id} property={p} />)}
</div>
)
// ✅ Runs only when dependencies change
const visible = useMemo(
() => properties.filter(p => p.confidenceScore >= 0.7).sort((a, b) => b.areaSqm - a.areaSqm),
[properties]
)
return <div>{visible.map(p => <PropertyCard key={p.id} property={p} />)}</div>
```
### 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 `<ProtectedRoute workspace={...} />` — 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
+226
View File
@@ -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 150200
- Feature component: hard limit 250 lines, target 100150
- 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<T>` or `ItemResponse<T>` from `services/types.ts`
- [ ] **Do `.tsx` files contain JSX and `.ts` files not?**
- Any file with JSX (`<Component />`, `<Box>`, `<Typography>`) 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 `<ErrorState message={error.message} />`
- Never leave an error state blank or silently swallowed
- [ ] **Does every list handle the empty state?**
- Empty results after filtering → `<FeedEmptyState filtered={true} />` with a reset action
- Empty because no data exists → `<FeedEmptyState filtered={false} />`
---
## 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 `<ProtectedRoute workspace={...} />`
- 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
```