Assessor_2.0/src/features/context-layer/context-cluster-facet.ts

64 lines
3.2 KiB
TypeScript

import type { AssessmentContextResponse } from '../../api/assessor-api-contract';
import type { FindingRecord } from '../../domain/finding';
import type { DynamicFacet } from '../search-filter/dynamic-facets';
import { publishContextReport } from '../../assessment-context/context-report-publication';
/**
* Work facet of the context layer (Phase 13E, decision (c)). It exists only while the layer is on and
* its context is loaded; switched off, the facet is absent and Work filters exactly as without it.
*/
export const CONTEXT_CLUSTER_FACET_ID = 'context:cluster';
const SOURCE_SCAN_FACET_ID = 'provenance:source-scan';
export interface ContextClusterIndex {
publication: ReturnType<typeof publishContextReport>;
facet: DynamicFacet;
/** The loaded clusters with their state, for the Pathfinder details (Phase 13E). */
clusters: AssessmentContextResponse['clusters'];
}
const field = (record: FindingRecord, label: string) =>
record.fields.find(item => item.label.toLocaleLowerCase('en-US') === label.toLocaleLowerCase('en-US'))?.value?.trim() ?? '';
const indexes = new WeakMap<AssessmentContextResponse, ContextClusterIndex>();
/** One shared index per loaded context object, so every Work consumer uses identical cache keys. */
export function contextClusterIndex(context: AssessmentContextResponse): ContextClusterIndex {
const existing = indexes.get(context);
if (existing) return existing;
const byOccurrence = new Map(context.findings.map(item => [item.occurrenceId, item.clusterIds]));
const bySetting = new Map(context.settings.map(item => [`${item.scope}\u0000${item.identityKey}`, item.clusterIds]));
// A record belongs to a cluster when it provides evidence for one of its conditions.
const valuesFor = (record: FindingRecord): string[] => {
const scanner = record.findingOccurrenceId ? byOccurrence.get(record.findingOccurrenceId) : undefined;
if (scanner) return scanner;
const identity = field(record, 'Technical Identity');
return identity ? bySetting.get(`${field(record, 'Scope')}\u0000${identity}`) ?? [] : [];
};
const index: ContextClusterIndex = {
publication: publishContextReport(context),
clusters: context.clusters,
facet: {
id: CONTEXT_CLUSTER_FACET_ID,
label: 'Context Cluster',
source: 'derived',
kind: 'category',
derivedKind: 'context-cluster',
preserveEmptyValues: true,
description: `Derived context layer (catalog ${context.catalog.version}): findings and GPO settings that provide evidence for a cluster. A second reading; it does not change results.`,
values: context.clusters.map(cluster => ({ value: cluster.clusterId, label: cluster.label.en, count: 0 })),
valuesFor
}
};
indexes.set(context, index);
return index;
}
/** Adds the context facet directly before the permanent final `Source: Scan` control. */
export function withContextClusterFacet(facets: DynamicFacet[], index: ContextClusterIndex | undefined): DynamicFacet[] {
if (!index) return facets;
const sourceScan = facets.findIndex(facet => facet.id === SOURCE_SCAN_FACET_ID);
return sourceScan < 0 ? [...facets, index.facet]
: [...facets.slice(0, sourceScan), index.facet, ...facets.slice(sourceScan)];
}