/** * cli:audit-ba — corpus/conventions.ts * * Requirement 8 of the audit-ba mandate: massive, measured corpus conventions * are ACCEPTED as-is — the tooling adapts to the project, never the reverse. * Detection is a pure function of the corpus (≥90% share decides), zero * config, zero mutable state. The divergence from the canonical doc form is * ONE project-scoped warn (CONV-001 / CONV-002), never an error per * occurrence (1 538 flat-kebab error codes = 1 warn, not 1 538 errs). */ import type { ProjectConventions } from '../types.js' import type { ModuleModel } from './model.js' const SHARE_THRESHOLD = 0.9 /** True when the UC code's section segments (between MOD and NNN) carry any * lowercase letter — the corpus-lowercase form. */ function ucSectionIsLower(ucCode: string): boolean { const middle = ucCode.split('-').slice(3, -1).join('-') return /[a-z]/.test(middle) } /** BR-011 canonical: `lower.dotted` ≥3 segments. Kebab flat = no dot at all. */ function errorCodeIsKebabFlat(code: string): boolean { return !code.includes('.') } export function detectConventions(modules: ModuleModel[]): ProjectConventions { const ucs = modules.flatMap((m) => m.ucs) const lowerCount = ucs.filter((u) => ucSectionIsLower(u.ucCode)).length const ucShare = ucs.length === 0 ? 0 : lowerCount / ucs.length const codes = modules.flatMap((m) => m.rules.map((r) => r.errorCode)).filter((c): c is string => !!c) const kebabCount = codes.filter(errorCodeIsKebabFlat).length const codeShare = codes.length === 0 ? 0 : kebabCount / codes.length return { ucSectionCase: ucs.length === 0 ? 'upper' : ucShare >= SHARE_THRESHOLD ? 'lower' : ucShare <= 1 - SHARE_THRESHOLD ? 'upper' : 'mixed', ucSectionLowerShare: Math.round(ucShare * 1000) / 1000, errorCodeShape: codes.length === 0 ? 'dotted' : codeShare >= SHARE_THRESHOLD ? 'kebab' : codeShare <= 1 - SHARE_THRESHOLD ? 'dotted' : 'mixed', errorCodeKebabShare: Math.round(codeShare * 1000) / 1000, } } /** Modules on the MINORITY side of the detected UC-case convention. */ export function ucCaseMinorityModules(modules: ModuleModel[], convention: 'lower' | 'upper'): string[] { const out: string[] = [] for (const m of modules) { if (m.ucs.length === 0) continue const lower = m.ucs.filter((u) => ucSectionIsLower(u.ucCode)).length const majorityLower = lower * 2 >= m.ucs.length if ((convention === 'lower') !== majorityLower) out.push(`${m.app}/${m.module}`) } return out }