import type { CanonicalAssessmentModel } from '../../src/domain/canonical-model'; import { createReferenceRecognizer, latestReferenceRuns, REFERENCE_RECOGNIZER_VERSION, type ReferenceCatalogSnapshot, type ReferenceRecognitionRun } from '../../src/domain/reference-recognition'; import type { AssessorRepository } from '../../src/persistence/assessor-repository'; import { RepositoryNotFoundError } from '../../src/persistence/repository-errors'; import { bundledReferenceCatalogs, referenceHash } from './reference-catalogs'; export function deriveReferenceRun(model: CanonicalAssessmentModel, scanRunId: string, catalogs: ReferenceCatalogSnapshot[], createdAt: string): ReferenceRecognitionRun { const occurrences = model.findingOccurrences.filter(item => item.scanRunId === scanRunId).sort((a, b) => a.id.localeCompare(b.id)); const files = new Map(model.importFiles.map(file => [file.id, file])); const inputSha256 = referenceHash(occurrences.map(item => ({ id: item.id, importFileId: item.importFileId, sourceFileSha256: files.get(item.importFileId)?.sha256, sourcePosition: item.sourcePosition, fields: item.originalFields }))); const catalogHashes = catalogs.map(catalog => catalog.sha256).sort(); const id = 'references-' + referenceHash([model.project.id, model.assessment.id, scanRunId, REFERENCE_RECOGNIZER_VERSION, inputSha256, catalogHashes]); const recognize = createReferenceRecognizer(catalogs); return { schemaVersion: 1, id, projectId: model.project.id, assessmentId: model.assessment.id, scanRunId, recognizerVersion: REFERENCE_RECOGNIZER_VERSION, inputSha256, catalogHashes, createdAt, results: occurrences.map(item => ({ occurrenceId: item.id, importFileId: item.importFileId, sourceFileSha256: files.get(item.importFileId)?.sha256, sourcePosition: item.sourcePosition, references: recognize(item.originalFields) })) }; } export class ReferenceRecognitionService { private active = new Map>(); constructor(private readonly repository: AssessorRepository, readonly catalogs: () => Promise = async () => bundledReferenceCatalogs) {} async status(projectId: string) { if (!await this.repository.getProject(projectId)) throw new RepositoryNotFoundError('Client nicht vorhanden.'); const scans = (await this.repository.listProjectScanRuns(projectId)).filter(scan => !scan.supersededByScanRunId); const runs = latestReferenceRuns(await this.repository.listReferenceRuns(projectId)); const catalogs = await this.catalogs(); const hashes = catalogs.map(catalog => catalog.sha256).sort().join(':'); const indexed = scans.filter(scan => runs.some(run => run.scanRunId === scan.id)); return { totalScans: scans.length, indexedScans: indexed.length, pendingScans: scans.filter(scan => !runs.some(run => run.scanRunId === scan.id && run.recognizerVersion === REFERENCE_RECOGNIZER_VERSION && [...run.catalogHashes].sort().join(':') === hashes)).length, catalogs: catalogs.map(({ framework, version, sha256 }) => ({ framework, version, sha256 })) }; } reprocess(projectId: string) { let task = this.active.get(projectId); if (!task) { task = this.performReprocess(projectId).finally(() => this.active.delete(projectId)); this.active.set(projectId, task); } return task; } private async performReprocess(projectId: string) { if (!await this.repository.getProject(projectId)) throw new RepositoryNotFoundError('Client nicht vorhanden.'); const catalogs = await this.catalogs(); const existing = new Map((await this.repository.listReferenceRuns(projectId)).map(run => [run.id, run])); let processedScans = 0, unchangedScans = 0, references = 0; for (const assessment of await this.repository.listAssessments(projectId)) { const model = await this.repository.loadAssessmentModel(assessment.id); if (!model) continue; for (const scan of model.scanRuns.filter(item => !item.supersededByScanRunId)) { const latestTimestamp = Math.max(0, ...[...existing.values()].filter(run => run.scanRunId === scan.id).map(run => Date.parse(run.createdAt))); const run = deriveReferenceRun(model, scan.id, catalogs, new Date(Math.max(Date.now(), latestTimestamp + 1)).toISOString()); const latest = latestReferenceRuns([...existing.values()]).find(item => item.scanRunId === scan.id); references += run.results.reduce((sum, item) => sum + item.references.length, 0); if (latest && latest.inputSha256 === run.inputSha256 && latest.recognizerVersion === run.recognizerVersion && [...latest.catalogHashes].sort().join(':') === run.catalogHashes.join(':')) { unchangedScans++; continue; } // Returning to a previously used catalog needs a new immutable run to become latest. if (existing.has(run.id)) run.id = 'references-' + referenceHash([run.id, run.createdAt]); await this.repository.saveReferenceRun(run, catalogs); existing.set(run.id, run); processedScans++; } } return { processedScans, unchangedScans, references }; } }