import { describe, expect, it } from 'vitest'; import { groundRegulatoryCandidateAssessmentResult, groundRegulatoryMappingResult, regulatoryCandidateAssessmentValidationErrors, regulatoryMappingResultValidationErrors, regulatoryMappingRunValidationErrors, selectRegulatoryMappingsFromAssessments, type RegulatoryCandidateAssessmentResult, type RegulatoryMappingRun } from '../../src/domain/regulatory-mapping'; const grounded = new Map([['5.1', new Set(['semantic-5.1', 'reference-5.1'])]]); describe('regulatory mapping output validation', () => { it('requires one grounded assessment for every supplied candidate', () => { const allowed = new Map([ ['5.1', new Set(['semantic-5.1'])], ['8.2', new Set(['semantic-8.2'])] ]); const valid = { schemaVersion: 1, outcome: 'assessed', assessments: [ { controlId: '5.1', relevance: 'primary', confidence: 'high', rationale: 'Direct.', evidenceChunkIds: ['semantic-5.1'] }, { controlId: '8.2', relevance: 'irrelevant', confidence: 'high', rationale: 'Only broad overlap.', evidenceChunkIds: ['semantic-8.2'] } ] }; expect(regulatoryCandidateAssessmentValidationErrors(valid, allowed)).toEqual([]); expect(regulatoryCandidateAssessmentValidationErrors({ ...valid, assessments: valid.assessments.slice(0, 1) }, allowed)) .toEqual(expect.arrayContaining([expect.stringContaining('candidate control 8.2'), expect.stringContaining('every supplied candidate')])); expect(regulatoryCandidateAssessmentValidationErrors({ ...valid, assessments: [...valid.assessments, valid.assessments[0]] }, allowed)) .toEqual(expect.arrayContaining([expect.stringContaining('duplicates control 5.1')])); }); it('grounds assessments and deterministically selects at most one primary plus three supporting controls', () => { const allowed = new Map([ ['5.1', new Set(['chunk-5.1'])], ['5.2', new Set(['chunk-5.2'])], ['5.3', new Set(['chunk-5.3'])], ['5.4', new Set(['chunk-5.4'])], ['5.5', new Set(['chunk-5.5'])] ]); const assessment = (controlId: string, relevance: 'primary' | 'supporting' | 'irrelevant', confidence: 'low' | 'medium' | 'high') => ({ controlId, relevance, confidence, rationale: `Synthetic ${controlId}.`, evidenceChunkIds: [`chunk-${controlId}`, 'foreign'] }); const groundedAssessment = groundRegulatoryCandidateAssessmentResult({ schemaVersion: 1, outcome: 'assessed', assessments: [ assessment('5.1', 'primary', 'medium'), assessment('5.2', 'primary', 'high'), assessment('5.3', 'supporting', 'high'), assessment('5.4', 'supporting', 'medium'), assessment('5.5', 'supporting', 'low') ] }, allowed) as RegulatoryCandidateAssessmentResult; expect(regulatoryCandidateAssessmentValidationErrors(groundedAssessment, allowed)).toEqual([]); const selected = selectRegulatoryMappingsFromAssessments(groundedAssessment, ['5.1', '5.2', '5.3', '5.4', '5.5']); expect(selected.mappings.map(item => `${item.controlId}:${item.relationship}`)).toEqual([ '5.2:primary', '5.1:supporting', '5.3:supporting', '5.4:supporting' ]); expect(selected.mappings.every(item => item.evidenceChunkIds.length === 1)).toBe(true); }); it('derives supporting-only and no-relevant-control outcomes without forcing a primary', () => { const supporting = selectRegulatoryMappingsFromAssessments({ schemaVersion: 1, outcome: 'assessed', assessments: [{ controlId: '5.1', relevance: 'supporting', confidence: 'medium', rationale: 'Material support.', evidenceChunkIds: ['semantic-5.1'] }] }, ['5.1']); expect(supporting).toMatchObject({ outcome: 'mapped', mappings: [{ controlId: '5.1', relationship: 'supporting' }] }); const irrelevant = selectRegulatoryMappingsFromAssessments({ schemaVersion: 1, outcome: 'assessed', assessments: [{ controlId: '5.1', relevance: 'irrelevant', confidence: 'high', rationale: 'Only topic overlap.', evidenceChunkIds: ['semantic-5.1'] }] }, ['5.1']); expect(irrelevant).toMatchObject({ outcome: 'no_relevant_control', mappings: [] }); }); it('accepts a grounded proposal and rejects unknown controls, duplicate controls, and foreign evidence', () => { const valid = { schemaVersion: 1, outcome: 'mapped', mappings: [{ controlId: '5.1', relationship: 'primary', confidence: 'high', rationale: 'Synthetic rationale.', evidenceChunkIds: ['semantic-5.1', 'reference-5.1'] }] }; expect(regulatoryMappingResultValidationErrors(valid, grounded)).toEqual([]); expect(regulatoryMappingResultValidationErrors({ ...valid, mappings: [ ...valid.mappings, { ...valid.mappings[0], evidenceChunkIds: ['other'] } ] }, grounded)).toEqual(expect.arrayContaining([ expect.stringContaining('duplicates control 5.1'), expect.stringContaining('not grounded') ])); expect(regulatoryMappingResultValidationErrors({ ...valid, mappings: [{ ...valid.mappings[0], controlId: '9.9' }] }, grounded)) .toEqual(expect.arrayContaining([expect.stringContaining('unknown candidate control')])); }); it('trims ungrounded evidence ids, drops fully ungrounded known-control mappings, and leaves unknown controls for validation', () => { const result = groundRegulatoryMappingResult({ schemaVersion: 1, outcome: 'mapped', mappings: [ { controlId: '5.1', relationship: 'primary', confidence: 'high', rationale: 'One good id kept.', evidenceChunkIds: ['semantic-5.1', 'foreign'] }, { controlId: '5.1', relationship: 'supporting', confidence: 'low', rationale: 'No real evidence.', evidenceChunkIds: ['foreign', 'other'] } ] }, grounded) as { mappings: Array<{ evidenceChunkIds: string[] }> }; expect(result.mappings).toHaveLength(1); expect(result.mappings[0].evidenceChunkIds).toEqual(['semantic-5.1']); const unknown = groundRegulatoryMappingResult({ schemaVersion: 1, outcome: 'mapped', mappings: [{ controlId: '9.9', relationship: 'primary', confidence: 'high', rationale: 'x', evidenceChunkIds: ['semantic-5.1'] }] }, grounded) as { mappings: unknown[] }; expect(unknown.mappings).toHaveLength(1); expect(regulatoryMappingResultValidationErrors(unknown, grounded)).toEqual(expect.arrayContaining([expect.stringContaining('unknown candidate control')])); }); it('requires refusal outcomes to contain no mappings and a bounded reason', () => { expect(regulatoryMappingResultValidationErrors({ schemaVersion: 1, outcome: 'no_relevant_control', reason: 'No direct relationship.', mappings: [] }, grounded)).toEqual([]); expect(regulatoryMappingResultValidationErrors({ schemaVersion: 1, outcome: 'insufficient_context', reason: 'Unknown.', mappings: [{ controlId: '5.1', relationship: 'supporting', confidence: 'low', rationale: 'Synthetic.', evidenceChunkIds: ['semantic-5.1'] }] }, grounded)).toContain('A refusal outcome must not include mappings.'); }); it('accepts a mapped result with supporting controls and no primary control', () => { expect(regulatoryMappingResultValidationErrors({ schemaVersion: 1, outcome: 'mapped', mappings: [{ controlId: '5.1', relationship: 'supporting', confidence: 'medium', rationale: 'Relevant, but not uniquely primary.', evidenceChunkIds: ['semantic-5.1'] }] }, grounded)).toEqual([]); }); it('accepts a twelve-control shortlist and rejects a thirteenth control', () => { const candidate = (index: number) => { const controlId = `8.${index + 1}`; return { controlId, annexAControlId: `A.${controlId}`, title: `Synthetic control ${controlId}`, score: 1 - index / 100, evidence: [{ chunkId: `chunk-${controlId}`, textSha256: 'a'.repeat(64), sourceSha256: 'b'.repeat(64), sourceStandard: 'ISO/IEC 27002' as const, sectionKind: 'control' as const, pageStart: 1, pageEnd: 1, text: 'Synthetic evidence.' }] }; }; const run: RegulatoryMappingRun = { version: 1, id: 'mapping-1', ruleKey: 'pingcastle:synthetic', sourceTool: 'pingcastle', status: 'running', queryHash: 'c'.repeat(64), preparedQuery: { schemaVersion: 2, query: 'Synthetic full context.', retrievalQuery: 'Synthetic concise query.', specificity: 'rule', publicTopics: [] }, corpusId: 'synthetic', corpusVersion: '1', sourceHashes: ['b'.repeat(64)], embeddingModel: 'synthetic', retrievalVersion: 'synthetic', promptVersion: 'synthetic', outputSchemaVersion: 1, model: 'synthetic', createdAt: '2026-09-17T00:00:00.000Z', updatedAt: '2026-09-17T00:00:00.000Z', candidates: Array.from({ length: 12 }, (_, index) => candidate(index)) }; expect(regulatoryMappingRunValidationErrors(run)).toEqual([]); expect(regulatoryMappingRunValidationErrors({ ...run, candidates: [...run.candidates, candidate(12)] })) .toContain('Regulatory mapping candidates must contain one to twelve controls when processing has started.'); }); });