ChronologyStoreVerifier.java

/*
 * Copyright © 2026 IKE Network (support@ike.network)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
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.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.DefaultEntityAggregator;
import dev.ikm.tinkar.entity.load.LoadEntitiesFromProtobufFile;
import dev.ikm.tinkar.entity.transform.EntityToTinkarSchemaTransformer;
import network.ike.knowledge.spi.ArtifactInput;
import network.ike.knowledge.spi.Finding;
import network.ike.knowledge.spi.KnowledgeVerifier;
import network.ike.knowledge.spi.VerifyReport;
import network.ike.knowledge.spi.VerifyRequest;

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;

/**
 * The chronology-store {@link KnowledgeVerifier}: assembles a pristine throwaway store
 * from the declared base plus the artifact under verification and runs the requested
 * checks against it — never against the store that produced the artifact
 * (IKE-Network/ike-issues#852, #951).
 *
 * <p>Implemented checks:
 * <ul>
 *   <li>{@link VerifyRequest.Check#PRESENCE} — the artifact loads into the pristine
 *       store with its own manifest verification and count-equality check intact: the
 *       exact gate a consumer's import runs, moved into the build.</li>
 *   <li>{@link VerifyRequest.Check#FIT_REFERENCES} — every loaded entity survives the
 *       protobuf transform, which resolves every reference it serializes: a reference
 *       to a component with no entity and no resolvable public id is reported with the
 *       carrying entity named.</li>
 * </ul>
 * Any other requested check fails closed with an {@link Finding.Severity#ERROR}: a
 * check this provider cannot run must never read as a check that passed.
 */
public final class ChronologyStoreVerifier implements KnowledgeVerifier {

    /** The cap on per-entity findings for one check, so a systemic defect reports as a
     * bounded list plus a remainder count rather than an unbounded flood. */
    private static final int FINDING_CAP = 50;

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

    @Override
    public VerifyReport verify(VerifyRequest request) {
        List<Finding> findings = new ArrayList<>();
        try {
            Path storeRoot = Files.createTempDirectory("ike-verify");
            try {
                CachingService.clearAll();
                ServiceProperties.set(ServiceKeys.DATA_STORE_ROOT, storeRoot.toFile());
                PrimitiveData.selectControllerByName("Open SpinedArrayStore");
                PrimitiveData.start();
                try {
                    loadBase(request.baseInputs(), storeRoot);
                    boolean loaded = checkPresence(request, findings);
                    if (loaded && request.checks().contains(VerifyRequest.Check.FIT_REFERENCES)) {
                        checkFitReferences(findings);
                    }
                    for (VerifyRequest.Check check : request.checks()) {
                        if (check != VerifyRequest.Check.PRESENCE
                                && check != VerifyRequest.Check.FIT_REFERENCES) {
                            findings.add(new Finding(Finding.Severity.ERROR, check,
                                    request.artifact().getFileName().toString(),
                                    "Check not implemented by " + getClass().getSimpleName()
                                            + " — failing closed rather than reading as passed"));
                        }
                    }
                } finally {
                    PrimitiveData.stop();
                }
            } finally {
                ChronologyStoreAssembler.deleteRecursively(storeRoot);
            }
        } catch (IOException e) {
            throw new UncheckedIOException("Knowledge verification failed to manage its store", e);
        }
        return new VerifyReport(findings);
    }

    /**
     * Loads the declared base into the pristine store, in order: an optional store
     * seed first, then entity loads inside the bulk-load bracket.
     *
     * @param baseInputs the base inputs, in load order
     * @param storeRoot  the pristine store root
     * @throws IOException if a store seed cannot be unzipped
     */
    private static void loadBase(List<ArtifactInput> baseInputs, Path storeRoot) throws IOException {
        if (baseInputs.isEmpty()) {
            return;
        }
        int firstLoad = 0;
        if (baseInputs.getFirst().role() == ArtifactInput.Role.STORE_SEED) {
            ChronologyStoreAssembler.unzipInto(baseInputs.getFirst().path(), storeRoot);
            firstLoad = 1;
        }
        EntityService.get().beginLoadPhase();
        try {
            for (int i = firstLoad; i < baseInputs.size(); i++) {
                new LoadEntitiesFromProtobufFile(baseInputs.get(i).path().toFile()).compute();
            }
        } finally {
            EntityService.get().endLoadPhase();
        }
    }

    /**
     * Loads the artifact under verification with the loader's own manifest and
     * count-equality gates intact.
     *
     * @param request  the verification request
     * @param findings the findings to append to
     * @return whether the artifact loaded, so dependent checks know to run
     */
    private static boolean checkPresence(VerifyRequest request, List<Finding> findings) {
        String artifactName = request.artifact().getFileName().toString();
        EntityService.get().beginLoadPhase();
        try {
            EntityCountSummary loaded =
                    new LoadEntitiesFromProtobufFile(request.artifact().toFile()).compute();
            findings.add(new Finding(Finding.Severity.INFO, VerifyRequest.Check.PRESENCE,
                    artifactName,
                    "Loaded " + loaded.getTotalCount() + " entities ("
                            + loaded.conceptCount() + " concepts, " + loaded.semanticCount()
                            + " semantics, " + loaded.patternCount() + " patterns, "
                            + loaded.stampCount() + " stamps); manifest counts exact"));
            return true;
        } catch (RuntimeException e) {
            findings.add(new Finding(Finding.Severity.ERROR, VerifyRequest.Check.PRESENCE,
                    artifactName, "Artifact failed to load into a pristine store: " + e));
            return false;
        } finally {
            EntityService.get().endLoadPhase();
        }
    }

    /**
     * Sweeps every entity in the assembled store through the protobuf transform — the
     * serialization that resolves every reference — reporting each entity whose
     * references do not fit, capped with a remainder count.
     *
     * @param findings the findings to append to
     */
    private static void checkFitReferences(List<Finding> findings) {
        EntityToTinkarSchemaTransformer transformer = EntityToTinkarSchemaTransformer.getInstance();
        List<Finding> failures = new ArrayList<>();
        long[] overflow = {0};
        new DefaultEntityAggregator().aggregateEntities(entity -> {
            try {
                transformer.transform(entity);
            } catch (RuntimeException e) {
                if (failures.size() < FINDING_CAP) {
                    failures.add(new Finding(Finding.Severity.ERROR,
                            VerifyRequest.Check.FIT_REFERENCES,
                            entity.getClass().getSimpleName() + " " + entity.publicId(),
                            "Reference does not resolve within the artifact plus declared base: " + e));
                } else {
                    overflow[0]++;
                }
            }
        });
        findings.addAll(failures);
        if (overflow[0] > 0) {
            findings.add(new Finding(Finding.Severity.ERROR, VerifyRequest.Check.FIT_REFERENCES,
                    "(aggregate)", overflow[0] + " further entities with unresolvable references"
                    + " beyond the first " + FINDING_CAP));
        }
    }
}