/** * lib/fs — findBaDocs: a BA SOURCE glob never returns non-application material. * * audit-ba writes each verdict to `/_audit/.md`, and five * dimensions share their basename with the document they judge (`entité.md`, * `use-case.md`, `rbac.md`, `screen.md`, `règles-métier.md`). A bare recursive * `findFiles` on `entité.md` therefore reads the audit's OWN OUTPUT as a * source — DEV-UI-034 turned the verdict line « DM-017 — Code patterns * cohérents » into an err on the client's corpus (support 1766f66848a9). * * Three things are pinned here: * 1. the helper's exclusion, at every depth; * 2. the fixture is POTENT (read as a source it does produce a near-miss) and * still worded like the engine — otherwise the two audit tests that use it * would pass on a harmless file; * 3. ADOPTION — no CLI under development/ globs a BA document recursively * with the bare `findFiles`. */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join, relative } from 'node:path' import { findBaDocs, findFiles } from '../fs.js' import { findCodePatternNearMisses } from '../code-pattern-grammar.js' import { SKILLS_ROOT, baSkillFile, listFiles } from './helpers/cli-options.js' import { AUDIT_ENTITE_VERDICT_RELPATH, AUDIT_VERDICT_DM017_MESSAGE, AUDIT_VERDICT_DM021_MESSAGE, auditEntiteVerdictMd, } from './helpers/audit-verdict-fixture.js' let root: string function put(rel: string, content = '# doc\n'): void { const abs = join(root, rel) mkdirSync(join(abs, '..'), { recursive: true }) writeFileSync(abs, content, 'utf8') } const rels = (files: string[]): string[] => files.map((f) => relative(root, f).replace(/\\/g, '/')).sort() beforeEach(() => { root = mkdtempSync(join(tmpdir(), 'ss-ba-docs-')) }) afterEach(() => rmSync(root, { recursive: true, force: true })) describe('findBaDocs — `_`-prefixed folders are never BA sources', () => { it('drops the verdict at the scope root and keeps the module + section documents', async () => { put('entité.md') put('factures/entité.md') put(AUDIT_ENTITE_VERDICT_RELPATH, auditEntiteVerdictMd()) expect(rels(await findBaDocs('**/entité.md', { cwd: root }))).toEqual(['entité.md', 'factures/entité.md']) }) it('drops it at ANY depth — a baRoot-wide glob crosses every module', async () => { put('CRM/PIPELINE/entité.md') put('CRM/PIPELINE/_audit/entité.md', auditEntiteVerdictMd()) put('CRM/PIPELINE/opportunites/_audit/entité.md', auditEntiteVerdictMd()) put('CRM/_audit/entité.md', auditEntiteVerdictMd()) put('_audit/entité.md', auditEntiteVerdictMd()) put('_plan/entité.md') expect(rels(await findBaDocs('**/entit*.md', { cwd: root }))).toEqual(['CRM/PIPELINE/entité.md']) }) it('covers the other shared basenames (use-case.md, jeu-de-test.md)', async () => { put('list/use-case.md') put('_audit/use-case.md') put('jeu-de-test.md') put('_audit/jeu-de-test.md') expect(rels(await findBaDocs('**/use-case.md', { cwd: root }))).toEqual(['list/use-case.md']) expect(rels(await findBaDocs('**/jeu-de-test.md', { cwd: root }))).toEqual(['jeu-de-test.md']) }) it('a caller-supplied ignore is ADDED to the exclusion, never a replacement', async () => { put('entité.md') put('archive/entité.md') put('_audit/entité.md') expect(rels(await findBaDocs('**/entité.md', { cwd: root, ignore: ['archive/**'] }))).toEqual(['entité.md']) }) it('an underscore INSIDE a name is not a prefix — `my_section/` stays a source', async () => { put('my_section/entité.md') expect(rels(await findBaDocs('**/entité.md', { cwd: root }))).toEqual(['my_section/entité.md']) }) it('pin — the bare findFiles still returns the verdict: the exclusion lives in the helper', async () => { put('entité.md') put('_audit/entité.md') expect(rels(await findFiles('**/entité.md', { cwd: root }))).toEqual(['_audit/entité.md', 'entité.md']) }) }) describe('the verdict fixture is potent and worded like the engine', () => { it('read as a source, the DM-017 bullet IS a near-miss (the incident shape)', () => { const misses = findCodePatternNearMisses(auditEntiteVerdictMd()) expect(misses.map((m) => ({ shape: m.shape, entity: m.entity }))).toEqual([{ shape: 'wrong-case', entity: null }]) expect(misses[0]!.text).toContain('DM-017') }) it('DM-017 / DM-021 messages are the ones audit-ba emits', () => { const dm = readFileSync(baSkillFile('audit-run/cli/audit-ba/rules/dm.ts'), 'utf8') expect(dm).toContain(`'${AUDIT_VERDICT_DM017_MESSAGE}'`) expect(dm).toContain(`'${AUDIT_VERDICT_DM021_MESSAGE}'`) }) }) // ─── Adoption ────────────────────────────────────────────────────────────── /** Documents of the `.smartstack/ba/` tree a verdict can shadow by basename. */ const BA_DOC_BASENAME = String.raw`(?:entit[^'"\`/]*|use-case|jeu-de-test|r[eè]gles-m[eé]tier|rbac|screen|acteur|index)\.md` // A `findFiles` call whose pattern is RECURSIVE and ends on a BA document. const BARE_RECURSIVE_BA_GLOB = new RegExp(String.raw`\bfindFiles\(\s*['"\`][^'"\`]*\*\*/` + BA_DOC_BASENAME + String.raw`['"\`]`) const DEV_ROOT = join(SKILLS_ROOT, 'development') const sources = listFiles( DEV_ROOT, (p) => p.endsWith('.ts') && !/[\\/]__tests__[\\/]/.test(p) && !p.endsWith('.test.ts'), ) describe('development/** — a BA document is globbed through findBaDocs', () => { it('scans a real tree (a gate over zero files proves nothing)', () => { expect(sources.length).toBeGreaterThan(50) }) it('no bare recursive findFiles over a BA document', () => { const offenders: string[] = [] for (const file of sources) { const lines = readFileSync(file, 'utf8').split(/\r?\n/) lines.forEach((line, i) => { if (BARE_RECURSIVE_BA_GLOB.test(line)) { offenders.push(`${relative(SKILLS_ROOT, file).replace(/\\/g, '/')}:${i + 1} ${line.trim()}`) } }) } expect(offenders, 'use findBaDocs (lib/fs) — a bare recursive glob reads `_audit/` as a source').toEqual([]) }) it('the gate itself matches the shape it refuses, and spares the ones it must', () => { expect(BARE_RECURSIVE_BA_GLOB.test(`for (const abs of await findFiles('**/entité.md', { cwd }))`)).toBe(true) expect(BARE_RECURSIVE_BA_GLOB.test(`await findFiles('**/entit*.md', { cwd: baRoot })`)).toBe(true) expect(BARE_RECURSIVE_BA_GLOB.test(`await findFiles(".smartstack/ba/**/use-case.md", { cwd })`)).toBe(true) expect(BARE_RECURSIVE_BA_GLOB.test(`await findBaDocs('**/entité.md', { cwd })`)).toBe(false) expect(BARE_RECURSIVE_BA_GLOB.test(`await findFiles('pagespecs/*.md', { cwd })`)).toBe(false) expect(BARE_RECURSIVE_BA_GLOB.test(`await findFiles('rbac.md', { cwd })`)).toBe(false) expect(BARE_RECURSIVE_BA_GLOB.test(`await findFiles('**/*Controller.cs', { cwd })`)).toBe(false) }) })