KnowledgeVerifyMojo.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.plugin;
import network.ike.knowledge.spi.ArtifactInput;
import network.ike.knowledge.spi.Finding;
import network.ike.knowledge.spi.IkeServiceBootstrap;
import network.ike.knowledge.spi.KnowledgeVerifier;
import network.ike.knowledge.spi.VerifyReport;
import network.ike.knowledge.spi.VerifyRequest;
import network.ike.knowledge.spi.ViewSpec;
import org.apache.maven.api.Project;
import org.apache.maven.api.Session;
import org.apache.maven.api.di.Inject;
import org.apache.maven.api.plugin.MojoException;
import org.apache.maven.api.plugin.annotations.Mojo;
import org.apache.maven.api.plugin.annotations.Parameter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
/**
* Verifies a knowledge artifact in a pristine store — the round-trip gate
* (IKE-Network/ike-issues#951): the artifact must load into a fresh store exactly as a
* consumer's import would load it, manifest and count-equality checks intact, and
* (with {@code FIT_REFERENCES}) every reference must resolve within the artifact plus
* its declared base. A defect surfaces here, in the producing build, instead of in a
* consumer's import dialog.
* <p>The goal is a thin face over the knowledge-pipeline SPI: it builds a typed
* {@link VerifyRequest}, and the {@link KnowledgeVerifier} implementation is resolved
* by ServiceLoader from the <em>project's</em> runtime classpath inside a forked seam.
* The build fails when the report carries any {@code ERROR} finding.
*/
@Mojo(name = IkeGoal.NAME_KNOWLEDGE_VERIFY,
defaultPhase = "verify")
public class KnowledgeVerifyMojo implements org.apache.maven.api.plugin.Mojo {
/** Creates this goal instance. */
public KnowledgeVerifyMojo() {}
@Inject
private org.apache.maven.api.plugin.Log log;
/**
* Access the Maven logger.
*
* @return the logger
*/
protected org.apache.maven.api.plugin.Log getLog() {
return log;
}
@Inject
private Session session;
@Inject
private Project project;
/**
* The artifact under verification — a protobuf zip produced earlier in this build.
*/
@Parameter(property = "ike.knowledgeVerify.artifact", required = true)
String artifact;
/**
* The base the artifact must fit, in load order — each entry
* {@code "ROLE spec"} where ROLE is STORE_SEED, PB, or CHANGESET and spec is a
* Maven coordinate ({@code g:a[:extension[:classifier]]:v}) or a file path. Empty
* for a self-contained (standalone) artifact.
*/
@Parameter
List<String> baseInputs = List.of();
/**
* The checks to run. Defaults to {@code PRESENCE} — the consumer-import round
* trip; add {@code FIT_REFERENCES} for the reference-resolution sweep.
*/
@Parameter
List<String> checks = List.of("PRESENCE");
/**
* The view specification's dotted dimension keys, stating only what differs from
* the implementation's defaults.
*/
@Parameter
Map<String, String> view = Map.of();
/**
* The knowledge-service implementation's simple class name, when the classpath
* carries several.
*/
@Parameter(property = "ike.knowledgeVerify.implementation")
String implementation;
/**
* Fork the seam (the default posture). Disable only for debugging.
*/
@Parameter(property = "ike.knowledgeVerify.fork", defaultValue = "true")
boolean fork;
/**
* Extra child-JVM arguments for the forked seam (heap, flags).
*/
@Parameter
List<String> forkJvmArguments = List.of();
/**
* The project's classes/resources directory, included on the seam classpath.
*/
@Parameter(property = "ike.knowledgeVerify.classesDirectory",
defaultValue = "${project.build.outputDirectory}")
String classesDirectory;
/**
* The build directory hosting the seam's request/result files.
*/
@Parameter(property = "ike.knowledgeVerify.buildDirectory",
defaultValue = "${project.build.directory}")
String buildDirectory;
/**
* Skip knowledge verification.
*/
@Parameter(property = "ike.knowledgeVerify.skip", defaultValue = "false")
boolean skip;
/**
* Builds the typed verification request, runs the {@link KnowledgeVerifier} across
* the forked seam, logs every finding, and fails the build on any ERROR.
*
* @throws MojoException if the artifact is missing, an input cannot be parsed or
* resolved, the seam fails, or the report carries an ERROR
*/
@Override
public void execute() {
if (skip) {
getLog().info("ike:knowledge-verify skipped (ike.knowledgeVerify.skip=true)");
return;
}
Path artifactPath = Path.of(artifact);
if (!Files.isRegularFile(artifactPath)) {
throw new MojoException("Artifact under verification does not exist: " + artifactPath
+ " — run the producing goal first");
}
Set<VerifyRequest.Check> requestedChecks = EnumSet.noneOf(VerifyRequest.Check.class);
for (String check : checks) {
try {
requestedChecks.add(VerifyRequest.Check.valueOf(check.strip()));
} catch (IllegalArgumentException e) {
throw new MojoException("Unknown check \"" + check + "\" — use PRESENCE,"
+ " FIT_REFERENCES, IDENTITY, DELTA, or CLASSIFICATION_FIT", e);
}
}
VerifyRequest request = new VerifyRequest(artifactPath, resolveInputs(), requestedChecks,
Optional.empty(), ViewSpec.of(view));
Properties wire = request.toProperties();
if (implementation != null && !implementation.isBlank()) {
wire.setProperty(IkeServiceBootstrap.IMPLEMENTATION_KEY, implementation);
}
Properties resultWire = new KnowledgeServiceRunner(getLog()).run(
KnowledgeVerifier.class.getName(), wire, seamClasspath(),
Path.of(buildDirectory, "ike-knowledge"), fork, forkJvmArguments);
VerifyReport report = VerifyReport.fromProperties(resultWire);
long errors = 0;
for (Finding finding : report.findings()) {
String line = finding.check() + " " + finding.component() + " — " + finding.message();
switch (finding.severity()) {
case ERROR -> {
errors++;
getLog().error(line);
}
case WARNING -> getLog().warn(line);
default -> getLog().info(line);
}
}
if (errors > 0) {
throw new MojoException("Knowledge verification failed: " + errors + " error finding"
+ (errors == 1 ? "" : "s") + " for " + artifactPath.getFileName()
+ " — see the findings above (IKE-Network/ike-issues#951)");
}
getLog().info("Knowledge verification passed: " + artifactPath.getFileName()
+ " (" + report.findings().size() + " finding(s), none blocking)");
}
private List<ArtifactInput> resolveInputs() {
List<ArtifactInput> resolved = new ArrayList<>();
for (String entry : baseInputs) {
String stripped = entry.strip();
int space = stripped.indexOf(' ');
if (space <= 0) {
throw new MojoException("Base input entry needs \"ROLE spec\": " + entry);
}
ArtifactInput.Role role;
try {
role = ArtifactInput.Role.valueOf(stripped.substring(0, space));
} catch (IllegalArgumentException e) {
throw new MojoException("Unknown input role in \"" + entry
+ "\" — use STORE_SEED, PB, or CHANGESET", e);
}
String spec = stripped.substring(space + 1).strip();
resolved.add(new ArtifactInput(role, resolveSpec(spec)));
}
return resolved;
}
/**
* A spec with two or more colons is a Maven coordinate
* ({@code g:a[:extension[:classifier]]:v}); anything else is a file path.
*/
private Path resolveSpec(String spec) {
if (spec.chars().filter(c -> c == ':').count() >= 2) {
try {
return session.resolveArtifact(session.createArtifactCoordinates(spec)).getPath();
} catch (Exception e) {
throw new MojoException("Cannot resolve knowledge input " + spec, e);
}
}
Path file = Path.of(spec);
if (!Files.isRegularFile(file)) {
throw new MojoException("Knowledge input file does not exist: " + file);
}
return file;
}
private List<Path> seamClasspath() {
List<Path> classpath = new ArrayList<>();
Path classesDir = Path.of(classesDirectory);
if (Files.isDirectory(classesDir)) {
classpath.add(classesDir);
}
classpath.addAll(RuntimeClasspathResolver.mainRuntimePaths(session, project));
return classpath;
}
}