SPB Git

Repository Public

db.ts 21.8 KB 510 lines typescript

1// UQO Éval — Métré : base `data/metre.db` (SQLite, better-sqlite3) — schéma versionné, migrations forward-only. SERVEUR seulement.2/**3 * Base DISTINCTE de cost.db (catalogue de coûts, lue seulement) et de4 * vraiprix.db / immoka.db (jamais touchées) : le module peut être désactivé5 * (METRE_ENABLED=0) ou sa base supprimée sans effet sur le reste d'UQO Éval.6 * Réversibilité : chaque migration est un ALTER/CREATE additif ; la version est7 * dans metre_meta.schema_version ; un retour arrière = restaurer la copie de8 * sauvegarde (docs/construction/SCHEMA_AND_MIGRATIONS.md).9 */10import Database from "better-sqlite3";11import fs from "fs";12import path from "path";1314export const METRE_DB_SCHEMA_VERSION = 4;1516let db: Database.Database | null = null;1718export function metreEnabled(): boolean {19  return process.env.METRE_ENABLED !== "0" && process.env.METRE_ENABLED !== "false";20}2122export function metreDbPath(): string {23  return process.env.METRE_DB ?? path.join(process.cwd(), "data", "metre.db");24}2526/** Répertoire des fichiers (plans, pages PNG, fichiers bruts des sources). */27export function metreFilesDir(): string {28  return process.env.METRE_FILES_DIR ?? path.join(process.cwd(), "data", "metre-files");29}3031export function getMetreDb(): Database.Database {32  if (db) return db;33  const p = metreDbPath();34  fs.mkdirSync(path.dirname(p), { recursive: true });35  db = new Database(p);36  db.pragma("journal_mode = WAL");37  db.pragma("foreign_keys = ON");38  db.pragma("busy_timeout = 5000");39  migrate(db);40  return db;41}4243/** Tests : base isolée (":memory:" ou chemin temporaire). */44export function openMetreDbAt(p: string): Database.Database {45  const d = new Database(p);46  d.pragma("foreign_keys = ON");47  migrate(d);48  return d;49}5051const SCHEMA = `52CREATE TABLE IF NOT EXISTS metre_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);5354/* ---------------------------------------------------------- utilisateurs */55CREATE TABLE IF NOT EXISTS metre_users (56  id TEXT PRIMARY KEY,57  display_name TEXT NOT NULL,58  role TEXT NOT NULL CHECK(role IN ('student','instructor','admin','professional')),59  access_code_id TEXT,60  course TEXT,61  created_at TEXT NOT NULL DEFAULT (datetime('now')),62  last_seen_at TEXT63);64CREATE TABLE IF NOT EXISTS metre_access_codes (65  id TEXT PRIMARY KEY,66  code_hash TEXT UNIQUE NOT NULL,67  code_hint TEXT NOT NULL,68  role TEXT NOT NULL,69  label TEXT,70  course TEXT,71  max_uses INTEGER,72  uses INTEGER NOT NULL DEFAULT 0,73  expires_at TEXT,74  active INTEGER NOT NULL DEFAULT 1,75  created_by TEXT,76  created_at TEXT NOT NULL DEFAULT (datetime('now'))77);78CREATE TABLE IF NOT EXISTS metre_sessions (79  id TEXT PRIMARY KEY,80  token_hash TEXT UNIQUE NOT NULL,81  user_id TEXT NOT NULL REFERENCES metre_users(id) ON DELETE CASCADE,82  created_at TEXT NOT NULL DEFAULT (datetime('now')),83  expires_at TEXT NOT NULL,84  ip TEXT,85  user_agent TEXT86);87CREATE INDEX IF NOT EXISTS idx_metre_sessions_user ON metre_sessions(user_id);8889/* -------------------------------------------------------------- projets */90CREATE TABLE IF NOT EXISTS metre_projects (91  id TEXT PRIMARY KEY,92  owner_id TEXT NOT NULL REFERENCES metre_users(id),93  name TEXT NOT NULL,94  address TEXT,95  municipality TEXT,96  region_code TEXT,97  property_id TEXT,98  valuation_date TEXT NOT NULL,99  building_kind TEXT NOT NULL DEFAULT 'detached',100  stories REAL NOT NULL DEFAULT 1,101  unit_system TEXT NOT NULL DEFAULT 'metric',102  notes TEXT,103  is_template INTEGER NOT NULL DEFAULT 0,104  locked INTEGER NOT NULL DEFAULT 0,105  spec_json TEXT,106  course TEXT,107  deleted_at TEXT,108  created_at TEXT NOT NULL DEFAULT (datetime('now')),109  updated_at TEXT NOT NULL DEFAULT (datetime('now'))110);111CREATE INDEX IF NOT EXISTS idx_metre_projects_owner ON metre_projects(owner_id);112CREATE TABLE IF NOT EXISTS metre_project_members (113  project_id TEXT NOT NULL REFERENCES metre_projects(id) ON DELETE CASCADE,114  user_id TEXT NOT NULL REFERENCES metre_users(id) ON DELETE CASCADE,115  role TEXT NOT NULL CHECK(role IN ('viewer','editor')),116  added_by TEXT,117  created_at TEXT NOT NULL DEFAULT (datetime('now')),118  PRIMARY KEY(project_id, user_id)119);120121/* ---------------------------------------------------------------- plans */122CREATE TABLE IF NOT EXISTS metre_plan_files (123  id TEXT PRIMARY KEY,124  project_id TEXT NOT NULL REFERENCES metre_projects(id) ON DELETE CASCADE,125  filename TEXT NOT NULL,126  mime TEXT NOT NULL,127  bytes INTEGER NOT NULL,128  sha256 TEXT NOT NULL,129  storage_path TEXT NOT NULL,130  page_count INTEGER NOT NULL DEFAULT 0,131  uploaded_by TEXT,132  created_at TEXT NOT NULL DEFAULT (datetime('now'))133);134CREATE INDEX IF NOT EXISTS idx_metre_plan_files_project ON metre_plan_files(project_id);135CREATE TABLE IF NOT EXISTS metre_plan_pages (136  id TEXT PRIMARY KEY,137  file_id TEXT NOT NULL REFERENCES metre_plan_files(id) ON DELETE CASCADE,138  project_id TEXT NOT NULL REFERENCES metre_projects(id) ON DELETE CASCADE,139  page_no INTEGER NOT NULL,140  width_px INTEGER NOT NULL,141  height_px INTEGER NOT NULL,142  image_path TEXT NOT NULL,143  label TEXT,144  current_calibration_id TEXT,145  source TEXT NOT NULL DEFAULT 'import',146  level TEXT,147  meta_json TEXT,148  created_at TEXT NOT NULL DEFAULT (datetime('now')),149  UNIQUE(file_id, page_no)150);151CREATE TABLE IF NOT EXISTS metre_calibrations (152  id TEXT PRIMARY KEY,153  page_id TEXT NOT NULL REFERENCES metre_plan_pages(id) ON DELETE CASCADE,154  revision INTEGER NOT NULL,155  p1_x REAL NOT NULL, p1_y REAL NOT NULL, p2_x REAL NOT NULL, p2_y REAL NOT NULL,156  real_length REAL NOT NULL,157  unit TEXT NOT NULL,158  px_per_meter REAL NOT NULL,159  uncertainty_pct REAL NOT NULL,160  created_by TEXT NOT NULL,161  created_at TEXT NOT NULL DEFAULT (datetime('now')),162  UNIQUE(page_id, revision)163);164CREATE TABLE IF NOT EXISTS metre_plan_objects (165  id TEXT PRIMARY KEY,166  page_id TEXT NOT NULL REFERENCES metre_plan_pages(id) ON DELETE CASCADE,167  project_id TEXT NOT NULL REFERENCES metre_projects(id) ON DELETE CASCADE,168  kind TEXT NOT NULL,169  layer TEXT NOT NULL DEFAULT 'default',170  name TEXT NOT NULL,171  points_json TEXT NOT NULL,172  closed INTEGER NOT NULL DEFAULT 1,173  props_json TEXT NOT NULL DEFAULT '{}',174  deleted_at TEXT,175  created_at TEXT NOT NULL DEFAULT (datetime('now')),176  updated_at TEXT NOT NULL DEFAULT (datetime('now'))177);178CREATE INDEX IF NOT EXISTS idx_metre_objects_page ON metre_plan_objects(page_id);179180/* -------------------------------------------------------------- registre */181CREATE TABLE IF NOT EXISTS metre_measurements (182  id TEXT PRIMARY KEY,183  project_id TEXT NOT NULL REFERENCES metre_projects(id) ON DELETE CASCADE,184  object_id TEXT REFERENCES metre_plan_objects(id) ON DELETE SET NULL,185  page_id TEXT,186  name TEXT NOT NULL,187  formula TEXT NOT NULL,188  formula_text TEXT NOT NULL,189  inputs_json TEXT NOT NULL DEFAULT '{}',190  value REAL NOT NULL,191  unit TEXT NOT NULL,192  calibration_revision INTEGER,193  stale INTEGER NOT NULL DEFAULT 0,194  confidence TEXT NOT NULL DEFAULT 'measured',195  verified_by TEXT,196  verified_at TEXT,197  lineage_json TEXT NOT NULL DEFAULT '[]',198  formula_version TEXT NOT NULL,199  computed_at TEXT NOT NULL,200  deleted_at TEXT,201  created_at TEXT NOT NULL DEFAULT (datetime('now'))202);203CREATE INDEX IF NOT EXISTS idx_metre_measurements_project ON metre_measurements(project_id);204CREATE INDEX IF NOT EXISTS idx_metre_measurements_object ON metre_measurements(object_id);205206/* ------------------------------------------------------------ assemblages */207CREATE TABLE IF NOT EXISTS metre_assemblies (208  id TEXT PRIMARY KEY,209  project_id TEXT REFERENCES metre_projects(id) ON DELETE CASCADE,210  code TEXT NOT NULL,211  name_fr TEXT NOT NULL,212  name_en TEXT NOT NULL,213  unit TEXT NOT NULL,214  output_quantity REAL NOT NULL DEFAULT 1,215  category TEXT NOT NULL DEFAULT 'custom',216  version INTEGER NOT NULL DEFAULT 1,217  components_json TEXT NOT NULL DEFAULT '[]',218  exclusions TEXT,219  created_by TEXT NOT NULL,220  deleted_at TEXT,221  created_at TEXT NOT NULL DEFAULT (datetime('now')),222  updated_at TEXT NOT NULL DEFAULT (datetime('now')),223  UNIQUE(project_id, code)224);225/* historique immuable des versions de recettes */226CREATE TABLE IF NOT EXISTS metre_assembly_versions (227  id TEXT PRIMARY KEY,228  assembly_id TEXT NOT NULL REFERENCES metre_assemblies(id) ON DELETE CASCADE,229  version INTEGER NOT NULL,230  snapshot_json TEXT NOT NULL,231  created_by TEXT NOT NULL,232  created_at TEXT NOT NULL DEFAULT (datetime('now')),233  UNIQUE(assembly_id, version)234);235236/* ------------------------------------------------------------- estimation */237CREATE TABLE IF NOT EXISTS metre_estimates (238  id TEXT PRIMARY KEY,239  project_id TEXT NOT NULL REFERENCES metre_projects(id) ON DELETE CASCADE,240  name TEXT NOT NULL,241  params_json TEXT NOT NULL,242  lines_json TEXT NOT NULL DEFAULT '[]',243  deleted_at TEXT,244  created_by TEXT NOT NULL,245  created_at TEXT NOT NULL DEFAULT (datetime('now')),246  updated_at TEXT NOT NULL DEFAULT (datetime('now'))247);248CREATE INDEX IF NOT EXISTS idx_metre_estimates_project ON metre_estimates(project_id);249/* révisions IMMUABLES (jamais UPDATE/DELETE applicatif) */250CREATE TABLE IF NOT EXISTS metre_estimate_versions (251  id TEXT PRIMARY KEY,252  estimate_id TEXT NOT NULL REFERENCES metre_estimates(id) ON DELETE CASCADE,253  project_id TEXT NOT NULL,254  number INTEGER NOT NULL,255  label TEXT NOT NULL,256  scenario TEXT,257  lines_json TEXT NOT NULL,258  params_json TEXT NOT NULL,259  result_json TEXT NOT NULL,260  pricing_snapshot_json TEXT NOT NULL,261  catalogue_version TEXT NOT NULL,262  formula_version TEXT NOT NULL,263  content_hash TEXT NOT NULL,264  total REAL NOT NULL,265  created_by TEXT NOT NULL,266  created_at TEXT NOT NULL DEFAULT (datetime('now')),267  UNIQUE(estimate_id, number)268);269270/* ------------------------------------------------------------- évaluation */271CREATE TABLE IF NOT EXISTS metre_valuation_cases (272  id TEXT PRIMARY KEY,273  project_id TEXT NOT NULL REFERENCES metre_projects(id) ON DELETE CASCADE,274  estimate_version_id TEXT REFERENCES metre_estimate_versions(id),275  name TEXT NOT NULL,276  input_json TEXT NOT NULL,277  result_json TEXT,278  created_by TEXT NOT NULL,279  deleted_at TEXT,280  created_at TEXT NOT NULL DEFAULT (datetime('now')),281  updated_at TEXT NOT NULL DEFAULT (datetime('now'))282);283284/* --------------------------------------------------------------- sources */285CREATE TABLE IF NOT EXISTS metre_sources (286  key TEXT PRIMARY KEY,287  name TEXT NOT NULL,288  publisher TEXT NOT NULL,289  url TEXT,290  license TEXT NOT NULL,291  license_url TEXT,292  evidence_class TEXT NOT NULL,293  access TEXT NOT NULL,               -- api | bulk | manual_import294  notes TEXT,295  active INTEGER NOT NULL DEFAULT 1,296  last_run_at TEXT,297  last_status TEXT,298  last_error TEXT299);300CREATE TABLE IF NOT EXISTS metre_source_documents (301  id TEXT PRIMARY KEY,302  source_key TEXT NOT NULL REFERENCES metre_sources(key),303  url TEXT,304  document_id TEXT,305  title TEXT,306  publisher TEXT,307  publication_date TEXT,308  effective_date TEXT,309  retrieved_at TEXT NOT NULL,310  license TEXT,311  content_hash TEXT NOT NULL,312  raw_path TEXT,313  bytes INTEGER,314  parser_version TEXT NOT NULL,315  qa_status TEXT NOT NULL DEFAULT 'new',   -- new | parsed | rejected | published316  qa_notes TEXT,317  records INTEGER NOT NULL DEFAULT 0,318  UNIQUE(source_key, content_hash)319);320CREATE INDEX IF NOT EXISTS idx_metre_docs_source ON metre_source_documents(source_key);321CREATE TABLE IF NOT EXISTS metre_price_observations (322  id TEXT PRIMARY KEY,323  source_key TEXT NOT NULL REFERENCES metre_sources(key),324  document_id TEXT NOT NULL REFERENCES metre_source_documents(id) ON DELETE CASCADE,325  evidence_class TEXT NOT NULL,326  external_id TEXT,327  resource_code TEXT,328  original_description TEXT NOT NULL,329  original_unit TEXT,330  original_amount REAL,331  normalized_unit TEXT,332  normalized_amount REAL,333  currency TEXT NOT NULL DEFAULT 'CAD',334  includes_material INTEGER, includes_labour INTEGER, includes_equipment INTEGER,335  tax_basis TEXT,                      -- before_tax | after_tax | unknown336  geography TEXT,337  region_code TEXT,338  buyer TEXT,339  classification_scheme TEXT,340  classification_code TEXT,341  classification_label TEXT,342  date_basis TEXT,343  effective_from TEXT,344  effective_to TEXT,345  license TEXT,346  extraction_method TEXT NOT NULL,347  lineage_json TEXT NOT NULL DEFAULT '{}',348  verification_status TEXT NOT NULL DEFAULT 'unverified',  -- unverified | verified | rejected349  uncertainty_note TEXT,350  source_url TEXT,351  created_at TEXT NOT NULL DEFAULT (datetime('now')),352  UNIQUE(source_key, external_id)353);354CREATE INDEX IF NOT EXISTS idx_metre_obs_source ON metre_price_observations(source_key, evidence_class);355CREATE INDEX IF NOT EXISTS idx_metre_obs_class ON metre_price_observations(classification_code);356CREATE TABLE IF NOT EXISTS metre_quality_flags (357  id INTEGER PRIMARY KEY AUTOINCREMENT,358  entity_type TEXT NOT NULL,359  entity_id TEXT NOT NULL,360  severity TEXT NOT NULL,361  code TEXT NOT NULL,362  message TEXT NOT NULL,363  status TEXT NOT NULL DEFAULT 'open',364  created_at TEXT NOT NULL DEFAULT (datetime('now')),365  resolved_at TEXT366);367CREATE TABLE IF NOT EXISTS metre_ingest_runs (368  id INTEGER PRIMARY KEY AUTOINCREMENT,369  source_key TEXT NOT NULL,370  started_at TEXT NOT NULL,371  finished_at TEXT,372  status TEXT NOT NULL DEFAULT 'running',373  documents INTEGER NOT NULL DEFAULT 0,374  records INTEGER NOT NULL DEFAULT 0,375  skipped INTEGER NOT NULL DEFAULT 0,376  errors INTEGER NOT NULL DEFAULT 0,377  log_json TEXT,378  triggered_by TEXT379);380381/* ----------------------------------------------------------- enseignement */382CREATE TABLE IF NOT EXISTS metre_assignments (383  id TEXT PRIMARY KEY,384  course TEXT NOT NULL,385  title TEXT NOT NULL,386  instructions TEXT,387  template_project_id TEXT REFERENCES metre_projects(id),388  due_at TEXT,389  answer_key_json TEXT,                -- JAMAIS renvoyé à un étudiant390  created_by TEXT NOT NULL,391  active INTEGER NOT NULL DEFAULT 1,392  created_at TEXT NOT NULL DEFAULT (datetime('now'))393);394CREATE TABLE IF NOT EXISTS metre_submissions (395  id TEXT PRIMARY KEY,396  assignment_id TEXT NOT NULL REFERENCES metre_assignments(id) ON DELETE CASCADE,397  user_id TEXT NOT NULL REFERENCES metre_users(id),398  project_id TEXT NOT NULL REFERENCES metre_projects(id),399  estimate_version_id TEXT REFERENCES metre_estimate_versions(id),400  submitted_at TEXT NOT NULL DEFAULT (datetime('now')),401  grade REAL,402  feedback TEXT,403  graded_by TEXT,404  graded_at TEXT,405  UNIQUE(assignment_id, user_id)406);407408/* ------------------------------------------------------------------ audit */409CREATE TABLE IF NOT EXISTS metre_audit_events (410  id INTEGER PRIMARY KEY AUTOINCREMENT,411  at TEXT NOT NULL DEFAULT (datetime('now')),412  user_id TEXT,413  role TEXT,414  action TEXT NOT NULL,415  entity_type TEXT,416  entity_id TEXT,417  project_id TEXT,418  details_json TEXT,419  ip TEXT420);421CREATE INDEX IF NOT EXISTS idx_metre_audit_project ON metre_audit_events(project_id, at);422CREATE TABLE IF NOT EXISTS metre_report_jobs (423  id TEXT PRIMARY KEY,424  project_id TEXT NOT NULL,425  estimate_version_id TEXT,426  kind TEXT NOT NULL,427  lang TEXT NOT NULL DEFAULT 'fr',428  requested_by TEXT NOT NULL,429  status TEXT NOT NULL DEFAULT 'done',430  bytes INTEGER,431  created_at TEXT NOT NULL DEFAULT (datetime('now'))432);433`;434435/* v2 : suggestions d'IA révisables (jamais appliquées automatiquement) */436const SCHEMA_V2 = `437CREATE TABLE IF NOT EXISTS metre_ai_suggestions (438  id TEXT PRIMARY KEY,439  project_id TEXT REFERENCES metre_projects(id) ON DELETE CASCADE,440  kind TEXT NOT NULL,                  -- assembly_mapping | missing_scope | explain | anomalies | classify441  target_type TEXT,442  target_id TEXT,443  model TEXT NOT NULL,444  prompt_version TEXT NOT NULL,445  input_hash TEXT NOT NULL,446  input_summary TEXT,447  output_json TEXT NOT NULL,448  evidence_json TEXT,449  status TEXT NOT NULL DEFAULT 'pending',   -- pending | accepted | rejected450  review_note TEXT,451  created_by TEXT NOT NULL,452  reviewed_by TEXT,453  reviewed_at TEXT,454  input_tokens INTEGER NOT NULL DEFAULT 0,455  output_tokens INTEGER NOT NULL DEFAULT 0,456  estimated_cost_usd REAL,457  latency_ms INTEGER,458  created_at TEXT NOT NULL DEFAULT (datetime('now'))459);460CREATE INDEX IF NOT EXISTS idx_metre_ai_project ON metre_ai_suggestions(project_id, created_at);461`;462463function migrate(d: Database.Database): void {464  d.exec(SCHEMA);465  const cur = Number((d.prepare("SELECT value FROM metre_meta WHERE key='schema_version'").get() as { value: string } | undefined)?.value ?? 0);466  // migrations forward-only, additives et tolérantes467  if (cur < 2) d.exec(SCHEMA_V2);468  if (cur < 3) {469    for (const sql of ["ALTER TABLE metre_projects ADD COLUMN locked INTEGER NOT NULL DEFAULT 0", "ALTER TABLE metre_plan_pages ADD COLUMN source TEXT NOT NULL DEFAULT 'import'", "ALTER TABLE metre_plan_pages ADD COLUMN level TEXT", "ALTER TABLE metre_plan_pages ADD COLUMN meta_json TEXT"]) { try { d.exec(sql); } catch { /* déjà présent */ } }470  }471  if (cur < 4) { try { d.exec("ALTER TABLE metre_projects ADD COLUMN spec_json TEXT"); } catch { /* déjà présent */ } }472  d.prepare("INSERT INTO metre_meta(key,value) VALUES('schema_version',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(String(METRE_DB_SCHEMA_VERSION));473  seedSources(d);474}475476/** Registre des sources (métadonnées de licence) — idempotent. */477function seedSources(d: Database.Database): void {478  const up = d.prepare(`INSERT INTO metre_sources(key,name,publisher,url,license,license_url,evidence_class,access,notes,active)479    VALUES(@key,@name,@publisher,@url,@license,@licenseUrl,@evidenceClass,@access,@notes,@active)480    ON CONFLICT(key) DO UPDATE SET name=excluded.name, publisher=excluded.publisher, url=excluded.url, license=excluded.license, license_url=excluded.license_url, evidence_class=excluded.evidence_class, access=excluded.access, notes=excluded.notes`);481  for (const s of SOURCE_REGISTRY) up.run({ ...s, active: s.active ? 1 : 0 });482}483484export interface SourceRegistryEntry {485  key: string; name: string; publisher: string; url: string | null; license: string; licenseUrl: string | null; evidenceClass: string; access: "api" | "bulk" | "manual_import"; notes: string; active: boolean;486}487488export const SOURCE_REGISTRY: SourceRegistryEntry[] = [489  { key: "seao", name: "SEAO — avis, octrois et contrats (OCDS)", publisher: "Secrétariat du Conseil du trésor (Québec)", url: "https://www.donneesquebec.ca/recherche/dataset/systeme-electronique-dappel-doffres-seao", license: "CC-BY 4.0", licenseUrl: "https://www.donneesquebec.ca/fr/licence/#cc-by", evidenceClass: "awarded_contract_total", access: "bulk", notes: "Fichiers JSON hebdomadaires au standard OCDS 1.1 (releases : tender, awards, contracts, items UNSPSC). Un montant de contrat n'est JAMAIS un prix unitaire : classe d'évidence « total de contrat octroyé ».", active: true },490  { key: "mtl_permits", name: "Ville de Montréal — permis de construction, transformation et démolition", publisher: "Ville de Montréal", url: "https://donnees.montreal.ca/dataset/permis-construction", license: "CC-BY 4.0", licenseUrl: "https://creativecommons.org/licenses/by/4.0/", evidenceClass: "permit_declared_value", access: "bulk", notes: "Valeur des travaux DÉCLARÉE au permis — distincte du coût réel ; sans valeur légale selon la Ville.", active: true },491  { key: "mtl_dtni", name: "Ville de Montréal — DTNI-11A répertoire des prix unitaires / DTNI-11B taux horaires", publisher: "Ville de Montréal (Service des infrastructures)", url: "https://ville.montreal.qc.ca/executiontravaux/", license: "Document public de la Ville (conditions du site) — import manuel", licenseUrl: null, evidenceClass: "published_schedule", access: "manual_import", notes: "Site protégé (HTTP 403 pour les accès automatisés le 2026-10-28) : téléchargement manuel du PDF/XLSX par un administrateur, puis import CSV. Prix d'INFRASTRUCTURE municipale (≠ prix résidentiels installés).", active: true },492  { key: "mtmd", name: "MTMD — prix unitaires et bordereaux (infrastructure routière)", publisher: "Ministère des Transports et de la Mobilité durable", url: "https://www.transports.gouv.qc.ca/", license: "À confirmer — import manuel", licenseUrl: null, evidenceClass: "published_schedule", access: "manual_import", notes: "Aucun jeu de prix unitaires trouvé sur Données Québec (2026-10-28). Import manuel seulement, étiqueté infrastructure.", active: false },493  { key: "statcan_bcpi", name: "Statistique Canada — 18-10-0289-01 indices des prix de la construction de bâtiments", publisher: "Statistique Canada", url: "https://www150.statcan.gc.ca/t1/tbl1/fr/tv.action?pid=1810028901", license: "Licence ouverte de Statistique Canada", licenseUrl: "https://www.statcan.gc.ca/fr/reference/licence", evidenceClass: "published_schedule", access: "api", notes: "Déjà ingéré par le connecteur `statcan` de la base de coûts (cost.db) ; le module l'utilise pour l'actualisation.", active: true },494  { key: "uqo_catalogue", name: "UQO Éval — catalogue de coûts (APCHQ, CCQ, Canac, BMR, Patrick Morin, StatCan, références)", publisher: "UQO Éval (cost.db)", url: "/cout", license: "voir cost_sources (licence par source)", licenseUrl: null, evidenceClass: "supplier_quote", access: "api", notes: "Articles, assemblages, grilles de main-d'œuvre et indices lus en direct dans cost.db (lecture seule).", active: true },495];496497export function metaGet(key: string, d: Database.Database = getMetreDb()): string | null {498  return (d.prepare("SELECT value FROM metre_meta WHERE key=?").get(key) as { value: string } | undefined)?.value ?? null;499}500export function metaSet(key: string, value: string, d: Database.Database = getMetreDb()): void {501  d.prepare("INSERT INTO metre_meta(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(key, value);502}503504export function audit(d: Database.Database, e: { userId: string | null; role: string | null; action: string; entityType?: string; entityId?: string; projectId?: string | null; details?: unknown; ip?: string | null }): void {505  d.prepare("INSERT INTO metre_audit_events(user_id,role,action,entity_type,entity_id,project_id,details_json,ip) VALUES(?,?,?,?,?,?,?,?)")506    .run(e.userId, e.role, e.action, e.entityType ?? null, e.entityId ?? null, e.projectId ?? null, e.details == null ? null : JSON.stringify(e.details).slice(0, 4000), e.ip ?? null);507}508509export const nowIso = (): string => new Date().toISOString();510
Files main
Menu

Theme