/** * lib/ba-entity-cartouche.ts — THE definition cartouche of an entity block. * * Three prose bullets, written right after `**Traçabilité**` and BEFORE the * attribute table (doctrine 2026-10-03): * * - **Description** : (every entity) * - **Périmètre** : (business entities) * - **Hors périmètre** : (optional) * * Until it, an entity had NO definition on disk — only a name, a prefix and a * table — so "the same entity modelled twice across applications" could only * be judged on the name (`Conducteur` in two apps may be two different things; * `Conducteur` and `Pilote` may be one). The cartouche is what DM-034 compares, * and `Hors périmètre` is the ONE persistent way to settle a candidate pair * without a judgment: when it names the other side, the two are disjoint by * declaration. * * Lives in its own module because BOTH `ba-entities` (parses the bullets) and * `code-pattern-grammar` (must NOT read them as a near-miss declaration — free * prose mentioning "pattern de numérotation" is not a `**Code pattern**` line) * import it, and `ba-entities` already imports `code-pattern-grammar`. */ /** NFD-strip accents + lowercase + trim (comparisons only). Local on purpose: * `ba-referential-codes` imports `code-pattern-grammar`, which imports THIS * module — reusing its `fold` would close a runtime import cycle. */ function fold(s: string): string { return s .normalize('NFD') .replace(/[̀-ͯ]/g, '') .replace(/’/g, "'") .toLowerCase() .trim() } /** Folded field keys (`ba-entities.foldKey` keeps the hyphen — `index.md` spells it « Hors-périmètre »). */ export const CARTOUCHE_FIELD_KEYS: ReadonlySet = new Set(['description', 'perimetre', 'hors perimetre', 'hors-perimetre']) /** The bullet labels, as a line test (bold label, any of the tolerated spellings). */ export const CARTOUCHE_LINE_RE = /^[-*]\s*\*\*\s*(?:description|p[eé]rim[eè]tre|hors[- ]p[eé]rim[eè]tre)\s*\*\*\s*:/i /** True for a cartouche bullet line (trimmed or not) — the near-miss scanners skip it. */ export function isCartoucheLine(line: string): boolean { return CARTOUCHE_LINE_RE.test(line.trim()) } /** Function words that carry no meaning for a definition comparison (FR + EN, folded). */ const STOP_WORDS: ReadonlySet = new Set([ 'dans', 'pour', 'avec', 'sans', 'cette', 'sont', 'leur', 'leurs', 'tous', 'toutes', 'entre', 'selon', 'elle', 'elles', 'cela', 'ainsi', 'comme', 'etre', 'avoir', 'dont', 'mais', 'donc', 'plus', 'moins', 'aussi', 'chaque', 'vers', 'lors', 'apres', 'avant', 'depuis', 'sous', 'chez', 'notre', 'votre', 'cette', 'celui', 'celle', 'ceux', 'celles', 'meme', 'this', 'that', 'with', 'from', 'into', 'their', 'there', 'which', 'when', 'what', 'where', 'have', 'been', 'will', 'entite', 'entity', 'table', 'donnees', 'data', 'module', 'application', ]) /** Content words of a cartouche text: folded, letters only, ≥ 4 chars, stop words out, deduplicated. */ export function cartoucheWords(text: string | null | undefined): Set { const out = new Set() if (!text) return out for (const w of fold(text).split(/[^a-z0-9]+/)) { if (w.length >= 4 && !STOP_WORDS.has(w)) out.add(w) } return out } /** Jaccard similarity of two word sets — 0 when either is empty (never 1 on two blanks). */ export function jaccard(a: ReadonlySet, b: ReadonlySet): number { if (a.size === 0 || b.size === 0) return 0 let inter = 0 for (const w of a) if (b.has(w)) inter++ const union = a.size + b.size - inter return union === 0 ? 0 : inter / union } /** * Disjoint BY DECLARATION: one side's `Hors périmètre` names the other — its * entity name, or a content word of its `Périmètre` / `Description`. The * mechanical resolution of a DM-034 candidate pair; nothing else settles one * without a judgment. */ export function declaredDisjoint( a: { name: string; description: string | null; perimeter: string | null; outOfScope: string | null }, b: { name: string; description: string | null; perimeter: string | null; outOfScope: string | null }, ): boolean { const names = (x: typeof a, y: typeof a): boolean => { const out = cartoucheWords(x.outOfScope) if (out.size === 0) return false if (out.has(fold(y.name))) return true for (const w of cartoucheWords(`${y.perimeter ?? ''} ${y.description ?? ''}`)) if (out.has(w)) return true return false } return names(a, b) || names(b, a) }