import { describe, it, expect } from 'vitest'; import { normToken, buildDependencyGraph, topoOrder, findHoles, decompositionGroups, coverageRegistry, requestsChecklist, readinessChecklist, effectiveNeeds, } from './coverage-machine.mjs'; const c = (skill, needs, produces, constraints = []) => ({ skill, needs, produces, constraints }); const CHAIN = [c('X', ['spec'], ['code']), c('Y', [], ['spec'])]; describe('normToken', () => { it('lower + trim', () => { expect(normToken(' Spec ')).toBe('spec'); }); }); // fix: tools/coverage-machine.mjs (F-1, аудит M1-M4) — покрытие по словам, не по подстроке describe('F-1: покрытие по границам слов, не по подстроке', () => { it('одиночная буква в produces не покрывает многословную просьбу', () => { const cl = requestsChecklist(['audit-rls-policy'], [{ skill: 'x', needs: [], produces: ['a'] }]); expect(cl[0].ok).toBe(false); }); it('coverageRegistry не гасит сироту по подстрочному совпадению', () => { const { orphans } = coverageRegistry([{ skill: 's', needs: [], produces: ['a'] }], { requests: ['audit-deals'] }); expect(orphans.length).toBe(1); }); it('точное слово и подмножество слов по-прежнему покрывают (coverage-machine.mjs)', () => { expect(requestsChecklist(['csv'], [{ skill: 'e', needs: [], produces: ['csv-export-deals'] }])[0].ok).toBe(true); expect(requestsChecklist(['csv-export'], [{ skill: 'e', needs: [], produces: ['csv-export'] }])[0].ok).toBe(true); }); }); describe('buildDependencyGraph (A: needs↔produces)', () => { it('ребро producer→consumer via need', () => { const g = buildDependencyGraph(CHAIN); expect(g.edges).toContainEqual({ from: 'Y', to: 'X', via: 'spec' }); }); }); describe('topoOrder (A: порядок = топосортировка, цикл = флаг)', () => { it('порядок: Y перед X', () => { const r = topoOrder(CHAIN); expect(r.cycle).toBe(null); expect(r.order.indexOf('Y')).toBeLessThan(r.order.indexOf('X')); }); it('цикл помечается', () => { const cyc = [c('A', ['b'], ['a']), c('B', ['a'], ['b'])]; const r = topoOrder(cyc); expect(r.order).toBe(null); expect(r.cycle.sort()).toEqual(['A', 'B']); }); }); describe('findHoles (A: нужда без producer; D: ограничения тоже)', () => { it('нужда, которую никто не производит → дыра', () => { const h = findHoles([c('X', ['spec'], ['code'])]); expect(h).toContainEqual({ need: 'spec', neededBy: 'X', kind: 'need' }); }); it('initialInputs закрывают нужду (не дыра)', () => { expect(findHoles([c('X', ['spec'], ['code'])], { initialInputs: ['spec'] })).toEqual([]); }); it('ограничение без покрытия → дыра kind=constraint (D)', () => { const h = findHoles([c('X', [], ['code'], ['must be RLS-safe'])]); expect(h).toContainEqual({ need: 'must be RLS-safe', neededBy: 'X', kind: 'constraint' }); }); it('produced нужда не дыра', () => { expect(findHoles(CHAIN)).toEqual([]); }); }); describe('decompositionGroups (A: связные группы)', () => { it('связанные скилы — одна группа, несвязанный — отдельная', () => { const groups = decompositionGroups([...CHAIN, c('Z', [], ['unrelated'])]); const sizes = groups.map((g) => g.length).sort(); expect(sizes).toEqual([1, 2]); }); }); describe('coverageRegistry (B: нужды↔решения, дыры + сироты)', () => { it('дыра попадает в holes', () => { const r = coverageRegistry([c('X', ['spec'], ['code'])]); expect(r.holes.map((h) => h.need)).toContain('spec'); }); it('сирота-скоупкрип: produces никому не нужен и не покрывает просьбу', () => { const r = coverageRegistry([c('X', ['spec'], ['code']), c('Y', [], ['spec']), c('Z', [], ['extra'])], { requests: ['code'] }); expect(r.orphans.map((o) => o.skill)).toContain('Z'); expect(r.orphans.map((o) => o.skill)).not.toContain('Y'); }); }); describe('requestsChecklist (C: просьбы цели → план)', () => { it('просьба, которую кто-то производит → ok', () => { const list = requestsChecklist(['code'], [c('X', [], ['code'])]); expect(list).toEqual([{ request: 'code', coveredBy: 'X', ok: true }]); }); it('непокрытая просьба → ok=false, coveredBy=null', () => { const list = requestsChecklist(['report'], [c('X', [], ['code'])]); expect(list[0]).toMatchObject({ request: 'report', coveredBy: null, ok: false }); }); it('покрытие по подстроке (мягкий край)', () => { const list = requestsChecklist(['csv'], [c('X', [], ['export to csv file'])]); expect(list[0].ok).toBe(true); }); }); describe('readinessChecklist (хребет — галочки + указатели §)', () => { it('полный план → ready=true, все галочки', () => { const r = readinessChecklist({ contracts: [c('Y', [], ['spec']), c('X', ['spec'], ['code'])], requests: ['code'] }); expect(r.ready).toBe(true); expect(r.items.every((i) => i.ok)).toBe(true); expect(r.items.every((i) => typeof i.pointer === 'string')).toBe(true); }); it('дыра → ready=false + пункт про дыры провален', () => { const r = readinessChecklist({ contracts: [c('X', ['spec'], ['code'])], requests: ['code'] }); expect(r.ready).toBe(false); expect(r.items.find((i) => /дыр|hole/i.test(i.label)).ok).toBe(false); }); it('цикл → пункт про циклы провален', () => { const r = readinessChecklist({ contracts: [c('A', ['b'], ['a']), c('B', ['a'], ['b'])], requests: [] }); expect(r.items.find((i) => /цикл|cycle/i.test(i.label)).ok).toBe(false); }); }); describe('effectiveNeeds + findHoles c inherent (#1 — присущее по природе = названная нужда)', () => { const withInh = { skill: 'fd', needs: ['design'], produces: [], constraints: ['a11y'], inherent: [{ need: 'мобильный', rationale: 'адаптив по природе' }] }; it('effectiveNeeds включает inherent (kind=inherent) наравне с needs/constraints', () => { const en = effectiveNeeds(withInh); expect(en).toContainEqual({ token: 'design', kind: 'need' }); expect(en).toContainEqual({ token: 'a11y', kind: 'constraint' }); expect(en).toContainEqual({ token: 'мобильный', kind: 'inherent' }); }); it('inherent-нужда без producer → дыра kind=inherent', () => { const h = findHoles([withInh]); expect(h).toContainEqual({ need: 'мобильный', neededBy: 'fd', kind: 'inherent' }); }); it('контракт без inherent → поведение не изменилось', () => { expect(effectiveNeeds({ skill: 'x', needs: ['n'], constraints: [] })).toEqual([{ token: 'n', kind: 'need' }]); }); }); describe('пустой/пробельный запрос НЕ покрывается (F3 — empty-token footgun)', () => { it('пробельная просьба → ok=false, coveredBy=null (а не ложное «покрыто»)', () => { const list = requestsChecklist([' '], [c('a', [], ['x'])]); expect(list[0]).toMatchObject({ coveredBy: null, ok: false }); }); it('пустая просьба не делает любой навык покрывающим → сирота считается', () => { const r = coverageRegistry([c('Z', [], ['extra'])], { requests: [' '] }); expect(r.orphans.map((o) => o.skill)).toContain('Z'); }); });