9f7062d137
- Add 50 mock properties (prop-050–099) tuned for UC-A (Fahrradhändler RETAIL),
UC-B (Umzugsfirma OFFICE Zürich-West) and UC-C (Vermögensverwalter PREMIUM)
- District-aware scoreLocation: district match→100, city-only→70 when need
specifies districts, canton→60, no match→35; fixes UC-C prop-001 over-scoring
- mustHaveScorer: add klimatisierung keyword; parking minimum count logic
- Soft factor scale fix: integer 0–100 values no longer multiplied ×100
- footfall: map passerbyFrequency string (HIGH→85, MEDIUM_HIGH→68…) before
enrichment fallback so RETAIL properties score correctly
- Parser: Kreis list extraction ("Kreis 3, 4, 5, und 8" → 4 district entries),
neighbourhood→district map (Seefeld, Bahnhofstrasse), prestige signals
- Results: remove VERIFIED_PORTFOLIO role gate — all users see portfolio cards,
enabling gold (85+) and silver (70–84) cards for every demo use case
- Fix flash of wrong cards on NeedBuilder nav (effectiveNeedId not activeNeed?.id)
- needs.ts: UC-A preferredLocations now includes Kreis 3/4/5/8 entries
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
146 lines
6.4 KiB
TypeScript
146 lines
6.4 KiB
TypeScript
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
|
||
EXTERNAL_MARKET: -3, // EXTERNAL_MARKET result type
|
||
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[]
|
||
}
|