/** * lib/display-field.ts — SSOT of "which field NAMES a row". * * Unifies the two heuristics that used to live apart (§26): scaffold-business's * pickDisplayField hardcoded ['Name','Label','Code','Title'] while * derive-related-tabs-data matched the wider DISPLAYISH family — and the * fallback below either list was "first string column", which labelled rows * with phone numbers, free comments or raw GUIDs on ten entities of one * project. The lists here are the ONLY sanctioned cascade; there is no silent * fallback anymore (scaffold-business fails closed, `**Affichage**` in * entité.md is the authoring channel, `**Affichage** : Id` the conscious * opt-out). */ /** * Stored-column display candidates, in priority order (PascalCase property * names). Same family as DISPLAYISH_RE, ordered by how strongly the name * asserts "this labels the row". */ export const PREFERRED_DISPLAY_FIELDS: readonly string[] = [ 'Name', 'Label', 'Code', 'Title', 'Libelle', 'Titre', 'Reference', 'Number', 'Numero', ] /** * The same cascade MINUS `Code`, for reference tables. * * A reference value's label IS its identity: on a `lookup`, the code is at * best a technical handle and at worst — measured — the thing every combobox * and every column ended up showing (`A_FAIRE` instead of « À faire », on 11 * services fixed BY HAND because the generator kept re-choosing `Code`). * Other classifications keep `Code` in the cascade: on an invoice or a * contract the code IS the handle people quote over the phone. */ export const LOOKUP_DISPLAY_FIELDS: readonly string[] = PREFERRED_DISPLAY_FIELDS.filter( (f) => f !== 'Code', ) /** * The stored cascade to use for an entity of this classification. Passing no * classification keeps the historical behaviour — the caller simply does not * know, and a reference table without its `**Affichage**` line is DM-015's * business, not a place to guess. */ export function preferredDisplayFieldsFor(classification?: string): readonly string[] { const folded = (classification ?? '') .normalize('NFD') .replace(/[̀-ͯ]/g, '') .toLowerCase() return folded.includes('lookup') ? LOOKUP_DISPLAY_FIELDS : PREFERRED_DISPLAY_FIELDS } /** * Core-PROJECTED display candidates (Person entities — the identity lives on * auth_Users). `FirstName`+`LastName` both projected compose into a full name * BEFORE this list is consulted. */ export const PREFERRED_PROJECTED_DISPLAY_FIELDS: readonly string[] = [ 'DisplayName', 'FullName', 'Name', 'Label', 'Title', 'LastName', 'FirstName', 'Email', ] /** * Case-insensitive "this key labels the row" test for kebab/camel column keys * (the derive-related-tabs-data / responsive-table instinct). */ export const DISPLAYISH_RE = /^(code|label|name|libelle|title|titre|number|numero|reference)$/i /** * The `- **Affichage** : ` line of an entité.md entity block — the * BA's authoring channel for "what names a row" (grammar: * business-analyse/_workflow/doc-templates.md; `: Id` is the conscious GUID * opt-out). First DETERMINISTIC parser of the line: until it, the token was * transported by an LLM step only (ba-develop phases-detail Phase 2). */ export const DISPLAY_FIELD_LINE_RE = /^-\s*\*\*Affichage\*\*\s*:\s*([A-Za-z][A-Za-z0-9]*)/m /** * Parse the entity-block-scoped `**Affichage**` line of `entité.md`. Scoping * mirrors derive-lifecycle's parseEntityStatusEnums (`### `-split + heading * word-boundary test) so a sibling entity's line never leaks in. Returns the * PascalCase token verbatim — `'Id'` included (callers treat it as the * conscious opt-out) — or `null` when the entity block or the line is absent. */ export function parseDisplayFieldLine(entiteMd: string, entity: string): string | null { const blocks = entiteMd.split(/^###\s+/m) const entityRe = new RegExp(`(^|[^A-Za-z0-9])${entity}([^A-Za-z0-9]|$)`) const block = blocks.find((b) => entityRe.test(b.split(/\r?\n/, 1)[0] ?? '')) if (block === undefined) return null const m = DISPLAY_FIELD_LINE_RE.exec(block) return m ? m[1]! : null }