import { ALGO_FACE, ARCH_FACE, FACE_SEPARATOR, FORCE_FACE, PAG_FACE, REASON_FACE, STAGE_FACE } from "@govlab/constants"; import { AXIS_KIND, LENS_KIND, NODE_KIND, TECHNIQUE_KIND, TEST_SURFACE_KIND, } from "#configuration/constants/ontology.constants"; import { type FieldSpec, type GovlabContext, ONTOLOGY_SCHEMA, PAG_KINDS, keywordIdOf, valuesAt } from "@govlab/context"; import type { RefIndex, Resolver, ReverseIndex } from "#types/ontology.types"; import { unreadKind, unresolvableTarget } from "#configuration/strings/ontology.strings"; import type { EdgeRef } from "@banes-lab/web/types/link.types.js"; import { forceNames } from "#core/converters/layer.converter"; import { refOf } from "#core/resolvers/ontology.resolver"; interface Indexed { readonly record: unknown; readonly self: EdgeRef; } interface Scope { readonly forces: ReadonlySet; readonly resolve: Resolver; } type KindRecords = (context: GovlabContext, resolve: Resolver) => readonly Indexed[]; const REASON_PREFIX = REASON_FACE + FACE_SEPARATOR; const push = function push(index: Map, key: string | null, value: EdgeRef): void { if (key === null) { return; } const held = index.get(key) ?? []; if (!held.some((entry) => entry.ref === value.ref && entry.label === value.label)) { index.set(key, [...held, value]); } }; export const lookup = function lookup( index: ReverseIndex, relation: string, face: string, id: string, ): readonly EdgeRef[] { return index.get(relation)?.get(refOf(face, id)) ?? []; }; const kindOf = function kindOf( records: (context: GovlabContext) => readonly T[], self: (record: T, resolve: Resolver) => EdgeRef, ): KindRecords { return (context, resolve) => records(context).map((record) => ({ record, self: self(record, resolve) })); }; const reasonKind = function reasonKind( kind: string, records: (context: GovlabContext) => readonly T[], ): KindRecords { return kindOf(records, (record, resolve) => resolve.reasonAs(kind, record.id)); }; const pagSelf = function pagSelf(resolve: Resolver, kind: string, id: string, label: string): EdgeRef { return { label, ref: resolve.pag(kind + FACE_SEPARATOR + id).ref }; }; const KIND_RECORDS: ReadonlyMap = new Map([ [REASON_PREFIX + AXIS_KIND, reasonKind(AXIS_KIND, (context) => context.reason.axes())], [REASON_PREFIX + LENS_KIND, reasonKind(LENS_KIND, (context) => context.reason.lenses())], [REASON_PREFIX + NODE_KIND, reasonKind(NODE_KIND, (context) => context.reason.nodes())], [REASON_PREFIX + TECHNIQUE_KIND, reasonKind(TECHNIQUE_KIND, (context) => context.reason.techniques())], [REASON_PREFIX + TEST_SURFACE_KIND, reasonKind(TEST_SURFACE_KIND, (context) => context.reason.testSurfaces())], [ `${REASON_PREFIX}edge`, kindOf( (context) => context.reason.edges(), (edge, resolve) => resolve.edgeSource(edge.from), ), ], [ `${ALGO_FACE}${FACE_SEPARATOR}contract`, kindOf( (context) => context.algo.all(), (contract, resolve) => resolve.algo(contract.id), ), ], [ `${ARCH_FACE}${FACE_SEPARATOR}principle`, kindOf( (context) => context.arch.all(), (principle, resolve) => resolve.archId(principle.id), ), ], [ PAG_FACE + FACE_SEPARATOR + PAG_KINDS.documentType, kindOf( (context) => context.pag.documentTypes(), (type, resolve) => pagSelf(resolve, PAG_KINDS.documentType, type.type, type.type), ), ], [ PAG_FACE + FACE_SEPARATOR + PAG_KINDS.keyword, kindOf( (context) => context.pag.keywords(), (keyword, resolve) => pagSelf(resolve, PAG_KINDS.keyword, keywordIdOf(keyword), keyword.keyword), ), ], [ PAG_FACE + FACE_SEPARATOR + PAG_KINDS.production, kindOf( (context) => context.pag.productions(), (production, resolve) => pagSelf(resolve, PAG_KINDS.production, production.lhs, production.lhs), ), ], ]); const TARGETS: ReadonlyMap EdgeRef> = new Map([ [ALGO_FACE, (value: string, scope: Scope) => scope.resolve.algo(value)], [ARCH_FACE, (value: string, scope: Scope) => scope.resolve.arch(value)], [STAGE_FACE, (value: string, scope: Scope) => scope.resolve.stage(value)], [FORCE_FACE, (value: string, scope: Scope) => scope.resolve.force(value, scope.forces)], ]); const targetOf = function targetOf(target: string | undefined, value: string, scope: Scope): EdgeRef { if (target === undefined) { return scope.resolve.target(value); } if (target.startsWith(REASON_PREFIX)) { return scope.resolve.reasonAs(target.slice(REASON_PREFIX.length), value); } const resolveIn = TARGETS.get(target); if (resolveIn === undefined) { throw new Error(unresolvableTarget(target)); } return resolveIn(value, scope); }; const inverseFields = function inverseFields( schema: Readonly>, ): readonly (readonly [string, FieldSpec & { readonly inverse: string }])[] { return Object.entries(schema).flatMap(([path, spec]) => { const { inverse } = spec; return inverse === undefined ? [] : [[path, { ...spec, inverse }] as const]; }); }; const recordsFor = function recordsFor(kind: string): KindRecords { const records = KIND_RECORDS.get(kind); if (records === undefined) { throw new Error(unreadKind(kind)); } return records; }; const indexRecord = function indexRecord( index: Map>, fields: ReturnType, indexed: Indexed, scope: Scope, ): void { for (const [path, spec] of fields) { const held = index.get(spec.inverse) ?? new Map(); index.set(spec.inverse, held); for (const value of valuesAt(indexed.record, path)) { push(held, targetOf(spec.target, value, scope).ref, indexed.self); } } }; export const createReverseIndex = function createReverseIndex(context: GovlabContext, resolve: Resolver): ReverseIndex { const scope: Scope = { forces: forceNames(context), resolve }; const index = new Map>(); for (const [kind, schema] of ONTOLOGY_SCHEMA) { const fields = inverseFields(schema); const records = fields.length === 0 ? [] : recordsFor(kind)(context, resolve); for (const indexed of records) { indexRecord(index, fields, indexed, scope); } } return new Map(index); };