# property-match — Engineering Guidelines

> **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).

---

## 1. Project Vision

Property Match is a **Decision Intelligence Platform** — not a real estate portal.

The distinction matters for every design decision:

| 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:**

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.

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.

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.

---

## 2. Stack

| 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 `<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
// src/provider/IPropertyProvider.ts
export interface IPropertyProvider {
  getAll(): Promise<Property[]>
  getById(id: string): Promise<Property | null>
  create(data: CreatePropertyInput): Promise<Property>
  update(id: string, data: UpdatePropertyInput): Promise<Property>
  remove(id: string): Promise<void>
}
```

```ts
// src/provider/MockupPropertyProvider.ts
export const MockupPropertyProvider: IPropertyProvider = {
  async getAll() { return [...properties] },
  async getById(id) { return properties.find(p => p.id === id) ?? null },
  // ...
}
```

**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 | 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<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)` | 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
<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
