/* SPDX-License-Identifier: AGPL-3.0-only
   SPDX-FileCopyrightText: 2026 Com'Together
   Com'Together — Ma communication. Copyright (C) 2026 Com'Together.
   Programme libre sous AGPL-3.0-only — voir LICENSE. */
/* ============================================================================
 * sentence.js — MOTEUR DE PHRASE (assemblage du message parlé/affiché)
 * ----------------------------------------------------------------------------
 * Extrait de app.js pour être PUR et TESTABLE hors du DOM. Aucune lecture de
 * `state` ni du DOM : tout passe par les paramètres. app.js délègue ici.
 *
 *   Sentence.build(tokens, ctx) -> phrase française finale (élision, contractions,
 *                                  conjugaison, accords, négation, impératif, ponctuation)
 *   ctx = { tense, registre, genre }   (genre = "feminin" | autre)
 *
 * Dépend de window.Grammar (grammar.js, chargé avant).
 * ==========================================================================*/
(function () {
  "use strict";
  var root = (typeof window !== "undefined") ? window : globalThis;
  function G() { return root.Grammar; }

  // ---- Données (pures) ----
  const DET_PLURAL = { "le": "les", "la": "les", "l'": "les", "les": "les", "un": "des", "une": "des",
    "des": "des", "mon": "mes", "ma": "mes", "mes": "mes", "ton": "tes", "ta": "tes", "ce": "ces",
    "cet": "ces", "cette": "ces", "ces": "ces", "du": "des" };
  const DET_FEM = { "le": "la", "un": "une", "mon": "ma", "ce": "cette", "ton": "ta", "du": "de la" };
  const DET_GENDER = { "le": "m", "un": "m", "mon": "m", "ce": "m", "cet": "m", "ton": "m", "du": "m",
    "la": "f", "une": "f", "ma": "f", "cette": "f", "ta": "f" };
  const SELF_SUBJ = new Set(["je", "j'", "j", "nous", "on", "me", "m'", "moi"]);
  const FEM_SUBJ = new Set(["elle", "elles"]);
  const OTHER_SUBJ = new Set(["il", "ils", "tu", "vous", "iel"]);
  // Sujets 1re/2e personne : genre NON inhérent → l'accord du pp-être suit le genre CHOISI (ctx.genre).
  const SUBJ_CTX_GENDER = new Set(["je", "j'", "j", "nous", "on", "tu", "vous", "me", "m'", "moi", "toi", "te", "t'"]);
  // Formes de l'auxiliaire ÊTRE (repérage du pp à accorder dans « je suis allé(e) »).
  const ETRE_AUX_FORMS = new Set(["suis", "es", "est", "sommes", "êtes", "sont", "étais", "était", "étions", "étiez", "étaient", "serai", "sera", "serons", "serez", "seront", "serais", "serait", "serions", "seriez", "seraient"]);
  const ADJ_FEM = { "beau": "belle", "nouveau": "nouvelle", "vieux": "vieille", "fou": "folle",
    "mou": "molle", "blanc": "blanche", "franc": "franche", "frais": "fraîche", "sec": "sèche",
    "long": "longue", "gentil": "gentille", "gros": "grosse", "bas": "basse", "gras": "grasse",
    "épais": "épaisse", "bon": "bonne", "mignon": "mignonne", "roux": "rousse", "doux": "douce",
    "faux": "fausse", "jaloux": "jalouse", "fier": "fière", "favori": "favorite",
    "inquiet": "inquiète", "complet": "complète", "secret": "secrète", "coquet": "coquette",
    "muet": "muette", "net": "nette", "malin": "maligne", "public": "publique", "turc": "turque",
    "grec": "grecque", "menteur": "menteuse", "vengeur": "vengeresse", "protecteur": "protectrice",
    "neuf": "neuve", "vif": "vive", "actif": "active", "sportif": "sportive" };
  const ETAT_AVOIR = new Set(["froid", "chaud", "faim", "soif", "envie", "mal", "peur", "sommeil", "besoin", "raison", "tort", "honte"]);
  const ETAT_ETRE = new Set(["content", "triste", "fatigué", "malade", "fâché", "inquiet", "fier", "surpris", "jaloux", "amoureux", "calme", "stressé", "heureux", "désolé", "seul", "prêt"]);
  const IMPER_CLI = { me: "moi", te: "toi", se: "toi", moi: "moi", toi: "toi",
    le: "le", la: "la", les: "les", lui: "lui", leur: "leur", nous: "nous", vous: "vous", y: "y", en: "en" };
  const _SUBJ = { "j": "je", "je": "je", "tu": "tu", "il": "il", "elle": "elle", "iel": "iel", "on": "on", "nous": "nous", "vous": "vous", "ils": "ils" };
  // Mots de négation à « traverser » pour trouver le verbe gouvernant (modal + infinitif).
  const NEG_WORDS = new Set(["ne", "n'", "pas", "plus", "jamais", "point", "guère", "rien"]);
  // Prépositions/locutions qui GOUVERNENT un infinitif (Le Bon Usage) : le verbe qui suit reste à
  // l'infinitif quel que soit le temps (« pour manger », « sans parler », « content de partir »,
  // « quelque chose à faire »). « en » est EXCLU : il régit le participe présent (gérondif « en mangeant »).
  const PREP_INF = new Set(["pour", "sans", "de", "d'", "à", "afin de", "avant de", "au lieu de",
    "par", "à force de", "de peur de", "au point de", "à moins de", "plutôt que de"]);
  // Prépositions qui régissent un pronom TONIQUE / disjonctif (Le Bon Usage) : un pronom sujet placé
  // après devient sa forme tonique (« avec je » → « avec moi », « pour tu » → « pour toi »).
  const PREP_PRON = new Set(["avec", "sans", "pour", "de", "d'", "à", "chez", "sur", "sous", "contre",
    "vers", "par", "entre", "derrière", "devant", "envers", "malgré", "selon", "parmi", "près de", "grâce à"]);
  // Sujet → tonique. elle/elles/nous/vous gardent leur forme ; on/iel laissés tels quels (neutre respecté).
  const DISJ = { "je": "moi", "tu": "toi", "il": "lui", "ils": "eux" };
  // Un pronom sujet ORPHELIN (aucun verbe ni état qu'il gouverne après lui) prend la forme tonique :
  // « je » seul → « moi » (— Qui ? — Moi). Dès qu'un verbe/état suit, il redevient sujet (« je mange »).
  function pronStranded(tokens, i) {
    for (let k = i + 1; k < tokens.length; k++) {
      const t = tokens[k];
      if (t.verb) return false;                 // un verbe suit → sujet (« je mange »)
      if (stateAux(tokens, k)) return false;    // un état gouverné suit → sujet (« j'ai mal », « je suis content »)
      if (t.subject) return true;               // un autre sujet d'abord → orphelin
    }
    return true;                                // rien de gouverné après → orphelin
  }

  // ---- Helpers (purs) ----
  function isDeterminer(label) { return Object.prototype.hasOwnProperty.call(DET_PLURAL, (label || "").toLowerCase().trim()); }
  function genderBefore(tokens, index) {
    let g = "m";
    for (let k = 0; k < index; k++) {
      const t = tokens[k], lab = (t.label || "").toLowerCase().trim();
      if (DET_GENDER[lab] !== undefined) g = DET_GENDER[lab];
      if (t.fem) g = "f";
    }
    return g;
  }
  function refFeminine(tokens, index, ctx) {
    const fem = ctx && ctx.genre === "feminin";
    for (let k = index - 1; k >= 0; k--) {
      const t = tokens[k]; if (t.verb || t.phrase) continue;
      const lab = (t.label || "").toLowerCase().trim();
      if (FEM_SUBJ.has(lab)) return true;
      if (OTHER_SUBJ.has(lab)) return false;
      if (SELF_SUBJ.has(lab)) return fem;
    }
    return fem;   // pas de pronom : on parle de soi
  }
  function pluralize(w) {
    if (/[sxz]$/i.test(w)) return w;
    if (/(au|eu)$/i.test(w)) return w + "x";
    if (/al$/i.test(w)) return w.replace(/al$/i, "aux");
    return w + "s";
  }
  function feminine(w) {
    const k = w.toLowerCase();
    if (ADJ_FEM[k]) return ADJ_FEM[k];
    if (/e$/.test(k)) return w;
    if (/er$/.test(k)) return w.slice(0, -2) + "ère";
    if (/(eux|eur)$/.test(k)) return w.replace(/(eux|eur)$/, "euse");
    if (/f$/.test(k)) return w.slice(0, -1) + "ve";
    if (/(el|eil|ul|il)$/.test(k)) return w + "le";
    if (/(en|on)$/.test(k)) return w + "ne";
    if (/(et|at|ot)$/.test(k)) return w + "te";
    return w + "e";
  }
  function stateAux(tokens, i) {
    const lab = (tokens[i].label || "").toLowerCase().trim();
    const aux = ETAT_AVOIR.has(lab) ? "avoir" : ETAT_ETRE.has(lab) ? "être" : null;
    if (!aux) return null;
    for (let k = i - 1; k >= 0; k--) {
      const p = tokens[k];
      if (p.phrase) return null;
      if (p.verb) return null;
      if (p.color === "nom" || isDeterminer(p.label)) return null;
      if (p.subject) return { aux, subj: p.subject };
    }
    return null;
  }
  function assembleImperative(toks, words) {
    // « moi/toi/nous/vous » portent un flag sujet mais sont des CLITIQUES en impératif
    // (« montre-moi ») : on ne les exclut donc pas ici, sinon ils étaient supprimés.
    // MAIS « le/la/les » peuvent être DÉTERMINANTS d'un nom qui suit (« baisse le son »,
    // « ferme la fenêtre ») : dans ce cas ce ne sont PAS des pronoms enclitiques.
    const isCli = (t, i) => {
      if (!t || t.verb) return false;
      const lab = (t.label || "").toLowerCase().trim();
      if (IMPER_CLI[lab] === undefined) return false;
      if (/^(le|la|les|l')$/.test(lab)) {
        const nx = toks[i + 1];
        if (nx && (nx.color === "nom" || nx.color === "descriptif")) return false;   // article, pas pronom
      }
      return true;
    };
    const attach = {}, consumed = new Set();
    toks.forEach((t, i) => {
      if (!isCli(t, i)) return;
      let L = i - 1; while (L >= 0 && isCli(toks[L], L)) L--;
      let R = i + 1; while (R < toks.length && isCli(toks[R], R)) R++;
      let vi = -1;
      // Impératif positif : le clitique est ENCLITIQUE au verbe qui PRÉCÈDE (« laisse-moi finir »,
      // pas « laisse finir-moi »). On privilégie donc le verbe précédent, puis le suivant en secours.
      if (toks[i - 1] && toks[i - 1].verb) vi = i - 1;
      else if (toks[i + 1] && toks[i + 1].verb) vi = i + 1;
      else if (L >= 0 && toks[L] && toks[L].verb) vi = L;
      else if (toks[R] && toks[R].verb) vi = R;
      if (vi >= 0) { (attach[vi] = attach[vi] || []).push(IMPER_CLI[(t.label || "").toLowerCase().trim()]); consumed.add(i); }
    });
    const out = [];
    toks.forEach((t, i) => {
      if (consumed.has(i) || t.subject) return;
      let w = words[i];
      if (attach[i]) {
        const cl = attach[i];
        if (/^(y|en)$/.test(cl[0]) && /[ae]$/i.test(w)) w += "s";
        w += "-" + cl.join("-");
      }
      out.push(w);
    });
    return out.join(" ");
  }
  function contract(s) {
    return s
      .replace(/(^|\s)à\s+les(?=\s|$)/gi, "$1aux").replace(/(^|\s)à\s+le(?=\s|$)/gi, "$1au")
      .replace(/(^|\s)de\s+les(?=\s|$)/gi, "$1des").replace(/(^|\s)de\s+le(?=\s|$)/gi, "$1du");
  }
  function elide(s) {
    // (?<!-) : un pronom ENCLITIQUE (« mets-le », « prends-le ») ne s'élide PAS avec le mot suivant
    // (« mets-le ici », jamais « mets-l'ici »). L'élision ne vaut que pour un proclitique isolé.
    return s.replace(/(?<!-)\b(je|le|la|me|te|se|ne|de|que)\s+(?=[aeiouyéèêàâîïôûh])/gi,
      (m, w) => w.toLowerCase().slice(0, -1) + "'");
  }
  function conjugatePhrase(label, ctx) {
    const t = (ctx && ctx.tense) || "present";
    const oral = ctx && ctx.registre === "oral";
    // Formes non finies : elles s'appliquent aux VERBES (tuiles verbe), pas aux amorces
    // conversationnelles figées (« je veux », « j'ai besoin »). On laisse la phrase intacte.
    if (t === "infinitif" || t === "participepresent" || t === "participepasse" || t === "gerondif") return label;
    if (t === "present") {
      if (!oral) return label;
      const mm = label.match(/^ce\s+n'est\s+pas\s+(.*)$/i);
      if (mm) return "c'est pas " + mm[1];
      return label.replace(/\bne\s+/i, "").replace(/\bn'/i, "");
    }
    const CEST = { imparfait: "c'était ", passe: "ça a été ", futur: "ça va être ", futursimple: "ce sera ", conditionnel: "ce serait " };
    const CNEST = { imparfait: "ce n'était pas ", passe: "ça n'a pas été ", futur: "ça ne va pas être ", futursimple: "ce ne sera pas ", conditionnel: "ce ne serait pas " };
    let m = label.match(/^c'est\s+(.*)$/i);
    if (m) return (CEST[t] || "c'est ") + m[1];
    m = label.match(/^ce\s+n'est\s+pas\s+(.*)$/i);
    if (m) return oral ? (CEST[t] || "c'est ") + "pas " + m[1] : (CNEST[t] || "ce n'est pas ") + m[1];
    m = label.match(/^(j'|je|tu|il|elle|iel|on|nous|vous|ils)\s*(ne\s+|n')?(\S+?)(?:\s+(pas|plus|jamais))?(\s.*)?$/i);
    if (!m) return label;
    const subj = _SUBJ[m[1].replace(/'/g, "").toLowerCase()];
    if (!subj) return label;
    const inf = G().presentToInf(m[3]);
    if (!inf) return label;
    const neg = m[2] ? (m[4] || "pas") : null;
    const rest = m[5] || "";
    const conj = G().conjugate(inf, subj, t);
    let verb;
    if (neg) {
      const parts = conj.split(" ");
      verb = `${oral ? "" : "ne "}${parts[0]} ${neg} ${parts.slice(1).join(" ")}`.trim();
    } else { verb = conj; }
    return contract(elide(`${subj} ${verb}${rest}`.replace(/\s+/g, " ").trim()));
  }
  const NONFINITE = new Set(["infinitif", "participepresent", "participepasse", "gerondif"]);
  function tokenDisplay(tok, i, tokens, ctx) {
    const tense = (ctx && ctx.tense) || "present";
    if (tok.phrase) return conjugatePhrase(tok.label, ctx);
    // Forme non finie : pas de sujet (infinitif « aller », pas « je aller »/« j'aller »). Un pronom
    // SUJET seul n'est pas rendu dans ces modes ; il réapparaît dès qu'on revient à un temps conjugué.
    if (NONFINITE.has(tense) && tok.subject && !tok.verb) return "";
    if (tok.verb) {
      // Un verbe GOUVERNÉ par un verbe conjugué reste à l'infinitif — même à travers la NÉGATION
      // (je ne veux pas ÊTRE touchée, je ne veux pas PARTIR) ET à travers les CLITIQUES objets
      // intercalés avant l'infinitif (je ne veux pas LE LUI donner, tu peux ME LE donner). On remonte
      // au 1er token « plein » en sautant négations + clitiques (jamais un sujet : nous/vous restent
      // sujets → on s'arrête et on conjugue).
      let g = i - 1;
      while (g >= 0) {
        const gl = (tokens[g].label || "").toLowerCase().trim();
        // clitique à sauter : objet avant le verbe (le/lui/y…) ou, à l'IMPÉRATIF, clitique
        // post-verbal (« laisse-moi FINIR » → l'infinitif est gouverné par « laisse » à travers « moi »).
        const cliMember = CLI_BEFORE.has(gl) || (tense === "imperatif" && IMPER_CLI[gl] !== undefined);
        if (NEG_WORDS.has(gl) || (cliMember && !tokens[g].verb && (tense === "imperatif" || !tokens[g].subject))) g--;
        else break;
      }
      if (g >= 0 && tokens[g].verb) return tok.verb;
      // Après une PRÉPOSITION à infinitif : le verbe reste à l'infinitif (pour manger, sans parler…).
      // On reconnaît AUSSI les amorces qui SE TERMINENT par une telle préposition (« j'ai besoin DE dormir »,
      // « j'ai envie DE partir », « quelque chose À faire ») : c'est le mot final qui gouverne l'infinitif.
      if (g >= 0) {
        const gl0 = (tokens[g].label || "").toLowerCase().trim();
        if (PREP_INF.has(gl0) || /(^|\s)(de|d'|à|pour|sans|afin de|avant de|au lieu de)$/.test(gl0)) return tok.verb;
      }
      let subj = G().subjectBefore(tokens, i);
      // À l'impératif sans sujet explicite : 2e pers. du singulier par défaut (laisse-moi, va, parle).
      if (tense === "imperatif" && !tokens.some(t => t.subject)) subj = "tu";
      const before = tokens.slice(0, i);
      if (tense !== "imperatif" && !before.some(t => t.subject) && before.some(t => (t.label || "").toLowerCase().trim() === "y")) subj = "il";
      // Participe passé (forme isolée) : accord en genre/nombre via les boutons féminin/pluriel
      // (allé → allée / allés / allées). Le pp du passé composé (être) s'accorde déjà avec le sujet.
      if (tense === "participepasse") {
        let pp = G().conjugate(tok.verb, subj, tense);
        if (tok.fem) pp = feminine(pp);
        if (tok.plural) pp = pluralize(pp);
        return pp;
      }
      let out = G().conjugate(tok.verb, subj, tense);
      // ACCORD DU PARTICIPE (auxiliaire ÊTRE) avec le GENRE de la personne — 1re/2e personne (je, tu,
      // nous, vous, on) : grammar.js accorde déjà le pronom à genre INHÉRENT (elle→allée) et le nombre
      // (nous→allés), mais il ne reçoit pas le genre DÉCLARÉ. Ici on féminise le pp après un aux « être »
      // si la personne a choisi le féminin (R35 : le genre est choisi, jamais déduit). Le pluriel éventuel
      // (nous/vous → allées) est conservé.
      if (tense === "passe" && ctx && ctx.genre === "feminin" && SUBJ_CTX_GENDER.has((subj || "").toLowerCase())) {
        const parts = out.split(" ");
        const ai = parts.findIndex(w => ETRE_AUX_FORMS.has(w.toLowerCase()));
        if (ai >= 0 && parts[ai + 1]) {
          const pp = parts[ai + 1], hasS = /s$/i.test(pp);
          const base = hasS ? pp.replace(/s$/i, "") : pp;
          const fem = feminine(base);
          if (fem !== base) parts[ai + 1] = hasS ? fem + "s" : fem;   // allé→allée ; allés→allées
          out = parts.join(" ");
        }
      }
      return out;
    }
    // Pronom TONIQUE : (a) après une préposition (« avec » + « je » → « avec moi ») ou (b) ORPHELIN,
    // sans verbe/état gouverné après lui (« je » seul → « moi »). Sinon il reste sujet (« je mange »).
    if (tok.subject) {
      let g = i - 1;
      while (g >= 0 && NEG_WORDS.has((tokens[g].label || "").toLowerCase().trim())) g--;
      const afterPrep = g >= 0 && PREP_PRON.has((tokens[g].label || "").toLowerCase().trim());
      if (afterPrep || pronStranded(tokens, i)) return DISJ[tok.subject] || tok.label;
    }
    const st = stateAux(tokens, i);
    if (st) {
      let adj = tok.label;
      if (st.aux === "être") {
        if (tok.fem || refFeminine(tokens, i, ctx)) adj = feminine(adj);
        // Accord en nombre de l'adjectif attribut avec le sujet (« ils sont contents »). On n'accorde
        // QUE pour des sujets sans ambiguïté de nombre : PAS « vous » (vouvoiement singulier possible),
        // et jamais les états en AVOIR (« ils ont faim » — « faim » est un nom, invariable ici).
        if (tok.plural || /^(nous|ils|elles)$/.test(st.subj)) adj = pluralize(adj);
      } else if (tok.plural) {
        adj = pluralize(adj);
      }
      return G().conjugate(st.aux, st.subj, tense) + " " + adj;
    }
    let w = tok.label;
    if (isDeterminer(w)) {
      if (tok.fem) w = DET_FEM[w.toLowerCase().trim()] || w;
      if (tok.plural) return DET_PLURAL[w.toLowerCase().trim()] || w;
      return w;
    }
    const fem = tok.fem || genderBefore(tokens, i) === "f" || refFeminine(tokens, i, ctx);
    if (tok.color === "descriptif") {
      if (fem) w = feminine(w);
      if (tok.plural) w = pluralize(w);
      return w;
    }
    if (tok.plural) return pluralize(w);
    return w;
  }
  // NORMALISATION : réécrit la suite de tokens d'une entrée simplifiée vers l'ordre du français
  // correct, AVANT l'assemblage. Deux transformations (le reste du moteur conjugue/élide ensuite) :
  //   (1) objet tonique juste après un verbe → clitique déplacé AVANT le verbe
  //       « je aime toi » → « je te aime » (→ « je t'aime »). Sûr : moi→me, toi→te, nous, vous.
  //   (2) négation autonome (pas/plus/jamais/ne…) → enveloppe « ne … pas » autour du verbe (+ clitiques)
  //       « je pas aime toi » → « je ne te aime pas » (→ « je ne t'aime pas ») ; oral = sans « ne ».
  const OBJ_CLIT = { "moi": "me", "toi": "te", "nous": "nous", "vous": "vous" };
  const NEG_MAP = { "pas": "pas", "ne pas": "pas", "plus": "plus", "ne plus": "plus",
    "jamais": "jamais", "ne jamais": "jamais", "ne": "" };
  const CLI_BEFORE = new Set(["me", "te", "se", "nous", "vous", "le", "la", "les", "lui", "leur", "y", "en"]);
  const REFLEX = { "je": "me", "j'": "me", "j": "me", "tu": "te", "il": "se", "elle": "se", "on": "se",
    "iel": "se", "nous": "nous", "vous": "vous", "ils": "se", "elles": "se" };
  const PLURAL_DET = new Set(["les", "des", "ces", "mes", "tes", "ses", "nos", "vos", "leurs", "plusieurs", "quelques"]);
  const SING_DET = new Set(["le", "la", "l'", "un", "une", "du", "de la", "de l'", "mon", "ma", "ton", "ta", "son", "sa", "ce", "cet", "cette", "notre", "votre", "leur", "chaque"]);
  function normalizeTokens(tokens, ctx) {
    let out = (tokens || []).map(t => Object.assign({}, t));
    const oral = ctx && ctx.registre === "oral";
    const imper = ctx && ctx.tense === "imperatif";
    // (0a) RÉFLÉCHI : le clitique « se » s'accorde au sujet (« je se laver » → « je me lave »).
    if (out.some(t => t.verb)) {
      const subjTok = out.find(t => !t.verb && Object.prototype.hasOwnProperty.call(REFLEX, (t.label || "").toLowerCase().trim()));
      const rf = subjTok ? REFLEX[(subjTok.label || "").toLowerCase().trim()] : null;
      if (rf) out.forEach(t => { if (!t.subject && (t.label || "").toLowerCase().trim() === "se") t.label = rf; });
    }
    // (0b) ACCORD DE NOMBRE dans le GROUPE NOMINAL : dès qu'un déterminant pluriel ouvre le GN,
    //      les noms ET les adjectifs qui suivent passent au pluriel jusqu'au prochain déterminant
    //      singulier, verbe ou pronom (« les grands chiens », « des jolies fleurs »). On ne se
    //      contente pas du voisin immédiat : l'adjectif intercalé ne doit pas « couper » l'accord.
    let plNP = false;
    for (let i = 0; i < out.length; i++) {
      const t = out[i]; const lab = (t.label || "").toLowerCase().trim();
      if (t.verb || t.subject) { plNP = false; continue; }
      if (PLURAL_DET.has(lab)) { plNP = true; continue; }
      if (SING_DET.has(lab) || isDeterminer(lab)) { plNP = false; continue; }
      if (plNP && !t.plural && !t.proper && (t.color === "nom" || t.color === "descriptif")) t.plural = true;
    }
    // (1) objet tonique après un verbe → clitique avant le verbe (sauf impératif positif : « aime-moi »)
    if (!imper) {
      for (let i = out.length - 1; i >= 1; i--) {
        const lab = (out[i].label || "").toLowerCase().trim();
        if (OBJ_CLIT[lab] && !out[i].subject && out[i - 1] && out[i - 1].verb) {
          out.splice(i, 1);
          out.splice(i - 1, 0, { label: OBJ_CLIT[lab] });
        }
      }
    }
    // (2) négation : seulement s'il y a un verbe (les états/phrases gardent le comportement existant)
    if (out.some(t => t.verb)) {
      // Repérer la POSITION et le type de la négation AVANT de la retirer.
      const negIdxs = []; let negKind = "";
      out.forEach((t, i) => { const lab = (t.label || "").toLowerCase().trim(); if (lab in NEG_MAP) { negIdxs.push(i); if (NEG_MAP[lab]) negKind = NEG_MAP[lab]; } });
      if (negIdxs.length) {
        if (!negKind) negKind = "pas";
        // Verbe GOUVERNÉ = celui le plus PROCHE du marqueur de négation (pas le premier de la phrase) :
        // « je suis grande, je ne veux pas … » → la négation reste sur « veux », pas sur « suis ». [B11]
        const negPos = negIdxs[0];
        let vOrig = -1, best = Infinity;
        out.forEach((t, i) => { if (t.verb) { const d = Math.abs(i - negPos); if (d < best) { best = d; vOrig = i; } } });
        const shift = negIdxs.filter(x => x < vOrig).length;
        out = out.filter((t, i) => negIdxs.indexOf(i) < 0);
        const vi = vOrig - shift;
        if (vi >= 0) {
          let start = vi;
          while (start - 1 >= 0 && CLI_BEFORE.has((out[start - 1].label || "").toLowerCase().trim())) start--;
          out.splice(vi + 1, 0, { label: negKind });        // « … pas » après le verbe gouverné
          if (!oral) out.splice(start, 0, { label: "ne" });  // « ne … » avant le cluster clitique
        }
      }
    }
    return out;
  }
  function build(tokens, ctx) {
    ctx = ctx || {};
    tokens = normalizeTokens(tokens, ctx);
    const words = (tokens || []).map((t, i) => (t.say && !t.verb && !t.plural) ? t.say : tokenDisplay(t, i, tokens, ctx));
    // Impératif NÉGATIF : le pronom reste AVANT le verbe, sans trait d'union (ne me touche pas),
    // contrairement à l'impératif positif (touche-moi). Donc en présence d'une négation, on garde
    // l'ordre normal ; sinon on rejette les pronoms après le verbe (assembleImperative).
    const hasNeg = (tokens || []).some(t => NEG_WORDS.has((t.label || "").toLowerCase().trim()));
    // Les modes non finis peuvent produire des chaînes vides (sujet supprimé) → on les retire.
    const joined = (ctx.tense === "imperatif" && !hasNeg) ? assembleImperative(tokens, words) : words.filter(w => w !== "").join(" ");
    return contract(elide(joined)).replace(/\s+([?!.,])/g, "$1");
  }

  root.Sentence = {
    build, tokenDisplay, conjugatePhrase, elide, contract, assembleImperative,
    isDeterminer, genderBefore, refFeminine, pluralize, feminine, stateAux,
    DET_PLURAL, DET_FEM, DET_GENDER, ADJ_FEM, ETAT_AVOIR, ETAT_ETRE,
  };
})();
