SPB Git forge

spb/immbot-ai

Public
1commits 1branches 0releases
1.5 MBsize
maindefault branch
20 days agolast push
TypeScript 98.3% CSS 0.9% Shell 0.7%
11.8 KB · 213 lines typescript
Raw Blame History
1// Amorçage de la plateforme : cours, compte administrateur initial (hash bcrypt + changement2// forcé), prompts versionnés, concepts/cartes/questions (seed-data/*.json), examens blancs.3// Idempotent : réexécutable sans dupliquer.45import { existsSync, readFileSync } from "node:fs";6import { resolve } from "node:path";7import { all, db, get, getSetting, run, setSetting, transaction } from "../lib/db/index.ts";8import { hashPassword } from "../lib/auth/password.ts";9import { seedPromptsFromFiles } from "../lib/prompts.ts";1011db();1213// ------------------------------ Cours ------------------------------14const COURSES = [15  {16    code: "IMM1003",17    full_code: "IMM1003-20",18    title: "Éléments d'évaluation immobilière",19    session_label: "Automne 2026",20    description:21      "Fondements de l'évaluation immobilière au Québec : cadre professionnel OEAQ, principes économiques, types de valeur, marché québécois, méthodes de comparaison, du coût et du revenu, réconciliation et rapport d'évaluation.",22    color: "#003E7E",23    source_path: process.env.COURSE_IMM1003_PATH || "../IMM1003-20",24  },25  {26    code: "IMM1033",27    full_code: "IMM1033-20",28    title: "Méthodes du coût en évaluation immobilière",29    session_label: "Automne 2026",30    description:31      "Maîtrise complète de la méthode du coût : évaluation du terrain, estimation du coût de reproduction ou de remplacement, coûts directs et indirects, profit de l'entrepreneur, mesure des trois formes de dépréciation et applications spécialisées.",32    color: "#B45309",33    source_path: process.env.COURSE_IMM1033_PATH || "../IMM1033-20",34  },35];3637for (const c of COURSES) {38  run(39    `INSERT INTO courses (code, full_code, title, session_label, description, color, source_path)40     VALUES (?, ?, ?, ?, ?, ?, ?)41     ON CONFLICT(code) DO UPDATE SET full_code=excluded.full_code, title=excluded.title,42       session_label=excluded.session_label, description=excluded.description,43       color=excluded.color, source_path=excluded.source_path`,44    c.code, c.full_code, c.title, c.session_label, c.description, c.color, c.source_path45  );46}47console.log(`✓ ${COURSES.length} cours`);4849// ------------------------------ Admin initial ------------------------------50if (process.env.DISABLE_INITIAL_ADMIN === "true") {51  console.log("• Compte admin initial désactivé (DISABLE_INITIAL_ADMIN=true)");52} else {53  const username = process.env.INITIAL_ADMIN_USERNAME || "admin";54  const password = process.env.INITIAL_ADMIN_PASSWORD || "admin123";55  const force = (process.env.FORCE_INITIAL_ADMIN_PASSWORD_CHANGE ?? "true") !== "false";56  const existing = get<{ id: number }>("SELECT id FROM users WHERE username = ?", username);57  if (!existing) {58    const r = run(59      `INSERT INTO users (username, display_name, password_hash, role, must_change_password, is_initial_admin)60       VALUES (?, 'Administrateur', ?, 'admin', ?, 1)`,61      username, hashPassword(password), force ? 1 : 062    );63    const adminId = Number(r.lastInsertRowid);64    for (const c of COURSES) run("INSERT OR IGNORE INTO enrollments (user_id, course_code) VALUES (?, ?)", adminId, c.code);65    console.log(`✓ Compte administrateur initial « ${username} » créé (changement de mot de passe ${force ? "FORCÉ à la première connexion" : "non forcé"})`);66  } else {67    console.log(`• Compte « ${username} » déjà présent — aucun changement`);68  }69}7071// ------------------------------ Prompts ------------------------------72seedPromptsFromFiles();73console.log(`✓ Prompts versionnés (${all("SELECT DISTINCT name FROM prompt_versions").length} noms)`);7475// ------------------------------ Contenu pédagogique ------------------------------76type ConceptSeed = { slug: string; name: string; description: string; week: number; importance: number; axis: string };77type LinkSeed = { from: string; to: string; type: string };78type CardSeed = { concept: string; type: string; front: string; back: string };79type QuestionSeed = { concept: string; type: string; difficulty: number; question: string; options: string[]; answer: string; explanation: string };8081function loadJSON<T>(file: string): T | null {82  const p = resolve(process.cwd(), "seed-data", file);83  if (!existsSync(p)) return null;84  return JSON.parse(readFileSync(p, "utf8")) as T;85}8687for (const course of COURSES) {88  const key = course.code.toLowerCase();89  const conceptsData = loadJSON<{ concepts: ConceptSeed[]; links: LinkSeed[] }>(`${key}-concepts.json`);90  if (!conceptsData) {91    console.log(`⚠ seed-data/${key}-concepts.json absent — contenu pédagogique de ${course.code} non amorcé`);92    continue;93  }94  transaction(() => {95    for (const c of conceptsData.concepts) {96      run(97        `INSERT INTO concepts (course_code, slug, name, description, week, importance, axis)98         VALUES (?, ?, ?, ?, ?, ?, ?)99         ON CONFLICT(course_code, slug) DO UPDATE SET name=excluded.name, description=excluded.description,100           week=excluded.week, importance=excluded.importance, axis=excluded.axis`,101        course.code, c.slug, c.name, c.description, c.week, c.importance, c.axis102      );103    }104    const idOf = new Map(105      all<{ id: number; slug: string }>("SELECT id, slug FROM concepts WHERE course_code = ?", course.code).map((r) => [r.slug, r.id])106    );107    for (const l of conceptsData.links ?? []) {108      const from = idOf.get(l.from), to = idOf.get(l.to);109      if (from && to) run("INSERT OR IGNORE INTO concept_links (from_id, to_id, type) VALUES (?, ?, ?)", from, to, l.type);110    }111112    const cards = loadJSON<{ flashcards: CardSeed[] }>(`${key}-flashcards.json`);113    if (cards) {114      const already = get<{ n: number }>("SELECT COUNT(*) as n FROM flashcards WHERE course_code = ? AND created_by = 'seed'", course.code);115      if ((already?.n ?? 0) === 0) {116        for (const f of cards.flashcards) {117          run(118            `INSERT INTO flashcards (course_code, concept_id, type, front, back, created_by, validated)119             VALUES (?, ?, ?, ?, ?, 'seed', 1)`,120            course.code, idOf.get(f.concept) ?? null, f.type, f.front, f.back121          );122        }123        console.log(`✓ ${course.code} : ${cards.flashcards.length} cartes mémoire`);124      }125    }126127    const quiz = loadJSON<{ questions: QuestionSeed[] }>(`${key}-quiz.json`);128    if (quiz) {129      const already = get<{ n: number }>("SELECT COUNT(*) as n FROM quiz_questions WHERE course_code = ? AND created_by = 'seed'", course.code);130      if ((already?.n ?? 0) === 0) {131        for (const q of quiz.questions) {132          run(133            `INSERT INTO quiz_questions (course_code, concept_id, type, difficulty, question, options, answer, explanation, created_by, validated)134             VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'seed', 1)`,135            course.code, idOf.get(q.concept) ?? null, q.type, q.difficulty, q.question, JSON.stringify(q.options ?? []), q.answer, q.explanation136          );137        }138        console.log(`✓ ${course.code} : ${quiz.questions.length} questions de quiz`);139      }140    }141  });142  console.log(`✓ ${course.code} : ${conceptsData.concepts.length} concepts, ${(conceptsData.links ?? []).length} liens`);143}144145// Lien inter-cours : la méthode du coût d'IMM1003 (survol) est approfondie par IMM1033.146const c1003cout = get<{ id: number }>(147  "SELECT id FROM concepts WHERE course_code = 'IMM1003' AND (slug LIKE '%cout%' OR slug LIKE '%coût%') ORDER BY importance DESC LIMIT 1"148);149const c1033roots = all<{ id: number }>(150  "SELECT id FROM concepts WHERE course_code = 'IMM1033' AND week <= 2 ORDER BY importance DESC LIMIT 2"151);152if (c1003cout && c1033roots.length) {153  for (const r of c1033roots) {154    run("INSERT OR IGNORE INTO concept_links (from_id, to_id, type) VALUES (?, ?, 'approfondissement')", r.id, c1003cout.id);155  }156}157158// ------------------------------ Examens blancs ------------------------------159type ExamSpec = { course: string; kind: string; title: string; description: string; duration: number; weeks: [number, number]; n: number };160const EXAMS: ExamSpec[] = [161  { course: "IMM1003", kind: "intra", title: "Intra blanc — Fondements et marché (S1-6)", description: "Simulation de l'examen de mi-session : fondements, cadre professionnel, principes, valeur, marché, collecte de données.", duration: 120, weeks: [1, 6], n: 18 },162  { course: "IMM1003", kind: "final", title: "Final blanc — Les trois méthodes (S1-13, accent 8-13)", description: "Simulation de l'examen final : méthodes de comparaison, du coût et du revenu, réconciliation, rapport.", duration: 170, weeks: [1, 13], n: 24 },163  { course: "IMM1003", kind: "thematic", title: "Examen thématique — Méthode du revenu", description: "RBP→RNE, capitalisation directe, TGA, DCF, multiplicateurs.", duration: 75, weeks: [11, 12], n: 12 },164  { course: "IMM1033", kind: "intra", title: "Intra blanc — Terrain et coûts (S1-8)", description: "Simulation de l'intra : fondements, évaluation du terrain, estimation des coûts directs et indirects.", duration: 120, weeks: [1, 8], n: 18 },165  { course: "IMM1033", kind: "final", title: "Final blanc — Dépréciation et intégration (S1-14)", description: "Simulation du final : les trois dépréciations, applications spécialisées, cas intégrateur.", duration: 170, weeks: [1, 14], n: 24 },166  { course: "IMM1033", kind: "thematic", title: "Examen thématique — Les trois dépréciations", description: "Physique, fonctionnelle, économique : classification, mesure, non-double-comptage.", duration: 75, weeks: [9, 12], n: 12 },167];168169for (const e of EXAMS) {170  const exists = get("SELECT id FROM mock_exams WHERE course_code = ? AND kind = ? AND title = ?", e.course, e.kind, e.title);171  if (exists) continue;172  // Sélection pondérée : questions des semaines visées, difficultés étalées, accent sur la fin de portée pour les finaux.173  const qs = all<{ id: number; difficulty: number; week: number | null }>(174    `SELECT q.id, q.difficulty, c.week FROM quiz_questions q175     LEFT JOIN concepts c ON c.id = q.concept_id176     WHERE q.course_code = ? AND (c.week IS NULL OR (c.week >= ? AND c.week <= ?))177     ORDER BY RANDOM()`,178    e.course, e.weeks[0], e.weeks[1]179  );180  if (qs.length < Math.min(8, e.n)) {181    console.log(`⚠ ${e.title} : banque insuffisante (${qs.length} questions) — examen non créé`);182    continue;183  }184  // Étaler les difficultés : trier par difficulté et prendre en alternance.185  const sorted = [...qs].sort((a, b) => a.difficulty - b.difficulty);186  const picked: number[] = [];187  const step = sorted.length / Math.min(e.n, sorted.length);188  for (let i = 0; i < Math.min(e.n, sorted.length); i++) picked.push(sorted[Math.floor(i * step)].id);189  run(190    `INSERT INTO mock_exams (course_code, kind, title, description, duration_minutes, question_ids, config, created_by, official)191     VALUES (?, ?, ?, ?, ?, ?, ?, 'seed', 1)`,192    e.course, e.kind, e.title, e.description, e.duration, JSON.stringify(picked), JSON.stringify({ weeks: e.weeks })193  );194  console.log(`✓ Examen blanc : ${e.title} (${picked.length} questions)`);195}196197// ------------------------------ Paramètres par défaut ------------------------------198if (getSetting("integrity_policy", null) === null) {199  setSetting("integrity_policy", { enabled: true, examLockdown: false, allowedModesDuringLockdown: ["tutor", "socratic"] });200}201if (getSetting("cross_course_enabled", null) === null) setSetting("cross_course_enabled", false);202if (getSetting("sharing_enabled", null) === null) setSetting("sharing_enabled", true);203console.log("✓ Paramètres par défaut");204205const counts = {206  users: get<{ n: number }>("SELECT COUNT(*) as n FROM users")?.n,207  concepts: get<{ n: number }>("SELECT COUNT(*) as n FROM concepts")?.n,208  flashcards: get<{ n: number }>("SELECT COUNT(*) as n FROM flashcards")?.n,209  questions: get<{ n: number }>("SELECT COUNT(*) as n FROM quiz_questions")?.n,210  exams: get<{ n: number }>("SELECT COUNT(*) as n FROM mock_exams")?.n,211};212console.log("— Seed terminé :", JSON.stringify(counts));213