ChronologyStoreExporter.java

package network.ike.knowledge.provider;

import dev.ikm.tinkar.common.id.PublicIds;
import dev.ikm.tinkar.common.service.CachingService;
import dev.ikm.tinkar.common.service.EntityCountSummary;
import dev.ikm.tinkar.common.service.PrimitiveData;
import dev.ikm.tinkar.common.service.ServiceKeys;
import dev.ikm.tinkar.common.service.ServiceProperties;
import dev.ikm.tinkar.entity.EntityService;
import dev.ikm.tinkar.entity.aggregator.TemporalEntityAggregator;
import dev.ikm.tinkar.entity.builder.KnowledgeSet;
import dev.ikm.tinkar.entity.builder.KnowledgeSetSource;
import dev.ikm.tinkar.entity.builder.KonceptExtractor;
import dev.ikm.tinkar.entity.export.ExportEntitiesToProtobufFile;
import network.ike.knowledge.spi.EntityCounts;
import network.ike.knowledge.spi.ExportRequest;
import network.ike.knowledge.spi.ExportResult;
import network.ike.knowledge.spi.KnowledgeExporter;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.UUID;

/**
 * Exports a knowledge set's change-set artifact from its ledger source — the
 * chronology-store {@link KnowledgeExporter}: discovers the {@link KnowledgeSetSource},
 * replays the composed session into a fresh in-memory store, exports the store by real
 * time (which excludes the store's startup sentinel stamp), and optionally extracts the
 * koncept definitions from the same materialized store — one materialization, both
 * artifacts.
 *
 * <p>The view specification's export dimensions are honored by the resolver increment
 * (IKE-Network/ike-issues#849); this provider currently exports the whole set, matching
 * the behavior the deprecated seam main established.
 */
public final class ChronologyStoreExporter implements KnowledgeExporter {

    /**
     * The identity of rich-surface-starter-knowledge's "Prose element pattern
     * (RichSurfaceTerms)" — the pattern every curated {@code koncept-narrative}
     * semantic is written against (see {@code NarrativeContentSet} in
     * ike-starter-set). Hardcoded here, not in tinkar-core's generic
     * {@link KonceptExtractor}: this is an IKE-ecosystem-specific convention,
     * so the reference belongs at this ecosystem-specific layer
     * (IKE-Network/ike-issues#879).
     */
    private static final UUID NARRATIVE_PATTERN_UUID =
            UUID.fromString("89b831a1-e773-5f83-87a6-2cfc8e107fb0");

    /** Creates the provider (ServiceLoader requirement). */
    public ChronologyStoreExporter() {
    }

    @Override
    public ExportResult export(ExportRequest request) {
        try {
            Files.createDirectories(request.outputFile().toAbsolutePath().getParent());
            Path storeRoot = Files.createTempDirectory("ike-knowledge-export");

            CachingService.clearAll();
            ServiceProperties.set(ServiceKeys.DATA_STORE_ROOT, storeRoot.toFile());
            PrimitiveData.selectControllerByName("Load Ephemeral Store");
            PrimitiveData.start();
            try {
                KnowledgeSet knowledgeSet = selectSource(request.sourceClass()).compose();
                knowledgeSet.write();

                EntityCountSummary summary = new ExportEntitiesToProtobufFile(
                        request.outputFile().toFile(),
                        new TemporalEntityAggregator(0L, Long.MAX_VALUE)).compute();

                Optional<Path> konceptsYml = request.konceptsYmlFile();
                if (konceptsYml.isPresent()) {
                    Files.createDirectories(konceptsYml.get().toAbsolutePath().getParent());
                    int narrativePatternNid = EntityService.get()
                            .nidForPublicId(PublicIds.of(NARRATIVE_PATTERN_UUID));
                    Files.writeString(konceptsYml.get(), KonceptExtractor.extractYaml(narrativePatternNid));
                }

                return new ExportResult(request.outputFile(),
                        new EntityCounts(summary.conceptCount(), summary.semanticCount(),
                                summary.patternCount(), summary.stampCount()),
                        konceptsYml);
            } finally {
                PrimitiveData.stop();
            }
        } catch (IOException e) {
            throw new UncheckedIOException("Change-set export failed", e);
        }
    }

    /**
     * Selects the ledger source: the named class when given, otherwise exactly one
     * discoverable {@link KnowledgeSetSource}.
     *
     * @param sourceClassName the source class name, when several are discoverable
     * @return the selected source
     * @throws IllegalStateException if no source or several sources are discoverable
     *                               without a selection, or the named class cannot be
     *                               loaded or instantiated
     */
    private static KnowledgeSetSource selectSource(Optional<String> sourceClassName) {
        if (sourceClassName.isPresent()) {
            try {
                return (KnowledgeSetSource) Class
                        .forName(sourceClassName.get(), true,
                                Thread.currentThread().getContextClassLoader())
                        .getDeclaredConstructor().newInstance();
            } catch (ReflectiveOperationException | ClassCastException e) {
                throw new IllegalStateException("Cannot instantiate the named ledger source "
                        + sourceClassName.get(), e);
            }
        }
        List<KnowledgeSetSource> found = new ArrayList<>();
        ServiceLoader.load(KnowledgeSetSource.class, Thread.currentThread().getContextClassLoader())
                .forEach(found::add);
        if (found.size() != 1) {
            throw new IllegalStateException("Expected exactly one KnowledgeSetSource on the classpath, found "
                    + found.size() + (found.isEmpty() ? "" : ": "
                    + found.stream().map(source -> source.getClass().getName()).toList())
                    + " — name the source class in the export request to select one");
        }
        return found.getFirst();
    }
}