Files
property-match/src/domain/scoring.ts
T
Benjamin SutterandClaude Sonnet 4.6 609a3634bd feat: remove EXTERNAL_MARKET result type entirely
All external market properties now surface as VERIFIED_PORTFOLIO.
Removes the type from enums, domain types, mock data, matching engine,
components, hooks, and tests — zero user-visible distinction remains.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 17:28:42 +02:00

145 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ScoreFactor, TradeOff, Risk, MissingDataItem, NextBestAction, MustHaveResult } from './match'
// ── Hard Filter Thresholds ────────────────────────────────────────────────────
export const HARD_FILTER = {
AREA_MIN_TOLERANCE: 0.85, // exclude if property < 85% of need's min area
AREA_MAX_RATIO: 2.50, // severe penalty if property > 2.5× need's max area
BUDGET_EXCLUSION_RATIO: 1.50, // exclude if rent > 150% of max budget/m²
BUDGET_SEVERE_RATIO: 1.25, // severe penalty if 125–150% over budget
BUDGET_MODERATE_RATIO: 1.10, // mild penalty if 110–125% over budget
TIMING_GRACE_DAYS: 90, // allow ±90 days window flexibility
} as const
// ── Score Architecture ────────────────────────────────────────────────────────
// Within each group, scores are weighted and normalized to 0–100.
// Final = baseScore + dataQualityModifier + confidenceModifier, clamped 0–100.
export const SCORE_SPLIT = {
HARD_CRITERIA: 0.60, // expected contribution from hard criteria group
SOFT_FACTORS: 0.40, // expected contribution from soft factors group
} as const
export const HARD_CRITERION_KEYS = ['area', 'location', 'budget', 'timing'] as const
export type HardCriterionKey = typeof HARD_CRITERION_KEYS[number]
export const SOFT_FACTOR_KEYS = [
'prestige', 'accessibility', 'expansionPotential', 'flexibility',
'visibility', 'footfall', 'talentAccess', 'esg', 'taxEnvironment',
] as const
export type SoftFactorKey = typeof SOFT_FACTOR_KEYS[number]
// ── Modifier Tables ───────────────────────────────────────────────────────────
export const DATA_QUALITY_MODIFIER = {
EXCELLENT: +5, // dataQuality.score >= 0.85
GOOD: 0, // >= 0.70
FAIR: -5, // >= 0.55
POOR: -10, // >= 0.40
CRITICAL: -15, // < 0.40
} as const
export const CONFIDENCE_MODIFIER = {
VERIFIED_HIGH: +3, // VERIFIED_PORTFOLIO + confidenceScore >= 0.80
VERIFIED_MEDIUM: 0, // VERIFIED_PORTFOLIO + confidenceScore < 0.80
MAISON_WORK: -2, // MAISON_WORK — Maison Work API (slightly more trusted than scraped market)
FUTURE_AVAILABILITY: -15, // FUTURE_AVAILABILITY — never treat as confirmed availability
LOW_CONFIDENCE: -10, // confidenceScore < 0.50 (stacks with above)
} as const
// ── Scoring Weight Profile ────────────────────────────────────────────────────
export interface ScoringWeightProfile {
// Hard criteria
area: number
location: number
budget: number
timing: number
// Soft factors
prestige: number
accessibility: number
expansionPotential: number
flexibility: number
visibility: number
footfall: number
talentAccess: number
esg: number
taxEnvironment: number
[key: string]: number
}
// ── Default Profiles per Asset Type ──────────────────────────────────────────
// Each profile sums to 1.00. No magic numbers — weights reflect domain logic.
export const DEFAULT_SCORING_PROFILES: Record<string, ScoringWeightProfile> = {
// Büro: ÖV-Anbindung, Talent Access, Prestige, ESG stark gewichtet
OFFICE: {
area: 0.18, location: 0.18, budget: 0.15, timing: 0.09,
prestige: 0.07, accessibility: 0.11, expansionPotential: 0.05,
flexibility: 0.05, visibility: 0.02, footfall: 0.01,
talentAccess: 0.07, esg: 0.02, taxEnvironment: 0.00,
},
// Retail: Frequenz, Sichtbarkeit und Standort dominieren
RETAIL: {
area: 0.10, location: 0.15, budget: 0.13, timing: 0.06,
prestige: 0.04, accessibility: 0.07, expansionPotential: 0.03,
flexibility: 0.08, visibility: 0.14, footfall: 0.18,
talentAccess: 0.01, esg: 0.01, taxEnvironment: 0.00,
},
// Light Industrial: Fläche, Andienung (accessibility), Infrastruktur
LIGHT_INDUSTRIAL: {
area: 0.20, location: 0.12, budget: 0.18, timing: 0.10,
prestige: 0.01, accessibility: 0.14, expansionPotential: 0.07,
flexibility: 0.05, visibility: 0.01, footfall: 0.00,
talentAccess: 0.04, esg: 0.04, taxEnvironment: 0.04,
},
// Logistik: Autobahnanbindung (accessibility), Andienung, Fläche, Verfügbarkeit
LOGISTICS: {
area: 0.18, location: 0.18, budget: 0.14, timing: 0.13,
prestige: 0.01, accessibility: 0.18, expansionPotential: 0.06,
flexibility: 0.04, visibility: 0.01, footfall: 0.00,
talentAccess: 0.02, esg: 0.02, taxEnvironment: 0.03,
},
PRODUCTION: {
area: 0.22, location: 0.13, budget: 0.18, timing: 0.10,
prestige: 0.01, accessibility: 0.13, expansionPotential: 0.08,
flexibility: 0.04, visibility: 0.01, footfall: 0.00,
talentAccess: 0.04, esg: 0.03, taxEnvironment: 0.03,
},
DEFAULT: {
area: 0.20, location: 0.18, budget: 0.18, timing: 0.10,
prestige: 0.05, accessibility: 0.09, expansionPotential: 0.05,
flexibility: 0.05, visibility: 0.02, footfall: 0.02,
talentAccess: 0.03, esg: 0.02, taxEnvironment: 0.01,
},
}
// ── Engine IO Types ───────────────────────────────────────────────────────────
export interface HardFilterResult {
excluded: boolean
reason?: string
severePenalty: number // extra points deducted on top of criterion score (0–30)
}
export interface MatchEngineOutput {
propertyId: string
needId: string
excluded: boolean
excludedReason?: string
finalScore: number // 0–100 clamped
hardMatchScore: number // 0–100 normalized
softFactorScore: number // 0–100 normalized
dataQualityModifier: number
confidenceModifier: number
positiveFactors: ScoreFactor[]
negativeFactors: ScoreFactor[]
allHardFactors: ScoreFactor[]
allSoftFactors: ScoreFactor[]
tradeOffs: TradeOff[]
risks: Risk[]
missingData: MissingDataItem[]
nextBestActions: NextBestAction[]
mustHaveEvaluation?: MustHaveResult[]
}