ChronologyStoreAssembler.java
package network.ike.knowledge.provider;
import dev.ikm.tinkar.common.service.CachingService;
import dev.ikm.tinkar.common.service.EntityCountSummary;
import dev.ikm.tinkar.common.service.PluggableService;
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.common.service.TrackingCallable;
import dev.ikm.tinkar.coordinate.Calculators;
import dev.ikm.tinkar.coordinate.Coordinates;
import dev.ikm.tinkar.entity.EntityService;
import dev.ikm.tinkar.entity.aggregator.TemporalEntityAggregator;
import dev.ikm.tinkar.entity.export.ExportEntitiesToProtobufFile;
import dev.ikm.tinkar.entity.load.LoadEntitiesFromProtobufFile;
import dev.ikm.tinkar.reasoner.service.ClassifierResults;
import dev.ikm.tinkar.reasoner.service.ReasonerService;
import dev.ikm.tinkar.terms.TinkarTerm;
import network.ike.knowledge.spi.ArtifactInput;
import network.ike.knowledge.spi.AssembleRequest;
import network.ike.knowledge.spi.AssembleResult;
import network.ike.knowledge.spi.ClassificationSummary;
import network.ike.knowledge.spi.EntityCounts;
import network.ike.knowledge.spi.KnowledgeBaseAssembler;
import network.ike.knowledge.spi.LoadSummary;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* Assembles a knowledge base in a Spined Array store — the chronology-store
* {@link KnowledgeBaseAssembler}: an optional store-seed layer, ordered entity loads
* inside the bulk-load bracket, the stated navigation derived from the loaded stated
* definitions ({@link StatedNavigationDeriver}), and, by default, a full classification
* whose inferred axiom and navigation semantics make the assembled KB navigable the
* moment a browser opens it. Assembling under a data-source directory (the install
* directory) makes the result directly selectable.
*
* <p>The view specification's resolution into coordinate records is the resolver
* increment (IKE-Network/ike-issues#849); classification currently runs under the
* engine's default view, matching the headless precedent. The effective view is
* reported beside the store root.
*/
public final class ChronologyStoreAssembler implements KnowledgeBaseAssembler {
private static final String DEFAULT_REASONER = "ElkSnomedReasonerService";
/** Creates the provider (ServiceLoader requirement). */
public ChronologyStoreAssembler() {
}
@Override
public AssembleResult assemble(AssembleRequest request) {
try {
Path storeRoot = request.storeRoot();
if (request.cleanStart() && Files.exists(storeRoot)) {
deleteRecursively(storeRoot);
}
Files.createDirectories(storeRoot);
List<ArtifactInput> inputs = request.inputs();
int firstLoad = 0;
if (inputs.getFirst().role() == ArtifactInput.Role.STORE_SEED) {
unzipInto(inputs.getFirst().path(), storeRoot);
firstLoad = 1;
}
List<LoadSummary> loads = new ArrayList<>();
Optional<ClassificationSummary> classification;
Optional<Path> reasonedPb;
CachingService.clearAll();
ServiceProperties.set(ServiceKeys.DATA_STORE_ROOT, storeRoot.toFile());
PrimitiveData.selectControllerByName("Open SpinedArrayStore");
PrimitiveData.start();
try {
EntityService.get().beginLoadPhase();
try {
for (int i = firstLoad; i < inputs.size(); i++) {
ArtifactInput input = inputs.get(i);
EntityCountSummary summary =
new LoadEntitiesFromProtobufFile(input.path().toFile()).compute();
loads.add(new LoadSummary(input.path().getFileName().toString(),
new EntityCounts(summary.conceptCount(), summary.semanticCount(),
summary.patternCount(), summary.stampCount())));
}
} finally {
EntityService.get().endLoadPhase();
}
// The loaded files carry stated navigation as it stood when they were
// written; a ledger that re-declares definitions leaves it stale, and the
// reasoner writes inferred navigation only (IKE-Network/ike-issues#1123).
StatedNavigationDeriver.derive(Coordinates.View.DefaultView());
classification = request.classify()
? Optional.of(classify(request.reasonerService().orElse(DEFAULT_REASONER)))
: Optional.empty();
// The reasoned-pb export must see the classified store while it is
// still open: full standalone export, inferred semantics included
// (IKE-Network/ike-issues#933).
reasonedPb = request.reasonedPbFile().isPresent()
? Optional.of(exportReasonedPb(request.reasonedPbFile().get()))
: Optional.empty();
} finally {
PrimitiveData.stop();
}
Path report = writeEffectiveViewReport(request);
if (request.installDirectory().isPresent()) {
Path install = request.installDirectory().get();
if (Files.exists(install)) {
deleteRecursively(install);
}
copyRecursively(storeRoot, install);
}
return new AssembleResult(loads, classification, Optional.of(report), reasonedPb);
} catch (IOException e) {
throw new UncheckedIOException("Knowledge-base assembly failed", e);
}
}
/**
* Exports the open, classified store as a full standalone reasoned protobuf — the
* {@code reasoned-pb} classifier form: every entity across all time, inferred
* semantics included, via the same temporal-aggregator export Komet's own export
* controller drives (IKE-Network/ike-issues#933). All time starts at the earliest
* representable instant, not at the epoch: a loaded file's versions stamped before
* the epoch are part of the store and belong in its export
* (IKE-Network/ike-issues#1123).
*
* @param file the export file to write
* @return the written file
* @throws IOException if the parent directory cannot be created
*/
static Path exportReasonedPb(Path file) throws IOException {
Path parent = file.toAbsolutePath().getParent();
if (parent != null) {
Files.createDirectories(parent);
}
new ExportEntitiesToProtobufFile(file.toFile(),
new TemporalEntityAggregator(Long.MIN_VALUE, Long.MAX_VALUE)).compute();
return file;
}
/**
* Runs the full classification sequence and returns its summary. Derived semantics
* (inferred axioms and inferred navigation) are written into the open store.
*/
private static ClassificationSummary classify(String reasonerName) {
List<ReasonerService> found = new ArrayList<>();
PluggableService.load(ReasonerService.class).forEach(found::add);
ReasonerService reasoner = found.stream()
.filter(candidate -> candidate.getClass().getSimpleName().equals(reasonerName))
.findFirst()
.orElseThrow(() -> new IllegalStateException("Reasoner service \"" + reasonerName
+ "\" is not on the classpath — found "
+ found.stream().map(candidate -> candidate.getClass().getSimpleName()).toList()));
long started = System.currentTimeMillis();
TrackingCallable<Void> progress = new TrackingCallable<>() {
@Override
protected Void compute() {
return null;
}
};
try {
reasoner.init(Calculators.View.Default(),
TinkarTerm.EL_PLUS_PLUS_STATED_AXIOMS_PATTERN,
TinkarTerm.EL_PLUS_PLUS_INFERRED_AXIOMS_PATTERN);
reasoner.extractData(progress);
reasoner.loadData(progress);
reasoner.computeInferences(progress);
reasoner.buildNecessaryNormalForm(progress);
ClassifierResults results = reasoner.writeInferredResults(progress);
return new ClassificationSummary(reasonerName,
results.getClassificationConceptSet().size(),
results.getConceptsWithInferredChanges().size(),
results.getConceptsWithNavigationChanges().size(),
System.currentTimeMillis() - started);
} catch (Exception e) {
throw new IllegalStateException("Classification failed in " + reasonerName, e);
}
}
/**
* Reports the view the assembly ran under — the stated dimensions plus the marker
* that unresolved dimensions took the engine's default view. Replaced by the fully
* resolved report when the resolver increment lands.
*/
private static Path writeEffectiveViewReport(AssembleRequest request) throws IOException {
Path report = request.storeRoot().toAbsolutePath().getParent()
.resolve(request.storeRoot().getFileName() + "-effective-view.properties");
Properties properties = request.view().toProperties("view.");
properties.setProperty("defaults", "engine default view (resolver: ike-issues#849)");
try (var out = Files.newOutputStream(report)) {
properties.store(out, "Effective view of the knowledge-base assembly");
}
return report;
}
static void unzipInto(Path zip, Path targetRoot) throws IOException {
try (ZipInputStream in = new ZipInputStream(Files.newInputStream(zip))) {
ZipEntry entry = in.getNextEntry();
while (entry != null) {
Path target = targetRoot.resolve(entry.getName()).normalize();
if (!target.startsWith(targetRoot)) {
throw new IOException("Store seed entry escapes the store root: " + entry.getName());
}
if (entry.isDirectory()) {
Files.createDirectories(target);
} else {
Files.createDirectories(target.getParent());
Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING);
}
entry = in.getNextEntry();
}
}
}
static void deleteRecursively(Path root) throws IOException {
Files.walkFileTree(root, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}
private static void copyRecursively(Path source, Path target) throws IOException {
Files.walkFileTree(source, new SimpleFileVisitor<>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Files.createDirectories(target.resolve(source.relativize(dir).toString()));
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.copy(file, target.resolve(source.relativize(file).toString()),
StandardCopyOption.REPLACE_EXISTING);
return FileVisitResult.CONTINUE;
}
});
}
}