/** * The entity definition cartouche (lib/ba-entity-cartouche + lib/ba-entities): * `**Description**` / `**Périmètre**` / `**Hors périmètre**` are typed by the * shared parser (both spellings of « Hors-périmètre »), never read as a * Code-pattern / decided-code near-miss by the scanners (free prose by * contract), and `declaredDisjoint` is the one mechanical resolution of a * cross-application candidate pair. */ import { describe, expect, it } from 'vitest' import { parseEntityDoc } from '../ba-entities.js' import { cartoucheWords, declaredDisjoint, isCartoucheLine, jaccard } from '../ba-entity-cartouche.js' import { findCodePatternNearMisses, findDecidedCodeNearMisses } from '../code-pattern-grammar.js' const DOC = ` # Modèle de données — GESTION_FLOTTE / FLOTTE ### ENT-001 — Conducteur (agrégat racine) - **Préfixe table** : \`flt_\` - **Traçabilité** : UC-GESTION_FLOTTE-FLOTTE-001 - **Description** : la personne habilitée à prendre le volant d'un véhicule de la flotte. - **Périmètre** : conducteurs salariés et intérimaires, permis vérifié, numérotés selon un pattern de numérotation annuel. - **Hors-périmètre** : le conducteur de projet (chef de projet) — autre application. | Attribut | Type | Contraintes | Calculé | |----------|------|-------------|---------| | Id | Guid | PK | — | | Nom | string(100) | requis | — | - **Relations** : Conducteur *→1 User — FK UserId, scope core (auth_Users), onDelete restrict. ### ENT-002 — TypePermis (lookup) - **Préfixe table** : \`flt_\` - **Description** : catégorie de permis de conduire. | Attribut | Type | Contraintes | Calculé | |----------|------|-------------|---------| | Id | Guid | PK | — | | Label | string(50) | requis | — | ` describe('ba-entities — the cartouche is typed', () => { const { entities, warnings } = parseEntityDoc(DOC, 'GESTION_FLOTTE/FLOTTE') it('Description / Périmètre / Hors-périmètre (hyphen spelling) land on the entity, verbatim', () => { const c = entities[0]! expect(c.description).toBe("la personne habilitée à prendre le volant d'un véhicule de la flotte.") expect(c.perimeter).toContain('conducteurs salariés et intérimaires') expect(c.outOfScope).toContain('conducteur de projet') expect(c.attributes.map((a) => a.name)).toEqual(['Id', 'Nom']) expect(c.relations).toHaveLength(1) }) it('an entity without the optional bullets carries null, not undefined', () => { const t = entities[1]! expect(t.description).toBe('catégorie de permis de conduire.') expect(t.perimeter).toBeNull() expect(t.outOfScope).toBeNull() }) it('the space spelling « Hors périmètre » parses too', () => { const doc = DOC.replace('**Hors-périmètre**', '**Hors périmètre**') expect(parseEntityDoc(doc, 'X/Y').entities[0]!.outOfScope).toContain('conducteur de projet') }) it('no parser warning is raised by the three bullets', () => { expect(warnings).toEqual([]) }) }) describe('code-pattern-grammar — a cartouche line is prose, never a near-miss', () => { it('« pattern de numérotation » inside **Périmètre** is NOT a wrong-case Code-pattern declaration', () => { expect(findCodePatternNearMisses(DOC)).toEqual([]) }) it('the same mention in an unknown bullet IS still caught (the exemption is the cartouche only)', () => { const doc = DOC.replace('- **Périmètre** :', '- **Numérotation** :') expect(findCodePatternNearMisses(doc).map((n) => n.shape)).toEqual(['wrong-case']) }) it('« décision utilisateur » inside **Description** is not a decided-code near-miss', () => { const doc = DOC.replace('- **Description** : la personne', '- **Description** : la personne (décision utilisateur du comité)') expect(findDecidedCodeNearMisses(doc)).toEqual([]) }) it('isCartoucheLine accepts the three labels in either spelling, with or without accents', () => { expect(isCartoucheLine('- **Description** : x')).toBe(true) expect(isCartoucheLine(' - **Perimetre** : x')).toBe(true) expect(isCartoucheLine('* **Hors périmètre** : x')).toBe(true) expect(isCartoucheLine('- **Hors-perimetre** : x')).toBe(true) expect(isCartoucheLine('- **Code pattern** : x')).toBe(false) expect(isCartoucheLine('| Description | x |')).toBe(false) }) }) describe('cartoucheWords / jaccard / declaredDisjoint', () => { it('content words: folded, ≥ 4 letters, stop words out', () => { expect([...cartoucheWords("La personne habilitée à prendre le volant d'un véhicule")].sort()).toEqual(['habilitee', 'personne', 'prendre', 'vehicule', 'volant']) expect(cartoucheWords(null).size).toBe(0) }) it('jaccard is 0 on an empty side (two blank cartouches are never « identical »)', () => { expect(jaccard(new Set(), new Set())).toBe(0) expect(jaccard(new Set(['a', 'b']), new Set(['b', 'c']))).toBeCloseTo(1 / 3) }) it('declaredDisjoint: a Hors périmètre naming the other side (name or a perimeter word) settles the pair', () => { const vehicule = { name: 'Conducteur', description: 'conducteur de véhicule', perimeter: 'salariés au volant', outOfScope: 'le conducteur de projet' } const projet = { name: 'Conducteur', description: 'pilote un projet', perimeter: 'chef de projet interne', outOfScope: null } const pilote = { name: 'Pilote', description: 'personne au volant', perimeter: 'salariés au volant des véhicules', outOfScope: null } expect(declaredDisjoint(vehicule, projet)).toBe(true) // « projet » is a perimeter word of the other side expect(declaredDisjoint(projet, vehicule)).toBe(true) // symmetric expect(declaredDisjoint(vehicule, pilote)).toBe(false) // nothing excludes a Pilote expect(declaredDisjoint({ ...vehicule, outOfScope: 'le Pilote' }, pilote)).toBe(true) // by name }) }) describe('code-pattern-grammar — a **Sensibilité** line is a typed declaration, never a near-miss', () => { it('its dated « décision client » facet is not a decided-code near-miss, and a « pattern » word in its reason is not a code-pattern one', () => { const doc = DOC.replace( '- **Hors-périmètre** :', '- **Sensibilité** : interne — financier — décision client du 2026-10-03 : le pattern de numérotation reste interne\n- **Hors-périmètre** :', ) expect(findDecidedCodeNearMisses(doc)).toEqual([]) expect(findCodePatternNearMisses(doc)).toEqual([]) }) })