KnowledgeExportMojo.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.ExportRequest;
import network.ike.knowledge.spi.ExportResult;
import network.ike.knowledge.spi.IkeServiceBootstrap;
import network.ike.knowledge.spi.KnowledgeExporter;
import network.ike.knowledge.spi.ViewSpec;
import org.apache.maven.api.ProducedArtifact;
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 org.apache.maven.api.services.ProjectManager;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
/**
* Exports a ledger-form knowledge set as a full standalone protobuf artifact and
* attaches it under a declared classifier — by default {@code unreasoned-pb}, the
* settled name for a stated-only export the consumer classifies on load
* (IKE-Network/ike-issues#933). The ledger replay is reasoner-blind, so what this goal
* produces is always the whole unreasoned set; {@code changeset} is reserved for a
* genuine delta layered on a released base (declare it explicitly where that is truly
* what ships — delta authoring doctrine is IKE-Network/ike-issues#844), and the
* classified counterpart, {@code reasoned-pb}, comes from {@code ike:kb-assemble}'s
* export step.
*
* <p>The goal is a thin face over the knowledge-pipeline SPI
* ({@code network.ike.tooling:ike-knowledge-spi}): it builds a typed
* {@link ExportRequest}, and the {@link KnowledgeExporter} implementation is resolved by
* ServiceLoader from the <em>project's</em> runtime classpath inside a forked seam —
* declare {@code network.ike.knowledge:ike-knowledge-provider} at the use site (or rely
* on the parent POM's default wiring). This plugin never depends on a store engine.
*
* <p>Typical use — a {@code *-changeset} module depending on its {@code *-terms} ledger
* module plus the provider:
*
* <pre>{@code
* <plugin>
* <groupId>network.ike.tooling</groupId>
* <artifactId>ike-maven-plugin</artifactId>
* <executions>
* <execution>
* <goals><goal>knowledge-export</goal></goals>
* </execution>
* </executions>
* </plugin>
* }</pre>
*
* @since 234
*/
@Mojo(name = IkeGoal.NAME_KNOWLEDGE_EXPORT,
defaultPhase = "package")
public class KnowledgeExportMojo implements org.apache.maven.api.plugin.Mojo {
/** Creates this goal instance. */
public KnowledgeExportMojo() {}
@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 export file to write and attach. When unset, defaults to
* {@code <buildDirectory>/<artifactId>-<version>-<classifier>.zip}.
*/
@Parameter(property = "ike.knowledgeExport.outputFile")
String outputFile;
/**
* The classifier the export is attached under — and, when {@link #outputFile} is
* unset, the suffix of the default file name. {@code unreasoned-pb} is the settled
* default: a ledger replay is stated-only, so the artifact is the whole standalone
* unreasoned set. Declare {@code changeset} only for a genuine delta
* (IKE-Network/ike-issues#933).
*/
@Parameter(property = "ike.knowledgeExport.classifier", defaultValue = "unreasoned-pb")
String classifier;
/**
* Fully qualified name of the {@code KnowledgeSetSource} implementation to compose.
* Optional: when absent, exactly one implementation must be discoverable on the
* project's runtime classpath.
*/
@Parameter(property = "ike.knowledgeExport.sourceClass")
String sourceClass;
/**
* Optional koncepts YAML to extract from the same loaded store — the standard
* glossary definition source. Extracting here reuses the single change-set
* materialization rather than a parallel read of the set.
*/
@Parameter(property = "ike.knowledgeExport.konceptsYmlFile")
String konceptsYmlFile;
/**
* The view specification's dotted dimension keys (IKE-KNOWLEDGE-VIEW), stating only
* what differs from the implementation's defaults — for example
* {@code <view><stamp.allowedStates>Active</stamp.allowedStates></view>}.
*/
@Parameter
Map<String, String> view = Map.of();
/**
* The knowledge-service implementation's simple class name, when the classpath
* carries several.
*/
@Parameter(property = "ike.knowledgeExport.implementation")
String implementation;
/**
* Fork the seam (the default posture — the store lifecycle is JVM-global, and the
* fork survives anything the implementation does). Disable only for debugging:
* same classpath, same bootstrap, no process boundary.
*/
@Parameter(property = "ike.knowledgeExport.fork", defaultValue = "true")
boolean fork;
/**
* Extra child-JVM arguments for the forked seam (heap, flags). The child inherits
* {@code --enable-preview} from the Maven JVM automatically.
*/
@Parameter
List<String> forkJvmArguments = List.of();
/**
* The project's classes/resources directory, included on the seam classpath so the
* module's own {@code META-INF/services} registrations are discoverable.
*/
@Parameter(property = "ike.knowledgeExport.classesDirectory",
defaultValue = "${project.build.outputDirectory}")
String classesDirectory;
/**
* Attach the exported file to the project under {@link #classifier} (extension
* {@code zip}), so it installs and deploys with the module.
*/
@Parameter(property = "ike.knowledgeExport.attach", defaultValue = "true")
boolean attach;
/**
* Skip change-set export.
*/
@Parameter(property = "ike.knowledgeExport.skip", defaultValue = "false")
boolean skip;
/**
* The build directory hosting the seam's request/result files (under
* {@code ike-knowledge/}).
*/
@Parameter(property = "ike.knowledgeExport.buildDirectory",
defaultValue = "${project.build.directory}")
String buildDirectory;
/**
* Builds the typed export request, runs the {@link KnowledgeExporter} across the
* forked seam on the project's runtime classpath, and attaches the export under
* the declared classifier.
*
* @throws MojoException if the classpath cannot be resolved, the seam fails, or the
* output file was not produced
*/
@Override
public void execute() {
if (skip) {
getLog().info("ike:knowledge-export skipped (ike.knowledgeExport.skip=true)");
return;
}
String effectiveOutput = (outputFile == null || outputFile.isBlank())
? Path.of(buildDirectory).resolve(
project.getArtifactId() + "-" + project.getVersion() + "-" + classifier + ".zip").toString()
: outputFile;
ExportRequest request = new ExportRequest(
Path.of(effectiveOutput),
Optional.ofNullable(konceptsYmlFile).filter(s -> !s.isBlank()).map(Path::of),
Optional.ofNullable(sourceClass).filter(s -> !s.isBlank()),
ViewSpec.of(view));
Properties wire = request.toProperties();
if (implementation != null && !implementation.isBlank()) {
wire.setProperty(IkeServiceBootstrap.IMPLEMENTATION_KEY, implementation);
}
Properties resultWire = new KnowledgeServiceRunner(getLog()).run(
KnowledgeExporter.class.getName(), wire, seamClasspath(),
Path.of(buildDirectory, "ike-knowledge"),
fork, forkJvmArguments);
ExportResult result = ExportResult.fromProperties(resultWire);
Path produced = result.outputFile();
if (!Files.isRegularFile(produced)) {
throw new MojoException("Knowledge export produced no file at " + produced);
}
getLog().info("Knowledge set exported: " + produced.getFileName() + " — "
+ result.counts().total() + " entities (" + result.counts().concepts()
+ " concepts, " + result.counts().semantics() + " semantics, "
+ result.counts().patterns() + " patterns, " + result.counts().stamps()
+ " stamps)");
result.konceptsYmlFile().ifPresent(yml ->
getLog().info("Koncepts extracted: " + yml));
if (attach) {
ProducedArtifact artifact = session.createProducedArtifact(
project.getGroupId(), project.getArtifactId(), project.getVersion(),
classifier, "zip", "zip");
session.getService(ProjectManager.class).attachArtifact(project, artifact, produced);
getLog().info("Attached knowledge export " + produced.getFileName()
+ " (classifier: " + classifier + ", extension: zip)");
} else {
getLog().info("Knowledge set written (not attached): " + produced);
}
}
private List<Path> seamClasspath() {
// Resolution goes through the plugin's serialized entry point — Maven 4 rc-5's
// resolver cache is not thread-safe under -T (IKE-Network/ike-issues#901).
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;
}
}