SPB Git forge

spb/vrai-prix

Public

Vrai-Prix — l'évaluation du vrai prix des propriétés résidentielles au Québec.

60commits 1branches 0releases
12.3 MBsize
maindefault branch
17 days agolast push
TypeScript 90.2% JavaScript 3.5% Python 3.4% CSS 1.9% HTML 0.6%
17.2 KB · 535 lines typescript
Raw Blame History
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2/**3 * Méthode du coût — types partagés du moteur, de la base de coûts et4 * de l'interface. Module PUR (aucun import serveur) : importable côté client.5 *6 * Vocabulaire :7 *  - article (cost item)   : matériau ou fourniture canonique (ex. 2x6 SPF 8 pi)8 *  - assemblage (assembly) : composante réelle de construction (ex. mur 2x6 R24 vinyle)9 *  - ligne (line)          : un assemblage quantifié et chiffré dans une estimation10 *  - provenance            : d'où vient chaque nombre (observé, calculé, indexé, hypothèse…)11 */1213/* ----------------------------------------------------------- provenance */1415/** Nature d'un prix affiché (icônes ● ◐ ○ ◇ AI dans l'UI). */16export type PriceKind =17  | "observed" // ● observé directement chez un fournisseur18  | "derived" // ◐ calculé depuis des composants19  | "indexed" // ○ observation ancienne actualisée par un indice20  | "reference" // ◇ prix de référence interne (hypothèse documentée, non observé)21  | "assumption" // ◇ hypothèse documentée (pourcentage, productivité…)22  | "official"; // ● taux réglementé / publication officielle (APCHQ/CCQ, StatCan)2324/** Origine d'une caractéristique du bâtiment. */25export type AttributeSource = "MAMH" | "listing" | "user" | "AI" | "derived" | "assumed" | "cadastral";2627/** Couche d'affichage (§208) : OBSERVÉ · INFÉRÉ PAR IA · CALCULÉ · PRIX SOURCÉ. */28export type Layer = "observed" | "ai_inferred" | "computed" | "sourced_price" | "assumption";2930export interface Provenance {31  kind: PriceKind;32  source: string; // nom de la source (ex. « Canac », « APCHQ », « Vrai-Prix — hypothèse »)33  sourceId?: number;34  sourceUrl?: string | null;35  observedAt?: string | null; // date de l'observation ou de la publication36  effectiveDate?: string | null;37  note?: string | null; // ex. « prix de mars 2026 actualisé à septembre 2026 (StatCan 18-10-0289) »38  confidence: number; // 0-10039}4041/* ------------------------------------------------------------ taxonomie */4243export type Quality = "economy" | "standard" | "superior" | "prestige";44export type Condition = "poor" | "below_average" | "average" | "good" | "very_good" | "excellent" | "renovated" | "new";45export type BuildingType = "detached" | "semi_detached" | "row" | "plex" | "condo" | "chalet" | "mobile" | "other";46export type Basement = "none" | "crawl" | "unfinished" | "partial" | "finished" | "walkout";47export type Foundation = "poured_concrete" | "concrete_block" | "slab_on_grade" | "piers" | "stone";48export type Structure = "wood_frame" | "steel" | "concrete" | "log" | "masonry";49export type RoofType = "asphalt_shingle" | "metal" | "membrane" | "cedar" | "slate_tile";50export type RoofGeometry = "gable" | "hip" | "flat" | "mansard" | "complex";51export type Siding = "vinyl" | "brick" | "fiber_cement" | "wood" | "stone" | "stucco" | "aluminum" | "steel";52export type WindowType = "pvc" | "hybrid" | "aluminum" | "wood";53export type Heating = "electric_baseboard" | "heat_pump" | "furnace_electric" | "furnace_gas" | "furnace_oil" | "hydronic" | "geothermal" | "wood";54export type GarageType = "none" | "attached" | "detached" | "integrated" | "carport";5556/** Catégories UX du résidentiel québécois (au-dessus des divisions MasterFormat). */57export type UxCategory =58  | "site"59  | "excavation"60  | "foundation"61  | "structure"62  | "roofing"63  | "envelope"64  | "openings"65  | "insulation"66  | "finishes_ext"67  | "interior"68  | "kitchen"69  | "bathroom"70  | "plumbing"71  | "hvac"72  | "electrical"73  | "garage"74  | "basement"75  | "exterior";7677/* ---------------------------------------------------------------- unités */7879export type CanonicalUnit =80  | "unit" // chaque / unité81  | "pi2" // pied carré82  | "pi_lin" // pied linéaire83  | "pi3"84  | "m2"85  | "m3"86  | "kg"87  | "lb"88  | "L"89  | "gal"90  | "h"91  | "day"92  | "lump"; // forfait9394/* ---------------------------------------------------------------- articles */9596export interface CostItem {97  id: number;98  code: string; // canonical_code (ex. LUM-2X6-8)99  masterformat: string; // « 06 »100  division: string; // libellé MasterFormat101  category: UxCategory;102  subcategory: string | null;103  nameFr: string;104  nameEn: string;105  descriptionFr: string | null;106  descriptionEn: string | null;107  unit: CanonicalUnit;108  materialClass: string | null;109  trade: string | null; // métier associé par défaut110  defaultWastePct: number;111  qualityLevel: Quality | null;112  /** prix de référence interne (hypothèse) si aucune observation — jamais présenté comme observé */113  referencePrice: number | null;114  referenceNote: string | null;115  active: boolean;116}117118/** Prix canonique courant d'un article (résultat de pricing.ts). */119export interface ItemPrice {120  itemCode: string;121  unit: CanonicalUnit;122  price: number; // médiane robuste123  low: number;124  high: number;125  kind: PriceKind;126  sourceCount: number;127  observedAt: string | null; // date de la plus récente observation128  sources: { source: string; price: number; url: string | null; observedAt: string; sourceUnit: string; conversionFactor: number; regular: boolean }[];129  confidence: number; // 0-100 (fraîcheur, nb sources, qualité, dispersion, localisation)130  indexed?: { fromDate: string; toDate: string; factor: number; index: string } | null;131  note: string | null;132}133134/* --------------------------------------------------------- main-d'œuvre */135136export interface LabourRate {137  tradeCode: string;138  tradeNameFr: string;139  tradeNameEn: string;140  sector: string; // residentiel_leger | residentiel_lourd | ic141  classification: string; // compagnon | apprenti-1…142  region: string; // QC (conventions provinciales) | isole143  effectiveFrom: string;144  effectiveTo: string | null;145  baseWage: number;146  vacationCost: number;147  benefitsCost: number;148  employerContributions: number;149  otherContributions: number;150  totalEmployerCost: number;151  source: string;152  sourceUrl: string | null;153  confidence: number;154}155156/* ------------------------------------------------------------ localisation */157158export interface CostLocation {159  code: string; // ex. « QC-MTL », « QC-OUT »160  nameFr: string;161  nameEn: string;162  regionCode: string;163  materialFactor: number;164  labourFactor: number;165  equipmentFactor: number;166  overallFactor: number;167  effectiveDate: string;168  sourceMethod: string;169  confidence: number;170}171172/* ------------------------------------------------------------ assemblages */173174export interface AssemblyComponent {175  itemCode: string;176  /** quantité d'article (dans l'unité de l'article) par unité d'assemblage */177  quantity: number;178  wasteFactor: number; // 0.07 = 7 % de pertes179  labourHours: number; // heures de main-d'œuvre par unité d'assemblage, portées par ce composant180  trade: string | null; // métier pour ces heures (défaut : métier de l'article)181  equipmentCost: number; // $ d'équipement par unité d'assemblage (hypothèse documentée)182  sequence: number;183  notes: string | null;184}185186export interface Assembly {187  code: string;188  masterformat: string;189  category: UxCategory;190  nameFr: string;191  nameEn: string;192  descriptionFr: string;193  descriptionEn: string;194  unit: CanonicalUnit;195  buildingType: string | null; // null = tous196  quality: Quality | null;197  version: number;198  /** vie économique typique de la composante (années) — dépréciation par composante */199  economicLife: number | null;200  /** catégorie de condition (roof, windows, kitchen…) pour les règles condition → âge effectif */201  conditionGroup: string | null;202  components: AssemblyComponent[];203  active: boolean;204}205206/** Coût unitaire d'un assemblage (par unité d'assemblage), avant facteur régional. */207export interface AssemblyUnitCost {208  code: string;209  unit: CanonicalUnit;210  material: number;211  labour: number;212  equipment: number;213  direct: number;214  /** écart-type relatif estimé du coût direct (dispersion des prix + heures) */215  sigmaPct: number;216  /** part du coût matériau provenant de prix observés/officiels (0-1) */217  observedShare: number;218  components: {219    itemCode: string;220    nameFr: string;221    nameEn: string;222    unit: CanonicalUnit;223    quantity: number;224    wasteFactor: number;225    unitPrice: number;226    materialCost: number;227    labourHours: number;228    trade: string | null;229    hourlyRate: number | null;230    labourCost: number;231    equipmentCost: number;232    provenance: Provenance;233    rateProvenance: Provenance | null;234  }[];235  freshestObservation: string | null;236}237238/* ------------------------------------------------------------- estimation */239240export type EstimateMode = "property" | "construction" | "listing";241242export interface SidingMix {243  vinyl?: number;244  brick?: number;245  fiber_cement?: number;246  wood?: number;247  stone?: number;248  stucco?: number;249  aluminum?: number;250  steel?: number;251}252253/** Description du bâtiment — chaque champ peut être MAMH, saisi, IA ou dérivé (attributeSources). */254export interface BuildingSpec {255  type: BuildingType;256  quality: Quality;257  grossFloorAreaSqft: number; // aire d'étages hors sous-sol258  footprintSqft: number | null;259  stories: number;260  yearBuilt: number | null;261  basement: Basement;262  basementFinishedPct: number; // 0-1263  garage: { type: GarageType; spaces: number; areaSqft: number | null };264  structure: Structure;265  foundation: Foundation;266  siding: SidingMix; // parts (somme ≈ 1)267  roof: RoofType;268  roofGeometry: RoofGeometry;269  roofPitch: number; // rise/12270  windows: WindowType;271  windowCount: number | null;272  heating: Heating;273  hasAirConditioning: boolean;274  hasAirExchanger: boolean;275  kitchens: number;276  kitchenQuality: Quality;277  bathrooms: number;278  powderRooms: number;279  bathroomQuality: Quality;280  bedrooms: number | null;281  flooring: { hardwood?: number; engineered?: number; vinyl_plank?: number; ceramic?: number; laminate?: number; carpet?: number };282  deckSqft: number;283  drivewaySqft: number;284  driveway: "asphalt" | "pavers" | "gravel" | "concrete" | "none";285  fenceLinFt: number;286  landscapingSqft: number;287  pool: "none" | "above_ground" | "inground";288  units: number; // logements (plex)289}290291export interface IndirectParams {292  plansPct: number;293  permitsPct: number;294  inspectionPct: number;295  insurancePct: number;296  mobilizationPct: number;297  siteManagementPct: number;298  financingPct: number;299  adminPct: number;300}301302export interface CostParams {303  indirect: IndirectParams;304  overheadPct: number;305  profitPct: number;306  contingencyPct: number;307}308309export interface FunctionalObsolescence {310  id: string;311  type: string;312  curable: boolean;313  costToCure: number;314  valueLoss: number;315  notes: string;316}317318export interface DepreciationInput {319  method: "age_life" | "components";320  effectiveAge: number | null; // années (défaut : âge chronologique)321  economicLife: number; // années322  effectiveAgeSource: AttributeSource;323  /** condition par groupe de composantes (roof, windows, kitchen, bathroom, mechanical, exterior, interior, structure) */324  componentConditions: Partial<Record<string, Condition>>;325  functional: FunctionalObsolescence[];326  externalValueLoss: number;327  externalNote: string;328}329330export interface LandInput {331  value: number | null;332  source: "role" | "user" | "market" | "residual" | "none";333  rollYear: number | null;334  method: string;335}336337export interface CostInput {338  mode: EstimateMode;339  propertyId: string | null; // id_provinc MAMH340  listingUid: string | null;341  address: string | null;342  municipality: string | null;343  lat: number | null;344  lng: number | null;345  locationCode: string | null; // forcer une localisation ; sinon dérivée346  building: BuildingSpec;347  /** surcharges de quantités par assemblage (unité de l'assemblage) */348  quantityOverrides: Record<string, number>;349  /** assemblages exclus */350  excludedAssemblies: string[];351  params: CostParams;352  depreciation: DepreciationInput;353  land: LandInput;354  attributeSources: Partial<Record<string, AttributeSource>>;355  /** contexte du rôle (mode propriété) */356  roll: { landValue: number | null; buildingValue: number | null; totalValue: number | null; year: number } | null;357  /** instantané de prix (date) — null = aujourd'hui */358  priceDate: string | null;359}360361export interface QuantityLine {362  assemblyCode: string;363  quantity: number;364  unit: CanonicalUnit;365  source: AttributeSource; // derived (géométrie), user (surcharge), AI, listing366  formula: string; // texte de la formule appliquée367  inputs: Record<string, number | string>;368}369370export interface EstimateLine {371  assemblyCode: string;372  nameFr: string;373  nameEn: string;374  category: UxCategory;375  quantity: number;376  unit: CanonicalUnit;377  quantitySource: AttributeSource;378  quantityFormula: string;379  unitCost: number; // direct par unité, avant localisation380  material: number;381  labour: number;382  equipment: number;383  direct: number; // avant localisation384  locationAdjustment: number; // $ ajouté/retiré par la localisation385  adjusted: number; // direct localisé386  sigma: number; // $ (écart-type de la ligne)387  observedShare: number;388  confidence: number;389  economicLife: number | null;390  conditionGroup: string | null;391  unitDetail: AssemblyUnitCost;392}393394export interface CategoryTotal {395  category: UxCategory;396  labelFr: string;397  labelEn: string;398  material: number;399  labour: number;400  equipment: number;401  direct: number;402  adjusted: number;403  sharePct: number;404}405406export interface ConfidenceBreakdown {407  freshness: number; // /20408  coverage: number; // /20409  location: number; // /15410  labour: number; // /15411  benchmarks: number; // /10412  building: number; // /20413  total: number; // /100414  letter: "A" | "B" | "C" | "D";415  notesFr: string[];416  notesEn: string[];417}418419export interface DepreciationComponentRow {420  conditionGroup: string;421  labelFr: string;422  labelEn: string;423  rcn: number;424  economicLife: number;425  condition: Condition | null;426  effectiveAge: number;427  depreciationPct: number;428  depreciation: number;429  remaining: number;430  rule: string;431}432433export interface DepreciationResult {434  method: "age_life" | "components";435  chronologicalAge: number | null;436  effectiveAge: number | null;437  economicLife: number;438  physicalPct: number;439  physical: number;440  components: DepreciationComponentRow[];441  functionalCurable: number;442  functionalIncurable: number;443  functional: number;444  external: number;445  total: number;446  depreciatedImprovementValue: number;447}448449export interface BenchmarkCheck {450  source: string;451  buildingType: string;452  market: string;453  unit: string;454  low: number;455  high: number;456  midpoint: number | null;457  year: number;458  estimatePerUnit: number;459  status: "within" | "below" | "above" | "unavailable";460  deviationPct: number | null;461  notes: string | null;462}463464export interface CostEstimate {465  id: string;466  createdAt: string;467  methodVersion: string;468  costDatabaseVersion: string;469  assemblyVersion: string;470  priceDate: string;471  input: CostInput;472  location: CostLocation;473  quantities: QuantityLine[];474  lines: EstimateLine[];475  categories: CategoryTotal[];476  directCost: number;477  directMaterial: number;478  directLabour: number;479  directEquipment: number;480  indirect: { key: keyof IndirectParams; labelFr: string; labelEn: string; pct: number; amount: number }[];481  indirectCost: number;482  contractorOverhead: number;483  contractorProfit: number;484  contingency: number;485  replacementCostNew: number;486  range: { p10: number; p90: number; sigma: number; low: number; high: number };487  perSqft: number;488  perM2: number;489  depreciation: DepreciationResult;490  landValue: number;491  costApproachValue: number;492  confidence: ConfidenceBreakdown;493  benchmarks: BenchmarkCheck[];494  coverage: { materialObservedShare: number; assembliesPriced: number; assembliesTotal: number; pricingCoverage: number };495  /** comparaison avec le moteur existant (mode propriété/annonce) */496  otherReadings: { hedonic: number | null; comparables: number | null; hybrid: number | null; askingPrice: number | null; rollValue: number | null; rollBuilding: number | null; rollLand: number | null } | null;497  warnings: string[];498}499500/* ---------------------------------------------------------- tableau de bord */501502export interface IndexPoint {503  period: string; // YYYY-MM-DD (début du trimestre)504  value: number;505  pctYoy: number | null;506}507508export interface IndexSeries {509  code: string;510  source: string;511  geography: string;512  buildingType: string;513  division: string;514  points: IndexPoint[];515  retrievedAt: string | null;516}517518export interface CostOverview {519  generatedAt: string;520  indices: IndexSeries[];521  kpis: {522    residentialIndex: { value: number; period: string; pctYoy: number | null; geography: string } | null;523    materialsProxy: { value: number; period: string; pctYoy: number | null; label: string } | null;524    labour: { trade: string; totalEmployerCost: number; effectiveFrom: number | string; pctYoy: number | null; source: string } | null;525    observations: number;526    itemsObserved: number;527    itemsTotal: number;528    lastSync: string | null;529  };530  materials: { itemCode: string; nameFr: string; nameEn: string; unit: CanonicalUnit; price: number | null; kind: PriceKind | null; sourceCount: number; observedAt: string | null; history: { date: string; price: number }[] }[];531  popularAssemblies: { code: string; nameFr: string; nameEn: string; unit: CanonicalUnit; direct: number; material: number; labour: number; equipment: number; observedShare: number }[];532  locations: CostLocation[];533  sources: { name: string; type: string; license: string; lastSync: string | null; active: boolean; observations: number; url: string | null }[];534}535