diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 2341636..30fb664 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -215,16 +215,31 @@ jobs:
with:
java-version: '21'
distribution: temurin
+ # spotless-maven-plugin / spotbugs-maven-plugin are declared only in each child module's own
+ # / (not the pom-packaging reactor root), so the bare prefix goals
+ # (`spotless:check`, `spotbugs:check`) fail with "No plugin found for prefix" — Maven resolves
+ # a short prefix against the INVOKING project (the root aggregator here), not the reactor
+ # members, regardless of which modules actually end up executing. Using the fully-qualified
+ # groupId:artifactId:goal form bypasses prefix resolution entirely (plugin *version* still
+ # resolves correctly via each module's inherited pluginManagement pin); -pl restricts
+ # execution to the three modules that actually declare the plugin, skipping the parent pom
+ # and the source-less relocation stub, neither of which registers it anywhere.
- name: Spotless check (fail fast on format violations)
- run: mvn -B --no-transfer-progress spotless:check
+ run: mvn -B --no-transfer-progress -pl srcmorph,srcmorph-cli,srcmorph-maven-plugin com.diffplug.spotless:spotless-maven-plugin:check
- name: SpotBugs check (fail fast on static-analysis findings)
- run: mvn -B --no-transfer-progress -DskipTests -Denforcer.skip=true compile spotbugs:check
+ run: mvn -B --no-transfer-progress -DskipTests -Denforcer.skip=true -pl srcmorph,srcmorph-cli,srcmorph-maven-plugin compile com.github.spotbugs:spotbugs-maven-plugin:check
- name: Print internal package dependency graph (jdeps, informational)
continue-on-error: true
run: |
mvn -B --no-transfer-progress -DskipTests -Denforcer.skip=true compile
- echo "=== internal package dependency graph (jdeps, bytecode) ==="
- jdeps -verbose:package llamacpp-ai-index-maven-plugin/target/classes | grep 'net.ladenthin.maven.llamacpp.aiindex' || true
+ # Three reactor modules each ship their own module-info.java / target/classes tree
+ # (srcmorph, srcmorph-cli, srcmorph-maven-plugin) — print each separately.
+ echo "=== internal package dependency graph (jdeps, bytecode) — srcmorph ==="
+ jdeps -verbose:package srcmorph/target/classes | grep 'net.ladenthin.srcmorph' || true
+ echo "=== internal package dependency graph (jdeps, bytecode) — srcmorph-cli ==="
+ jdeps -verbose:package srcmorph-cli/target/classes | grep 'net.ladenthin.srcmorph' || true
+ echo "=== internal package dependency graph (jdeps, bytecode) — srcmorph-maven-plugin ==="
+ jdeps -verbose:package srcmorph-maven-plugin/target/classes | grep 'net.ladenthin.maven.srcmorph' || true
build:
name: Build
@@ -240,7 +255,17 @@ jobs:
- name: Build
run: mvn --batch-mode --no-transfer-progress -DskipTests package
- uses: actions/upload-artifact@v7
- with: { name: plugin-jars, path: llamacpp-ai-index-maven-plugin/target/*.jar }
+ with:
+ name: plugin-jars
+ # All three reactor modules now produce their own jars: srcmorph (core library),
+ # srcmorph-cli (includes the jar-with-dependencies fat jar — the CLI's deliverable),
+ # and srcmorph-maven-plugin (the Maven plugin, renamed from llamacpp-ai-index-maven-plugin;
+ # the retired coordinates now live only in the pom-only relocation-stub module, which
+ # produces no jar).
+ path: |
+ srcmorph/target/*.jar
+ srcmorph-cli/target/*.jar
+ srcmorph-maven-plugin/target/*.jar
test:
name: Test
@@ -256,11 +281,20 @@ jobs:
- name: Memory before tests
run: free -h
- name: Test
+ # Reactor-wide (no -pl): runs every module's tests. The `jcstress` profile is only
+ # declared in srcmorph-maven-plugin/pom.xml, so it activates there and is a
+ # silent no-op for srcmorph/srcmorph-cli (neither declares a profile with that id).
run: mvn -e --batch-mode --no-transfer-progress -P jcstress verify
- uses: actions/upload-artifact@v7
with:
name: jacoco-report
- path: llamacpp-ai-index-maven-plugin/target/site/jacoco/jacoco.xml
+ # srcmorph carries the bulk of the test suite (and the only PIT gate); its report is
+ # the one forwarded to Coveralls/Codecov in the `report` job below, matching the
+ # single-primary-module precedent already used by the sibling java-llama.cpp reactor
+ # (which reports only its `llama` core module, not an aggregate across all modules).
+ # srcmorph-cli / srcmorph-maven-plugin jacoco reports are not currently
+ # aggregated or uploaded (documented gap, see TODO.md).
+ path: srcmorph/target/site/jacoco/jacoco.xml
if-no-files-found: ignore
- name: Memory after tests
if: always()
@@ -270,21 +304,29 @@ jobs:
uses: actions/upload-artifact@v7
with:
name: crash-dumps-test
+ # Repo-wide glob: a forked surefire JVM crash can happen in any of the three module
+ # working directories (srcmorph / srcmorph-cli / srcmorph-maven-plugin), each
+ # with its own target/ tree.
path: |
- ${{ github.workspace }}/llamacpp-ai-index-maven-plugin/hs_err_pid*.log
- ${{ github.workspace }}/llamacpp-ai-index-maven-plugin/*.hprof
- ${{ github.workspace }}/llamacpp-ai-index-maven-plugin/target/surefire-reports/*.dump
- ${{ github.workspace }}/llamacpp-ai-index-maven-plugin/target/surefire-reports/*.dumpstream
- ${{ github.workspace }}/llamacpp-ai-index-maven-plugin/target/surefire-reports/*.txt
- ${{ github.workspace }}/llamacpp-ai-index-maven-plugin/target/surefire-reports/TEST-*.xml
+ ${{ github.workspace }}/**/hs_err_pid*.log
+ ${{ github.workspace }}/**/*.hprof
+ ${{ github.workspace }}/**/target/surefire-reports/*.dump
+ ${{ github.workspace }}/**/target/surefire-reports/*.dumpstream
+ ${{ github.workspace }}/**/target/surefire-reports/*.txt
+ ${{ github.workspace }}/**/target/surefire-reports/TEST-*.xml
if-no-files-found: ignore
- name: Run PIT mutation tests
- run: mvn --batch-mode --no-transfer-progress test-compile org.pitest:pitest-maven:mutationCoverage
+ # Scoped to srcmorph: it is the only reactor module with a pitest-maven execution
+ # (srcmorph-cli and srcmorph-maven-plugin each document, in their own pom, why
+ # they have no PIT gate yet — see TODO.md). `-am` builds srcmorph's own reactor
+ # dependencies (none today) before running; without `-pl` this goal would otherwise be
+ # invoked (harmlessly, but pointlessly) against every reactor module.
+ run: mvn --batch-mode --no-transfer-progress -pl srcmorph -am test-compile org.pitest:pitest-maven:mutationCoverage
- name: Extract PIT survivors
if: always()
run: |
echo "=== PIT Survived Mutations ==="
- for html_file in $(find llamacpp-ai-index-maven-plugin/target/pit-reports -name "*.html" -type f 2>/dev/null | sort); do
+ for html_file in $(find srcmorph/target/pit-reports -name "*.html" -type f 2>/dev/null | sort); do
if grep -q "SURVIVED" "$html_file"; then
echo "Found survivors in $html_file:"
grep -B 2 -A 3 "SURVIVED" "$html_file"
@@ -293,7 +335,7 @@ jobs:
done
- uses: actions/upload-artifact@v7
if: always()
- with: { name: pit-reports, path: llamacpp-ai-index-maven-plugin/target/pit-reports/ }
+ with: { name: pit-reports, path: srcmorph/target/pit-reports/ }
vmlens:
name: Test (vmlens interleavings)
@@ -304,14 +346,25 @@ jobs:
- uses: actions/setup-java@v5
with: { java-version: '21', distribution: temurin, cache: maven }
- name: Test under vmlens (one class — staged scope)
+ # VmlensInterleavingSmokeTest and the `vmlens` profile both live in
+ # srcmorph-maven-plugin/pom.xml (they were not relocated during the core
+ # extraction — see CLAUDE.md) — scope directly to that module rather than the whole
+ # reactor. `-am` is required (not just an optimization): srcmorph is a reactor-local
+ # dependency, never published, so without `-am` this module's compile would fail trying
+ # to resolve net.ladenthin:srcmorph:1.1.0-SNAPSHOT from a remote repo. That in turn means
+ # the `test` phase also runs for srcmorph, where -Dtest=VmlensInterleavingSmokeTest
+ # matches no class — `-DfailIfNoTests=false` does NOT suppress that (it only covers "no
+ # tests at all"); the actual guard for "the -Dtest pattern matched nothing" is
+ # `-Dsurefire.failIfNoSpecifiedTests=false` (verified locally: omitting it fails the
+ # build on srcmorph with "No tests matching pattern ... were executed!").
run: >-
- mvn --batch-mode --no-transfer-progress -Pvmlens test
- -Dtest=VmlensInterleavingSmokeTest -DfailIfNoTests=false
+ mvn --batch-mode --no-transfer-progress -pl srcmorph-maven-plugin -am -Pvmlens test
+ -Dtest=VmlensInterleavingSmokeTest -DfailIfNoTests=false -Dsurefire.failIfNoSpecifiedTests=false
- uses: actions/upload-artifact@v7
if: always()
with:
name: vmlens-report
- path: llamacpp-ai-index-maven-plugin/target/vmlens-report/
+ path: srcmorph-maven-plugin/target/vmlens-report/
if-no-files-found: ignore
report:
@@ -324,22 +377,26 @@ jobs:
- uses: actions/checkout@v7
- uses: actions/setup-java@v5
with: { java-version: '21', distribution: temurin }
+ # Only srcmorph's jacoco report is uploaded by the `test` job (see the comment there) —
+ # it carries the bulk of the test suite and the only PIT gate. Coveralls/Codecov are
+ # pointed at it directly rather than an aggregated multi-module report, matching the
+ # single-primary-module precedent already used by the sibling java-llama.cpp reactor.
- uses: actions/download-artifact@v8
- with: { name: jacoco-report, path: llamacpp-ai-index-maven-plugin/target/site/jacoco/ }
+ with: { name: jacoco-report, path: srcmorph/target/site/jacoco/ }
continue-on-error: true
- uses: advanced-security/maven-dependency-submission-action@v5
- name: Coveralls
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- file: llamacpp-ai-index-maven-plugin/target/site/jacoco/jacoco.xml
+ file: srcmorph/target/site/jacoco/jacoco.xml
format: jacoco
continue-on-error: true
- name: Codecov
uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
- files: llamacpp-ai-index-maven-plugin/target/site/jacoco/jacoco.xml
+ files: srcmorph/target/site/jacoco/jacoco.xml
continue-on-error: true
check-snapshot:
@@ -402,14 +459,19 @@ jobs:
MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
# Runs even when the deploy step failed: a Central publish-poll timeout reds the
# job *after* the bundle was uploaded (and typically published server-side), while
- # the signed jars + .asc files already exist in llamacpp-ai-index-maven-plugin/target/ (signing happens at
+ # the signed jars + .asc files already exist in each module's target/ (signing happens at
# verify). Collecting on failure lets the github-snapshot job still attach them.
- name: Collect signed artifacts
if: ${{ !cancelled() }}
+ # All three reactor modules produce their own jars/asc files (root pom has no jar).
run: |
mkdir -p signed-snapshot-assets
- cp llamacpp-ai-index-maven-plugin/target/*.jar signed-snapshot-assets/ 2>/dev/null || true
- cp llamacpp-ai-index-maven-plugin/target/*.jar.asc signed-snapshot-assets/ 2>/dev/null || true
+ cp srcmorph/target/*.jar signed-snapshot-assets/ 2>/dev/null || true
+ cp srcmorph/target/*.jar.asc signed-snapshot-assets/ 2>/dev/null || true
+ cp srcmorph-cli/target/*.jar signed-snapshot-assets/ 2>/dev/null || true
+ cp srcmorph-cli/target/*.jar.asc signed-snapshot-assets/ 2>/dev/null || true
+ cp srcmorph-maven-plugin/target/*.jar signed-snapshot-assets/ 2>/dev/null || true
+ cp srcmorph-maven-plugin/target/*.jar.asc signed-snapshot-assets/ 2>/dev/null || true
- uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
@@ -478,14 +540,19 @@ jobs:
MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
# Runs even when the deploy step failed: a Central publish-poll timeout reds the
# job *after* the bundle was uploaded (and typically published server-side), while
- # the signed jars + .asc files already exist in llamacpp-ai-index-maven-plugin/target/ (signing happens at
+ # the signed jars + .asc files already exist in each module's target/ (signing happens at
# verify). Collecting on failure lets the github-release job still attach them.
- name: Collect signed artifacts
if: ${{ !cancelled() }}
+ # All three reactor modules produce their own jars/asc files (root pom has no jar).
run: |
mkdir -p signed-release-assets
- cp llamacpp-ai-index-maven-plugin/target/*.jar signed-release-assets/ 2>/dev/null || true
- cp llamacpp-ai-index-maven-plugin/target/*.jar.asc signed-release-assets/ 2>/dev/null || true
+ cp srcmorph/target/*.jar signed-release-assets/ 2>/dev/null || true
+ cp srcmorph/target/*.jar.asc signed-release-assets/ 2>/dev/null || true
+ cp srcmorph-cli/target/*.jar signed-release-assets/ 2>/dev/null || true
+ cp srcmorph-cli/target/*.jar.asc signed-release-assets/ 2>/dev/null || true
+ cp srcmorph-maven-plugin/target/*.jar signed-release-assets/ 2>/dev/null || true
+ cp srcmorph-maven-plugin/target/*.jar.asc signed-release-assets/ 2>/dev/null || true
- uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
diff --git a/.gitignore b/.gitignore
index 4556a94..23ff8f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,17 +33,17 @@ replay_pid*
*.hprof
# jcstress / jqwik test outputs (generated at the repo root pre-reactor; each module's own
-# basedir post-reactor, e.g. llamacpp-ai-index-maven-plugin/.jqwik-database)
+# basedir post-reactor, e.g. srcmorph-maven-plugin/.jqwik-database)
**/.jqwik-database
# Benchmark/experiment scratch output dirs (not part of the build)
-llamacpp-ai-index-maven-plugin/src/siteGusto/
-llamacpp-ai-index-maven-plugin/src/siteMistral3/
-llamacpp-ai-index-maven-plugin/src/siteQwenBRUTAL/
+srcmorph-maven-plugin/src/siteGusto/
+srcmorph-maven-plugin/src/siteMistral3/
+srcmorph-maven-plugin/src/siteQwenBRUTAL/
# IDE config
.idea/
# Generated AI-index output from the self-index run (keep the tracked placeholder)
-/llamacpp-ai-index-maven-plugin/src/site/ai/*
-!/llamacpp-ai-index-maven-plugin/src/site/ai/empty.md
+/srcmorph-maven-plugin/src/site/ai/*
+!/srcmorph-maven-plugin/src/site/ai/empty.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac9c7ce..73a35bb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,53 @@ The release procedure (prompt template and step-by-step instructions) lives in [
## [Unreleased]
+### Added
+- **Reactor split**: the former single-module `llamacpp-ai-index-maven-plugin` is now a 3-module
+ Maven reactor under a new parent, `net.ladenthin:srcmorph-parent` — `srcmorph` (new core library,
+ `net.ladenthin:srcmorph`, framework-free — no Maven Plugin API dependency), `srcmorph-cli` (new
+ standalone CLI, `net.ladenthin:srcmorph-cli`, driven by a single JSON/YAML configuration file,
+ ships as a `java -jar`-ready fat jar), and `srcmorph-maven-plugin` (the original plugin, now a
+ thin wrapper depending on `srcmorph`). All three (plus the parent pom) release together at one
+ shared version.
+- **Plugin renamed** from `net.ladenthin:llamacpp-ai-index-maven-plugin` to
+ `net.ladenthin:srcmorph-maven-plugin` in this same release (goal prefix `ai-index` → `srcmorph`;
+ package `net.ladenthin.maven.llamacpp.aiindex.mojo` → `net.ladenthin.maven.srcmorph.mojo`; every
+ `@Parameter` property `aiIndex.*` → `srcmorph.*`). A new, independently-versioned relocation-stub
+ module/POM (`net.ladenthin:llamacpp-ai-index-maven-plugin:1.0.4`, no source, no dependencies, only
+ ``) keeps the old coordinates resolvable on Maven Central,
+ redirecting to `net.ladenthin:srcmorph-maven-plugin:1.1.0`.
+- New engine layer in `srcmorph` (`GenerateEngine`, `AggregatePackagesEngine`,
+ `AggregateProjectEngine`, `CalibrateEngine`) extracted from what used to be each mojo's
+ `execute()` body, plus a new shared root configuration object,
+ `net.ladenthin.srcmorph.config.SrcMorphConfiguration`, bindable identically from Maven plexus XML,
+ Jackson JSON/YAML (the new CLI), or plain Java code.
+- New `examples/` directory at the repo root: paired `config_*.json`/`.yaml` fixtures for every
+ `srcmorph-cli` command (`Plan`, `GenerateFileIndex`, `All`, `Calibrate`), paired `run_*.sh`/`.bat`
+ launcher scripts, and an example `logbackConfiguration.xml` — all runnable out of the box with the
+ `mock` provider (no GGUF model required).
+- Per-module `README.md` files (`srcmorph/README.md`, `srcmorph-cli/README.md`) and a rewritten,
+ product-level root `README.md`/`CLAUDE.md` describing the reactor.
+
+### Changed
+- Logging in the extracted core/CLI layers moved from a constructor-injected Maven `Log` to
+ `org.slf4j.Logger` (see the `1.0.x` entries below for the indexer-layer half of this change,
+ already shipped before the reactor split).
+- `.github/workflows/publish.yml` adapted to the 4-module reactor: per-module jar upload/release
+ globs, a repo-wide crash-dump glob, the PIT step scoped to `srcmorph` (the only module with a
+ mutation-testing gate), the `vmlens` job scoped to `srcmorph-maven-plugin` (where its
+ test actually lives), and Coveralls/Codecov pointed at `srcmorph`'s jacoco report.
+
+### Notes
+- **This release renames the Maven plugin's coordinates, package, goal prefix, and `@Parameter`
+ property names.** `net.ladenthin:llamacpp-ai-index-maven-plugin` → `net.ladenthin:srcmorph-maven-plugin`;
+ package `net.ladenthin.maven.llamacpp.aiindex.mojo` → `net.ladenthin.maven.srcmorph.mojo`; goal
+ prefix `ai-index` → `srcmorph`; every `aiIndex.*` property → `srcmorph.*`. Existing consumers of
+ the old coordinates are not broken: a new, independently-versioned relocation-stub artifact
+ (`net.ladenthin:llamacpp-ai-index-maven-plugin:1.0.4`, POM-only, no source/dependencies) is
+ published with a `` pointing at
+ `net.ladenthin:srcmorph-maven-plugin:1.1.0`, so Maven transparently redirects any build still
+ declaring the old artifactId.
+
## [1.0.2] - 2026-07-02
### Changed
diff --git a/CLAUDE.md b/CLAUDE.md
index 481db1e..9193d2b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,339 +1,288 @@
-# CLAUDE.md — llamacpp-ai-index-maven-plugin
+# CLAUDE.md — srcmorph (reactor)
-This document provides guidance for AI assistants working on the llamacpp-ai-index-maven-plugin codebase.
+This document provides guidance for AI assistants working on this codebase.
---
## Project Overview
-**llamacpp-ai-index-maven-plugin** is a free Maven plugin that generates AI-readable hierarchical index and summary files for Java source code projects using llama.cpp-compatible local models (GGUF format). It produces structured `.ai.md` files with metadata headers and AI-generated summaries for both individual source files and packages.
+**srcmorph** is a prompt-driven source-tree transformer: it walks a source tree and processes each
+file through a configurable local LLM prompt (via llama.cpp / GGUF models, no cloud calls), producing
+layered output — per-file, then per-package, then per-project. Today it emits structured `.ai.md`
+Markdown summaries for AI-assisted code navigation; the same rule-routed pipeline is generic enough
+to eventually emit wikis, architecture docs, diagrams, or source-to-source transformations.
+
+**This repository completed its migration to a 4-module Maven reactor.** It started as a single
+Maven plugin (`net.ladenthin:llamacpp-ai-index-maven-plugin`) and was restructured into: a
+framework-free core library, a standalone CLI, the original plugin (now a thin wrapper around the
+library, **renamed** to `net.ladenthin:srcmorph-maven-plugin`), and a tiny relocation-stub module
+that keeps the old `net.ladenthin:llamacpp-ai-index-maven-plugin` coordinates alive on Maven
+Central purely as a `` pointer. **The plugin rename is done**:
+coordinates, package, goal prefix, and every `@Parameter` property changed in this step — do not
+write `aiIndex.*` properties, the `ai-index` goal prefix, or the
+`net.ladenthin.maven.llamacpp.aiindex` package in new documentation or code; use `srcmorph.*`,
+`srcmorph`, and `net.ladenthin.maven.srcmorph.mojo` instead (see the plugin module's own section
+below). Actually publishing the `1.1.0` reactor release and the `1.0.4` relocation stub to Maven
+Central remains a separate, later action by the user.
- **Group ID:** `net.ladenthin`
-- **Artifact ID:** `llamacpp-ai-index-maven-plugin`
-- **Version:** 1.0.2-SNAPSHOT
-- **Java:** target bytecode 1.8, built with JDK 21
+- **Java:** target bytecode 1.8 (production code), Java 21 test sources, built with JDK 21
- **License:** Apache 2.0
- **Author:** Bernard Ladenthin (Copyright 2026)
-- **Plugin goal prefix:** `ai-index`
+- **Reactor version:** `1.1.0` (single shared version across `srcmorph`, `srcmorph-cli`,
+ and `srcmorph-maven-plugin`; the relocation stub below is version-pinned independently)
---
-## Build System
+## Repository layout — Maven reactor
-The project uses **Maven** (minimum 3.6.3).
+```
+llamacpp-ai-index-maven-plugin/ (repo root; reactor parent)
+├── pom.xml net.ladenthin:srcmorph-parent:1.1.0 (packaging=pom)
+│ shared build plugins + dependencyManagement + release profile
+├── srcmorph/ CORE LIBRARY net.ladenthin:srcmorph (Java 8, Maven-API-free)
+│ └── src/main/java/net/ladenthin/srcmorph/
+│ ├── config/ 18+ POJOs (incl. the shared root SrcMorphConfiguration)
+│ ├── engine/ GenerateEngine, AggregatePackagesEngine, AggregateProjectEngine,
+│ │ CalibrateEngine, SrcMorphException, GenerateResult, CalibrationReport, EngineSupport
+│ ├── indexer/ SourceFileIndexer, PackageIndexer, ProjectIndexer, AiFieldGenerationSupport, ...
+│ ├── document/ prompt/ provider/ support/
+│ └── src/test/resources/SmolLM2-135M-Instruct-Q3_K_M.gguf (real-model tests live here)
+├── srcmorph-cli/ CLI net.ladenthin:srcmorph-cli (fat jar = deliverable)
+│ └── src/main/java/net/ladenthin/srcmorph/cli/
+│ ├── Main.java BitcoinAddressFinder cli/Main.java pattern
+│ └── configuration/ CConfiguration + CCommand (BAF public-field style)
+├── srcmorph-maven-plugin/ Maven plugin net.ladenthin:srcmorph-maven-plugin, goalPrefix srcmorph
+│ └── src/main/java/net/ladenthin/maven/srcmorph/mojo/ (5 mojos; renamed package/properties)
+├── llamacpp-ai-index-maven-plugin/ RELOCATION STUB net.ladenthin:llamacpp-ai-index-maven-plugin:1.0.4
+│ └── pom.xml only — no source, no
+├── examples/ config_*.json/.yaml + run_*.sh/.bat + logbackConfiguration.xml
+├── docs/ RELEASE.md + the ai-index model-benchmark writeups
+└── .github/workflows/ CI adapted to the 4-module reactor
+```
-### Common Commands
+Every module except the relocation stub inherits its `` from the parent
+(`net.ladenthin:srcmorph-parent`), so `srcmorph`/`srcmorph-cli`/`srcmorph-maven-plugin` ship in
+lockstep by construction. Bump their version reactor-wide with
+`mvn versions:set -DnewVersion=X -DgenerateBackupPoms=false` from the repo root — but the relocation
+stub (`llamacpp-ai-index-maven-plugin/`) has **no ``** and is version-pinned independently
+at `1.0.4`; because it is still listed in the root ``, a plain `versions:set` run from the
+root walks it too and would overwrite that pin unless excluded:
```bash
-# Compile only
-mvn compile
+mvn versions:set -DnewVersion=X -DgenerateBackupPoms=false \
+ -Dexcludes=net.ladenthin:llamacpp-ai-index-maven-plugin
+```
-# Run all tests
-mvn test
+### `srcmorph` — the core library
+
+Framework-free: **no dependency on `org.apache.maven..`** anywhere (enforced by
+`CoreArchitectureTest#coreIsMavenFree`, the load-bearing ArchUnit rule for this module). Depends on
+`net.ladenthin:llama` (the llama.cpp JNI binding, used only by the `provider` package), SLF4J, jspecify
++ checker-qual, Lombok (provided). Package root: `net.ladenthin.srcmorph`.
+
+- **`config/`** — mutable JavaBeans (no Maven annotations) bindable structurally from Maven plexus XML,
+ a Jackson `ObjectMapper`/`YAMLMapper`, or plain Java code. The root object is
+ **`SrcMorphConfiguration`**: one bean holding everything a run needs (`baseDirectory`,
+ `outputDirectory`, `subtrees`, `excludes`, `fileExtensions`, the size band, `force`, `planOnly`,
+ `generationProvider`, `promptDefinitions`, `aiDefinitions`, `fieldGenerations`, `factDefinitions`, the
+ `llama*` fallback params, `pluginVersion`/`aiVersion`/`projectName`). **Field names intentionally
+ mirror the Maven plugin's current `@Parameter` names** so a JSON/YAML key reads identically to the
+ matching `` XML element — see `SrcMorphConfiguration`'s own Javadoc.
+- **`engine/`** — one class per phase, each constructed from a `SrcMorphConfiguration` and owning its
+ own AI provider lifecycle (try-with-resources; one model resident at a time):
+ `GenerateEngine` (plan → validate → planOnly early-out → per-model-group indexing loop + progress
+ bar), `AggregatePackagesEngine`, `AggregateProjectEngine` (deterministic listing + optional one-call
+ AI overview), `CalibrateEngine` (per-model preflight + timing). All four throw the checked
+ `SrcMorphException` on misconfiguration, never a Maven `MojoExecutionException` — callers (the
+ plugin's mojos, the CLI's `Main`) wrap it into whatever their own surface expects.
+- **`indexer/`** — the walk/plan/write orchestration (`SourceFileIndexer`, `PackageIndexer`,
+ `ProjectIndexer`, `AiFieldGenerationSupport`, `AiIndexPlan`, `AiCalibrationRunner`, ...). Logs via
+ `org.slf4j.Logger` (a private static final field per class), not a Maven `Log` — this is what makes
+ the module Maven-free; Maven's own `maven-slf4j-provider` (ships since Maven ≥ 3.1) makes these lines
+ surface as ordinary `[INFO]`/`[WARN]` output inside a plugin execution with zero glue, and the CLI
+ ships a logback binding for the same log lines outside Maven.
+- **`document/`** — the `.ai.md` model + codecs (`AiMdDocument`, `AiMdHeader`, `AiMdDocumentCodec`,
+ `AiMdHeaderCodec`, `AiMdHeaderSupport`, `AiMdChildEntryLineFormatter`, `AiMdLeadExtractor`,
+ `AiGenerationRequest`/`AiGenerationResult`).
+- **`prompt/`** — `AiPromptDefinition`, `AiPreparedPrompt`, `AiPromptSupport`,
+ `AiPromptPreparationSupport`.
+- **`provider/`** — the AI backend abstraction: `AiGenerationProvider` (`Closeable`),
+ `AiGenerationProviderFactory` (looks up `"mock"` / `"llamacpp-jni"`), `MockAiGenerationProvider`,
+ `LlamaCppJniAiGenerationProvider`, `LlamaCppJniConfig` + `LlamaCppJniConfigFactory` (the pure 26-field
+ mapping from a resolved `AiGenerationConfig` to the native binding's parameter objects — extracted
+ from the old mojo so it is unit- and PIT-testable without a Maven runtime), `AiCompletionParser`.
+- **`support/`** — foundation helpers with no dependency on anything above them: `AiChecksumSupport`,
+ `AiTimeSupport`, `AiPathSupport`, `AiSourceExcludeFilter`, `AiProgressBar`, `AiSourceChunker`,
+ `AiDeterministicSummary`, `AiGenerationTimeEstimator`, `Java8CompatibilityHelper`, `ConvertToRecord`.
+
+**Architecture rules** (`CoreArchitectureTest`, ArchUnit): `coreIsMavenFree` (the load-bearing rule
+above), `layeredArchitecture` (`engine` on top → `indexer` → `provider`/`document`/`prompt` → `config`
+→ `support`), `noPackageCycles`, `loggersArePrivateStaticFinal`, `noSystemExit`,
+`noTestFrameworksInProduction`, `noJavaUtilLogging`, `noSystemOutOrErrInProduction`,
+`noInternalJdkImports`, `noPublicMutableFields`, `noNewRandom`, `noThreadSleep`,
+`jniConfinedToProvider` (only the `provider` package may touch the llama.cpp JNI binding).
+
+**Test suite** (`srcmorph/src/test/java/net/ladenthin/srcmorph/`, package-renamed 1:1 with production):
+~63 test files, incl. jqwik properties, an ArchUnit suite, a Lincheck race test
+(`AiGenerationKindLincheckTest`), and the model-backed real tests gated on
+`src/test/resources/SmolLM2-135M-Instruct-Q3_K_M.gguf`. **PIT mutation testing**: `mutationThreshold`
+100 over an explicit `targetClasses` list in `srcmorph/pom.xml` — currently 47 classes across
+config/document/indexer/prompt/provider/support, all killed at 100%. `srcmorph-cli` and the plugin
+module do not have a PIT gate yet (see `TODO.md`). The `gpu-cuda`/`gpu-vulkan` profiles (swap the
+`net.ladenthin:llama` classifier via the `llama.classifier` property) live here; the `jcstress` and
+`vmlens` profiles/tests currently still live in the **plugin** module (they were not moved in the
+extraction — see that module's section below), not here.
+
+### `srcmorph-cli` — the standalone CLI
+
+`net.ladenthin:srcmorph-cli`, packaging `jar`, package root `net.ladenthin.srcmorph.cli`. A BAF-style
+CLI driven by a single JSON or YAML configuration file:
+
+- **`cli/Main.java`** — extension-dispatched loader (`.json`/`.js` → Jackson `ObjectMapper`,
+ `.yaml`/`.yml` → `YAMLMapper`, both with `FAIL_ON_UNKNOWN_PROPERTIES` enabled so a config typo fails
+ fast, mirroring what plexus does on the Maven XML side); echoes the parsed configuration back
+ (re-serialized as both JSON and YAML) for review before anything runs; no `System.exit` anywhere — a
+ failure propagates as an unchecked exception out of `main(String[])`. Dispatches on `CConfiguration`'s
+ `command` field to one or more `net.ladenthin.srcmorph.engine.*` engines.
+- **`cli/configuration/CConfiguration`** / **`CCommand`** — public-mutable-field JavaBeans (the BAF
+ `cli.configuration.CConfiguration` convention; carved out of the `noPublicMutableFields` ArchUnit rule
+ via this package's explicit exception). `CConfiguration.srcMorph` is the **same**
+ `net.ladenthin.srcmorph.config.SrcMorphConfiguration` the Maven plugin's mojos build from their own
+ `@Parameter` fields — a JSON/YAML key under `srcMorph` reads identically to the matching plugin XML
+ element. `CCommand` is `Plan | GenerateFileIndex | AggregatePackages | AggregateProject | All |
+ Calibrate`; the default is `Plan` (safe: no model load, nothing written).
+- The fat jar (`srcmorph-cli--jar-with-dependencies.jar`, main class
+ `net.ladenthin.srcmorph.cli.Main`) is bound **unconditionally** to the `package` phase (a deliberate
+ divergence from BAF's `-P assembly` opt-in — for this module the fat jar IS the deliverable).
+- Ships its own logback binding (`ch.qos.logback:logback-classic`, runtime scope) — unlike the library
+ (consumer picks any SLF4J binding) and the plugin (gets one for free from Maven's own
+ `maven-slf4j-provider`), a standalone `java -jar` process needs to bring its own or every log line is
+ silently dropped.
+- **Architecture rules** (`CliArchitectureTest`): `cliIsLeaf` (nothing else in the reactor may depend on
+ this module — it is the leaf-most consumer), `noPublicMutableFields` (with the `configuration`
+ package carve-out), `noSystemExit`, `mavenFree` (must never depend on the Maven Plugin API — that
+ boundary belongs to the plugin module), `noTestFrameworksInProduction`, `noInternalJdkImports`,
+ `loggersArePrivateStaticFinal`.
+- **Tests**: `MainTest`, `configuration.ConfigBindingTest` (round-trips a private
+ `src/test/resources/test-fixtures/minimal-generate.{json,yaml}` pair through both parsers),
+ `ExamplesConfigBindingTest` (sweeps every shipped `examples/config_*.{json,yaml}` fixture — the
+ public, documented examples — through the same strict mappers), `CliEndToEndTest` (drives the `All`
+ and `Plan` commands against the mock provider end to end, no forked process).
+
+### `srcmorph-maven-plugin` — the Maven plugin (renamed; formerly `llamacpp-ai-index-maven-plugin`)
+
+**Renamed in the final migration step**: coordinates `net.ladenthin:srcmorph-maven-plugin` (was
+`net.ladenthin:llamacpp-ai-index-maven-plugin`), package `net.ladenthin.maven.srcmorph.mojo` (was
+`net.ladenthin.maven.llamacpp.aiindex.mojo`), goal prefix `srcmorph` (was `ai-index`), every
+`@Parameter` property now spelled `srcmorph.*` (e.g. `srcmorph.skip`, `srcmorph.file.skip`,
+`srcmorph.planOnly`, `srcmorph.generationProvider`, `srcmorph.llama.modelPath` — was `aiIndex.*`).
+The old coordinates are kept alive only via the separate relocation-stub module (see "Repository
+layout" above and its own paragraph below) — never describe the plugin using the old
+coordinates/package/properties in new documentation or code. The plugin's *contents* stay thin: it
+depends on `net.ladenthin:srcmorph` (compile scope) for everything except the 5 mojo classes
+themselves.
+
+- **`AbstractAiIndexMojo`** — shared `@Parameter` fields + `buildConfiguration()`, which maps them onto
+ a new `SrcMorphConfiguration` for the matching engine to run. Concrete mojos
+ (`GenerateMojo`/`AggregatePackagesMojo`/`AggregateProjectMojo`/`CalibrateMojo`) each add their own
+ goal-specific `@Parameter`s (e.g. `skipFile`/`skipPackage`/`skipProject`, `planOnly`, `fileExtensions`,
+ `excludes`, `factDefinitions`), build the configuration, and delegate the whole run to one
+ `net.ladenthin.srcmorph.engine.*` engine — mojos are now thin (≤ ~30 lines of actual logic each) and
+ translate a caught `SrcMorphException`/`IOException` into a `MojoExecutionException`. The class
+ itself keeps its historical name (`AbstractAiIndexMojo`) even though the package/goal-prefix/
+ properties around it were renamed — only the Maven-facing surface (coordinates, package, goal
+ prefix, `@Parameter` property strings) was in scope for the rename.
+- **Skip flags stay mojo-side** (`skip`, `skipFile`, `skipPackage`, `skipProject`) — a Maven lifecycle
+ concern, not part of `SrcMorphConfiguration`; an engine built from a configuration always executes
+ when asked. See `MojoPhaseSkipTest`.
+- **Architecture rules** (`PluginArchitectureTest`): Maven-annotation confinement to `mojo`, every mojo
+ extends `AbstractMojo`, plus this module's slice of the shared conventions.
+- **jcstress** (`jcstress/AiGenerationKindRace.java`) and **vmlens**
+ (`vmlens/VmlensInterleavingSmokeTest.java`) tests/profiles currently live in this module, not in
+ `srcmorph` — they were not relocated during the core extraction.
+- Full goal/parameter reference: `srcmorph-maven-plugin/README.md`.
+
+### `llamacpp-ai-index-maven-plugin` — the relocation stub (retired coordinates)
+
+A separate, minimal 4th reactor module — **not** a renamed copy of the plugin above, and not a
+child of `srcmorph-parent` (no `` at all). Its entire `pom.xml` is `groupId` +
+`artifactId` (`llamacpp-ai-index-maven-plugin`) + a version pinned independently at `1.0.4` +
+`` pointing at `net.ladenthin:srcmorph-maven-plugin:1.1.0`.
+No source, no tests, no dependencies. Its sole purpose is so a consumer still declaring the old
+Maven coordinates gets redirected by Maven to the renamed plugin once both are published. Because
+it is still listed in the root `` for aggregation, a reactor-wide `mvn versions:set` run
+from the repo root must exclude it explicitly — see "Repository layout" above for the exact
+command; the same warning is repeated in the stub's own `pom.xml` comment and in `TODO.md`.
-# Build the plugin JAR
-mvn package
+---
-# Build without tests
-mvn package -DskipTests
+## Build Commands
-# Run the plugin against itself (self-test profile)
-mvn ai-index:generate -P ai-index-selftest
+### Whole reactor (repo root)
-# Install to local Maven repository
-mvn install
+```bash
+mvn compile # Compiles every module (Java + generates nothing native; pure Java reactor)
+mvn test # Runs every module's tests
+mvn package # Builds all five reactor projects: parent pom + 3 jars (incl. the CLI's fat
+ # jar) + the relocation-stub pom (trivial — no source, packaging=pom)
+mvn install # Installs all five into ~/.m2 (needed before iterating on a single module — see below)
```
-### JVM / Compiler Configuration
-
-- Java 1.8 source and target (compiled with JDK 21)
-- UTF-8 encoding
-- `maven-enforcer-plugin` requires Maven ≥ 3.6.3
-
-### Offline / Restricted-Network Environments
+### Iterating on one module
-When Maven cannot reach the internet (proxy, air-gap, restricted CI), use the options below.
+Maven resolves inter-module dependencies (`srcmorph-maven-plugin` and `srcmorph-cli` both
+depend on `net.ladenthin:srcmorph`) via the local repository, not in-reactor classes, unless you use
+`-pl`/`-am`:
-**Offline Maven (requires warm `~/.m2/repository` cache):**
```bash
-# Run tests without downloading anything
-mvn test -o
+# Build/install the core first if you're iterating on the CLI or the plugin against local core changes:
+mvn -pl srcmorph -am install -DskipTests
-# Package without downloading anything
-mvn package -o -DskipTests
+# Then work on just one module:
+mvn -pl srcmorph-cli test
+mvn -pl srcmorph-maven-plugin test
```
-The cache is warm after any previous successful `mvn test` or `mvn install` run.
+### Offline / restricted-network environments
-**Direct `javac` compilation (fallback, no Maven required):**
```bash
-# Gather classpath from already-downloaded JARs
-CP=$(find ~/.m2/repository -name "*.jar" | tr '\n' ':')
-OUT=/tmp/aiindex-classes && mkdir -p "$OUT"
-
-# Compile production sources
-find src/main/java -name "*.java" | xargs javac -cp "$CP" -d "$OUT" --release 8
-
-# Compile test sources (after production classes are compiled)
-TOUT=/tmp/aiindex-test-classes && mkdir -p "$TOUT"
-find src/test/java -name "*.java" | xargs javac -cp "$CP:$OUT" -d "$TOUT" --release 8
+mvn test -o # requires a warm ~/.m2/repository cache
+mvn package -o -DskipTests
```
-Zero compiler output means zero errors.
-
----
-
-## Project Structure
+### Run the self-test profile (plugin module only)
+```bash
+mvn -pl srcmorph-maven-plugin srcmorph:generate -P srcmorph-selftest
```
-llamacpp-ai-index-maven-plugin/
-├── src/
-│ ├── main/
-│ │ └── java/net/ladenthin/maven/llamacpp/aiindex/ # layered packages (top → bottom)
-│ │ ├── mojo/ # Entry: AbstractAiIndexMojo, GenerateMojo, AggregatePackagesMojo
-│ │ ├── indexer/ # Orchestration: SourceFileIndexer, PackageIndexer, AiFieldGenerationSupport
-│ │ ├── provider/ # AI backends: AiGenerationProvider(+Factory), Mock/LlamaCppJni providers,
-│ │ │ # AiCompletionParser, LlamaCppJniConfig
-│ │ ├── document/ # .ai.md model + codecs: AiMdDocument, AiMdHeader, AiMd*Codec,
-│ │ │ # AiMdHeaderSupport, AiMdChildEntryLineFormatter,
-│ │ │ # AiGenerationRequest, AiGenerationResult (carry an AiMdHeader)
-│ │ ├── prompt/ # AiPromptDefinition, AiPreparedPrompt, AiPromptSupport, AiPromptPreparationSupport
-│ │ ├── config/ # AiGenerationConfig, AiGenerationKind, AiFieldGenerationConfig,
-│ │ │ # AiFieldGenerationSelector, AiModelDefinition, AiModelDefinitionSupport
-│ │ └── support/ # Foundation: AiChecksumSupport, AiTimeSupport, AiPathSupport,
-│ │ # AiSourceExcludeFilter, Java8CompatibilityHelper, ConvertToRecord
-│ ├── site/
-│ │ └── ai/ # Output directory for .ai.md files
-│ └── test/
-│ ├── java/net/ladenthin/maven/llamacpp/aiindex/
-│ │ └── *.java # JUnit Jupiter tests
-│ └── resources/
-│ └── SmolLM2-135M-Instruct-Q3_K_M.gguf # Small test model
-├── .github/workflows/ # CI/CD pipelines
-├── pom.xml
-└── README.md
-```
-
----
-
-## Core Architecture
-
-### Three-Phase Operation
-The plugin operates in three logical phases, building a navigable index from fine to coarse:
+### Run the CLI
-**Phase 1 — File Indexing & Summarization**
-```
-[Source .java files] → SourceFileIndexer → [*.java.ai.md files (deterministic header + AI body)]
-```
-The `generate` goal is **plan-then-execute** and **rule-routed**: it first walks all candidate files,
-routes each via the `` rules to a `(model, prompt)` — or a *skip*, or the explicit
-*fallback* — and logs a tree grouped by model (which file gets which model + prompt). It then loads
-**each model once** (groups sharing an `aiDefinitionKey`, sequentially → one model resident at a time,
-bounded RAM) and indexes that group's files. A rule matches by a composable **`` tree** — ``/`` (each wraps its children in
-`…`), `` (a single nested condition), over leaves
-``, `` (`min`/`max` bytes), `` (`min`/`max`), ``/``
-(ISO-8601 instant vs file mtime), `` (base-relative glob) — evaluated by `AiConditionEvaluator`
-against an `AiFileContext`; new leaf kinds are one field + one branch. Among matches the highest
-`priority` wins (ties by declaration order). `true` ignores matching files (a high-priority
-skip beats routes and the fallback); exactly one `true` (no condition) catches the
-rest; a file matching no rule and no fallback **fails the build**. `aiIndex.planOnly=true` prints the
-Markdown plan (file → rule id → prompt → rough time estimate, summed per model and overall) and stops
-(no model loaded). The plan also computes each routed file's **context-window fit** up front (via
-`AiInputWindowCalculator`, the same threshold the run uses to trim): a file larger than its routed
-model's window is flagged in the plan and, **by default (`onOversize=fail`), fails the build** (a hard
-abort) — the plugin never auto-picks a model. The resolution is **configuration only**, via the rule's
-**``** strategy (`AiOversizeStrategy`): `fail` (default), `sample` (trim to the window head,
-one call), `mapReduce` (chunk at line boundaries → summarize each → combine via a **hierarchical reduce**
-that batches partials to fit the window so whole-file `=0` coverage isn't trimmed away;
-`` bounds the time — `AiSourceChunker`; the chunk window carries a token-safety headroom so a
-token-dense chunk can't overflow the model context), or `deterministic` (model-free metadata+sample body
-— `AiDeterministicSummary`).
-So oversized files are either routed to a larger-context model or handled by a strategy; only `fail`
-entries abort. A rule may also carry an orthogonal, language-agnostic **``** list (`AiFactCounter`
-+ `AiFactExtractor`): each `{label, pattern}` reports its regex match count over the **whole** source,
-prepended to the body of **every** file the rule matches (oversize or not) — exact structural counts
-(SQL `INSERT` rows / tables / views, Java type declarations / `\bboolean\b` fields, …) that give
-downstream agents authoritative numbers the (possibly sampled) AI prose cannot reliably produce. A fact
-set can be defined once in a shared `` group and referenced from many rules by
-`` (`AiFactDefinition` + `AiFactDefinitionSupport`, resolved onto each rule before indexing),
-instead of repeating the `` block inline. Generation is also guarded by a **retry-on-overflow**:
-if the provider rejects a prompt as exceeding the context (the char-based trim under-counted tokens on
-dense content), the call is re-trimmed to a smaller window and retried rather than failing the build. See `AiFieldGenerationSelector` (selection + validation), `AiCondition`/
-`AiConditionEvaluator` (the tree), and `AiIndexPlan` (the rendered plan). This is how one run can index
-different file kinds/sizes with different models *and* prompts.
-
-**Phase 2 — Package Aggregation & Summarization**
-```
-[*.java.ai.md files] → PackageIndexer → [package.ai.md files (deterministic header + AI body)]
-```
-
-**Phase 3 — Project Index (deterministic listing; optional AI overview)**
-```
-[package.ai.md files] → ProjectIndexer → [one project.ai.md: per-package lead (body) + link (header F)]
-```
-Phase 3 harvests the one-sentence blockquote lead each `package.ai.md` already begins with
-(via `AiMdLeadExtractor`) and writes a single, always-loadable table of contents linking to
-every package — the entry point an agent reads first to navigate project → package → file → raw.
-The per-package listing calls no model, so it is cheap and scales to hundreds of packages (no
-embeddings/vector DB). **Optional (opt-in):** when a `` is configured on the
-goal, *one* extra AI call writes a short `#### Overview` paragraph from the package leads (never
-the full bodies). Incrementality is preserved by folding the overview generation signature
-(`promptKey:aiDefinitionKey`) — not the AI output — into the `c` checksum, so an unchanged project
-is never re-inferred but enabling/switching the overview, or a package change, rebuilds it.
-
-**Each phase is independently switchable.** Every goal is a separate Maven execution, and each can be
-toggled on/off on its own via a phase-specific skip flag named after the index level (`aiIndex.file.skip`,
-`aiIndex.package.skip`, `aiIndex.project.skip` — the `x` node types), with `aiIndex.skip` as a global
-"skip all" switch — so you can run nothing, all three, or any subset (e.g. file + project only). The
-mechanism is generalized symmetrically: `AbstractAiIndexMojo` owns the global `skip`, the abstract
-`isPhaseSkipped()` seam, and `shouldSkip() = skip || isPhaseSkipped()`; each concrete mojo adds exactly
-one `skip` `@Parameter` plus a one-line `isPhaseSkipped()` override and calls `shouldSkip()` at
-the top of `execute()`. Covered by `MojoPhaseSkipTest`.
-
-### Key Components
-
-| Class | Role |
-|---|---|
-| `AbstractAiIndexMojo` | Shared `@Parameter` fields and utilities for the AI generate/aggregate-packages mojos |
-| `GenerateMojo` | Phase 1: index + summarize source files |
-| `AggregatePackagesMojo` | Phase 2: aggregate + summarize package index files |
-| `AggregateProjectMojo` | Phase 3: build the single `project.ai.md` (deterministic listing; extends `AbstractAiIndexMojo` and builds a provider **only** when a `` opts into the AI overview) |
-| `SourceFileIndexer` | `collectCandidates` (walk + filters) / `classify` (→ `AiIndexPlan`) / `indexFile` (write one file with a given rule + provider) — split so a run plans first and loads one model per group |
-| `AiFieldGenerationSelector` | Routes a file to a rule by its `` + priority; explicit fallback; `skip`; plus `validate(rules)` (fail-fast config check) |
-| `AiCondition` / `AiConditionEvaluator` | Composable and/or/not condition tree (leaves: extensions/size/lines/modifiedAfter/modifiedBefore/pathGlob) + its evaluator (`matches`/`validate`/`usesLines`) |
-| `AiIndexPlan` | The routing plan: routes grouped by model id, skips, unmatched, per-file context-window fit; renders the up-front tree |
-| `AiInputWindowCalculator` | Pure calculator for the input window (max source chars before trimming + whether a source exceeds it); single source of truth shared by the run-time trim (`AiFieldGenerationSupport`) and the plan-time over-window check (`SourceFileIndexer.classify`) |
-| `AiOversizeStrategy` | Enum of the per-rule `` strategies (`fail`/`sample`/`mapReduce`/`deterministic`) + case-insensitive parser; default `fail` |
-| `AiSourceChunker` | Pure line-boundary chunker for `mapReduce` (overlap + `maxChunks` representative sampling) |
-| `AiFactCounter` | Maven `@Parameter` POJO for one `` — a `{label, pattern}` deterministic counter |
-| `AiFactExtractor` | Pure: counts each `` regex over the whole source → the exact "facts" block prepended to the body (+ fail-fast pattern validation) |
-| `AiFactDefinition` | Maven `@Parameter` POJO for a named, reusable `` group `{key, facts}` |
-| `AiFactDefinitionSupport` | Key-indexed lookup: resolves each rule's `` to its shared group's counters (copies onto the rule) |
-| `CalibrateMojo` / `AiCalibrationRunner` | `ai-index:calibrate` goal (thin orchestration) + the indexer-layer measurer (warmup + two sized generations, prefers the binding's reported throughput, else a wall-clock differential) → `AiCalibrationMeasurement` |
-| `AiCalibration` | `` `@Parameter` POJO on `` (`prefillTokensPerSecond`/`decodeTokensPerSecond`/`charsPerToken`); makes `AiGenerationTimeEstimator` use measured per-machine rates instead of the built-in reference-CPU model |
-| `AiDeterministicSummary` | Pure model-free body builder for `deterministic` (size, line count, head/tail sample) |
-| `AiProgressBar` | Pure ASCII progress-bar renderer (`[##### ] 42%`); `GenerateMojo` logs it after each file, advancing by the running sum of per-file plan estimates over the grand total (with estimated time left + actual elapsed) |
-| `PackageIndexer` | Creates `package.ai.md` files with contents listings, calls AI to fill the document body |
-| `ProjectIndexer` | Phase 3: harvests each package's lead + relative link into one `project.ai.md`; deterministic listing, with an optional one-call AI `#### Overview` from the leads |
-| `AiMdLeadExtractor` | Pure extraction of the one-line blockquote lead from an `.ai.md` body (fallback: first non-blank line) |
-| `AiGenerationProvider` | Interface for AI backends (llama.cpp JNI or mock) |
-| `AiFieldGenerationSupport` | Shared field-generation loop extracted from both indexers |
-| `AiGenerationResult` | Immutable carrier for the AI-generated body text out of the loop |
-| `AiModelDefinition` | Maven `@Parameter` POJO for a named AI model definition |
-| `AiModelDefinitionSupport` | Key-indexed lookup: converts `AiModelDefinition` → `AiGenerationConfig` |
-| `AiMdDocumentCodec` | Reads and writes `.ai.md` files |
-| `AiMdHeaderCodec` | Encodes/decodes the YAML-like metadata header |
-| `AiPromptSupport` | Looks up prompt templates by key |
-| `AiPromptPreparationSupport` | Prepares prompts with source substitution and trimming |
-
-### Document Format
-
-Each `.ai.md` file begins with a metadata header block:
-
-```
-### main/java/com/example
-- H: 1.0
-- C: A1B2C3D4
-- D: 2026-01-01T00:00:00Z
-- T: 2026-01-01T00:01:00Z
-- G: 0.1.0
-- A: 1.0.0
-- X: package
-- F: [MyClass.java](MyClass.java.ai.md)
-- F: [sub/](sub/package.ai.md)
----
-> Handles example domain logic for ...
-(AI-generated body text continues here)
+```bash
+mvn -pl srcmorph-cli package
+java -jar srcmorph-cli/target/srcmorph-cli-1.1.0-jar-with-dependencies.jar examples/config_All.json
```
-The header carries only deterministic metadata. All AI-generated
-content lives in the document body after the `---` separator, keeping
-the header machine-parseable without AI involvement (see
-`AiMdHeader.java` Javadoc for the rationale). The repeatable `F` field
-holds the deterministic child links — for a `package`, one per child
-`.ai.md` (files and sub-packages); for a `project`, one per package —
-so navigation (project → package → file) stays uniformly in the header
-while the body stays free-form. `AiMdDocumentCodec` parses only the
-header block, so a `- F:` line in the body is never read as a link.
-
-| Field | Meaning |
-|---|---|
-| `h` | Header format version |
-| `title` | File or package path |
-| `c` | CRC32 checksum of the source file (8-char uppercase hex; see `AiChecksumSupport`) |
-| `d` | Index creation timestamp (ISO-8601) |
-| `t` | Last generation timestamp |
-| `g` | Plugin version (`project.version`) |
-| `a` | AI model version |
-| `x` | Node type: `file`, `package`, or `project` |
-| `F` | Repeatable child link (markdown), e.g. `[Foo.java](Foo.java.ai.md)`; the navigation list for `package`/`project` nodes, absent for `file` |
-
-### Provider Pattern
-
-`AiGenerationProvider` is a `Closeable` interface for AI backends:
-
-| Implementation | Description |
-|---|---|
-| `LlamaCppJniAiGenerationProvider` | Uses the `net.ladenthin:llama` JNI binding to run local GGUF models |
-| `MockAiGenerationProvider` | Returns deterministic mock responses; used in all tests |
-
-`AiGenerationProviderFactory` selects the provider by name (`"llamacpp-jni"` or `"mock"`).
-
----
-
-## Maven Plugin Goals
-
-| Goal | Description |
-|---|---|
-| `ai-index:generate` | Phase 1: index source files and fill the AI-generated document body |
-| `ai-index:aggregate-packages` | Phase 2: aggregate package index files and fill the AI-generated document body |
-| `ai-index:aggregate-project` | Phase 3: harvest per-package leads into one `project.ai.md` (deterministic listing; optional one-call AI `#### Overview` when a `` is configured) |
-| `ai-index:calibrate` | Preflight + per-machine timing (between `planOnly` and a real run): loads each routed model once (catches bad path / OOM / wrong native), measures prefill/decode throughput, and prints a paste-ready `` block per `` |
-
-### Key Parameters (`GenerateMojo`)
-
-| Parameter | Property | Default | Description |
-|---|---|---|---|
-| `outputDirectory` | `aiIndex.outputDirectory` | `${basedir}/src/site/ai` | Where `.ai.md` files are written |
-| `skip` | `aiIndex.skip` | `false` | Global switch: skip **every** phase |
-| `skipFile` | `aiIndex.file.skip` | `false` | Skip only the **file** phase (`generate` goal) |
-| `skipPackage` | `aiIndex.package.skip` | `false` | Skip only the **package** phase (`aggregate-packages` goal) |
-| `skipProject` | `aiIndex.project.skip` | `false` | Skip only the **project** phase (`aggregate-project` goal) |
-| `force` | `aiIndex.force` | `false` | Regenerate even if summary exists |
-| `subtrees` | `aiIndex.subtrees` | *(all)* | Limit to specific source subdirectories |
-| `fileExtensions` | `aiIndex.fileExtensions` | `.java` | File extensions to index |
-| `excludes` | `aiIndex.excludes` | *(none)* | Glob patterns (base-relative, `/` separators) for source files to skip, e.g. `**/package-info.java`, `**/generated/**` (`AiSourceExcludeFilter`) |
-| `minFileSizeBytes` | `aiIndex.file.minSizeBytes` | `0` | Exclusive lower size bound; files `<=` this are skipped (`0` = no lower bound). For size-tiering across multiple `generate` executions |
-| `maxFileSizeBytes` | `aiIndex.file.maxSizeBytes` | `0` | Inclusive upper size bound; files `>` this are skipped (`0` = unlimited). Pairs with `minFileSizeBytes` to form a band |
-| `planOnly` | `aiIndex.planOnly` | `false` | Print the routing plan tree (model → files → prompt → window fit) and stop; no model loaded, nothing generated |
-| `generationProvider` | `aiIndex.generationProvider` | `mock` | `mock` or `llamacpp-jni` |
-| `llamaModelPath` | `aiIndex.llama.modelPath` | — | Path to GGUF model file |
-| `llamaContextSize` | `aiIndex.llama.contextSize` | `2048` | Context window size |
-| `llamaMaxOutputTokens` | `aiIndex.llama.maxOutputTokens` | `128` | Max generated output tokens |
-| `llamaTemperature` | `aiIndex.llama.temperature` | `0.15` | Sampling temperature |
-| `llamaThreads` | `aiIndex.llama.threads` | `2` | CPU threads for inference |
+See `examples/` (repo root) for ready-to-run `config_*.json`/`.yaml` + paired `run_*.sh`/`.bat`
+launcher scripts, all using the `mock` provider (no GGUF model required).
---
## Testing
-### Frameworks
-
-- **JUnit Jupiter** (6.1.0) — test runner (`@Test`, `@BeforeEach`, `@TempDir`)
-- **Hamcrest** — matchers (`assertThat`, `is`, `equalTo`)
-- **`MockAiGenerationProvider`** — deterministic AI responses for all tests
-
-### Test Model
+Every module uses JUnit Jupiter + Hamcrest; `MockAiGenerationProvider` gives fully deterministic tests
+with no model or JNI dependency. Model-backed tests (in `srcmorph`) are gated on
+`srcmorph/src/test/resources/SmolLM2-135M-Instruct-Q3_K_M.gguf` and self-skip when the native library
+is unavailable.
-`src/test/resources/SmolLM2-135M-Instruct-Q3_K_M.gguf` is a small (≈90 MB) GGUF model used by integration tests that exercise the real `LlamaCppJniAiGenerationProvider`. These tests are skipped when the JNI native library is unavailable.
+- `srcmorph` — the bulk of the test suite (framework-free logic); see that module's section above.
+- `srcmorph-cli` — `MainTest`, `ConfigBindingTest`, `ExamplesConfigBindingTest`, `CliArchitectureTest`,
+ `CliEndToEndTest`.
+- `srcmorph-maven-plugin` — `PluginArchitectureTest`, `MojoPhaseSkipTest`, plus the jcstress/
+ vmlens tests noted above. (The `llamacpp-ai-index-maven-plugin` relocation stub has no tests.)
-### Conventions
-
-- All tests that invoke the real llama.cpp JNI must guard with an availability check.
-- Tests that only exercise Java logic use `MockAiGenerationProvider`.
-- Use `Files.createTempDirectory(...)` for temporary file system state.
-- See `TEST_WRITING_GUIDE.md` for full conventions.
+See `TEST_WRITING_GUIDE.md` (repo root, applies to every module) for full conventions.
---
@@ -341,24 +290,44 @@ header block, so a `- F:` line in the body is never read as a link.
### Logging
-Production code uses `org.apache.maven.plugin.logging.Log` (not SLF4J), obtained via `AbstractMojo.getLog()` or passed via constructor. For constructor-based logger injection see `CODE_WRITING_GUIDE.md`.
+`srcmorph` and `srcmorph-cli` log via `org.slf4j.Logger` (`private static final Logger LOGGER = ...`,
+enforced by each module's own `loggersArePrivateStaticFinal` ArchUnit rule). The plugin module's mojos
+still use `AbstractMojo#getLog()` (a Maven `Log`) at the mojo boundary only — everything they delegate
+to below that boundary is SLF4J.
### Null Safety
-- Mark nullable return types and parameters explicitly.
-- Prefer early null/empty guards with `log.warn(...)` over silent skips.
+- JSpecify `@Nullable`/`@NonNull` (default) annotations; NullAway + Checker Framework enforce this at
+ compile time in every module. Mark nullable return types and parameters explicitly.
+- Prefer early null/empty guards with a logged warning over silent skips.
### Named Constants
-Every meaningful literal (string keys, header field names, node types, version strings) must be a `public static final` or `private static final` named constant with Javadoc. See `CODE_WRITING_GUIDE.md`.
+Every meaningful literal (string keys, header field names, node types, version strings) must be a
+`public static final` or `private static final` named constant with Javadoc.
### License Headers
-All source files must include the Apache 2.0 license header wrapped in `// @formatter:off` / `// @formatter:on`. See any existing source file for the template.
+All source files must include the Apache 2.0 license header wrapped in `// @formatter:off` /
+`// @formatter:on` (or the file type's equivalent comment syntax — see `examples/` for the `#`/`REM`/
+XML-comment conventions used there; JSON/YAML example fixtures carry no inline header, see
+`REUSE.toml`).
### Records
-Immutable value types are implemented as Java `record` types (e.g., `AiMdDocument`, `AiMdHeader`, `AiPreparedPrompt`, `AiGenerationRequest`). Prefer records for data carriers.
+Immutable value types are Java `record`s where practical (e.g. `AiMdDocument`, `AiMdHeader`,
+`AiPreparedPrompt`, `AiGenerationRequest`, `GenerateResult`).
+
+### `useModulePath=false` (all three modules)
+
+Every module's `maven-surefire-plugin` configuration forces `false`.
+Each module ships a `module-info.java`, but it is release metadata for module-path *consumers* only —
+this reactor's own build, tests, and every real consumer today load these jars on the plain classpath
+(the production bytecode targets Java 8, where `module-info.class` is inert). Leaving Surefire's
+module-path auto-detection on would patch test classes into the named module and then also require
+every test-only dependency (e.g. `archunit-junit5`) to be explicitly module-readable, which
+`module-info.java` intentionally does not declare — so classpath mode is not a workaround, it is the
+actually-representative test environment.
---
@@ -367,45 +336,50 @@ Immutable value types are implemented as Java `record` types (e.g., `AiMdDocumen
| Workflow | Trigger | Purpose |
|---|---|---|
| `publish.yml` | Push, PR, manual dispatch | Unified build/test/coverage/package pipeline; publishes snapshots and Maven Central releases |
-| `codeql.yml` | Schedule / Push | GitHub CodeQL security scanning |
+| `codeql.yml` | Schedule/Push | GitHub CodeQL security scanning |
| `scorecard.yml` | Schedule / Push | OpenSSF Scorecard supply-chain security analysis |
| `osv-scanner.yml` | Schedule / Push / PR | Google OSV-Scanner dependency vulnerability scan |
-| `reuse.yml` | Push / PR | FSFE REUSE license-compliance check |
+| `reuse.yml` | Push / PR | FSFE REUSE license-compliance check (`fsfe/reuse-action`) |
| `claude-code-review.yml` | PR | AI-powered code review |
| `claude.yml` | Issue/PR comment with `@claude` | Claude Code interactive assistant |
+`publish.yml` still reflects the pre-reactor single-module layout in places (report globs, artifact
+paths); adapting it fully to the 3-module reactor is a separate, later step (see `TODO.md`) — do not
+assume it has already been updated.
+
---
## Dependencies Summary
-| Dependency | Version | Purpose |
-|---|---|---|
-| `net.ladenthin:llama` | 5.0.6 | llama.cpp JNI binding (GGUF inference); released on Maven Central, carries the prompt-cache/slot APIs + GPU classifier jars. Brings `slf4j-api` transitively, converged to 2.0.18 via ``. |
-| `org.apache.maven:maven-plugin-api` | 3.9.13 | Maven plugin API (provided) |
-| `org.apache.maven.plugin-tools:maven-plugin-annotations` | 3.15.1 | `@Mojo`, `@Parameter` annotations (provided) |
-
-Test-only:
-
-| Dependency | Version | Purpose |
+| Dependency | Version | Used by |
|---|---|---|
-| `org.junit.jupiter:junit-jupiter` | 6.1.0 | Test runner |
-| `org.hamcrest:hamcrest` | 3.0 | Matchers |
+| `net.ladenthin:llama` | 5.0.6 | `srcmorph` (`provider` package only) — llama.cpp JNI binding |
+| `org.slf4j:slf4j-api` | 2.0.18 (converged in the parent) | `srcmorph`, `srcmorph-cli`, the plugin |
+| `ch.qos.logback:logback-classic` | 1.5.37 (converged in the parent) | `srcmorph-cli` (runtime binding) |
+| `com.fasterxml.jackson.core:jackson-databind` | pinned in parent | `srcmorph-cli` (JSON config) |
+| `com.fasterxml.jackson.dataformat:jackson-dataformat-yaml` | pinned in parent | `srcmorph-cli` (YAML config) |
+| `org.apache.maven:maven-plugin-api` | 3.9.16 | `srcmorph-maven-plugin` (provided) |
+| `org.apache.maven.plugin-tools:maven-plugin-annotations` | 3.15.2 | `srcmorph-maven-plugin` (provided) |
+
+Test-only (every module): `org.junit.jupiter:junit-jupiter`, `org.hamcrest:hamcrest`,
+`com.tngtech.archunit:archunit-junit5`. `srcmorph` additionally uses jqwik (pinned ≤ 1.9.3 — see the
+jqwik policy link below), JMH, jcstress, Lincheck, vmlens.
---
## Test / Code Writing Compliance
-After modifying or creating any `.java` file:
+After modifying or creating any `.java` file, in whichever module it lives:
- For `*Test.java` files, follow the workspace version chain:
[`../workspace/guides/test/TEST_WRITING_GUIDE-8.md`](../workspace/guides/test/TEST_WRITING_GUIDE-8.md)
- (this repo is Java 8) **and** this repo's own `TEST_WRITING_GUIDE.md`
- (plugin-specific supplement).
+ (baseline) **and** this repo's own `TEST_WRITING_GUIDE.md` (repo-wide supplement; applies to every
+ module — there is one guide file at the repo root, not one per module).
- For production sources, follow the workspace version chain:
[`../workspace/guides/src/CODE_WRITING_GUIDE-8.md`](../workspace/guides/src/CODE_WRITING_GUIDE-8.md)
- (this repo is Java 8) **and** this repo's own `CODE_WRITING_GUIDE.md`.
-- Apply all fixable violations automatically; report only those that
- cannot be resolved without a large refactor.
+ (baseline) **and** this repo's own `CODE_WRITING_GUIDE.md`.
+- Apply all fixable violations automatically; report only those that cannot be resolved without a
+ large refactor.
---
@@ -417,12 +391,24 @@ See [`../workspace/workflows/pull-request-workflow.md`](../workspace/workflows/p
## Key Design Principles
-1. **Local-first** — all AI inference runs locally via llama.cpp; no cloud API calls, no data leaves the machine.
-2. **Deterministic indexing** — same source produces the same `.ai.md` skeleton (deterministic header); only the AI-generated body varies.
-3. **Incremental updates** — files with existing summaries are skipped unless `force=true`; checksums detect source changes.
-4. **Unified indexing and summarization** — each indexer (`SourceFileIndexer`, `PackageIndexer`) both creates the `.ai.md` skeleton and fills in AI fields in a single pass; no separate summarization step is needed.
-5. **Provider abstraction** — AI backends are pluggable through `AiGenerationProvider`; mock provider enables fully deterministic tests.
-6. **Configuration-driven prompts & rule-based routing** — prompt templates are defined in POM configuration, not hardcoded in Java; changing a prompt requires no code change. For the `generate` goal each `` is a routing **rule** (`AiFieldGenerationSelector`): a composable `` tree (``/``/`` over leaves extensions/size/lines/modifiedAfter/modifiedBefore/pathGlob — `AiCondition`/`AiConditionEvaluator`), a `` (highest matching wins, ties by order), and a kind — route (``+``), `true` (ignore), or exactly one explicit `true`. So one run can route different file kinds/sizes to different models **and** prompts; the model id (`aiDefinitionKey`) carries the full parameter set (path, context, sampling…), and the run loads each model once. A file matching no rule and no fallback fails the build; `aiIndex.planOnly=true` previews the plan tree without loading a model. Prompts live once in a shared plugin-level `` and are referenced by id across all rules/executions (no duplication).
+1. **Local-first** — all AI inference runs locally via llama.cpp; no cloud API calls, no data leaves
+ the machine.
+2. **Deterministic indexing** — the same source produces the same `.ai.md` skeleton (deterministic
+ header); only the AI-generated body varies.
+3. **Incremental updates** — files with existing summaries are skipped unless `force=true`; checksums
+ detect source changes.
+4. **One shared configuration object** — `net.ladenthin.srcmorph.config.SrcMorphConfiguration` is
+ bindable from Maven plexus XML, Jackson JSON/YAML (the CLI), or plain Java code, so a JSON/YAML key
+ always reads identically to the matching Maven `` XML element.
+5. **Provider abstraction** — AI backends are pluggable through `AiGenerationProvider`; the mock
+ provider enables fully deterministic tests everywhere.
+6. **Configuration-driven prompts & rule-based routing** — prompt templates and the ``
+ routing rules (composable `` tree, priority, skip, exactly one fallback) are data, never
+ hardcoded in Java; see `SrcMorphConfiguration`'s Javadoc and each engine's own Javadoc for the
+ `generate`/`aggregate-packages`/`aggregate-project`/`calibrate` semantics.
+7. **Staged, always-green migration** — the plugin's public coordinates never change out from under an
+ existing consumer mid-migration; the rename to `srcmorph-maven-plugin` is a deliberately isolated,
+ later step (see `TODO.md`).
## Javadoc Conventions
@@ -431,15 +417,25 @@ See [`../workspace/policies/javadoc-conventions.md`](../workspace/policies/javad
## SpotBugs Suppressions
See [`../workspace/policies/spotbugs-suppressions.md`](../workspace/policies/spotbugs-suppressions.md).
+Each module has its own `spotbugs-exclude.xml` (`srcmorph/spotbugs-exclude.xml`,
+`srcmorph-cli/spotbugs-exclude.xml`, `srcmorph-maven-plugin/spotbugs-exclude.xml`).
## Spotless Formatting
See [`../workspace/policies/spotless-formatting.md`](../workspace/policies/spotless-formatting.md).
-Run `mvn spotless:apply` before every commit that touches `.java` files.
+Run `mvn spotless:apply` before every commit that touches `.java` files (reactor-wide from the root, or
+scoped to one module with `-pl`).
## jqwik Policy
See [`../workspace/policies/jqwik-prompt-injection.md`](../workspace/policies/jqwik-prompt-injection.md).
+jqwik is a test dependency of `srcmorph` only.
+
+## Lombok Config
+
+See [`../workspace/policies/lombok-config.md`](../workspace/policies/lombok-config.md).
+`lombok.config` lives once at the repo root and is inherited by every module (Lombok walks up the
+directory tree from each source file looking for `lombok.config`).
## CI Test Diagnostics
@@ -448,18 +444,16 @@ See [`../workspace/policies/ci-test-diagnostics.md`](../workspace/policies/ci-te
## PIT Mutation Testing
See [`../workspace/policies/pit-mutation-testing.md`](../workspace/policies/pit-mutation-testing.md).
-Run PIT with the lifecycle prefix — `mvn test-compile org.pitest:pitest-maven:mutationCoverage`.
-
-## Lombok Config
-
-See [`../workspace/policies/lombok-config.md`](../workspace/policies/lombok-config.md).
+Run PIT with the lifecycle prefix, scoped to the gated module —
+`mvn test-compile org.pitest:pitest-maven:mutationCoverage -f srcmorph/pom.xml` (only `srcmorph` is
+PIT-gated today; see `TODO.md`).
## JPMS Module Descriptor
-This repo ships a `module-info.java` compiled in a separate `release 9` execution. Javadoc
-currently runs in **classpath mode** (javadoc `` resolves to `8`), which is the *only*
-thing keeping it clear of the JPMS module-mode javadoc trap that bit BAF. **Before raising the
-Java / javadoc source level to ≥ 9, read**
+Each module ships a `module-info.java` compiled in a separate `release 9` execution, and each module's
+Javadoc runs in **classpath mode** (`` resolves to `8`), which is the *only* thing keeping it
+clear of the JPMS module-mode javadoc trap that bit BAF. **Before raising the Java / javadoc source
+level to ≥ 9 in any module, read**
[`../workspace/policies/jpms-module-descriptor.md`](../workspace/policies/jpms-module-descriptor.md).
## Open TODOs
diff --git a/CODE_WRITING_GUIDE.md b/CODE_WRITING_GUIDE.md
index 9a31ec0..ab53f8c 100644
--- a/CODE_WRITING_GUIDE.md
+++ b/CODE_WRITING_GUIDE.md
@@ -1,4 +1,4 @@
-# Code Writing Guide — llamacpp-ai-index-maven-plugin (Plugin-Specific Supplement)
+# Code Writing Guide — srcmorph (reactor-wide Supplement)
> **Canonical workspace rules** for production sources live in
> [`../workspace/guides/src/CODE_WRITING_GUIDE-8.md`](../workspace/guides/src/CODE_WRITING_GUIDE-8.md)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b1dbcf0..d446106 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -30,13 +30,13 @@ mvn package -DskipTests
mvn install
# Run the plugin against itself (self-test; requires a local GGUF model)
-mvn clean install -Pai-index-selftest
+mvn clean install -Psrcmorph-selftest
# Run with native llama.cpp JNI tests enabled
-mvn clean install -Pai-index-selftest -DrunNativeLlamaTests=true
+mvn clean install -Psrcmorph-selftest -DrunNativeLlamaTests=true
# Skip AI generation during build
-mvn clean install -DaiIndex.skip=true
+mvn clean install -Dsrcmorph.skip=true
```
### Offline / restricted-network environments
diff --git a/README.md b/README.md
index 4a42e75..9c41029 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@
[](https://coveralls.io/github/bernardladenthin/llamacpp-ai-index-maven-plugin?branch=main)
[](https://codecov.io/gh/bernardladenthin/llamacpp-ai-index-maven-plugin)
[](https://codecov.io/gh/bernardladenthin/llamacpp-ai-index-maven-plugin)
-[-brightgreen)](https://github.com/bernardladenthin/llamacpp-ai-index-maven-plugin/actions/workflows/publish.yml)
+[-brightgreen)](https://github.com/bernardladenthin/llamacpp-ai-index-maven-plugin/actions/workflows/publish.yml)
**Quality:**
[](https://sonarcloud.io/dashboard?id=bernardladenthin_llamacpp-ai-index-maven-plugin)
@@ -36,12 +36,6 @@
[](https://sonarcloud.io/dashboard?id=bernardladenthin_llamacpp-ai-index-maven-plugin)
**Security:**
-
[](https://snyk.io/test/github/bernardladenthin/llamacpp-ai-index-maven-plugin?targetFile=pom.xml)
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fbernardladenthin%2Fllamacpp-ai-index-maven-plugin?ref=badge_shield)
[](https://libraries.io/github/bernardladenthin/llamacpp-ai-index-maven-plugin)
@@ -72,594 +66,151 @@ using the assigned ID:
[](https://treeware.earth)
[](https://stand-with-ukraine.pp.ua)
-# llamacpp-ai-index-maven-plugin
-A Maven plugin for generating hierarchical, AI-readable documentation of source code projects using local llama.cpp-compatible models.
-It creates structured `.ai.md` files per source file and aggregates them into package-level summaries for fast semantic navigation and retrieval.
-## Features
-- Generate AI summaries for source files
-- Per-language prompts — Java, SQL schema, and a generic fallback — selected by file extension
-- Weave searchable type, API and domain names into every summary
-- Aggregate summaries at package level
-- Build a single project index (one line + link per package) for top-down navigation, optionally with a one-call AI `#### Overview`
-- Run any phase independently — toggle file / package / project on or off
-- Exclude trivial or generated files with glob patterns
-- Uses local models via llama.cpp (no cloud dependency)
-- Incremental updates (skips unchanged files)
-- Logs a rough per-file duration estimate before each generation (size, token count, expected time)
-- Optimized for AI-assisted code understanding
-## How It Works
-The plugin runs in three phases, building a navigable index from fine to coarse.
-### 1. File Generation (generate)
-- Scans configured source directories
-- Creates `.ai.md` files per source file
-- Each file contains metadata header and markdown summary
-### 2. Package Aggregation (aggregate-packages)
-- Traverses generated `.ai.md` files
-- Builds hierarchical package summaries
-- Produces `package.ai.md` files; the header carries a deterministic `F` link list to each child (package → file navigation)
-### 3. Project Index (aggregate-project)
-- Harvests the one-line lead from every `package.ai.md` — the per-package listing is deterministic, no AI call
-- Produces a single `project.ai.md`: one body line per package (its lead) with the clickable links in the header `F` list
-- A compact, always-loadable table of contents an agent reads first to navigate down
-- **Optional** (opt-in): configure a `` on this goal and it makes *one* extra AI call to
- write a short `#### Overview` paragraph from the package leads. The deterministic per-package listing is
- unchanged; with no field generation the goal stays purely deterministic and calls no model.
-## Example Output
-```
-### AiMdDocument.java
-- H: 1.0
-- C: A48CED8C
-- D: 2026-03-15T23:31:52Z
-- T: 2026-03-19T18:13:31Z
-- G: 1.0.0
-- A: 0.0.0
-- X: file
----
-> Immutable value type pairing a deterministic metadata header with the AI-generated markdown body of one .ai.md document.
-
-#### Purpose
-- Hold one parsed `.ai.md` document as an `AiMdHeader` plus its markdown body.
-
-#### Type
-- record-shaped value class (Java); marked `@ConvertToRecord`.
-
-#### Public API
-- `header() -> AiMdHeader` — the document's metadata header.
-- `body() -> String` — the AI-generated markdown body.
-```
-## Requirements
-- Java 8+ (production code targets Java 8; CI builds on Java 8 via temurin)
-- Maven 3.6.3+
-- Local GGUF model (llama.cpp compatible)
-
-### Running under Java 8: override `checker-qual`
-
-The plugin itself is compiled to Java 8 bytecode, but it pulls in
-[`org.checkerframework:checker-qual`](https://central.sonatype.com/artifact/org.checkerframework/checker-qual)
-transitively, and the version it builds against (`4.2.0`) ships its classes as **Java 11
-bytecode** (class-file major version 55). On a **Java 8** JVM, loading those annotation
-classes fails with `UnsupportedClassVersionError`.
-
-If you run the plugin under a Java 8 Maven/JVM, **override `checker-qual` to `3.55.1`** — the
-**last release whose runtime classes are Java 8 bytecode** (major 52). The `checker-qual` line
-switched to Java 11 bytecode in `4.0.0`; every `3.x` release (`3.42.0` … `3.55.1`) is Java 8
-loadable, and `3.55.1` is the newest of them. (`checker-qual` `3.43.0`–`3.55.1` additionally
-carry a root `module-info.class`, which a Java 8 classpath simply ignores; if you want a jar
-with no `module-info.class` at all, `3.42.0` is the last such release.)
-
-Because this is a *plugin* dependency, the override goes inside the plugin's own
-`` block (a project-level `` does **not** affect a plugin's
-classpath):
-
-```xml
-
- net.ladenthin
- llamacpp-ai-index-maven-plugin
-
-
-
-
- org.checkerframework
- checker-qual
- 3.55.1
-
-
-
-```
-
-Running the plugin under Java 11 or newer needs no override.
+# srcmorph
+
+**srcmorph** is a prompt-driven source-tree transformer: it walks a source tree and processes each
+file through a configurable local LLM prompt — via [llama.cpp](https://github.com/ggerganov/llama.cpp)
+GGUF models, entirely local, no cloud calls — producing layered output: per-file, then per-package,
+then per-project. Today it emits structured, AI-readable `.ai.md` Markdown summaries for fast semantic
+code navigation; the same rule-routed pipeline (composable file-matching conditions, per-rule
+model + prompt, oversize handling) is generic enough to eventually emit wikis, architecture docs,
+diagrams, or even source-to-source transformations.
+
+> **Migration note.** This repository was restructured from a single Maven plugin into a reactor of
+> four artifacts — see "Module overview" below. As part of that migration the Maven plugin was
+> **renamed** from `net.ladenthin:llamacpp-ai-index-maven-plugin` to
+> `net.ladenthin:srcmorph-maven-plugin` (goal prefix `srcmorph`, properties `srcmorph.*`). Existing
+> consumers who still declare the old coordinates are **not** broken: the old artifactId is
+> published as a tiny relocation-stub POM (``) that redirects
+> Maven to the new coordinates automatically — no consumer action required, though updating your own
+> POM to the new coordinates directly is recommended. See [`CLAUDE.md`](CLAUDE.md) and
+> [`TODO.md`](TODO.md) for the full migration status.
+
+## Module overview
+
+| Module | Artifact | What it is |
+|---|---|---|
+| [`srcmorph/`](srcmorph/README.md) | `net.ladenthin:srcmorph` | The core library: a framework-free Java API (no Maven dependency) — the shared `SrcMorphConfiguration` plus the `generate`/`aggregate-packages`/`aggregate-project`/`calibrate` engines. Use this to embed srcmorph in your own tooling. |
+| [`srcmorph-cli/`](srcmorph-cli/README.md) | `net.ladenthin:srcmorph-cli` | A standalone CLI driven by one JSON or YAML configuration file (ships as a `java -jar`-ready fat jar). No Maven project required. |
+| [`srcmorph-maven-plugin/`](srcmorph-maven-plugin/README.md) | `net.ladenthin:srcmorph-maven-plugin` | The Maven plugin, a thin wrapper around `srcmorph` — **renamed** from `net.ladenthin:llamacpp-ai-index-maven-plugin` (goal prefix `srcmorph`, was `ai-index`; properties `srcmorph.*`, were `aiIndex.*`). |
+| [`llamacpp-ai-index-maven-plugin/`](llamacpp-ai-index-maven-plugin/pom.xml) | `net.ladenthin:llamacpp-ai-index-maven-plugin` (pinned at `1.0.4`) | A tiny relocation-stub POM — no source, no dependencies — that redirects Maven Central consumers of the old plugin coordinates to `srcmorph-maven-plugin`. |
-## Dependency
+The first three modules (plus the reactor parent pom) are released together from this one
+repository at the same version; the relocation stub is versioned and published independently (see
+`CLAUDE.md`).
-The plugin depends on [`net.ladenthin:llama`](https://central.sonatype.com/artifact/net.ladenthin/llama), the Java/JNI binding for llama.cpp.
-It is published on Maven Central and resolves automatically — no manual installation required.
+## Quickstart
-```xml
-
- net.ladenthin
- llama
- 5.0.3
-
-```
-## Configuration
-The plugin is configured from three building blocks, declared on the plugin inside
-``:
-
-1. **``** — define each GGUF model once (path + sampling parameters), each with a ``.
-2. **``** — define each prompt template once, each with a ``. A template takes
- two `%s` placeholders: the file/package name and the source (for packages, the child summaries; for
- the optional project overview, the per-package leads).
-3. **``** — per goal, the routing **rules**. Each rule maps a `` to an
- `` (model id, which carries the full parameter set) and selects files with a
- composable **``** tree: ``/``/`` over the leaves ``, ``
- (``/`` bytes), `` (``/``), ``/`` (ISO-8601
- instant vs the file's last-modified time), and `` (base-relative glob). When several rules
- match, the highest `` wins (ties by declaration order). A rule may instead be
- `true` (ignore matching files — a high-priority skip beats routes and the fallback), and
- **exactly one** `true` (no condition) catches everything else. A file that
- matches no rule and no fallback **fails the build**. So one `generate` run can index different file
- kinds/sizes with **different models *and* prompts**; it loads each model once. Run with
- `-DaiIndex.planOnly=true` to print the routing plan (a copy-pasteable Markdown table: file → rule id →
- prompt → context-window fit → rough time estimate, summed per model and overall) and stop before
- loading any model. The plan also checks each file against its routed model's **context window**: a
- file too large for the window would lose content if trimmed. What happens then is **configuration
- only** — the plugin never picks a model for you — and is chosen per rule with ``:
- - **`fail`** *(default)* — abort the build (a hard fail). The fix is to add a `` rule
- with a size `` that routes oversized files to a model with a large enough window (see the
- `granite-4.0-h-tiny-bigwindow` definition + the `big-window-java` / `big-window-sql` rules in the
- POM — IBM Granite 4.0-H-Tiny, Apache-2.0, a hybrid Mamba model whose KV cache grows only linearly,
- configured at a 384K window to cover files up to ~1 MB; verified summarizing a ~995 KB / ~268K-token
- file on an 8 GB GPU with no OOM).
- - **`sample`** — feed only the head of the file (trimmed to the window) in a single call. Fast and
- bounded; good for repetitive data where the head represents the whole.
- - **`mapReduce`** — split the file into window-sized chunks at line boundaries (with overlap so
- records are never torn mid-syntax), summarize each chunk, then combine the partial summaries in one
- final call. A `` cap bounds the time (a representative subset — always head + tail,
- evenly spaced — is summarized when the file would exceed the cap), so an arbitrarily large file
- (e.g. 7 MB) stays within a chosen per-file budget. **Route oversized files to a *small*, fast model
- for this — not the big-window one.** Prefill is `O(n²)` in prompt length, so chunks should be small:
- many cheap small-window passes are far faster than a few giant ones (one 384K-token pass alone can
- dwarf a whole run). E.g. a 16K-window model with `maxChunks=6` is ~7 calls (~1 h order on a
- reference CPU; less on GPU) and samples a representative slice — the right trade-off for repetitive
- data. mapReduce *on* a big window is the slowest possible combination; avoid it.
- - **`deterministic`** — no model at all: emit a deterministic body (size, line count, head/tail
- sample). Instant; for pure data where no AI analysis is needed.
-
- Quality from a real model is best within Granite's validated 128K (~500 KB) and degrades gradually
- beyond it — a whole-file (or chunked) summary still beats a trimmed one.
-
- **Exact counts with `` (optional, every file — not just oversize).** A sampled/chunked AI
- summary can't reliably *count* things (no single call sees the whole file — it will guess "25 rows").
- Add a `` list to a rule and each `{label, pattern}` reports its regex match count over the
- **whole** source, prepended to the body of every file the rule matches as an exact facts line — so
- downstream agents get authoritative structural counts in every summary. It's fully generic — the
- meaning is in the regex, so the same mechanism counts SQL `INSERT` rows or Java `\bboolean\b` fields;
- multi-line matching is opt-in via the inline `(?m)` flag. Keep patterns robust (a fact that miscounts
- is worse than none). Example: `**Facts (exact, whole file):** INSERT rows: 36738; tables: 122; views: 4`.
+### 1. Maven plugin
```xml
net.ladenthin
- llamacpp-ai-index-maven-plugin
- 1.0.2
-
+ srcmorph-maven-plugin
+ 1.1.0
-
-
- src/main/java/com/example
-
-
- llamacpp-jni
-
-
+ mock
+
+
+ file-body
+
+
+
+
+
+ file-body
+ coder
+ true
+
+ coder/path/to/model.gguf
- 32768
- 1536
- 0.7
- 8
+
+
+```
-
-
-
- file-body-java
-
-
-
- file-body-fallback
-
+Deprecated: old net.ladenthin:llamacpp-ai-index-maven-plugin coordinates (auto-relocates)
-File: %s
+```xml
+
+ net.ladenthin
+ llamacpp-ai-index-maven-plugin
+ 1.0.4
+
+
+```
-Source:
-%s]]>
-
-
- package-body
-
-Package: %s
+### 2. Standalone CLI
-File summaries:
-%s]]>
-
-
-
- project-body
- -jar-with-dependencies.jar examples/config_All.json
+```
-Index file: %s
+Every `examples/config_*.json`/`.yaml` file works out of the box with the `mock` provider (no GGUF
+model required) — see [`examples/`](examples/) for the full set plus paired `run_*.sh`/`run_*.bat`
+launcher scripts, and [`srcmorph-cli/README.md`](srcmorph-cli/README.md) for the config-file
+reference.
-%s]]>
-
-
-
+### 3. Library dependency
-
-
-
- ai-generate
- generate-resources
- generate
-
-
-
-
- java
- file-body-java
- coder
-
- .java
-
-
-
- fallback
- true
- file-body-fallback
- coder
-
-
-
-
-
- ai-aggregate-packages
- process-resources
- aggregate-packages
-
-
-
- package-body
- coder
-
-
-
-
-
-
- ai-aggregate-project
- prepare-package
- aggregate-project
-
-
-
- project-body
- coder
-
-
-
-
-
-
+```xml
+
+ net.ladenthin
+ srcmorph
+ 1.1.0
+
```
-### Routing rules (conditions, priority, skip, plan)
-Within one `generate` run you can route files to different models **and** prompts by size, language,
-age or path. Example — small/medium/large Java files to three context presets, skip generated sources,
-and a fallback for everything else:
-
-```xml
-
-
- skip-generatedtrue100
- **/generated/**
-
-
- java-smallfile-body-java-tersegpt-oss-20B-c16k
-
- .java
- 16384
-
-
-
- java-midfile-body-javagpt-oss-20B-c48k
-
- .java
- 1638449152
-
-
-
- java-largefile-body-java-detailedgpt-oss-20B-c96k
-
- .java
- 49152
-
-
-
- java-hugefile-body-javagpt-oss-20B-c16k
- mapReduce6
-
- \bboolean\b
- (?m)^\s+(public|private|protected).*\(
-
-
- .java
- 1048576
-
-
-
- fallbacktrue
- file-body-fallbackgpt-oss-20B-c96k
-
-
+```java
+SrcMorphConfiguration config = new SrcMorphConfiguration();
+config.setBaseDirectory(new File("."));
+// ... set outputDirectory, promptDefinitions, aiDefinitions, fieldGenerations ...
+new GenerateEngine(config).execute();
```
-Notes: size bounds are **min-exclusive / max-inclusive** so `band2.min == band1.max` is non-overlapping;
-`` works the same way; `2026-01-01T00:00:00Z` only (re)indexes
-recently changed files. `` (`fail` *(default)* / `sample` / `mapReduce` / `deterministic` —
-see the context-window note above) chooses what a rule does when a file is still larger than its routed
-model's window; `` caps how many chunks `mapReduce` summarizes. Preview the mapping without
-running a model:
+See [`srcmorph/README.md`](srcmorph/README.md) for the full API tour (the four engines, exception
+types, and a complete embedding example).
-```
-mvn ai-index:generate -DaiIndex.planOnly=true
-```
+## How It Works
-## Usage
-Run AI index generation:
-```
-mvn clean install -Pai-index-selftest
-```
-With native llama tests:
-```
-mvn clean install -Pai-index-selftest -DrunNativeLlamaTests=true
-```
-## Plugin Configuration
-Run-level parameters (set in ``):
-- `outputDirectory` — target directory for `.ai.md` files (default: `${project.basedir}/src/site/ai`)
-- `subtrees` — source directories to index, relative to the project base dir (default: `src/main/java`)
-- `fileExtensions` — file extensions to index (default: `.java`)
-- `excludes` — glob patterns for source files to skip, matched against each file's path relative to
- the base dir with `/` separators, e.g. `**/package-info.java`, `**/generated/**` (default: none).
- `*` stays within one path segment, `**` spans directories, `?` is a single character.
-- `generationProvider` — AI backend: `mock` (default) or `llamacpp-jni`
-- `force` — regenerate even when a body already exists (default: `false`)
-- `skip` — global switch: skip **every** phase (default: `false`)
-- Per-phase switches — turn any of the three phases on/off independently (each default `false`).
- Named after the three index levels (`file` / `package` / `project`, the `x` node types):
- - `aiIndex.file.skip` — skip the **file** phase (the `generate` goal)
- - `aiIndex.package.skip` — skip the **package** phase (the `aggregate-packages` goal)
- - `aiIndex.project.skip` — skip the **project** phase (the `aggregate-project` goal)
-
- So `-DaiIndex.package.skip=true` runs file + project only, `-DaiIndex.skip=true` runs
- nothing, and the defaults run all three.
-- `aiDefinitions` / `promptDefinitions` — named models / prompt templates, referenced by key
-- `fieldGenerations` — per goal: which `promptKey` runs with which `aiDefinitionKey` (**required**)
-
-### Per-model `` parameters
-
-Every model knob lives inside its `` (referenced by `aiDefinitionKey`), not as a top-level
-plugin parameter. Only `key` and `modelPath` are required; everything else has a default. The defaults
-below are the shipped values (`AiGenerationConfig.DEFAULT_*`).
-
-| Element | Default | Description |
-|---|---|---|
-| `key` | *(required)* | Identifier referenced by a rule's `aiDefinitionKey` |
-| `modelPath` | *(required)* | Path to the GGUF model file |
-| `contextSize` | `32768` | Context window in tokens |
-| `maxOutputTokens` | `128` | Max generated tokens per call |
-| `threads` | `8` | CPU threads for inference |
-| `temperature` | `0.15` | Sampling temperature |
-| `topP` | `0.9` | Nucleus (top-p) sampling threshold |
-| `topK` | `40` | Top-k sampling limit (`0` = disabled) |
-| `minP` | `0.0` | Min-p sampling threshold (`0.0` = disabled) |
-| `topNSigma` | `-1.0` | Top-n-sigma sampling threshold (`-1.0` = disabled) |
-| `repeatPenalty` | `1.0` | Repetition penalty (`1.0` = disabled) |
-| `charsPerToken` | `4` | Chars-per-token estimate; drives the automatic `maxInputChars` trim budget (`maxInputChars` itself is derived, not a field). Use a value at or below your model's real ratio so the budget stays conservative |
-| `warnOnTrim` | `true` | Log a warning when the source is trimmed to fit the window |
-| `cachePrompt` | `true` | Reuse the shared prompt-prefix KV across files (`cache_prompt`) |
-| `swaFull` | `true` | Keep the full-size sliding-window-attention KV cache (`--swa-full`) |
-| `cacheReuse` | `256` | KV prefix-reuse minimum chunk size in tokens (`--cache-reuse`; `0` = off) |
-| `gpuLayers` | `-1` | GPU layers to offload (`--gpu-layers`); `-1` = auto-fit to free VRAM, `0` = force CPU, `>0` = partial. GPU native only |
-| `mainGpu` | `-1` | Primary GPU index (`--main-gpu`); `-1` = leave default. Matters on multi-GPU hosts (e.g. a Vulkan build enumerates every GPU) |
-| `devices` | *(empty)* | Explicit device selection (`--device`), comma-separated backend device names (e.g. `Vulkan1`); takes precedence over `mainGpu` |
-| `chatTemplateEnableThinking` | `true` | Enable the chat template's thinking mode |
-| `reasoningEffort` | `low` | gpt-oss harmony reasoning effort (`low`/`medium`/`high`); empty omits the kwarg (e.g. for non-gpt-oss models) |
-| `reasoningBudgetTokens` | `-1` | Cap on harmony reasoning tokens (`-1` = unrestricted) |
-| `dryMultiplier` | `0.0` | DRY repetition-penalty multiplier (`0.0` = disabled); the other `dry*` knobs only apply when this is `> 0` |
-| `dryBase` | `1.75` | DRY exponential base |
-| `dryAllowedLength` | `2` | Longest n-gram that may repeat without DRY penalty |
-| `dryPenaltyLastN` | `-1` | DRY look-back window in tokens (`-1` = whole context, `0` = off) |
-| `drySequenceBreakers` | *(empty)* | DRY sequence-breaker strings; empty = the binding defaults |
-| `stopStrings` | *(empty)* | Extra stop strings that end generation |
-
-## Prompt System
-Prompts are defined in the plugin configuration (``) and referenced by key
-from ``. The self-test profile defines five:
-- `file-body-java` — summarizes a single Java source file (types, public API, dependencies)
-- `file-body-sql` — summarizes a single SQL file as schema (tables/views/procedures, columns,
- the tables it reads vs writes, and relationships)
-- `file-body-fallback` — generic multi-language summary for any other source file
-- `package-body` — synthesizes a package summary from the already-generated file summaries
-- `project-body` — (optional) synthesizes the short project `#### Overview` paragraph from the package leads
-
-For the `generate` goal the file-level prompt is selected per file by extension: the first field
-generation whose `` matches the file name wins; otherwise the first entry without a
-`` filter is the fallback. A single field generation with no filter (the historical
-shape) keeps working — it is simply the fallback for every file.
-
-Each summary begins with a one-sentence blockquote lead, followed by structured `####` sections.
-Prompts are optimized to avoid code blocks, formatter artifacts, and empty outputs, and to produce structured markdown.
-## Output Structure
-```
-src/site/ai/
-└── main/
- └── java/
- └── com/
- └── example/
- ├── MyClass.java.ai.md
- ├── AnotherClass.java.ai.md
- └── package.ai.md
-```
-## Design Principles
-- Deterministic metadata (hash-based change detection)
-- Separation of concerns (header = metadata, body = summary)
-- AI-friendly structure (predictable and hierarchical)
-- Local-first (no external APIs required)
-## Known Limitations
-- Model output may require normalization (handled in code)
-- Large models increase runtime
-- Output quality depends on chosen model
-
-## TODO
-- **Expand PIT mutation-testing scope.** `` in `pom.xml` lists an explicit subset of classes verified at 100% mutation parity; widen it incrementally toward the whole `net.ladenthin.maven.llamacpp.aiindex.*` tree (the streambuffer whole-package model) as more classes reach parity. Generic PIT setup and invocation: see the [PIT policy](../workspace/policies/pit-mutation-testing.md).
-## Recommended Models
-Based on an 8-model × 2-prompt benchmark run against this codebase — full results, per-model
-pros/cons, a source-faithfulness deep-dive, and reproduction steps in
-[docs/ai-index-benchmark](docs/ai-index-benchmark/COMPARISON.md):
-
-- **`gpt-oss-20B-mxfp4` — the production default** (switch with `-Dai.model=`). The native MXFP4
- quant at a 96K window; it inherits the benchmark's accuracy lead (gpt-oss-20b was most *accurate* per
- file, won 5/6 in the per-file matrix — measured on the `c96k`/UD-Q4_K_XL quant, but E5 shows quant
- choice is within noise so the native MXFP4 is the better-quality swap), run at `reasoningEffort=low`
- and a 96K window so it covers files up to ~250 KB untrimmed. Slowest of the set (~2× the 30B) — the
- accepted cost for accuracy. See the preset/timing details below.
-- **Qwen3-Coder-30B-A3B-Instruct** — throughput alternative (and best of the non-reasoning models for
- large Java files): most complete/faithful of the fast models, code-specialized, Apache-2.0,
- ~3.3B-active MoE, 262K context. Pick it when throughput beats the last points of fidelity.
-- **Granite-4.0-H-Tiny** — fastest on CPU (~4×, flat-KV hybrid, Apache-2.0); best for very large
- or many files when throughput beats the last points of fidelity.
-- **Seed-Coder-8B-Instruct** — clean, permissive (MIT) small dense coder.
-- Avoid `Qwen3.5-4B` (thinking tax, no quality gain).
-
-### gpt-oss-20b presets, large files, and timing
-
-`gpt-oss` is a *reasoning* model whose analysis tokens share the output budget, so use
-`reasoningEffort=low` for code summaries (best quality here) and size the budget to the file. The pom
-ships three ready presets, tiered by the largest file you must cover — full rationale and measurements
-in [COMPARISON.md §11](docs/ai-index-benchmark/COMPARISON.md):
-
-| Preset | context | covers up to | ~ time (CPU) |
-|---|---|---|---|
-| `gpt-oss-20B-c16k` | 16K | ~40 KB | ~1–2 min |
-| `gpt-oss-20B-c48k` | 48K | ~125 KB | ~25 min @ 100 KB |
-| **`gpt-oss-20B-c96k` (default)** | 96K | ~260 KB | ~80 min @ 250 KB |
-
-- **`c96k` is the default:** a measured A/B shows a wider context window costs only RAM, not per-file
- time, so it covers every file up to ~250 KB with no trimming while small files stay just as fast.
- Downshift only to save RAM. Hard ceiling is the 128K window (~480–500 KB of code).
-- **Timing is quadratic, not linear:** prefill ≈ `24.4·n + 0.000674·n²` ms (n = prompt tokens),
- because attention is O(n) per token — which is why throughput drops as files grow. The plugin logs
- this estimate per file.
-
-## GPU acceleration (opt-in)
-The default native is CPU (Ninja build, bundled in the main `net.ladenthin:llama` jar). On an NVIDIA
-RTX 3070 a CUDA build measured **~4.5× the CPU decode speed**; Vulkan also works (AMD + NVIDIA) but pays
-a one-time shader-compilation cost on the first run. OpenCL is intentionally not offered (llama.cpp's
-OpenCL backend does not support NVIDIA GPUs).
-
-How the native is found: `net.ladenthin.llama.loader.LlamaLoader` tries `net.ladenthin.llama.lib.path`,
-then `java.library.path`, then **extracts the native bundled in whatever `net.ladenthin:llama` jar is on
-the classpath**. So there are two ways to enable a GPU when running the *published* plugin in your own
-build.
-
-**Recommended — add the GPU classifier to the plugin's own classpath.** Declare the matching
-`net.ladenthin:llama` classifier as a dependency of the plugin in your POM; the loader then extracts the
-GPU `jllama.dll` from it — no library path to manage:
+srcmorph runs in three phases, building a navigable index from fine to coarse — see each module's own
+README for the full details (routing rules, oversize strategies, exact-count facts, the project
+overview) and [`CLAUDE.md`](CLAUDE.md) for the architecture.
-```xml
-
- net.ladenthin
- llamacpp-ai-index-maven-plugin
- ...
-
-
- net.ladenthin
- llama
- 5.0.3
- cuda13-windows-x86-64
-
-
-
-```
+1. **File generation** — scans configured source directories, writes one `.ai.md` file per source
+ file (deterministic metadata header + AI-generated Markdown body).
+2. **Package aggregation** — traverses the generated `.ai.md` files, writes one `package.ai.md` per
+ directory with a deterministic child-link list plus an AI-generated summary.
+3. **Project index** — harvests the one-line lead from every `package.ai.md` into a single
+ `project.ai.md` table of contents (deterministic; an optional one-call AI overview paragraph can be
+ enabled).
-```
-mvn ai-index:generate -Dai.gpuLayers=20 # + the GPU runtime on PATH (see below)
-```
+Each phase is independently switchable and incremental (checksums skip unchanged files); a rule-based
+router lets one run send different files to different models **and** prompts by extension, size, age,
+or path.
-**Alternative — runtime library override (no POM change).** Point `net.ladenthin.llama.lib.path` at a
-folder holding the GPU `jllama.dll` (extracted once from the classifier jar); it is tried before the
-bundled native:
+## Snapshot builds
-```
-mvn ai-index:generate -Dnet.ladenthin.llama.lib.path=C:\path\to\gpu-native -Dai.gpuLayers=20
-```
+Snapshots are published to the Central Snapshots repository on every push to `main`:
-In both cases:
-
-- **CUDA** needs a matching CUDA 13 toolkit + driver, and the toolkit's `bin\x64` (with `cudart64_13.dll`,
- `cublas64_13.dll`) on `PATH` — the classifier jar bundles only `jllama.dll`, not the CUDA runtime.
-- **`ai.gpuLayers`** (on the gpt-oss presets): `-1` (default) does **not** pin a layer count, so
- llama.cpp **auto-fits** as many layers as fit the card's free VRAM — the robust "runs on any card"
- setting (it never over-commits, so no OOM on a 6 GB card, and uses more layers on a bigger one). Pin a
- positive number only to force a specific **partial** split (a fixed count disables auto-fit), or `0` to
- force CPU. Measured on an 8 GB RTX 3070, auto-fit gpt-oss-20b ≈ 29 decode t/s (vs ≈ 8 on CPU); a card
- with ≥ 16 GB fits all layers and is far faster.
-- **Picking a GPU on a multi-GPU host** (`ai.mainGpu` / `ai.devices` on the gpt-oss presets): a **CUDA**
- build only enumerates NVIDIA devices, so a single-NVIDIA host needs nothing. A **Vulkan** build
- enumerates *every* GPU (an integrated GPU is often device `0`), so the default may pick the slower one —
- set `-Dai.mainGpu=1` to select the discrete GPU, or `-Dai.devices=Vulkan1` for explicit device names
- (these map to the binding's `--main-gpu` / `--device`). On any model definition the same knobs are the
- `` / `` elements.
-
-**Profiles (this repo's own reactor build only — test/benchmark).** `-P gpu-cuda` / `-P gpu-vulkan`
-swap the `net.ladenthin:llama` classifier (via the `llama.classifier` property) for the reactor's
-test/compile classpath — handy for the native test or benchmarking on GPU here. They do **not** change
-the native used when the *published* plugin runs in another build (the POM is not flattened, so the
-classifier stays a property that resolves to the CPU default downstream) — use one of the two methods
-above for real indexing.
-
-## Development
-
-Run full build:
-```
-mvn clean install
-```
-Skip AI generation:
```
-mvn clean install -DaiIndex.skip=true
+https://central.sonatype.com/repository/maven-snapshots/net/ladenthin/
```
-### Contributors: do not upgrade jqwik past 1.9.3
-
-> ⚠️ **DO NOT UPGRADE jqwik past 1.9.3.** jqwik 1.10.0 added an anti-AI prompt-injection string to test stdout; the 1.10.1 user guide states the library "is not meant to be used by any 'AI' coding agents at all." 1.9.3 is the last pre-disclosure release and is the pinned version. See `CLAUDE.md` section "jqwik prompt-injection in test output" for the full context. Dependabot is configured to ignore **all** `net.jqwik` updates (every version, including patches) — see the `ignore` rule in [`.github/dependabot.yml`](./.github/dependabot.yml).
+Current reactor version: `1.1.0`.
## A Note on History
@@ -669,13 +220,5 @@ on April 4, 2026. Google Cloud eventually formalized the very same pattern into
announced on June 12, 2026.
## License
-Apache License 2.0
----
-
-
-🍺 Beer-driven architectural review (2026-05-26)
-
-> _Sometimes you spend two beers debating "architektonisch" vs "architekturiell" and the only conclusion is: ship the code anyway. Cheers!_
-
-
\ No newline at end of file
+Apache License 2.0
diff --git a/REUSE.toml b/REUSE.toml
index 55888a5..85b73b0 100644
--- a/REUSE.toml
+++ b/REUSE.toml
@@ -7,18 +7,28 @@ SPDX-PackageDownloadLocation = "https://github.com/bernardladenthin/llamacpp-ai-
path = [
"**.md",
"CITATION.cff",
- "llamacpp-ai-index-maven-plugin/src/site/ai/empty.md",
+ "srcmorph-maven-plugin/src/site/ai/empty.md",
"docs/RELEASE.md",
".github/ISSUE_TEMPLATE/bug_report.md",
".github/ISSUE_TEMPLATE/feature_request.md",
".github/PULL_REQUEST_TEMPLATE.md",
+ # JSON/YAML example configuration fixtures (no comment syntax; same precedent as
+ # BitcoinAddressFinder's examples/config_*.json entries in its own REUSE.toml).
+ "examples/config_Plan.json",
+ "examples/config_GenerateFileIndex.json",
+ "examples/config_GenerateFileIndex.yaml",
+ "examples/config_All.json",
+ "examples/config_Calibrate.json",
+ # Pre-existing private test fixtures, same "no comment syntax" JSON/YAML precedent.
+ "srcmorph-cli/src/test/resources/test-fixtures/minimal-generate.json",
+ "srcmorph-cli/src/test/resources/test-fixtures/minimal-generate.yaml",
]
precedence = "aggregate"
SPDX-FileCopyrightText = "2026 Bernard Ladenthin "
SPDX-License-Identifier = "Apache-2.0"
[[annotations]]
-path = "llamacpp-ai-index-maven-plugin/src/test/resources/SmolLM2-135M-Instruct-Q3_K_M.gguf"
+path = "srcmorph/src/test/resources/SmolLM2-135M-Instruct-Q3_K_M.gguf"
precedence = "aggregate"
SPDX-FileCopyrightText = "HuggingFaceTB"
SPDX-License-Identifier = "Apache-2.0"
diff --git a/TEST_WRITING_GUIDE.md b/TEST_WRITING_GUIDE.md
index 6b03890..ee07b66 100644
--- a/TEST_WRITING_GUIDE.md
+++ b/TEST_WRITING_GUIDE.md
@@ -1,4 +1,4 @@
-# Unit Test Writing Guide — llamacpp-ai-index-maven-plugin (Plugin-Specific Supplement)
+# Unit Test Writing Guide — srcmorph (reactor-wide Supplement)
> **Canonical workspace rules** for test sources live in
> [`../workspace/guides/test/TEST_WRITING_GUIDE-8.md`](../workspace/guides/test/TEST_WRITING_GUIDE-8.md)
@@ -39,7 +39,7 @@ Every test file **must** start with the formatter-off block enclosing the Apache
*
*/
// @formatter:on
-package net.ladenthin.maven.llamacpp.aiindex;
+package net.ladenthin.maven.srcmorph;
```
- The `// @formatter:off` / `// @formatter:on` pair wraps **only** the license block.
@@ -337,7 +337,7 @@ public class AiMdHeaderCodecTest {
```
For sources shared across multiple test classes, reference a fully-qualified method:
-`@MethodSource("net.ladenthin.maven.llamacpp.aiindex.CommonDataProvider#nodeTypes")`.
+`@MethodSource("net.ladenthin.maven.srcmorph.CommonDataProvider#nodeTypes")`.
---
@@ -504,7 +504,7 @@ The goal is to **minimize the diff** to only lines that actually need changing.
*
*/
// @formatter:on
-package net.ladenthin.maven.llamacpp.aiindex;
+package net.ladenthin.maven.srcmorph;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
diff --git a/TODO.md b/TODO.md
index edef143..7fdb6fc 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,20 +1,85 @@
-# TODO — llamacpp-ai-index-maven-plugin
+# TODO — srcmorph reactor (Maven plugin now `srcmorph-maven-plugin`, formerly `llamacpp-ai-index-maven-plugin`)
Open work items for this repo. Cross-cutting tracking lives in
[`../workspace/crossrepostatus.md`](../workspace/crossrepostatus.md); items here are
-plugin-specific or this repo's slice of a cross-cutting initiative. Completed work is
+repo-specific or this repo's slice of a cross-cutting initiative. Completed work is
recorded in git history and `crossrepostatus.md`, not here.
## Open
-- **jqwik pin policy** — see [`../workspace/policies/jqwik-prompt-injection.md`](../workspace/policies/jqwik-prompt-injection.md). `jqwik.version ≤ 1.9.3` is mandatory.
+- **Migration step 8 — CI full pass + first reactor release.** `.github/workflows/publish.yml`
+ is now adapted to the 3-module reactor (module names as of that step; the plugin module has
+ since been renamed `srcmorph-maven-plugin` in step 9 below, and CI was updated to match): the
+ `build` job uploads jars from all three modules
+ (`srcmorph` / `srcmorph-cli`, including its `jar-with-dependencies` fat jar / the plugin module);
+ crash-dump globs are repo-wide (`**/hs_err_pid*.log` etc., since a forked surefire JVM can crash
+ in any module's own working directory); the PIT step is scoped to `-pl srcmorph -am` (the only
+ module with a `pitest-maven` execution) with its report glob at `srcmorph/target/pit-reports/**`;
+ the `vmlens` job is scoped to `-pl srcmorph-maven-plugin -am` (where
+ `VmlensInterleavingSmokeTest` and the `vmlens` profile actually live — not relocated during the
+ core extraction) and additionally passes `-Dsurefire.failIfNoSpecifiedTests=false` (the
+ `-DfailIfNoTests=false` flag alone does not suppress the "-Dtest pattern matched nothing"
+ failure that `-am` now triggers in the upstream `srcmorph` module); jdeps prints a graph per
+ module; and Coveralls/Codecov are pointed at `srcmorph`'s jacoco report only (the
+ single-primary-module precedent already used by the sibling java-llama.cpp reactor — `srcmorph-cli`
+ and the plugin module's own coverage is not currently aggregated or uploaded). `mvn -q clean
+ verify` and a `-P release verify -DskipTests -Dgpg.skip=true` dry run (package/sources/javadoc
+ jars for all three modules + the CLI fat jar, no `.asc` files since signing was skipped) both
+ pass locally. **Still open: actually cutting the first real `1.1.0` release** (tag + `mvn -P
+ release deploy` with real credentials) — that action was deliberately left to the user, not
+ performed as part of this CI-adaptation step. This is the gate the user asked for before step 9
+ ("if all is working stat I can safely do the final rename").
-- **`@VisibleForTesting` audit.** No usages currently. Walk the production tree for package-private/protected methods or fields that exist purely so tests can reach them, and either annotate (`com.google.common.annotations.VisibleForTesting`) or move into the test source tree.
+- **Migration step 9 — plugin rename + Maven Central relocation. DONE (structurally; publishing is
+ still the user's own later action).** The former `llamacpp-ai-index-maven-plugin` module was
+ renamed to `srcmorph-maven-plugin` (`net.ladenthin:srcmorph-maven-plugin`, goal prefix `srcmorph`,
+ package `net.ladenthin.maven.srcmorph.mojo`, properties `aiIndex.*` → `srcmorph.*`), and a new,
+ independent relocation-stub module `llamacpp-ai-index-maven-plugin/` (pom-only, no ``,
+ pinned at version `1.0.4`, only `` pointing at
+ `net.ladenthin:srcmorph-maven-plugin:1.1.0`) was added back to the root `` list so
+ existing consumers resolving the old coordinates get redirected once it is actually published.
+ This is the last, isolated step of the migration in terms of code/POM structure — actually
+ publishing both the `1.1.0` reactor release and the `1.0.4` relocation stub to Maven Central is
+ still the user's own action (this task never ran `mvn deploy` or signed anything).
-- **Null-safety refinement.** JSpecify + NullAway are enforced at compile time in strict JSpecify mode (see `pom.xml`); `@NullMarked` on the package; framework-populated POJOs carry class-level `@SuppressWarnings({"NullAway.Init","initialization.fields.uninitialized"})`. Open follow-up: review remaining unannotated public API surfaces for places where `@Nullable` would be more precise than the implicit non-null default.
+ **Caveat — exclude the stub from reactor-wide version bumps.** Because the relocation stub is
+ listed in the root ``, a `mvn versions:set -DnewVersion=X -DgenerateBackupPoms=false` run
+ from the repo root walks every module reachable from that list — including the stub — and would
+ overwrite its frozen `1.0.4` unless explicitly excluded:
+ ```bash
+ mvn versions:set -DnewVersion=X -DgenerateBackupPoms=false \
+ -Dexcludes=net.ladenthin:llamacpp-ai-index-maven-plugin
+ ```
+ The same warning is documented in the stub's own `pom.xml` comment and in the root `CLAUDE.md`
+ reactor-layout section.
-- **Expand PIT mutation scope (optional).** `pom.xml` wires `100` over an explicit `` list (config / document / prompt / provider / support value+logic classes, plus `indexer.AiInputWindowCalculator` and `support.AiProgressBar`), all killed at 100%. Still out (optional, need careful fixtures): `document.AiMdDocumentCodec` / `AiMdHeaderCodec`, `prompt.AiPromptPreparationSupport`, and the newer `indexer.AiIndexPlan` / `config.AiConditionGroup`. The orchestration layers (`indexer.*` walk, `mojo.*`) and the JNI provider stay out of PIT — they need a Maven/native context rather than pure-unit mutation (see crossrepostatus "Deliberate non-parity").
+- **PIT mutation-testing gate for `srcmorph-cli` and the plugin module.** Only `srcmorph`
+ (`srcmorph/pom.xml`) is PIT-gated today — `100` over an
+ explicit `` list of 47 classes across config/document/engine/indexer/prompt/
+ provider/support, all killed at 100%. Neither `srcmorph-cli` nor
+ `srcmorph-maven-plugin` has a `pitest-maven` execution of its own yet (both poms document
+ this explicitly with a comment at the spot a PIT plugin block would go). For the CLI: the pure
+ helpers worth mutation-gating are the config copy/round-trip (`Main#copyWithPlanOnlyForced`) and
+ command dispatch; `Main`'s I/O-heavy entry points (`main`, `loadConfiguration`) are better served by
+ integration tests than unit-mutation gating. For the plugin: the 5 mojo classes are Maven-lifecycle
+ orchestration (typically integration-tested via Maven invoker/executor, not unit-mutation-tested) —
+ confirm that reasoning still holds before assuming it's a permanent exemption rather than a gap.
+
+- **Expand `srcmorph`'s own PIT mutation scope (optional).** `srcmorph/pom.xml` wires
+ `100` over an explicit `` list (config /
+ document / prompt / provider / support value+logic classes, plus `indexer.AiInputWindowCalculator`
+ and `support.AiProgressBar`), all killed at 100%. Still out (optional, need careful fixtures):
+ `document.AiMdDocumentCodec` / `AiMdHeaderCodec`, `prompt.AiPromptPreparationSupport`, and the newer
+ `indexer.AiIndexPlan` / `config.AiConditionGroup`. The orchestration layers (`indexer.*` walk,
+ the plugin's `mojo.*`) and the JNI provider stay out of PIT — they need a Maven/native context
+ rather than pure-unit mutation (see crossrepostatus "Deliberate non-parity").
+
+- **jqwik pin policy** — see [`../workspace/policies/jqwik-prompt-injection.md`](../workspace/policies/jqwik-prompt-injection.md). `jqwik.version ≤ 1.9.3` is mandatory (declared in `srcmorph/pom.xml`, the only reactor module with a jqwik test dependency).
+
+- **`@VisibleForTesting` audit.** No usages currently in any module. Walk each module's production tree for package-private/protected methods or fields that exist purely so tests can reach them, and either annotate (`com.google.common.annotations.VisibleForTesting`) or move into the test source tree.
+
+- **Null-safety refinement.** JSpecify + NullAway are enforced at compile time in strict JSpecify mode in every module (see each module's own `pom.xml`); `@NullMarked` on the package; framework-populated POJOs carry class-level `@SuppressWarnings({"NullAway.Init","initialization.fields.uninitialized"})`. Open follow-up: review remaining unannotated public API surfaces for places where `@Nullable` would be more precise than the implicit non-null default.
- **Cross-repo code-quality TODOs** — see [`../workspace/policies/code-quality-todos.md`](../workspace/policies/code-quality-todos.md) for the canonical `@VisibleForTesting` design-fit review, package hierarchy review, and class/method naming review. This repo has no `@VisibleForTesting` usages today; the package and naming reviews are still open here.
-- **SLF4J gap CLOSED.** The four `indexer.*` classes (`SourceFileIndexer`, `PackageIndexer`, `ProjectIndexer`, `AiFieldGenerationSupport`) now log via `org.slf4j.Logger` instead of the constructor-injected Maven `Log`; only the 5 mojos (and the `PluginArchitectureTest` `mavenMojoAnnotationsConfinedToMojo`/`nonMojoIsMavenFree` rules) still touch the Maven Plugin API. Maven's `maven-slf4j-provider` binds `slf4j-api` inside plugin executions, so every `LOGGER.info(...)`/`.warn(...)`/`.debug(...)` call surfaces as a normal `[INFO]`/`[WARN]`/`[DEBUG]` line in `mvn` output with zero glue (verified end-to-end with the `generate` goal against a scratch project). `AiFieldGenerationSupportTest` exercises the binding/configuration directly via a logback `ListAppender` attached to the class logger (the LogCaptor-smoke-test role this note used to flag as future work).
+- **SLF4J gap CLOSED (core + CLI).** `srcmorph`'s `indexer.*` classes (`SourceFileIndexer`, `PackageIndexer`, `ProjectIndexer`, `AiFieldGenerationSupport`) and `engine.*` classes all log via `org.slf4j.Logger` instead of a Maven `Log`; `srcmorph-cli`'s `Main` does the same. Only the plugin module's 5 mojos (and `PluginArchitectureTest`'s Maven-confinement rules) still touch the Maven Plugin API directly — that boundary is deliberate (see `CLAUDE.md`), not a gap. Maven's `maven-slf4j-provider` binds `slf4j-api` inside plugin executions, so every `LOGGER.info(...)`/`.warn(...)`/`.debug(...)` call in the delegated-to `srcmorph` code surfaces as a normal `[INFO]`/`[WARN]`/`[DEBUG]` line in `mvn` output with zero glue; the CLI ships its own logback binding for the same log lines outside Maven. `AiFieldGenerationSupportTest` (in `srcmorph`) exercises the binding/configuration directly via a logback `ListAppender` attached to the class logger.
diff --git a/examples/config_All.json b/examples/config_All.json
new file mode 100644
index 0000000..9ba3c8f
--- /dev/null
+++ b/examples/config_All.json
@@ -0,0 +1,56 @@
+{
+ "command": "All",
+ "srcMorph": {
+ "baseDirectory": ".",
+ "outputDirectory": "target/srcmorph-ai",
+ "subtrees": [
+ "src/main/java"
+ ],
+ "fileExtensions": [
+ ".java"
+ ],
+ "excludes": [
+ "**/package-info.java"
+ ],
+ "generationProvider": "mock",
+ "projectName": "my-project",
+ "promptDefinitions": [
+ {
+ "key": "file-body-java",
+ "template": "Summarize ONE Java source file as structured markdown: types, public API, dependencies."
+ },
+ {
+ "key": "file-body-fallback",
+ "template": "Summarize ONE source file as structured markdown."
+ }
+ ],
+ "aiDefinitions": [
+ {
+ "key": "mock-model",
+ "modelPath": "unused-with-mock-provider.gguf",
+ "contextSize": 2048,
+ "maxOutputTokens": 64,
+ "temperature": 0.15,
+ "threads": 2
+ }
+ ],
+ "fieldGenerations": [
+ {
+ "id": "java",
+ "promptKey": "file-body-java",
+ "aiDefinitionKey": "mock-model",
+ "condition": {
+ "extensions": [
+ ".java"
+ ]
+ }
+ },
+ {
+ "id": "fallback",
+ "fallback": true,
+ "promptKey": "file-body-fallback",
+ "aiDefinitionKey": "mock-model"
+ }
+ ]
+ }
+}
diff --git a/examples/config_Calibrate.json b/examples/config_Calibrate.json
new file mode 100644
index 0000000..3de914a
--- /dev/null
+++ b/examples/config_Calibrate.json
@@ -0,0 +1,31 @@
+{
+ "command": "Calibrate",
+ "srcMorph": {
+ "baseDirectory": ".",
+ "outputDirectory": "target/srcmorph-ai",
+ "generationProvider": "mock",
+ "promptDefinitions": [
+ {
+ "key": "file-body",
+ "template": "Summarize this file as structured markdown."
+ }
+ ],
+ "aiDefinitions": [
+ {
+ "key": "mock-model",
+ "modelPath": "unused-with-mock-provider.gguf",
+ "contextSize": 2048,
+ "maxOutputTokens": 64,
+ "temperature": 0.15,
+ "threads": 2
+ }
+ ],
+ "fieldGenerations": [
+ {
+ "promptKey": "file-body",
+ "aiDefinitionKey": "mock-model",
+ "fallback": true
+ }
+ ]
+ }
+}
diff --git a/examples/config_GenerateFileIndex.json b/examples/config_GenerateFileIndex.json
new file mode 100644
index 0000000..406cac4
--- /dev/null
+++ b/examples/config_GenerateFileIndex.json
@@ -0,0 +1,37 @@
+{
+ "command": "GenerateFileIndex",
+ "srcMorph": {
+ "baseDirectory": ".",
+ "outputDirectory": "target/srcmorph-ai",
+ "subtrees": [
+ "src/main/java"
+ ],
+ "fileExtensions": [
+ ".java"
+ ],
+ "generationProvider": "mock",
+ "promptDefinitions": [
+ {
+ "key": "file-body",
+ "template": "Summarize this file as structured markdown."
+ }
+ ],
+ "aiDefinitions": [
+ {
+ "key": "mock-model",
+ "modelPath": "unused-with-mock-provider.gguf",
+ "contextSize": 2048,
+ "maxOutputTokens": 64,
+ "temperature": 0.15,
+ "threads": 2
+ }
+ ],
+ "fieldGenerations": [
+ {
+ "promptKey": "file-body",
+ "aiDefinitionKey": "mock-model",
+ "fallback": true
+ }
+ ]
+ }
+}
diff --git a/examples/config_GenerateFileIndex.yaml b/examples/config_GenerateFileIndex.yaml
new file mode 100644
index 0000000..ab4aa40
--- /dev/null
+++ b/examples/config_GenerateFileIndex.yaml
@@ -0,0 +1,23 @@
+command: GenerateFileIndex
+srcMorph:
+ baseDirectory: "."
+ outputDirectory: "target/srcmorph-ai"
+ subtrees:
+ - "src/main/java"
+ fileExtensions:
+ - ".java"
+ generationProvider: mock
+ promptDefinitions:
+ - key: file-body
+ template: "Summarize this file as structured markdown."
+ aiDefinitions:
+ - key: mock-model
+ modelPath: unused-with-mock-provider.gguf
+ contextSize: 2048
+ maxOutputTokens: 64
+ temperature: 0.15
+ threads: 2
+ fieldGenerations:
+ - promptKey: file-body
+ aiDefinitionKey: mock-model
+ fallback: true
diff --git a/examples/config_Plan.json b/examples/config_Plan.json
new file mode 100644
index 0000000..97ef6d1
--- /dev/null
+++ b/examples/config_Plan.json
@@ -0,0 +1,37 @@
+{
+ "command": "Plan",
+ "srcMorph": {
+ "baseDirectory": ".",
+ "outputDirectory": "target/srcmorph-ai",
+ "subtrees": [
+ "src/main/java"
+ ],
+ "fileExtensions": [
+ ".java"
+ ],
+ "generationProvider": "mock",
+ "promptDefinitions": [
+ {
+ "key": "file-body",
+ "template": "Summarize this file as structured markdown."
+ }
+ ],
+ "aiDefinitions": [
+ {
+ "key": "mock-model",
+ "modelPath": "unused-with-mock-provider.gguf",
+ "contextSize": 2048,
+ "maxOutputTokens": 64,
+ "temperature": 0.15,
+ "threads": 2
+ }
+ ],
+ "fieldGenerations": [
+ {
+ "promptKey": "file-body",
+ "aiDefinitionKey": "mock-model",
+ "fallback": true
+ }
+ ]
+ }
+}
diff --git a/examples/logbackConfiguration.xml b/examples/logbackConfiguration.xml
new file mode 100644
index 0000000..afb09ad
--- /dev/null
+++ b/examples/logbackConfiguration.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
diff --git a/examples/run_all.bat b/examples/run_all.bat
new file mode 100644
index 0000000..1e1ea3a
--- /dev/null
+++ b/examples/run_all.bat
@@ -0,0 +1,23 @@
+REM SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+REM
+REM SPDX-License-Identifier: Apache-2.0
+
+@echo off
+rem Runs the srcmorph CLI against config_All.json: all three phases in order (GenerateFileIndex,
+rem AggregatePackages, AggregateProject). Uses the mock provider, so no GGUF model is required -
+rem point generationProvider at "llamacpp-jni" and set a real modelPath to run a model.
+rem
+rem The fat jar's file name is version-qualified (e.g.
+rem srcmorph-cli-1.1.0-jar-with-dependencies.jar) and changes on every version bump, so
+rem the loop below picks whichever one was last built by "mvn package" in ..\srcmorph-cli.
+setlocal
+cd /d "%~dp0"
+
+set "JARFILE="
+for %%f in (..\srcmorph-cli\target\srcmorph-cli-*-jar-with-dependencies.jar) do set "JARFILE=%%f"
+if "%JARFILE%"=="" (
+ echo srcmorph-cli fat jar not found under ..\srcmorph-cli\target\ - run "mvn package" first. 1>&2
+ exit /b 1
+)
+
+java -jar "%JARFILE%" config_All.json
diff --git a/examples/run_all.sh b/examples/run_all.sh
new file mode 100755
index 0000000..9078d3c
--- /dev/null
+++ b/examples/run_all.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+# SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+#
+# SPDX-License-Identifier: Apache-2.0
+
+# Runs the srcmorph CLI against config_All.json: all three phases in order (GenerateFileIndex,
+# AggregatePackages, AggregateProject). Uses the mock provider, so no GGUF model is required -
+# point generationProvider at "llamacpp-jni" and set a real modelPath to run a model.
+#
+# The fat jar's file name is version-qualified (e.g.
+# srcmorph-cli-1.1.0-jar-with-dependencies.jar) and changes on every version bump, so the
+# glob below picks whichever one was last built by `mvn package` in ../srcmorph-cli.
+set -euo pipefail
+cd "$(dirname "$0")"
+
+JAR=$(ls ../srcmorph-cli/target/srcmorph-cli-*-jar-with-dependencies.jar 2>/dev/null | head -n 1)
+if [ -z "$JAR" ]; then
+ echo "srcmorph-cli fat jar not found under ../srcmorph-cli/target/ - run 'mvn package' first." >&2
+ exit 1
+fi
+
+java -jar "$JAR" config_All.json
diff --git a/examples/run_calibrate.bat b/examples/run_calibrate.bat
new file mode 100644
index 0000000..5b3db0a
--- /dev/null
+++ b/examples/run_calibrate.bat
@@ -0,0 +1,24 @@
+REM SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+REM
+REM SPDX-License-Identifier: Apache-2.0
+
+@echo off
+rem Runs the srcmorph CLI against config_Calibrate.json: loads each distinct routed model once
+rem and prints a paste-ready block per model. With the mock provider (as shipped)
+rem this is a no-op smoke check; swap generationProvider to "llamacpp-jni" and set a real
+rem modelPath to calibrate an actual GGUF model on this machine.
+rem
+rem The fat jar's file name is version-qualified (e.g.
+rem srcmorph-cli-1.1.0-jar-with-dependencies.jar) and changes on every version bump, so
+rem the loop below picks whichever one was last built by "mvn package" in ..\srcmorph-cli.
+setlocal
+cd /d "%~dp0"
+
+set "JARFILE="
+for %%f in (..\srcmorph-cli\target\srcmorph-cli-*-jar-with-dependencies.jar) do set "JARFILE=%%f"
+if "%JARFILE%"=="" (
+ echo srcmorph-cli fat jar not found under ..\srcmorph-cli\target\ - run "mvn package" first. 1>&2
+ exit /b 1
+)
+
+java -jar "%JARFILE%" config_Calibrate.json
diff --git a/examples/run_calibrate.sh b/examples/run_calibrate.sh
new file mode 100755
index 0000000..e0fd30c
--- /dev/null
+++ b/examples/run_calibrate.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+# SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+#
+# SPDX-License-Identifier: Apache-2.0
+
+# Runs the srcmorph CLI against config_Calibrate.json: loads each distinct routed model once and
+# prints a paste-ready block per model. With the mock provider (as shipped) this is
+# a no-op smoke check; swap generationProvider to "llamacpp-jni" and set a real modelPath to
+# calibrate an actual GGUF model on this machine.
+#
+# The fat jar's file name is version-qualified (e.g.
+# srcmorph-cli-1.1.0-jar-with-dependencies.jar) and changes on every version bump, so the
+# glob below picks whichever one was last built by `mvn package` in ../srcmorph-cli.
+set -euo pipefail
+cd "$(dirname "$0")"
+
+JAR=$(ls ../srcmorph-cli/target/srcmorph-cli-*-jar-with-dependencies.jar 2>/dev/null | head -n 1)
+if [ -z "$JAR" ]; then
+ echo "srcmorph-cli fat jar not found under ../srcmorph-cli/target/ - run 'mvn package' first." >&2
+ exit 1
+fi
+
+java -jar "$JAR" config_Calibrate.json
diff --git a/examples/run_generate.bat b/examples/run_generate.bat
new file mode 100644
index 0000000..22c6a91
--- /dev/null
+++ b/examples/run_generate.bat
@@ -0,0 +1,23 @@
+REM SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+REM
+REM SPDX-License-Identifier: Apache-2.0
+
+@echo off
+rem Runs the srcmorph CLI against config_GenerateFileIndex.json (Phase 1 only: index source files
+rem and fill in their AI-generated summary bodies). Uses the mock provider, so no GGUF model is
+rem required - point generationProvider at "llamacpp-jni" and set a real modelPath to run a model.
+rem
+rem The fat jar's file name is version-qualified (e.g.
+rem srcmorph-cli-1.1.0-jar-with-dependencies.jar) and changes on every version bump, so
+rem the loop below picks whichever one was last built by "mvn package" in ..\srcmorph-cli.
+setlocal
+cd /d "%~dp0"
+
+set "JARFILE="
+for %%f in (..\srcmorph-cli\target\srcmorph-cli-*-jar-with-dependencies.jar) do set "JARFILE=%%f"
+if "%JARFILE%"=="" (
+ echo srcmorph-cli fat jar not found under ..\srcmorph-cli\target\ - run "mvn package" first. 1>&2
+ exit /b 1
+)
+
+java -jar "%JARFILE%" config_GenerateFileIndex.json
diff --git a/examples/run_generate.sh b/examples/run_generate.sh
new file mode 100755
index 0000000..3a251ac
--- /dev/null
+++ b/examples/run_generate.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+# SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+#
+# SPDX-License-Identifier: Apache-2.0
+
+# Runs the srcmorph CLI against config_GenerateFileIndex.json (Phase 1 only: index source files
+# and fill in their AI-generated summary bodies). Uses the mock provider, so no GGUF model is
+# required - point generationProvider at "llamacpp-jni" and set a real modelPath to run a model.
+#
+# The fat jar's file name is version-qualified (e.g.
+# srcmorph-cli-1.1.0-jar-with-dependencies.jar) and changes on every version bump, so the
+# glob below picks whichever one was last built by `mvn package` in ../srcmorph-cli.
+set -euo pipefail
+cd "$(dirname "$0")"
+
+JAR=$(ls ../srcmorph-cli/target/srcmorph-cli-*-jar-with-dependencies.jar 2>/dev/null | head -n 1)
+if [ -z "$JAR" ]; then
+ echo "srcmorph-cli fat jar not found under ../srcmorph-cli/target/ - run 'mvn package' first." >&2
+ exit 1
+fi
+
+java -jar "$JAR" config_GenerateFileIndex.json
diff --git a/examples/run_plan.bat b/examples/run_plan.bat
new file mode 100644
index 0000000..a720abf
--- /dev/null
+++ b/examples/run_plan.bat
@@ -0,0 +1,23 @@
+REM SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+REM
+REM SPDX-License-Identifier: Apache-2.0
+
+@echo off
+rem Runs the srcmorph CLI against config_Plan.json: builds and logs the routing plan only (the
+rem GenerateFileIndex phase with planOnly forced true by the Plan command) - no model is loaded
+rem and nothing is written. Safe to run with no GGUF model on disk (generationProvider is "mock").
+rem
+rem The fat jar's file name is version-qualified (e.g.
+rem srcmorph-cli-1.1.0-jar-with-dependencies.jar) and changes on every version bump, so
+rem the loop below picks whichever one was last built by "mvn package" in ..\srcmorph-cli.
+setlocal
+cd /d "%~dp0"
+
+set "JARFILE="
+for %%f in (..\srcmorph-cli\target\srcmorph-cli-*-jar-with-dependencies.jar) do set "JARFILE=%%f"
+if "%JARFILE%"=="" (
+ echo srcmorph-cli fat jar not found under ..\srcmorph-cli\target\ - run "mvn package" first. 1>&2
+ exit /b 1
+)
+
+java -jar "%JARFILE%" config_Plan.json
diff --git a/examples/run_plan.sh b/examples/run_plan.sh
new file mode 100755
index 0000000..42add78
--- /dev/null
+++ b/examples/run_plan.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+# SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+#
+# SPDX-License-Identifier: Apache-2.0
+
+# Runs the srcmorph CLI against config_Plan.json: builds and logs the routing plan only (the
+# GenerateFileIndex phase with planOnly forced true by the Plan command) - no model is loaded and
+# nothing is written. Safe to run with no GGUF model on disk (generationProvider is "mock").
+#
+# The fat jar's file name is version-qualified (e.g.
+# srcmorph-cli-1.1.0-jar-with-dependencies.jar) and changes on every version bump, so the
+# glob below picks whichever one was last built by `mvn package` in ../srcmorph-cli.
+set -euo pipefail
+cd "$(dirname "$0")"
+
+JAR=$(ls ../srcmorph-cli/target/srcmorph-cli-*-jar-with-dependencies.jar 2>/dev/null | head -n 1)
+if [ -z "$JAR" ]; then
+ echo "srcmorph-cli fat jar not found under ../srcmorph-cli/target/ - run 'mvn package' first." >&2
+ exit 1
+fi
+
+java -jar "$JAR" config_Plan.json
diff --git a/llamacpp-ai-index-maven-plugin/pom.xml b/llamacpp-ai-index-maven-plugin/pom.xml
index 09bb6b6..6610829 100644
--- a/llamacpp-ai-index-maven-plugin/pom.xml
+++ b/llamacpp-ai-index-maven-plugin/pom.xml
@@ -4,2338 +4,47 @@ SPDX-FileCopyrightText: 2026 Bernard Ladenthin
SPDX-License-Identifier: Apache-2.0
-->
+
4.0.0
-
- net.ladenthin
- srcmorph-parent
- 1.1.0-SNAPSHOT
- ../pom.xml
-
-
+ net.ladenthinllamacpp-ai-index-maven-plugin
- maven-plugin
-
- llamacpp-ai-index-maven-plugin
- Free Maven plugin for hierarchical AI-readable indexing and summarization of source code projects using llama.cpp-compatible local models.
-
-
- bernardladenthin
- 8
- 21
- UTF-8
- 3.15.2
- 3.9.16
-
- 5.0.6
-
-
-
- gpt-oss-20B-mxfp4
-
- -1
-
- -1
-
-
- 6.1.2
- 3.0
- 1.37
- 0.16
- 3.6
- 1.2.28
- 2.50.0
- 0.13.7
- 1.0.0
- 4.2.1
-
- 1.9.3
- 1.4.2
- 4.10.2.0
- 1.18.46
- 7.7.4
- 1.14.0
- 3.8.0
- 2.94.0
-
- ${project.basedir}/src/site/ai
-
-
- true
-
-
-
- ${git.commit.time}
-
-
+ 1.0.4
+ pom
-
+ llamacpp-ai-index-maven-plugin (relocated)
+ Relocated. This artifact moved to net.ladenthin:srcmorph-maven-plugin; see the distributionManagement/relocation below.
-
-
- org.projectlombok
- lombok
- ${lombok.version}
- provided
-
-
- org.jspecify
- jspecify
- ${jspecify.version}
-
-
- org.checkerframework
- checker-qual
- ${checker.version}
-
-
+
+ net.ladenthin
- llama
- ${llama.version}
- ${llama.classifier}
-
-
-
-
- org.slf4j
- slf4j-api
-
-
-
- org.apache.maven
- maven-plugin-api
- ${maven.version}
- provided
-
-
-
- org.apache.maven.plugin-tools
- maven-plugin-annotations
- ${maven.plugin.tools.version}
- provided
-
-
-
- org.junit.jupiter
- junit-jupiter
- ${junit.version}
- test
-
-
-
- org.hamcrest
- hamcrest
- ${hamcrest.version}
- test
-
-
- net.jqwik
- jqwik
- ${jqwik.version}
- test
-
-
- com.tngtech.archunit
- archunit-junit5
- ${archunit.version}
- test
-
-
- org.openjdk.jmh
- jmh-core
- ${jmh.version}
- test
-
-
- org.openjdk.jmh
- jmh-generator-annprocess
- ${jmh.version}
- test
-
-
- org.openjdk.jcstress
- jcstress-core
- ${jcstress.version}
- test
-
-
- org.jetbrains.lincheck
- lincheck
- ${lincheck.version}
- test
-
-
-
- com.vmlens
- api
- ${vmlens.version}
- test
-
-
-
- ch.qos.logback
- logback-classic
- test
-
-
-
-
-
-
-
- com.diffplug.spotless
- spotless-maven-plugin
- ${spotless.version}
-
-
- com.github.spotbugs
- spotbugs-maven-plugin
- ${spotbugs.version}
-
-
- com.vmlens
- vmlens-maven-plugin
- ${vmlens.version}
-
-
- io.github.git-commit-id
- git-commit-id-maven-plugin
- 10.0.0
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
- 3.15.0
-
-
- org.apache.maven.plugins
- maven-enforcer-plugin
- 3.6.3
-
-
- org.apache.maven.plugins
- maven-gpg-plugin
- 3.2.8
-
-
- org.apache.maven.plugins
- maven-jar-plugin
- 3.5.0
-
-
- org.apache.maven.plugins
- maven-javadoc-plugin
- 3.12.0
-
-
- org.apache.maven.plugins
- maven-plugin-plugin
- ${maven.plugin.tools.version}
-
-
- org.apache.maven.plugins
- maven-source-plugin
- 3.4.0
-
-
- org.apache.maven.plugins
- maven-surefire-plugin
- 3.5.6
-
-
- @{argLine} -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=. -XX:ErrorFile=hs_err_pid%p.log
-
-
- **/VmlensInterleavingSmokeTest.java
-
-
-
-
- org.codehaus.mojo
- exec-maven-plugin
- 3.6.3
-
-
- org.jacoco
- jacoco-maven-plugin
- 0.8.15
-
-
- org.pitest
- pitest-maven
- 1.25.7
-
-
- org.sonatype.central
- central-publishing-maven-plugin
- 0.11.0
-
-
-
-
-
- io.github.git-commit-id
- git-commit-id-maven-plugin
-
-
- get-git-properties
-
- revision
-
- initialize
-
-
-
- yyyy-MM-dd'T'HH:mm:ss'Z'
- UTC
- false
- false
-
-
-
- org.apache.maven.plugins
- maven-enforcer-plugin
-
-
- enforce-maven
-
- enforce
-
-
-
-
- [3.6.3,)
-
-
- [1.8,)
-
-
-
-
-
- commons-logging:commons-logging
-
- log4j:log4j
-
- org.hamcrest:hamcrest-core
- org.hamcrest:hamcrest-library
- org.hamcrest:hamcrest-all
-
- junit:junit
- junit:junit-dep
-
-
-
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
-
- ${maven.compiler.release}
- ${maven.compiler.testRelease}
- true
- true
-
-
- -Xlint:all,-serial,-options,-classfile,-processing
- -Werror
-
- -processor
- lombok.launch.AnnotationProcessorHider$AnnotationProcessor,lombok.launch.AnnotationProcessorHider$ClaimingProcessor,org.checkerframework.checker.nullness.NullnessChecker
-
- -AskipDefs=^net\.ladenthin\.llamacpp_ai_index_maven_plugin\..*$
- -XDaddTypeAnnotationsToSymbol=true
- -XDcompilePolicy=simple
- --should-stop=ifError=FLOW
-
- -Xplugin:ErrorProne -XepExcludedPaths:.*/generated-sources/.* -Xep:NullAway:ERROR -XepOpt:NullAway:OnlyNullMarked=true -XepOpt:NullAway:ExcludedFieldAnnotations=org.apache.maven.plugins.annotations.Parameter,org.apache.maven.plugins.annotations.Component -XepOpt:NullAway:JSpecifyMode=true -XepOpt:NullAway:CheckOptionalEmptiness=true -XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true -XepOpt:NullAway:AcknowledgeAndroidRecent=true -XepOpt:NullAway:AssertsEnabled=true -Xep:BoxedPrimitiveEquality:ERROR -Xep:EqualsHashCode:ERROR -Xep:EqualsIncompatibleType:ERROR -Xep:IdentityBinaryExpression:ERROR -Xep:SelfAssignment:ERROR -Xep:SelfComparison:ERROR -Xep:SelfEquals:ERROR -Xep:DeadException:ERROR -Xep:FormatString:ERROR -Xep:InvalidPatternSyntax:ERROR -Xep:OptionalEquality:ERROR -Xep:ImpossibleNullComparison:ERROR
-
-
-
- org.projectlombok
- lombok
- ${lombok.version}
-
-
- com.google.errorprone
- error_prone_core
- ${errorprone.version}
-
-
- com.uber.nullaway
- nullaway
- ${nullaway.version}
-
-
- org.checkerframework
- checker
- ${checker.version}
-
-
-
-
-
- default-compile
-
-
-
- module-info.java
-
-
-
-
- module-info-compile
- compile
-
- compile
-
-
-
- 9
-
- module-info.java
-
-
-
-
-
-
- default-testCompile
-
-
- false
-
- -XDaddTypeAnnotationsToSymbol=true
- -XDcompilePolicy=simple
- --should-stop=ifError=FLOW
- -Xplugin:ErrorProne -Xep:NullAway:OFF -Xep:GuardedBy:OFF -Xep:IdentityBinaryExpression:OFF
-
-
-
- org.openjdk.jcstress
- jcstress-core
- ${jcstress.version}
-
-
- org.openjdk.jmh
- jmh-generator-annprocess
- ${jmh.version}
-
-
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-surefire-plugin
-
-
-
- org.apache.maven.plugins
- maven-source-plugin
-
-
- attach-sources
- verify
-
- jar-no-fork
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-javadoc-plugin
-
- ${maven.compiler.release}
- true
- true
- all
-
- **/HelpMojo.java
-
-
-
-
- attach-javadocs
-
- jar
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-plugin-plugin
-
- ai-index
-
-
-
- default-descriptor
-
- descriptor
-
-
-
- help-goal
-
- helpmojo
-
-
-
-
-
-
- org.jacoco
- jacoco-maven-plugin
-
-
- prepare-agent
-
- prepare-agent
-
-
-
- report
- test
-
- report
-
-
-
-
-
- com.diffplug.spotless
- spotless-maven-plugin
-
-
-
- src/main/java/**/*.java
- src/test/java/**/*.java
-
-
- ${palantir-java-format.version}
-
-
-
-
-
-
-
-
- spotless-check
- verify
-
- check
-
-
-
-
-
- com.github.spotbugs
- spotbugs-maven-plugin
-
- Max
- Low
- true
- false
- spotbugs-exclude.xml
-
-
- com.mebigfatguy.fb-contrib
- fb-contrib
- ${fb-contrib.version}
-
-
- com.h3xstream.findsecbugs
- findsecbugs-plugin
- ${findsecbugs.version}
-
-
-
-
-
- spotbugs-check
- verify
-
- check
-
-
-
-
-
- org.codehaus.mojo
- exec-maven-plugin
-
- org.openjdk.jmh.Main
- test
-
-
-
-
- org.pitest
- pitest-maven
-
-
- org.pitest
- pitest-junit5-plugin
- 1.2.3
-
-
-
-
-
- net.ladenthin.maven.llamacpp.aiindex.config.AiCalibration
- net.ladenthin.maven.llamacpp.aiindex.config.AiCondition
- net.ladenthin.maven.llamacpp.aiindex.config.AiConditionEvaluator
- net.ladenthin.maven.llamacpp.aiindex.config.AiFieldGenerationConfig
- net.ladenthin.maven.llamacpp.aiindex.config.AiFieldGenerationSelector
- net.ladenthin.maven.llamacpp.aiindex.config.AiFileContext
- net.ladenthin.maven.llamacpp.aiindex.config.AiGenerationConfig
- net.ladenthin.maven.llamacpp.aiindex.config.AiRangeCondition
- net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinition
- net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinitionSupport
- net.ladenthin.maven.llamacpp.aiindex.indexer.AiCalibrationMeasurement
- net.ladenthin.maven.llamacpp.aiindex.indexer.AiInputWindowCalculator
- net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationRequest
- net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationResult
- net.ladenthin.maven.llamacpp.aiindex.document.AiMdChildEntryLineFormatter
- net.ladenthin.maven.llamacpp.aiindex.document.AiMdDocument
- net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeader
- net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeaderSupport
- net.ladenthin.maven.llamacpp.aiindex.document.AiMdLeadExtractor
- net.ladenthin.maven.llamacpp.aiindex.prompt.AiPreparedPrompt
- net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptDefinition
- net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptPreparationSupport
- net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptSupport
- net.ladenthin.maven.llamacpp.aiindex.provider.AiCompletionParser
- net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationTimings
- net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationProvider
- net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationProviderFactory
- net.ladenthin.maven.llamacpp.aiindex.provider.MockAiGenerationProvider
- net.ladenthin.maven.llamacpp.aiindex.config.AiOversizeStrategy
- net.ladenthin.maven.llamacpp.aiindex.config.AiFactCounter
- net.ladenthin.maven.llamacpp.aiindex.config.AiFactDefinition
- net.ladenthin.maven.llamacpp.aiindex.config.AiFactDefinitionSupport
- net.ladenthin.maven.llamacpp.aiindex.config.AiFactExtractor
- net.ladenthin.maven.llamacpp.aiindex.support.AiChecksumSupport
- net.ladenthin.maven.llamacpp.aiindex.support.AiDeterministicSummary
- net.ladenthin.maven.llamacpp.aiindex.support.AiGenerationTimeEstimator
- net.ladenthin.maven.llamacpp.aiindex.support.AiPathSupport
- net.ladenthin.maven.llamacpp.aiindex.support.AiProgressBar
- net.ladenthin.maven.llamacpp.aiindex.support.AiSourceExcludeFilter
- net.ladenthin.maven.llamacpp.aiindex.support.AiTimeSupport
- net.ladenthin.maven.llamacpp.aiindex.support.Java8CompatibilityHelper
-
-
- net.ladenthin.maven.llamacpp.aiindex.*
-
- 100
- 30000
-
-
-
-
-
-
-
-
- gpu-cuda
-
- cuda13-windows-x86-64
-
-
-
- gpu-vulkan
-
- vulkan-windows-x86-64
-
-
-
-
-
-
- ai-index-selftest
-
-
-
- ${project.groupId}
- ${project.artifactId}
- ${project.version}
-
-
- ${ai.index.output.directory}
-
-
- src/main/java/net/ladenthin/maven/llamacpp/aiindex/config
- src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider
-
-
-
-
- **/package-info.java
- **/module-info.java
-
-
- llamacpp-jni
-
-
-
-
-
-
- Mistral-7B-Instruct-v0.3-Q4_K_M-C16k
- X:/Modelle/Mistral-7B-Instruct-v0.3-Q4_K_M.gguf
- 16384
- 2048
- 1.0
- 8
- 4
- true
- 1.1
-
-
-
- Meta-Llama-3.1-8B-Instruct-Q5_K_M-C32k
- X:/Modelle/Meta-Llama-3.1-8B-Instruct-Q5_K_M.gguf
- 32768
- 1536
- 1.0
- 8
- 4
- true
- 1.1
-
-
-
- Codestral-22B-v0.1-Q4_K_M-C32k
- X:/Modelle/Codestral-22B-v0.1-Q4_K_M.gguf
- 32768
- 1536
- 1.0
- 8
- 4
- true
- 1.1
-
-
-
- Codestral-22B-v0.1-Q6_K-C32k
- X:/Modelle/Codestral-22B-v0.1-Q6_K.gguf
- 32768
- 1536
- 1.0
- 8
- 4
- true
- 1.1
-
-
-
- Qwen2.5-Coder-7B-Instruct-Q5_K_M-C16k
- X:/Modelle/qwen2.5-coder-7b-instruct-q5_k_m.gguf
- 16384
- 2048
- 1.0
- 8
- 4
- true
- 1.1
-
-
-
- Ministral-3-8B-Instruct-2512-Q4_K_M-C32k
- X:/Modelle/Ministral-3-8B-Instruct-2512-Q4_K_M.gguf
- 32768
- 2048
- 0.1
- 8
- 4
- true
- 1.1
-
-
-
- Ministral-3-14B-Instruct-2512-Q4_K_M-C32k
- X:/Modelle/Ministral-3-14B-Instruct-2512-Q4_K_M.gguf
- 32768
- 2048
- 0.1
- 8
- 4
- true
- 1.1
-
-
-
- Ministral-3-8B-Instruct-2512-Q5_K_M-C32k
- X:/Modelle/Ministral-3-8B-Instruct-2512-Q5_K_M.gguf
- 32768
- 2048
- 0.1
- 8
- 4
- true
- 1.1
-
-
-
- Ministral-3-14B-Instruct-2512-Q5_K_M-C32k
- X:/Modelle/Ministral-3-14B-Instruct-2512-Q5_K_M.gguf
- 32768
- 2048
- 0.1
- 8
- 4
- true
- 1.1
-
-
-
- Gemma-4-26B-A4B-it-UD-Q4_K_M-C32k
- X:/Modelle/gemma-4-26B-A4B-it-UD-Q4_K_M.gguf
- 32768
- 8192
- 1.0
- 8
- 4
- true
- 0.95
- 64
- 1.1
- false
-
- <end_of_turn>
-
-
-
-
- Gemma-4-12B-it-Q4_K_M-C32k
- X:/Modelle/gemma-4-12B-it-Q4_K_M.gguf
- 32768
- 8192
- 1.0
- 8
- 4
- true
- 0.95
- 64
- 1.1
- false
-
- <end_of_turn>
-
-
-
-
- Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q4_K_M-C32k
- X:/Modelle/Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf
- 32768
- 8192
- 1.0
- 8
- 4
- true
- 0.95
- 64
- 1.1
- false
-
- <end_of_turn>
-
-
-
-
- Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q6_K_P-C32k
- X:/Modelle/Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q6_K_P.gguf
- 32768
- 8192
- 1.0
- 8
- 4
- true
- 0.95
- 64
- 1.1
- false
-
- <end_of_turn>
-
-
-
-
- Qwen3-Coder-30B-A3B-Instruct-UD-Q4_K_XL-C64k
- X:/Modelle/Qwen3-Coder-30B-A3B-Instruct-UD-Q4_K_XL.gguf
-
- 16384
- 1536
- 0.7
- 8
- 4
- true
- 0.8
- 20
- 1.05
- ${ai.cachePrompt}
-
-
-
- Qwen2.5-Coder-14B-Instruct-Q4_K_M-C32k
- X:/Modelle/Qwen2.5-Coder-14B-Instruct-Q4_K_M.gguf
- 32768
- 2048
- 0.7
- 8
- 4
- true
- 0.8
- 20
- 1.05
-
-
-
- Qwen3-4B-Instruct-2507-Q5_K_M-C32k
- X:/Modelle/Qwen3-4B-Instruct-2507-Q5_K_M.gguf
- 32768
- 2048
- 0.7
- 8
- 4
- true
- 0.8
- 20
- 1.05
-
-
-
- Granite-4.0-H-Tiny-Q4_K_M-C128k
- X:/Modelle/ibm-granite_granite-4.0-h-tiny-Q4_K_M.gguf
- 131072
- 2048
- 0.2
- 8
- 4
- true
- 1.1
-
-
-
- Granite-4.0-H-Small-Q4_K_M-C128k
- X:/Modelle/ibm-granite_granite-4.0-h-small-Q4_K_M.gguf
- 131072
- 2048
- 0.2
- 8
- 4
- true
- 1.1
-
-
-
- EXP-Qwen3.5-4B
- X:/Modelle/Qwen3.5-4B-Q4_K_M.gguf
- 16384
- 1536
- 0.7
- 8
- 4
- true
- 0.8
- 20
- 1.05
- false
-
-
- EXP-Qwen2.5-Coder-7B
- X:/Modelle/qwen2.5-coder-7b-instruct-q5_k_m.gguf
- 16384
- 1536
- 0.7
- 8
- 4
- true
- 0.8
- 20
- 1.05
-
-
-
- gpt-oss-20B-c16k
- X:/Modelle/gpt-oss-20b-UD-Q4_K_XL.gguf
- 16384
- ${ai.gpuLayers}
- ${ai.mainGpu}
- ${ai.devices}
- 2048
- 0.7
- 8
- 3
- true
- 1.0
- 0
- 0.05
- 1.0
- low
-
-
- gpt-oss-20B-c48k
- X:/Modelle/gpt-oss-20b-UD-Q4_K_XL.gguf
- 49152
- ${ai.gpuLayers}
- ${ai.mainGpu}
- ${ai.devices}
- 4096
- 0.7
- 8
- 3
- true
- 1.0
- 0
- 0.05
- 1.0
- low
-
-
- gpt-oss-20B-c96k
- X:/Modelle/gpt-oss-20b-UD-Q4_K_XL.gguf
- 98304
- ${ai.gpuLayers}
- ${ai.mainGpu}
- ${ai.devices}
- 6144
- 0.7
- 8
- 3
- true
- 1.0
- 0
- 0.05
- 1.0
- low
-
-
-
-
- gpt-oss-20B-mxfp4
- X:/Modelle/gpt-oss-20b-mxfp4.gguf
- 98304
- ${ai.gpuLayers}
- ${ai.mainGpu}
- ${ai.devices}
- 6144
- 0.7
- 8
- 3
- true
- 1.0
- 0
- 0.05
- 1.0
- low
-
-
- gpt-oss-20B-Q4_K_M
- X:/Modelle/gpt-oss-20b-Q4_K_M.gguf
- 98304
- ${ai.gpuLayers}
- ${ai.mainGpu}
- ${ai.devices}
- 6144
- 0.7
- 8
- 3
- true
- 1.0
- 0
- 0.05
- 1.0
- low
-
-
- gpt-oss-20B-Q5_K_M
- X:/Modelle/gpt-oss-20b-Q5_K_M.gguf
- 98304
- ${ai.gpuLayers}
- ${ai.mainGpu}
- ${ai.devices}
- 6144
- 0.7
- 8
- 3
- true
- 1.0
- 0
- 0.05
- 1.0
- low
-
-
- gpt-oss-20B-Q8_0
- X:/Modelle/gpt-oss-20b-Q8_0.gguf
- 98304
- ${ai.gpuLayers}
- ${ai.mainGpu}
- ${ai.devices}
- 6144
- 0.7
- 8
- 3
- true
- 1.0
- 0
- 0.05
- 1.0
- low
-
-
- gpt-oss-20B-F16
- X:/Modelle/gpt-oss-20b-F16.gguf
- 98304
- ${ai.gpuLayers}
- ${ai.mainGpu}
- ${ai.devices}
- 6144
- 0.7
- 8
- 3
- true
- 1.0
- 0
- 0.05
- 1.0
- low
-
-
-
-
- granite-4.0-h-tiny-bigwindow
- X:/Modelle/granite-4.0-h-tiny-Q4_K_M.gguf
- 393216
- ${ai.gpuLayers}
- ${ai.mainGpu}
- ${ai.devices}
- 4096
- 0.2
- 8
- 3
- true
- 1.0
- 0
- 0.05
- 1.0
-
-
-
-
-
- granite-4.0-h-1b-fastchunk
- X:/Modelle/granite-4.0-h-1b-Q4_K_M.gguf
- 16384
- ${ai.gpuLayers}
- ${ai.mainGpu}
- ${ai.devices}
- 1536
- 0.2
- 8
- 3
- true
- 1.0
- 0
- 0.05
- 1.0
-
-
-
-
- EXP-Qwen3-Coder-30B
- X:/Modelle/Qwen3-Coder-30B-A3B-Instruct-UD-Q4_K_XL.gguf
- 16384
- 1536
- 0.7
- 8
- 4
- true
- 0.8
- 20
- 1.05
-
-
-
- EXP-DeepSeek-Coder-V2-Lite
- X:/Modelle/DeepSeek-Coder-V2-Lite-Instruct-Q4_K_M.gguf
- 16384
- 1536
- 0.3
- 8
- 4
- true
- 0.9
- 40
- 1.05
-
-
- EXP-Seed-Coder-8B
- X:/Modelle/Seed-Coder-8B-Instruct-Q4_K_M.gguf
- 16384
- 1536
- 0.3
- 8
- 4
- true
- 0.9
- 40
- 1.05
-
-
- EXP-Granite-4.0-H-Tiny
- X:/Modelle/granite-4.0-h-tiny-Q4_K_M.gguf
- 16384
- 1536
- 0.0
- 8
- 4
- true
- 1.0
- 0
- 1.0
-
-
-
- EXP-Granite-4.0-H-Small
- X:/Modelle/granite-4.0-h-small-UD-Q4_K_XL.gguf
- 16384
- 1536
- 0.0
- 8
- 4
- true
- 1.0
- 0
- 1.0
-
-
- EXP-Qwen3-4B-Instruct-2507
- X:/Modelle/Qwen3-4B-Instruct-2507-Q4_K_M.gguf
- 16384
- 1536
- 0.7
- 8
- 4
- true
- 0.8
- 20
- 1.05
-
-
-
-
-
-
- file-body-java
-
-
-Then output these sections as markdown. Omit any section that does not apply. Use '####' for section labels. Never use '###' or higher.
-
-#### Purpose
-One or two bullets: what the type is for.
-
-#### Type
-Kind (class / interface / enum / record / annotation) and modifiers (abstract, final, sealed). extends X; implements Y; key generics and type bounds. Notable annotations (@Mojo, @Entity, Lombok, etc.).
-
-#### Input
-What comes in: constructor and method parameters, injected dependencies, consumed fields, read resources (files, buffers, streams, config).
-
-#### Output
-What comes out: return types, produced state, mutated fields, written resources, side effects.
-
-#### Core logic
-The essential steps / algorithm / responsibility as bullets. This is the most important section.
-
-#### Public API
-Each public/protected member as `name(params) -> returnType` + a clause (max 6 words) on its purpose. For a FAMILY of near-identical members (e.g. `fooN`), do not list them all: in the analysis channel enumerate and count them one per line, then in the final answer emit only the exact total and index range, e.g. `foo1..fooN (N total)`.
-
-#### Dependencies
-Imports and referenced types, as searchable names.
-
-#### Exceptions / Errors
-Notable thrown/caught exceptions, null-handling, and error conditions.
-
-#### Concurrency
-Threading, synchronization, immutability, or thread-safety notes, if any.
-
-## Rules
-- Markdown bullets and short clauses, not prose paragraphs. Sentences allowed only where a bullet would lose meaning.
-- Weave class / type / domain names into the bullets so they are searchable.
-- No code fences, no ```markdown / ```java, no XML tags, no formatter comments.
-- COUNT EXACTLY. When you state a count of methods/fields/members, count them precisely from the source and recount before finalizing. Never round, approximate, or write "similar"/"~".
-- The filename heading is already rendered above your output. Start immediately with the blockquote lead. Never produce empty output.
-
-## Critical
-If the source contains "[EOF - source was truncated]", index ONLY what is literally present. Never infer, guess, or invent any unit, field, or behavior beyond the shown source.
-
-The file path and its full source are provided in the next (user) message: the path on the first
-line, then a blank line, then the source. Index only what is literally present there.
- ]]>
-
-
- file-body-sql
-
-
-Then output these sections as markdown. Omit any section that does not apply. Use '####' for section labels. Never use '###' or higher.
-
-#### Purpose
-One or two bullets: what this SQL object is for.
-
-#### Type
-SQL object kind: table / view / materialized view / procedure / function / trigger / index / sequence. Note the dialect if evident (PostgreSQL, MySQL, Oracle, T-SQL).
-
-#### Schema
-For tables/views: each column as `name type` plus constraints (PK, FK, NOT NULL, UNIQUE, DEFAULT, CHECK). Mark the primary key and any indexes.
-
-#### Reads
-Tables and columns this object READS from (FROM / JOIN / subquery sources, function inputs).
-
-#### Writes
-Tables and columns this object WRITES (INSERT / UPDATE / DELETE / MERGE targets, or the table a trigger fires on).
-
-#### Relationships
-Foreign keys and join relationships to other tables, as searchable `table.column -> table.column` pairs.
-
-#### Routine logic
-For procedures / functions / triggers: parameters, the essential steps, and the result or effect, as bullets.
-
-#### Dependencies
-Other tables, views, functions, or sequences referenced, as searchable names.
-
-## Rules
-- Markdown bullets and short clauses, not prose paragraphs.
-- Weave table / column / constraint / domain names into the bullets so they are searchable.
-- No code fences, no ```markdown / ```sql, no XML tags, no formatter comments.
-- The filename heading is already rendered above your output. Start immediately with the blockquote lead. Never produce empty output.
-
-## Critical
-If the source contains "[EOF - source was truncated]", index ONLY what is literally present. Never infer, guess, or invent any table, column, or relationship beyond the shown source.
-
-The file path and its full source are provided in the next (user) message: the path on the first
-line, then a blank line, then the source. Index only what is literally present there.
- ]]>
-
-
- file-body-fallback
-
-
-Then output these sections as markdown. Omit any section that does not apply. Use '####' for section labels. Never use '###' or higher.
-
-#### Purpose
-One or two bullets: what the file/unit is for.
-
-#### Type
-Kind (class / interface / enum / record / header / kernel / SQL object) and language. For OO code: extends X; implements Y; key generics. For C: header vs implementation, exported vs static. For SQL: table / view / procedure / function / trigger.
-
-#### Input
-What comes in: key parameters, consumed fields, read resources (files, buffers, streams, kernels, tables/columns, env/config).
-
-#### Output
-What comes out: return types, produced state, written resources/tables, side effects.
-
-#### Core logic
-The essential steps / algorithm / responsibility as bullets. This is the most important section.
-
-#### Public API
-Each public/exported unit as `name(params) -> returnType` + a clause (max 6 words) on its purpose. For SQL: procedures/functions with params and result.
-
-#### Dependencies
-Imports / includes / referenced modules or tables, as searchable names.
-
-#### Exceptions / Errors
-Notable thrown/handled exceptions or error conditions.
-
-## Rules
-- Markdown bullets and short clauses, not prose paragraphs. Sentences allowed only where a bullet would lose meaning.
-- Weave class / type / table / domain names into the bullets so they are searchable.
-- No code fences, no ```markdown / ```java / ```sql, no XML tags, no formatter comments.
-- The filename heading is already rendered above your output. Start immediately with the blockquote lead. Never produce empty output.
-
-## Critical
-If the source contains "[EOF - source was truncated]", index ONLY what is literally present. Never infer, guess, or invent any unit, field, table, or behavior beyond the shown source.
-
-The file path and its full source are provided in the next (user) message: the path on the first
-line, then a blank line, then the source. Index only what is literally present there.
- ]]>
-
-
- package-body
-
-
-Then output these sections as markdown. Omit any that do not apply. Use '####' for labels. Never '###' or higher.
-
-#### Purpose
-One or two bullets: the package's overall responsibility.
-
-#### Responsibilities
-The main functional groups inside the package, as bullets. Group files by role; do not list every file separately for large packages.
-
-#### Key units
-The central classes / interfaces / modules / SQL objects and their job, one clause each, with their searchable names.
-
-#### Data flow
-How inputs move through the package to outputs, if a clear pipeline or collaboration exists across the files.
-
-#### Dependencies
-Notable internal collaborations and external modules/packages/tables the package depends on.
-
-#### Cross-cutting
-Recurring patterns across files: shared base types/interfaces, common exception/error handling, threading/concurrency notes, configuration.
-
-## Rules
-- Markdown bullets and short clauses, not prose paragraphs.
-- Weave type / module / table / domain names in as searchable terms.
-- Base every statement only on the provided file summaries. Do NOT invent units, methods, tables, or behavior not present in them.
-- No code fences, no ```markdown, no XML tags, no formatter comments.
-- The package-name heading is already rendered above your output. Start immediately with the blockquote lead. Never empty.
-
-## Critical
-If the input contains "[EOF - source was truncated]", summarize ONLY what is present. Never infer or invent files or content beyond the provided summaries.
-
-The package path and its already-generated per-file summaries are provided in the next (user)
-message: the path on the first line, then a blank line, then the summaries to synthesize.
- ]]>
-
-
- project-body
-
-
-
-
- file-body-java-v2
-
-
-Then output ONLY the sections below that carry real information. OMIT any section that would be empty, "none", or "not applicable" - do not emit a heading just to say nothing. Use '####' for section labels, never '###' or higher.
-
-TRIVIAL-FILE RULE: if the unit has no public behavior to describe (e.g. a marker/annotation, an enum that only lists constants, or a near-empty type), output ONLY the blockquote lead and a single '#### Purpose' line, and stop.
-
-#### Purpose
-One or two bullets: what the type is for.
-
-#### Type
-One line: kind (class/interface/enum/record/annotation) + modifiers; extends/implements; key generics; notable annotations (@Mojo, Lombok, etc.).
-
-#### Input
-What comes in: constructor/method parameters, injected dependencies, consumed fields, read resources.
-
-#### Output
-What comes out: return types, produced/mutated state, written resources, side effects.
-
-#### Core logic
-The essential steps/algorithm/responsibility as bullets. The most important section.
-
-#### Public API
-Each public/protected member as `name(params) -> returnType` + a <=6-word purpose clause. EXCEPTION: for plain data/config classes that are only getters/setters (e.g. Lombok beans), DO NOT list each accessor - write one line: "JavaBean getters/setters for: ".
-
-#### Dependencies
-Referenced types as searchable names. EXCLUDE this file's own type and java.lang.* .
-
-#### Exceptions / Errors
-Notable thrown/caught exceptions, null-handling, error conditions.
-
-#### Concurrency
-Threading, immutability, or thread-safety notes - only if the source actually shows them.
-
-## Rules
-- Markdown bullets and short clauses, not prose paragraphs.
-- Weave class/type/domain names into the bullets so they are searchable.
-- Describe ONLY what is literally in the source. NEVER invent units, fields, behavior, or concepts; in particular never mention keywords, tags, embeddings, search, or features the code does not contain.
-- No code fences, no ```markdown/```java, no XML tags, no formatter comments.
-- The filename heading is already rendered above your output. Start immediately with the blockquote lead. Never produce empty output.
-
-## Critical
-If the source contains "[EOF - source was truncated]", index ONLY what is literally present.
-
-The file path and its full source are provided in the next (user) message: the path on the first
-line, then a blank line, then the source. Index only what is literally present there.
- ]]>
-
-
- package-body-v2
-
-
-SINGLE-CHILD RULE: if the input describes only ONE child package/file and no other content, output ONLY the blockquote lead restating that child's responsibility, and stop - emit no sections.
-
-Otherwise output ONLY the sections below that carry real information. OMIT any that would be empty. Use '####' for labels, never '###' or higher.
-
-#### Purpose
-One or two bullets: the package's overall responsibility.
-
-#### Responsibilities
-The main functional groups inside the package, as bullets. Group files by role; do not list every file for large packages.
-
-#### Key units
-The central classes/interfaces/modules and their job, one clause each, with searchable names.
-
-#### Data flow
-How inputs move through the package to outputs, if a clear pipeline exists across the files.
-
-#### Dependencies
-Notable internal collaborations and external modules the package depends on. Focus on cross-unit/cross-package relationships, not raw JDK/Lombok imports.
-
-#### Cross-cutting
-Recurring patterns across files: shared base types, common error handling, concurrency, configuration.
-
-## Rules
-- Markdown bullets and short clauses, not prose paragraphs.
-- Weave type/module/domain names in as searchable terms.
-- Base every statement ONLY on the provided file summaries. NEVER invent units, methods, behavior, or concepts (in particular never mention keywords, tags, or embeddings) not present in them.
-- No code fences, no ```markdown, no XML tags, no formatter comments.
-- The package-name heading is already rendered above your output. Start immediately with the blockquote lead. Never empty.
-
-## Critical
-If the input contains "[EOF - source was truncated]", summarize ONLY what is present.
-
-The package path and its already-generated per-file summaries are provided in the next (user)
-message: the path on the first line, then a blank line, then the summaries to synthesize.
- ]]>
-
-
-
-
-
-
-
-
- ai-generate
- generate-resources
-
- generate
-
-
-
-
- .java
- .sql
-
-
-
-
- java-facts
-
- (?m)^\s*(?:public\s+|final\s+|abstract\s+|sealed\s+|non-sealed\s+|static\s+)*(?:class|interface|enum|record)\s+\w
- \bpublic\b
- \b(?:TODO|FIXME)\b
- @Override\b
-
- (?m)^[ \t]*(?:(?:public|private|protected|static|final|abstract|default|synchronized|native|strictfp)[ \t]+)*(?:<[^>]+>[ \t]*)?(?!(?:if|for|while|switch|catch|return|new|else|do|try|synchronized|assert|throw)\b)[A-Za-z_$][\w$.]*(?:<[^;{}=]*>)?(?:\[\])*[ \t]+([A-Za-z_$]\w*)[ \t]*\([^;{]*\)(?:[ \t]*throws[ \t][\w$., \t]+)?[ \t]*[{;]
- (?m)^[ \t]*(?:(?:public|private|protected)[ \t]+)?([A-Z][A-Za-z0-9_$]*)[ \t]*\([^;{]*\)(?:[ \t]*throws[ \t][\w$., \t]+)?[ \t]*\{
- (?m)^[ \t]*(?:(?:public|private|protected|static|transient|volatile|final)[ \t]+)*(?:public|private|protected|static|transient|volatile)[ \t]+(?:(?:public|private|protected|static|transient|volatile|final)[ \t]+)*(?!class\b|interface\b|enum\b|record\b|void\b|new\b)[A-Za-z_$][\w$.]*(?:<[^;{}=]*>)?(?:\[\])*[ \t]+([A-Za-z_$]\w*(?:[ \t]*,[ \t]*[A-Za-z_$]\w*)*)[ \t]*(?:=[^;]*)?;
-
-
-
- sql-facts
-
- (?im)^\s*INSERT\s+INTO\b
- (?im)^\s*CREATE\s+TABLE\b
- (?im)^\s*CREATE\s+(?:OR\s+REPLACE\s+)?VIEW\b
- (?im)^\s*CREATE\s+(?:UNIQUE\s+)?INDEX\b
- (?im)^\s*CREATE\s+(?:OR\s+REPLACE\s+)?(?:FUNCTION|PROCEDURE)\b
-
-
-
-
-
-
- huge-java
- file-body-java
- granite-4.0-h-1b-fastchunk
- 200
-
- mapReduce
- 6
- java-facts
-
-
-
-
- .java
-
-
- 1000000
-
-
-
-
-
-
- huge-sql
- file-body-sql
- granite-4.0-h-1b-fastchunk
- 200
- mapReduce
- 6
-
- sql-facts
-
-
-
-
- .sql
-
-
- 1000000
-
-
-
-
-
-
- big-window-java
- file-body-java
- granite-4.0-h-tiny-bigwindow
- 100
-
-
-
-
-
- .java
-
-
- 275000
-
-
-
-
-
-
- big-window-sql
- file-body-sql
- granite-4.0-h-tiny-bigwindow
- 100
-
-
-
-
- .sql
-
-
- 275000
-
-
-
-
-
-
- java
- file-body-java
- ${ai.model}
-
- java-facts
-
- .java
-
-
-
- sql
- file-body-sql
- ${ai.model}
- sql-facts
-
- .sql
-
-
-
-
- fallback
- file-body-fallback
- ${ai.model}
- true
-
-
-
-
-
-
- ai-aggregate-packages
- process-resources
-
- aggregate-packages
-
-
-
-
- package-body
- ${ai.model}
-
-
-
-
-
-
-
- ai-aggregate-project
- prepare-package
-
- aggregate-project
-
-
-
-
- project-body
- ${ai.model}
-
-
-
-
-
-
-
-
-
-
-
- vmlens
-
-
-
- com.vmlens
- vmlens-maven-plugin
-
-
-
- **/VmlensInterleavingSmokeTest.java
-
-
-
-
- vmlens-test
-
- test
-
-
-
-
-
-
-
-
- jcstress
-
-
-
- org.codehaus.mojo
- exec-maven-plugin
-
-
- jcstress
- test
- exec
-
- ${java.home}/bin/java
- test
-
- -classpath
-
- org.openjdk.jcstress.Main
- -v
- -m
- default
-
-
-
-
-
-
-
-
-
+ srcmorph-maven-plugin
+ 1.1.0
+
+
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/mojo/AbstractAiIndexMojo.java b/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/mojo/AbstractAiIndexMojo.java
deleted file mode 100644
index 0a1bcaf..0000000
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/mojo/AbstractAiIndexMojo.java
+++ /dev/null
@@ -1,384 +0,0 @@
-// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
-//
-// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.mojo;
-
-import java.io.File;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFieldGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinition;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinitionSupport;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptDefinition;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptSupport;
-import net.ladenthin.maven.llamacpp.aiindex.provider.LlamaCppJniConfig;
-import org.apache.maven.plugin.AbstractMojo;
-import org.apache.maven.plugin.MojoExecutionException;
-import org.apache.maven.plugins.annotations.Parameter;
-
-/**
- * Base class for all AI index mojos. Centralises the parameters shared by every goal
- * ({@code generate}, {@code summarize-files}, {@code summarize-packages},
- * {@code aggregate-packages}) and provides the utility methods {@link #resolveSubtrees},
- * {@link #sizeOf}, {@link #buildLlamaCppJniConfig}, and {@link #buildPromptSupport}.
- *
- *
Concrete subclasses must implement {@link #getLlamaContextSize()} and
- * {@link #getLlamaThreads()} so that each goal can declare its own
- * {@code @Parameter}-annotated field with the appropriate default value.
- *
- *
{@code toString} is generated by Lombok over the {@code @Parameter} fields so
- * Maven debug logs ({@code -X}) print a useful dump of the goal's resolved
- * configuration. {@code equals}/{@code hashCode} are intentionally NOT generated:
- * Maven instantiates mojos per execution and manages them by identity.
- */
-// @Parameter fields are populated by the Maven plugin framework via reflection after
-// construction. NullAway is configured via ExcludedFieldAnnotations to skip them; Checker
-// Framework has no equivalent option for plugin-framework fields, so we suppress class-level.
-@SuppressWarnings("initialization.fields.uninitialized")
-@ToString(callSuper = true)
-public abstract class AbstractAiIndexMojo extends AbstractMojo {
-
- /** Creates a new {@link AbstractAiIndexMojo}. */
- protected AbstractAiIndexMojo() {
- // no-op
- }
-
- /** The Maven project base directory, injected by Maven. */
- @Parameter(defaultValue = "${project.basedir}", readonly = true, required = true)
- protected File baseDirectory;
-
- /**
- * Directory into which all {@code .ai.md} files are written.
- * Defaults to {@code ${project.basedir}/src/site/ai}.
- */
- @Parameter(property = "aiIndex.outputDirectory", defaultValue = "${project.basedir}/src/site/ai")
- protected File outputDirectory;
-
- /**
- * Global skip switch: when {@code true}, every AI index goal (generate,
- * aggregate-packages, aggregate-project) skips and returns immediately. To toggle a single
- * phase instead, use that goal's own {@code skip} flag (see {@link #isPhaseSkipped()}).
- */
- @Parameter(property = "aiIndex.skip", defaultValue = "false")
- protected boolean skip;
-
- /**
- * When {@code true}, regenerates AI fields even when they already have a value.
- * When {@code false}, only missing or changed entries are processed.
- */
- @Parameter(property = "aiIndex.force", defaultValue = "false")
- protected boolean force;
-
- /**
- * Source subdirectory paths (relative to {@code basedir}) to restrict processing.
- * When empty, all discovered source roots are used.
- */
- @Parameter(property = "aiIndex.subtrees")
- protected List subtrees;
-
- /**
- * Name of the AI generation provider to use.
- * Supported values: {@code mock}, {@code llamacpp-jni}.
- *
- * @see net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationProviderFactory
- */
- @Parameter(property = "aiIndex.generationProvider", defaultValue = "mock")
- protected String generationProvider;
-
- /** Prompt template definitions referenced by field generation configurations. */
- @Parameter
- protected List promptDefinitions;
-
- /**
- * AI model definitions that pair a lookup key with a complete set of model parameters.
- * Field-generation entries and the provider configuration reference these definitions
- * by key rather than embedding the full parameter set inline.
- *
- * @see net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinition
- * @see net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinitionSupport
- */
- @Parameter
- protected List aiDefinitions;
-
- /** Per-field AI generation configurations controlling which prompt and AI definition to use. */
- @Parameter
- protected List fieldGenerations;
-
- /**
- * Optional native library path passed to the llama.cpp JNI provider.
- * Leave unset to use the bundled native library.
- */
- @Parameter(property = "aiIndex.llama.libraryPath")
- protected String llamaLibraryPath;
-
- /** Path to the GGUF model file used by the llama.cpp JNI provider. */
- @Parameter(property = "aiIndex.llama.modelPath")
- protected String llamaModelPath;
-
- /** Maximum number of output tokens the model may generate per request. */
- @Parameter(property = "aiIndex.llama.maxOutputTokens", defaultValue = "128")
- protected int llamaMaxOutputTokens;
-
- /** Sampling temperature for the llama.cpp model (lower = more deterministic). */
- @Parameter(property = "aiIndex.llama.temperature", defaultValue = "0.15")
- protected float llamaTemperature;
-
- // -------------------------------------------------------------------------
- // Abstract methods — subclasses declare @Parameter fields with their defaults
- // -------------------------------------------------------------------------
-
- /**
- * Returns the llama.cpp context window size for this goal.
- * Each concrete mojo declares its own {@code @Parameter}-annotated field and
- * implements this method to return it.
- *
- * @return configured llama.cpp context window size
- */
- protected abstract int getLlamaContextSize();
-
- /**
- * Returns the number of CPU threads for llama.cpp inference for this goal.
- * Each concrete mojo declares its own {@code @Parameter}-annotated field and
- * implements this method to return it.
- *
- * @return configured number of CPU threads for llama.cpp inference
- */
- protected abstract int getLlamaThreads();
-
- /**
- * Returns whether this individual goal (phase) is disabled by its own phase-specific skip flag,
- * independent of the global {@link #skip}. Each concrete goal declares its own
- * {@code skip} {@code @Parameter} and returns it here, so the three phases (generate,
- * aggregate-packages, aggregate-project) can be switched on and off independently.
- *
- * @return {@code true} if this phase's own skip flag is set
- */
- protected abstract boolean isPhaseSkipped();
-
- // -------------------------------------------------------------------------
- // Shared utility methods
- // -------------------------------------------------------------------------
-
- /**
- * Returns whether this goal should skip execution: either the global {@link #skip} (which skips
- * every phase) or this phase's own {@link #isPhaseSkipped()} flag. Centralised here so every goal
- * applies the same rule identically.
- *
- * @return {@code true} if the goal must not run
- */
- protected boolean shouldSkip() {
- return skip || isPhaseSkipped();
- }
-
- /**
- * Resolves the configured {@link #subtrees} strings against {@code basePath},
- * filtering out any paths that do not exist on disk.
- *
- * @param basePath absolute, normalised project base directory
- * @return list of resolved, existing subtree paths; empty if none configured
- */
- protected List resolveSubtrees(final Path basePath) {
- final List resolved = new ArrayList<>();
-
- if (subtrees == null || subtrees.isEmpty()) {
- return resolved;
- }
-
- for (String subtree : subtrees) {
- final Path path = basePath.resolve(subtree).normalize();
- if (path.toFile().exists()) {
- resolved.add(path);
- } else {
- getLog().warn("Skipping missing subtree: " + path);
- }
- }
-
- return resolved;
- }
-
- /**
- * Returns the size of {@code collection}, or {@code 0} when {@code collection} is {@code null}.
- *
- * @param collection any collection, or {@code null}
- * @return number of elements, or {@code 0}
- */
- protected int sizeOf(final Collection> collection) {
- return collection == null ? 0 : collection.size();
- }
-
- /**
- * Builds a {@link net.ladenthin.maven.llamacpp.aiindex.provider.LlamaCppJniConfig} for the AI generation provider.
- *
- *
When {@link #fieldGenerations} is non-empty, all model parameters
- * (model path, context size, max output tokens, temperature, threads) are taken from
- * the {@link net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinition} referenced by the first entry's
- * {@link net.ladenthin.maven.llamacpp.aiindex.config.AiFieldGenerationConfig#getAiDefinitionKey()}. This ensures the provider is
- * always configured from the same definition that drives field generation.
- *
- *
When {@link #fieldGenerations} is {@code null} or empty, the individual
- * {@code llamaModelPath}, {@code llamaContextSize}, {@code llamaMaxOutputTokens},
- * {@code llamaTemperature}, and {@code llamaThreads} parameters are used as a fallback.
- *
- * @return fully populated llama.cpp configuration
- * @throws IllegalArgumentException if the first field generation's
- * {@link net.ladenthin.maven.llamacpp.aiindex.config.AiFieldGenerationConfig#getAiDefinitionKey()}
- * does not match any registered definition
- * @throws MojoExecutionException propagated from {@link #buildAiModelDefinitionSupport()}
- * if any AI definition is missing a required field
- */
- protected LlamaCppJniConfig buildLlamaCppJniConfig() throws MojoExecutionException {
- if (fieldGenerations != null && !fieldGenerations.isEmpty()) {
- return buildLlamaCppJniConfig(fieldGenerations.get(0).getAiDefinitionKey());
- }
- return new LlamaCppJniConfig(
- llamaLibraryPath,
- llamaModelPath,
- getLlamaContextSize(),
- llamaMaxOutputTokens,
- llamaTemperature,
- getLlamaThreads(),
- AiGenerationConfig.DEFAULT_TOP_P,
- AiGenerationConfig.DEFAULT_TOP_K,
- AiGenerationConfig.DEFAULT_MIN_P,
- AiGenerationConfig.DEFAULT_TOP_N_SIGMA,
- AiGenerationConfig.DEFAULT_REPEAT_PENALTY,
- AiGenerationConfig.DEFAULT_CHAT_TEMPLATE_ENABLE_THINKING,
- AiGenerationConfig.DEFAULT_CACHE_PROMPT,
- AiGenerationConfig.DEFAULT_SWA_FULL,
- AiGenerationConfig.DEFAULT_CACHE_REUSE,
- AiGenerationConfig.DEFAULT_GPU_LAYERS,
- AiGenerationConfig.DEFAULT_MAIN_GPU,
- AiGenerationConfig.DEFAULT_DEVICES,
- AiGenerationConfig.DEFAULT_REASONING_EFFORT,
- AiGenerationConfig.DEFAULT_REASONING_BUDGET_TOKENS,
- AiGenerationConfig.DEFAULT_DRY_MULTIPLIER,
- AiGenerationConfig.DEFAULT_DRY_BASE,
- AiGenerationConfig.DEFAULT_DRY_ALLOWED_LENGTH,
- AiGenerationConfig.DEFAULT_DRY_PENALTY_LAST_N,
- Collections.emptyList(),
- Collections.emptyList());
- }
-
- /**
- * Builds a {@link net.ladenthin.maven.llamacpp.aiindex.provider.LlamaCppJniConfig} from a specific
- * AI model definition, identified by its key. Used by the {@code generate} goal to load a separate
- * model per routing group (one provider per distinct {@code aiDefinitionKey}).
- *
- * @param aiDefinitionKey the {@link net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinition} key
- * @return fully populated llama.cpp configuration for that definition
- * @throws IllegalArgumentException if {@code aiDefinitionKey} matches no registered definition
- * @throws MojoExecutionException propagated from {@link #buildAiModelDefinitionSupport()}
- */
- protected LlamaCppJniConfig buildLlamaCppJniConfig(final String aiDefinitionKey) throws MojoExecutionException {
- final AiGenerationConfig config = buildAiModelDefinitionSupport().getConfig(aiDefinitionKey);
- final List stopStrings = config.getStopStrings();
- final List drySequenceBreakers = config.getDrySequenceBreakers();
- return new LlamaCppJniConfig(
- llamaLibraryPath,
- config.getModelPath(),
- config.getContextSize(),
- config.getMaxOutputTokens(),
- config.getTemperature(),
- config.getThreads(),
- config.getTopP(),
- config.getTopK(),
- config.getMinP(),
- config.getTopNSigma(),
- config.getRepeatPenalty(),
- config.isChatTemplateEnableThinking(),
- config.isCachePrompt(),
- config.isSwaFull(),
- config.getCacheReuse(),
- config.getGpuLayers(),
- config.getMainGpu(),
- config.getDevices(),
- config.getReasoningEffort(),
- config.getReasoningBudgetTokens(),
- config.getDryMultiplier(),
- config.getDryBase(),
- config.getDryAllowedLength(),
- config.getDryPenaltyLastN(),
- drySequenceBreakers != null ? drySequenceBreakers : Collections.emptyList(),
- stopStrings != null ? stopStrings : Collections.emptyList());
- }
-
- /**
- * Builds an {@link net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptSupport} from the configured {@link #promptDefinitions}.
- *
- *
If any {@code } entry in the POM is missing its {@code key}
- * or {@code template}, the underlying constructor throws
- * {@link NullPointerException} with a message naming the offending list index and
- * the bad entry. This method translates that into a
- * {@link MojoExecutionException} so Maven reports it as a user configuration
- * error (the "Invalid plugin {@code }" framing) rather than as
- * a plugin bug.
- *
- * @return prompt support instance backed by the configured definitions
- * @throws MojoExecutionException if any prompt definition is missing a required field
- */
- protected AiPromptSupport buildPromptSupport() throws MojoExecutionException {
- try {
- return new AiPromptSupport(promptDefinitions);
- } catch (NullPointerException e) {
- throw new MojoExecutionException("Invalid plugin configuration: " + e.getMessage(), e);
- }
- }
-
- /**
- * Builds an {@link net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinitionSupport} from the configured {@link #aiDefinitions}.
- *
- *
If any {@code } entry in the POM is missing its {@code key},
- * the underlying constructor throws {@link NullPointerException} with a message
- * naming the offending list index and the bad entry. This method translates that
- * into a {@link MojoExecutionException} so Maven reports it as a user
- * configuration error rather than as a plugin bug.
- *
- * @return model definition support instance backed by the configured definitions
- * @throws MojoExecutionException if any AI definition is missing a required field
- */
- protected AiModelDefinitionSupport buildAiModelDefinitionSupport() throws MojoExecutionException {
- try {
- return new AiModelDefinitionSupport(aiDefinitions);
- } catch (NullPointerException e) {
- throw new MojoExecutionException("Invalid plugin configuration: " + e.getMessage(), e);
- }
- }
-
- /**
- * Logs the standard set of execution parameters that are common to every goal.
- *
- *
Always logs: the start message, base directory, output directory, subtrees,
- * force flag, and provider name. When {@code resolvedExtensions} is non-{@code null}
- * it is also logged; goals that do not use file-extension filtering (e.g.
- * {@code aggregate-packages}) pass {@code null} to suppress that line.
- *
- * @param startMessage first log line that identifies which goal is starting
- * @param basePath resolved, absolute project base directory
- * @param outputPath resolved, absolute output directory
- * @param resolvedSubtrees resolved subtree paths; may be empty but not {@code null}
- * @param resolvedExtensions file extensions in scope; pass an empty list when not applicable
- */
- protected void logExecutionParameters(
- final String startMessage,
- final Path basePath,
- final Path outputPath,
- final List resolvedSubtrees,
- final List resolvedExtensions) {
- getLog().info(startMessage);
- getLog().info("Base directory : " + basePath);
- getLog().info("Output directory: " + outputPath);
- getLog().info("Subtrees : " + resolvedSubtrees);
- if (!resolvedExtensions.isEmpty()) {
- getLog().info("Extensions : " + resolvedExtensions);
- }
- getLog().info("Force : " + force);
- getLog().info("Provider : " + generationProvider);
- getLog().info("LlamaCpp Temperature: " + llamaTemperature);
- getLog().info("LlamaCpp Max Output Tokens: " + llamaMaxOutputTokens);
- }
-}
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/mojo/AggregatePackagesMojo.java b/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/mojo/AggregatePackagesMojo.java
deleted file mode 100644
index 15cec8d..0000000
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/mojo/AggregatePackagesMojo.java
+++ /dev/null
@@ -1,120 +0,0 @@
-// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
-//
-// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.mojo;
-
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.Collections;
-import java.util.List;
-import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinitionSupport;
-import net.ladenthin.maven.llamacpp.aiindex.indexer.PackageIndexer;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptSupport;
-import net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationProvider;
-import net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationProviderFactory;
-import org.apache.maven.plugin.MojoExecutionException;
-import org.apache.maven.plugins.annotations.Mojo;
-import org.apache.maven.plugins.annotations.Parameter;
-
-/**
- * Maven goal {@code ai-index:aggregate-packages}: aggregates per-package
- * {@code .ai.md} index files and fills in their AI-generated summary and keyword fields.
- */
-// @Parameter fields are populated by the Maven plugin framework via reflection after
-// construction. NullAway is configured via ExcludedFieldAnnotations to skip them; Checker
-// Framework has no equivalent option for plugin-framework fields, so we suppress class-level.
-@SuppressWarnings("initialization.fields.uninitialized")
-@Mojo(name = "aggregate-packages", threadSafe = true)
-@ToString(callSuper = true)
-public class AggregatePackagesMojo extends AbstractAiIndexMojo {
-
- /** Creates a new {@link AggregatePackagesMojo}. */
- public AggregatePackagesMojo() {
- // no-op
- }
-
- /**
- * Phase switch for the package phase (the {@code aggregate-packages} goal): when
- * {@code true}, only this phase is skipped. The global {@link #skip} still skips every phase.
- */
- @Parameter(property = "aiIndex.package.skip", defaultValue = "false")
- private boolean skipPackage;
-
- @Parameter(defaultValue = "${project.version}", readonly = true)
- private String pluginVersion;
-
- @Parameter(property = "aiIndex.aiVersion", defaultValue = "0.0.0")
- private String aiVersion;
-
- /** llama.cpp context window size; smaller default suits the fast aggregate pass. */
- @Parameter(property = "aiIndex.llama.contextSize", defaultValue = "2048")
- private int llamaContextSize;
-
- /** CPU threads for llama.cpp inference during package aggregation. */
- @Parameter(property = "aiIndex.llama.threads", defaultValue = "2")
- private int llamaThreads;
-
- @Override
- protected int getLlamaContextSize() {
- return llamaContextSize;
- }
-
- @Override
- protected int getLlamaThreads() {
- return llamaThreads;
- }
-
- @Override
- protected boolean isPhaseSkipped() {
- return skipPackage;
- }
-
- @Override
- public void execute() throws MojoExecutionException {
- if (shouldSkip()) {
- getLog().info("AI package aggregation skipped.");
- return;
- }
-
- final Path basePath = baseDirectory.toPath().toAbsolutePath().normalize();
- final Path outputPath = outputDirectory.toPath().toAbsolutePath().normalize();
- final List resolvedSubtrees = resolveSubtrees(basePath);
-
- logExecutionParameters(
- "Starting AI package aggregation", basePath, outputPath, resolvedSubtrees, Collections.emptyList());
-
- if (!outputPath.toFile().exists()) {
- getLog().info("AI output directory does not exist, skipping package aggregation: " + outputPath);
- return;
- }
-
- try {
- final AiPromptSupport promptSupport = buildPromptSupport();
- final AiModelDefinitionSupport modelDefinitionSupport = buildAiModelDefinitionSupport();
- final AiGenerationProviderFactory providerFactory = new AiGenerationProviderFactory();
-
- try (AiGenerationProvider provider =
- providerFactory.create(generationProvider, buildLlamaCppJniConfig(), promptSupport)) {
- final PackageIndexer packageIndexer = new PackageIndexer(
- basePath,
- outputPath,
- pluginVersion,
- aiVersion,
- resolvedSubtrees,
- force,
- provider,
- fieldGenerations,
- promptSupport,
- modelDefinitionSupport);
-
- final int aggregated = packageIndexer.aggregate(outputPath);
- getLog().info("Aggregated packages: " + aggregated);
- }
- } catch (IOException e) {
- throw new MojoExecutionException("Failed to aggregate package AI index files under " + outputPath, e);
- }
-
- getLog().info("AI package aggregation finished.");
- }
-}
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/mojo/AggregateProjectMojo.java b/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/mojo/AggregateProjectMojo.java
deleted file mode 100644
index fda4c25..0000000
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/mojo/AggregateProjectMojo.java
+++ /dev/null
@@ -1,169 +0,0 @@
-// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
-//
-// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.mojo;
-
-import java.io.IOException;
-import java.nio.file.Path;
-import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFieldGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinitionSupport;
-import net.ladenthin.maven.llamacpp.aiindex.indexer.ProjectIndexer;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptSupport;
-import net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationProvider;
-import net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationProviderFactory;
-import net.ladenthin.maven.llamacpp.aiindex.support.Java8CompatibilityHelper;
-import org.apache.maven.plugin.MojoExecutionException;
-import org.apache.maven.plugins.annotations.Mojo;
-import org.apache.maven.plugins.annotations.Parameter;
-
-/**
- * Maven goal {@code ai-index:aggregate-project}: aggregates every per-package
- * {@code package.ai.md} into a single project-level {@code project.ai.md} — the top of the
- * three-level index.
- *
- *
By default this goal is fully deterministic: it harvests the one-line lead already present in
- * each package summary and writes a compact, navigable table of contents that links to every
- * package, calling no AI model. It then needs no provider, model, or prompt configuration — only
- * {@link #outputDirectory}, {@link #force}, and {@link #skip}.
- *
- *
Optional AI overview (opt-in). When a {@code } is configured
- * (a {@code promptKey} + {@code aiDefinitionKey}), the goal additionally makes one AI call
- * to write a short project overview paragraph from the per-package leads (see {@link ProjectIndexer}).
- * Because the overview reuses the shared provider/prompt/model parameters, this mojo extends
- * {@code AbstractAiIndexMojo} (the provider-oriented base) — but it builds a provider only when a
- * field generation is present, so the deterministic default needs no model and pays no model cost.
- *
- *
{@code toString} is generated by Lombok over the {@code @Parameter} fields so Maven debug logs
- * ({@code -X}) print a useful dump of the goal's resolved configuration. {@code equals}/{@code hashCode}
- * are intentionally NOT generated: Maven instantiates mojos per execution and manages them by identity.
- */
-// @Parameter fields are populated by the Maven plugin framework via reflection after
-// construction. NullAway is configured via ExcludedFieldAnnotations to skip them; Checker
-// Framework has no equivalent option for plugin-framework fields, so we suppress class-level.
-@SuppressWarnings("initialization.fields.uninitialized")
-@Mojo(name = "aggregate-project", threadSafe = true)
-@ToString(callSuper = true)
-public class AggregateProjectMojo extends AbstractAiIndexMojo {
-
- /** Creates a new {@link AggregateProjectMojo}. */
- public AggregateProjectMojo() {
- // no-op
- }
-
- /**
- * Title used for the project index when the Maven {@code ${project.name}} is absent or blank.
- */
- private static final String DEFAULT_PROJECT_TITLE = "project";
-
- /**
- * Phase switch for the project phase (the {@code aggregate-project} goal): when
- * {@code true}, only this phase is skipped. The global {@link #skip} still skips every phase.
- */
- @Parameter(property = "aiIndex.project.skip", defaultValue = "false")
- private boolean skipProject;
-
- /** Plugin version recorded in the project index header. */
- @Parameter(defaultValue = "${project.version}", readonly = true)
- private String pluginVersion;
-
- /** AI summarisation logic version recorded in the project index header. */
- @Parameter(property = "aiIndex.aiVersion", defaultValue = "0.0.0")
- private String aiVersion;
-
- /** Maven project name recorded as the project index title. */
- @Parameter(defaultValue = "${project.name}", readonly = true)
- private String projectName;
-
- /**
- * llama.cpp context window size used only as the fallback when no {@code } is
- * configured; with an overview field generation present, the size comes from its AI definition.
- */
- @Parameter(property = "aiIndex.llama.contextSize", defaultValue = "4096")
- private int llamaContextSize;
-
- /** CPU threads for llama.cpp inference, used only on the no-field-generation fallback path. */
- @Parameter(property = "aiIndex.llama.threads", defaultValue = "2")
- private int llamaThreads;
-
- private final Java8CompatibilityHelper compatibilityHelper = new Java8CompatibilityHelper();
-
- @Override
- protected int getLlamaContextSize() {
- return llamaContextSize;
- }
-
- @Override
- protected int getLlamaThreads() {
- return llamaThreads;
- }
-
- @Override
- protected boolean isPhaseSkipped() {
- return skipProject;
- }
-
- @Override
- public void execute() throws MojoExecutionException {
- if (shouldSkip()) {
- getLog().info("AI project index aggregation skipped.");
- return;
- }
-
- final Path outputPath = outputDirectory.toPath().toAbsolutePath().normalize();
-
- getLog().info("Starting AI project index aggregation");
- getLog().info("Output directory: " + outputPath);
- getLog().info("Force : " + force);
-
- if (!outputPath.toFile().exists()) {
- getLog().info("AI output directory does not exist, skipping project index: " + outputPath);
- return;
- }
-
- final String title =
- projectName == null || compatibilityHelper.isBlank(projectName) ? DEFAULT_PROJECT_TITLE : projectName;
-
- try {
- final int written;
- if (fieldGenerations != null && !fieldGenerations.isEmpty()) {
- written = aggregateWithOverview(outputPath, title, fieldGenerations.get(0));
- } else {
- getLog().info("Project overview generation: disabled (no fieldGenerations configured)");
- final ProjectIndexer indexer = new ProjectIndexer(title, pluginVersion, aiVersion, force);
- written = indexer.aggregate(outputPath);
- }
- getLog().info("Project index files written: " + written);
- } catch (IOException e) {
- throw new MojoExecutionException("Failed to aggregate project AI index file under " + outputPath, e);
- }
-
- getLog().info("AI project index aggregation finished.");
- }
-
- /**
- * Aggregates the project index with the optional AI overview paragraph enabled, generated by the
- * given field generation. A provider is created (and closed) only on this path, so the
- * deterministic default never instantiates a model.
- *
- * @param outputPath the resolved output directory holding the {@code .ai.md} tree
- * @param title the resolved project title
- * @param overview the overview field generation (prompt + model)
- * @return {@code 1} if the project index was written or refreshed, {@code 0} otherwise
- * @throws MojoExecutionException if the provider or model definitions are misconfigured
- * @throws IOException if the output tree cannot be read or the index cannot be written
- */
- private int aggregateWithOverview(final Path outputPath, final String title, final AiFieldGenerationConfig overview)
- throws MojoExecutionException, IOException {
- final AiPromptSupport promptSupport = buildPromptSupport();
- final AiModelDefinitionSupport modelDefinitionSupport = buildAiModelDefinitionSupport();
- getLog().info("Project overview generation: enabled (prompt '" + overview.getPromptKey() + "')");
- final AiGenerationProviderFactory providerFactory = new AiGenerationProviderFactory();
- try (AiGenerationProvider provider =
- providerFactory.create(generationProvider, buildLlamaCppJniConfig(), promptSupport)) {
- final ProjectIndexer indexer = new ProjectIndexer(
- title, pluginVersion, aiVersion, force, provider, overview, promptSupport, modelDefinitionSupport);
- return indexer.aggregate(outputPath);
- }
- }
-}
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/mojo/CalibrateMojo.java b/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/mojo/CalibrateMojo.java
deleted file mode 100644
index fdbc8ee..0000000
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/mojo/CalibrateMojo.java
+++ /dev/null
@@ -1,192 +0,0 @@
-// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
-//
-// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.mojo;
-
-import java.io.IOException;
-import java.util.LinkedHashMap;
-import java.util.Locale;
-import java.util.Map;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFieldGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinitionSupport;
-import net.ladenthin.maven.llamacpp.aiindex.indexer.AiCalibrationMeasurement;
-import net.ladenthin.maven.llamacpp.aiindex.indexer.AiCalibrationRunner;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptPreparationSupport;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptSupport;
-import net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationProvider;
-import net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationProviderFactory;
-import org.apache.maven.plugin.MojoExecutionException;
-import org.apache.maven.plugins.annotations.Mojo;
-import org.apache.maven.plugins.annotations.Parameter;
-
-/**
- * Maven goal {@code ai-index:calibrate}: a preflight + timing pass that sits between {@code planOnly} (no
- * model loaded) and a real {@code generate} run.
- *
- *
For each distinct model a run would load (the {@code aiDefinitionKey}s referenced by
- * {@code }) it loads the model once (catching a bad path / OOM / wrong native early),
- * runs a couple of representative generations, reads the model's own measured prefill/decode throughput,
- * and prints a paste-ready {@code } block. Paste that onto the matching {@code }
- * so the plan's time estimate reflects this machine instead of the built-in reference-CPU
- * coefficients. The numbers are per machine (GPU/CPU, quant, context), so re-run on each host.
- */
-@SuppressWarnings("initialization.fields.uninitialized")
-@Mojo(name = "calibrate", threadSafe = true)
-public class CalibrateMojo extends AbstractAiIndexMojo {
-
- /** Creates a new {@link CalibrateMojo}. */
- public CalibrateMojo() {
- // no-op
- }
-
- /** llama.cpp context window size (unused by calibrate, which uses each model's own config). */
- @Parameter(property = "aiIndex.llama.contextSize", defaultValue = "2048")
- private int llamaContextSize;
-
- /** CPU threads (unused by calibrate, which uses each model's own config). */
- @Parameter(property = "aiIndex.llama.threads", defaultValue = "2")
- private int llamaThreads;
-
- private final AiCalibrationRunner calibrationRunner = new AiCalibrationRunner();
-
- @Override
- protected int getLlamaContextSize() {
- return llamaContextSize;
- }
-
- @Override
- protected int getLlamaThreads() {
- return llamaThreads;
- }
-
- @Override
- protected boolean isPhaseSkipped() {
- // calibrate is a manual diagnostic goal, not one of the file/package/project phases; only the
- // global aiIndex.skip disables it.
- return false;
- }
-
- @Override
- public void execute() throws MojoExecutionException {
- if (shouldSkip()) {
- getLog().info("AI index calibration skipped.");
- return;
- }
- if (fieldGenerations == null || fieldGenerations.isEmpty()) {
- throw new MojoExecutionException("No configured; nothing to calibrate.");
- }
-
- final AiPromptSupport promptSupport = buildPromptSupport();
- final AiModelDefinitionSupport modelDefinitionSupport = buildAiModelDefinitionSupport();
- final AiPromptPreparationSupport promptPreparationSupport = new AiPromptPreparationSupport(promptSupport);
- final AiGenerationProviderFactory providerFactory = new AiGenerationProviderFactory();
-
- // Distinct routed models, each mapped to a representative prompt key (skip rules have none).
- final Map modelToPrompt = new LinkedHashMap<>();
- for (final AiFieldGenerationConfig rule : fieldGenerations) {
- if (rule == null || rule.getAiDefinitionKey() == null || rule.getPromptKey() == null) {
- continue;
- }
- modelToPrompt.putIfAbsent(rule.getAiDefinitionKey(), rule.getPromptKey());
- }
- if (modelToPrompt.isEmpty()) {
- throw new MojoExecutionException("No routable (model + prompt) rule found to calibrate.");
- }
-
- getLog().info("AI index calibration: " + modelToPrompt.size() + " model(s). Provider: " + generationProvider);
- final StringBuilder pasteBlocks = new StringBuilder();
- for (final Map.Entry entry : modelToPrompt.entrySet()) {
- calibrateModel(
- entry.getKey(),
- entry.getValue(),
- modelDefinitionSupport,
- promptSupport,
- promptPreparationSupport,
- providerFactory,
- pasteBlocks);
- }
-
- getLog().info("");
- getLog().info("Paste each onto its matching (numbers are per machine).");
- getLog().info("These are the model's own measured prefill/decode throughput (from the engine's "
- + "per-call timings); only the mock/no-timings path falls back to a wall-clock estimate.");
- for (final String line : pasteBlocks.toString().split("\n", -1)) {
- getLog().info(line);
- }
- }
-
- /**
- * Loads one model, measures it via {@link AiCalibrationRunner}, logs the result, and appends a
- * paste-ready {@code } block for it.
- *
- * @param modelKey the aiDefinitionKey to calibrate
- * @param promptKey a representative prompt key routed to this model
- * @param modelDefinitionSupport model lookup
- * @param promptSupport prompt lookup (for the provider)
- * @param promptPreparationSupport prompt preparation (for the window calculation)
- * @param providerFactory provider factory
- * @param pasteBlocks accumulator for the printable {@code } blocks
- * @throws MojoExecutionException if the model fails to load or generate
- */
- private void calibrateModel(
- final String modelKey,
- final String promptKey,
- final AiModelDefinitionSupport modelDefinitionSupport,
- final AiPromptSupport promptSupport,
- final AiPromptPreparationSupport promptPreparationSupport,
- final AiGenerationProviderFactory providerFactory,
- final StringBuilder pasteBlocks)
- throws MojoExecutionException {
- final AiGenerationConfig config = modelDefinitionSupport.getConfig(modelKey);
- final long windowChars = calibrationRunner.windowChars(config, promptKey, promptPreparationSupport);
-
- getLog().info("");
- getLog().info("Model '" + modelKey + "': loading (window ~" + windowChars + " source chars)...");
- try (AiGenerationProvider provider =
- providerFactory.create(generationProvider, buildLlamaCppJniConfig(modelKey), promptSupport)) {
- final AiCalibrationMeasurement m =
- calibrationRunner.measure(provider, config, promptKey, promptPreparationSupport);
-
- getLog().info(String.format(
- Locale.ROOT,
- "Model '%s': loaded+first-gen ~%.1fs | prefill %.0f tok/s | decode %.0f tok/s | ~%.2f chars/token",
- modelKey,
- m.loadSeconds(),
- m.prefillTokensPerSecond(),
- m.decodeTokensPerSecond(),
- m.charsPerToken()));
- if (m.prefillTokensPerSecond() > 0 && m.midPrefillTokensPerSecond() > 0) {
- getLog().info(String.format(
- Locale.ROOT,
- " (mid-window prefill %.0f tok/s vs near-window %.0f tok/s - larger gap => more curvature)",
- m.midPrefillTokensPerSecond(),
- m.prefillTokensPerSecond()));
- }
- appendPasteBlock(pasteBlocks, modelKey, m);
- } catch (final IOException e) {
- throw new MojoExecutionException("Calibration failed for model '" + modelKey + "': " + e.getMessage(), e);
- }
- }
-
- /**
- * Appends a copy-pasteable {@code } block (with a comment naming the model) to the buffer.
- *
- * @param out the buffer
- * @param modelKey the model key (for the comment)
- * @param m the measurement
- */
- private static void appendPasteBlock(
- final StringBuilder out, final String modelKey, final AiCalibrationMeasurement m) {
- out.append("\n");
- out.append("\n");
- out.append(String.format(
- Locale.ROOT,
- " %.1f%n",
- m.prefillTokensPerSecond()));
- out.append(String.format(
- Locale.ROOT, " %.1f%n", m.decodeTokensPerSecond()));
- out.append(String.format(Locale.ROOT, " %.2f%n", m.charsPerToken()));
- out.append("\n");
- }
-}
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/mojo/GenerateMojo.java b/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/mojo/GenerateMojo.java
deleted file mode 100644
index d2395bc..0000000
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/mojo/GenerateMojo.java
+++ /dev/null
@@ -1,308 +0,0 @@
-// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
-//
-// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.mojo;
-
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFactDefinition;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFactDefinitionSupport;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFieldGenerationSelector;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinitionSupport;
-import net.ladenthin.maven.llamacpp.aiindex.indexer.AiFieldGenerationSupport;
-import net.ladenthin.maven.llamacpp.aiindex.indexer.AiIndexPlan;
-import net.ladenthin.maven.llamacpp.aiindex.indexer.SourceFileIndexer;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptPreparationSupport;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptSupport;
-import net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationProvider;
-import net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationProviderFactory;
-import net.ladenthin.maven.llamacpp.aiindex.support.AiGenerationTimeEstimator;
-import net.ladenthin.maven.llamacpp.aiindex.support.AiProgressBar;
-import net.ladenthin.maven.llamacpp.aiindex.support.Java8CompatibilityHelper;
-import org.apache.maven.plugin.MojoExecutionException;
-import org.apache.maven.plugins.annotations.Mojo;
-import org.apache.maven.plugins.annotations.Parameter;
-
-/**
- * Maven goal {@code ai-index:generate}: indexes source files and fills in their
- * AI-generated summary and keyword fields.
- */
-// @Parameter fields are populated by the Maven plugin framework via reflection after
-// construction. NullAway is configured via ExcludedFieldAnnotations to skip them; Checker
-// Framework has no equivalent option for plugin-framework fields, so we suppress class-level.
-@SuppressWarnings("initialization.fields.uninitialized")
-@Mojo(name = "generate", threadSafe = true)
-@ToString(callSuper = true)
-public class GenerateMojo extends AbstractAiIndexMojo {
-
- /** Creates a new {@link GenerateMojo}. */
- public GenerateMojo() {
- // no-op
- }
-
- /**
- * Default file extension used when no explicit {@code fileExtensions} parameter
- * is configured. Only files whose names end with this extension are indexed.
- */
- private static final String DEFAULT_FILE_EXTENSION = ".java";
-
- /** Nanoseconds per second, for converting the measured run elapsed time to whole seconds. */
- private static final long NANOS_PER_SECOND = 1_000_000_000L;
-
- /**
- * Phase switch for the file phase (the {@code generate} goal): when {@code true},
- * only this phase is skipped. The global {@link #skip} still skips every phase.
- */
- @Parameter(property = "aiIndex.file.skip", defaultValue = "false")
- private boolean skipFile;
-
- @Parameter(defaultValue = "${project.version}", readonly = true)
- private String pluginVersion;
-
- @Parameter(property = "aiIndex.aiVersion", defaultValue = "0.0.0")
- private String aiVersion;
-
- @Parameter(property = "aiIndex.fileExtensions")
- private List fileExtensions;
-
- /**
- * Glob patterns for source files to skip, matched against each file's path relative to the
- * project base directory with {@code /} separators (e.g. {@code **}{@code /package-info.java},
- * {@code **}{@code /generated/**}). Lets the index stay focused by excluding trivial or generated
- * sources. Empty by default — nothing is excluded.
- *
- * @see net.ladenthin.maven.llamacpp.aiindex.support.AiSourceExcludeFilter
- */
- @Parameter(property = "aiIndex.excludes")
- private List excludes;
-
- /**
- * Reusable, named {@code } groups referenced from a rule's {@code }, so a
- * fact set (e.g. {@code java-facts}, {@code sql-facts}) is defined once and shared across rules
- * instead of repeated inline. Resolved onto each rule's {@code facts} before indexing.
- *
- * @see net.ladenthin.maven.llamacpp.aiindex.config.AiFactDefinitionSupport
- */
- @Parameter
- private List factDefinitions;
-
- /**
- * Exclusive lower file-size bound in bytes: source files whose size is {@code <= this} are skipped.
- * {@code 0} (default) disables the lower bound. Together with {@link #maxFileSizeBytes} this lets one
- * {@code generate} execution target a size band, so several executions (each with its own model,
- * context size and prompt) can tier a project by file size while the source-checksum skip indexes
- * every file exactly once. Use non-overlapping bands ({@code band2.min == band1.max}); make the last
- * band unbounded ({@code maxFileSizeBytes=0}) so files above all bands still get indexed.
- */
- @Parameter(property = "aiIndex.file.minSizeBytes", defaultValue = "0")
- private long minFileSizeBytes;
-
- /**
- * Inclusive upper file-size bound in bytes: source files whose size is {@code > this} are skipped.
- * {@code 0} (default) disables the upper bound (unlimited). See {@link #minFileSizeBytes} for the
- * size-tiering pattern.
- */
- @Parameter(property = "aiIndex.file.maxSizeBytes", defaultValue = "0")
- private long maxFileSizeBytes;
-
- /** llama.cpp context window size; smaller default suits the fast generate pass. */
- @Parameter(property = "aiIndex.llama.contextSize", defaultValue = "2048")
- private int llamaContextSize;
-
- /** CPU threads for llama.cpp inference during the generate pass. */
- @Parameter(property = "aiIndex.llama.threads", defaultValue = "2")
- private int llamaThreads;
-
- /**
- * When {@code true}, only build and log the routing plan (which model indexes which files with which
- * prompt) and then stop — no model is loaded and nothing is generated. Useful to verify routing
- * before a long run.
- */
- @Parameter(property = "aiIndex.planOnly", defaultValue = "false")
- private boolean planOnly;
-
- private final Java8CompatibilityHelper compatibilityHelper = new Java8CompatibilityHelper();
-
- @Override
- protected int getLlamaContextSize() {
- return llamaContextSize;
- }
-
- @Override
- protected int getLlamaThreads() {
- return llamaThreads;
- }
-
- @Override
- protected boolean isPhaseSkipped() {
- return skipFile;
- }
-
- @Override
- public void execute() throws MojoExecutionException {
- if (shouldSkip()) {
- getLog().info("AI index generation skipped.");
- return;
- }
-
- final Path basePath = baseDirectory.toPath().toAbsolutePath().normalize();
- final Path outputPath = outputDirectory.toPath().toAbsolutePath().normalize();
- final List resolvedSubtrees = resolveSubtrees(basePath);
- final List resolvedExtensions = resolveFileExtensions();
-
- logExecutionParameters(
- "Starting AI index generation", basePath, outputPath, resolvedSubtrees, resolvedExtensions);
-
- if (fieldGenerations == null || fieldGenerations.isEmpty()) {
- throw new MojoExecutionException("No configured for the generate goal.");
- }
-
- final AiPromptSupport promptSupport = buildPromptSupport();
- final AiModelDefinitionSupport modelDefinitionSupport = buildAiModelDefinitionSupport();
- final AiPromptPreparationSupport promptPreparationSupport = new AiPromptPreparationSupport(promptSupport);
- // Resolve each rule's factsKey to its shared factDefinitions group (copies the counters onto the
- // rule's facts) BEFORE validation, so the resolved fact patterns are validated too.
- resolveSharedFacts();
-
- final AiFieldGenerationSelector selector = new AiFieldGenerationSelector();
- // Fail fast on a bad rule set (e.g. >1 fallback, a route rule missing prompt/model).
- selector.validate(fieldGenerations);
-
- final SourceFileIndexer fileIndexer = new SourceFileIndexer(
- basePath,
- outputPath,
- resolvedExtensions,
- pluginVersion,
- aiVersion,
- resolvedSubtrees,
- excludes,
- minFileSizeBytes,
- maxFileSizeBytes,
- force);
-
- try {
- // 1. Collect candidate files across the configured subtrees.
- final List candidates = new ArrayList<>();
- for (final Path subtree : resolvedSubtrees.isEmpty()
- ? compatibilityHelper.listOf(basePath.resolve("src/main/java"))
- : resolvedSubtrees) {
- if (!subtree.toFile().exists()) {
- getLog().warn("Skipping missing subtree: " + subtree);
- continue;
- }
- candidates.addAll(fileIndexer.collectCandidates(subtree));
- }
-
- // 2. Plan the run: which model + prompt each file gets (or skip / unmatched), and whether
- // each file fits its routed model's context window (computed up front, same threshold the
- // run uses to trim — see AiInputWindowCalculator).
- final AiIndexPlan plan = fileIndexer.classify(
- candidates, fieldGenerations, modelDefinitionSupport, promptPreparationSupport);
- getLog().info("AI index plan (Markdown):\n" + plan.renderMarkdown(basePath));
-
- // 3. A file that matched no rule and no fallback is a fatal misconfiguration.
- if (!plan.unmatched().isEmpty()) {
- throw new MojoExecutionException(plan.unmatched().size()
- + " source file(s) matched no rule and no fallback is configured; "
- + "add a rule or a matching rule (see the plan above).");
- }
-
- // 3b. A file larger than its routed model's window would lose content if trimmed. By default
- // (onOversize=fail) this is a hard failure: the fix is user configuration, never an
- // automatic model choice — route oversized files to a larger-context model, or set the
- // rule's onOversize (sample/mapReduce/deterministic) to handle them at run time. Only the
- // fail entries abort here; the handled ones are processed during generation.
- final int overWindowFailCount = plan.windowFailCount();
- if (overWindowFailCount > 0) {
- throw new MojoExecutionException(overWindowFailCount
- + " source file(s) exceed their routed model's context window with onOversize=fail "
- + "(see the 'Over window' section in the plan above). Route them to a model with a "
- + "large enough context window, or set onOversize=sample|mapReduce|deterministic on "
- + "the rule. The build does not pick a model for you; this is configuration only.");
- }
-
- if (planOnly) {
- getLog().info("planOnly=true: stopping after the plan; no model loaded, nothing generated.");
- return;
- }
-
- // 4. Execute model group by model group: load each model once, index its files, close.
- // Progress is the running sum of each finished file's PLAN estimate over the grand total
- // (no re-estimation), logged as a bar + percent after every file, with the estimated time
- // left and the actual wall-clock elapsed for comparison.
- final AiGenerationProviderFactory providerFactory = new AiGenerationProviderFactory();
- final AiGenerationTimeEstimator estimator = new AiGenerationTimeEstimator();
- final long totalEstimatedSeconds = plan.totalEstimatedSeconds();
- final int totalFiles = plan.routedCount();
- final long runStartNanos = System.nanoTime();
- long doneEstimatedSeconds = 0;
- int doneFiles = 0;
- int wrote = 0;
- int unchanged = 0;
- for (final Map.Entry> group :
- plan.routesByModel().entrySet()) {
- final String aiDefinitionKey = group.getKey();
- getLog().info("Loading model '" + aiDefinitionKey + "' for "
- + group.getValue().size() + " file(s)");
- try (AiGenerationProvider provider = providerFactory.create(
- generationProvider, buildLlamaCppJniConfig(aiDefinitionKey), promptSupport)) {
- final AiFieldGenerationSupport support =
- new AiFieldGenerationSupport(provider, promptPreparationSupport, modelDefinitionSupport);
- for (final AiIndexPlan.Entry entry : group.getValue()) {
- if (fileIndexer.indexFile(entry.file(), entry.rule(), support)) {
- wrote++;
- } else {
- unchanged++;
- }
- doneEstimatedSeconds += entry.estimatedSeconds();
- doneFiles++;
- final long elapsedSeconds = (System.nanoTime() - runStartNanos) / NANOS_PER_SECOND;
- final long remainingSeconds = Math.max(0L, totalEstimatedSeconds - doneEstimatedSeconds);
- getLog().info(AiProgressBar.render(doneEstimatedSeconds, totalEstimatedSeconds)
- + " " + doneFiles + "/" + totalFiles + " files - est. "
- + estimator.formatDuration(doneEstimatedSeconds) + "/"
- + estimator.formatDuration(totalEstimatedSeconds) + " done, "
- + estimator.formatDuration(remainingSeconds) + " left (estimate) | "
- + estimator.formatDuration(elapsedSeconds) + " elapsed (actual)");
- }
- }
- }
-
- getLog().info("Generated AI files: " + wrote + " written, " + unchanged + " unchanged, "
- + plan.skipped().size() + " skipped");
-
- } catch (IOException e) {
- throw new MojoExecutionException(
- "Failed to generate AI index files under " + outputPath + " from base " + basePath, e);
- }
-
- getLog().info("AI index generation finished.");
- }
-
- private List resolveFileExtensions() {
- final List configured = fileExtensions;
- if (configured == null || configured.isEmpty()) {
- return compatibilityHelper.listOf(DEFAULT_FILE_EXTENSION);
- }
- return configured;
- }
-
- /**
- * Resolves each rule's {@code factsKey} to its shared {@code } group, copying the
- * counters onto the rule's {@code facts}. Translates a misconfiguration (unknown key, or a definition
- * with a null key) into a {@link MojoExecutionException} so Maven reports a user configuration error.
- *
- * @throws MojoExecutionException if a {@code factsKey} matches no group or a definition has a null key
- */
- private void resolveSharedFacts() throws MojoExecutionException {
- try {
- new AiFactDefinitionSupport(factDefinitions).resolveFactsKeys(fieldGenerations);
- } catch (final IllegalArgumentException | NullPointerException e) {
- throw new MojoExecutionException("Invalid factDefinitions/factsKey configuration: " + e.getMessage(), e);
- }
- }
-}
diff --git a/pom.xml b/pom.xml
index 31c443f..14fb334 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,7 +12,7 @@ SPDX-License-Identifier: Apache-2.0
net.ladenthinsrcmorph-parent
- 1.1.0-SNAPSHOT
+ 1.1.0pom${project.groupId}:${project.artifactId}
@@ -52,13 +52,38 @@ SPDX-License-Identifier: Apache-2.0
+
+
+ 2.22.0
+
+
+ srcmorph
+ srcmorph-cli
+ srcmorph-maven-pluginllamacpp-ai-index-maven-plugin
@@ -84,6 +109,25 @@ SPDX-License-Identifier: Apache-2.0
logback-classic1.5.37
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${jackson.version}
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-yaml
+ ${jackson.version}
+
diff --git a/srcmorph-cli/README.md b/srcmorph-cli/README.md
new file mode 100644
index 0000000..29874ca
--- /dev/null
+++ b/srcmorph-cli/README.md
@@ -0,0 +1,107 @@
+# srcmorph-cli
+
+`net.ladenthin:srcmorph-cli` is a standalone command-line front end for
+[srcmorph](../README.md): a single `java -jar` invocation driven by one JSON or YAML configuration
+file, with no Maven project or build required. It wraps the
+[`srcmorph`](../srcmorph/README.md) core library's engines exactly the way the
+[Maven plugin](../srcmorph-maven-plugin/README.md) does, following the
+[BitcoinAddressFinder](https://github.com/bernardladenthin/BitcoinAddressFinder) `cli.Main` pattern.
+
+## Running it
+
+```bash
+java -jar srcmorph-cli--jar-with-dependencies.jar
+```
+
+The fat jar (`srcmorph-cli--jar-with-dependencies.jar`, built by `mvn package`) bundles every
+dependency, including a logback binding, so it runs standalone. Both `.json`/`.js` (parsed via a
+Jackson `ObjectMapper`) and `.yaml`/`.yml` (parsed via `YAMLMapper`) are supported — pick whichever you
+prefer; both mappers are configured strictly (`FAIL_ON_UNKNOWN_PROPERTIES`), so a typo'd key fails the
+run immediately rather than being silently ignored. On startup the CLI logs the parsed configuration
+back to you (re-serialized as both JSON and YAML) so you can confirm exactly what will run before any
+model loads or file is written.
+
+See [`../examples/`](../examples/) for a complete, ready-to-run set of `config_*.json`/`.yaml` files
+(all using the `mock` provider, so they run with no GGUF model on disk) plus paired
+`run_*.sh`/`run_*.bat` launcher scripts and an example `logbackConfiguration.xml`.
+
+## Config-file reference
+
+The root object is `net.ladenthin.srcmorph.cli.configuration.CConfiguration` — a plain, public-field
+JavaBean (Jackson binds it directly, no getters/setters needed):
+
+```java
+public class CConfiguration {
+ public CCommand command = CCommand.Plan;
+ public SrcMorphConfiguration srcMorph = new SrcMorphConfiguration();
+}
+```
+
+- **`command`** — one of the six `CCommand` values below. Defaults to `Plan` — the safe default: no
+ command means no model load and nothing written.
+- **`srcMorph`** — the **same** `net.ladenthin.srcmorph.config.SrcMorphConfiguration` object the Maven
+ plugin's mojos build from their own `@Parameter` fields. **Every key under `srcMorph` in your JSON/YAML
+ file reads identically to the matching Maven `` XML element** — `baseDirectory`,
+ `outputDirectory`, `subtrees`, `excludes`, `fileExtensions`, `generationProvider`,
+ `promptDefinitions`, `aiDefinitions`, `fieldGenerations`, `factDefinitions`, the `llama*` fallback
+ parameters, `force`, `planOnly`, `projectName`. If you already know the plugin's XML shape, you
+ already know this shape — see
+ [`../srcmorph-maven-plugin/README.md`](../srcmorph-maven-plugin/README.md)'s
+ "Configuration" section for the full field-by-field reference (routing-rule conditions, oversize
+ strategies, per-model sampling parameters, etc.), which applies verbatim here.
+
+### The six `CCommand` values
+
+| Command | What it does |
+|---|---|
+| `Plan` | Runs `GenerateEngine` with `planOnly` **forced** to `true`, regardless of what the file says — builds and logs the routing plan (which model + prompt each file would get) and stops. No model is loaded, nothing is written. The default; always safe to run. |
+| `GenerateFileIndex` | Phase 1, exactly as configured: indexes source files and fills in their AI-generated summaries (`GenerateEngine`). |
+| `AggregatePackages` | Phase 2: aggregates per-package `.ai.md` index files (`AggregatePackagesEngine`). |
+| `AggregateProject` | Phase 3: aggregates the single project-level `.ai.md` index (`AggregateProjectEngine`). |
+| `All` | Runs `GenerateFileIndex`, then `AggregatePackages`, then `AggregateProject` in order, stopping at the first phase that fails. |
+| `Calibrate` | Loads each distinct model your `fieldGenerations` would route to, measures its prefill/decode throughput, and prints a paste-ready `` XML block per model to standard output (`CalibrateEngine`). |
+
+### One shared `srcMorph` object across commands — a nuance to know
+
+Unlike the Maven plugin, where each goal execution gets its **own** `` block in the
+POM, the CLI's `All` and `Calibrate` commands reuse the **one** `srcMorph.fieldGenerations` list across
+every engine they run. That list means different things to different engines:
+
+- **`GenerateFileIndex`** *routes*: each file is matched to exactly one rule by `condition` + `priority`
+ (the explicit `fallback` catches anything unmatched).
+- **`AggregatePackages`** does **not** route by condition — it applies **every** entry in
+ `fieldGenerations` to every package, in list order, and the **last** entry's output is kept.
+- **`AggregateProject`**'s optional AI overview paragraph uses only `fieldGenerations[0]`.
+
+If your `fieldGenerations` list has more than one entry (e.g. a conditioned Java-file rule plus a
+fallback, as in [`../examples/config_All.json`](../examples/config_All.json)), running `All` will
+still aggregate every package using the *entire* list's rules layered on top of each other (last one
+wins) — which is usually fine (the rules typically write similar structured Markdown), but is worth
+knowing if you see a package's body reflecting your fallback rule's prompt rather than the
+file-specific one. A single fallback-only rule sidesteps the question entirely and behaves identically
+across all three phases. See [`../srcmorph/README.md`](../srcmorph/README.md) for the engine-level
+detail.
+
+## Testing
+
+- `MainTest` — unit tests for the extension-dispatched loader and the JSON/YAML mappers.
+- `configuration.ConfigBindingTest` — round-trips a private ad-hoc
+ `src/test/resources/test-fixtures/minimal-generate.{json,yaml}` fixture pair.
+- `ExamplesConfigBindingTest` — sweeps every shipped `../examples/config_*.{json,yaml}` file (the
+ public, documented examples) through the same strict mappers, so an example can never silently drift
+ out of sync with `SrcMorphConfiguration`'s field set.
+- `CliEndToEndTest` — drives `Main#run()` directly (no forked process) against the `mock` provider for
+ the `All` and `Plan` commands, asserting the expected `.ai.md` tree lands on disk (or, for `Plan`,
+ that nothing is written and the original configuration object is never mutated).
+- `CliArchitectureTest` — ArchUnit rules: this module is the reactor's leaf-most consumer, no public
+ mutable fields outside `configuration`, no `System.exit`, no dependency on the Maven Plugin API.
+
+## Building
+
+```bash
+mvn -pl srcmorph-cli -am package
+```
+
+produces `srcmorph-cli/target/srcmorph-cli-.jar` (plain jar) and
+`srcmorph-cli/target/srcmorph-cli--jar-with-dependencies.jar` (the fat jar you actually run —
+bound unconditionally to the `package` phase, since for this module the fat jar *is* the deliverable).
diff --git a/srcmorph-cli/pom.xml b/srcmorph-cli/pom.xml
new file mode 100644
index 0000000..26c98f6
--- /dev/null
+++ b/srcmorph-cli/pom.xml
@@ -0,0 +1,590 @@
+
+
+
+ 4.0.0
+
+
+ net.ladenthin
+ srcmorph-parent
+ 1.1.0
+ ../pom.xml
+
+
+ srcmorph-cli
+ jar
+
+ srcmorph-cli
+ Standalone CLI for srcmorph, driven by a single JSON/YAML configuration file (BitcoinAddressFinder cli/Main.java pattern). Wraps the srcmorph core library's engines; ships as a jar-with-dependencies fat jar.
+
+
+ bernardladenthin
+ 8
+ 21
+ UTF-8
+
+ 6.1.2
+ 3.0
+ 2.50.0
+ 0.13.7
+ 1.0.0
+ 4.2.1
+ 1.4.2
+ 4.10.2.0
+ 1.18.46
+ 7.7.4
+ 1.14.0
+ 3.8.0
+ 2.94.0
+
+ 2026-07-15T20:07:58Z
+
+
+
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+ provided
+
+
+ org.jspecify
+ jspecify
+ ${jspecify.version}
+
+
+ org.checkerframework
+ checker-qual
+ ${checker.version}
+
+
+
+
+ net.ladenthin
+ srcmorph
+ ${project.version}
+
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ ch.qos.logback
+ logback-classic
+ runtime
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-yaml
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.version}
+ test
+
+
+
+ org.hamcrest
+ hamcrest
+ ${hamcrest.version}
+ test
+
+
+
+ com.tngtech.archunit
+ archunit-junit5
+ ${archunit.version}
+ test
+
+
+
+
+
+
+
+ com.diffplug.spotless
+ spotless-maven-plugin
+ ${spotless.version}
+
+
+ com.github.spotbugs
+ spotbugs-maven-plugin
+ ${spotbugs.version}
+
+
+ io.github.git-commit-id
+ git-commit-id-maven-plugin
+ 10.0.0
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+ 3.8.0
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.15.0
+
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+ 3.6.3
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.2.8
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.5.0
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.12.0
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.4.0
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.6
+
+
+ @{argLine} -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=. -XX:ErrorFile=hs_err_pid%p.log
+
+ false
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.15
+
+
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.11.0
+
+
+
+
+
+ io.github.git-commit-id
+ git-commit-id-maven-plugin
+
+
+ get-git-properties
+
+ revision
+
+ initialize
+
+
+
+ yyyy-MM-dd'T'HH:mm:ss'Z'
+ UTC
+ false
+ false
+
+
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+
+
+ enforce-maven
+
+ enforce
+
+
+
+
+ [3.6.3,)
+
+
+ [1.8,)
+
+
+
+
+
+ commons-logging:commons-logging
+
+ log4j:log4j
+
+ org.hamcrest:hamcrest-core
+ org.hamcrest:hamcrest-library
+ org.hamcrest:hamcrest-all
+
+ junit:junit
+ junit:junit-dep
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ ${maven.compiler.release}
+ ${maven.compiler.testRelease}
+ true
+ true
+
+
+ -Xlint:all,-serial,-options,-classfile,-processing
+ -Werror
+
+ -processor
+ lombok.launch.AnnotationProcessorHider$AnnotationProcessor,lombok.launch.AnnotationProcessorHider$ClaimingProcessor,org.checkerframework.checker.nullness.NullnessChecker
+ -XDaddTypeAnnotationsToSymbol=true
+ -XDcompilePolicy=simple
+ --should-stop=ifError=FLOW
+ -Xplugin:ErrorProne -XepExcludedPaths:.*/generated-sources/.* -Xep:NullAway:ERROR -XepOpt:NullAway:OnlyNullMarked=true -XepOpt:NullAway:ExcludedFieldAnnotations=org.apache.maven.plugins.annotations.Parameter,org.apache.maven.plugins.annotations.Component -XepOpt:NullAway:JSpecifyMode=true -XepOpt:NullAway:CheckOptionalEmptiness=true -XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true -XepOpt:NullAway:AcknowledgeAndroidRecent=true -XepOpt:NullAway:AssertsEnabled=true -Xep:BoxedPrimitiveEquality:ERROR -Xep:EqualsHashCode:ERROR -Xep:EqualsIncompatibleType:ERROR -Xep:IdentityBinaryExpression:ERROR -Xep:SelfAssignment:ERROR -Xep:SelfComparison:ERROR -Xep:SelfEquals:ERROR -Xep:DeadException:ERROR -Xep:FormatString:ERROR -Xep:InvalidPatternSyntax:ERROR -Xep:OptionalEquality:ERROR -Xep:ImpossibleNullComparison:ERROR
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+ com.google.errorprone
+ error_prone_core
+ ${errorprone.version}
+
+
+ com.uber.nullaway
+ nullaway
+ ${nullaway.version}
+
+
+ org.checkerframework
+ checker
+ ${checker.version}
+
+
+
+
+
+ default-compile
+
+
+
+ module-info.java
+
+
+
+
+ module-info-compile
+ compile
+
+ compile
+
+
+
+ 9
+
+ module-info.java
+
+
+
+
+
+
+ default-testCompile
+
+
+ -XDaddTypeAnnotationsToSymbol=true
+ -XDcompilePolicy=simple
+ --should-stop=ifError=FLOW
+ -Xplugin:ErrorProne -Xep:NullAway:OFF -Xep:GuardedBy:OFF -Xep:IdentityBinaryExpression:OFF
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+ attach-sources
+ verify
+
+ jar-no-fork
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+ ${maven.compiler.release}
+ true
+ true
+ all
+
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ net.ladenthin.srcmorph.cli.Main
+ net.ladenthin.srcmorph.cli
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+
+ jar-with-dependencies
+
+
+
+ net.ladenthin.srcmorph.cli.Main
+
+
+
+
+
+ build-fat-jar
+ package
+
+ single
+
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+
+
+ prepare-agent
+
+ prepare-agent
+
+
+
+ report
+ test
+
+ report
+
+
+
+
+
+ com.diffplug.spotless
+ spotless-maven-plugin
+
+
+
+ src/main/java/**/*.java
+ src/test/java/**/*.java
+
+
+ ${palantir-java-format.version}
+
+
+
+
+
+
+
+
+ spotless-check
+ verify
+
+ check
+
+
+
+
+
+ com.github.spotbugs
+ spotbugs-maven-plugin
+
+ Max
+ Low
+ true
+ false
+ spotbugs-exclude.xml
+
+
+ com.mebigfatguy.fb-contrib
+ fb-contrib
+ ${fb-contrib.version}
+
+
+ com.h3xstream.findsecbugs
+ findsecbugs-plugin
+ ${findsecbugs.version}
+
+
+
+
+
+ spotbugs-check
+ verify
+
+ check
+
+
+
+
+
+
+
+
+
diff --git a/srcmorph-cli/spotbugs-exclude.xml b/srcmorph-cli/spotbugs-exclude.xml
new file mode 100644
index 0000000..2f41447
--- /dev/null
+++ b/srcmorph-cli/spotbugs-exclude.xml
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/srcmorph-cli/src/main/java/module-info.java b/srcmorph-cli/src/main/java/module-info.java
new file mode 100644
index 0000000..03f4222
--- /dev/null
+++ b/srcmorph-cli/src/main/java/module-info.java
@@ -0,0 +1,71 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+
+/**
+ * JPMS module descriptor for the srcmorph CLI.
+ *
+ *
The module exports the two packages that make up the CLI: {@code cli} (the {@code Main}
+ * entry point) and {@code cli.configuration} ({@code CConfiguration} + {@code CCommand}, the
+ * JSON/YAML-bindable wrapper around the core library's own {@code SrcMorphConfiguration}). It
+ * requires the {@code net.ladenthin.srcmorph} core module (for the shared configuration object and
+ * the {@code engine} orchestration classes {@code Main} drives) and Jackson's {@code databind}
+ * module (for the JSON/YAML binding); the {@code configuration} package is {@code opens} to
+ * {@code com.fasterxml.jackson.databind} because Jackson's field-based binding style — the same
+ * public-mutable-field JavaBean convention {@code SrcMorphConfiguration} and BitcoinAddressFinder's
+ * own {@code CConfiguration} use — needs reflective access under the Java Platform Module System's
+ * strong encapsulation, which {@code exports} alone does not grant for field/method injection
+ * (only compile-time type visibility).
+ *
+ *
JSpecify {@code @NullMarked} is declared at the module level here so that no source
+ * file compiled at {@code --release 8} references the JSpecify annotation type directly.
+ * Otherwise javac would emit an unsuppressible {@code unknown enum constant
+ * ElementType.MODULE} classfile-read warning for each source compiled at release 8 that
+ * resolves {@code @NullMarked} ({@code @NullMarked} carries
+ * {@code @Target({MODULE, PACKAGE, TYPE})} and Java 8 does not know about
+ * {@code ElementType.MODULE}). Confining the reference to {@code module-info.java} —
+ * which compiles at {@code --release 9} — keeps that warning out of the build entirely.
+ *
+ *
{@code requires static org.jspecify} is needed only at compile time of this
+ * descriptor; JSpecify annotations carry {@code RetentionPolicy.CLASS} so module-path
+ * consumers never need jspecify on their runtime path. Checker Framework qualifiers are
+ * likewise compile-time only. The fat jar produced by {@code maven-assembly-plugin} is a
+ * classpath-only artifact and flattens multiple {@code module-info.class} entries harmlessly,
+ * same as any other jar-with-dependencies build.
+ *
+ *
This descriptor is NOT purely decorative — unlike the core/plugin modules'
+ * own {@code module-info.java} (which only need to satisfy the isolated, release-9
+ * {@code module-info-compile} execution), Maven Surefire's default module-path detection
+ * (triggered by the presence of {@code module-info.class} in {@code target/classes} once the
+ * {@code compile} phase has run, i.e. before {@code test}) resolves this module's runtime
+ * dependency graph from the {@code requires} clauses below. Both real JPMS modules Jackson's
+ * side pulls in — {@code com.fasterxml.jackson.databind} and
+ * {@code com.fasterxml.jackson.dataformat.yaml} — must be listed explicitly or a real
+ * {@link java.lang.IllegalAccessError} is thrown at test time (not merely a compile-time
+ * warning): {@code YAMLMapper} lives in a distinct named module from {@code ObjectMapper}, and
+ * JPMS strong encapsulation blocks any access this module does not declare a {@code requires}
+ * read-edge for, even though both jars sit on the same dependency list.
+ *
+ *
This descriptor compiles at {@code --release 9}; the rest of the source compiles
+ * at {@code --release 8}. Java 8 runtimes silently ignore {@code module-info.class} at
+ * the JAR root.
+ */
+@org.jspecify.annotations.NullMarked
+module net.ladenthin.srcmorph.cli {
+ requires static org.jspecify;
+
+ // Lombok is `provided` scope: only used at compile time to generate equals/hashCode/toString.
+ // `requires static` means the runtime does not need the lombok jar on the module path —
+ // the @lombok.Generated annotation carried on generated members has CLASS retention.
+ requires static lombok;
+ requires net.ladenthin.srcmorph;
+ requires org.slf4j;
+ requires com.fasterxml.jackson.databind;
+ requires com.fasterxml.jackson.dataformat.yaml;
+
+ opens net.ladenthin.srcmorph.cli.configuration to
+ com.fasterxml.jackson.databind;
+
+ exports net.ladenthin.srcmorph.cli;
+ exports net.ladenthin.srcmorph.cli.configuration;
+}
diff --git a/srcmorph-cli/src/main/java/net/ladenthin/srcmorph/cli/Main.java b/srcmorph-cli/src/main/java/net/ladenthin/srcmorph/cli/Main.java
new file mode 100644
index 0000000..27bbb95
--- /dev/null
+++ b/srcmorph-cli/src/main/java/net/ladenthin/srcmorph/cli/Main.java
@@ -0,0 +1,321 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.cli;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Locale;
+import lombok.ToString;
+import net.ladenthin.srcmorph.cli.configuration.CCommand;
+import net.ladenthin.srcmorph.cli.configuration.CConfiguration;
+import net.ladenthin.srcmorph.config.SrcMorphConfiguration;
+import net.ladenthin.srcmorph.engine.AggregatePackagesEngine;
+import net.ladenthin.srcmorph.engine.AggregateProjectEngine;
+import net.ladenthin.srcmorph.engine.CalibrateEngine;
+import net.ladenthin.srcmorph.engine.CalibrationReport;
+import net.ladenthin.srcmorph.engine.GenerateEngine;
+import net.ladenthin.srcmorph.engine.SrcMorphException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * CLI entry point: loads a single JSON/YAML {@link CConfiguration} file (the program's sole
+ * argument) and dispatches to the configured {@link CCommand}.
+ *
+ *
Mirrors the BitcoinAddressFinder {@code cli.Main} pattern: extension-dispatched
+ * deserialization ({@code .json}/{@code .js} via Jackson's {@code ObjectMapper},
+ * {@code .yaml}/{@code .yml} via {@code YAMLMapper}), an echo-back of the parsed configuration for
+ * review before anything runs, and no {@link System#exit(int)} anywhere — a failure propagates as
+ * an unchecked exception out of {@link #main(String[])} so the JVM's own uncaught-exception
+ * handling reports it (a non-zero process exit without bypassing normal shutdown).
+ */
+@ToString
+public class Main {
+
+ /**
+ * File extension for JavaScript configuration files; treated identically to {@link #FILE_EXTENSION_JSON}.
+ */
+ static final String FILE_EXTENSION_JS = ".js";
+
+ /**
+ * Standard file extension for JSON configuration files.
+ *
+ * @see #FILE_EXTENSION_JS
+ */
+ static final String FILE_EXTENSION_JSON = ".json";
+
+ /**
+ * Standard long-form file extension for YAML configuration files.
+ *
+ * @see #FILE_EXTENSION_YML
+ */
+ static final String FILE_EXTENSION_YAML = ".yaml";
+
+ /** Short-form file extension for YAML configuration files; treated identically to {@link #FILE_EXTENSION_YAML}. */
+ static final String FILE_EXTENSION_YML = ".yml";
+
+ /** SLF4J logger for the CLI entry point. */
+ private static final Logger LOGGER = LoggerFactory.getLogger(Main.class);
+
+ private final CConfiguration configuration;
+
+ /**
+ * Creates a new main instance for the given, already-loaded configuration.
+ *
+ * @param configuration the loaded configuration
+ */
+ public Main(final CConfiguration configuration) {
+ this.configuration = configuration;
+ }
+
+ /**
+ * Reads the entire contents of {@code path} as a UTF-8 string.
+ *
+ * @param path the file to read
+ * @return the file contents
+ * @throws IOException if the file cannot be read
+ */
+ public static String readString(final Path path) throws IOException {
+ return new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Parses a JSON configuration string.
+ *
+ * @param configurationString the JSON document
+ * @return the parsed {@link CConfiguration}
+ * @throws IOException if the JSON cannot be deserialised, including an unknown property (the
+ * mapper is strict — see {@link #newJsonMapper()})
+ */
+ public static CConfiguration fromJson(final String configurationString) throws IOException {
+ final ObjectMapper mapper = newJsonMapper();
+ return mapper.readValue(configurationString, CConfiguration.class);
+ }
+
+ /**
+ * Parses a YAML configuration string.
+ *
+ * @param configurationString the YAML document
+ * @return the parsed {@link CConfiguration}
+ * @throws IOException if the YAML cannot be deserialised, including an unknown property (the
+ * mapper is strict — see {@link #newYamlMapper()})
+ */
+ public static CConfiguration fromYaml(final String configurationString) throws IOException {
+ final YAMLMapper mapper = newYamlMapper();
+ return mapper.readValue(configurationString, CConfiguration.class);
+ }
+
+ /**
+ * Serialises a {@link CConfiguration} as indented JSON.
+ *
+ * @param configuration the configuration to serialise
+ * @return the JSON representation
+ * @throws IOException if the configuration cannot be serialised
+ */
+ public static String configurationToJson(final CConfiguration configuration) throws IOException {
+ final ObjectMapper mapper = newJsonMapper();
+ mapper.enable(SerializationFeature.INDENT_OUTPUT);
+ return mapper.writeValueAsString(configuration);
+ }
+
+ /**
+ * Serialises a {@link CConfiguration} as YAML.
+ *
+ * @param configuration the configuration to serialise
+ * @return the YAML representation
+ * @throws IOException if the configuration cannot be serialised
+ */
+ public static String configurationToYAML(final CConfiguration configuration) throws IOException {
+ final YAMLMapper mapper = newYamlMapper();
+ return mapper.writeValueAsString(configuration);
+ }
+
+ /**
+ * Loads a configuration file from disk, picking the parser based on the file extension.
+ *
+ *
Supported extensions (case-insensitive):
+ *
+ *
{@link #FILE_EXTENSION_JSON} / {@link #FILE_EXTENSION_JS} → parsed as JSON via {@link #fromJson(String)}
+ *
{@link #FILE_EXTENSION_YAML} / {@link #FILE_EXTENSION_YML} → parsed as YAML via {@link #fromYaml(String)}
+ *
+ *
+ * @param configurationPath the configuration file
+ * @return the parsed configuration
+ * @throws IOException if the file cannot be read or its content cannot be deserialised
+ * @throws IllegalArgumentException if {@code configurationPath} does not end with a supported extension
+ */
+ public static CConfiguration loadConfiguration(final Path configurationPath) throws IOException {
+ final String configurationAsString = readString(configurationPath);
+ final String lowerPath = configurationPath.toString().toLowerCase(Locale.ROOT);
+ if (lowerPath.endsWith(FILE_EXTENSION_JS) || lowerPath.endsWith(FILE_EXTENSION_JSON)) {
+ return fromJson(configurationAsString);
+ } else if (lowerPath.endsWith(FILE_EXTENSION_YAML) || lowerPath.endsWith(FILE_EXTENSION_YML)) {
+ return fromYaml(configurationAsString);
+ } else {
+ throw new IllegalArgumentException("Unknown file ending for: " + configurationPath);
+ }
+ }
+
+ /**
+ * Java entry point. Loads the configuration file passed as the first argument, logs the parsed
+ * configuration back for review, and runs the configured command.
+ *
+ * @param args the program arguments; expects a single path to a JSON or YAML configuration file
+ * @throws IOException if the configuration file cannot be read or deserialised
+ */
+ public static void main(final String[] args) throws IOException {
+ if (args.length != 1) {
+ LOGGER.error("Invalid arguments. Pass path to configuration as first argument.");
+ return;
+ }
+ final Path configurationPath = Paths.get(args[0]);
+ final CConfiguration configuration = loadConfiguration(configurationPath);
+ final Main main = new Main(configuration);
+ main.logConfigurationTransformation();
+ main.run();
+ }
+
+ /**
+ * Logs the JSON and YAML representations of the loaded configuration for review, so a user can
+ * confirm exactly what was parsed before any model loads or file is written.
+ *
+ * @throws IOException if the configuration cannot be serialised
+ */
+ public void logConfigurationTransformation() throws IOException {
+ final String json = configurationToJson(configuration);
+ final String yaml = configurationToYAML(configuration);
+ LOGGER.info("Please review the transformed configuration to ensure it aligns with your expectations "
+ + "and requirements before proceeding.:\n"
+ + "########## BEGIN transformed JSON configuration ##########\n"
+ + json
+ + "\n"
+ + "########## END transformed JSON configuration ##########\n"
+ + "\n"
+ + "########## BEGIN transformed YAML configuration ##########\n"
+ + yaml
+ + "\n"
+ + "########## END transformed YAML configuration ##########\n");
+ }
+
+ /**
+ * Dispatches to the engine(s) selected by {@link CConfiguration#command} and runs them.
+ *
+ *
No {@link System#exit(int)} is called anywhere: a failure is wrapped as an unchecked
+ * {@link IllegalStateException} and rethrown, so it propagates out of {@link #main(String[])}
+ * as an ordinary uncaught exception.
+ */
+ public void run() {
+ LOGGER.info(configuration.command.name());
+ try {
+ switch (configuration.command) {
+ case Plan: {
+ new GenerateEngine(copyWithPlanOnlyForced(configuration.srcMorph)).execute();
+ break;
+ }
+ case GenerateFileIndex: {
+ new GenerateEngine(configuration.srcMorph).execute();
+ break;
+ }
+ case AggregatePackages: {
+ new AggregatePackagesEngine(configuration.srcMorph).execute();
+ break;
+ }
+ case AggregateProject: {
+ new AggregateProjectEngine(configuration.srcMorph).execute();
+ break;
+ }
+ case All: {
+ new GenerateEngine(configuration.srcMorph).execute();
+ new AggregatePackagesEngine(configuration.srcMorph).execute();
+ new AggregateProjectEngine(configuration.srcMorph).execute();
+ break;
+ }
+ case Calibrate: {
+ final CalibrationReport report = new CalibrateEngine(configuration.srcMorph).execute();
+ // Deliberate System.out, not SLF4J: this is a paste-ready XML
+ // snippet meant for the user to copy into their POM's , not a log
+ // line, so it must not carry a logger's timestamp/level prefix.
+ System.out.println(report.renderXml()); // NOPMD - intentional stdout, see above
+ break;
+ }
+ default:
+ throw new UnsupportedOperationException(
+ "Command: " + configuration.command.name() + " currently not supported.");
+ }
+ LOGGER.info("Main#run end.");
+ } catch (final SrcMorphException | IOException e) {
+ LOGGER.error("Fatal error during Main.run.", e);
+ throw new IllegalStateException(
+ "Main.run() failed on thread " + Thread.currentThread().getName(), e);
+ }
+ }
+
+ /**
+ * Builds a deep copy of {@code original} with {@link SrcMorphConfiguration#setPlanOnly(boolean)}
+ * forced to {@code true}, regardless of what the configuration file said. Used by the
+ * {@link CCommand#Plan} command so it never accidentally runs a real (model-loading) generation
+ * even if the file's own {@code planOnly} was left {@code false} — and never mutates the shared
+ * {@link CConfiguration#srcMorph} instance the caller already logged.
+ *
+ *
The copy is a round-trip through a JSON mapper, which keeps this in lockstep with every
+ * field {@link SrcMorphConfiguration} gains in the future with no manual field-by-field copy to
+ * maintain. Deliberately uses a lenient mapper (unlike {@link #newJsonMapper()}, which
+ * loads user-authored config files and must fail fast on a typo): the source object also carries
+ * derived, read-only, setter-less getters that a strict mapper's serialize-then-deserialize
+ * round-trip would otherwise reject as "unknown properties" (e.g.
+ * {@code AiFieldGenerationConfig#getOversizeStrategy()}, computed from the raw
+ * {@code onOversize} string field). This copy step never touches user input — {@code original}
+ * was already accepted by the strict mapper when the configuration file was first loaded — so
+ * leniency here does not weaken the config-file-typo-fails-fast contract.
+ *
+ * @param original the configuration to copy
+ * @return a deep copy of {@code original} with {@code planOnly} forced {@code true}
+ */
+ private static SrcMorphConfiguration copyWithPlanOnlyForced(final SrcMorphConfiguration original) {
+ try {
+ final ObjectMapper mapper = new ObjectMapper();
+ mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+ final SrcMorphConfiguration copy =
+ mapper.readValue(mapper.writeValueAsString(original), SrcMorphConfiguration.class);
+ copy.setPlanOnly(true);
+ return copy;
+ } catch (final IOException e) {
+ throw new IllegalStateException("Failed to copy the configuration for the Plan command.", e);
+ }
+ }
+
+ /**
+ * Creates the JSON mapper used for both the CLI's own {@link CConfiguration} parsing and the
+ * internal {@link #copyWithPlanOnlyForced(SrcMorphConfiguration)} round-trip.
+ *
+ * @return a new {@link ObjectMapper} with {@link DeserializationFeature#FAIL_ON_UNKNOWN_PROPERTIES}
+ * explicitly enabled (already Jackson's own default; enabled explicitly here so the strict
+ * intent — a typo in the config file fails fast rather than silently no-op'ing, mirroring
+ * what plexus does for the Maven XML side — is documented in code, not left implicit)
+ */
+ private static ObjectMapper newJsonMapper() {
+ final ObjectMapper mapper = new ObjectMapper();
+ mapper.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+ return mapper;
+ }
+
+ /**
+ * Creates the YAML mapper used for {@link CConfiguration} parsing.
+ *
+ * @return a new {@link YAMLMapper} with {@link DeserializationFeature#FAIL_ON_UNKNOWN_PROPERTIES}
+ * explicitly enabled (see {@link #newJsonMapper()} for the rationale)
+ */
+ private static YAMLMapper newYamlMapper() {
+ final YAMLMapper mapper = new YAMLMapper();
+ mapper.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+ return mapper;
+ }
+}
diff --git a/srcmorph-cli/src/main/java/net/ladenthin/srcmorph/cli/configuration/CCommand.java b/srcmorph-cli/src/main/java/net/ladenthin/srcmorph/cli/configuration/CCommand.java
new file mode 100644
index 0000000..5537c72
--- /dev/null
+++ b/srcmorph-cli/src/main/java/net/ladenthin/srcmorph/cli/configuration/CCommand.java
@@ -0,0 +1,49 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.cli.configuration;
+
+/**
+ * Selects which {@code srcmorph} phase (or phases) {@link net.ladenthin.srcmorph.cli.Main} runs for
+ * a given {@link CConfiguration}.
+ */
+public enum CCommand {
+
+ /**
+ * Builds and logs the Phase 1 routing plan only ({@code GenerateEngine} with {@code planOnly}
+ * forced {@code true} regardless of what the configuration file says); no model is loaded and
+ * nothing is written. The safe default (see {@link CConfiguration#command}).
+ */
+ Plan,
+
+ /**
+ * Phase 1: indexes source files and fills in their AI-generated summary fields
+ * ({@code net.ladenthin.srcmorph.engine.GenerateEngine}, exactly as configured).
+ */
+ GenerateFileIndex,
+
+ /**
+ * Phase 2: aggregates per-package {@code .ai.md} index files
+ * ({@code net.ladenthin.srcmorph.engine.AggregatePackagesEngine}).
+ */
+ AggregatePackages,
+
+ /**
+ * Phase 3: aggregates the single project-level {@code .ai.md} index
+ * ({@code net.ladenthin.srcmorph.engine.AggregateProjectEngine}).
+ */
+ AggregateProject,
+
+ /**
+ * Runs all three phases in order: {@link #GenerateFileIndex}, then {@link #AggregatePackages},
+ * then {@link #AggregateProject}, stopping at the first phase that fails.
+ */
+ All,
+
+ /**
+ * Calibrates every distinct model referenced by the configured routing rules
+ * ({@code net.ladenthin.srcmorph.engine.CalibrateEngine}) and prints a paste-ready
+ * {@code } XML block per model to standard output.
+ */
+ Calibrate
+}
diff --git a/srcmorph-cli/src/main/java/net/ladenthin/srcmorph/cli/configuration/CConfiguration.java b/srcmorph-cli/src/main/java/net/ladenthin/srcmorph/cli/configuration/CConfiguration.java
new file mode 100644
index 0000000..d82b54e
--- /dev/null
+++ b/srcmorph-cli/src/main/java/net/ladenthin/srcmorph/cli/configuration/CConfiguration.java
@@ -0,0 +1,39 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.cli.configuration;
+
+import lombok.ToString;
+import net.ladenthin.srcmorph.config.SrcMorphConfiguration;
+
+/**
+ * Root configuration object loaded from the CLI's JSON/YAML config file.
+ *
+ *
Public mutable-field JavaBean (BitcoinAddressFinder {@code cli.configuration.CConfiguration}
+ * convention), bindable via Jackson's {@code ObjectMapper}/{@code YAMLMapper} without any getters or
+ * setters. Carved out of the {@code noPublicMutableFields} ArchUnit rule via this package's explicit
+ * exception (see {@code CliArchitectureTest}).
+ */
+@ToString
+public class CConfiguration {
+
+ /** Creates a new {@link CConfiguration} with every default applied. */
+ public CConfiguration() {
+ // no-op
+ }
+
+ /**
+ * The command selecting which {@code srcmorph} phase(s) {@link net.ladenthin.srcmorph.cli.Main}
+ * runs. Defaults to {@link CCommand#Plan} so that a missing or misconfigured command never
+ * accidentally triggers a real (possibly expensive, model-loading) run.
+ */
+ public CCommand command = CCommand.Plan;
+
+ /**
+ * The shared core configuration passed to whichever engine {@link #command} selects — the SAME
+ * {@link SrcMorphConfiguration} type the {@code llamacpp-ai-index-maven-plugin} module's mojos
+ * build from their own {@code @Parameter} fields, so a JSON/YAML key here reads identically to
+ * the matching Maven {@code } XML element.
+ */
+ public SrcMorphConfiguration srcMorph = new SrcMorphConfiguration();
+}
diff --git a/srcmorph-cli/src/main/java/net/ladenthin/srcmorph/cli/configuration/package-info.java b/srcmorph-cli/src/main/java/net/ladenthin/srcmorph/cli/configuration/package-info.java
new file mode 100644
index 0000000..56b7587
--- /dev/null
+++ b/srcmorph-cli/src/main/java/net/ladenthin/srcmorph/cli/configuration/package-info.java
@@ -0,0 +1,11 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+
+/**
+ * The CLI's own configuration wrapper: {@link net.ladenthin.srcmorph.cli.configuration.CConfiguration}
+ * (the JSON/YAML document root) and {@link net.ladenthin.srcmorph.cli.configuration.CCommand} (the
+ * selected phase), bindable via Jackson's {@code ObjectMapper} / {@code YAMLMapper}
+ * (BitcoinAddressFinder {@code configuration} package convention: public mutable-field JavaBeans).
+ */
+package net.ladenthin.srcmorph.cli.configuration;
diff --git a/srcmorph-cli/src/main/java/net/ladenthin/srcmorph/cli/package-info.java b/srcmorph-cli/src/main/java/net/ladenthin/srcmorph/cli/package-info.java
new file mode 100644
index 0000000..88f5fa2
--- /dev/null
+++ b/srcmorph-cli/src/main/java/net/ladenthin/srcmorph/cli/package-info.java
@@ -0,0 +1,10 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+
+/**
+ * The srcmorph CLI entry point ({@link net.ladenthin.srcmorph.cli.Main}), driven by a single
+ * JSON/YAML {@link net.ladenthin.srcmorph.cli.configuration.CConfiguration} file passed as the
+ * program's sole argument.
+ */
+package net.ladenthin.srcmorph.cli;
diff --git a/srcmorph-cli/src/test/java/net/ladenthin/srcmorph/cli/CliArchitectureTest.java b/srcmorph-cli/src/test/java/net/ladenthin/srcmorph/cli/CliArchitectureTest.java
new file mode 100644
index 0000000..a331724
--- /dev/null
+++ b/srcmorph-cli/src/test/java/net/ladenthin/srcmorph/cli/CliArchitectureTest.java
@@ -0,0 +1,129 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.cli;
+
+import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.fields;
+import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
+
+import com.tngtech.archunit.core.importer.ImportOption;
+import com.tngtech.archunit.junit.AnalyzeClasses;
+import com.tngtech.archunit.junit.ArchTest;
+import com.tngtech.archunit.lang.ArchRule;
+import org.slf4j.Logger;
+
+/**
+ * Architecture rules for the srcmorph CLI ({@code net.ladenthin.srcmorph.cli}).
+ *
+ *
Mirrors the conventions established by {@code CoreArchitectureTest} (the {@code srcmorph}
+ * module) and BitcoinAddressFinder's own {@code BitcoinAddressFinderArchitectureTest}: no
+ * {@code System.exit}, no public mutable fields outside the {@code configuration} package's
+ * Jackson-bound JavaBeans, no reverse dependency onto this module (it is the reactor's leaf-most
+ * consumer, nothing else may depend on it), and no accidental dependency on the Maven plugin API
+ * (this is a plain standalone CLI, not a Maven-aware component).
+ */
+@AnalyzeClasses(packages = "net.ladenthin.srcmorph.cli", importOptions = ImportOption.DoNotIncludeTests.class)
+public class CliArchitectureTest {
+
+ /**
+ * The CLI is the reactor's leaf-most consumer: {@code srcmorph-cli} is not a compile dependency
+ * of any sibling reactor module (only {@code srcmorph}'s core library is — see the root
+ * {@code pom.xml}'s module comment), so nothing else in the reactor can ever depend on it by
+ * construction (a reverse dependency would simply fail to resolve/compile). Because
+ * {@code @AnalyzeClasses} here only scans this module's own {@code net.ladenthin.srcmorph.cli}
+ * package tree, the in-module half of that check is necessarily vacuous
+ * ({@code allowEmptyShould(true)}); this rule exists as a documentation-level regression guard
+ * should a future package ever be added to this module outside {@code cli..} that reaches back
+ * into it incorrectly.
+ */
+ @ArchTest
+ static final ArchRule cliIsLeaf = noClasses()
+ .that()
+ .resideOutsideOfPackage("net.ladenthin.srcmorph.cli..")
+ .should()
+ .dependOnClassesThat()
+ .resideInAPackage("net.ladenthin.srcmorph.cli..")
+ .allowEmptyShould(true);
+
+ /**
+ * Public mutable state forbidden: any non-static field declared {@code public} must also be
+ * {@code final}. The single documented exception is
+ * {@code net.ladenthin.srcmorph.cli.configuration} — the Jackson-bound JavaBeans
+ * ({@code CConfiguration}) that mirror the core library's own {@code SrcMorphConfiguration} and
+ * BitcoinAddressFinder's {@code cli.configuration.CConfiguration} convention.
+ */
+ @ArchTest
+ static final ArchRule noPublicMutableFields = fields().that()
+ .arePublic()
+ .and()
+ .areNotStatic()
+ .and()
+ .areDeclaredInClassesThat()
+ .resideOutsideOfPackage("net.ladenthin.srcmorph.cli.configuration..")
+ .should()
+ .beFinal()
+ .allowEmptyShould(true); // vacuous today: Main has no public instance fields
+
+ /**
+ * Production code must not call {@link System#exit(int)}; a failure propagates as an unchecked
+ * exception out of {@link Main#main(String[])} instead, so the JVM's own uncaught-exception
+ * handling reports it without bypassing normal shutdown.
+ */
+ @ArchTest
+ static final ArchRule noSystemExit = noClasses()
+ .that()
+ .resideInAPackage("net.ladenthin.srcmorph.cli..")
+ .should()
+ .callMethod(System.class, "exit", int.class)
+ .allowEmptyShould(true);
+
+ /**
+ * This is a plain standalone CLI, not a Maven-aware component: it must never depend on the
+ * Maven Plugin API (the {@code llamacpp-ai-index-maven-plugin} sibling module owns that
+ * boundary).
+ */
+ @ArchTest
+ static final ArchRule mavenFree = noClasses()
+ .that()
+ .resideInAPackage("net.ladenthin.srcmorph.cli..")
+ .should()
+ .dependOnClassesThat()
+ .resideInAPackage("org.apache.maven..")
+ .allowEmptyShould(true);
+
+ /**
+ * Test-framework classes must not appear in production code.
+ */
+ @ArchTest
+ static final ArchRule noTestFrameworksInProduction = noClasses()
+ .that()
+ .resideInAPackage("net.ladenthin.srcmorph.cli..")
+ .should()
+ .dependOnClassesThat()
+ .resideInAnyPackage("org.junit..", "net.jqwik..", "com.tngtech.archunit..");
+
+ /**
+ * Production code must not import unsupported / internal JDK packages.
+ */
+ @ArchTest
+ static final ArchRule noInternalJdkImports = noClasses()
+ .that()
+ .resideInAPackage("net.ladenthin.srcmorph.cli..")
+ .should()
+ .dependOnClassesThat()
+ .resideInAnyPackage("sun..", "com.sun..", "jdk.internal..");
+
+ /**
+ * Every SLF4J {@link Logger} field must be {@code private static final} — a single shared
+ * logger per class, never an instance field or a mutable/visible one.
+ */
+ @ArchTest
+ static final ArchRule loggersArePrivateStaticFinal = fields().that()
+ .haveRawType(Logger.class)
+ .should()
+ .bePrivate()
+ .andShould()
+ .beStatic()
+ .andShould()
+ .beFinal();
+}
diff --git a/srcmorph-cli/src/test/java/net/ladenthin/srcmorph/cli/CliEndToEndTest.java b/srcmorph-cli/src/test/java/net/ladenthin/srcmorph/cli/CliEndToEndTest.java
new file mode 100644
index 0000000..157c1d5
--- /dev/null
+++ b/srcmorph-cli/src/test/java/net/ladenthin/srcmorph/cli/CliEndToEndTest.java
@@ -0,0 +1,99 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.cli;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import net.ladenthin.srcmorph.cli.configuration.CCommand;
+import net.ladenthin.srcmorph.cli.configuration.CConfiguration;
+import net.ladenthin.srcmorph.config.AiFieldGenerationConfig;
+import net.ladenthin.srcmorph.config.AiModelDefinition;
+import net.ladenthin.srcmorph.document.AiMdHeaderCodec;
+import net.ladenthin.srcmorph.prompt.AiPromptDefinition;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * End-to-end exercise of the {@link CCommand#All} command against the {@code mock} provider (no
+ * model load, no forked {@code java -jar} process — {@link Main#run()} is invoked directly), proving
+ * the CLI drives all three engines and the expected {@code .ai.md} tree lands on disk.
+ */
+public class CliEndToEndTest {
+
+ @TempDir
+ Path tempDir;
+
+ private CConfiguration buildAllCommandConfiguration() throws IOException {
+ final Path sourceRoot = tempDir.resolve("src/main/java");
+ Files.createDirectories(sourceRoot.resolve("com/example"));
+ Files.write(sourceRoot.resolve("com/example/Foo.java"), "class Foo {}\n".getBytes(StandardCharsets.UTF_8));
+
+ final CConfiguration configuration = new CConfiguration();
+ configuration.command = CCommand.All;
+ configuration.srcMorph.setBaseDirectory(tempDir.toFile());
+ configuration.srcMorph.setOutputDirectory(tempDir.resolve("out").toFile());
+ configuration.srcMorph.setGenerationProvider("mock");
+ configuration.srcMorph.setPluginVersion("1.0.0-test");
+ configuration.srcMorph.setAiVersion("0.0.0");
+
+ final AiPromptDefinition prompt = new AiPromptDefinition();
+ prompt.setKey("file-body");
+ prompt.setTemplate("Summarize:\n%s");
+ configuration.srcMorph.setPromptDefinitions(Collections.singletonList(prompt));
+
+ final AiModelDefinition model = new AiModelDefinition();
+ model.setKey("mock-model");
+ model.setModelPath("mock.gguf");
+ // Disable automatic maxInputChars calculation so the tiny test source never trips the
+ // oversize path (mirrors GenerateEngineTest's own mock model definition).
+ model.setCharsPerToken(0);
+ configuration.srcMorph.setAiDefinitions(Collections.singletonList(model));
+
+ final AiFieldGenerationConfig rule = new AiFieldGenerationConfig();
+ rule.setPromptKey("file-body");
+ rule.setAiDefinitionKey("mock-model");
+ rule.setFallback(true);
+ configuration.srcMorph.setFieldGenerations(Collections.singletonList(rule));
+
+ return configuration;
+ }
+
+ @Test
+ public void run_allCommand_writesFileAndPackageAndProjectIndex() throws Exception {
+ final CConfiguration configuration = buildAllCommandConfiguration();
+
+ new Main(configuration).run();
+
+ final Path outputRoot = tempDir.resolve("out");
+ // Phase 1: SourceFileIndexer relativises against the "src" root but keeps "main/java/...".
+ assertThat(Files.exists(outputRoot.resolve("main/java/com/example/Foo.java.ai.md")), is(true));
+ // Phase 2: one package.ai.md per directory from the output root down to the leaf package.
+ assertThat(
+ Files.exists(outputRoot.resolve("main/java/com/example/" + AiMdHeaderCodec.PACKAGE_AI_MD_FILENAME)),
+ is(true));
+ assertThat(Files.exists(outputRoot.resolve(AiMdHeaderCodec.PACKAGE_AI_MD_FILENAME)), is(true));
+ // Phase 3: the single project-level index.
+ assertThat(Files.exists(outputRoot.resolve(AiMdHeaderCodec.PROJECT_AI_MD_FILENAME)), is(true));
+ }
+
+ @Test
+ public void run_planCommand_forcesPlanOnlyAndWritesNothing() throws Exception {
+ final CConfiguration configuration = buildAllCommandConfiguration();
+ // The file explicitly asks NOT to plan-only; Plan must force it anyway.
+ configuration.command = CCommand.Plan;
+ configuration.srcMorph.setPlanOnly(false);
+
+ new Main(configuration).run();
+
+ assertThat(Files.exists(tempDir.resolve("out")), is(false));
+ // The original configuration object handed to Main must not have been mutated in place.
+ assertThat(configuration.srcMorph.isPlanOnly(), is(false));
+ }
+}
diff --git a/srcmorph-cli/src/test/java/net/ladenthin/srcmorph/cli/ExamplesConfigBindingTest.java b/srcmorph-cli/src/test/java/net/ladenthin/srcmorph/cli/ExamplesConfigBindingTest.java
new file mode 100644
index 0000000..2a722a0
--- /dev/null
+++ b/srcmorph-cli/src/test/java/net/ladenthin/srcmorph/cli/ExamplesConfigBindingTest.java
@@ -0,0 +1,107 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.cli;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import net.ladenthin.srcmorph.cli.configuration.CConfiguration;
+import net.ladenthin.srcmorph.config.SrcMorphConfiguration;
+import org.junit.jupiter.api.DynamicTest;
+import org.junit.jupiter.api.TestFactory;
+
+/**
+ * Round-trips every shipped {@code examples/config_*.{json,yaml}} fixture (the public, documented
+ * configuration examples referenced from the root and per-module {@code README.md} files) through
+ * the CLI's actual Jackson mappers ({@link Main#fromJson(String)} / {@link Main#fromYaml(String)})
+ * and asserts a minimally sane {@link CConfiguration}/{@link SrcMorphConfiguration} object graph.
+ *
+ *
Unlike {@code configuration.ConfigBindingTest} (which pins the exact object graph of one
+ * ad-hoc private fixture pair), this test is a coverage sweep over the public examples
+ * directory: every {@code config_*.json} and {@code config_*.yaml} file under the repository-root
+ * {@code examples/} directory must parse without error under the CLI's strict
+ * ({@code FAIL_ON_UNKNOWN_PROPERTIES}) mappers, so an example can never silently drift out of sync
+ * with {@link SrcMorphConfiguration}'s actual field set. Maven runs tests with the working directory
+ * set to the module's own base directory ({@code srcmorph-cli/}), so the examples directory is
+ * addressed relative to that as {@code ../examples}.
+ */
+public class ExamplesConfigBindingTest {
+
+ /**
+ * The repository-root {@code examples/} directory, addressed relative to this module's own base
+ * directory (Maven Surefire's working directory during a test run).
+ */
+ private static final Path EXAMPLES_DIRECTORY = Path.of("../examples");
+
+ /**
+ * Builds one {@link DynamicTest} per {@code config_*.json}/{@code config_*.yaml} file found under
+ * {@link #EXAMPLES_DIRECTORY}, each asserting that the file parses cleanly and yields a sane
+ * configuration object graph.
+ *
+ * @return one dynamic test per example configuration file
+ * @throws IOException if {@link #EXAMPLES_DIRECTORY} cannot be listed
+ */
+ @TestFactory
+ public List everyExampleConfig_bindsSuccessfully() throws IOException {
+ final List exampleConfigs = listExampleConfigFiles();
+ assertThat(
+ "expected at least one example config fixture under " + EXAMPLES_DIRECTORY,
+ exampleConfigs.size(),
+ greaterThan(0));
+
+ final List tests = new ArrayList<>(exampleConfigs.size());
+ for (final Path exampleConfig : exampleConfigs) {
+ tests.add(DynamicTest.dynamicTest(
+ exampleConfig.getFileName().toString(), () -> assertBindsSuccessfully(exampleConfig)));
+ }
+ return tests;
+ }
+
+ private static List listExampleConfigFiles() throws IOException {
+ final List found = new ArrayList<>();
+ try (DirectoryStream stream = Files.newDirectoryStream(EXAMPLES_DIRECTORY, "config_*.{json,yaml}")) {
+ for (final Path candidate : stream) {
+ found.add(candidate);
+ }
+ }
+ return found;
+ }
+
+ private static void assertBindsSuccessfully(final Path exampleConfig) throws IOException {
+ final String fileName = exampleConfig.getFileName().toString();
+ final String content = Main.readString(exampleConfig);
+
+ final CConfiguration configuration =
+ fileName.endsWith(".yaml") ? Main.fromYaml(content) : Main.fromJson(content);
+
+ assertThat(fileName + ": command must be set", configuration.command, is(notNullValue()));
+
+ final SrcMorphConfiguration srcMorph = configuration.srcMorph;
+ assertThat(fileName + ": srcMorph must be set", srcMorph, is(notNullValue()));
+ assertThat(fileName + ": baseDirectory must be set", srcMorph.getBaseDirectory(), is(notNullValue()));
+ assertThat(
+ fileName + ": generationProvider must be the safe 'mock' default",
+ srcMorph.getGenerationProvider(),
+ is(equalTo("mock")));
+
+ // Every shipped example configures at least the fallback routing rule, so the run has
+ // something to do (a config with an empty fails GenerateEngine/
+ // CalibrateEngine at run time, so an example silently missing it would be useless).
+ assertThat(
+ fileName + ": fieldGenerations must be configured", srcMorph.getFieldGenerations(), is(notNullValue()));
+ assertThat(
+ fileName + ": fieldGenerations must not be empty",
+ srcMorph.getFieldGenerations().size(),
+ greaterThan(0));
+ }
+}
diff --git a/srcmorph-cli/src/test/java/net/ladenthin/srcmorph/cli/MainTest.java b/srcmorph-cli/src/test/java/net/ladenthin/srcmorph/cli/MainTest.java
new file mode 100644
index 0000000..94ac0ee
--- /dev/null
+++ b/srcmorph-cli/src/test/java/net/ladenthin/srcmorph/cli/MainTest.java
@@ -0,0 +1,142 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.cli;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import net.ladenthin.srcmorph.cli.configuration.CCommand;
+import net.ladenthin.srcmorph.cli.configuration.CConfiguration;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * Argument validation and JSON/YAML parsing behaviour of {@link Main}, all against the {@code mock}
+ * provider (never loads a real model), plus one dedicated ad-hoc test fixture pair (kept private to
+ * this module's {@code src/test/resources} — the public, documented {@code examples/} fixture set is
+ * a later migration step).
+ */
+public class MainTest {
+
+ @TempDir
+ Path tempDir;
+
+ private static final Path MINIMAL_GENERATE_JSON = Path.of("src/test/resources/test-fixtures/minimal-generate.json");
+ private static final Path MINIMAL_GENERATE_YAML = Path.of("src/test/resources/test-fixtures/minimal-generate.yaml");
+
+ private static final String MINIMAL_JSON_STRING = "{\"command\":\"Plan\"}";
+ private static final String MINIMAL_YAML_STRING = "command: Plan\n";
+
+ @Test
+ public void main_noArgumentGiven_returnsWithoutException() {
+ assertDoesNotThrow(() -> Main.main(new String[0]));
+ }
+
+ @Test
+ public void main_tooManyArgumentsGiven_returnsWithoutException() {
+ assertDoesNotThrow(() -> Main.main(new String[] {"one", "two"}));
+ }
+
+ @Test
+ public void main_unknownExtensionPath_throwsIllegalArgumentException() throws IOException {
+ final Path configFile = tempDir.resolve("config.txt");
+ Files.write(configFile, MINIMAL_JSON_STRING.getBytes(StandardCharsets.UTF_8));
+
+ assertThrows(IllegalArgumentException.class, () -> Main.main(new String[] {configFile.toString()}));
+ }
+
+ @Test
+ public void loadConfiguration_jsonExtension_parsesFixture() throws IOException {
+ final CConfiguration configuration = Main.loadConfiguration(MINIMAL_GENERATE_JSON);
+
+ assertThat(configuration, is(notNullValue()));
+ assertThat(configuration.command, is(equalTo(CCommand.GenerateFileIndex)));
+ assertThat(configuration.srcMorph.getGenerationProvider(), is(equalTo("mock")));
+ }
+
+ @Test
+ public void loadConfiguration_yamlExtension_parsesFixture() throws IOException {
+ final CConfiguration configuration = Main.loadConfiguration(MINIMAL_GENERATE_YAML);
+
+ assertThat(configuration, is(notNullValue()));
+ assertThat(configuration.command, is(equalTo(CCommand.GenerateFileIndex)));
+ assertThat(configuration.srcMorph.getGenerationProvider(), is(equalTo("mock")));
+ }
+
+ @Test
+ public void fromJson_validJsonString_returnsExpectedConfiguration() throws IOException {
+ final CConfiguration configuration = Main.fromJson(MINIMAL_JSON_STRING);
+
+ assertThat(configuration, is(notNullValue()));
+ assertThat(configuration.command, is(equalTo(CCommand.Plan)));
+ }
+
+ @Test
+ public void fromYaml_validYamlString_returnsExpectedConfiguration() throws IOException {
+ final CConfiguration configuration = Main.fromYaml(MINIMAL_YAML_STRING);
+
+ assertThat(configuration, is(notNullValue()));
+ assertThat(configuration.command, is(equalTo(CCommand.Plan)));
+ }
+
+ @Test
+ public void fromJson_unknownTopLevelProperty_throwsIOException() {
+ assertThrows(IOException.class, () -> Main.fromJson("{\"thisFieldDoesNotExist\":true}"));
+ }
+
+ @Test
+ public void fromYaml_unknownTopLevelProperty_throwsIOException() {
+ assertThrows(IOException.class, () -> Main.fromYaml("thisFieldDoesNotExist: true\n"));
+ }
+
+ @Test
+ public void configurationToJsonThenFromJson_roundTripsCommand() throws IOException {
+ final CConfiguration original = new CConfiguration();
+ original.command = CCommand.AggregateProject;
+
+ final String json = Main.configurationToJson(original);
+ final CConfiguration roundTripped = Main.fromJson(json);
+
+ assertThat(roundTripped.command, is(equalTo(CCommand.AggregateProject)));
+ }
+
+ @Test
+ public void configurationToYAMLThenFromYaml_roundTripsCommand() throws IOException {
+ final CConfiguration original = new CConfiguration();
+ original.command = CCommand.Calibrate;
+
+ final String yaml = Main.configurationToYAML(original);
+ final CConfiguration roundTripped = Main.fromYaml(yaml);
+
+ assertThat(roundTripped.command, is(equalTo(CCommand.Calibrate)));
+ }
+
+ @Test
+ public void logConfigurationTransformation_defaultConfiguration_doesNotThrow() {
+ final Main main = new Main(new CConfiguration());
+
+ assertDoesNotThrow(main::logConfigurationTransformation);
+ }
+
+ @Test
+ public void run_planCommandWithNoFieldGenerations_throwsIllegalStateException() {
+ final CConfiguration configuration = new CConfiguration();
+ configuration.command = CCommand.Plan;
+ configuration.srcMorph.setBaseDirectory(tempDir.toFile());
+ configuration.srcMorph.setOutputDirectory(tempDir.resolve("out").toFile());
+ // No configured -> GenerateEngine.execute() throws SrcMorphException,
+ // which Main.run() wraps as an unchecked IllegalStateException (no System.exit anywhere).
+ final Main main = new Main(configuration);
+
+ assertThrows(IllegalStateException.class, main::run);
+ }
+}
diff --git a/srcmorph-cli/src/test/java/net/ladenthin/srcmorph/cli/configuration/ConfigBindingTest.java b/srcmorph-cli/src/test/java/net/ladenthin/srcmorph/cli/configuration/ConfigBindingTest.java
new file mode 100644
index 0000000..89f1900
--- /dev/null
+++ b/srcmorph-cli/src/test/java/net/ladenthin/srcmorph/cli/configuration/ConfigBindingTest.java
@@ -0,0 +1,91 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.cli.configuration;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import net.ladenthin.srcmorph.cli.Main;
+import net.ladenthin.srcmorph.config.AiFieldGenerationConfig;
+import net.ladenthin.srcmorph.config.AiModelDefinition;
+import net.ladenthin.srcmorph.config.SrcMorphConfiguration;
+import net.ladenthin.srcmorph.prompt.AiPromptDefinition;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Round-trips the ad-hoc {@code test-fixtures/minimal-generate.{json,yaml}} pair (kept private to
+ * this module — the public, documented {@code examples/} fixture set is a later migration step)
+ * through both {@link Main#fromJson(String)} and {@link Main#fromYaml(String)} and asserts the
+ * resulting {@link CConfiguration}/{@link SrcMorphConfiguration} object graphs are equivalent.
+ *
+ *
Locks in the shared-config-object contract described in {@link SrcMorphConfiguration}'s own
+ * Javadoc: the same field names bind identically whether the source is Maven XML (plexus, not
+ * exercised here), JSON, or YAML.
+ */
+public class ConfigBindingTest {
+
+ private static final Path MINIMAL_GENERATE_JSON = Path.of("src/test/resources/test-fixtures/minimal-generate.json");
+ private static final Path MINIMAL_GENERATE_YAML = Path.of("src/test/resources/test-fixtures/minimal-generate.yaml");
+
+ @Test
+ public void jsonFixture_bindsExpectedObjectGraph() throws IOException {
+ final CConfiguration configuration = Main.fromJson(Main.readString(MINIMAL_GENERATE_JSON));
+
+ assertConfigurationMatchesFixture(configuration);
+ }
+
+ @Test
+ public void yamlFixture_bindsExpectedObjectGraph() throws IOException {
+ final CConfiguration configuration = Main.fromYaml(Main.readString(MINIMAL_GENERATE_YAML));
+
+ assertConfigurationMatchesFixture(configuration);
+ }
+
+ @Test
+ public void jsonAndYamlFixtures_bindToEquivalentConfigurations() throws IOException {
+ final CConfiguration fromJson = Main.fromJson(Main.readString(MINIMAL_GENERATE_JSON));
+ final CConfiguration fromYaml = Main.fromYaml(Main.readString(MINIMAL_GENERATE_YAML));
+
+ assertThat(fromJson.command, is(equalTo(fromYaml.command)));
+ assertThat(fromJson.srcMorph.getGenerationProvider(), is(equalTo(fromYaml.srcMorph.getGenerationProvider())));
+ assertThat(
+ fromJson.srcMorph.getPromptDefinitions().get(0).getKey(),
+ is(equalTo(fromYaml.srcMorph.getPromptDefinitions().get(0).getKey())));
+ assertThat(
+ fromJson.srcMorph.getAiDefinitions().get(0).getKey(),
+ is(equalTo(fromYaml.srcMorph.getAiDefinitions().get(0).getKey())));
+ assertThat(
+ fromJson.srcMorph.getFieldGenerations().get(0).getPromptKey(),
+ is(equalTo(fromYaml.srcMorph.getFieldGenerations().get(0).getPromptKey())));
+ }
+
+ private static void assertConfigurationMatchesFixture(final CConfiguration configuration) {
+ assertThat(configuration.command, is(equalTo(CCommand.GenerateFileIndex)));
+
+ final SrcMorphConfiguration srcMorph = configuration.srcMorph;
+ assertThat(srcMorph.getGenerationProvider(), is(equalTo("mock")));
+
+ assertThat(srcMorph.getPromptDefinitions().size(), is(equalTo(1)));
+ final AiPromptDefinition prompt = srcMorph.getPromptDefinitions().get(0);
+ assertThat(prompt.getKey(), is(equalTo("file-body")));
+ assertThat(prompt.getTemplate(), is(equalTo("Summarize this file:\n%s")));
+
+ assertThat(srcMorph.getAiDefinitions().size(), is(equalTo(1)));
+ final AiModelDefinition model = srcMorph.getAiDefinitions().get(0);
+ assertThat(model.getKey(), is(equalTo("mock-model")));
+ assertThat(model.getModelPath(), is(equalTo("unused-with-mock-provider.gguf")));
+ assertThat(model.getContextSize(), is(equalTo(2048)));
+ assertThat(model.getMaxOutputTokens(), is(equalTo(64)));
+ assertThat(model.getThreads(), is(equalTo(2)));
+
+ assertThat(srcMorph.getFieldGenerations().size(), is(equalTo(1)));
+ final AiFieldGenerationConfig rule = srcMorph.getFieldGenerations().get(0);
+ assertThat(rule.getPromptKey(), is(equalTo("file-body")));
+ assertThat(rule.getAiDefinitionKey(), is(equalTo("mock-model")));
+ assertThat(rule.isFallback(), is(true));
+ }
+}
diff --git a/srcmorph-cli/src/test/resources/test-fixtures/minimal-generate.json b/srcmorph-cli/src/test/resources/test-fixtures/minimal-generate.json
new file mode 100644
index 0000000..4aaaa4f
--- /dev/null
+++ b/srcmorph-cli/src/test/resources/test-fixtures/minimal-generate.json
@@ -0,0 +1,29 @@
+{
+ "command": "GenerateFileIndex",
+ "srcMorph": {
+ "generationProvider": "mock",
+ "promptDefinitions": [
+ {
+ "key": "file-body",
+ "template": "Summarize this file:\n%s"
+ }
+ ],
+ "aiDefinitions": [
+ {
+ "key": "mock-model",
+ "modelPath": "unused-with-mock-provider.gguf",
+ "contextSize": 2048,
+ "maxOutputTokens": 64,
+ "temperature": 0.15,
+ "threads": 2
+ }
+ ],
+ "fieldGenerations": [
+ {
+ "promptKey": "file-body",
+ "aiDefinitionKey": "mock-model",
+ "fallback": true
+ }
+ ]
+ }
+}
diff --git a/srcmorph-cli/src/test/resources/test-fixtures/minimal-generate.yaml b/srcmorph-cli/src/test/resources/test-fixtures/minimal-generate.yaml
new file mode 100644
index 0000000..9056963
--- /dev/null
+++ b/srcmorph-cli/src/test/resources/test-fixtures/minimal-generate.yaml
@@ -0,0 +1,17 @@
+command: GenerateFileIndex
+srcMorph:
+ generationProvider: mock
+ promptDefinitions:
+ - key: file-body
+ template: "Summarize this file:\n%s"
+ aiDefinitions:
+ - key: mock-model
+ modelPath: unused-with-mock-provider.gguf
+ contextSize: 2048
+ maxOutputTokens: 64
+ temperature: 0.15
+ threads: 2
+ fieldGenerations:
+ - promptKey: file-body
+ aiDefinitionKey: mock-model
+ fallback: true
diff --git a/srcmorph-maven-plugin/README.md b/srcmorph-maven-plugin/README.md
new file mode 100644
index 0000000..2bb2864
--- /dev/null
+++ b/srcmorph-maven-plugin/README.md
@@ -0,0 +1,625 @@
+# srcmorph-maven-plugin
+
+A Maven plugin for generating hierarchical, AI-readable documentation of source code projects
+using local llama.cpp-compatible models. It creates structured `.ai.md` files per source file and
+aggregates them into package-level (and project-level) summaries for fast semantic navigation and
+retrieval.
+
+> This module is one of three in the [srcmorph](../README.md) Maven reactor — see the root README
+> for the product overview, badges, and build status. **This plugin was renamed** from
+> `net.ladenthin:llamacpp-ai-index-maven-plugin` to `net.ladenthin:srcmorph-maven-plugin` (goal
+> prefix `srcmorph`, properties `srcmorph.*`) as part of the reactor migration's final step.
+> Consumers still declaring the old coordinates are transparently redirected: the old artifactId is
+> now published only as a tiny relocation-stub POM
+> (`../llamacpp-ai-index-maven-plugin/pom.xml`, ``) pointing here
+> — see the root README's migration note. Internally, the plugin depends on
+> [`net.ladenthin:srcmorph`](../srcmorph/README.md) (a sibling reactor module) for all of
+> its engine/indexing logic — the 5 Mojo classes in this module are thin wrappers that map their
+> `@Parameter` fields onto a shared `SrcMorphConfiguration` and delegate to one of `srcmorph`'s
+> `engine.*` classes. That split is an internal implementation detail; nothing below changes for a
+> user of this plugin.
+
+## Features
+- Generate AI summaries for source files
+- Per-language prompts — Java, SQL schema, and a generic fallback — selected by file extension
+- Weave searchable type, API and domain names into every summary
+- Aggregate summaries at package level
+- Build a single project index (one line + link per package) for top-down navigation, optionally with a one-call AI `#### Overview`
+- Run any phase independently — toggle file / package / project on or off
+- Exclude trivial or generated files with glob patterns
+- Uses local models via llama.cpp (no cloud dependency)
+- Incremental updates (skips unchanged files)
+- Logs a rough per-file duration estimate before each generation (size, token count, expected time)
+- Optimized for AI-assisted code understanding
+## How It Works
+The plugin runs in three phases, building a navigable index from fine to coarse.
+### 1. File Generation (generate)
+- Scans configured source directories
+- Creates `.ai.md` files per source file
+- Each file contains metadata header and markdown summary
+### 2. Package Aggregation (aggregate-packages)
+- Traverses generated `.ai.md` files
+- Builds hierarchical package summaries
+- Produces `package.ai.md` files; the header carries a deterministic `F` link list to each child (package → file navigation)
+### 3. Project Index (aggregate-project)
+- Harvests the one-line lead from every `package.ai.md` — the per-package listing is deterministic, no AI call
+- Produces a single `project.ai.md`: one body line per package (its lead) with the clickable links in the header `F` list
+- A compact, always-loadable table of contents an agent reads first to navigate down
+- **Optional** (opt-in): configure a `` on this goal and it makes *one* extra AI call to
+ write a short `#### Overview` paragraph from the package leads. The deterministic per-package listing is
+ unchanged; with no field generation the goal stays purely deterministic and calls no model.
+## Example Output
+```
+### AiMdDocument.java
+- H: 1.0
+- C: A48CED8C
+- D: 2026-03-15T23:31:52Z
+- T: 2026-03-19T18:13:31Z
+- G: 1.0.0
+- A: 0.0.0
+- X: file
+---
+> Immutable value type pairing a deterministic metadata header with the AI-generated markdown body of one .ai.md document.
+
+#### Purpose
+- Hold one parsed `.ai.md` document as an `AiMdHeader` plus its markdown body.
+
+#### Type
+- record-shaped value class (Java); marked `@ConvertToRecord`.
+
+#### Public API
+- `header() -> AiMdHeader` — the document's metadata header.
+- `body() -> String` — the AI-generated markdown body.
+```
+## Requirements
+- Java 8+ (production code targets Java 8; CI builds on Java 8 via temurin)
+- Maven 3.6.3+
+- Local GGUF model (llama.cpp compatible)
+
+### Running under Java 8: override `checker-qual`
+
+The plugin itself is compiled to Java 8 bytecode, but it pulls in
+[`org.checkerframework:checker-qual`](https://central.sonatype.com/artifact/org.checkerframework/checker-qual)
+transitively, and the version it builds against (`4.2.1`) ships its classes as **Java 11
+bytecode** (class-file major version 55). On a **Java 8** JVM, loading those annotation
+classes fails with `UnsupportedClassVersionError`.
+
+If you run the plugin under a Java 8 Maven/JVM, **override `checker-qual` to `3.55.1`** — the
+**last release whose runtime classes are Java 8 bytecode** (major 52). The `checker-qual` line
+switched to Java 11 bytecode in `4.0.0`; every `3.x` release (`3.42.0` … `3.55.1`) is Java 8
+loadable, and `3.55.1` is the newest of them. (`checker-qual` `3.43.0`–`3.55.1` additionally
+carry a root `module-info.class`, which a Java 8 classpath simply ignores; if you want a jar
+with no `module-info.class` at all, `3.42.0` is the last such release.)
+
+Because this is a *plugin* dependency, the override goes inside the plugin's own
+`` block (a project-level `` does **not** affect a plugin's
+classpath):
+
+```xml
+
+ net.ladenthin
+ srcmorph-maven-plugin
+
+
+
+
+ org.checkerframework
+ checker-qual
+ 3.55.1
+
+
+
+```
+
+Running the plugin under Java 11 or newer needs no override.
+
+## Dependency
+
+The plugin depends on [`net.ladenthin:llama`](https://central.sonatype.com/artifact/net.ladenthin/llama), the Java/JNI binding for llama.cpp (via the `srcmorph` core library it wraps).
+It is published on Maven Central and resolves automatically — no manual installation required.
+
+```xml
+
+ net.ladenthin
+ llama
+ 5.0.6
+
+```
+## Configuration
+The plugin is configured from three building blocks, declared on the plugin inside
+``:
+
+1. **``** — define each GGUF model once (path + sampling parameters), each with a ``.
+2. **``** — define each prompt template once, each with a ``. A template takes
+ two `%s` placeholders: the file/package name and the source (for packages, the child summaries; for
+ the optional project overview, the per-package leads).
+3. **``** — per goal, the routing **rules**. Each rule maps a `` to an
+ `` (model id, which carries the full parameter set) and selects files with a
+ composable **``** tree: ``/``/`` over the leaves ``, ``
+ (``/`` bytes), `` (``/``), ``/`` (ISO-8601
+ instant vs the file's last-modified time), and `` (base-relative glob). When several rules
+ match, the highest `` wins (ties by declaration order). A rule may instead be
+ `true` (ignore matching files — a high-priority skip beats routes and the fallback), and
+ **exactly one** `true` (no condition) catches the rest. A file that
+ matches no rule and no fallback **fails the build**. So one `generate` run can index different file
+ kinds/sizes with **different models *and* prompts**; it loads each model once. Run with
+ `-Dsrcmorph.planOnly=true` to print the routing plan (a copy-pasteable Markdown table: file → rule id →
+ prompt → context-window fit → rough time estimate, summed per model and overall) and stop before
+ loading any model. The plan also checks each file against its routed model's **context window**: a
+ file too large for the window would lose content if trimmed. What happens then is **configuration
+ only** — the plugin never picks a model for you — and is chosen per rule with ``:
+ - **`fail`** *(default)* — abort the build (a hard fail). The fix is to add a `` rule
+ with a size `` that routes oversized files to a model with a large enough window (see the
+ `granite-4.0-h-tiny-bigwindow` definition + the `big-window-java` / `big-window-sql` rules in the
+ POM — IBM Granite 4.0-H-Tiny, Apache-2.0, a hybrid Mamba model whose KV cache grows only linearly,
+ configured at a 384K window to cover files up to ~1 MB; verified summarizing a ~995 KB / ~268K-token
+ file on an 8 GB GPU with no OOM).
+ - **`sample`** — feed only the head of the file (trimmed to the window) in a single call. Fast and
+ bounded; good for repetitive data where the head represents the whole.
+ - **`mapReduce`** — split the file into window-sized chunks at line boundaries (with overlap so
+ records are never torn mid-syntax), summarize each chunk, then combine the partial summaries in one
+ final call. A `` cap bounds the time (a representative subset — always head + tail,
+ evenly spaced — is summarized when the file would exceed the cap), so an arbitrarily large file
+ (e.g. 7 MB) stays within a chosen per-file budget. **Route oversized files to a *small*, fast model
+ for this — not the big-window one.** Prefill is `O(n²)` in prompt length, so chunks should be small:
+ many cheap small-window passes are far faster than a few giant ones (one 384K-token pass alone can
+ dwarf a whole run). E.g. a 16K-window model with `maxChunks=6` is ~7 calls (~1 h order on a
+ reference CPU; less on GPU) and samples a representative slice — the right trade-off for repetitive
+ data. mapReduce *on* a big window is the slowest possible combination; avoid it.
+ - **`deterministic`** — no model at all: emit a deterministic body (size, line count, head/tail
+ sample). Instant; for pure data where no AI analysis is needed.
+
+ Quality from a real model is best within Granite's validated 128K (~500 KB) and degrades gradually
+ beyond it — a whole-file (or chunked) summary still beats a trimmed one.
+
+ **Exact counts with `` (optional, every file — not just oversize).** A sampled/chunked AI
+ summary can't reliably *count* things (no single call sees the whole file — it will guess "25 rows").
+ Add a `` list to a rule and each `{label, pattern}` reports its regex match count over the
+ **whole** source, prepended to the body of every file the rule matches as an exact facts line — so
+ downstream agents get authoritative structural counts in every summary. It's fully generic — the
+ meaning is in the regex, so the same mechanism counts SQL `INSERT` rows or Java `\bboolean\b` fields;
+ multi-line matching is opt-in via the inline `(?m)` flag. Keep patterns robust (a fact that miscounts
+ is worse than none). Example: `**Facts (exact, whole file):** INSERT rows: 36738; tables: 122; views: 4`.
+
+```xml
+
+ net.ladenthin
+ srcmorph-maven-plugin
+ 1.1.0
+
+
+
+
+ src/main/java/com/example
+
+
+ llamacpp-jni
+
+
+
+
+ coder
+ /path/to/model.gguf
+ 32768
+ 1536
+ 0.7
+ 8
+
+
+
+
+
+
+ file-body-java
+
+
+
+ file-body-fallback
+
+
+
+ package-body
+
+
+
+
+ project-body
+
+
+
+
+
+
+
+
+ ai-generate
+ generate-resources
+ generate
+
+
+
+
+ java
+ file-body-java
+ coder
+
+ .java
+
+
+
+ fallback
+ true
+ file-body-fallback
+ coder
+
+
+
+
+
+ ai-aggregate-packages
+ process-resources
+ aggregate-packages
+
+
+
+ package-body
+ coder
+
+
+
+
+
+
+ ai-aggregate-project
+ prepare-package
+ aggregate-project
+
+
+
+ project-body
+ coder
+
+
+
+
+
+
+```
+
+### Routing rules (conditions, priority, skip, plan)
+Within one `generate` run you can route files to different models **and** prompts by size, language,
+age or path. Example — small/medium/large Java files to three context presets, skip generated sources,
+and a fallback for everything else:
+
+```xml
+
+
+ skip-generatedtrue100
+ **/generated/**
+
+
+ java-smallfile-body-java-tersegpt-oss-20B-c16k
+
+ .java
+ 16384
+
+
+
+ java-midfile-body-javagpt-oss-20B-c48k
+
+ .java
+ 1638449152
+
+
+
+ java-largefile-body-java-detailedgpt-oss-20B-c96k
+
+ .java
+ 49152
+
+
+
+ java-hugefile-body-javagpt-oss-20B-c16k
+ mapReduce6
+
+ \bboolean\b
+ (?m)^\s+(public|private|protected).*\(
+
+
+ .java
+ 1048576
+
+
+
+ fallbacktrue
+ file-body-fallbackgpt-oss-20B-c96k
+
+
+```
+
+Notes: size bounds are **min-exclusive / max-inclusive** so `band2.min == band1.max` is non-overlapping;
+`` works the same way; `2026-01-01T00:00:00Z` only (re)indexes
+recently changed files. `` (`fail` *(default)* / `sample` / `mapReduce` / `deterministic` —
+see the context-window note above) chooses what a rule does when a file is still larger than its routed
+model's window; `` caps how many chunks `mapReduce` summarizes. Preview the mapping without
+running a model:
+
+```
+mvn srcmorph:generate -Dsrcmorph.planOnly=true
+```
+
+## Usage
+Run AI index generation:
+```
+mvn clean install -Psrcmorph-selftest
+```
+With native llama tests:
+```
+mvn clean install -Psrcmorph-selftest -DrunNativeLlamaTests=true
+```
+## Plugin Configuration
+Run-level parameters (set in ``):
+- `outputDirectory` — target directory for `.ai.md` files (default: `${project.basedir}/src/site/ai`)
+- `subtrees` — source directories to index, relative to the project base dir (default: `src/main/java`)
+- `fileExtensions` — file extensions to index (default: `.java`)
+- `excludes` — glob patterns for source files to skip, matched against each file's path relative to
+ the base dir with `/` separators, e.g. `**/package-info.java`, `**/generated/**` (default: none).
+ `*` stays within one path segment, `**` spans directories, `?` is a single character.
+- `generationProvider` — AI backend: `mock` (default) or `llamacpp-jni`
+- `force` — regenerate even when a body already exists (default: `false`)
+- `skip` — global switch: skip **every** phase (default: `false`)
+- Per-phase switches — turn any of the three phases on/off independently (each default `false`).
+ Named after the three index levels (`file` / `package` / `project`, the `x` node types):
+ - `srcmorph.file.skip` — skip the **file** phase (the `generate` goal)
+ - `srcmorph.package.skip` — skip the **package** phase (the `aggregate-packages` goal)
+ - `srcmorph.project.skip` — skip the **project** phase (the `aggregate-project` goal)
+
+ So `-Dsrcmorph.package.skip=true` runs file + project only, `-Dsrcmorph.skip=true` runs
+ nothing, and the defaults run all three.
+- `aiDefinitions` / `promptDefinitions` — named models / prompt templates, referenced by key
+- `fieldGenerations` — per goal: which `promptKey` runs with which `aiDefinitionKey` (**required**)
+
+### Per-model `` parameters
+
+Every model knob lives inside its `` (referenced by `aiDefinitionKey`), not as a top-level
+plugin parameter. Only `key` and `modelPath` are required; everything else has a default. The defaults
+below are the shipped values (`AiGenerationConfig.DEFAULT_*`).
+
+| Element | Default | Description |
+|---|---|---|
+| `key` | *(required)* | Identifier referenced by a rule's `aiDefinitionKey` |
+| `modelPath` | *(required)* | Path to the GGUF model file |
+| `contextSize` | `32768` | Context window in tokens |
+| `maxOutputTokens` | `128` | Max generated tokens per call |
+| `threads` | `8` | CPU threads for inference |
+| `temperature` | `0.15` | Sampling temperature |
+| `topP` | `0.9` | Nucleus (top-p) sampling threshold |
+| `topK` | `40` | Top-k sampling limit (`0` = disabled) |
+| `minP` | `0.0` | Min-p sampling threshold (`0.0` = disabled) |
+| `topNSigma` | `-1.0` | Top-n-sigma sampling threshold (`-1.0` = disabled) |
+| `repeatPenalty` | `1.0` | Repetition penalty (`1.0` = disabled) |
+| `charsPerToken` | `4` | Chars-per-token estimate; drives the automatic `maxInputChars` trim budget (`maxInputChars` itself is derived, not a field). Use a value at or below your model's real ratio so the budget stays conservative |
+| `warnOnTrim` | `true` | Log a warning when the source is trimmed to fit the window |
+| `cachePrompt` | `true` | Reuse the shared prompt-prefix KV across files (`cache_prompt`) |
+| `swaFull` | `true` | Keep the full-size sliding-window-attention KV cache (`--swa-full`) |
+| `cacheReuse` | `256` | KV prefix-reuse minimum chunk size in tokens (`--cache-reuse`; `0` = off) |
+| `gpuLayers` | `-1` | GPU layers to offload (`--gpu-layers`); `-1` = auto-fit to free VRAM, `0` = force CPU, `>0` = partial. GPU native only |
+| `mainGpu` | `-1` | Primary GPU index (`--main-gpu`); `-1` = leave default. Matters on multi-GPU hosts (e.g. a Vulkan build enumerates every GPU) |
+| `devices` | *(empty)* | Explicit device selection (`--device`), comma-separated backend device names (e.g. `Vulkan1`); takes precedence over `mainGpu` |
+| `chatTemplateEnableThinking` | `true` | Enable the chat template's thinking mode |
+| `reasoningEffort` | `low` | gpt-oss harmony reasoning effort (`low`/`medium`/`high`); empty omits the kwarg (e.g. for non-gpt-oss models) |
+| `reasoningBudgetTokens` | `-1` | Cap on harmony reasoning tokens (`-1` = unrestricted) |
+| `dryMultiplier` | `0.0` | DRY repetition-penalty multiplier (`0.0` = disabled); the other `dry*` knobs only apply when this is `> 0` |
+| `dryBase` | `1.75` | DRY exponential base |
+| `dryAllowedLength` | `2` | Longest n-gram that may repeat without DRY penalty |
+| `dryPenaltyLastN` | `-1` | DRY look-back window in tokens (`-1` = whole context, `0` = off) |
+| `drySequenceBreakers` | *(empty)* | DRY sequence-breaker strings; empty = the binding defaults |
+| `stopStrings` | *(empty)* | Extra stop strings that end generation |
+
+## Prompt System
+Prompts are defined in the plugin configuration (``) and referenced by key
+from ``. The self-test profile defines five:
+- `file-body-java` — summarizes a single Java source file (types, public API, dependencies)
+- `file-body-sql` — summarizes a single SQL file as schema (tables/views/procedures, columns,
+ the tables it reads vs writes, and relationships)
+- `file-body-fallback` — generic multi-language summary for any other source file
+- `package-body` — synthesizes a package summary from the already-generated file summaries
+- `project-body` — (optional) synthesizes the short project `#### Overview` paragraph from the package leads
+
+For the `generate` goal the file-level prompt is selected per file by extension: the first field
+generation whose `` matches the file name wins; otherwise the first entry without a
+`` filter is the fallback. A single field generation with no filter (the historical
+shape) keeps working — it is simply the fallback for every file.
+
+Each summary begins with a one-sentence blockquote lead, followed by structured `####` sections.
+Prompts are optimized to avoid code blocks, formatter artifacts, and empty outputs, and to produce structured markdown.
+## Output Structure
+```
+src/site/ai/
+└── main/
+ └── java/
+ └── com/
+ └── example/
+ ├── MyClass.java.ai.md
+ ├── AnotherClass.java.ai.md
+ └── package.ai.md
+```
+## Design Principles
+- Deterministic metadata (hash-based change detection)
+- Separation of concerns (header = metadata, body = summary)
+- AI-friendly structure (predictable and hierarchical)
+- Local-first (no external APIs required)
+## Known Limitations
+- Model output may require normalization (handled in code)
+- Large models increase runtime
+- Output quality depends on chosen model
+
+## TODO
+- **Expand PIT mutation-testing scope.** `srcmorph/pom.xml`'s `` lists an explicit subset of classes verified at 100% mutation parity; this plugin module has no PIT gate of its own yet. Generic PIT setup and invocation: see the [PIT policy](../../workspace/policies/pit-mutation-testing.md).
+## Recommended Models
+Based on an 8-model × 2-prompt benchmark run against this codebase — full results, per-model
+pros/cons, a source-faithfulness deep-dive, and reproduction steps in
+[docs/ai-index-benchmark](../docs/ai-index-benchmark/COMPARISON.md):
+
+- **`gpt-oss-20B-mxfp4` — the production default** (switch with `-Dai.model=`). The native MXFP4
+ quant at a 96K window; it inherits the benchmark's accuracy lead (gpt-oss-20b was most *accurate* per
+ file, won 5/6 in the per-file matrix — measured on the `c96k`/UD-Q4_K_XL quant, but E5 shows quant
+ choice is within noise so the native MXFP4 is the better-quality swap), run at `reasoningEffort=low`
+ and a 96K window so it covers files up to ~250 KB untrimmed. Slowest of the set (~2× the 30B) — the
+ accepted cost for accuracy. See the preset/timing details below.
+- **Qwen3-Coder-30B-A3B-Instruct** — throughput alternative (and best of the non-reasoning models for
+ large Java files): most complete/faithful of the fast models, code-specialized, Apache-2.0,
+ ~3.3B-active MoE, 262K context. Pick it when throughput beats the last points of fidelity.
+- **Granite-4.0-H-Tiny** — fastest on CPU (~4×, flat-KV hybrid, Apache-2.0); best for very large
+ or many files when throughput beats the last points of fidelity.
+- **Seed-Coder-8B-Instruct** — clean, permissive (MIT) small dense coder.
+- Avoid `Qwen3.5-4B` (thinking tax, no quality gain).
+
+### gpt-oss-20b presets, large files, and timing
+
+`gpt-oss` is a *reasoning* model whose analysis tokens share the output budget, so use
+`reasoningEffort=low` for code summaries (best quality here) and size the budget to the file. The pom
+ships three ready presets, tiered by the largest file you must cover — full rationale and measurements
+in [COMPARISON.md §11](../docs/ai-index-benchmark/COMPARISON.md):
+
+| Preset | context | covers up to | ~ time (CPU) |
+|---|---|---|---|
+| `gpt-oss-20B-c16k` | 16K | ~40 KB | ~1–2 min |
+| `gpt-oss-20B-c48k` | 48K | ~125 KB | ~25 min @ 100 KB |
+| **`gpt-oss-20B-c96k` (default)** | 96K | ~260 KB | ~80 min @ 250 KB |
+
+- **`c96k` is the default:** a measured A/B shows a wider context window costs only RAM, not per-file
+ time, so it covers every file up to ~250 KB with no trimming while small files stay just as fast.
+ Downshift only to save RAM. Hard ceiling is the 128K window (~480–500 KB of code).
+- **Timing is quadratic, not linear:** prefill ≈ `24.4·n + 0.000674·n²` ms (n = prompt tokens),
+ because attention is O(n) per token — which is why throughput drops as files grow. The plugin logs
+ this estimate per file.
+
+## GPU acceleration (opt-in)
+The default native is CPU (Ninja build, bundled in the main `net.ladenthin:llama` jar). On an NVIDIA
+RTX 3070 a CUDA build measured **~4.5× the CPU decode speed**; Vulkan also works (AMD + NVIDIA) but pays
+a one-time shader-compilation cost on the first run. OpenCL is intentionally not offered (llama.cpp's
+OpenCL backend does not support NVIDIA GPUs).
+
+How the native is found: `net.ladenthin.llama.loader.LlamaLoader` tries `net.ladenthin.llama.lib.path`,
+then `java.library.path`, then **extracts the native bundled in whatever `net.ladenthin:llama` jar is on
+the classpath**. So there are two ways to enable a GPU when running the *published* plugin in your own
+build.
+
+**Recommended — add the GPU classifier to the plugin's own classpath.** Declare the matching
+`net.ladenthin:llama` classifier as a dependency of the plugin in your POM; the loader then extracts the
+GPU `jllama.dll` from it — no library path to manage:
+
+```xml
+
+ net.ladenthin
+ srcmorph-maven-plugin
+ ...
+
+
+ net.ladenthin
+ llama
+ 5.0.6
+ cuda13-windows-x86-64
+
+
+
+```
+
+```
+mvn srcmorph:generate -Dai.gpuLayers=20 # + the GPU runtime on PATH (see below)
+```
+
+**Alternative — runtime library override (no POM change).** Point `net.ladenthin.llama.lib.path` at a
+folder holding the GPU `jllama.dll` (extracted once from the classifier jar); it is tried before the
+bundled native:
+
+```
+mvn srcmorph:generate -Dnet.ladenthin.llama.lib.path=C:\path\to\gpu-native -Dai.gpuLayers=20
+```
+
+In both cases:
+
+- **CUDA** needs a matching CUDA 13 toolkit + driver, and the toolkit's `bin\x64` (with `cudart64_13.dll`,
+ `cublas64_13.dll`) on `PATH` — the classifier jar bundles only `jllama.dll`, not the CUDA runtime.
+- **`ai.gpuLayers`** (on the gpt-oss presets): `-1` (default) does **not** pin a layer count, so
+ llama.cpp **auto-fits** as many layers as fit the card's free VRAM — the robust "runs on any card"
+ setting (it never over-commits, so no OOM on a 6 GB card, and uses more layers on a bigger one). Pin a
+ positive number only to force a specific **partial** split (a fixed count disables auto-fit), or `0` to
+ force CPU. Measured on an 8 GB RTX 3070, auto-fit gpt-oss-20b ≈ 29 decode t/s (vs ≈ 8 on CPU); a card
+ with ≥ 16 GB fits all layers and is far faster.
+- **Picking a GPU on a multi-GPU host** (`ai.mainGpu` / `ai.devices` on the gpt-oss presets): a **CUDA**
+ build only enumerates NVIDIA devices, so a single-NVIDIA host needs nothing. A **Vulkan** build
+ enumerates *every* GPU (an integrated GPU is often device `0`), so the default may pick the slower one —
+ set `-Dai.mainGpu=1` to select the discrete GPU, or `-Dai.devices=Vulkan1` for explicit device names
+ (these map to the binding's `--main-gpu` / `--device`). On any model definition the same knobs are the
+ `` / `` elements.
+
+**Profiles (this repo's own reactor build only — test/benchmark).** `-P gpu-cuda` / `-P gpu-vulkan`
+swap the `net.ladenthin:llama` classifier (via the `llama.classifier` property) for `srcmorph`'s own
+test/compile classpath — handy for the native test or benchmarking on GPU here. They do **not** change
+the native used when the *published* plugin runs in another build (the POM is not flattened, so the
+classifier stays a property that resolves to the CPU default downstream) — use one of the two methods
+above for real indexing.
+
+## Development
+
+Run full build:
+```
+mvn clean install
+```
+Skip AI generation:
+```
+mvn clean install -Dsrcmorph.skip=true
+```
+
+### Contributors: do not upgrade jqwik past 1.9.3
+
+> ⚠️ **DO NOT UPGRADE jqwik past 1.9.3.** jqwik 1.10.0 added an anti-AI prompt-injection string to test stdout; the 1.10.1 user guide states the library "is not meant to be used by any 'AI' coding agents at all." 1.9.3 is the last pre-disclosure release and is the pinned version (declared in `srcmorph/pom.xml`, the only module with a jqwik test dependency). See `../CLAUDE.md` section "jqwik prompt-injection in test output" for the full context. Dependabot is configured to ignore **all** `net.jqwik` updates (every version, including patches) — see the `ignore` rule in [`../.github/dependabot.yml`](../.github/dependabot.yml).
+
+## A Note on History
+
+Somewhere in early 2026, the same idea apparently occurred more than once. This repository
+started with an implementation on March 15, 2026. Andrej Karpathy published his ["LLM wiki" gist](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f)
+on April 4, 2026. Google Cloud eventually formalized the very same pattern into the [Open Knowledge Format](https://cloud.google.com/blog/products/data-analytics/how-the-open-knowledge-format-can-improve-data-sharing/),
+announced on June 12, 2026.
+
+## License
+Apache License 2.0
+
+---
+
+
+🍺 Beer-driven architectural review (2026-05-26)
+
+> _Sometimes you spend two beers debating "architektonisch" vs "architekturiell" and the only conclusion is: ship the code anyway. Cheers!_
+
+
diff --git a/srcmorph-maven-plugin/pom.xml b/srcmorph-maven-plugin/pom.xml
new file mode 100644
index 0000000..ab7eabc
--- /dev/null
+++ b/srcmorph-maven-plugin/pom.xml
@@ -0,0 +1,2265 @@
+
+
+
+ 4.0.0
+
+
+ net.ladenthin
+ srcmorph-parent
+ 1.1.0
+ ../pom.xml
+
+
+ srcmorph-maven-plugin
+ maven-plugin
+
+ srcmorph-maven-plugin
+ Free Maven plugin for hierarchical AI-readable indexing and summarization of source code projects using llama.cpp-compatible local models. Formerly published as net.ladenthin:llamacpp-ai-index-maven-plugin (see the relocation stub of that name).
+
+
+ bernardladenthin
+ 8
+ 21
+ UTF-8
+ 3.15.2
+ 3.9.16
+
+
+ gpt-oss-20B-mxfp4
+
+ -1
+
+ -1
+
+
+ 6.1.2
+ 3.0
+ 1.37
+ 0.16
+ 1.2.28
+ 2.50.0
+ 0.13.7
+ 1.0.0
+ 4.2.1
+
+ 1.4.2
+ 4.10.2.0
+ 1.18.46
+ 7.7.4
+ 1.14.0
+ 3.8.0
+ 2.94.0
+
+ ${project.basedir}/src/site/ai
+
+
+ true
+
+
+
+ 2026-07-15T20:07:58Z
+
+
+
+
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+ provided
+
+
+ org.jspecify
+ jspecify
+ ${jspecify.version}
+
+
+ org.checkerframework
+ checker-qual
+ ${checker.version}
+
+
+
+ net.ladenthin
+ srcmorph
+ ${project.version}
+
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+ org.apache.maven
+ maven-plugin-api
+ ${maven.version}
+ provided
+
+
+
+ org.apache.maven.plugin-tools
+ maven-plugin-annotations
+ ${maven.plugin.tools.version}
+ provided
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.version}
+ test
+
+
+
+ org.hamcrest
+ hamcrest
+ ${hamcrest.version}
+ test
+
+
+
+ com.tngtech.archunit
+ archunit-junit5
+ ${archunit.version}
+ test
+
+
+ org.openjdk.jmh
+ jmh-core
+ ${jmh.version}
+ test
+
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ ${jmh.version}
+ test
+
+
+ org.openjdk.jcstress
+ jcstress-core
+ ${jcstress.version}
+ test
+
+
+
+ com.vmlens
+ api
+ ${vmlens.version}
+ test
+
+
+
+
+
+
+
+ com.diffplug.spotless
+ spotless-maven-plugin
+ ${spotless.version}
+
+
+ com.github.spotbugs
+ spotbugs-maven-plugin
+ ${spotbugs.version}
+
+
+ com.vmlens
+ vmlens-maven-plugin
+ ${vmlens.version}
+
+
+ io.github.git-commit-id
+ git-commit-id-maven-plugin
+ 10.0.0
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.15.0
+
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+ 3.6.3
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.2.8
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.5.0
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.12.0
+
+
+ org.apache.maven.plugins
+ maven-plugin-plugin
+ ${maven.plugin.tools.version}
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.4.0
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.6
+
+
+ @{argLine} -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=. -XX:ErrorFile=hs_err_pid%p.log
+
+
+ **/VmlensInterleavingSmokeTest.java
+
+
+ false
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.6.3
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.15
+
+
+ org.pitest
+ pitest-maven
+ 1.25.7
+
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.11.0
+
+
+
+
+
+ io.github.git-commit-id
+ git-commit-id-maven-plugin
+
+
+ get-git-properties
+
+ revision
+
+ initialize
+
+
+
+ yyyy-MM-dd'T'HH:mm:ss'Z'
+ UTC
+ false
+ false
+
+
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+
+
+ enforce-maven
+
+ enforce
+
+
+
+
+ [3.6.3,)
+
+
+ [1.8,)
+
+
+
+
+
+ commons-logging:commons-logging
+
+ log4j:log4j
+
+ org.hamcrest:hamcrest-core
+ org.hamcrest:hamcrest-library
+ org.hamcrest:hamcrest-all
+
+ junit:junit
+ junit:junit-dep
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ ${maven.compiler.release}
+ ${maven.compiler.testRelease}
+ true
+ true
+
+
+ -Xlint:all,-serial,-options,-classfile,-processing
+ -Werror
+
+ -processor
+ lombok.launch.AnnotationProcessorHider$AnnotationProcessor,lombok.launch.AnnotationProcessorHider$ClaimingProcessor,org.checkerframework.checker.nullness.NullnessChecker
+
+ -AskipDefs=^net\.ladenthin\.srcmorph_maven_plugin\..*$
+ -XDaddTypeAnnotationsToSymbol=true
+ -XDcompilePolicy=simple
+ --should-stop=ifError=FLOW
+
+ -Xplugin:ErrorProne -XepExcludedPaths:.*/generated-sources/.* -Xep:NullAway:ERROR -XepOpt:NullAway:OnlyNullMarked=true -XepOpt:NullAway:ExcludedFieldAnnotations=org.apache.maven.plugins.annotations.Parameter,org.apache.maven.plugins.annotations.Component -XepOpt:NullAway:JSpecifyMode=true -XepOpt:NullAway:CheckOptionalEmptiness=true -XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true -XepOpt:NullAway:AcknowledgeAndroidRecent=true -XepOpt:NullAway:AssertsEnabled=true -Xep:BoxedPrimitiveEquality:ERROR -Xep:EqualsHashCode:ERROR -Xep:EqualsIncompatibleType:ERROR -Xep:IdentityBinaryExpression:ERROR -Xep:SelfAssignment:ERROR -Xep:SelfComparison:ERROR -Xep:SelfEquals:ERROR -Xep:DeadException:ERROR -Xep:FormatString:ERROR -Xep:InvalidPatternSyntax:ERROR -Xep:OptionalEquality:ERROR -Xep:ImpossibleNullComparison:ERROR
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+ com.google.errorprone
+ error_prone_core
+ ${errorprone.version}
+
+
+ com.uber.nullaway
+ nullaway
+ ${nullaway.version}
+
+
+ org.checkerframework
+ checker
+ ${checker.version}
+
+
+
+
+
+ default-compile
+
+
+
+ module-info.java
+
+
+
+
+ module-info-compile
+ compile
+
+ compile
+
+
+
+ 9
+
+ module-info.java
+
+
+
+
+
+
+ default-testCompile
+
+
+ false
+
+ -XDaddTypeAnnotationsToSymbol=true
+ -XDcompilePolicy=simple
+ --should-stop=ifError=FLOW
+ -Xplugin:ErrorProne -Xep:NullAway:OFF -Xep:GuardedBy:OFF -Xep:IdentityBinaryExpression:OFF
+
+
+
+ org.openjdk.jcstress
+ jcstress-core
+ ${jcstress.version}
+
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ ${jmh.version}
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+ attach-sources
+ verify
+
+ jar-no-fork
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+ ${maven.compiler.release}
+ true
+ true
+ all
+
+ **/HelpMojo.java
+
+
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-plugin-plugin
+
+ srcmorph
+
+
+
+ default-descriptor
+
+ descriptor
+
+
+
+ help-goal
+
+ helpmojo
+
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+
+
+ prepare-agent
+
+ prepare-agent
+
+
+
+ report
+ test
+
+ report
+
+
+
+
+
+ com.diffplug.spotless
+ spotless-maven-plugin
+
+
+
+ src/main/java/**/*.java
+ src/test/java/**/*.java
+
+
+ ${palantir-java-format.version}
+
+
+
+
+
+
+
+
+ spotless-check
+ verify
+
+ check
+
+
+
+
+
+ com.github.spotbugs
+ spotbugs-maven-plugin
+
+ Max
+ Low
+ true
+ false
+ spotbugs-exclude.xml
+
+
+ com.mebigfatguy.fb-contrib
+ fb-contrib
+ ${fb-contrib.version}
+
+
+ com.h3xstream.findsecbugs
+ findsecbugs-plugin
+ ${findsecbugs.version}
+
+
+
+
+
+ spotbugs-check
+ verify
+
+ check
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+
+ org.openjdk.jmh.Main
+ test
+
+
+
+
+
+
+
+
+
+
+ srcmorph-selftest
+
+
+
+ ${project.groupId}
+ ${project.artifactId}
+ ${project.version}
+
+
+ ${ai.index.output.directory}
+
+
+ ../srcmorph/src/main/java/net/ladenthin/srcmorph/config
+ ../srcmorph/src/main/java/net/ladenthin/srcmorph/provider
+
+
+
+
+ **/package-info.java
+ **/module-info.java
+
+
+ llamacpp-jni
+
+
+
+
+
+
+ Mistral-7B-Instruct-v0.3-Q4_K_M-C16k
+ X:/Modelle/Mistral-7B-Instruct-v0.3-Q4_K_M.gguf
+ 16384
+ 2048
+ 1.0
+ 8
+ 4
+ true
+ 1.1
+
+
+
+ Meta-Llama-3.1-8B-Instruct-Q5_K_M-C32k
+ X:/Modelle/Meta-Llama-3.1-8B-Instruct-Q5_K_M.gguf
+ 32768
+ 1536
+ 1.0
+ 8
+ 4
+ true
+ 1.1
+
+
+
+ Codestral-22B-v0.1-Q4_K_M-C32k
+ X:/Modelle/Codestral-22B-v0.1-Q4_K_M.gguf
+ 32768
+ 1536
+ 1.0
+ 8
+ 4
+ true
+ 1.1
+
+
+
+ Codestral-22B-v0.1-Q6_K-C32k
+ X:/Modelle/Codestral-22B-v0.1-Q6_K.gguf
+ 32768
+ 1536
+ 1.0
+ 8
+ 4
+ true
+ 1.1
+
+
+
+ Qwen2.5-Coder-7B-Instruct-Q5_K_M-C16k
+ X:/Modelle/qwen2.5-coder-7b-instruct-q5_k_m.gguf
+ 16384
+ 2048
+ 1.0
+ 8
+ 4
+ true
+ 1.1
+
+
+
+ Ministral-3-8B-Instruct-2512-Q4_K_M-C32k
+ X:/Modelle/Ministral-3-8B-Instruct-2512-Q4_K_M.gguf
+ 32768
+ 2048
+ 0.1
+ 8
+ 4
+ true
+ 1.1
+
+
+
+ Ministral-3-14B-Instruct-2512-Q4_K_M-C32k
+ X:/Modelle/Ministral-3-14B-Instruct-2512-Q4_K_M.gguf
+ 32768
+ 2048
+ 0.1
+ 8
+ 4
+ true
+ 1.1
+
+
+
+ Ministral-3-8B-Instruct-2512-Q5_K_M-C32k
+ X:/Modelle/Ministral-3-8B-Instruct-2512-Q5_K_M.gguf
+ 32768
+ 2048
+ 0.1
+ 8
+ 4
+ true
+ 1.1
+
+
+
+ Ministral-3-14B-Instruct-2512-Q5_K_M-C32k
+ X:/Modelle/Ministral-3-14B-Instruct-2512-Q5_K_M.gguf
+ 32768
+ 2048
+ 0.1
+ 8
+ 4
+ true
+ 1.1
+
+
+
+ Gemma-4-26B-A4B-it-UD-Q4_K_M-C32k
+ X:/Modelle/gemma-4-26B-A4B-it-UD-Q4_K_M.gguf
+ 32768
+ 8192
+ 1.0
+ 8
+ 4
+ true
+ 0.95
+ 64
+ 1.1
+ false
+
+ <end_of_turn>
+
+
+
+
+ Gemma-4-12B-it-Q4_K_M-C32k
+ X:/Modelle/gemma-4-12B-it-Q4_K_M.gguf
+ 32768
+ 8192
+ 1.0
+ 8
+ 4
+ true
+ 0.95
+ 64
+ 1.1
+ false
+
+ <end_of_turn>
+
+
+
+
+ Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q4_K_M-C32k
+ X:/Modelle/Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf
+ 32768
+ 8192
+ 1.0
+ 8
+ 4
+ true
+ 0.95
+ 64
+ 1.1
+ false
+
+ <end_of_turn>
+
+
+
+
+ Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q6_K_P-C32k
+ X:/Modelle/Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q6_K_P.gguf
+ 32768
+ 8192
+ 1.0
+ 8
+ 4
+ true
+ 0.95
+ 64
+ 1.1
+ false
+
+ <end_of_turn>
+
+
+
+
+ Qwen3-Coder-30B-A3B-Instruct-UD-Q4_K_XL-C64k
+ X:/Modelle/Qwen3-Coder-30B-A3B-Instruct-UD-Q4_K_XL.gguf
+
+ 16384
+ 1536
+ 0.7
+ 8
+ 4
+ true
+ 0.8
+ 20
+ 1.05
+ ${ai.cachePrompt}
+
+
+
+ Qwen2.5-Coder-14B-Instruct-Q4_K_M-C32k
+ X:/Modelle/Qwen2.5-Coder-14B-Instruct-Q4_K_M.gguf
+ 32768
+ 2048
+ 0.7
+ 8
+ 4
+ true
+ 0.8
+ 20
+ 1.05
+
+
+
+ Qwen3-4B-Instruct-2507-Q5_K_M-C32k
+ X:/Modelle/Qwen3-4B-Instruct-2507-Q5_K_M.gguf
+ 32768
+ 2048
+ 0.7
+ 8
+ 4
+ true
+ 0.8
+ 20
+ 1.05
+
+
+
+ Granite-4.0-H-Tiny-Q4_K_M-C128k
+ X:/Modelle/ibm-granite_granite-4.0-h-tiny-Q4_K_M.gguf
+ 131072
+ 2048
+ 0.2
+ 8
+ 4
+ true
+ 1.1
+
+
+
+ Granite-4.0-H-Small-Q4_K_M-C128k
+ X:/Modelle/ibm-granite_granite-4.0-h-small-Q4_K_M.gguf
+ 131072
+ 2048
+ 0.2
+ 8
+ 4
+ true
+ 1.1
+
+
+
+ EXP-Qwen3.5-4B
+ X:/Modelle/Qwen3.5-4B-Q4_K_M.gguf
+ 16384
+ 1536
+ 0.7
+ 8
+ 4
+ true
+ 0.8
+ 20
+ 1.05
+ false
+
+
+ EXP-Qwen2.5-Coder-7B
+ X:/Modelle/qwen2.5-coder-7b-instruct-q5_k_m.gguf
+ 16384
+ 1536
+ 0.7
+ 8
+ 4
+ true
+ 0.8
+ 20
+ 1.05
+
+
+
+ gpt-oss-20B-c16k
+ X:/Modelle/gpt-oss-20b-UD-Q4_K_XL.gguf
+ 16384
+ ${ai.gpuLayers}
+ ${ai.mainGpu}
+ ${ai.devices}
+ 2048
+ 0.7
+ 8
+ 3
+ true
+ 1.0
+ 0
+ 0.05
+ 1.0
+ low
+
+
+ gpt-oss-20B-c48k
+ X:/Modelle/gpt-oss-20b-UD-Q4_K_XL.gguf
+ 49152
+ ${ai.gpuLayers}
+ ${ai.mainGpu}
+ ${ai.devices}
+ 4096
+ 0.7
+ 8
+ 3
+ true
+ 1.0
+ 0
+ 0.05
+ 1.0
+ low
+
+
+ gpt-oss-20B-c96k
+ X:/Modelle/gpt-oss-20b-UD-Q4_K_XL.gguf
+ 98304
+ ${ai.gpuLayers}
+ ${ai.mainGpu}
+ ${ai.devices}
+ 6144
+ 0.7
+ 8
+ 3
+ true
+ 1.0
+ 0
+ 0.05
+ 1.0
+ low
+
+
+
+
+ gpt-oss-20B-mxfp4
+ X:/Modelle/gpt-oss-20b-mxfp4.gguf
+ 98304
+ ${ai.gpuLayers}
+ ${ai.mainGpu}
+ ${ai.devices}
+ 6144
+ 0.7
+ 8
+ 3
+ true
+ 1.0
+ 0
+ 0.05
+ 1.0
+ low
+
+
+ gpt-oss-20B-Q4_K_M
+ X:/Modelle/gpt-oss-20b-Q4_K_M.gguf
+ 98304
+ ${ai.gpuLayers}
+ ${ai.mainGpu}
+ ${ai.devices}
+ 6144
+ 0.7
+ 8
+ 3
+ true
+ 1.0
+ 0
+ 0.05
+ 1.0
+ low
+
+
+ gpt-oss-20B-Q5_K_M
+ X:/Modelle/gpt-oss-20b-Q5_K_M.gguf
+ 98304
+ ${ai.gpuLayers}
+ ${ai.mainGpu}
+ ${ai.devices}
+ 6144
+ 0.7
+ 8
+ 3
+ true
+ 1.0
+ 0
+ 0.05
+ 1.0
+ low
+
+
+ gpt-oss-20B-Q8_0
+ X:/Modelle/gpt-oss-20b-Q8_0.gguf
+ 98304
+ ${ai.gpuLayers}
+ ${ai.mainGpu}
+ ${ai.devices}
+ 6144
+ 0.7
+ 8
+ 3
+ true
+ 1.0
+ 0
+ 0.05
+ 1.0
+ low
+
+
+ gpt-oss-20B-F16
+ X:/Modelle/gpt-oss-20b-F16.gguf
+ 98304
+ ${ai.gpuLayers}
+ ${ai.mainGpu}
+ ${ai.devices}
+ 6144
+ 0.7
+ 8
+ 3
+ true
+ 1.0
+ 0
+ 0.05
+ 1.0
+ low
+
+
+
+
+ granite-4.0-h-tiny-bigwindow
+ X:/Modelle/granite-4.0-h-tiny-Q4_K_M.gguf
+ 393216
+ ${ai.gpuLayers}
+ ${ai.mainGpu}
+ ${ai.devices}
+ 4096
+ 0.2
+ 8
+ 3
+ true
+ 1.0
+ 0
+ 0.05
+ 1.0
+
+
+
+
+
+ granite-4.0-h-1b-fastchunk
+ X:/Modelle/granite-4.0-h-1b-Q4_K_M.gguf
+ 16384
+ ${ai.gpuLayers}
+ ${ai.mainGpu}
+ ${ai.devices}
+ 1536
+ 0.2
+ 8
+ 3
+ true
+ 1.0
+ 0
+ 0.05
+ 1.0
+
+
+
+
+ EXP-Qwen3-Coder-30B
+ X:/Modelle/Qwen3-Coder-30B-A3B-Instruct-UD-Q4_K_XL.gguf
+ 16384
+ 1536
+ 0.7
+ 8
+ 4
+ true
+ 0.8
+ 20
+ 1.05
+
+
+
+ EXP-DeepSeek-Coder-V2-Lite
+ X:/Modelle/DeepSeek-Coder-V2-Lite-Instruct-Q4_K_M.gguf
+ 16384
+ 1536
+ 0.3
+ 8
+ 4
+ true
+ 0.9
+ 40
+ 1.05
+
+
+ EXP-Seed-Coder-8B
+ X:/Modelle/Seed-Coder-8B-Instruct-Q4_K_M.gguf
+ 16384
+ 1536
+ 0.3
+ 8
+ 4
+ true
+ 0.9
+ 40
+ 1.05
+
+
+ EXP-Granite-4.0-H-Tiny
+ X:/Modelle/granite-4.0-h-tiny-Q4_K_M.gguf
+ 16384
+ 1536
+ 0.0
+ 8
+ 4
+ true
+ 1.0
+ 0
+ 1.0
+
+
+
+ EXP-Granite-4.0-H-Small
+ X:/Modelle/granite-4.0-h-small-UD-Q4_K_XL.gguf
+ 16384
+ 1536
+ 0.0
+ 8
+ 4
+ true
+ 1.0
+ 0
+ 1.0
+
+
+ EXP-Qwen3-4B-Instruct-2507
+ X:/Modelle/Qwen3-4B-Instruct-2507-Q4_K_M.gguf
+ 16384
+ 1536
+ 0.7
+ 8
+ 4
+ true
+ 0.8
+ 20
+ 1.05
+
+
+
+
+
+
+ file-body-java
+
+
+Then output these sections as markdown. Omit any section that does not apply. Use '####' for section labels. Never use '###' or higher.
+
+#### Purpose
+One or two bullets: what the type is for.
+
+#### Type
+Kind (class / interface / enum / record / annotation) and modifiers (abstract, final, sealed). extends X; implements Y; key generics and type bounds. Notable annotations (@Mojo, @Entity, Lombok, etc.).
+
+#### Input
+What comes in: constructor and method parameters, injected dependencies, consumed fields, read resources (files, buffers, streams, config).
+
+#### Output
+What comes out: return types, produced state, mutated fields, written resources, side effects.
+
+#### Core logic
+The essential steps / algorithm / responsibility as bullets. This is the most important section.
+
+#### Public API
+Each public/protected member as `name(params) -> returnType` + a clause (max 6 words) on its purpose. For a FAMILY of near-identical members (e.g. `fooN`), do not list them all: in the analysis channel enumerate and count them one per line, then in the final answer emit only the exact total and index range, e.g. `foo1..fooN (N total)`.
+
+#### Dependencies
+Imports and referenced types, as searchable names.
+
+#### Exceptions / Errors
+Notable thrown/caught exceptions, null-handling, and error conditions.
+
+#### Concurrency
+Threading, synchronization, immutability, or thread-safety notes, if any.
+
+## Rules
+- Markdown bullets and short clauses, not prose paragraphs. Sentences allowed only where a bullet would lose meaning.
+- Weave class / type / domain names into the bullets so they are searchable.
+- No code fences, no ```markdown / ```java, no XML tags, no formatter comments.
+- COUNT EXACTLY. When you state a count of methods/fields/members, count them precisely from the source and recount before finalizing. Never round, approximate, or write "similar"/"~".
+- The filename heading is already rendered above your output. Start immediately with the blockquote lead. Never produce empty output.
+
+## Critical
+If the source contains "[EOF - source was truncated]", index ONLY what is literally present. Never infer, guess, or invent any unit, field, or behavior beyond the shown source.
+
+The file path and its full source are provided in the next (user) message: the path on the first
+line, then a blank line, then the source. Index only what is literally present there.
+ ]]>
+
+
+ file-body-sql
+
+
+Then output these sections as markdown. Omit any section that does not apply. Use '####' for section labels. Never use '###' or higher.
+
+#### Purpose
+One or two bullets: what this SQL object is for.
+
+#### Type
+SQL object kind: table / view / materialized view / procedure / function / trigger / index / sequence. Note the dialect if evident (PostgreSQL, MySQL, Oracle, T-SQL).
+
+#### Schema
+For tables/views: each column as `name type` plus constraints (PK, FK, NOT NULL, UNIQUE, DEFAULT, CHECK). Mark the primary key and any indexes.
+
+#### Reads
+Tables and columns this object READS from (FROM / JOIN / subquery sources, function inputs).
+
+#### Writes
+Tables and columns this object WRITES (INSERT / UPDATE / DELETE / MERGE targets, or the table a trigger fires on).
+
+#### Relationships
+Foreign keys and join relationships to other tables, as searchable `table.column -> table.column` pairs.
+
+#### Routine logic
+For procedures / functions / triggers: parameters, the essential steps, and the result or effect, as bullets.
+
+#### Dependencies
+Other tables, views, functions, or sequences referenced, as searchable names.
+
+## Rules
+- Markdown bullets and short clauses, not prose paragraphs.
+- Weave table / column / constraint / domain names into the bullets so they are searchable.
+- No code fences, no ```markdown / ```sql, no XML tags, no formatter comments.
+- The filename heading is already rendered above your output. Start immediately with the blockquote lead. Never produce empty output.
+
+## Critical
+If the source contains "[EOF - source was truncated]", index ONLY what is literally present. Never infer, guess, or invent any table, column, or relationship beyond the shown source.
+
+The file path and its full source are provided in the next (user) message: the path on the first
+line, then a blank line, then the source. Index only what is literally present there.
+ ]]>
+
+
+ file-body-fallback
+
+
+Then output these sections as markdown. Omit any section that does not apply. Use '####' for section labels. Never use '###' or higher.
+
+#### Purpose
+One or two bullets: what the file/unit is for.
+
+#### Type
+Kind (class / interface / enum / record / header / kernel / SQL object) and language. For OO code: extends X; implements Y; key generics. For C: header vs implementation, exported vs static. For SQL: table / view / procedure / function / trigger.
+
+#### Input
+What comes in: key parameters, consumed fields, read resources (files, buffers, streams, kernels, tables/columns, env/config).
+
+#### Output
+What comes out: return types, produced state, written resources/tables, side effects.
+
+#### Core logic
+The essential steps / algorithm / responsibility as bullets. This is the most important section.
+
+#### Public API
+Each public/exported unit as `name(params) -> returnType` + a clause (max 6 words) on its purpose. For SQL: procedures/functions with params and result.
+
+#### Dependencies
+Imports / includes / referenced modules or tables, as searchable names.
+
+#### Exceptions / Errors
+Notable thrown/handled exceptions or error conditions.
+
+## Rules
+- Markdown bullets and short clauses, not prose paragraphs. Sentences allowed only where a bullet would lose meaning.
+- Weave class / type / table / domain names into the bullets so they are searchable.
+- No code fences, no ```markdown / ```java / ```sql, no XML tags, no formatter comments.
+- The filename heading is already rendered above your output. Start immediately with the blockquote lead. Never produce empty output.
+
+## Critical
+If the source contains "[EOF - source was truncated]", index ONLY what is literally present. Never infer, guess, or invent any unit, field, table, or behavior beyond the shown source.
+
+The file path and its full source are provided in the next (user) message: the path on the first
+line, then a blank line, then the source. Index only what is literally present there.
+ ]]>
+
+
+ package-body
+
+
+Then output these sections as markdown. Omit any that do not apply. Use '####' for labels. Never '###' or higher.
+
+#### Purpose
+One or two bullets: the package's overall responsibility.
+
+#### Responsibilities
+The main functional groups inside the package, as bullets. Group files by role; do not list every file separately for large packages.
+
+#### Key units
+The central classes / interfaces / modules / SQL objects and their job, one clause each, with their searchable names.
+
+#### Data flow
+How inputs move through the package to outputs, if a clear pipeline or collaboration exists across the files.
+
+#### Dependencies
+Notable internal collaborations and external modules/packages/tables the package depends on.
+
+#### Cross-cutting
+Recurring patterns across files: shared base types/interfaces, common exception/error handling, threading/concurrency notes, configuration.
+
+## Rules
+- Markdown bullets and short clauses, not prose paragraphs.
+- Weave type / module / table / domain names in as searchable terms.
+- Base every statement only on the provided file summaries. Do NOT invent units, methods, tables, or behavior not present in them.
+- No code fences, no ```markdown, no XML tags, no formatter comments.
+- The package-name heading is already rendered above your output. Start immediately with the blockquote lead. Never empty.
+
+## Critical
+If the input contains "[EOF - source was truncated]", summarize ONLY what is present. Never infer or invent files or content beyond the provided summaries.
+
+The package path and its already-generated per-file summaries are provided in the next (user)
+message: the path on the first line, then a blank line, then the summaries to synthesize.
+ ]]>
+
+
+ project-body
+
+
+
+
+ file-body-java-v2
+
+
+Then output ONLY the sections below that carry real information. OMIT any section that would be empty, "none", or "not applicable" - do not emit a heading just to say nothing. Use '####' for section labels, never '###' or higher.
+
+TRIVIAL-FILE RULE: if the unit has no public behavior to describe (e.g. a marker/annotation, an enum that only lists constants, or a near-empty type), output ONLY the blockquote lead and a single '#### Purpose' line, and stop.
+
+#### Purpose
+One or two bullets: what the type is for.
+
+#### Type
+One line: kind (class/interface/enum/record/annotation) + modifiers; extends/implements; key generics; notable annotations (@Mojo, Lombok, etc.).
+
+#### Input
+What comes in: constructor/method parameters, injected dependencies, consumed fields, read resources.
+
+#### Output
+What comes out: return types, produced/mutated state, written resources, side effects.
+
+#### Core logic
+The essential steps/algorithm/responsibility as bullets. The most important section.
+
+#### Public API
+Each public/protected member as `name(params) -> returnType` + a <=6-word purpose clause. EXCEPTION: for plain data/config classes that are only getters/setters (e.g. Lombok beans), DO NOT list each accessor - write one line: "JavaBean getters/setters for: ".
+
+#### Dependencies
+Referenced types as searchable names. EXCLUDE this file's own type and java.lang.* .
+
+#### Exceptions / Errors
+Notable thrown/caught exceptions, null-handling, error conditions.
+
+#### Concurrency
+Threading, immutability, or thread-safety notes - only if the source actually shows them.
+
+## Rules
+- Markdown bullets and short clauses, not prose paragraphs.
+- Weave class/type/domain names into the bullets so they are searchable.
+- Describe ONLY what is literally in the source. NEVER invent units, fields, behavior, or concepts; in particular never mention keywords, tags, embeddings, search, or features the code does not contain.
+- No code fences, no ```markdown/```java, no XML tags, no formatter comments.
+- The filename heading is already rendered above your output. Start immediately with the blockquote lead. Never produce empty output.
+
+## Critical
+If the source contains "[EOF - source was truncated]", index ONLY what is literally present.
+
+The file path and its full source are provided in the next (user) message: the path on the first
+line, then a blank line, then the source. Index only what is literally present there.
+ ]]>
+
+
+ package-body-v2
+
+
+SINGLE-CHILD RULE: if the input describes only ONE child package/file and no other content, output ONLY the blockquote lead restating that child's responsibility, and stop - emit no sections.
+
+Otherwise output ONLY the sections below that carry real information. OMIT any that would be empty. Use '####' for labels, never '###' or higher.
+
+#### Purpose
+One or two bullets: the package's overall responsibility.
+
+#### Responsibilities
+The main functional groups inside the package, as bullets. Group files by role; do not list every file for large packages.
+
+#### Key units
+The central classes/interfaces/modules and their job, one clause each, with searchable names.
+
+#### Data flow
+How inputs move through the package to outputs, if a clear pipeline exists across the files.
+
+#### Dependencies
+Notable internal collaborations and external modules the package depends on. Focus on cross-unit/cross-package relationships, not raw JDK/Lombok imports.
+
+#### Cross-cutting
+Recurring patterns across files: shared base types, common error handling, concurrency, configuration.
+
+## Rules
+- Markdown bullets and short clauses, not prose paragraphs.
+- Weave type/module/domain names in as searchable terms.
+- Base every statement ONLY on the provided file summaries. NEVER invent units, methods, behavior, or concepts (in particular never mention keywords, tags, or embeddings) not present in them.
+- No code fences, no ```markdown, no XML tags, no formatter comments.
+- The package-name heading is already rendered above your output. Start immediately with the blockquote lead. Never empty.
+
+## Critical
+If the input contains "[EOF - source was truncated]", summarize ONLY what is present.
+
+The package path and its already-generated per-file summaries are provided in the next (user)
+message: the path on the first line, then a blank line, then the summaries to synthesize.
+ ]]>
+
+
+
+
+
+
+
+
+ ai-generate
+ generate-resources
+
+ generate
+
+
+
+
+ .java
+ .sql
+
+
+
+
+ java-facts
+
+ (?m)^\s*(?:public\s+|final\s+|abstract\s+|sealed\s+|non-sealed\s+|static\s+)*(?:class|interface|enum|record)\s+\w
+ \bpublic\b
+ \b(?:TODO|FIXME)\b
+ @Override\b
+
+ (?m)^[ \t]*(?:(?:public|private|protected|static|final|abstract|default|synchronized|native|strictfp)[ \t]+)*(?:<[^>]+>[ \t]*)?(?!(?:if|for|while|switch|catch|return|new|else|do|try|synchronized|assert|throw)\b)[A-Za-z_$][\w$.]*(?:<[^;{}=]*>)?(?:\[\])*[ \t]+([A-Za-z_$]\w*)[ \t]*\([^;{]*\)(?:[ \t]*throws[ \t][\w$., \t]+)?[ \t]*[{;]
+ (?m)^[ \t]*(?:(?:public|private|protected)[ \t]+)?([A-Z][A-Za-z0-9_$]*)[ \t]*\([^;{]*\)(?:[ \t]*throws[ \t][\w$., \t]+)?[ \t]*\{
+ (?m)^[ \t]*(?:(?:public|private|protected|static|transient|volatile|final)[ \t]+)*(?:public|private|protected|static|transient|volatile)[ \t]+(?:(?:public|private|protected|static|transient|volatile|final)[ \t]+)*(?!class\b|interface\b|enum\b|record\b|void\b|new\b)[A-Za-z_$][\w$.]*(?:<[^;{}=]*>)?(?:\[\])*[ \t]+([A-Za-z_$]\w*(?:[ \t]*,[ \t]*[A-Za-z_$]\w*)*)[ \t]*(?:=[^;]*)?;
+
+
+
+ sql-facts
+
+ (?im)^\s*INSERT\s+INTO\b
+ (?im)^\s*CREATE\s+TABLE\b
+ (?im)^\s*CREATE\s+(?:OR\s+REPLACE\s+)?VIEW\b
+ (?im)^\s*CREATE\s+(?:UNIQUE\s+)?INDEX\b
+ (?im)^\s*CREATE\s+(?:OR\s+REPLACE\s+)?(?:FUNCTION|PROCEDURE)\b
+
+
+
+
+
+
+ huge-java
+ file-body-java
+ granite-4.0-h-1b-fastchunk
+ 200
+
+ mapReduce
+ 6
+ java-facts
+
+
+
+
+ .java
+
+
+ 1000000
+
+
+
+
+
+
+ huge-sql
+ file-body-sql
+ granite-4.0-h-1b-fastchunk
+ 200
+ mapReduce
+ 6
+
+ sql-facts
+
+
+
+
+ .sql
+
+
+ 1000000
+
+
+
+
+
+
+ big-window-java
+ file-body-java
+ granite-4.0-h-tiny-bigwindow
+ 100
+
+
+
+
+
+ .java
+
+
+ 275000
+
+
+
+
+
+
+ big-window-sql
+ file-body-sql
+ granite-4.0-h-tiny-bigwindow
+ 100
+
+
+
+
+ .sql
+
+
+ 275000
+
+
+
+
+
+
+ java
+ file-body-java
+ ${ai.model}
+
+ java-facts
+
+ .java
+
+
+
+ sql
+ file-body-sql
+ ${ai.model}
+ sql-facts
+
+ .sql
+
+
+
+
+ fallback
+ file-body-fallback
+ ${ai.model}
+ true
+
+
+
+
+
+
+ ai-aggregate-packages
+ process-resources
+
+ aggregate-packages
+
+
+
+
+ package-body
+ ${ai.model}
+
+
+
+
+
+
+
+ ai-aggregate-project
+ prepare-package
+
+ aggregate-project
+
+
+
+
+ project-body
+ ${ai.model}
+
+
+
+
+
+
+
+
+
+
+
+ vmlens
+
+
+
+ com.vmlens
+ vmlens-maven-plugin
+
+
+
+ **/VmlensInterleavingSmokeTest.java
+
+
+
+
+ vmlens-test
+
+ test
+
+
+
+
+
+
+
+
+ jcstress
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+
+
+ jcstress
+ test
+ exec
+
+ ${java.home}/bin/java
+ test
+
+ -classpath
+
+ org.openjdk.jcstress.Main
+ -v
+ -m
+ default
+
+
+
+
+
+
+
+
+
+
diff --git a/srcmorph-maven-plugin/spotbugs-exclude.xml b/srcmorph-maven-plugin/spotbugs-exclude.xml
new file mode 100644
index 0000000..d000788
--- /dev/null
+++ b/srcmorph-maven-plugin/spotbugs-exclude.xml
@@ -0,0 +1,175 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/module-info.java b/srcmorph-maven-plugin/src/main/java/module-info.java
similarity index 66%
rename from llamacpp-ai-index-maven-plugin/src/main/java/module-info.java
rename to srcmorph-maven-plugin/src/main/java/module-info.java
index 130d25c..94f795b 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/module-info.java
+++ b/srcmorph-maven-plugin/src/main/java/module-info.java
@@ -3,11 +3,13 @@
// SPDX-License-Identifier: Apache-2.0
/**
- * JPMS module descriptor for the llamacpp-ai-index Maven plugin.
+ * JPMS module descriptor for the srcmorph Maven plugin.
*
- *
The module exports the single public package {@code net.ladenthin.maven.llamacpp.aiindex}
- * that holds the configuration POJOs and Mojo entry points. The auto-generated
- * {@code net.ladenthin.llamacpp_ai_index_maven_plugin.HelpMojo} package is deliberately
+ *
The module exports the single public package {@code net.ladenthin.maven.srcmorph.mojo}
+ * that holds the Mojo entry points. The configuration POJOs, document codec, indexers, prompt
+ * support, and AI generation provider abstraction now live in the {@code net.ladenthin.srcmorph}
+ * module (the extracted core library), which this module {@code requires}. The auto-generated
+ * {@code net.ladenthin.srcmorph_maven_plugin.HelpMojo} package is deliberately
* not exported: Maven loads the plugin via its own classpath classloader and
* never consults the module descriptor for Mojo discovery, so the {@code HelpMojo} class
* remains reachable as an ordinary classpath type.
@@ -25,18 +27,17 @@
* descriptor; JSpecify annotations carry {@code RetentionPolicy.CLASS} so module-path
* consumers never need jspecify on their runtime path. Annotations from
* {@code maven-plugin-annotations} and Checker Framework qualifiers are likewise
- * compile-time only. The classes imported from {@code maven-plugin-api} and
- * {@code net.ladenthin:llama} are referenced from Mojo source files only; javac in the
- * separate {@code module-info-compile} execution compiles {@code module-info.java} in
- * isolation and therefore does not need their module names. Maven, in turn, loads the
- * plugin classpath-only and ignores the descriptor at runtime.
+ * compile-time only. The classes imported from {@code maven-plugin-api} are referenced from
+ * Mojo source files only; javac in the separate {@code module-info-compile} execution compiles
+ * {@code module-info.java} in isolation and therefore does not need their module names. Maven,
+ * in turn, loads the plugin classpath-only and ignores the descriptor at runtime.
*
*
This descriptor compiles at {@code --release 9}; the rest of the source compiles
* at {@code --release 8}. Java 8 runtimes silently ignore {@code module-info.class} at
* the JAR root.
*/
@org.jspecify.annotations.NullMarked
-module net.ladenthin.maven.llamacpp.aiindex {
+module net.ladenthin.maven.srcmorph {
requires static org.jspecify;
// Lombok is `provided` scope: only used at compile time to generate equals/hashCode/toString.
@@ -44,11 +45,8 @@
// the @lombok.Generated annotation carried on generated members has CLASS retention.
requires static lombok;
- exports net.ladenthin.maven.llamacpp.aiindex.config;
- exports net.ladenthin.maven.llamacpp.aiindex.document;
- exports net.ladenthin.maven.llamacpp.aiindex.indexer;
- exports net.ladenthin.maven.llamacpp.aiindex.mojo;
- exports net.ladenthin.maven.llamacpp.aiindex.prompt;
- exports net.ladenthin.maven.llamacpp.aiindex.provider;
- exports net.ladenthin.maven.llamacpp.aiindex.support;
+ // The extracted core library: config/document/indexer/prompt/provider/support now live there.
+ requires net.ladenthin.srcmorph;
+
+ exports net.ladenthin.maven.srcmorph.mojo;
}
diff --git a/srcmorph-maven-plugin/src/main/java/net/ladenthin/maven/srcmorph/mojo/AbstractAiIndexMojo.java b/srcmorph-maven-plugin/src/main/java/net/ladenthin/maven/srcmorph/mojo/AbstractAiIndexMojo.java
new file mode 100644
index 0000000..d0a86db
--- /dev/null
+++ b/srcmorph-maven-plugin/src/main/java/net/ladenthin/maven/srcmorph/mojo/AbstractAiIndexMojo.java
@@ -0,0 +1,215 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.maven.srcmorph.mojo;
+
+import java.io.File;
+import java.util.List;
+import lombok.ToString;
+import net.ladenthin.srcmorph.config.AiFieldGenerationConfig;
+import net.ladenthin.srcmorph.config.AiModelDefinition;
+import net.ladenthin.srcmorph.config.SrcMorphConfiguration;
+import net.ladenthin.srcmorph.engine.SrcMorphException;
+import net.ladenthin.srcmorph.prompt.AiPromptDefinition;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugins.annotations.Parameter;
+
+/**
+ * Base class for all AI index mojos. Centralises the parameters shared by every goal
+ * ({@code generate}, {@code aggregate-packages}, {@code aggregate-project}, {@code calibrate}) and
+ * provides {@link #buildConfiguration()}, which maps the shared {@code @Parameter} fields onto a
+ * {@link net.ladenthin.srcmorph.config.SrcMorphConfiguration} for the matching
+ * {@code net.ladenthin.srcmorph.engine} engine to execute.
+ *
+ *
Concrete subclasses must implement {@link #getLlamaContextSize()} and
+ * {@link #getLlamaThreads()} so that each goal can declare its own
+ * {@code @Parameter}-annotated field with the appropriate default value.
+ *
+ *
{@code toString} is generated by Lombok over the {@code @Parameter} fields so
+ * Maven debug logs ({@code -X}) print a useful dump of the goal's resolved
+ * configuration. {@code equals}/{@code hashCode} are intentionally NOT generated:
+ * Maven instantiates mojos per execution and manages them by identity.
+ */
+// @Parameter fields are populated by the Maven plugin framework via reflection after
+// construction. NullAway is configured via ExcludedFieldAnnotations to skip them; Checker
+// Framework has no equivalent option for plugin-framework fields, so we suppress class-level.
+@SuppressWarnings("initialization.fields.uninitialized")
+@ToString(callSuper = true)
+public abstract class AbstractAiIndexMojo extends AbstractMojo {
+
+ /** Creates a new {@link AbstractAiIndexMojo}. */
+ protected AbstractAiIndexMojo() {
+ // no-op
+ }
+
+ /** The Maven project base directory, injected by Maven. */
+ @Parameter(defaultValue = "${project.basedir}", readonly = true, required = true)
+ protected File baseDirectory;
+
+ /**
+ * Directory into which all {@code .ai.md} files are written.
+ * Defaults to {@code ${project.basedir}/src/site/ai}.
+ */
+ @Parameter(property = "srcmorph.outputDirectory", defaultValue = "${project.basedir}/src/site/ai")
+ protected File outputDirectory;
+
+ /**
+ * Global skip switch: when {@code true}, every AI index goal (generate,
+ * aggregate-packages, aggregate-project) skips and returns immediately. To toggle a single
+ * phase instead, use that goal's own {@code skip} flag (see {@link #isPhaseSkipped()}).
+ */
+ @Parameter(property = "srcmorph.skip", defaultValue = "false")
+ protected boolean skip;
+
+ /**
+ * When {@code true}, regenerates AI fields even when they already have a value.
+ * When {@code false}, only missing or changed entries are processed.
+ */
+ @Parameter(property = "srcmorph.force", defaultValue = "false")
+ protected boolean force;
+
+ /**
+ * Source subdirectory paths (relative to {@code basedir}) to restrict processing.
+ * When empty, all discovered source roots are used.
+ */
+ @Parameter(property = "srcmorph.subtrees")
+ protected List subtrees;
+
+ /**
+ * Name of the AI generation provider to use.
+ * Supported values: {@code mock}, {@code llamacpp-jni}.
+ *
+ * @see net.ladenthin.srcmorph.provider.AiGenerationProviderFactory
+ */
+ @Parameter(property = "srcmorph.generationProvider", defaultValue = "mock")
+ protected String generationProvider;
+
+ /** Prompt template definitions referenced by field generation configurations. */
+ @Parameter
+ protected List promptDefinitions;
+
+ /**
+ * AI model definitions that pair a lookup key with a complete set of model parameters.
+ * Field-generation entries and the provider configuration reference these definitions
+ * by key rather than embedding the full parameter set inline.
+ *
+ * @see net.ladenthin.srcmorph.config.AiModelDefinition
+ * @see net.ladenthin.srcmorph.config.AiModelDefinitionSupport
+ */
+ @Parameter
+ protected List aiDefinitions;
+
+ /** Per-field AI generation configurations controlling which prompt and AI definition to use. */
+ @Parameter
+ protected List fieldGenerations;
+
+ /**
+ * Optional native library path passed to the llama.cpp JNI provider.
+ * Leave unset to use the bundled native library.
+ */
+ @Parameter(property = "srcmorph.llama.libraryPath")
+ protected String llamaLibraryPath;
+
+ /** Path to the GGUF model file used by the llama.cpp JNI provider. */
+ @Parameter(property = "srcmorph.llama.modelPath")
+ protected String llamaModelPath;
+
+ /** Maximum number of output tokens the model may generate per request. */
+ @Parameter(property = "srcmorph.llama.maxOutputTokens", defaultValue = "128")
+ protected int llamaMaxOutputTokens;
+
+ /** Sampling temperature for the llama.cpp model (lower = more deterministic). */
+ @Parameter(property = "srcmorph.llama.temperature", defaultValue = "0.15")
+ protected float llamaTemperature;
+
+ // -------------------------------------------------------------------------
+ // Abstract methods — subclasses declare @Parameter fields with their defaults
+ // -------------------------------------------------------------------------
+
+ /**
+ * Returns the llama.cpp context window size for this goal.
+ * Each concrete mojo declares its own {@code @Parameter}-annotated field and
+ * implements this method to return it.
+ *
+ * @return configured llama.cpp context window size
+ */
+ protected abstract int getLlamaContextSize();
+
+ /**
+ * Returns the number of CPU threads for llama.cpp inference for this goal.
+ * Each concrete mojo declares its own {@code @Parameter}-annotated field and
+ * implements this method to return it.
+ *
+ * @return configured number of CPU threads for llama.cpp inference
+ */
+ protected abstract int getLlamaThreads();
+
+ /**
+ * Returns whether this individual goal (phase) is disabled by its own phase-specific skip flag,
+ * independent of the global {@link #skip}. Each concrete goal declares its own
+ * {@code skip} {@code @Parameter} and returns it here, so the three phases (generate,
+ * aggregate-packages, aggregate-project) can be switched on and off independently.
+ *
+ * @return {@code true} if this phase's own skip flag is set
+ */
+ protected abstract boolean isPhaseSkipped();
+
+ // -------------------------------------------------------------------------
+ // Shared utility methods
+ // -------------------------------------------------------------------------
+
+ /**
+ * Returns whether this goal should skip execution: either the global {@link #skip} (which skips
+ * every phase) or this phase's own {@link #isPhaseSkipped()} flag. Centralised here so every goal
+ * applies the same rule identically. Skip flags stay mojo-side (a Maven lifecycle concern); an
+ * engine built from {@link #buildConfiguration()} always executes when asked.
+ *
+ * @return {@code true} if the goal must not run
+ */
+ protected boolean shouldSkip() {
+ return skip || isPhaseSkipped();
+ }
+
+ /**
+ * Maps the {@code @Parameter} fields shared by every goal onto a new
+ * {@link net.ladenthin.srcmorph.config.SrcMorphConfiguration}. Concrete subclasses that need
+ * additional fields (e.g. {@code pluginVersion}, {@code aiVersion}, {@code fileExtensions})
+ * call this first and then set their own goal-specific fields on the returned instance.
+ *
+ * @return a configuration populated from every shared {@code @Parameter} field
+ */
+ protected SrcMorphConfiguration buildConfiguration() {
+ final SrcMorphConfiguration config = new SrcMorphConfiguration();
+ config.setBaseDirectory(baseDirectory);
+ config.setOutputDirectory(outputDirectory);
+ config.setForce(force);
+ config.setSubtrees(subtrees);
+ config.setGenerationProvider(generationProvider);
+ config.setPromptDefinitions(promptDefinitions);
+ config.setAiDefinitions(aiDefinitions);
+ config.setFieldGenerations(fieldGenerations);
+ config.setLlamaLibraryPath(llamaLibraryPath);
+ config.setLlamaModelPath(llamaModelPath);
+ config.setLlamaContextSize(getLlamaContextSize());
+ config.setLlamaMaxOutputTokens(llamaMaxOutputTokens);
+ config.setLlamaTemperature(llamaTemperature);
+ config.setLlamaThreads(getLlamaThreads());
+ return config;
+ }
+
+ /**
+ * Returns {@code cause}'s message for wrapping into a
+ * {@link org.apache.maven.plugin.MojoExecutionException}, falling back to {@link Throwable#toString()}
+ * when the message is {@code null}. Every {@link SrcMorphException} constructor requires a non-null
+ * message in practice, but {@link Throwable#getMessage()} is not statically non-null, so every mojo's
+ * {@code catch (SrcMorphException e)} block goes through this helper rather than passing
+ * {@code e.getMessage()} straight through.
+ *
+ * @param cause the caught engine exception
+ * @return a non-null message suitable for {@code MojoExecutionException}'s constructor
+ */
+ protected static String messageOf(final SrcMorphException cause) {
+ final String message = cause.getMessage();
+ return message != null ? message : cause.toString();
+ }
+}
diff --git a/srcmorph-maven-plugin/src/main/java/net/ladenthin/maven/srcmorph/mojo/AggregatePackagesMojo.java b/srcmorph-maven-plugin/src/main/java/net/ladenthin/maven/srcmorph/mojo/AggregatePackagesMojo.java
new file mode 100644
index 0000000..380aa6b
--- /dev/null
+++ b/srcmorph-maven-plugin/src/main/java/net/ladenthin/maven/srcmorph/mojo/AggregatePackagesMojo.java
@@ -0,0 +1,101 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.maven.srcmorph.mojo;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import lombok.ToString;
+import net.ladenthin.srcmorph.config.SrcMorphConfiguration;
+import net.ladenthin.srcmorph.engine.AggregatePackagesEngine;
+import net.ladenthin.srcmorph.engine.SrcMorphException;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+
+/**
+ * Maven goal {@code srcmorph:aggregate-packages}: aggregates per-package
+ * {@code .ai.md} index files and fills in their AI-generated summary and keyword fields.
+ *
+ *
Thin wrapper: builds a {@link SrcMorphConfiguration} from its {@code @Parameter} fields and
+ * delegates the whole run to {@link AggregatePackagesEngine}.
+ */
+// @Parameter fields are populated by the Maven plugin framework via reflection after
+// construction. NullAway is configured via ExcludedFieldAnnotations to skip them; Checker
+// Framework has no equivalent option for plugin-framework fields, so we suppress class-level.
+@SuppressWarnings("initialization.fields.uninitialized")
+@Mojo(name = "aggregate-packages", threadSafe = true)
+@ToString(callSuper = true)
+public class AggregatePackagesMojo extends AbstractAiIndexMojo {
+
+ /** Creates a new {@link AggregatePackagesMojo}. */
+ public AggregatePackagesMojo() {
+ // no-op
+ }
+
+ /**
+ * Phase switch for the package phase (the {@code aggregate-packages} goal): when
+ * {@code true}, only this phase is skipped. The global {@link #skip} still skips every phase.
+ */
+ @Parameter(property = "srcmorph.package.skip", defaultValue = "false")
+ private boolean skipPackage;
+
+ @Parameter(defaultValue = "${project.version}", readonly = true)
+ private String pluginVersion;
+
+ @Parameter(property = "srcmorph.aiVersion", defaultValue = "0.0.0")
+ private String aiVersion;
+
+ /** llama.cpp context window size; smaller default suits the fast aggregate pass. */
+ @Parameter(property = "srcmorph.llama.contextSize", defaultValue = "2048")
+ private int llamaContextSize;
+
+ /** CPU threads for llama.cpp inference during package aggregation. */
+ @Parameter(property = "srcmorph.llama.threads", defaultValue = "2")
+ private int llamaThreads;
+
+ @Override
+ protected int getLlamaContextSize() {
+ return llamaContextSize;
+ }
+
+ @Override
+ protected int getLlamaThreads() {
+ return llamaThreads;
+ }
+
+ @Override
+ protected boolean isPhaseSkipped() {
+ return skipPackage;
+ }
+
+ /**
+ * Maps this goal's own {@code @Parameter} fields onto the shared configuration built by
+ * {@link AbstractAiIndexMojo#buildConfiguration()}.
+ *
+ * @return the fully populated configuration for {@link AggregatePackagesEngine}
+ */
+ private SrcMorphConfiguration buildAggregatePackagesConfiguration() {
+ final SrcMorphConfiguration config = buildConfiguration();
+ config.setPluginVersion(pluginVersion);
+ config.setAiVersion(aiVersion);
+ return config;
+ }
+
+ @Override
+ public void execute() throws MojoExecutionException {
+ if (shouldSkip()) {
+ getLog().info("AI package aggregation skipped.");
+ return;
+ }
+
+ final Path outputPath = outputDirectory.toPath().toAbsolutePath().normalize();
+ try {
+ new AggregatePackagesEngine(buildAggregatePackagesConfiguration()).execute();
+ } catch (SrcMorphException e) {
+ throw new MojoExecutionException(messageOf(e), e);
+ } catch (IOException e) {
+ throw new MojoExecutionException("Failed to aggregate package AI index files under " + outputPath, e);
+ }
+ }
+}
diff --git a/srcmorph-maven-plugin/src/main/java/net/ladenthin/maven/srcmorph/mojo/AggregateProjectMojo.java b/srcmorph-maven-plugin/src/main/java/net/ladenthin/maven/srcmorph/mojo/AggregateProjectMojo.java
new file mode 100644
index 0000000..2aaf1eb
--- /dev/null
+++ b/srcmorph-maven-plugin/src/main/java/net/ladenthin/maven/srcmorph/mojo/AggregateProjectMojo.java
@@ -0,0 +1,130 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.maven.srcmorph.mojo;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import lombok.ToString;
+import net.ladenthin.srcmorph.config.SrcMorphConfiguration;
+import net.ladenthin.srcmorph.engine.AggregateProjectEngine;
+import net.ladenthin.srcmorph.engine.SrcMorphException;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+
+/**
+ * Maven goal {@code srcmorph:aggregate-project}: aggregates every per-package
+ * {@code package.ai.md} into a single project-level {@code project.ai.md} — the top of the
+ * three-level index.
+ *
+ *
By default this goal is fully deterministic: it harvests the one-line lead already present in
+ * each package summary and writes a compact, navigable table of contents that links to every
+ * package, calling no AI model. It then needs no provider, model, or prompt configuration — only
+ * {@link #outputDirectory}, {@link #force}, and {@link #skip}.
+ *
+ *
Optional AI overview (opt-in). When a {@code } is configured
+ * (a {@code promptKey} + {@code aiDefinitionKey}), the goal additionally makes one AI call
+ * to write a short project overview paragraph from the per-package leads — see
+ * {@link net.ladenthin.srcmorph.indexer.ProjectIndexer}. Because the overview reuses the shared
+ * provider/prompt/model parameters, this mojo extends {@code AbstractAiIndexMojo} (the
+ * provider-oriented base) — but {@link net.ladenthin.srcmorph.engine.AggregateProjectEngine} builds a
+ * provider only when a field generation is present, so the deterministic default needs no model and
+ * pays no model cost.
+ *
+ *
Thin wrapper: builds a {@link SrcMorphConfiguration} from its {@code @Parameter} fields and
+ * delegates the whole run to {@link AggregateProjectEngine}.
+ *
+ *
{@code toString} is generated by Lombok over the {@code @Parameter} fields so Maven debug logs
+ * ({@code -X}) print a useful dump of the goal's resolved configuration. {@code equals}/{@code hashCode}
+ * are intentionally NOT generated: Maven instantiates mojos per execution and manages them by identity.
+ */
+// @Parameter fields are populated by the Maven plugin framework via reflection after
+// construction. NullAway is configured via ExcludedFieldAnnotations to skip them; Checker
+// Framework has no equivalent option for plugin-framework fields, so we suppress class-level.
+@SuppressWarnings("initialization.fields.uninitialized")
+@Mojo(name = "aggregate-project", threadSafe = true)
+@ToString(callSuper = true)
+public class AggregateProjectMojo extends AbstractAiIndexMojo {
+
+ /** Creates a new {@link AggregateProjectMojo}. */
+ public AggregateProjectMojo() {
+ // no-op
+ }
+
+ /**
+ * Phase switch for the project phase (the {@code aggregate-project} goal): when
+ * {@code true}, only this phase is skipped. The global {@link #skip} still skips every phase.
+ */
+ @Parameter(property = "srcmorph.project.skip", defaultValue = "false")
+ private boolean skipProject;
+
+ /** Plugin version recorded in the project index header. */
+ @Parameter(defaultValue = "${project.version}", readonly = true)
+ private String pluginVersion;
+
+ /** AI summarisation logic version recorded in the project index header. */
+ @Parameter(property = "srcmorph.aiVersion", defaultValue = "0.0.0")
+ private String aiVersion;
+
+ /** Maven project name recorded as the project index title. */
+ @Parameter(defaultValue = "${project.name}", readonly = true)
+ private String projectName;
+
+ /**
+ * llama.cpp context window size used only as the fallback when no {@code } is
+ * configured; with an overview field generation present, the size comes from its AI definition.
+ */
+ @Parameter(property = "srcmorph.llama.contextSize", defaultValue = "4096")
+ private int llamaContextSize;
+
+ /** CPU threads for llama.cpp inference, used only on the no-field-generation fallback path. */
+ @Parameter(property = "srcmorph.llama.threads", defaultValue = "2")
+ private int llamaThreads;
+
+ @Override
+ protected int getLlamaContextSize() {
+ return llamaContextSize;
+ }
+
+ @Override
+ protected int getLlamaThreads() {
+ return llamaThreads;
+ }
+
+ @Override
+ protected boolean isPhaseSkipped() {
+ return skipProject;
+ }
+
+ /**
+ * Maps this goal's own {@code @Parameter} fields onto the shared configuration built by
+ * {@link AbstractAiIndexMojo#buildConfiguration()}.
+ *
+ * @return the fully populated configuration for {@link AggregateProjectEngine}
+ */
+ private SrcMorphConfiguration buildAggregateProjectConfiguration() {
+ final SrcMorphConfiguration config = buildConfiguration();
+ config.setPluginVersion(pluginVersion);
+ config.setAiVersion(aiVersion);
+ config.setProjectName(projectName);
+ return config;
+ }
+
+ @Override
+ public void execute() throws MojoExecutionException {
+ if (shouldSkip()) {
+ getLog().info("AI project index aggregation skipped.");
+ return;
+ }
+
+ final Path outputPath = outputDirectory.toPath().toAbsolutePath().normalize();
+ try {
+ new AggregateProjectEngine(buildAggregateProjectConfiguration()).execute();
+ } catch (SrcMorphException e) {
+ throw new MojoExecutionException(messageOf(e), e);
+ } catch (IOException e) {
+ throw new MojoExecutionException("Failed to aggregate project AI index file under " + outputPath, e);
+ }
+ }
+}
diff --git a/srcmorph-maven-plugin/src/main/java/net/ladenthin/maven/srcmorph/mojo/CalibrateMojo.java b/srcmorph-maven-plugin/src/main/java/net/ladenthin/maven/srcmorph/mojo/CalibrateMojo.java
new file mode 100644
index 0000000..eedd4b6
--- /dev/null
+++ b/srcmorph-maven-plugin/src/main/java/net/ladenthin/maven/srcmorph/mojo/CalibrateMojo.java
@@ -0,0 +1,83 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.maven.srcmorph.mojo;
+
+import net.ladenthin.srcmorph.engine.CalibrateEngine;
+import net.ladenthin.srcmorph.engine.CalibrationReport;
+import net.ladenthin.srcmorph.engine.SrcMorphException;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+
+/**
+ * Maven goal {@code srcmorph:calibrate}: a preflight + timing pass that sits between {@code planOnly} (no
+ * model loaded) and a real {@code generate} run.
+ *
+ *
For each distinct model a run would load (the {@code aiDefinitionKey}s referenced by
+ * {@code }) it loads the model once (catching a bad path / OOM / wrong native early),
+ * runs a couple of representative generations, reads the model's own measured prefill/decode throughput,
+ * and prints a paste-ready {@code } block. Paste that onto the matching {@code }
+ * so the plan's time estimate reflects this machine instead of the built-in reference-CPU
+ * coefficients. The numbers are per machine (GPU/CPU, quant, context), so re-run on each host.
+ *
+ *
Thin wrapper: builds a {@link net.ladenthin.srcmorph.config.SrcMorphConfiguration} from the shared
+ * {@code @Parameter} fields and delegates the whole run to {@link CalibrateEngine}; only the final
+ * paste-ready banner and {@link CalibrationReport#renderXml()} output stay in this mojo.
+ */
+@SuppressWarnings("initialization.fields.uninitialized")
+@Mojo(name = "calibrate", threadSafe = true)
+public class CalibrateMojo extends AbstractAiIndexMojo {
+
+ /** Creates a new {@link CalibrateMojo}. */
+ public CalibrateMojo() {
+ // no-op
+ }
+
+ /** llama.cpp context window size (unused by calibrate, which uses each model's own config). */
+ @Parameter(property = "srcmorph.llama.contextSize", defaultValue = "2048")
+ private int llamaContextSize;
+
+ /** CPU threads (unused by calibrate, which uses each model's own config). */
+ @Parameter(property = "srcmorph.llama.threads", defaultValue = "2")
+ private int llamaThreads;
+
+ @Override
+ protected int getLlamaContextSize() {
+ return llamaContextSize;
+ }
+
+ @Override
+ protected int getLlamaThreads() {
+ return llamaThreads;
+ }
+
+ @Override
+ protected boolean isPhaseSkipped() {
+ // calibrate is a manual diagnostic goal, not one of the file/package/project phases; only the
+ // global srcmorph.skip disables it.
+ return false;
+ }
+
+ @Override
+ public void execute() throws MojoExecutionException {
+ if (shouldSkip()) {
+ getLog().info("AI index calibration skipped.");
+ return;
+ }
+
+ try {
+ final CalibrationReport report = new CalibrateEngine(buildConfiguration()).execute();
+
+ getLog().info("");
+ getLog().info("Paste each onto its matching (numbers are per machine).");
+ getLog().info("These are the model's own measured prefill/decode throughput (from the engine's "
+ + "per-call timings); only the mock/no-timings path falls back to a wall-clock estimate.");
+ for (final String line : report.renderXml().split("\n", -1)) {
+ getLog().info(line);
+ }
+ } catch (SrcMorphException e) {
+ throw new MojoExecutionException(messageOf(e), e);
+ }
+ }
+}
diff --git a/srcmorph-maven-plugin/src/main/java/net/ladenthin/maven/srcmorph/mojo/GenerateMojo.java b/srcmorph-maven-plugin/src/main/java/net/ladenthin/maven/srcmorph/mojo/GenerateMojo.java
new file mode 100644
index 0000000..6972e9d
--- /dev/null
+++ b/srcmorph-maven-plugin/src/main/java/net/ladenthin/maven/srcmorph/mojo/GenerateMojo.java
@@ -0,0 +1,162 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.maven.srcmorph.mojo;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
+import lombok.ToString;
+import net.ladenthin.srcmorph.config.AiFactDefinition;
+import net.ladenthin.srcmorph.config.SrcMorphConfiguration;
+import net.ladenthin.srcmorph.engine.GenerateEngine;
+import net.ladenthin.srcmorph.engine.SrcMorphException;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+
+/**
+ * Maven goal {@code srcmorph:generate}: indexes source files and fills in their
+ * AI-generated summary and keyword fields.
+ *
+ *
Thin wrapper: builds a {@link SrcMorphConfiguration} from its {@code @Parameter} fields and
+ * delegates the whole run to {@link GenerateEngine}.
+ */
+// @Parameter fields are populated by the Maven plugin framework via reflection after
+// construction. NullAway is configured via ExcludedFieldAnnotations to skip them; Checker
+// Framework has no equivalent option for plugin-framework fields, so we suppress class-level.
+@SuppressWarnings("initialization.fields.uninitialized")
+@Mojo(name = "generate", threadSafe = true)
+@ToString(callSuper = true)
+public class GenerateMojo extends AbstractAiIndexMojo {
+
+ /** Creates a new {@link GenerateMojo}. */
+ public GenerateMojo() {
+ // no-op
+ }
+
+ /**
+ * Phase switch for the file phase (the {@code generate} goal): when {@code true},
+ * only this phase is skipped. The global {@link #skip} still skips every phase.
+ */
+ @Parameter(property = "srcmorph.file.skip", defaultValue = "false")
+ private boolean skipFile;
+
+ @Parameter(defaultValue = "${project.version}", readonly = true)
+ private String pluginVersion;
+
+ @Parameter(property = "srcmorph.aiVersion", defaultValue = "0.0.0")
+ private String aiVersion;
+
+ @Parameter(property = "srcmorph.fileExtensions")
+ private List fileExtensions;
+
+ /**
+ * Glob patterns for source files to skip, matched against each file's path relative to the
+ * project base directory with {@code /} separators (e.g. {@code **}{@code /package-info.java},
+ * {@code **}{@code /generated/**}). Lets the index stay focused by excluding trivial or generated
+ * sources. Empty by default — nothing is excluded.
+ *
+ * @see net.ladenthin.srcmorph.support.AiSourceExcludeFilter
+ */
+ @Parameter(property = "srcmorph.excludes")
+ private List excludes;
+
+ /**
+ * Reusable, named {@code } groups referenced from a rule's {@code }, so a
+ * fact set (e.g. {@code java-facts}, {@code sql-facts}) is defined once and shared across rules
+ * instead of repeated inline. Resolved onto each rule's {@code facts} before indexing.
+ *
+ * @see net.ladenthin.srcmorph.config.AiFactDefinitionSupport
+ */
+ @Parameter
+ private List factDefinitions;
+
+ /**
+ * Exclusive lower file-size bound in bytes: source files whose size is {@code <= this} are skipped.
+ * {@code 0} (default) disables the lower bound. Together with {@link #maxFileSizeBytes} this lets one
+ * {@code generate} execution target a size band, so several executions (each with its own model,
+ * context size and prompt) can tier a project by file size while the source-checksum skip indexes
+ * every file exactly once. Use non-overlapping bands ({@code band2.min == band1.max}); make the last
+ * band unbounded ({@code maxFileSizeBytes=0}) so files above all bands still get indexed.
+ */
+ @Parameter(property = "srcmorph.file.minSizeBytes", defaultValue = "0")
+ private long minFileSizeBytes;
+
+ /**
+ * Inclusive upper file-size bound in bytes: source files whose size is {@code > this} are skipped.
+ * {@code 0} (default) disables the upper bound (unlimited). See {@link #minFileSizeBytes} for the
+ * size-tiering pattern.
+ */
+ @Parameter(property = "srcmorph.file.maxSizeBytes", defaultValue = "0")
+ private long maxFileSizeBytes;
+
+ /** llama.cpp context window size; smaller default suits the fast generate pass. */
+ @Parameter(property = "srcmorph.llama.contextSize", defaultValue = "2048")
+ private int llamaContextSize;
+
+ /** CPU threads for llama.cpp inference during the generate pass. */
+ @Parameter(property = "srcmorph.llama.threads", defaultValue = "2")
+ private int llamaThreads;
+
+ /**
+ * When {@code true}, only build and log the routing plan (which model indexes which files with which
+ * prompt) and then stop — no model is loaded and nothing is generated. Useful to verify routing
+ * before a long run.
+ */
+ @Parameter(property = "srcmorph.planOnly", defaultValue = "false")
+ private boolean planOnly;
+
+ @Override
+ protected int getLlamaContextSize() {
+ return llamaContextSize;
+ }
+
+ @Override
+ protected int getLlamaThreads() {
+ return llamaThreads;
+ }
+
+ @Override
+ protected boolean isPhaseSkipped() {
+ return skipFile;
+ }
+
+ /**
+ * Maps this goal's own {@code @Parameter} fields onto the shared configuration built by
+ * {@link AbstractAiIndexMojo#buildConfiguration()}.
+ *
+ * @return the fully populated configuration for {@link GenerateEngine}
+ */
+ private SrcMorphConfiguration buildGenerateConfiguration() {
+ final SrcMorphConfiguration config = buildConfiguration();
+ config.setPluginVersion(pluginVersion);
+ config.setAiVersion(aiVersion);
+ config.setFileExtensions(fileExtensions);
+ config.setExcludes(excludes);
+ config.setFactDefinitions(factDefinitions);
+ config.setMinFileSizeBytes(minFileSizeBytes);
+ config.setMaxFileSizeBytes(maxFileSizeBytes);
+ config.setPlanOnly(planOnly);
+ return config;
+ }
+
+ @Override
+ public void execute() throws MojoExecutionException {
+ if (shouldSkip()) {
+ getLog().info("AI index generation skipped.");
+ return;
+ }
+
+ final Path basePath = baseDirectory.toPath().toAbsolutePath().normalize();
+ final Path outputPath = outputDirectory.toPath().toAbsolutePath().normalize();
+ try {
+ new GenerateEngine(buildGenerateConfiguration()).execute();
+ } catch (SrcMorphException e) {
+ throw new MojoExecutionException(messageOf(e), e);
+ } catch (IOException e) {
+ throw new MojoExecutionException(
+ "Failed to generate AI index files under " + outputPath + " from base " + basePath, e);
+ }
+ }
+}
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/mojo/package-info.java b/srcmorph-maven-plugin/src/main/java/net/ladenthin/maven/srcmorph/mojo/package-info.java
similarity index 54%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/mojo/package-info.java
rename to srcmorph-maven-plugin/src/main/java/net/ladenthin/maven/srcmorph/mojo/package-info.java
index 8aa9f7b..64af843 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/mojo/package-info.java
+++ b/srcmorph-maven-plugin/src/main/java/net/ladenthin/maven/srcmorph/mojo/package-info.java
@@ -3,6 +3,6 @@
// SPDX-License-Identifier: Apache-2.0
/**
- * Maven goal entry points (the {@code ai-index} mojos).
+ * Maven goal entry points (the {@code srcmorph} mojos).
*/
-package net.ladenthin.maven.llamacpp.aiindex.mojo;
+package net.ladenthin.maven.srcmorph.mojo;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/package-info.java b/srcmorph-maven-plugin/src/main/java/net/ladenthin/maven/srcmorph/package-info.java
similarity index 96%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/package-info.java
rename to srcmorph-maven-plugin/src/main/java/net/ladenthin/maven/srcmorph/package-info.java
index eacf45f..0305bfa 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/package-info.java
+++ b/srcmorph-maven-plugin/src/main/java/net/ladenthin/maven/srcmorph/package-info.java
@@ -20,4 +20,4 @@
* unsuppressible {@code unknown enum constant ElementType.MODULE}
* classfile-read warning that javac emits otherwise.
*/
-package net.ladenthin.maven.llamacpp.aiindex;
+package net.ladenthin.maven.srcmorph;
diff --git a/llamacpp-ai-index-maven-plugin/src/site/ai/empty.md b/srcmorph-maven-plugin/src/site/ai/empty.md
similarity index 100%
rename from llamacpp-ai-index-maven-plugin/src/site/ai/empty.md
rename to srcmorph-maven-plugin/src/site/ai/empty.md
diff --git a/srcmorph-maven-plugin/src/test/java/net/ladenthin/maven/srcmorph/PluginArchitectureTest.java b/srcmorph-maven-plugin/src/test/java/net/ladenthin/maven/srcmorph/PluginArchitectureTest.java
new file mode 100644
index 0000000..aa1ba44
--- /dev/null
+++ b/srcmorph-maven-plugin/src/test/java/net/ladenthin/maven/srcmorph/PluginArchitectureTest.java
@@ -0,0 +1,188 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.maven.srcmorph;
+
+import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.fields;
+import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
+import static com.tngtech.archunit.library.Architectures.layeredArchitecture;
+import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices;
+
+import com.tngtech.archunit.core.importer.ImportOption;
+import com.tngtech.archunit.junit.AnalyzeClasses;
+import com.tngtech.archunit.junit.ArchTest;
+import com.tngtech.archunit.lang.ArchRule;
+import java.util.Random;
+import org.slf4j.Logger;
+
+/**
+ * Architecture rules for the srcmorph Maven plugin module itself — the {@code mojo}
+ * package (Mojo entry points) — after the {@code config}/{@code document}/{@code indexer}/
+ * {@code prompt}/{@code provider}/{@code support} packages were extracted into the sibling
+ * {@code srcmorph} core library. The rules that governed those layers (the internal layered
+ * architecture, {@code nonMojoIsMavenFree}, {@code jniConfinedToProvider}, etc.) now live in that
+ * module's own {@code CoreArchitectureTest}; only mojo-relevant rules remain here.
+ */
+@AnalyzeClasses(packages = "net.ladenthin.maven.srcmorph", importOptions = ImportOption.DoNotIncludeTests.class)
+public class PluginArchitectureTest {
+
+ /**
+ * Maven plugins use Maven's Log interface for logging; java.util.logging must not be used.
+ */
+ @ArchTest
+ static final ArchRule noJavaUtilLogging = noClasses()
+ .that()
+ .resideInAPackage("net.ladenthin.maven.srcmorph..")
+ .should()
+ .dependOnClassesThat()
+ .resideInAPackage("java.util.logging..");
+
+ /**
+ * Test-framework classes must not appear in production code.
+ */
+ @ArchTest
+ static final ArchRule noTestFrameworksInProduction = noClasses()
+ .that()
+ .resideInAPackage("net.ladenthin.maven.srcmorph..")
+ .should()
+ .dependOnClassesThat()
+ .resideInAnyPackage("org.junit..", "net.jqwik..", "com.tngtech.archunit..");
+
+ /**
+ * Production code must not write to {@code System.out} / {@code System.err}; all output
+ * goes through Maven's {@code Log} via {@code getLog()}. Currently vacuous (no usage); acts
+ * as a regression guard.
+ */
+ @ArchTest
+ static final ArchRule noSystemOutOrErrInProduction = noClasses()
+ .that()
+ .resideInAPackage("net.ladenthin.maven.srcmorph..")
+ .should()
+ .accessField(System.class, "out")
+ .orShould()
+ .accessField(System.class, "err");
+
+ /**
+ * Production code must not import unsupported / internal JDK packages.
+ * These are not part of the Java SE API and may change or disappear without notice.
+ */
+ @ArchTest
+ static final ArchRule noInternalJdkImports = noClasses()
+ .that()
+ .resideInAPackage("net.ladenthin.maven.srcmorph..")
+ .should()
+ .dependOnClassesThat()
+ .resideInAnyPackage("sun..", "com.sun..", "jdk.internal..");
+
+ /**
+ * No package cycles between the layered sub-packages.
+ */
+ @ArchTest
+ static final ArchRule noPackageCycles = slices().matching("net.ladenthin.maven.srcmorph.(*)..")
+ .should()
+ .beFreeOfCycles()
+ .allowEmptyShould(true);
+
+ /**
+ * Layered architecture — now a single {@code Mojo} layer (the other tiers moved to the
+ * srcmorph core library and are governed by that module's own {@code CoreArchitectureTest}).
+ * Kept as a regression guard: nothing inside this module may depend on the {@code mojo}
+ * package (the plugin's own code never needs to reach "up" into its own entry points).
+ */
+ @ArchTest
+ static final ArchRule layeredArchitecture = layeredArchitecture()
+ .consideringOnlyDependenciesInLayers()
+ .layer("Mojo")
+ .definedBy("net.ladenthin.maven.srcmorph.mojo..")
+ .whereLayer("Mojo")
+ .mayNotBeAccessedByAnyLayer();
+
+ /**
+ * Public mutable state forbidden: any non-static field declared
+ * {@code public} must also be {@code final}. Maven plugin configuration
+ * POJOs (now in the srcmorph module) use {@code private} fields with setters;
+ * this rule is a regression guard on whatever remains here.
+ */
+ @ArchTest
+ static final ArchRule noPublicMutableFields = fields().that()
+ .arePublic()
+ .and()
+ .areNotStatic()
+ .should()
+ .beFinal()
+ .allowEmptyShould(true); // regression guard; passes vacuously today
+
+ /**
+ * Production code must not call {@link System#exit(int)}; throw a
+ * {@link org.apache.maven.plugin.MojoExecutionException} or
+ * {@link org.apache.maven.plugin.MojoFailureException} instead so Maven
+ * surfaces the failure to its caller.
+ */
+ @ArchTest
+ static final ArchRule noSystemExit = noClasses()
+ .that()
+ .resideInAPackage("net.ladenthin.maven.srcmorph..")
+ .should()
+ .callMethod(System.class, "exit", int.class)
+ .allowEmptyShould(true);
+
+ /**
+ * Production code must not construct {@link java.util.Random}; {@code Random} is a non-cryptographic
+ * PRNG (CWE-338). Use {@link java.security.SecureRandom} or {@link java.util.concurrent.ThreadLocalRandom}
+ * depending on whether cryptographic strength or thread-local fast jitter is needed.
+ */
+ @ArchTest
+ static final ArchRule noNewRandom = noClasses()
+ .that()
+ .resideInAPackage("net.ladenthin.maven.srcmorph..")
+ .should()
+ .callConstructor(Random.class)
+ .orShould()
+ .callConstructor(Random.class, long.class)
+ .allowEmptyShould(true);
+
+ /**
+ * Production code must not call {@link Thread#sleep(long)} / {@link Thread#sleep(long, int)};
+ * prefer {@link java.util.concurrent.BlockingQueue#poll(long, java.util.concurrent.TimeUnit)} or
+ * {@link java.util.concurrent.locks.Condition#await(long, java.util.concurrent.TimeUnit)}.
+ */
+ @ArchTest
+ static final ArchRule noThreadSleep = noClasses()
+ .that()
+ .resideInAPackage("net.ladenthin.maven.srcmorph..")
+ .should()
+ .callMethod(Thread.class, "sleep", long.class)
+ .orShould()
+ .callMethod(Thread.class, "sleep", long.class, int.class)
+ .allowEmptyShould(true);
+
+ /**
+ * The Maven Mojo SPI annotations ({@code @Mojo} / {@code @Parameter} from
+ * {@code org.apache.maven.plugins.annotations..}) may only appear in the {@code mojo}
+ * package — the goal entry points. Any future non-mojo package added to this module must
+ * stay plain Java and must not be annotated as a Maven component.
+ */
+ @ArchTest
+ static final ArchRule mavenMojoAnnotationsConfinedToMojo = noClasses()
+ .that()
+ .resideOutsideOfPackage("net.ladenthin.maven.srcmorph.mojo..")
+ .should()
+ .dependOnClassesThat()
+ .resideInAPackage("org.apache.maven.plugins.annotations..")
+ .allowEmptyShould(true);
+
+ /**
+ * Every SLF4J {@link Logger} field must be {@code private static final} — a single shared
+ * logger per class, never an instance field or a mutable/visible one.
+ */
+ @ArchTest
+ static final ArchRule loggersArePrivateStaticFinal = fields().that()
+ .haveRawType(Logger.class)
+ .should()
+ .bePrivate()
+ .andShould()
+ .beStatic()
+ .andShould()
+ .beFinal()
+ .allowEmptyShould(true);
+}
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/jcstress/AiGenerationKindRace.java b/srcmorph-maven-plugin/src/test/java/net/ladenthin/maven/srcmorph/jcstress/AiGenerationKindRace.java
similarity index 90%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/jcstress/AiGenerationKindRace.java
rename to srcmorph-maven-plugin/src/test/java/net/ladenthin/maven/srcmorph/jcstress/AiGenerationKindRace.java
index b0fb7af..4c0ff33 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/jcstress/AiGenerationKindRace.java
+++ b/srcmorph-maven-plugin/src/test/java/net/ladenthin/maven/srcmorph/jcstress/AiGenerationKindRace.java
@@ -1,9 +1,9 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.jcstress;
+package net.ladenthin.maven.srcmorph.jcstress;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiGenerationKind;
+import net.ladenthin.srcmorph.config.AiGenerationKind;
import org.openjdk.jcstress.annotations.Actor;
import org.openjdk.jcstress.annotations.Description;
import org.openjdk.jcstress.annotations.Expect;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/mojo/MojoPhaseSkipTest.java b/srcmorph-maven-plugin/src/test/java/net/ladenthin/maven/srcmorph/mojo/MojoPhaseSkipTest.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/mojo/MojoPhaseSkipTest.java
rename to srcmorph-maven-plugin/src/test/java/net/ladenthin/maven/srcmorph/mojo/MojoPhaseSkipTest.java
index a7d35f1..d3a1c02 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/mojo/MojoPhaseSkipTest.java
+++ b/srcmorph-maven-plugin/src/test/java/net/ladenthin/maven/srcmorph/mojo/MojoPhaseSkipTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.mojo;
+package net.ladenthin.maven.srcmorph.mojo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/vmlens/VmlensInterleavingSmokeTest.java b/srcmorph-maven-plugin/src/test/java/net/ladenthin/maven/srcmorph/vmlens/VmlensInterleavingSmokeTest.java
similarity index 97%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/vmlens/VmlensInterleavingSmokeTest.java
rename to srcmorph-maven-plugin/src/test/java/net/ladenthin/maven/srcmorph/vmlens/VmlensInterleavingSmokeTest.java
index 22577ed..adcd02d 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/vmlens/VmlensInterleavingSmokeTest.java
+++ b/srcmorph-maven-plugin/src/test/java/net/ladenthin/maven/srcmorph/vmlens/VmlensInterleavingSmokeTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.vmlens;
+package net.ladenthin.maven.srcmorph.vmlens;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
diff --git a/srcmorph/README.md b/srcmorph/README.md
new file mode 100644
index 0000000..5cbe5a0
--- /dev/null
+++ b/srcmorph/README.md
@@ -0,0 +1,147 @@
+# srcmorph (core library)
+
+`net.ladenthin:srcmorph` is the framework-free Java API behind the
+[srcmorph](../README.md) project: a prompt-driven source-tree transformer built on local llama.cpp
+(GGUF) models. It has **no dependency on the Maven Plugin API** (enforced by an ArchUnit rule —
+see `CoreArchitectureTest#coreIsMavenFree`), so it can be embedded in any JVM tool: a Maven plugin (see
+[`../srcmorph-maven-plugin/`](../srcmorph-maven-plugin/README.md)), a standalone CLI
+(see [`../srcmorph-cli/`](../srcmorph-cli/README.md)), a Gradle plugin, a test harness, or plain code.
+
+Package root: `net.ladenthin.srcmorph`.
+
+## The shared configuration object: `SrcMorphConfiguration`
+
+`net.ladenthin.srcmorph.config.SrcMorphConfiguration` is one mutable JavaBean holding everything a
+run needs. It is the single object every binding surface populates the same way — Maven plexus
+reflection (structural XML binding, no annotations needed), a Jackson `ObjectMapper`/`YAMLMapper`
+(the CLI), or plain Java setter calls. **Field names intentionally mirror the Maven plugin's
+`@Parameter` names**, so a JSON/YAML key reads identically to the matching `` XML
+element.
+
+Major field groups:
+
+| Group | Fields |
+|---|---|
+| Source selection | `baseDirectory`, `subtrees`, `excludes`, `fileExtensions`, `minFileSizeBytes`/`maxFileSizeBytes` |
+| Output | `outputDirectory`, `force`, `planOnly` |
+| Routing & prompts | `promptDefinitions` (`List`), `aiDefinitions` (`List`), `fieldGenerations` (`List` — the routing rules), `factDefinitions` |
+| AI backend | `generationProvider` (`"mock"` or `"llamacpp-jni"`), `llamaLibraryPath`, plus the `llama*` fallback params (`llamaModelPath`/`llamaContextSize`/`llamaMaxOutputTokens`/`llamaTemperature`/`llamaThreads`), used only when `fieldGenerations` is empty |
+| Header metadata | `pluginVersion`, `aiVersion`, `projectName` |
+
+Every other `config/` class is a plain, Maven-annotation-free JavaBean too: `AiModelDefinition` (a
+named, complete set of GGUF sampling parameters), `AiPromptDefinition` (a named prompt template —
+its `template` string is the system instructions, used verbatim; `AiPromptSupport` automatically
+appends the file/package name and the source text as a separate user message, so a template needs no
+placeholder), `AiFieldGenerationConfig` (one routing rule: a
+`condition` tree, `priority`, and either a route, a `skip`, or the explicit `fallback`),
+`AiCondition`/`AiConditionGroup`/`AiRangeCondition` (the composable and/or/not condition tree over
+extensions/size/lines/modified-time/path-glob), `AiFactCounter`/`AiFactDefinition` (deterministic
+regex-count "facts" prepended to a generated body), `AiOversizeStrategy` (`fail`/`sample`/
+`mapReduce`/`deterministic` — what a rule does when a file is larger than its model's context
+window), `AiCalibration` (per-machine measured throughput, pasted onto an `AiModelDefinition`).
+
+## The four engines
+
+Each engine is constructed from one `SrcMorphConfiguration` and owns its own AI provider lifecycle
+(try-with-resources; one model resident in memory at a time). All four throw the checked
+`net.ladenthin.srcmorph.engine.SrcMorphException` on misconfiguration (an invalid rule set, an
+unmatched file with no fallback, an oversized file with `onOversize=fail`, a bad prompt/model
+reference) and let a plain `java.io.IOException` propagate for genuine I/O failures — callers (the
+Maven plugin's mojos, the CLI's `Main`) translate these into whatever their own surface expects.
+
+| Engine | Purpose | Result |
+|---|---|---|
+| `GenerateEngine` | Phase 1: plans the whole run (which model + prompt each file gets, or skip/unmatched, and whether it fits its routed model's context window), fails fast on an unmatched file or a hard oversize failure, stops after the plan when `planOnly` is set, otherwise loads each distinct routed model once and indexes its files. | `GenerateResult` (`planOnly()`, `written()`, `unchanged()`, `skipped()`) |
+| `AggregatePackagesEngine` | Phase 2: aggregates every package beneath the output directory into a `package.ai.md` per directory. | `int` — package index files written or refreshed |
+| `AggregateProjectEngine` | Phase 3: harvests every package's one-line lead into a single `project.ai.md` table of contents. Fully deterministic by default (no model call); when `fieldGenerations` has at least one entry, its **first** entry is used for one extra AI call that writes a short `#### Overview` paragraph. | `int` — `1` if written/refreshed, `0` otherwise |
+| `CalibrateEngine` | Loads each distinct model a `GenerateEngine` run would load, measures its prefill/decode throughput via a couple of representative generations, and returns a paste-ready report. | `CalibrationReport` (`measurements()`, `renderXml()` — a paste-ready `` XML block per model) |
+
+**A design nuance worth knowing if you drive more than one engine off the same
+`SrcMorphConfiguration`** (as the CLI's `All`/`Calibrate` commands do): `GenerateEngine` *routes* each
+file to exactly one matching `fieldGenerations` rule (by `condition` + `priority`, with the explicit
+`fallback` catching the rest). `AggregatePackagesEngine` and the optional project overview do **not**
+route by condition — `AggregatePackagesEngine` applies **every** entry in `fieldGenerations` to every
+package in list order (the last entry's output wins), and `AggregateProjectEngine`'s optional overview
+uses only `fieldGenerations.get(0)`. A single fallback-only rule (no condition needed) behaves
+identically across all three phases; a multi-rule routing table is meaningful for `GenerateEngine`
+but you should reason explicitly about what happens when the same list feeds package/project
+aggregation. See `srcmorph-cli/README.md`'s config-file reference for the CLI's own take on this.
+
+## Use as a library
+
+```java
+import java.io.File;
+import java.util.Collections;
+import net.ladenthin.srcmorph.config.AiFieldGenerationConfig;
+import net.ladenthin.srcmorph.config.AiModelDefinition;
+import net.ladenthin.srcmorph.config.SrcMorphConfiguration;
+import net.ladenthin.srcmorph.engine.GenerateEngine;
+import net.ladenthin.srcmorph.engine.GenerateResult;
+import net.ladenthin.srcmorph.engine.SrcMorphException;
+import net.ladenthin.srcmorph.prompt.AiPromptDefinition;
+
+SrcMorphConfiguration config = new SrcMorphConfiguration();
+config.setBaseDirectory(new File("."));
+config.setOutputDirectory(new File("target/srcmorph-ai"));
+config.setGenerationProvider("mock"); // or "llamacpp-jni" with a real GGUF model below
+
+AiPromptDefinition prompt = new AiPromptDefinition();
+prompt.setKey("file-body");
+prompt.setTemplate("Summarize this file as structured markdown.");
+config.setPromptDefinitions(Collections.singletonList(prompt));
+
+AiModelDefinition model = new AiModelDefinition();
+model.setKey("coder");
+model.setModelPath("/path/to/model.gguf"); // unused by the mock provider
+config.setAiDefinitions(Collections.singletonList(model));
+
+AiFieldGenerationConfig rule = new AiFieldGenerationConfig();
+rule.setPromptKey("file-body");
+rule.setAiDefinitionKey("coder");
+rule.setFallback(true);
+config.setFieldGenerations(Collections.singletonList(rule));
+
+try {
+ GenerateResult result = new GenerateEngine(config).execute();
+ System.out.println("written=" + result.written() + " unchanged=" + result.unchanged());
+} catch (SrcMorphException | java.io.IOException e) {
+ // handle: misconfiguration vs. I/O failure
+}
+```
+
+## Provider abstraction
+
+`net.ladenthin.srcmorph.provider.AiGenerationProvider` (a `Closeable`) abstracts the AI backend.
+`AiGenerationProviderFactory` looks up an implementation by name:
+
+- `"mock"` (default) — `MockAiGenerationProvider`, deterministic canned responses; no model, no JNI,
+ no native library required. Used throughout every module's test suite.
+- `"llamacpp-jni"` — `LlamaCppJniAiGenerationProvider`, backed by
+ [`net.ladenthin:llama`](https://central.sonatype.com/artifact/net.ladenthin/llama) (the llama.cpp
+ JNI binding). This is the **only** package in this library that touches the JNI binding
+ (`CoreArchitectureTest#jniConfinedToProvider`).
+
+## Package layout
+
+```
+net.ladenthin.srcmorph/
+├── config/ mutable JavaBeans; SrcMorphConfiguration is the shared root object
+├── engine/ GenerateEngine, AggregatePackagesEngine, AggregateProjectEngine, CalibrateEngine,
+│ SrcMorphException, GenerateResult, CalibrationReport, EngineSupport
+├── indexer/ the walk/plan/write orchestration engines delegate to
+├── document/ the .ai.md model + codecs (AiMdDocument, AiMdHeader, AiMdDocumentCodec, ...)
+├── prompt/ prompt template lookup + preparation (trimming to a model's context window)
+├── provider/ the AI backend abstraction (see above)
+└── support/ foundation helpers with no dependency on anything above them
+```
+
+See [`../CLAUDE.md`](../CLAUDE.md) for the full architecture rules (`CoreArchitectureTest`), the
+layered-package dependency direction, and the PIT mutation-testing scope (currently 47 classes at
+100%).
+
+## Testing
+
+Every test in this module is model-free by default (`MockAiGenerationProvider`); a handful of
+real-model tests are gated on `src/test/resources/SmolLM2-135M-Instruct-Q3_K_M.gguf` and self-skip
+when the native library is unavailable. See the root [`TEST_WRITING_GUIDE.md`](../TEST_WRITING_GUIDE.md)
+for conventions and [`../CLAUDE.md`](../CLAUDE.md) for how to run a single test class.
diff --git a/srcmorph/pom.xml b/srcmorph/pom.xml
new file mode 100644
index 0000000..e84218c
--- /dev/null
+++ b/srcmorph/pom.xml
@@ -0,0 +1,673 @@
+
+
+
+ 4.0.0
+
+
+ net.ladenthin
+ srcmorph-parent
+ 1.1.0
+ ../pom.xml
+
+
+ srcmorph
+ jar
+
+ srcmorph
+ Core library extracted from llamacpp-ai-index-maven-plugin: routing/configuration model, the .ai.md document codec, the file/package/project indexers, prompt templating support, and the AI generation provider abstraction (incl. the llama.cpp JNI binding). Framework-free (no Maven plugin API); the srcmorph-maven-plugin module supplies only the Mojo entry points on top of this library.
+
+
+ bernardladenthin
+ 8
+ 21
+ UTF-8
+
+ 5.0.6
+
+
+ 6.1.2
+ 3.0
+ 3.6
+ 2.50.0
+ 0.13.7
+ 1.0.0
+ 4.2.1
+
+ 1.9.3
+ 1.4.2
+ 4.10.2.0
+ 1.18.46
+ 7.7.4
+ 1.14.0
+ 3.8.0
+ 2.94.0
+
+ 2026-07-15T20:07:58Z
+
+
+
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+ provided
+
+
+ org.jspecify
+ jspecify
+ ${jspecify.version}
+
+
+ org.checkerframework
+ checker-qual
+ ${checker.version}
+
+
+ net.ladenthin
+ llama
+ ${llama.version}
+ ${llama.classifier}
+
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.version}
+ test
+
+
+
+ org.hamcrest
+ hamcrest
+ ${hamcrest.version}
+ test
+
+
+ net.jqwik
+ jqwik
+ ${jqwik.version}
+ test
+
+
+ com.tngtech.archunit
+ archunit-junit5
+ ${archunit.version}
+ test
+
+
+
+ org.jetbrains.lincheck
+ lincheck
+ ${lincheck.version}
+ test
+
+
+
+ ch.qos.logback
+ logback-classic
+ test
+
+
+
+
+
+
+
+ com.diffplug.spotless
+ spotless-maven-plugin
+ ${spotless.version}
+
+
+ com.github.spotbugs
+ spotbugs-maven-plugin
+ ${spotbugs.version}
+
+
+ io.github.git-commit-id
+ git-commit-id-maven-plugin
+ 10.0.0
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.15.0
+
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+ 3.6.3
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.2.8
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.5.0
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.12.0
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.4.0
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.6
+
+
+ @{argLine} -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=. -XX:ErrorFile=hs_err_pid%p.log
+
+ false
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.15
+
+
+ org.pitest
+ pitest-maven
+ 1.25.7
+
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.11.0
+
+
+
+
+
+ io.github.git-commit-id
+ git-commit-id-maven-plugin
+
+
+ get-git-properties
+
+ revision
+
+ initialize
+
+
+
+ yyyy-MM-dd'T'HH:mm:ss'Z'
+ UTC
+ false
+ false
+
+
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+
+
+ enforce-maven
+
+ enforce
+
+
+
+
+ [3.6.3,)
+
+
+ [1.8,)
+
+
+
+
+
+ commons-logging:commons-logging
+
+ log4j:log4j
+
+ org.hamcrest:hamcrest-core
+ org.hamcrest:hamcrest-library
+ org.hamcrest:hamcrest-all
+
+ junit:junit
+ junit:junit-dep
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ ${maven.compiler.release}
+ ${maven.compiler.testRelease}
+ true
+ true
+
+
+ -Xlint:all,-serial,-options,-classfile,-processing
+ -Werror
+
+ -processor
+ lombok.launch.AnnotationProcessorHider$AnnotationProcessor,lombok.launch.AnnotationProcessorHider$ClaimingProcessor,org.checkerframework.checker.nullness.NullnessChecker
+ -XDaddTypeAnnotationsToSymbol=true
+ -XDcompilePolicy=simple
+ --should-stop=ifError=FLOW
+ -Xplugin:ErrorProne -XepExcludedPaths:.*/generated-sources/.* -Xep:NullAway:ERROR -XepOpt:NullAway:OnlyNullMarked=true -XepOpt:NullAway:ExcludedFieldAnnotations=org.apache.maven.plugins.annotations.Parameter,org.apache.maven.plugins.annotations.Component -XepOpt:NullAway:JSpecifyMode=true -XepOpt:NullAway:CheckOptionalEmptiness=true -XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true -XepOpt:NullAway:AcknowledgeAndroidRecent=true -XepOpt:NullAway:AssertsEnabled=true -Xep:BoxedPrimitiveEquality:ERROR -Xep:EqualsHashCode:ERROR -Xep:EqualsIncompatibleType:ERROR -Xep:IdentityBinaryExpression:ERROR -Xep:SelfAssignment:ERROR -Xep:SelfComparison:ERROR -Xep:SelfEquals:ERROR -Xep:DeadException:ERROR -Xep:FormatString:ERROR -Xep:InvalidPatternSyntax:ERROR -Xep:OptionalEquality:ERROR -Xep:ImpossibleNullComparison:ERROR
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+ com.google.errorprone
+ error_prone_core
+ ${errorprone.version}
+
+
+ com.uber.nullaway
+ nullaway
+ ${nullaway.version}
+
+
+ org.checkerframework
+ checker
+ ${checker.version}
+
+
+
+
+
+ default-compile
+
+
+
+ module-info.java
+
+
+
+
+ module-info-compile
+ compile
+
+ compile
+
+
+
+ 9
+
+ module-info.java
+
+
+
+
+
+
+ default-testCompile
+
+
+ false
+
+ -XDaddTypeAnnotationsToSymbol=true
+ -XDcompilePolicy=simple
+ --should-stop=ifError=FLOW
+ -Xplugin:ErrorProne -Xep:NullAway:OFF -Xep:GuardedBy:OFF -Xep:IdentityBinaryExpression:OFF
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+ attach-sources
+ verify
+
+ jar-no-fork
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+ ${maven.compiler.release}
+ true
+ true
+ all
+
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+
+
+ prepare-agent
+
+ prepare-agent
+
+
+
+ report
+ test
+
+ report
+
+
+
+
+
+ com.diffplug.spotless
+ spotless-maven-plugin
+
+
+
+ src/main/java/**/*.java
+ src/test/java/**/*.java
+
+
+ ${palantir-java-format.version}
+
+
+
+
+
+
+
+
+ spotless-check
+ verify
+
+ check
+
+
+
+
+
+ com.github.spotbugs
+ spotbugs-maven-plugin
+
+ Max
+ Low
+ true
+ false
+ spotbugs-exclude.xml
+
+
+ com.mebigfatguy.fb-contrib
+ fb-contrib
+ ${fb-contrib.version}
+
+
+ com.h3xstream.findsecbugs
+ findsecbugs-plugin
+ ${findsecbugs.version}
+
+
+
+
+
+ spotbugs-check
+ verify
+
+ check
+
+
+
+
+
+
+ org.pitest
+ pitest-maven
+
+
+ org.pitest
+ pitest-junit5-plugin
+ 1.2.3
+
+
+
+
+
+ net.ladenthin.srcmorph.config.AiCalibration
+ net.ladenthin.srcmorph.config.AiCondition
+ net.ladenthin.srcmorph.config.AiConditionEvaluator
+ net.ladenthin.srcmorph.config.AiFieldGenerationConfig
+ net.ladenthin.srcmorph.config.AiFieldGenerationSelector
+ net.ladenthin.srcmorph.config.AiFileContext
+ net.ladenthin.srcmorph.config.AiGenerationConfig
+ net.ladenthin.srcmorph.config.AiRangeCondition
+ net.ladenthin.srcmorph.config.AiModelDefinition
+ net.ladenthin.srcmorph.config.AiModelDefinitionSupport
+ net.ladenthin.srcmorph.indexer.AiCalibrationMeasurement
+ net.ladenthin.srcmorph.indexer.AiInputWindowCalculator
+ net.ladenthin.srcmorph.document.AiGenerationRequest
+ net.ladenthin.srcmorph.document.AiGenerationResult
+ net.ladenthin.srcmorph.document.AiMdChildEntryLineFormatter
+ net.ladenthin.srcmorph.document.AiMdDocument
+ net.ladenthin.srcmorph.document.AiMdHeader
+ net.ladenthin.srcmorph.document.AiMdHeaderSupport
+ net.ladenthin.srcmorph.document.AiMdLeadExtractor
+ net.ladenthin.srcmorph.prompt.AiPreparedPrompt
+ net.ladenthin.srcmorph.prompt.AiPromptDefinition
+ net.ladenthin.srcmorph.prompt.AiPromptPreparationSupport
+ net.ladenthin.srcmorph.prompt.AiPromptSupport
+ net.ladenthin.srcmorph.provider.AiCompletionParser
+ net.ladenthin.srcmorph.provider.AiGenerationTimings
+ net.ladenthin.srcmorph.provider.AiGenerationProvider
+ net.ladenthin.srcmorph.provider.AiGenerationProviderFactory
+ net.ladenthin.srcmorph.provider.MockAiGenerationProvider
+ net.ladenthin.srcmorph.config.AiOversizeStrategy
+ net.ladenthin.srcmorph.config.AiFactCounter
+ net.ladenthin.srcmorph.config.AiFactDefinition
+ net.ladenthin.srcmorph.config.AiFactDefinitionSupport
+ net.ladenthin.srcmorph.config.AiFactExtractor
+ net.ladenthin.srcmorph.support.AiChecksumSupport
+ net.ladenthin.srcmorph.support.AiDeterministicSummary
+ net.ladenthin.srcmorph.support.AiGenerationTimeEstimator
+ net.ladenthin.srcmorph.support.AiPathSupport
+ net.ladenthin.srcmorph.support.AiProgressBar
+ net.ladenthin.srcmorph.support.AiSourceExcludeFilter
+ net.ladenthin.srcmorph.support.AiTimeSupport
+ net.ladenthin.srcmorph.support.Java8CompatibilityHelper
+ net.ladenthin.srcmorph.config.SrcMorphConfiguration
+ net.ladenthin.srcmorph.provider.LlamaCppJniConfigFactory
+ net.ladenthin.srcmorph.engine.EngineSupport
+ net.ladenthin.srcmorph.engine.GenerateResult
+ net.ladenthin.srcmorph.engine.CalibrationReport
+ net.ladenthin.srcmorph.engine.CalibrationReport$ModelMeasurement
+
+
+ net.ladenthin.srcmorph.*
+
+ 100
+ 30000
+
+
+
+
+
+
+
+
+ gpu-cuda
+
+ cuda13-windows-x86-64
+
+
+
+ gpu-vulkan
+
+ vulkan-windows-x86-64
+
+
+
+
+
+
diff --git a/llamacpp-ai-index-maven-plugin/spotbugs-exclude.xml b/srcmorph/spotbugs-exclude.xml
similarity index 54%
rename from llamacpp-ai-index-maven-plugin/spotbugs-exclude.xml
rename to srcmorph/spotbugs-exclude.xml
index 8d41c61..4d92b35 100644
--- a/llamacpp-ai-index-maven-plugin/spotbugs-exclude.xml
+++ b/srcmorph/spotbugs-exclude.xml
@@ -9,129 +9,17 @@ SPDX-License-Identifier: Apache-2.0
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubusercontent.com/spotbugs/spotbugs/4.8.6/spotbugs/etc/findbugsfilter.xsd">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
@@ -166,7 +54,7 @@ SPDX-License-Identifier: Apache-2.0
Identity is the correct semantics for a stateful service object.
-->
-
+
@@ -187,7 +75,7 @@ SPDX-License-Identifier: Apache-2.0
semantics for this helper, matching AiModelDefinitionSupport above.
-->
-
+
@@ -211,7 +99,9 @@ SPDX-License-Identifier: Apache-2.0
equals / hashCode / canEqual / toString matches every method name Lombok can
emit. The collateral cost is small: any handwritten member of those four names
that genuinely stores-then-immediately-returns is either a debugger-friendly
- local-variable pattern or a micro-optimisation, both intentional here.
+ local-variable pattern or a micro-optimisation, both intentional here. (Duplicated
+ verbatim in the llamacpp-ai-index-maven-plugin module's spotbugs-exclude.xml since
+ this rule is not class-scoped and both modules have Lombok-annotated classes.)
-->
@@ -242,76 +132,46 @@ SPDX-License-Identifier: Apache-2.0
-->
-
-
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
+
+
+
-
+
@@ -325,7 +185,7 @@ SPDX-License-Identifier: Apache-2.0
message-based detection deliberately avoids. Scoped to the one retry method.
-->
-
+
@@ -338,7 +198,7 @@ SPDX-License-Identifier: Apache-2.0
fallback act), not NPE. Scoped to the one method.
-->
-
+
@@ -350,23 +210,10 @@ SPDX-License-Identifier: Apache-2.0
rationale as the AiGenerationTimeEstimator / AiProgressBar OPM suppressions.
-->
-
+
-
-
-
-
-
-
-
-
-
+
+
@@ -407,15 +254,23 @@ SPDX-License-Identifier: Apache-2.0
/ unmatched collections and Entry.rule reference the same single-threaded, throwaway data; the
Entry.rule is the shared routing rule (an AiFieldGenerationConfig) and is intentionally aliased,
not copied. No untrusted caller can mutate shared state through these accessors.
+
+ SrcMorphConfiguration (migration step 5, engine extraction) is the same kind of bean, one tier
+ up: the mutable JavaBean bindable from Maven plexus reflection, a future Jackson ObjectMapper, or
+ plain Java code, holding every List field (subtrees/excludes/fileExtensions/promptDefinitions/
+ aiDefinitions/fieldGenerations/factDefinitions) the same way the mojo's own @Parameter fields used
+ to. One instance is built once per goal invocation and handed to exactly one engine, which reads
+ it and discards it - the same single-threaded, no-shared-state lifecycle as the beans above.
-->
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -423,6 +278,23 @@ SPDX-License-Identifier: Apache-2.0
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
@@ -448,10 +320,10 @@ SPDX-License-Identifier: Apache-2.0
IMC_IMMATURE_CLASS_NO_TOSTRING on AiIndexPlan: it is an internal DTO whose human-readable
form is the purpose-built renderMarkdown(...) (a structured Markdown plan), not a debug
toString. A generated toString over its nested maps would duplicate that intent and never
- be called. Matches the HelpMojo IMC suppression rationale above.
+ be called.
-->
-
+
@@ -462,7 +334,7 @@ SPDX-License-Identifier: Apache-2.0
choice and has no runtime effect.
-->
-
+
@@ -482,23 +354,48 @@ SPDX-License-Identifier: Apache-2.0
exercised directly by unit tests.
- UCPM_USE_CHARACTER_PARAMETERIZED_METHOD: renderMarkdown appends short literal strings
for the Markdown template; spelling them as String literals keeps the template readable.
- - WEM_WEAK_EXCEPTION_MESSAGING: the AiConditionEvaluator.validate and GenerateMojo.execute
- messages describe a POM configuration error in operator terms; they intentionally read as
- user-facing config diagnostics rather than carrying internal state.
+ - WEM_WEAK_EXCEPTION_MESSAGING: AiConditionEvaluator.validate's message describes a POM
+ configuration error in operator terms; it intentionally reads as a user-facing config
+ diagnostic rather than carrying internal state (the matching GenerateMojo/CalibrateMojo
+ WEM suppression lives in the llamacpp-ai-index-maven-plugin module's own filter file).
- RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE: arises from the Objects.requireNonNull /
- @NullMarked validation style (see the DCN rationale above); the explicit guards are the
- deliberate fail-fast contract, not dead checks.
+ @NullMarked validation style (see the DCN rationale in the plugin module's filter file);
+ the explicit guards are the deliberate fail-fast contract, not dead checks.
+
+ Migration step 5 (engine extraction) additions, same rationale class:
+ - LlamaCppJniConfigFactory.fromGenerationConfig: the pure field-by-field translation from
+ AiGenerationConfig to LlamaCppJniConfig (moved verbatim out of the old
+ AbstractAiIndexMojo.buildLlamaCppJniConfig) necessarily touches the other type's getters
+ heavily (CE_CLASS_ENVY) and its null-guard on drySequenceBreakers mirrors the same
+ Objects.requireNonNull-adjacent style (RCN).
+ - CalibrateEngine.execute(): the rule.getAiDefinitionKey()/getPromptKey() null checks building
+ the routable-model map are the same deliberate defensive guard style as
+ AiConditionEvaluator.validate (RCN); its thrown messages are user-facing config diagnostics,
+ not internal state (WEM) - moved verbatim out of the old CalibrateMojo.
+ - GenerateEngine.resolveSharedFacts/execute(): resolveSharedFacts's fieldGenerations parameter
+ is typed to the concrete List the caller already has (OCP), and execute()'s thrown messages
+ are the same user-facing config diagnostics (WEM) - moved verbatim out of the old
+ GenerateMojo.
+ - CalibrationReport's constructor parameter is typed List (not Collection) because callers
+ always have an ordered List of measurements and the class explicitly preserves that order
+ (OCP_OVERLY_CONCRETE_COLLECTION_PARAMETER).
+ - EngineSupport.resolveLlamaCppJniConfig(config, modelDefinitionSupport)'s fallback branch
+ throws NullPointerException("llamaModelPath") with a static message when
+ SrcMorphConfiguration#getLlamaModelPath() is unset - the same user-facing "which field is
+ missing" diagnostic style as everywhere else in this list (WEM).
-->
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
@@ -520,14 +417,18 @@ SPDX-License-Identifier: Apache-2.0
case-insensitively is the intended, safe behaviour. Scoped to the one parsing method.
-->
-
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/srcmorph/src/main/java/module-info.java b/srcmorph/src/main/java/module-info.java
new file mode 100644
index 0000000..e66c833
--- /dev/null
+++ b/srcmorph/src/main/java/module-info.java
@@ -0,0 +1,62 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+
+/**
+ * JPMS module descriptor for the srcmorph core library.
+ *
+ *
The module exports the seven framework-free packages extracted from the
+ * llamacpp-ai-index-maven-plugin's non-mojo layers: {@code config}, {@code document},
+ * {@code engine}, {@code indexer}, {@code prompt}, {@code provider}, and {@code support}. These
+ * hold the routing/configuration model (incl. the shared root {@code SrcMorphConfiguration}), the
+ * {@code .ai.md} document codec, the per-phase orchestration engines, the file/package/project
+ * indexers, the prompt templating support, the AI generation provider abstraction (incl. the
+ * llama.cpp JNI binding), and the shared stateless helpers. The Maven plugin module
+ * ({@code net.ladenthin.maven.srcmorph}, in its own reactor module {@code srcmorph-maven-plugin})
+ * depends on this module and supplies only the {@code mojo} entry points, which now delegate to the
+ * {@code engine} package.
+ *
+ *
JSpecify {@code @NullMarked} is declared at the module level here so that no source
+ * file compiled at {@code --release 8} references the JSpecify annotation type directly.
+ * Otherwise javac would emit an unsuppressible {@code unknown enum constant
+ * ElementType.MODULE} classfile-read warning for each source compiled at release 8 that
+ * resolves {@code @NullMarked} ({@code @NullMarked} carries
+ * {@code @Target({MODULE, PACKAGE, TYPE})} and Java 8 does not know about
+ * {@code ElementType.MODULE}). Confining the reference to {@code module-info.java} —
+ * which compiles at {@code --release 9} — keeps that warning out of the build entirely.
+ *
+ *
{@code requires static org.jspecify} is needed only at compile time of this
+ * descriptor; JSpecify annotations carry {@code RetentionPolicy.CLASS} so module-path
+ * consumers never need jspecify on their runtime path. Checker Framework qualifiers are
+ * likewise compile-time only. The classes imported from {@code net.ladenthin:llama} (used by
+ * the {@code provider} package) are referenced from ordinary source files only; javac in the
+ * separate {@code module-info-compile} execution compiles {@code module-info.java} in
+ * isolation and therefore does not need their module name. Maven, in turn, loads any consumer
+ * of this jar classpath-only and ignores the descriptor at runtime.
+ *
+ *
This descriptor compiles at {@code --release 9}; the rest of the source compiles
+ * at {@code --release 8}. Java 8 runtimes silently ignore {@code module-info.class} at
+ * the JAR root.
+ */
+@org.jspecify.annotations.NullMarked
+module net.ladenthin.srcmorph {
+ requires static org.jspecify;
+
+ // Lombok is `provided` scope: only used at compile time to generate equals/hashCode/toString.
+ // `requires static` means the runtime does not need the lombok jar on the module path —
+ // the @lombok.Generated annotation carried on generated members has CLASS retention.
+ requires static lombok;
+
+ // indexer/ and engine/ classes log via SLF4J at runtime (not compile-time-only, unlike the
+ // annotation-only requires above), so this must be a real (non-static) requires: a
+ // module-path consumer needs org.slf4j resolvable at runtime too, not just here.
+ requires org.slf4j;
+
+ exports net.ladenthin.srcmorph.config;
+ exports net.ladenthin.srcmorph.document;
+ exports net.ladenthin.srcmorph.engine;
+ exports net.ladenthin.srcmorph.indexer;
+ exports net.ladenthin.srcmorph.prompt;
+ exports net.ladenthin.srcmorph.provider;
+ exports net.ladenthin.srcmorph.support;
+}
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiCalibration.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiCalibration.java
similarity index 97%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiCalibration.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiCalibration.java
index 5e49ea2..b165602 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiCalibration.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiCalibration.java
@@ -1,12 +1,12 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import lombok.ToString;
/**
- * Per-model, per-machine timing calibration measured by the {@code ai-index:calibrate} goal and pasted
+ * Per-model, per-machine timing calibration measured by the {@code srcmorph:calibrate} goal and pasted
* onto an {@code } via {@code }. It makes the plan's time estimate reflect the
* actual hardware (GPU/CPU, quant, context) instead of the built-in reference-CPU coefficients.
*
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiCondition.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiCondition.java
similarity index 99%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiCondition.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiCondition.java
index 7addf3b..21b0559 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiCondition.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiCondition.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import java.util.ArrayList;
import java.util.Collection;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiConditionEvaluator.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiConditionEvaluator.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiConditionEvaluator.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiConditionEvaluator.java
index c9ba67d..6eb9cb4 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiConditionEvaluator.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiConditionEvaluator.java
@@ -1,13 +1,13 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.Collections;
import java.util.List;
-import net.ladenthin.maven.llamacpp.aiindex.support.AiSourceExcludeFilter;
+import net.ladenthin.srcmorph.support.AiSourceExcludeFilter;
import org.jspecify.annotations.Nullable;
/**
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiConditionGroup.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiConditionGroup.java
similarity index 96%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiConditionGroup.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiConditionGroup.java
index 2bf337b..548adfb 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiConditionGroup.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiConditionGroup.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import java.util.ArrayList;
import java.util.Collection;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactCounter.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiFactCounter.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactCounter.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiFactCounter.java
index 0741395..1d4149c 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactCounter.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiFactCounter.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import lombok.ToString;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactDefinition.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiFactDefinition.java
similarity index 97%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactDefinition.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiFactDefinition.java
index 86b790d..d20face 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactDefinition.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiFactDefinition.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import java.util.List;
import lombok.ToString;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactDefinitionSupport.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiFactDefinitionSupport.java
similarity index 91%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactDefinitionSupport.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiFactDefinitionSupport.java
index 3178fa2..5c23103 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactDefinitionSupport.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiFactDefinitionSupport.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import java.util.Collections;
import java.util.HashMap;
@@ -9,7 +9,7 @@
import java.util.Map;
import java.util.Objects;
import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.support.Java8CompatibilityHelper;
+import net.ladenthin.srcmorph.support.Java8CompatibilityHelper;
import org.jspecify.annotations.Nullable;
/**
@@ -23,9 +23,10 @@
* already-resolved {@code getFacts()} uniformly, whether the counters were inline or shared.
*
*
Every entry must have a non-null {@code key} (a null key throws {@link NullPointerException} naming
- * the index); a {@code factsKey} that matches no group throws {@link IllegalArgumentException}. Mojos
- * wrap these in {@link org.apache.maven.plugin.MojoExecutionException} so a misconfigured POM fails fast
- * as a user error.
+ * the index); a {@code factsKey} that matches no group throws {@link IllegalArgumentException}. Callers
+ * (e.g. the llamacpp-ai-index-maven-plugin module's Mojos) wrap these in
+ * {@code org.apache.maven.plugin.MojoExecutionException} so a misconfigured POM fails fast as a user
+ * error. This library itself has no Maven dependency, so that type is referenced here only in prose.
*
* @see AiFactDefinition
* @see AiFieldGenerationConfig#getFactsKey()
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactExtractor.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiFactExtractor.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactExtractor.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiFactExtractor.java
index 95f714e..eb45fa3 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactExtractor.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiFactExtractor.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import java.util.List;
import java.util.regex.Matcher;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFieldGenerationConfig.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiFieldGenerationConfig.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFieldGenerationConfig.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiFieldGenerationConfig.java
index b45baef..8fef70a 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFieldGenerationConfig.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiFieldGenerationConfig.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import java.util.List;
import lombok.ToString;
@@ -140,7 +140,7 @@ public String getPromptKey() {
/**
* Sets the prompt template key.
*
- * @param promptKey key that references an {@link net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptDefinition}
+ * @param promptKey key that references an {@link net.ladenthin.srcmorph.prompt.AiPromptDefinition}
*/
public void setPromptKey(final String promptKey) {
this.promptKey = promptKey;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFieldGenerationSelector.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiFieldGenerationSelector.java
similarity index 99%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFieldGenerationSelector.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiFieldGenerationSelector.java
index 059df68..8452f8e 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFieldGenerationSelector.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiFieldGenerationSelector.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import lombok.ToString;
import org.jspecify.annotations.Nullable;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFileContext.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiFileContext.java
similarity index 97%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFileContext.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiFileContext.java
index b0177c3..79a5656 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFileContext.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiFileContext.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import lombok.ToString;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiGenerationConfig.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiGenerationConfig.java
similarity index 99%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiGenerationConfig.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiGenerationConfig.java
index fbd9b1f..d9d392e 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiGenerationConfig.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiGenerationConfig.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import java.util.ArrayList;
import java.util.Collections;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiGenerationKind.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiGenerationKind.java
similarity index 89%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiGenerationKind.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiGenerationKind.java
index c10c43a..e5252c2 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiGenerationKind.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiGenerationKind.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
/** Identifies whether an AI generation operates on a single source file or a whole package. */
public enum AiGenerationKind {
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiModelDefinition.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiModelDefinition.java
similarity index 99%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiModelDefinition.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiModelDefinition.java
index fbba4df..8ace934 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiModelDefinition.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiModelDefinition.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import java.util.ArrayList;
import java.util.Collection;
@@ -67,7 +67,7 @@ public AiModelDefinition() {
private @Nullable List drySequenceBreakers;
private @Nullable List stopStrings;
- /** Optional per-machine timing calibration ({@code }), measured by {@code ai-index:calibrate}. */
+ /** Optional per-machine timing calibration ({@code }), measured by {@code srcmorph:calibrate}. */
private @Nullable AiCalibration calibration;
/**
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiModelDefinitionSupport.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiModelDefinitionSupport.java
similarity index 91%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiModelDefinitionSupport.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiModelDefinitionSupport.java
index 980d375..9d4ceb3 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiModelDefinitionSupport.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiModelDefinitionSupport.java
@@ -1,14 +1,15 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.support.Java8CompatibilityHelper;
+import net.ladenthin.srcmorph.support.Java8CompatibilityHelper;
+import org.jspecify.annotations.Nullable;
/**
* Resolves {@link AiModelDefinition} entries by their key, returning the corresponding
@@ -25,9 +26,11 @@
* entry. This is the contract enforcement boundary that makes misconfigured POM
* {@code } fail at build configuration time rather than silently
* dropping the entry and surfacing later as a "Missing AI model definition" failure
- * deeper in the goal. Mojos wrap construction in {@link NullPointerException}
- * → {@link org.apache.maven.plugin.MojoExecutionException} so the Maven
- * framework reports it as a user configuration error rather than a plugin bug.
+ * deeper in the goal. Callers (e.g. the llamacpp-ai-index-maven-plugin module's Mojos) wrap
+ * construction in {@link NullPointerException}
+ * → {@code org.apache.maven.plugin.MojoExecutionException} so the Maven
+ * framework reports it as a user configuration error rather than a plugin bug. This library
+ * itself has no Maven dependency, so that type is referenced here only in prose.
*
*
Lookups for missing keys throw {@link IllegalArgumentException} so that
* configuration errors are detected eagerly at runtime.
@@ -59,7 +62,7 @@ public final class AiModelDefinitionSupport {
* well-formed
* @throws NullPointerException if any entry has a {@code null} {@code key}
*/
- public AiModelDefinitionSupport(final List definitions) {
+ public AiModelDefinitionSupport(final @Nullable List definitions) {
if (definitions == null) {
this.configs = new HashMap<>(compatibilityHelper.hashMapCapacityFor(0));
return;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiOversizeStrategy.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiOversizeStrategy.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiOversizeStrategy.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiOversizeStrategy.java
index f20d728..eae9c7b 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiOversizeStrategy.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiOversizeStrategy.java
@@ -3,7 +3,7 @@
//
// SPDX-License-Identifier: Apache-2.0
// @formatter:on
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import org.jspecify.annotations.Nullable;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiRangeCondition.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiRangeCondition.java
similarity index 97%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiRangeCondition.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiRangeCondition.java
index 6da350a..794a6cc 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/AiRangeCondition.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/AiRangeCondition.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import lombok.ToString;
diff --git a/srcmorph/src/main/java/net/ladenthin/srcmorph/config/SrcMorphConfiguration.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/SrcMorphConfiguration.java
new file mode 100644
index 0000000..122586f
--- /dev/null
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/SrcMorphConfiguration.java
@@ -0,0 +1,506 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.config;
+
+import java.io.File;
+import java.util.List;
+import lombok.ToString;
+import net.ladenthin.srcmorph.prompt.AiPromptDefinition;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Root mutable JavaBean holding every parameter a {@code srcmorph} run needs, independent of how it is
+ * bound: today from Maven {@code @Parameter} fields (the {@code srcmorph-maven-plugin} module's
+ * mojos each build one of these from their own annotated fields), tomorrow from a JSON/YAML config file
+ * (a future CLI) or from plain Java code constructing one directly.
+ *
+ *
Field names intentionally mirror today's Maven {@code @Parameter} field names (e.g.
+ * {@link #outputDirectory}, {@link #fieldGenerations}, {@link #llamaContextSize}) so that a future
+ * JSON/YAML config's keys read the same as the existing plugin XML — this class is the single shared
+ * config object bindable from every surface. See {@code net.ladenthin.maven.srcmorph.mojo}'s
+ * mojo classes (in the sibling plugin module) for the exact provenance of every field.
+ *
+ *
Deliberately excluded: the per-goal {@code skip}/{@code skipFile}/{@code skipPackage}/
+ * {@code skipProject} flags. Those are a Maven lifecycle concern (whether an execution runs at all) and
+ * stay mojo-side; an engine constructed from this configuration always executes when asked.
+ *
+ *
Note: this class must remain a mutable JavaBean with setters so that every binding
+ * technology (Maven plexus reflection, a Jackson {@code ObjectMapper}/{@code YAMLMapper}, or a plain
+ * caller) can populate it the same way.
+ */
+@SuppressWarnings({"NullAway.Init", "initialization.fields.uninitialized"})
+@ToString
+public class SrcMorphConfiguration {
+
+ /** Default output directory (relative to {@link #baseDirectory}) when none is configured. */
+ public static final String DEFAULT_OUTPUT_DIRECTORY = "src/site/ai";
+
+ /** Default AI generation provider name when none is configured. */
+ public static final String DEFAULT_GENERATION_PROVIDER = "mock";
+
+ /** Default llama.cpp context window size when none is configured. */
+ public static final int DEFAULT_LLAMA_CONTEXT_SIZE = 2048;
+
+ /** Default maximum number of llama.cpp output tokens per call when none is configured. */
+ public static final int DEFAULT_LLAMA_MAX_OUTPUT_TOKENS = 128;
+
+ /** Default llama.cpp sampling temperature when none is configured. */
+ public static final float DEFAULT_LLAMA_TEMPERATURE = 0.15f;
+
+ /** Default number of llama.cpp CPU threads when none is configured. */
+ public static final int DEFAULT_LLAMA_THREADS = 2;
+
+ /** Default AI summarisation logic version when none is configured. */
+ public static final String DEFAULT_AI_VERSION = "0.0.0";
+
+ /** Creates a new {@link SrcMorphConfiguration} with every default applied. */
+ public SrcMorphConfiguration() {
+ // no-op
+ }
+
+ private File baseDirectory;
+ private File outputDirectory = new File(DEFAULT_OUTPUT_DIRECTORY);
+ private boolean force;
+ private @Nullable List subtrees;
+ private @Nullable List excludes;
+ private @Nullable List fileExtensions;
+ private long minFileSizeBytes;
+ private long maxFileSizeBytes;
+ private boolean planOnly;
+ private String generationProvider = DEFAULT_GENERATION_PROVIDER;
+ private @Nullable List promptDefinitions;
+ private @Nullable List aiDefinitions;
+ private @Nullable List fieldGenerations;
+ private @Nullable List factDefinitions;
+ private @Nullable String llamaLibraryPath;
+ private @Nullable String llamaModelPath;
+ private int llamaContextSize = DEFAULT_LLAMA_CONTEXT_SIZE;
+ private int llamaMaxOutputTokens = DEFAULT_LLAMA_MAX_OUTPUT_TOKENS;
+ private float llamaTemperature = DEFAULT_LLAMA_TEMPERATURE;
+ private int llamaThreads = DEFAULT_LLAMA_THREADS;
+ private String pluginVersion = "";
+ private String aiVersion = DEFAULT_AI_VERSION;
+ private @Nullable String projectName;
+
+ /**
+ * Returns the project base directory.
+ *
+ * @return the base directory
+ */
+ public File getBaseDirectory() {
+ return baseDirectory;
+ }
+
+ /**
+ * Sets the project base directory.
+ *
+ * @param baseDirectory the base directory
+ */
+ public void setBaseDirectory(final File baseDirectory) {
+ this.baseDirectory = baseDirectory;
+ }
+
+ /**
+ * Returns the directory into which {@code .ai.md} files are written.
+ *
+ * @return the output directory; defaults to {@link #DEFAULT_OUTPUT_DIRECTORY}
+ */
+ public File getOutputDirectory() {
+ return outputDirectory;
+ }
+
+ /**
+ * Sets the directory into which {@code .ai.md} files are written.
+ *
+ * @param outputDirectory the output directory
+ */
+ public void setOutputDirectory(final File outputDirectory) {
+ this.outputDirectory = outputDirectory;
+ }
+
+ /**
+ * Returns whether AI fields are regenerated even when they already have a value.
+ *
+ * @return {@code true} to always regenerate
+ */
+ public boolean isForce() {
+ return force;
+ }
+
+ /**
+ * Sets whether AI fields are regenerated even when they already have a value.
+ *
+ * @param force {@code true} to always regenerate
+ */
+ public void setForce(final boolean force) {
+ this.force = force;
+ }
+
+ /**
+ * Returns the source subdirectory paths (relative to {@link #baseDirectory}) that restrict processing.
+ *
+ * @return the configured subtrees, or {@code null} when every discovered source root is in scope
+ */
+ public @Nullable List getSubtrees() {
+ return subtrees;
+ }
+
+ /**
+ * Sets the source subdirectory paths that restrict processing.
+ *
+ * @param subtrees the subtrees, or {@code null} for every discovered source root
+ */
+ public void setSubtrees(final @Nullable List subtrees) {
+ this.subtrees = subtrees;
+ }
+
+ /**
+ * Returns the glob patterns (base-relative, {@code /} separators) for source files to skip.
+ *
+ * @return the configured excludes, or {@code null} when nothing is excluded
+ */
+ public @Nullable List getExcludes() {
+ return excludes;
+ }
+
+ /**
+ * Sets the glob patterns for source files to skip.
+ *
+ * @param excludes the exclude globs, or {@code null} for none
+ */
+ public void setExcludes(final @Nullable List excludes) {
+ this.excludes = excludes;
+ }
+
+ /**
+ * Returns the file extensions to index.
+ *
+ * @return the configured extensions, or {@code null} to use the engine's own default (typically
+ * {@code .java})
+ */
+ public @Nullable List getFileExtensions() {
+ return fileExtensions;
+ }
+
+ /**
+ * Sets the file extensions to index.
+ *
+ * @param fileExtensions the extensions, or {@code null} to use the engine's own default
+ */
+ public void setFileExtensions(final @Nullable List fileExtensions) {
+ this.fileExtensions = fileExtensions;
+ }
+
+ /**
+ * Returns the exclusive lower file-size bound in bytes.
+ *
+ * @return the lower bound; {@code <= 0} disables it
+ */
+ public long getMinFileSizeBytes() {
+ return minFileSizeBytes;
+ }
+
+ /**
+ * Sets the exclusive lower file-size bound in bytes.
+ *
+ * @param minFileSizeBytes the lower bound; {@code <= 0} disables it
+ */
+ public void setMinFileSizeBytes(final long minFileSizeBytes) {
+ this.minFileSizeBytes = minFileSizeBytes;
+ }
+
+ /**
+ * Returns the inclusive upper file-size bound in bytes.
+ *
+ * @return the upper bound; {@code <= 0} means unlimited
+ */
+ public long getMaxFileSizeBytes() {
+ return maxFileSizeBytes;
+ }
+
+ /**
+ * Sets the inclusive upper file-size bound in bytes.
+ *
+ * @param maxFileSizeBytes the upper bound; {@code <= 0} means unlimited
+ */
+ public void setMaxFileSizeBytes(final long maxFileSizeBytes) {
+ this.maxFileSizeBytes = maxFileSizeBytes;
+ }
+
+ /**
+ * Returns whether a {@code generate} run stops after printing the routing plan, without loading any
+ * model or generating anything.
+ *
+ * @return {@code true} to stop after planning
+ */
+ public boolean isPlanOnly() {
+ return planOnly;
+ }
+
+ /**
+ * Sets whether a {@code generate} run stops after printing the routing plan.
+ *
+ * @param planOnly {@code true} to stop after planning
+ */
+ public void setPlanOnly(final boolean planOnly) {
+ this.planOnly = planOnly;
+ }
+
+ /**
+ * Returns the name of the AI generation provider to use.
+ *
+ * @return the provider name ({@code mock} or {@code llamacpp-jni}); defaults to
+ * {@link #DEFAULT_GENERATION_PROVIDER}
+ */
+ public String getGenerationProvider() {
+ return generationProvider;
+ }
+
+ /**
+ * Sets the name of the AI generation provider to use.
+ *
+ * @param generationProvider the provider name ({@code mock} or {@code llamacpp-jni})
+ */
+ public void setGenerationProvider(final String generationProvider) {
+ this.generationProvider = generationProvider;
+ }
+
+ /**
+ * Returns the prompt template definitions referenced by field generation configurations.
+ *
+ * @return the prompt definitions, or {@code null} when none are configured
+ */
+ public @Nullable List getPromptDefinitions() {
+ return promptDefinitions;
+ }
+
+ /**
+ * Sets the prompt template definitions.
+ *
+ * @param promptDefinitions the prompt definitions, or {@code null} for none
+ */
+ public void setPromptDefinitions(final @Nullable List promptDefinitions) {
+ this.promptDefinitions = promptDefinitions;
+ }
+
+ /**
+ * Returns the AI model definitions that pair a lookup key with a complete set of model parameters.
+ *
+ * @return the AI model definitions, or {@code null} when none are configured
+ */
+ public @Nullable List getAiDefinitions() {
+ return aiDefinitions;
+ }
+
+ /**
+ * Sets the AI model definitions.
+ *
+ * @param aiDefinitions the AI model definitions, or {@code null} for none
+ */
+ public void setAiDefinitions(final @Nullable List aiDefinitions) {
+ this.aiDefinitions = aiDefinitions;
+ }
+
+ /**
+ * Returns the per-field AI generation configurations (routing rules) controlling which prompt and AI
+ * definition each matched file uses.
+ *
+ * @return the field generation rules, or {@code null} when none are configured
+ */
+ public @Nullable List getFieldGenerations() {
+ return fieldGenerations;
+ }
+
+ /**
+ * Sets the per-field AI generation configurations (routing rules).
+ *
+ * @param fieldGenerations the field generation rules, or {@code null} for none
+ */
+ public void setFieldGenerations(final @Nullable List fieldGenerations) {
+ this.fieldGenerations = fieldGenerations;
+ }
+
+ /**
+ * Returns the reusable, named {@code } groups referenced from a rule's
+ * {@code factsKey}.
+ *
+ * @return the fact definitions, or {@code null} when none are configured
+ */
+ public @Nullable List getFactDefinitions() {
+ return factDefinitions;
+ }
+
+ /**
+ * Sets the reusable, named {@code } groups.
+ *
+ * @param factDefinitions the fact definitions, or {@code null} for none
+ */
+ public void setFactDefinitions(final @Nullable List factDefinitions) {
+ this.factDefinitions = factDefinitions;
+ }
+
+ /**
+ * Returns the optional native library path passed to the llama.cpp JNI provider.
+ *
+ * @return the native library path, or {@code null} to use the bundled native library
+ */
+ public @Nullable String getLlamaLibraryPath() {
+ return llamaLibraryPath;
+ }
+
+ /**
+ * Sets the optional native library path passed to the llama.cpp JNI provider.
+ *
+ * @param llamaLibraryPath the native library path, or {@code null} to use the bundled library
+ */
+ public void setLlamaLibraryPath(final @Nullable String llamaLibraryPath) {
+ this.llamaLibraryPath = llamaLibraryPath;
+ }
+
+ /**
+ * Returns the path to the GGUF model file used as the llama.cpp JNI provider fallback (only used
+ * when {@link #fieldGenerations} is empty).
+ *
+ * @return the model path, or {@code null} if not set
+ */
+ public @Nullable String getLlamaModelPath() {
+ return llamaModelPath;
+ }
+
+ /**
+ * Sets the path to the GGUF model file used as the llama.cpp JNI provider fallback.
+ *
+ * @param llamaModelPath the model path
+ */
+ public void setLlamaModelPath(final @Nullable String llamaModelPath) {
+ this.llamaModelPath = llamaModelPath;
+ }
+
+ /**
+ * Returns the llama.cpp context window size used as the fallback (only used when
+ * {@link #fieldGenerations} is empty).
+ *
+ * @return the context window size; defaults to {@link #DEFAULT_LLAMA_CONTEXT_SIZE}
+ */
+ public int getLlamaContextSize() {
+ return llamaContextSize;
+ }
+
+ /**
+ * Sets the llama.cpp context window size used as the fallback.
+ *
+ * @param llamaContextSize the context window size
+ */
+ public void setLlamaContextSize(final int llamaContextSize) {
+ this.llamaContextSize = llamaContextSize;
+ }
+
+ /**
+ * Returns the maximum number of llama.cpp output tokens per call, used as the fallback.
+ *
+ * @return the maximum output tokens; defaults to {@link #DEFAULT_LLAMA_MAX_OUTPUT_TOKENS}
+ */
+ public int getLlamaMaxOutputTokens() {
+ return llamaMaxOutputTokens;
+ }
+
+ /**
+ * Sets the maximum number of llama.cpp output tokens per call, used as the fallback.
+ *
+ * @param llamaMaxOutputTokens the maximum output tokens
+ */
+ public void setLlamaMaxOutputTokens(final int llamaMaxOutputTokens) {
+ this.llamaMaxOutputTokens = llamaMaxOutputTokens;
+ }
+
+ /**
+ * Returns the llama.cpp sampling temperature, used as the fallback.
+ *
+ * @return the sampling temperature; defaults to {@link #DEFAULT_LLAMA_TEMPERATURE}
+ */
+ public float getLlamaTemperature() {
+ return llamaTemperature;
+ }
+
+ /**
+ * Sets the llama.cpp sampling temperature, used as the fallback.
+ *
+ * @param llamaTemperature the sampling temperature
+ */
+ public void setLlamaTemperature(final float llamaTemperature) {
+ this.llamaTemperature = llamaTemperature;
+ }
+
+ /**
+ * Returns the number of CPU threads for llama.cpp inference, used as the fallback.
+ *
+ * @return the thread count; defaults to {@link #DEFAULT_LLAMA_THREADS}
+ */
+ public int getLlamaThreads() {
+ return llamaThreads;
+ }
+
+ /**
+ * Sets the number of CPU threads for llama.cpp inference, used as the fallback.
+ *
+ * @param llamaThreads the thread count
+ */
+ public void setLlamaThreads(final int llamaThreads) {
+ this.llamaThreads = llamaThreads;
+ }
+
+ /**
+ * Returns the generator (plugin) version recorded in every written {@code .ai.md} header.
+ *
+ * @return the generator version
+ */
+ public String getPluginVersion() {
+ return pluginVersion;
+ }
+
+ /**
+ * Sets the generator (plugin) version recorded in every written {@code .ai.md} header.
+ *
+ * @param pluginVersion the generator version
+ */
+ public void setPluginVersion(final String pluginVersion) {
+ this.pluginVersion = pluginVersion;
+ }
+
+ /**
+ * Returns the AI summarisation logic version recorded in every written {@code .ai.md} header.
+ *
+ * @return the AI version; defaults to {@link #DEFAULT_AI_VERSION}
+ */
+ public String getAiVersion() {
+ return aiVersion;
+ }
+
+ /**
+ * Sets the AI summarisation logic version recorded in every written {@code .ai.md} header.
+ *
+ * @param aiVersion the AI version
+ */
+ public void setAiVersion(final String aiVersion) {
+ this.aiVersion = aiVersion;
+ }
+
+ /**
+ * Returns the project title recorded as the {@code aggregate-project} index title.
+ *
+ * @return the project name, or {@code null} to fall back to the engine's own default title
+ */
+ public @Nullable String getProjectName() {
+ return projectName;
+ }
+
+ /**
+ * Sets the project title recorded as the {@code aggregate-project} index title.
+ *
+ * @param projectName the project name, or {@code null} to use the engine's own default title
+ */
+ public void setProjectName(final @Nullable String projectName) {
+ this.projectName = projectName;
+ }
+}
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/package-info.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/package-info.java
similarity index 78%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/package-info.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/config/package-info.java
index e30fda8..e749d63 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/config/package-info.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/config/package-info.java
@@ -5,4 +5,4 @@
/**
* Generation configuration, model definitions and their lookups.
*/
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiGenerationRequest.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiGenerationRequest.java
similarity index 85%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiGenerationRequest.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiGenerationRequest.java
index 513fb1a..0e1954c 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiGenerationRequest.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiGenerationRequest.java
@@ -1,20 +1,20 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.document;
+package net.ladenthin.srcmorph.document;
import java.nio.file.Path;
import java.util.Objects;
import lombok.EqualsAndHashCode;
import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.support.ConvertToRecord;
+import net.ladenthin.srcmorph.support.ConvertToRecord;
/**
- * Immutable request object passed to an {@link net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationProvider}: identifies the
+ * Immutable request object passed to an {@link net.ladenthin.srcmorph.provider.AiGenerationProvider}: identifies the
* prompt template, the source file being processed, its current text, and the existing
* header (if any).
*
- *
Record-shaped value type marked {@link net.ladenthin.maven.llamacpp.aiindex.support.ConvertToRecord} for the future Java 17+
+ *
Record-shaped value type marked {@link net.ladenthin.srcmorph.support.ConvertToRecord} for the future Java 17+
* migration. Accessors are record-style. {@code equals}, {@code hashCode} and
* {@code toString} are generated by Lombok over all four fields; full value semantics
* apply.
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiGenerationResult.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiGenerationResult.java
similarity index 80%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiGenerationResult.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiGenerationResult.java
index 3ace2fc..79a6b39 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiGenerationResult.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiGenerationResult.java
@@ -1,21 +1,21 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.document;
+package net.ladenthin.srcmorph.document;
import java.util.Objects;
import lombok.EqualsAndHashCode;
import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.support.ConvertToRecord;
+import net.ladenthin.srcmorph.support.ConvertToRecord;
/**
- * Immutable result produced by {@link net.ladenthin.maven.llamacpp.aiindex.indexer.AiFieldGenerationSupport#processFieldGenerations}.
+ * Immutable result produced by {@link net.ladenthin.srcmorph.indexer.AiFieldGenerationSupport#processFieldGenerations}.
*
*
Carries the AI-generated document body text produced by a single processing pass.
* Defaults to an empty string when no body target is present in the field generation
* configuration.
*
- *
Record-shaped value type marked {@link net.ladenthin.maven.llamacpp.aiindex.support.ConvertToRecord} for the future Java 17+
+ *
Record-shaped value type marked {@link net.ladenthin.srcmorph.support.ConvertToRecord} for the future Java 17+
* migration. The accessor methods follow record-style ({@code body()}, not
* {@code getBody()}) so the migration is mechanical. {@code equals}, {@code hashCode}
* and {@code toString} are generated by Lombok over the single field; full value
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdChildEntryLineFormatter.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiMdChildEntryLineFormatter.java
similarity index 88%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdChildEntryLineFormatter.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiMdChildEntryLineFormatter.java
index 8bb8c97..95203d9 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdChildEntryLineFormatter.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiMdChildEntryLineFormatter.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.document;
+package net.ladenthin.srcmorph.document;
import lombok.ToString;
@@ -15,10 +15,10 @@
* concatenated string as the input to the parent package's CRC32 checksum.
*
*
Kept separate from {@link AiMdHeaderSupport} because the use case is
- * narrow (only {@link net.ladenthin.maven.llamacpp.aiindex.indexer.PackageIndexer} calls this) and unrelated to the
+ * narrow (only {@link net.ladenthin.srcmorph.indexer.PackageIndexer} calls this) and unrelated to the
* rewrite-decision logic that lives on {@code AiMdHeaderSupport}.
*
- * @see net.ladenthin.maven.llamacpp.aiindex.indexer.PackageIndexer
+ * @see net.ladenthin.srcmorph.indexer.PackageIndexer
*/
@ToString
public class AiMdChildEntryLineFormatter {
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdDocument.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiMdDocument.java
similarity index 83%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdDocument.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiMdDocument.java
index 4b8f7cc..9f80495 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdDocument.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiMdDocument.java
@@ -1,17 +1,17 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.document;
+package net.ladenthin.srcmorph.document;
import java.util.Objects;
import lombok.EqualsAndHashCode;
import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.support.ConvertToRecord;
+import net.ladenthin.srcmorph.support.ConvertToRecord;
/**
* Immutable representation of an {@code .ai.md} document consisting of a header and a body.
*
- *
Record-shaped value type marked {@link net.ladenthin.maven.llamacpp.aiindex.support.ConvertToRecord} for the future Java 17+
+ *
Record-shaped value type marked {@link net.ladenthin.srcmorph.support.ConvertToRecord} for the future Java 17+
* migration. Accessors are record-style ({@code header()}, {@code body()}). {@code equals},
* {@code hashCode} and {@code toString} are generated by Lombok over both fields.
*/
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdDocumentCodec.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiMdDocumentCodec.java
similarity index 96%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdDocumentCodec.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiMdDocumentCodec.java
index ab49d47..ec7273f 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdDocumentCodec.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiMdDocumentCodec.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.document;
+package net.ladenthin.srcmorph.document;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -10,7 +10,7 @@
import java.util.ArrayList;
import java.util.List;
import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.support.Java8CompatibilityHelper;
+import net.ladenthin.srcmorph.support.Java8CompatibilityHelper;
/** Reads and writes {@code .ai.md} documents (header plus body) from and to disk. */
@ToString
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdHeader.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiMdHeader.java
similarity index 96%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdHeader.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiMdHeader.java
index e6e2574..52a5901 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdHeader.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiMdHeader.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.document;
+package net.ladenthin.srcmorph.document;
import java.util.ArrayList;
import java.util.Collection;
@@ -10,7 +10,7 @@
import java.util.Objects;
import lombok.EqualsAndHashCode;
import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.support.ConvertToRecord;
+import net.ladenthin.srcmorph.support.ConvertToRecord;
/**
* Canonical header model for a single {@code .ai.md} document.
@@ -55,7 +55,7 @@
*
The generator should preserve the body when the structural state did not change.
*
*
- *
Record-shaped value type marked {@link net.ladenthin.maven.llamacpp.aiindex.support.ConvertToRecord} for the future Java 17+
+ *
Record-shaped value type marked {@link net.ladenthin.srcmorph.support.ConvertToRecord} for the future Java 17+
* migration. Accessors are record-style ({@code title()}, {@code h()}, ...). {@code equals},
* {@code hashCode} and {@code toString} are generated by Lombok across all fields (including
* {@code children}); full value semantics apply.
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdHeaderCodec.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiMdHeaderCodec.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdHeaderCodec.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiMdHeaderCodec.java
index 783c399..6d10c98 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdHeaderCodec.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiMdHeaderCodec.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.document;
+package net.ladenthin.srcmorph.document;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -12,7 +12,7 @@
import java.util.List;
import java.util.Map;
import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.support.Java8CompatibilityHelper;
+import net.ladenthin.srcmorph.support.Java8CompatibilityHelper;
/** Reads and writes the metadata header section of an {@code .ai.md} document. */
@ToString
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdHeaderSupport.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiMdHeaderSupport.java
similarity index 94%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdHeaderSupport.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiMdHeaderSupport.java
index edc5962..561ae2d 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdHeaderSupport.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiMdHeaderSupport.java
@@ -1,13 +1,13 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.document;
+package net.ladenthin.srcmorph.document;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.support.Java8CompatibilityHelper;
+import net.ladenthin.srcmorph.support.Java8CompatibilityHelper;
/** Header comparison helpers that decide whether an {@code .ai.md} file needs to be rewritten. */
@ToString
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdLeadExtractor.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiMdLeadExtractor.java
similarity index 94%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdLeadExtractor.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiMdLeadExtractor.java
index 4352f34..7b20f6c 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdLeadExtractor.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/document/AiMdLeadExtractor.java
@@ -1,10 +1,10 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.document;
+package net.ladenthin.srcmorph.document;
import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.support.Java8CompatibilityHelper;
+import net.ladenthin.srcmorph.support.Java8CompatibilityHelper;
/**
* Extracts the one-line lead from an {@code .ai.md} document body.
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/package-info.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/document/package-info.java
similarity index 76%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/package-info.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/document/package-info.java
index 4f938e7..a28e725 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/document/package-info.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/document/package-info.java
@@ -5,4 +5,4 @@
/**
* The {@code .ai.md} document model and its codecs.
*/
-package net.ladenthin.maven.llamacpp.aiindex.document;
+package net.ladenthin.srcmorph.document;
diff --git a/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/AggregatePackagesEngine.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/AggregatePackagesEngine.java
new file mode 100644
index 0000000..38062e7
--- /dev/null
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/AggregatePackagesEngine.java
@@ -0,0 +1,100 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.engine;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
+import lombok.ToString;
+import net.ladenthin.srcmorph.config.AiModelDefinitionSupport;
+import net.ladenthin.srcmorph.config.SrcMorphConfiguration;
+import net.ladenthin.srcmorph.indexer.PackageIndexer;
+import net.ladenthin.srcmorph.prompt.AiPromptSupport;
+import net.ladenthin.srcmorph.provider.AiGenerationProvider;
+import net.ladenthin.srcmorph.provider.AiGenerationProviderFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Phase 2 orchestration: aggregates per-package {@code .ai.md} index files and fills in their
+ * AI-generated summary fields.
+ *
+ *
Extracted from what was {@code AggregatePackagesMojo.execute()} in the
+ * {@code llamacpp-ai-index-maven-plugin} module.
+ */
+@ToString
+public final class AggregatePackagesEngine {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(AggregatePackagesEngine.class);
+
+ private final SrcMorphConfiguration config;
+
+ /**
+ * Creates a new {@link AggregatePackagesEngine} for the given run configuration.
+ *
+ * @param config the run configuration
+ */
+ public AggregatePackagesEngine(final SrcMorphConfiguration config) {
+ this.config = config;
+ }
+
+ /**
+ * Aggregates every package beneath the configured output directory.
+ *
+ * @return the number of package index files written or refreshed; {@code 0} when the output
+ * directory does not yet exist
+ * @throws SrcMorphException if the prompt/model definitions are misconfigured
+ * @throws IOException if the output tree cannot be read or written
+ */
+ public int execute() throws SrcMorphException, IOException {
+ final Path basePath =
+ config.getBaseDirectory().toPath().toAbsolutePath().normalize();
+ final Path outputPath =
+ config.getOutputDirectory().toPath().toAbsolutePath().normalize();
+ final List resolvedSubtrees = EngineSupport.resolveSubtrees(basePath, config.getSubtrees());
+
+ LOGGER.info("Starting AI package aggregation");
+ LOGGER.info("Base directory : {}", basePath);
+ LOGGER.info("Output directory: {}", outputPath);
+ LOGGER.info("Subtrees : {}", resolvedSubtrees);
+ LOGGER.info("Force : {}", config.isForce());
+ LOGGER.info("Provider : {}", config.getGenerationProvider());
+ LOGGER.info("LlamaCpp Temperature: {}", config.getLlamaTemperature());
+ LOGGER.info("LlamaCpp Max Output Tokens: {}", config.getLlamaMaxOutputTokens());
+
+ if (!outputPath.toFile().exists()) {
+ LOGGER.info("AI output directory does not exist, skipping package aggregation: {}", outputPath);
+ return 0;
+ }
+
+ final AiPromptSupport promptSupport = EngineSupport.buildPromptSupport(config.getPromptDefinitions());
+ final AiModelDefinitionSupport modelDefinitionSupport =
+ EngineSupport.buildAiModelDefinitionSupport(config.getAiDefinitions());
+ final AiGenerationProviderFactory providerFactory = new AiGenerationProviderFactory();
+
+ final int aggregated;
+ try (AiGenerationProvider provider = providerFactory.create(
+ config.getGenerationProvider(),
+ EngineSupport.resolveLlamaCppJniConfig(config, modelDefinitionSupport),
+ promptSupport)) {
+ final PackageIndexer packageIndexer = new PackageIndexer(
+ basePath,
+ outputPath,
+ config.getPluginVersion(),
+ config.getAiVersion(),
+ resolvedSubtrees,
+ config.isForce(),
+ provider,
+ config.getFieldGenerations(),
+ promptSupport,
+ modelDefinitionSupport);
+
+ aggregated = packageIndexer.aggregate(outputPath);
+ }
+
+ LOGGER.info("Aggregated packages: {}", aggregated);
+ LOGGER.info("AI package aggregation finished.");
+ return aggregated;
+ }
+}
diff --git a/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/AggregateProjectEngine.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/AggregateProjectEngine.java
new file mode 100644
index 0000000..8867eb6
--- /dev/null
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/AggregateProjectEngine.java
@@ -0,0 +1,128 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.engine;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
+import lombok.ToString;
+import net.ladenthin.srcmorph.config.AiFieldGenerationConfig;
+import net.ladenthin.srcmorph.config.AiModelDefinitionSupport;
+import net.ladenthin.srcmorph.config.SrcMorphConfiguration;
+import net.ladenthin.srcmorph.indexer.ProjectIndexer;
+import net.ladenthin.srcmorph.prompt.AiPromptSupport;
+import net.ladenthin.srcmorph.provider.AiGenerationProvider;
+import net.ladenthin.srcmorph.provider.AiGenerationProviderFactory;
+import net.ladenthin.srcmorph.support.Java8CompatibilityHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Phase 3 orchestration: aggregates every per-package {@code package.ai.md} into a single project-level
+ * {@code project.ai.md} — the top of the three-level index.
+ *
+ *
Extracted from what was {@code AggregateProjectMojo.execute()} in the
+ * {@code llamacpp-ai-index-maven-plugin} module. By default this is fully deterministic (a per-package
+ * lead/link table of contents, no model call); when
+ * {@link SrcMorphConfiguration#getFieldGenerations()} carries at least one entry, its first entry is used
+ * as the optional AI overview generation (one extra call synthesising a short project overview
+ * paragraph from the per-package leads) — see {@link ProjectIndexer}.
+ */
+@ToString
+public final class AggregateProjectEngine {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(AggregateProjectEngine.class);
+
+ /** Title used for the project index when {@link SrcMorphConfiguration#getProjectName()} is absent or blank. */
+ private static final String DEFAULT_PROJECT_TITLE = "project";
+
+ private final SrcMorphConfiguration config;
+ private final Java8CompatibilityHelper compatibilityHelper = new Java8CompatibilityHelper();
+
+ /**
+ * Creates a new {@link AggregateProjectEngine} for the given run configuration.
+ *
+ * @param config the run configuration
+ */
+ public AggregateProjectEngine(final SrcMorphConfiguration config) {
+ this.config = config;
+ }
+
+ /**
+ * Aggregates the project index beneath the configured output directory.
+ *
+ * @return {@code 1} if the project index was written or refreshed, {@code 0} otherwise (no package
+ * index files yet, the output directory does not exist, or the existing file is up to date)
+ * @throws SrcMorphException if the overview's prompt/model definitions are misconfigured
+ * @throws IOException if the output tree cannot be read or the project index cannot be written
+ */
+ public int execute() throws SrcMorphException, IOException {
+ final Path outputPath =
+ config.getOutputDirectory().toPath().toAbsolutePath().normalize();
+
+ LOGGER.info("Starting AI project index aggregation");
+ LOGGER.info("Output directory: {}", outputPath);
+ LOGGER.info("Force : {}", config.isForce());
+
+ if (!outputPath.toFile().exists()) {
+ LOGGER.info("AI output directory does not exist, skipping project index: {}", outputPath);
+ return 0;
+ }
+
+ final String projectName = config.getProjectName();
+ final String title =
+ projectName == null || compatibilityHelper.isBlank(projectName) ? DEFAULT_PROJECT_TITLE : projectName;
+
+ final int written;
+ final List fieldGenerations = config.getFieldGenerations();
+ if (fieldGenerations != null && !fieldGenerations.isEmpty()) {
+ written = aggregateWithOverview(outputPath, title, fieldGenerations.get(0));
+ } else {
+ LOGGER.info("Project overview generation: disabled (no fieldGenerations configured)");
+ final ProjectIndexer indexer =
+ new ProjectIndexer(title, config.getPluginVersion(), config.getAiVersion(), config.isForce());
+ written = indexer.aggregate(outputPath);
+ }
+ LOGGER.info("Project index files written: {}", written);
+
+ LOGGER.info("AI project index aggregation finished.");
+ return written;
+ }
+
+ /**
+ * Aggregates the project index with the optional AI overview paragraph enabled, generated by the
+ * given field generation. A provider is created (and closed) only on this path, so the deterministic
+ * default never instantiates a model.
+ *
+ * @param outputPath the resolved output directory holding the {@code .ai.md} tree
+ * @param title the resolved project title
+ * @param overview the overview field generation (prompt + model)
+ * @return {@code 1} if the project index was written or refreshed, {@code 0} otherwise
+ * @throws SrcMorphException if the provider or model definitions are misconfigured
+ * @throws IOException if the output tree cannot be read or written
+ */
+ private int aggregateWithOverview(final Path outputPath, final String title, final AiFieldGenerationConfig overview)
+ throws SrcMorphException, IOException {
+ final AiPromptSupport promptSupport = EngineSupport.buildPromptSupport(config.getPromptDefinitions());
+ final AiModelDefinitionSupport modelDefinitionSupport =
+ EngineSupport.buildAiModelDefinitionSupport(config.getAiDefinitions());
+ LOGGER.info("Project overview generation: enabled (prompt '{}')", overview.getPromptKey());
+ final AiGenerationProviderFactory providerFactory = new AiGenerationProviderFactory();
+ try (AiGenerationProvider provider = providerFactory.create(
+ config.getGenerationProvider(),
+ EngineSupport.resolveLlamaCppJniConfig(config, modelDefinitionSupport),
+ promptSupport)) {
+ final ProjectIndexer indexer = new ProjectIndexer(
+ title,
+ config.getPluginVersion(),
+ config.getAiVersion(),
+ config.isForce(),
+ provider,
+ overview,
+ promptSupport,
+ modelDefinitionSupport);
+ return indexer.aggregate(outputPath);
+ }
+ }
+}
diff --git a/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/CalibrateEngine.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/CalibrateEngine.java
new file mode 100644
index 0000000..2fd0ba3
--- /dev/null
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/CalibrateEngine.java
@@ -0,0 +1,157 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.engine;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import lombok.ToString;
+import net.ladenthin.srcmorph.config.AiFieldGenerationConfig;
+import net.ladenthin.srcmorph.config.AiGenerationConfig;
+import net.ladenthin.srcmorph.config.AiModelDefinitionSupport;
+import net.ladenthin.srcmorph.config.SrcMorphConfiguration;
+import net.ladenthin.srcmorph.indexer.AiCalibrationMeasurement;
+import net.ladenthin.srcmorph.indexer.AiCalibrationRunner;
+import net.ladenthin.srcmorph.prompt.AiPromptPreparationSupport;
+import net.ladenthin.srcmorph.prompt.AiPromptSupport;
+import net.ladenthin.srcmorph.provider.AiGenerationProvider;
+import net.ladenthin.srcmorph.provider.AiGenerationProviderFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Preflight + per-machine timing pass that sits between {@code planOnly} (no model loaded) and a real
+ * {@link GenerateEngine} run.
+ *
+ *
Extracted from what was {@code CalibrateMojo.execute()} in the {@code llamacpp-ai-index-maven-plugin}
+ * module. For each distinct model a {@link GenerateEngine} run would load (the {@code aiDefinitionKey}s
+ * referenced by {@link SrcMorphConfiguration#getFieldGenerations()}), loads the model once (catching a
+ * bad path / OOM / wrong native early), runs a couple of representative generations via
+ * {@link AiCalibrationRunner}, and reads the model's own measured prefill/decode throughput. The caller
+ * pastes {@link CalibrationReport#renderXml()} onto the matching {@code } so the plan's time
+ * estimate reflects this machine instead of the built-in reference-CPU coefficients.
+ */
+@ToString
+public final class CalibrateEngine {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(CalibrateEngine.class);
+
+ private final SrcMorphConfiguration config;
+ private final AiCalibrationRunner calibrationRunner = new AiCalibrationRunner();
+
+ /**
+ * Creates a new {@link CalibrateEngine} for the given run configuration.
+ *
+ * @param config the run configuration
+ */
+ public CalibrateEngine(final SrcMorphConfiguration config) {
+ this.config = config;
+ }
+
+ /**
+ * Calibrates every distinct model referenced by the configured routing rules.
+ *
+ * @return the calibration report, one measurement per distinct routed model
+ * @throws SrcMorphException if no routable rule is configured, or a model fails to load/generate (any
+ * {@link IOException} from the provider is caught per-model and rewrapped
+ * with the failing model's key)
+ */
+ public CalibrationReport execute() throws SrcMorphException {
+ final List fieldGenerations = config.getFieldGenerations();
+ if (fieldGenerations == null || fieldGenerations.isEmpty()) {
+ throw new SrcMorphException("No configured; nothing to calibrate.");
+ }
+
+ final AiPromptSupport promptSupport = EngineSupport.buildPromptSupport(config.getPromptDefinitions());
+ final AiModelDefinitionSupport modelDefinitionSupport =
+ EngineSupport.buildAiModelDefinitionSupport(config.getAiDefinitions());
+ final AiPromptPreparationSupport promptPreparationSupport = new AiPromptPreparationSupport(promptSupport);
+ final AiGenerationProviderFactory providerFactory = new AiGenerationProviderFactory();
+
+ // Distinct routed models, each mapped to a representative prompt key (skip rules have none).
+ final Map modelToPrompt = new LinkedHashMap<>();
+ for (final AiFieldGenerationConfig rule : fieldGenerations) {
+ if (rule == null || rule.getAiDefinitionKey() == null || rule.getPromptKey() == null) {
+ continue;
+ }
+ modelToPrompt.putIfAbsent(rule.getAiDefinitionKey(), rule.getPromptKey());
+ }
+ if (modelToPrompt.isEmpty()) {
+ throw new SrcMorphException("No routable (model + prompt) rule found to calibrate.");
+ }
+
+ LOGGER.info(
+ "AI index calibration: {} model(s). Provider: {}",
+ modelToPrompt.size(),
+ config.getGenerationProvider());
+ final List measurements = new ArrayList<>(modelToPrompt.size());
+ for (final Map.Entry entry : modelToPrompt.entrySet()) {
+ measurements.add(calibrateModel(
+ entry.getKey(),
+ entry.getValue(),
+ modelDefinitionSupport,
+ promptSupport,
+ promptPreparationSupport,
+ providerFactory));
+ }
+
+ return new CalibrationReport(measurements);
+ }
+
+ /**
+ * Loads one model, measures it via {@link AiCalibrationRunner}, and logs the result.
+ *
+ * @param modelKey the aiDefinitionKey to calibrate
+ * @param promptKey a representative prompt key routed to this model
+ * @param modelDefinitionSupport model lookup
+ * @param promptSupport prompt lookup (for the provider)
+ * @param promptPreparationSupport prompt preparation (for the window calculation)
+ * @param providerFactory provider factory
+ * @return the model's key paired with its measurement
+ * @throws SrcMorphException if the model fails to load or generate
+ */
+ private CalibrationReport.ModelMeasurement calibrateModel(
+ final String modelKey,
+ final String promptKey,
+ final AiModelDefinitionSupport modelDefinitionSupport,
+ final AiPromptSupport promptSupport,
+ final AiPromptPreparationSupport promptPreparationSupport,
+ final AiGenerationProviderFactory providerFactory)
+ throws SrcMorphException {
+ final AiGenerationConfig modelConfig = modelDefinitionSupport.getConfig(modelKey);
+ final long windowChars = calibrationRunner.windowChars(modelConfig, promptKey, promptPreparationSupport);
+
+ LOGGER.info("");
+ LOGGER.info("Model '{}': loading (window ~{} source chars)...", modelKey, windowChars);
+ try (AiGenerationProvider provider = providerFactory.create(
+ config.getGenerationProvider(),
+ EngineSupport.resolveLlamaCppJniConfig(config, modelDefinitionSupport, modelKey),
+ promptSupport)) {
+ final AiCalibrationMeasurement m =
+ calibrationRunner.measure(provider, modelConfig, promptKey, promptPreparationSupport);
+
+ LOGGER.info(String.format(
+ Locale.ROOT,
+ "Model '%s': loaded+first-gen ~%.1fs | prefill %.0f tok/s | decode %.0f tok/s | ~%.2f chars/token",
+ modelKey,
+ m.loadSeconds(),
+ m.prefillTokensPerSecond(),
+ m.decodeTokensPerSecond(),
+ m.charsPerToken()));
+ if (m.prefillTokensPerSecond() > 0 && m.midPrefillTokensPerSecond() > 0) {
+ LOGGER.info(String.format(
+ Locale.ROOT,
+ " (mid-window prefill %.0f tok/s vs near-window %.0f tok/s - larger gap => more curvature)",
+ m.midPrefillTokensPerSecond(),
+ m.prefillTokensPerSecond()));
+ }
+ return new CalibrationReport.ModelMeasurement(modelKey, m);
+ } catch (final IOException e) {
+ throw new SrcMorphException("Calibration failed for model '" + modelKey + "': " + e.getMessage(), e);
+ }
+ }
+}
diff --git a/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/CalibrationReport.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/CalibrationReport.java
new file mode 100644
index 0000000..b1ad648
--- /dev/null
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/CalibrationReport.java
@@ -0,0 +1,118 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.engine;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import net.ladenthin.srcmorph.indexer.AiCalibrationMeasurement;
+
+/**
+ * The outcome of one {@link CalibrateEngine#execute()} run: one {@link ModelMeasurement} per distinct
+ * routed model, in calibration order.
+ *
+ *
{@link #renderXml()} is the pure, paste-ready {@code } XML block renderer — the same
+ * text the {@code srcmorph:calibrate} goal has always printed, extracted here so it is unit-testable
+ * without a Maven {@code Log} and reusable by any future caller (e.g. a CLI).
+ */
+@ToString
+@EqualsAndHashCode
+public final class CalibrationReport {
+
+ private final List measurements;
+
+ /**
+ * Creates a new {@link CalibrationReport}.
+ *
+ * @param measurements one measurement per distinct routed model, in calibration order
+ */
+ public CalibrationReport(final List measurements) {
+ this.measurements = new ArrayList<>(measurements);
+ }
+
+ /**
+ * Returns an unmodifiable view of the per-model measurements, in calibration order.
+ *
+ * @return the measurements
+ */
+ public List measurements() {
+ return Collections.unmodifiableList(measurements);
+ }
+
+ /**
+ * Renders one paste-ready {@code } XML block per measured model, each preceded by a
+ * comment naming the model key, in calibration order.
+ *
+ * @return the rendered blocks, or an empty string when no model was measured
+ */
+ public String renderXml() {
+ final StringBuilder out = new StringBuilder();
+ for (final ModelMeasurement entry : measurements) {
+ appendPasteBlock(out, entry.modelKey(), entry.measurement());
+ }
+ return out.toString();
+ }
+
+ /**
+ * Appends a copy-pasteable {@code } block (with a comment naming the model) to the buffer.
+ *
+ * @param out the buffer
+ * @param modelKey the model key (for the comment)
+ * @param m the measurement
+ */
+ private static void appendPasteBlock(
+ final StringBuilder out, final String modelKey, final AiCalibrationMeasurement m) {
+ out.append("\n");
+ out.append("\n");
+ out.append(String.format(
+ Locale.ROOT,
+ " %.1f%n",
+ m.prefillTokensPerSecond()));
+ out.append(String.format(
+ Locale.ROOT, " %.1f%n", m.decodeTokensPerSecond()));
+ out.append(String.format(Locale.ROOT, " %.2f%n", m.charsPerToken()));
+ out.append("\n");
+ }
+
+ /** One model's key paired with its {@link AiCalibrationMeasurement}. */
+ @ToString
+ @EqualsAndHashCode
+ public static final class ModelMeasurement {
+
+ private final String modelKey;
+ private final AiCalibrationMeasurement measurement;
+
+ /**
+ * Creates a new {@link ModelMeasurement}.
+ *
+ * @param modelKey the {@code aiDefinitionKey} that was calibrated
+ * @param measurement the measurement
+ */
+ public ModelMeasurement(final String modelKey, final AiCalibrationMeasurement measurement) {
+ this.modelKey = modelKey;
+ this.measurement = measurement;
+ }
+
+ /**
+ * Returns the calibrated model's key.
+ *
+ * @return the {@code aiDefinitionKey}
+ */
+ public String modelKey() {
+ return modelKey;
+ }
+
+ /**
+ * Returns the measurement.
+ *
+ * @return the calibration measurement
+ */
+ public AiCalibrationMeasurement measurement() {
+ return measurement;
+ }
+ }
+}
diff --git a/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/EngineSupport.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/EngineSupport.java
new file mode 100644
index 0000000..469e83b
--- /dev/null
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/EngineSupport.java
@@ -0,0 +1,149 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.engine;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import net.ladenthin.srcmorph.config.AiFieldGenerationConfig;
+import net.ladenthin.srcmorph.config.AiGenerationConfig;
+import net.ladenthin.srcmorph.config.AiModelDefinition;
+import net.ladenthin.srcmorph.config.AiModelDefinitionSupport;
+import net.ladenthin.srcmorph.config.SrcMorphConfiguration;
+import net.ladenthin.srcmorph.prompt.AiPromptDefinition;
+import net.ladenthin.srcmorph.prompt.AiPromptSupport;
+import net.ladenthin.srcmorph.provider.LlamaCppJniConfig;
+import net.ladenthin.srcmorph.provider.LlamaCppJniConfigFactory;
+import org.jspecify.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Package-private helpers shared by at least two of the {@code engine} package's per-phase engines.
+ * Anything used by only one engine stays local to that engine class instead.
+ */
+final class EngineSupport {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(EngineSupport.class);
+
+ private EngineSupport() {
+ // utility class — not instantiable
+ }
+
+ /**
+ * Resolves the configured subtree strings against {@code basePath}, filtering out any paths that do
+ * not exist on disk. Used by {@link GenerateEngine} and {@link AggregatePackagesEngine}.
+ *
+ * @param basePath absolute, normalised project base directory
+ * @param subtrees configured subtree strings, or {@code null}/empty for none
+ * @return list of resolved, existing subtree paths; empty if none configured or none exist
+ */
+ static List resolveSubtrees(final Path basePath, final @Nullable List subtrees) {
+ final List resolved = new ArrayList<>();
+
+ if (subtrees == null || subtrees.isEmpty()) {
+ return resolved;
+ }
+
+ for (final String subtree : subtrees) {
+ final Path path = basePath.resolve(subtree).normalize();
+ if (path.toFile().exists()) {
+ resolved.add(path);
+ } else {
+ LOGGER.warn("Skipping missing subtree: {}", path);
+ }
+ }
+
+ return resolved;
+ }
+
+ /**
+ * Builds an {@link AiPromptSupport} from the configured prompt definitions, translating a
+ * misconfiguration ({@link NullPointerException} from a missing {@code key}/{@code template}) into
+ * a {@link SrcMorphException} so the caller reports it as a configuration error.
+ *
+ * @param promptDefinitions the configured prompt definitions, or {@code null} for none
+ * @return prompt support instance backed by the configured definitions
+ * @throws SrcMorphException if any prompt definition is missing a required field
+ */
+ static AiPromptSupport buildPromptSupport(final @Nullable List promptDefinitions)
+ throws SrcMorphException {
+ try {
+ return new AiPromptSupport(promptDefinitions);
+ } catch (final NullPointerException e) {
+ throw new SrcMorphException("Invalid plugin configuration: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Builds an {@link AiModelDefinitionSupport} from the configured AI model definitions, translating a
+ * misconfiguration ({@link NullPointerException} from a missing {@code key}) into a
+ * {@link SrcMorphException} so the caller reports it as a configuration error.
+ *
+ * @param aiDefinitions the configured AI model definitions, or {@code null} for none
+ * @return model definition support instance backed by the configured definitions
+ * @throws SrcMorphException if any AI definition is missing a required field
+ */
+ static AiModelDefinitionSupport buildAiModelDefinitionSupport(final @Nullable List aiDefinitions)
+ throws SrcMorphException {
+ try {
+ return new AiModelDefinitionSupport(aiDefinitions);
+ } catch (final NullPointerException e) {
+ throw new SrcMorphException("Invalid plugin configuration: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Resolves the {@link LlamaCppJniConfig} for a run with no specific routed model in mind: when
+ * {@link SrcMorphConfiguration#getFieldGenerations()} is non-empty, all model parameters come from the
+ * {@link AiModelDefinition} referenced by the first entry's
+ * {@link AiFieldGenerationConfig#getAiDefinitionKey()}; otherwise the configuration's individual
+ * {@code llama*} fallback fields are used. Used by {@link AggregatePackagesEngine} and
+ * {@link AggregateProjectEngine}, which drive a single provider for the whole run.
+ *
+ * @param config the run configuration
+ * @param modelDefinitionSupport model lookup built from {@link SrcMorphConfiguration#getAiDefinitions()}
+ * @return the fully populated llama.cpp configuration
+ * @throws IllegalArgumentException if the first field generation's {@code aiDefinitionKey} matches no
+ * registered definition
+ */
+ static LlamaCppJniConfig resolveLlamaCppJniConfig(
+ final SrcMorphConfiguration config, final AiModelDefinitionSupport modelDefinitionSupport) {
+ final List fieldGenerations = config.getFieldGenerations();
+ if (fieldGenerations != null && !fieldGenerations.isEmpty()) {
+ return resolveLlamaCppJniConfig(
+ config, modelDefinitionSupport, fieldGenerations.get(0).getAiDefinitionKey());
+ }
+ final String modelPath = config.getLlamaModelPath();
+ if (modelPath == null) {
+ throw new NullPointerException("llamaModelPath");
+ }
+ return LlamaCppJniConfigFactory.fromFallbackParameters(
+ config.getLlamaLibraryPath(),
+ modelPath,
+ config.getLlamaContextSize(),
+ config.getLlamaMaxOutputTokens(),
+ config.getLlamaTemperature(),
+ config.getLlamaThreads());
+ }
+
+ /**
+ * Resolves the {@link LlamaCppJniConfig} for one specific {@link AiModelDefinition}, identified by its
+ * key. Used by {@link GenerateEngine} (one provider per routing group) and {@link CalibrateEngine}
+ * (one provider per calibrated model).
+ *
+ * @param config the run configuration (supplies {@code llamaLibraryPath})
+ * @param modelDefinitionSupport model lookup built from {@link SrcMorphConfiguration#getAiDefinitions()}
+ * @param aiDefinitionKey the {@link AiModelDefinition} key
+ * @return the fully populated llama.cpp configuration for that definition
+ * @throws IllegalArgumentException if {@code aiDefinitionKey} matches no registered definition
+ */
+ static LlamaCppJniConfig resolveLlamaCppJniConfig(
+ final SrcMorphConfiguration config,
+ final AiModelDefinitionSupport modelDefinitionSupport,
+ final String aiDefinitionKey) {
+ final AiGenerationConfig modelConfig = modelDefinitionSupport.getConfig(aiDefinitionKey);
+ return LlamaCppJniConfigFactory.fromGenerationConfig(config.getLlamaLibraryPath(), modelConfig);
+ }
+}
diff --git a/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/GenerateEngine.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/GenerateEngine.java
new file mode 100644
index 0000000..45ee06d
--- /dev/null
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/GenerateEngine.java
@@ -0,0 +1,250 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.engine;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import lombok.ToString;
+import net.ladenthin.srcmorph.config.AiFactDefinitionSupport;
+import net.ladenthin.srcmorph.config.AiFieldGenerationConfig;
+import net.ladenthin.srcmorph.config.AiFieldGenerationSelector;
+import net.ladenthin.srcmorph.config.AiModelDefinitionSupport;
+import net.ladenthin.srcmorph.config.SrcMorphConfiguration;
+import net.ladenthin.srcmorph.indexer.AiFieldGenerationSupport;
+import net.ladenthin.srcmorph.indexer.AiIndexPlan;
+import net.ladenthin.srcmorph.indexer.SourceFileIndexer;
+import net.ladenthin.srcmorph.prompt.AiPromptPreparationSupport;
+import net.ladenthin.srcmorph.prompt.AiPromptSupport;
+import net.ladenthin.srcmorph.provider.AiGenerationProvider;
+import net.ladenthin.srcmorph.provider.AiGenerationProviderFactory;
+import net.ladenthin.srcmorph.support.AiGenerationTimeEstimator;
+import net.ladenthin.srcmorph.support.AiProgressBar;
+import net.ladenthin.srcmorph.support.Java8CompatibilityHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Phase 1 orchestration: indexes source files and fills in their AI-generated summary fields.
+ *
+ *
Extracted from what was {@code GenerateMojo.execute()} in the {@code llamacpp-ai-index-maven-plugin}
+ * module. Plan-then-execute and rule-routed: {@link #execute()} first plans the whole run (which model +
+ * prompt each file gets, or skip/unmatched, and whether each file fits its routed model's context
+ * window), fails fast on an unmatched file or a hard oversize failure, stops after the plan when
+ * {@link SrcMorphConfiguration#isPlanOnly()}, and otherwise loads each distinct routed model exactly once
+ * (one provider resident at a time) and indexes that group's files.
+ */
+@ToString
+public final class GenerateEngine {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(GenerateEngine.class);
+
+ /**
+ * Default file extension used when {@link SrcMorphConfiguration#getFileExtensions()} is unset. Only
+ * files whose names end with this extension are indexed.
+ */
+ private static final String DEFAULT_FILE_EXTENSION = ".java";
+
+ /** Default source subtree used when no {@link SrcMorphConfiguration#getSubtrees()} is configured. */
+ private static final String DEFAULT_SOURCE_SUBTREE = "src/main/java";
+
+ /** Nanoseconds per second, for converting the measured run elapsed time to whole seconds. */
+ private static final long NANOS_PER_SECOND = 1_000_000_000L;
+
+ private final SrcMorphConfiguration config;
+ private final Java8CompatibilityHelper compatibilityHelper = new Java8CompatibilityHelper();
+
+ /**
+ * Creates a new {@link GenerateEngine} for the given run configuration.
+ *
+ * @param config the run configuration
+ */
+ public GenerateEngine(final SrcMorphConfiguration config) {
+ this.config = config;
+ }
+
+ /**
+ * Runs the full plan-then-execute Phase 1 pipeline.
+ *
+ * @return the outcome of the run (a plan-only result when {@link SrcMorphConfiguration#isPlanOnly()})
+ * @throws SrcMorphException if the run is misconfigured (no {@code fieldGenerations}, an unmatched
+ * file with no fallback, a hard oversize failure, or an invalid rule set)
+ * @throws IOException if the source tree cannot be walked or a target file cannot be written
+ */
+ public GenerateResult execute() throws SrcMorphException, IOException {
+ final Path basePath =
+ config.getBaseDirectory().toPath().toAbsolutePath().normalize();
+ final Path outputPath =
+ config.getOutputDirectory().toPath().toAbsolutePath().normalize();
+ final List resolvedSubtrees = EngineSupport.resolveSubtrees(basePath, config.getSubtrees());
+ final List resolvedExtensions = resolveFileExtensions();
+
+ LOGGER.info("Starting AI index generation");
+ LOGGER.info("Base directory : {}", basePath);
+ LOGGER.info("Output directory: {}", outputPath);
+ LOGGER.info("Subtrees : {}", resolvedSubtrees);
+ if (!resolvedExtensions.isEmpty()) {
+ LOGGER.info("Extensions : {}", resolvedExtensions);
+ }
+ LOGGER.info("Force : {}", config.isForce());
+ LOGGER.info("Provider : {}", config.getGenerationProvider());
+ LOGGER.info("LlamaCpp Temperature: {}", config.getLlamaTemperature());
+ LOGGER.info("LlamaCpp Max Output Tokens: {}", config.getLlamaMaxOutputTokens());
+
+ final List fieldGenerations = config.getFieldGenerations();
+ if (fieldGenerations == null || fieldGenerations.isEmpty()) {
+ throw new SrcMorphException("No configured for the generate goal.");
+ }
+
+ final AiPromptSupport promptSupport = EngineSupport.buildPromptSupport(config.getPromptDefinitions());
+ final AiModelDefinitionSupport modelDefinitionSupport =
+ EngineSupport.buildAiModelDefinitionSupport(config.getAiDefinitions());
+ final AiPromptPreparationSupport promptPreparationSupport = new AiPromptPreparationSupport(promptSupport);
+ // Resolve each rule's factsKey to its shared factDefinitions group (copies the counters onto the
+ // rule's facts) BEFORE validation, so the resolved fact patterns are validated too.
+ resolveSharedFacts(fieldGenerations);
+
+ final AiFieldGenerationSelector selector = new AiFieldGenerationSelector();
+ // Fail fast on a bad rule set (e.g. >1 fallback, a route rule missing prompt/model).
+ selector.validate(fieldGenerations);
+
+ final SourceFileIndexer fileIndexer = new SourceFileIndexer(
+ basePath,
+ outputPath,
+ resolvedExtensions,
+ config.getPluginVersion(),
+ config.getAiVersion(),
+ resolvedSubtrees,
+ config.getExcludes(),
+ config.getMinFileSizeBytes(),
+ config.getMaxFileSizeBytes(),
+ config.isForce());
+
+ // 1. Collect candidate files across the configured subtrees.
+ final List candidates = new ArrayList<>();
+ for (final Path subtree : resolvedSubtrees.isEmpty()
+ ? compatibilityHelper.listOf(basePath.resolve(DEFAULT_SOURCE_SUBTREE))
+ : resolvedSubtrees) {
+ if (!subtree.toFile().exists()) {
+ LOGGER.warn("Skipping missing subtree: {}", subtree);
+ continue;
+ }
+ candidates.addAll(fileIndexer.collectCandidates(subtree));
+ }
+
+ // 2. Plan the run: which model + prompt each file gets (or skip / unmatched), and whether
+ // each file fits its routed model's context window (computed up front, same threshold the
+ // run uses to trim — see AiInputWindowCalculator).
+ final AiIndexPlan plan =
+ fileIndexer.classify(candidates, fieldGenerations, modelDefinitionSupport, promptPreparationSupport);
+ LOGGER.info("AI index plan (Markdown):\n{}", plan.renderMarkdown(basePath));
+
+ // 3. A file that matched no rule and no fallback is a fatal misconfiguration.
+ if (!plan.unmatched().isEmpty()) {
+ throw new SrcMorphException(plan.unmatched().size()
+ + " source file(s) matched no rule and no fallback is configured; "
+ + "add a rule or a matching rule (see the plan above).");
+ }
+
+ // 3b. A file larger than its routed model's window would lose content if trimmed. By default
+ // (onOversize=fail) this is a hard failure: the fix is user configuration, never an
+ // automatic model choice — route oversized files to a larger-context model, or set the
+ // rule's onOversize (sample/mapReduce/deterministic) to handle them at run time. Only the
+ // fail entries abort here; the handled ones are processed during generation.
+ final int overWindowFailCount = plan.windowFailCount();
+ if (overWindowFailCount > 0) {
+ throw new SrcMorphException(overWindowFailCount
+ + " source file(s) exceed their routed model's context window with onOversize=fail "
+ + "(see the 'Over window' section in the plan above). Route them to a model with a "
+ + "large enough context window, or set onOversize=sample|mapReduce|deterministic on "
+ + "the rule. The build does not pick a model for you; this is configuration only.");
+ }
+
+ if (config.isPlanOnly()) {
+ LOGGER.info("planOnly=true: stopping after the plan; no model loaded, nothing generated.");
+ return GenerateResult.planned();
+ }
+
+ // 4. Execute model group by model group: load each model once, index its files, close.
+ // Progress is the running sum of each finished file's PLAN estimate over the grand total
+ // (no re-estimation), logged as a bar + percent after every file, with the estimated time
+ // left and the actual wall-clock elapsed for comparison.
+ final AiGenerationProviderFactory providerFactory = new AiGenerationProviderFactory();
+ final AiGenerationTimeEstimator estimator = new AiGenerationTimeEstimator();
+ final long totalEstimatedSeconds = plan.totalEstimatedSeconds();
+ final int totalFiles = plan.routedCount();
+ final long runStartNanos = System.nanoTime();
+ long doneEstimatedSeconds = 0;
+ int doneFiles = 0;
+ int wrote = 0;
+ int unchanged = 0;
+ for (final Map.Entry> group :
+ plan.routesByModel().entrySet()) {
+ final String aiDefinitionKey = group.getKey();
+ LOGGER.info(
+ "Loading model '{}' for {} file(s)",
+ aiDefinitionKey,
+ group.getValue().size());
+ try (AiGenerationProvider provider = providerFactory.create(
+ config.getGenerationProvider(),
+ EngineSupport.resolveLlamaCppJniConfig(config, modelDefinitionSupport, aiDefinitionKey),
+ promptSupport)) {
+ final AiFieldGenerationSupport support =
+ new AiFieldGenerationSupport(provider, promptPreparationSupport, modelDefinitionSupport);
+ for (final AiIndexPlan.Entry entry : group.getValue()) {
+ if (fileIndexer.indexFile(entry.file(), entry.rule(), support)) {
+ wrote++;
+ } else {
+ unchanged++;
+ }
+ doneEstimatedSeconds += entry.estimatedSeconds();
+ doneFiles++;
+ final long elapsedSeconds = (System.nanoTime() - runStartNanos) / NANOS_PER_SECOND;
+ final long remainingSeconds = Math.max(0L, totalEstimatedSeconds - doneEstimatedSeconds);
+ LOGGER.info(
+ "{} {}/{} files - est. {}/{} done, {} left (estimate) | {} elapsed (actual)",
+ AiProgressBar.render(doneEstimatedSeconds, totalEstimatedSeconds),
+ doneFiles,
+ totalFiles,
+ estimator.formatDuration(doneEstimatedSeconds),
+ estimator.formatDuration(totalEstimatedSeconds),
+ estimator.formatDuration(remainingSeconds),
+ estimator.formatDuration(elapsedSeconds));
+ }
+ }
+ }
+
+ final int skippedCount = plan.skipped().size();
+ LOGGER.info("Generated AI files: {} written, {} unchanged, {} skipped", wrote, unchanged, skippedCount);
+ LOGGER.info("AI index generation finished.");
+
+ return new GenerateResult(false, wrote, unchanged, skippedCount);
+ }
+
+ private List resolveFileExtensions() {
+ final List configured = config.getFileExtensions();
+ if (configured == null || configured.isEmpty()) {
+ return compatibilityHelper.listOf(DEFAULT_FILE_EXTENSION);
+ }
+ return configured;
+ }
+
+ /**
+ * Resolves each rule's {@code factsKey} to its shared {@code } group, copying the
+ * counters onto the rule's {@code facts}. Translates a misconfiguration (unknown key, or a definition
+ * with a null key) into a {@link SrcMorphException} so the caller reports a configuration error.
+ *
+ * @param fieldGenerations the routing rules whose {@code factsKey} references are resolved in place
+ * @throws SrcMorphException if a {@code factsKey} matches no group or a definition has a null key
+ */
+ private void resolveSharedFacts(final List fieldGenerations) throws SrcMorphException {
+ try {
+ new AiFactDefinitionSupport(config.getFactDefinitions()).resolveFactsKeys(fieldGenerations);
+ } catch (final IllegalArgumentException | NullPointerException e) {
+ throw new SrcMorphException("Invalid factDefinitions/factsKey configuration: " + e.getMessage(), e);
+ }
+ }
+}
diff --git a/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/GenerateResult.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/GenerateResult.java
new file mode 100644
index 0000000..cade778
--- /dev/null
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/GenerateResult.java
@@ -0,0 +1,86 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.engine;
+
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import net.ladenthin.srcmorph.support.ConvertToRecord;
+
+/**
+ * The outcome of one {@link GenerateEngine#execute()} run.
+ *
+ *
Record-shaped value type marked {@link ConvertToRecord} for the future Java 17+ migration.
+ */
+@ConvertToRecord
+@ToString
+@EqualsAndHashCode
+public final class GenerateResult {
+
+ private final boolean planOnly;
+ private final int written;
+ private final int unchanged;
+ private final int skipped;
+
+ /**
+ * Creates a new {@link GenerateResult}.
+ *
+ * @param planOnly {@code true} when the run stopped after printing the routing plan (no model was
+ * loaded and nothing was generated)
+ * @param written number of {@code .ai.md} files written or refreshed
+ * @param unchanged number of routed files left untouched (already up to date)
+ * @param skipped number of files matched by a {@code } rule
+ */
+ public GenerateResult(final boolean planOnly, final int written, final int unchanged, final int skipped) {
+ this.planOnly = planOnly;
+ this.written = written;
+ this.unchanged = unchanged;
+ this.skipped = skipped;
+ }
+
+ /**
+ * Creates a {@link GenerateResult} for a {@code planOnly} run: the plan was printed and validated, but
+ * no model was loaded and nothing was generated.
+ *
+ * @return a plan-only result with every count at zero
+ */
+ public static GenerateResult planned() {
+ return new GenerateResult(true, 0, 0, 0);
+ }
+
+ /**
+ * Returns whether the run stopped after printing the routing plan.
+ *
+ * @return {@code true} when the run was {@code planOnly}
+ */
+ public boolean planOnly() {
+ return planOnly;
+ }
+
+ /**
+ * Returns the number of {@code .ai.md} files written or refreshed.
+ *
+ * @return the written count
+ */
+ public int written() {
+ return written;
+ }
+
+ /**
+ * Returns the number of routed files left untouched (already up to date).
+ *
+ * @return the unchanged count
+ */
+ public int unchanged() {
+ return unchanged;
+ }
+
+ /**
+ * Returns the number of files matched by a {@code } rule.
+ *
+ * @return the skipped count
+ */
+ public int skipped() {
+ return skipped;
+ }
+}
diff --git a/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/SrcMorphException.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/SrcMorphException.java
new file mode 100644
index 0000000..4277e78
--- /dev/null
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/SrcMorphException.java
@@ -0,0 +1,38 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.engine;
+
+/**
+ * Checked exception thrown by the {@code engine} package for a run that fails because of misconfiguration
+ * (an invalid rule set, an unmatched file with no fallback, an oversized file with {@code onOversize=fail},
+ * a bad prompt/model definition, …) rather than an I/O failure.
+ *
+ *
This is the framework-free replacement for {@code org.apache.maven.plugin.MojoExecutionException}
+ * inside core code: the {@code llamacpp-ai-index-maven-plugin} module's mojos catch this (alongside a
+ * plain {@link java.io.IOException} for genuine I/O failures) and rewrap it into a
+ * {@code MojoExecutionException} so Maven still reports the same user-facing error it always has.
+ */
+public class SrcMorphException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Creates a new {@link SrcMorphException} with the given message.
+ *
+ * @param message the detail message
+ */
+ public SrcMorphException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new {@link SrcMorphException} with the given message and cause.
+ *
+ * @param message the detail message
+ * @param cause the underlying cause
+ */
+ public SrcMorphException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/package-info.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/package-info.java
new file mode 100644
index 0000000..fbc9705
--- /dev/null
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/engine/package-info.java
@@ -0,0 +1,10 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+
+/**
+ * Per-phase orchestration engines, extracted from the {@code llamacpp-ai-index-maven-plugin} module's
+ * mojo {@code execute()} bodies so a run can be driven from plain Java (and, eventually, a CLI) without a
+ * Maven runtime. Sits above every other package in this library.
+ */
+package net.ladenthin.srcmorph.engine;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiCalibrationMeasurement.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/AiCalibrationMeasurement.java
similarity index 96%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiCalibrationMeasurement.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/AiCalibrationMeasurement.java
index a227761..bdd243d 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiCalibrationMeasurement.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/AiCalibrationMeasurement.java
@@ -1,11 +1,11 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.indexer;
+package net.ladenthin.srcmorph.indexer;
import lombok.EqualsAndHashCode;
import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.support.ConvertToRecord;
+import net.ladenthin.srcmorph.support.ConvertToRecord;
/**
* The result of one model's calibration measurement (see {@link AiCalibrationRunner}): the load time and
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiCalibrationRunner.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/AiCalibrationRunner.java
similarity index 94%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiCalibrationRunner.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/AiCalibrationRunner.java
index 3f13c6b..b6b57c1 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiCalibrationRunner.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/AiCalibrationRunner.java
@@ -1,20 +1,20 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.indexer;
+package net.ladenthin.srcmorph.indexer;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationRequest;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeader;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptPreparationSupport;
-import net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationProvider;
-import net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationTimings;
+import net.ladenthin.srcmorph.config.AiGenerationConfig;
+import net.ladenthin.srcmorph.document.AiGenerationRequest;
+import net.ladenthin.srcmorph.document.AiMdHeader;
+import net.ladenthin.srcmorph.prompt.AiPromptPreparationSupport;
+import net.ladenthin.srcmorph.provider.AiGenerationProvider;
+import net.ladenthin.srcmorph.provider.AiGenerationTimings;
/**
- * Measures one model's per-machine timing for the {@code ai-index:calibrate} goal: it triggers the load,
+ * Measures one model's per-machine timing for the {@code srcmorph:calibrate} goal: it triggers the load,
* runs two representative generations (mid- and near-window sized), and reads the model's own measured
* prefill/decode throughput. Lives in the indexer layer because it builds requests and drives the
* provider; the mojo stays thin (orchestrate + format).
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiFieldGenerationSupport.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/AiFieldGenerationSupport.java
similarity index 93%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiFieldGenerationSupport.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/AiFieldGenerationSupport.java
index 5fa364e..915b9cf 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiFieldGenerationSupport.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/AiFieldGenerationSupport.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.indexer;
+package net.ladenthin.srcmorph.indexer;
import java.io.IOException;
import java.nio.file.Path;
@@ -10,22 +10,22 @@
import java.util.List;
import java.util.Map;
import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiCalibration;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFactExtractor;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFieldGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinitionSupport;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiOversizeStrategy;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationRequest;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationResult;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeader;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPreparedPrompt;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptPreparationSupport;
-import net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationProvider;
-import net.ladenthin.maven.llamacpp.aiindex.support.AiDeterministicSummary;
-import net.ladenthin.maven.llamacpp.aiindex.support.AiGenerationTimeEstimator;
-import net.ladenthin.maven.llamacpp.aiindex.support.AiSourceChunker;
-import net.ladenthin.maven.llamacpp.aiindex.support.Java8CompatibilityHelper;
+import net.ladenthin.srcmorph.config.AiCalibration;
+import net.ladenthin.srcmorph.config.AiFactExtractor;
+import net.ladenthin.srcmorph.config.AiFieldGenerationConfig;
+import net.ladenthin.srcmorph.config.AiGenerationConfig;
+import net.ladenthin.srcmorph.config.AiModelDefinitionSupport;
+import net.ladenthin.srcmorph.config.AiOversizeStrategy;
+import net.ladenthin.srcmorph.document.AiGenerationRequest;
+import net.ladenthin.srcmorph.document.AiGenerationResult;
+import net.ladenthin.srcmorph.document.AiMdHeader;
+import net.ladenthin.srcmorph.prompt.AiPreparedPrompt;
+import net.ladenthin.srcmorph.prompt.AiPromptPreparationSupport;
+import net.ladenthin.srcmorph.provider.AiGenerationProvider;
+import net.ladenthin.srcmorph.support.AiDeterministicSummary;
+import net.ladenthin.srcmorph.support.AiGenerationTimeEstimator;
+import net.ladenthin.srcmorph.support.AiSourceChunker;
+import net.ladenthin.srcmorph.support.Java8CompatibilityHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -33,8 +33,8 @@
* Shared field-generation logic used by both {@link SourceFileIndexer} and
* {@link PackageIndexer}.
*
- *
Iterates over a list of {@link net.ladenthin.maven.llamacpp.aiindex.config.AiFieldGenerationConfig} entries, prepares the
- * prompt for each, delegates generation to the configured {@link net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationProvider},
+ *
Iterates over a list of {@link net.ladenthin.srcmorph.config.AiFieldGenerationConfig} entries, prepares the
+ * prompt for each, delegates generation to the configured {@link net.ladenthin.srcmorph.provider.AiGenerationProvider},
* and accumulates the generated text as the document body.
* A trim warning is logged whenever the source text had to be truncated to fit within the
* configured maximum input character budget.
@@ -194,7 +194,7 @@ public class AiFieldGenerationSupport {
*
* @param generationProvider AI backend used to generate text for each field
* @param promptPreparationSupport helper that resolves and prepares prompt templates
- * @param modelDefinitionSupport lookup for {@link net.ladenthin.maven.llamacpp.aiindex.config.AiGenerationConfig} by key
+ * @param modelDefinitionSupport lookup for {@link net.ladenthin.srcmorph.config.AiGenerationConfig} by key
*/
public AiFieldGenerationSupport(
final AiGenerationProvider generationProvider,
@@ -207,13 +207,13 @@ public AiFieldGenerationSupport(
/**
* Processes each entry in {@code fieldGenerations}, generates the requested AI text,
- * and accumulates the results into an {@link net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationResult}.
+ * and accumulates the results into an {@link net.ladenthin.srcmorph.document.AiGenerationResult}.
*
- *
For each non-null {@link net.ladenthin.maven.llamacpp.aiindex.config.AiFieldGenerationConfig}:
+ *
For each non-null {@link net.ladenthin.srcmorph.config.AiFieldGenerationConfig}:
*
- *
The prompt is prepared via {@link net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptPreparationSupport#preparePrompt}.
+ *
The prompt is prepared via {@link net.ladenthin.srcmorph.prompt.AiPromptPreparationSupport#preparePrompt}.
*
A trim warning is logged if the source was truncated and
- * {@link net.ladenthin.maven.llamacpp.aiindex.config.AiGenerationConfig#isWarnOnTrim()} is {@code true}.
+ * {@link net.ladenthin.srcmorph.config.AiGenerationConfig#isWarnOnTrim()} is {@code true}.
*
The AI provider generates a value for the trimmed source.
*
If the provider returns a blank body, a single warning is emitted (fail-fast).
* Blank output no longer occurs in normal operation with a non-greedy temperature
@@ -231,11 +231,11 @@ public AiFieldGenerationSupport(
* {@code "package"}); embedded in trim-warning log messages
* @param sourceText full source text passed as input to the prompt preparation step
* @param baseHeader current header; passed through to each
- * {@link net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationRequest}
- * @return an {@link net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationResult} with the generated body; defaults to empty string
+ * {@link net.ladenthin.srcmorph.document.AiGenerationRequest}
+ * @return an {@link net.ladenthin.srcmorph.document.AiGenerationResult} with the generated body; defaults to empty string
* @throws IOException if the AI provider throws during generation
- * @throws IllegalArgumentException if a field's {@link net.ladenthin.maven.llamacpp.aiindex.config.AiFieldGenerationConfig#getAiDefinitionKey()}
- * does not match any registered {@link net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinition}
+ * @throws IllegalArgumentException if a field's {@link net.ladenthin.srcmorph.config.AiFieldGenerationConfig#getAiDefinitionKey()}
+ * does not match any registered {@link net.ladenthin.srcmorph.config.AiModelDefinition}
*/
public AiGenerationResult processFieldGenerations(
final List fieldGenerations,
@@ -599,10 +599,10 @@ private static String contextName(final Path contextFile) {
/**
* Returns the effective {@code maxInputChars} for the given field generation entry.
*
- *
When the {@link net.ladenthin.maven.llamacpp.aiindex.config.AiGenerationConfig#getCharsPerToken()} value is greater than zero,
+ *
When the {@link net.ladenthin.srcmorph.config.AiGenerationConfig#getCharsPerToken()} value is greater than zero,
* the maximum is computed once per unique {@code (aiDefinitionKey, promptKey)} pair,
* logged in detail, cached, and reused for all subsequent files. When
- * {@code charsPerToken} is zero, the static {@link net.ladenthin.maven.llamacpp.aiindex.config.AiGenerationConfig#getMaxInputChars()}
+ * {@code charsPerToken} is zero, the static {@link net.ladenthin.srcmorph.config.AiGenerationConfig#getMaxInputChars()}
* fallback is returned instead.
*
* @param fieldGeneration the field configuration identifying the definition and prompt keys
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiIndexPlan.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/AiIndexPlan.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiIndexPlan.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/AiIndexPlan.java
index 1d1b7c0..fbf1518 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiIndexPlan.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/AiIndexPlan.java
@@ -1,16 +1,16 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.indexer;
+package net.ladenthin.srcmorph.indexer;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFieldGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiOversizeStrategy;
-import net.ladenthin.maven.llamacpp.aiindex.support.AiGenerationTimeEstimator;
+import net.ladenthin.srcmorph.config.AiFieldGenerationConfig;
+import net.ladenthin.srcmorph.config.AiOversizeStrategy;
+import net.ladenthin.srcmorph.support.AiGenerationTimeEstimator;
/**
* The routing plan for one {@code generate} run: which files each AI model will index (with which
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiInputWindowCalculator.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/AiInputWindowCalculator.java
similarity index 95%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiInputWindowCalculator.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/AiInputWindowCalculator.java
index c5b082c..431d6e1 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiInputWindowCalculator.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/AiInputWindowCalculator.java
@@ -3,10 +3,10 @@
//
// SPDX-License-Identifier: Apache-2.0
// @formatter:on
-package net.ladenthin.maven.llamacpp.aiindex.indexer;
+package net.ladenthin.srcmorph.indexer;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptPreparationSupport;
+import net.ladenthin.srcmorph.config.AiGenerationConfig;
+import net.ladenthin.srcmorph.prompt.AiPromptPreparationSupport;
/**
* Pure calculator for the input window: how many source characters fit a model's context window
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/PackageIndexer.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/PackageIndexer.java
similarity index 92%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/PackageIndexer.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/PackageIndexer.java
index 14b43cc..43e8e72 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/PackageIndexer.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/PackageIndexer.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.indexer;
+package net.ladenthin.srcmorph.indexer;
import java.io.IOException;
import java.nio.file.Files;
@@ -12,22 +12,22 @@
import java.util.List;
import java.util.stream.Stream;
import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFieldGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinitionSupport;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationResult;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdChildEntryLineFormatter;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdDocument;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdDocumentCodec;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeader;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeaderCodec;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeaderSupport;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptPreparationSupport;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptSupport;
-import net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationProvider;
-import net.ladenthin.maven.llamacpp.aiindex.support.AiChecksumSupport;
-import net.ladenthin.maven.llamacpp.aiindex.support.AiPathSupport;
-import net.ladenthin.maven.llamacpp.aiindex.support.AiTimeSupport;
-import net.ladenthin.maven.llamacpp.aiindex.support.Java8CompatibilityHelper;
+import net.ladenthin.srcmorph.config.AiFieldGenerationConfig;
+import net.ladenthin.srcmorph.config.AiModelDefinitionSupport;
+import net.ladenthin.srcmorph.document.AiGenerationResult;
+import net.ladenthin.srcmorph.document.AiMdChildEntryLineFormatter;
+import net.ladenthin.srcmorph.document.AiMdDocument;
+import net.ladenthin.srcmorph.document.AiMdDocumentCodec;
+import net.ladenthin.srcmorph.document.AiMdHeader;
+import net.ladenthin.srcmorph.document.AiMdHeaderCodec;
+import net.ladenthin.srcmorph.document.AiMdHeaderSupport;
+import net.ladenthin.srcmorph.prompt.AiPromptPreparationSupport;
+import net.ladenthin.srcmorph.prompt.AiPromptSupport;
+import net.ladenthin.srcmorph.provider.AiGenerationProvider;
+import net.ladenthin.srcmorph.support.AiChecksumSupport;
+import net.ladenthin.srcmorph.support.AiPathSupport;
+import net.ladenthin.srcmorph.support.AiTimeSupport;
+import net.ladenthin.srcmorph.support.Java8CompatibilityHelper;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -225,7 +225,7 @@ private boolean shouldCreatePackageFile(final Path directory) throws IOException
/**
* Returns {@code true} when {@code directory} contains a
- * {@link net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeaderCodec#PACKAGE_AI_MD_FILENAME} file.
+ * {@link net.ladenthin.srcmorph.document.AiMdHeaderCodec#PACKAGE_AI_MD_FILENAME} file.
*
* @param directory the directory to examine
* @return {@code true} if the package AI index file exists inside {@code directory}
@@ -374,7 +374,7 @@ private String markdownLink(final String label, final String target) {
* package contents.
*
*
No explicit length cap is applied here: the combined text is subject to the existing
- * trim logic in {@link net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptPreparationSupport#preparePrompt}, which truncates at a line
+ * trim logic in {@link net.ladenthin.srcmorph.prompt.AiPromptPreparationSupport#preparePrompt}, which truncates at a line
* boundary to the computed {@code maxInputChars} budget and triggers the {@code warnOnTrim}
* warning — so large packages degrade gracefully without a bespoke truncation scheme.
*
@@ -595,8 +595,8 @@ private String calculatePackageDate(final Path directory) throws IOException {
}
/**
- * Reads and returns the {@link net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeader} from the
- * {@link net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeaderCodec#PACKAGE_AI_MD_FILENAME} file inside {@code directory}.
+ * Reads and returns the {@link net.ladenthin.srcmorph.document.AiMdHeader} from the
+ * {@link net.ladenthin.srcmorph.document.AiMdHeaderCodec#PACKAGE_AI_MD_FILENAME} file inside {@code directory}.
* The caller must ensure {@link #hasPackageAiMdFile(Path)} is {@code true} first.
*
* @param directory a directory that contains a {@code package.ai.md} file
@@ -625,11 +625,11 @@ private String laterDate(final String current, final String candidate) {
/**
* Returns {@code true} when {@code name} refers to a child AI index file that should
* be included in content listings and checksum calculations. Excludes the
- * {@link net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeaderCodec#PACKAGE_AI_MD_FILENAME} file itself, the
- * project-level {@link net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeaderCodec#PROJECT_AI_MD_FILENAME}
+ * {@link net.ladenthin.srcmorph.document.AiMdHeaderCodec#PACKAGE_AI_MD_FILENAME} file itself, the
+ * project-level {@link net.ladenthin.srcmorph.document.AiMdHeaderCodec#PROJECT_AI_MD_FILENAME}
* (which the aggregator writes into the output root and must not fold back into its own
* checksum or summary on a re-run), and any
- * {@link net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeaderCodec#GENERATED_BY_PREFIX} marker files.
+ * {@link net.ladenthin.srcmorph.document.AiMdHeaderCodec#GENERATED_BY_PREFIX} marker files.
*
* @param name file name of the path under examination
* @return {@code true} if the file is a regular content AI index entry
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/ProjectIndexer.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/ProjectIndexer.java
similarity index 95%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/ProjectIndexer.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/ProjectIndexer.java
index 6c29756..88ae580 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/ProjectIndexer.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/ProjectIndexer.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.indexer;
+package net.ladenthin.srcmorph.indexer;
import java.io.IOException;
import java.nio.file.Files;
@@ -12,21 +12,21 @@
import java.util.List;
import java.util.stream.Stream;
import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFieldGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinitionSupport;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationResult;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdDocument;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdDocumentCodec;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeader;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeaderCodec;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeaderSupport;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdLeadExtractor;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptPreparationSupport;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptSupport;
-import net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationProvider;
-import net.ladenthin.maven.llamacpp.aiindex.support.AiChecksumSupport;
-import net.ladenthin.maven.llamacpp.aiindex.support.AiTimeSupport;
-import net.ladenthin.maven.llamacpp.aiindex.support.Java8CompatibilityHelper;
+import net.ladenthin.srcmorph.config.AiFieldGenerationConfig;
+import net.ladenthin.srcmorph.config.AiModelDefinitionSupport;
+import net.ladenthin.srcmorph.document.AiGenerationResult;
+import net.ladenthin.srcmorph.document.AiMdDocument;
+import net.ladenthin.srcmorph.document.AiMdDocumentCodec;
+import net.ladenthin.srcmorph.document.AiMdHeader;
+import net.ladenthin.srcmorph.document.AiMdHeaderCodec;
+import net.ladenthin.srcmorph.document.AiMdHeaderSupport;
+import net.ladenthin.srcmorph.document.AiMdLeadExtractor;
+import net.ladenthin.srcmorph.prompt.AiPromptPreparationSupport;
+import net.ladenthin.srcmorph.prompt.AiPromptSupport;
+import net.ladenthin.srcmorph.provider.AiGenerationProvider;
+import net.ladenthin.srcmorph.support.AiChecksumSupport;
+import net.ladenthin.srcmorph.support.AiTimeSupport;
+import net.ladenthin.srcmorph.support.Java8CompatibilityHelper;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/SourceFileIndexer.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/SourceFileIndexer.java
similarity index 92%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/SourceFileIndexer.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/SourceFileIndexer.java
index 82dafef..aabeeca 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/SourceFileIndexer.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/SourceFileIndexer.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.indexer;
+package net.ladenthin.srcmorph.indexer;
import java.io.IOException;
import java.nio.file.Files;
@@ -12,28 +12,28 @@
import java.util.List;
import java.util.stream.Stream;
import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiCalibration;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiCondition;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiConditionEvaluator;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFieldGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFieldGenerationSelector;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFileContext;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinitionSupport;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiOversizeStrategy;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationResult;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdDocument;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdDocumentCodec;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeader;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeaderCodec;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeaderSupport;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptPreparationSupport;
-import net.ladenthin.maven.llamacpp.aiindex.support.AiChecksumSupport;
-import net.ladenthin.maven.llamacpp.aiindex.support.AiGenerationTimeEstimator;
-import net.ladenthin.maven.llamacpp.aiindex.support.AiPathSupport;
-import net.ladenthin.maven.llamacpp.aiindex.support.AiSourceExcludeFilter;
-import net.ladenthin.maven.llamacpp.aiindex.support.AiTimeSupport;
-import net.ladenthin.maven.llamacpp.aiindex.support.Java8CompatibilityHelper;
+import net.ladenthin.srcmorph.config.AiCalibration;
+import net.ladenthin.srcmorph.config.AiCondition;
+import net.ladenthin.srcmorph.config.AiConditionEvaluator;
+import net.ladenthin.srcmorph.config.AiFieldGenerationConfig;
+import net.ladenthin.srcmorph.config.AiFieldGenerationSelector;
+import net.ladenthin.srcmorph.config.AiFileContext;
+import net.ladenthin.srcmorph.config.AiGenerationConfig;
+import net.ladenthin.srcmorph.config.AiModelDefinitionSupport;
+import net.ladenthin.srcmorph.config.AiOversizeStrategy;
+import net.ladenthin.srcmorph.document.AiGenerationResult;
+import net.ladenthin.srcmorph.document.AiMdDocument;
+import net.ladenthin.srcmorph.document.AiMdDocumentCodec;
+import net.ladenthin.srcmorph.document.AiMdHeader;
+import net.ladenthin.srcmorph.document.AiMdHeaderCodec;
+import net.ladenthin.srcmorph.document.AiMdHeaderSupport;
+import net.ladenthin.srcmorph.prompt.AiPromptPreparationSupport;
+import net.ladenthin.srcmorph.support.AiChecksumSupport;
+import net.ladenthin.srcmorph.support.AiGenerationTimeEstimator;
+import net.ladenthin.srcmorph.support.AiPathSupport;
+import net.ladenthin.srcmorph.support.AiSourceExcludeFilter;
+import net.ladenthin.srcmorph.support.AiTimeSupport;
+import net.ladenthin.srcmorph.support.Java8CompatibilityHelper;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/package-info.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/package-info.java
similarity index 79%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/package-info.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/package-info.java
index 4ee43fd..d78b932 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/indexer/package-info.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/indexer/package-info.java
@@ -5,4 +5,4 @@
/**
* Orchestration: walks source/package trees and drives field generation.
*/
-package net.ladenthin.maven.llamacpp.aiindex.indexer;
+package net.ladenthin.srcmorph.indexer;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/prompt/AiPreparedPrompt.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/prompt/AiPreparedPrompt.java
similarity index 92%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/prompt/AiPreparedPrompt.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/prompt/AiPreparedPrompt.java
index 21e4d19..b919606 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/prompt/AiPreparedPrompt.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/prompt/AiPreparedPrompt.java
@@ -1,18 +1,18 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.prompt;
+package net.ladenthin.srcmorph.prompt;
import java.util.Objects;
import lombok.EqualsAndHashCode;
import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.support.ConvertToRecord;
+import net.ladenthin.srcmorph.support.ConvertToRecord;
/**
* Immutable result of preparing a prompt: the substituted prompt text, the source text
* actually included, and metrics describing how the source was trimmed (if at all).
*
- *
Record-shaped value type marked {@link net.ladenthin.maven.llamacpp.aiindex.support.ConvertToRecord} for the future Java 17+
+ *
Record-shaped value type marked {@link net.ladenthin.srcmorph.support.ConvertToRecord} for the future Java 17+
* migration. Accessors are record-style ({@code prompt()}, {@code sourceText()}, ...).
* {@code equals}, {@code hashCode} and {@code toString} are generated by Lombok over all
* six fields; full value semantics apply.
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/prompt/AiPromptDefinition.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/prompt/AiPromptDefinition.java
similarity index 97%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/prompt/AiPromptDefinition.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/prompt/AiPromptDefinition.java
index f41c61b..5da82f1 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/prompt/AiPromptDefinition.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/prompt/AiPromptDefinition.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.prompt;
+package net.ladenthin.srcmorph.prompt;
import lombok.ToString;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/prompt/AiPromptPreparationSupport.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/prompt/AiPromptPreparationSupport.java
similarity index 95%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/prompt/AiPromptPreparationSupport.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/prompt/AiPromptPreparationSupport.java
index 6833061..168be92 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/prompt/AiPromptPreparationSupport.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/prompt/AiPromptPreparationSupport.java
@@ -1,13 +1,13 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.prompt;
+package net.ladenthin.srcmorph.prompt;
import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationRequest;
+import net.ladenthin.srcmorph.document.AiGenerationRequest;
/**
- * Prepares prompts for {@link net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationProvider} calls by substituting the source
+ * Prepares prompts for {@link net.ladenthin.srcmorph.provider.AiGenerationProvider} calls by substituting the source
* text into a template and trimming it at a line boundary so that the prompt fits
* within the configured character budget.
*/
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/prompt/AiPromptSupport.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/prompt/AiPromptSupport.java
similarity index 91%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/prompt/AiPromptSupport.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/prompt/AiPromptSupport.java
index b8234ed..f61b423 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/prompt/AiPromptSupport.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/prompt/AiPromptSupport.java
@@ -1,15 +1,16 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.prompt;
+package net.ladenthin.srcmorph.prompt;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationRequest;
-import net.ladenthin.maven.llamacpp.aiindex.support.Java8CompatibilityHelper;
+import net.ladenthin.srcmorph.document.AiGenerationRequest;
+import net.ladenthin.srcmorph.support.Java8CompatibilityHelper;
+import org.jspecify.annotations.Nullable;
/** Registry of prompt templates that renders prompt strings for AI generation requests. */
@ToString
@@ -38,10 +39,12 @@ public final class AiPromptSupport {
* makes misconfigured POM {@code } fail at build
* configuration time rather than silently dropping the entry and
* surfacing as a "Missing prompt template for key" failure deeper in
- * the goal. Mojos wrap construction in {@link NullPointerException}
- * → {@link org.apache.maven.plugin.MojoExecutionException} so the
+ * the goal. Callers (e.g. the llamacpp-ai-index-maven-plugin module's Mojos) wrap
+ * construction in {@link NullPointerException}
+ * → {@code org.apache.maven.plugin.MojoExecutionException} so the
* Maven framework reports it as a user configuration error rather
- * than a plugin bug.
+ * than a plugin bug. This library itself has no Maven dependency, so that type is
+ * referenced here only in prose.
*
* @param promptDefinitions prompt definitions to register; may be
* {@code null} (treated as no definitions);
@@ -49,7 +52,7 @@ public final class AiPromptSupport {
* @throws NullPointerException if any entry has a {@code null} {@code key}
* or {@code null} {@code template}
*/
- public AiPromptSupport(final List promptDefinitions) {
+ public AiPromptSupport(final @Nullable List promptDefinitions) {
if (promptDefinitions == null) {
this.templates = new HashMap<>(compatibilityHelper.hashMapCapacityFor(0));
return;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/prompt/package-info.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/prompt/package-info.java
similarity index 76%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/prompt/package-info.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/prompt/package-info.java
index 711c7e3..53e7a5b 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/prompt/package-info.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/prompt/package-info.java
@@ -5,4 +5,4 @@
/**
* Prompt templates, preparation and lookup.
*/
-package net.ladenthin.maven.llamacpp.aiindex.prompt;
+package net.ladenthin.srcmorph.prompt;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiCompletionParser.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/provider/AiCompletionParser.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiCompletionParser.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/provider/AiCompletionParser.java
index 91e7384..801119f 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiCompletionParser.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/provider/AiCompletionParser.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.provider;
+package net.ladenthin.srcmorph.provider;
import java.io.IOException;
import lombok.ToString;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiGenerationProvider.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/provider/AiGenerationProvider.java
similarity index 85%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiGenerationProvider.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/provider/AiGenerationProvider.java
index 3699410..69a608b 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiGenerationProvider.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/provider/AiGenerationProvider.java
@@ -1,13 +1,13 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.provider;
+package net.ladenthin.srcmorph.provider;
import java.io.IOException;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationRequest;
+import net.ladenthin.srcmorph.document.AiGenerationRequest;
/**
- * Pluggable AI backend that produces text for an {@link net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationRequest}.
+ * Pluggable AI backend that produces text for an {@link net.ladenthin.srcmorph.document.AiGenerationRequest}.
* Implementations may run locally (llama.cpp) or be mock providers for tests.
*/
public interface AiGenerationProvider extends AutoCloseable {
@@ -23,7 +23,7 @@ public interface AiGenerationProvider extends AutoCloseable {
/**
* Generates text and returns it together with the model's measured timing, for the
- * {@code ai-index:calibrate} goal. The default implementation delegates to {@link #generate} and
+ * {@code srcmorph:calibrate} goal. The default implementation delegates to {@link #generate} and
* reports no timings (rates {@code 0}); providers that expose timings override this.
*
* @param request the generation request
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiGenerationProviderFactory.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/provider/AiGenerationProviderFactory.java
similarity index 89%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiGenerationProviderFactory.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/provider/AiGenerationProviderFactory.java
index 3a4e0f1..a7aeb34 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiGenerationProviderFactory.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/provider/AiGenerationProviderFactory.java
@@ -1,11 +1,11 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.provider;
+package net.ladenthin.srcmorph.provider;
import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptSupport;
-import net.ladenthin.maven.llamacpp.aiindex.support.Java8CompatibilityHelper;
+import net.ladenthin.srcmorph.prompt.AiPromptSupport;
+import net.ladenthin.srcmorph.support.Java8CompatibilityHelper;
/** Selects and instantiates an {@link AiGenerationProvider} implementation by name. */
@ToString
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiGenerationTimings.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/provider/AiGenerationTimings.java
similarity index 93%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiGenerationTimings.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/provider/AiGenerationTimings.java
index f493442..d33084d 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiGenerationTimings.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/provider/AiGenerationTimings.java
@@ -1,15 +1,15 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.provider;
+package net.ladenthin.srcmorph.provider;
import lombok.EqualsAndHashCode;
import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.support.ConvertToRecord;
+import net.ladenthin.srcmorph.support.ConvertToRecord;
/**
* A generated text plus the model's measured timing for that generation, used by the
- * {@code ai-index:calibrate} goal to derive per-machine throughput. Prefill = prompt processing; decode =
+ * {@code srcmorph:calibrate} goal to derive per-machine throughput. Prefill = prompt processing; decode =
* answer generation. Rates of {@code 0} mean the provider did not report timings (e.g. the mock provider
* or a binding that omits them).
*
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/LlamaCppJniAiGenerationProvider.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/provider/LlamaCppJniAiGenerationProvider.java
similarity index 97%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/LlamaCppJniAiGenerationProvider.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/provider/LlamaCppJniAiGenerationProvider.java
index ea0aa5a..9614e1b 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/LlamaCppJniAiGenerationProvider.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/provider/LlamaCppJniAiGenerationProvider.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.provider;
+package net.ladenthin.srcmorph.provider;
import java.io.IOException;
import java.util.ArrayList;
@@ -18,9 +18,9 @@
import net.ladenthin.llama.value.ChatResponse;
import net.ladenthin.llama.value.Pair;
import net.ladenthin.llama.value.Timings;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationRequest;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptSupport;
-import net.ladenthin.maven.llamacpp.aiindex.support.Java8CompatibilityHelper;
+import net.ladenthin.srcmorph.document.AiGenerationRequest;
+import net.ladenthin.srcmorph.prompt.AiPromptSupport;
+import net.ladenthin.srcmorph.support.Java8CompatibilityHelper;
import org.jspecify.annotations.Nullable;
/**
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/LlamaCppJniConfig.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/provider/LlamaCppJniConfig.java
similarity index 96%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/LlamaCppJniConfig.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/provider/LlamaCppJniConfig.java
index 644a435..8867878 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/LlamaCppJniConfig.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/provider/LlamaCppJniConfig.java
@@ -1,19 +1,20 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.provider;
+package net.ladenthin.srcmorph.provider;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import lombok.EqualsAndHashCode;
import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.support.ConvertToRecord;
+import net.ladenthin.srcmorph.support.ConvertToRecord;
+import org.jspecify.annotations.Nullable;
/**
* Immutable configuration for the llama.cpp JNI provider.
*
- *
Record-shaped value type marked {@link net.ladenthin.maven.llamacpp.aiindex.support.ConvertToRecord} for the future Java 17+
+ *
Record-shaped value type marked {@link net.ladenthin.srcmorph.support.ConvertToRecord} for the future Java 17+
* migration. Accessors are record-style. {@code equals}, {@code hashCode} and
* {@code toString} are generated by Lombok over all twenty-six fields; full value semantics
* apply. Lombok emits the exact-bit float comparison
@@ -23,7 +24,7 @@
@ToString
@EqualsAndHashCode
public final class LlamaCppJniConfig {
- private final String libraryPath;
+ private final @Nullable String libraryPath;
private final String modelPath;
private final int contextSize;
private final int maxOutputTokens;
@@ -81,7 +82,7 @@ public final class LlamaCppJniConfig {
* @param stopStrings stop strings; may be {@code null} (treated as empty)
*/
public LlamaCppJniConfig(
- String libraryPath,
+ @Nullable String libraryPath,
String modelPath,
int contextSize,
int maxOutputTokens,
@@ -141,7 +142,7 @@ public LlamaCppJniConfig(
*
* @return native library path, or {@code null} to use the bundled library
*/
- public String libraryPath() {
+ public @Nullable String libraryPath() {
return libraryPath;
}
diff --git a/srcmorph/src/main/java/net/ladenthin/srcmorph/provider/LlamaCppJniConfigFactory.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/provider/LlamaCppJniConfigFactory.java
new file mode 100644
index 0000000..f5a8ed7
--- /dev/null
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/provider/LlamaCppJniConfigFactory.java
@@ -0,0 +1,119 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.provider;
+
+import java.util.Collections;
+import java.util.List;
+import net.ladenthin.srcmorph.config.AiGenerationConfig;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Pure mapping from a resolved {@link AiGenerationConfig} (or a small set of fallback parameters) to an
+ * immutable {@link LlamaCppJniConfig}.
+ *
+ *
Extracted from what was {@code AbstractAiIndexMojo.buildLlamaCppJniConfig} in the
+ * {@code llamacpp-ai-index-maven-plugin} module: that method's model-lookup and
+ * fieldGenerations-vs-fallback branching now lives in the {@code engine} package's
+ * {@code EngineSupport}, which calls the two static methods here to do the actual field-by-field
+ * translation. Both methods are pure — no I/O, no lookups, no thrown exceptions — so they are fully
+ * unit-testable and are the project's PIT mutation-coverage target for this translation.
+ */
+public final class LlamaCppJniConfigFactory {
+
+ private LlamaCppJniConfigFactory() {
+ // utility class — not instantiable
+ }
+
+ /**
+ * Builds a {@link LlamaCppJniConfig} by copying every field from a resolved
+ * {@link AiGenerationConfig} (an {@link net.ladenthin.srcmorph.config.AiModelDefinition} looked up by
+ * key). {@code null} {@link AiGenerationConfig#getStopStrings()} /
+ * {@link AiGenerationConfig#getDrySequenceBreakers()} are normalised to an empty list.
+ *
+ * @param libraryPath native library path; may be {@code null} to use the bundled native library
+ * @param config the resolved AI model generation config
+ * @return the fully populated llama.cpp configuration
+ */
+ public static LlamaCppJniConfig fromGenerationConfig(
+ final @Nullable String libraryPath, final AiGenerationConfig config) {
+ final List stopStrings = config.getStopStrings();
+ final List drySequenceBreakers = config.getDrySequenceBreakers();
+ return new LlamaCppJniConfig(
+ libraryPath,
+ config.getModelPath(),
+ config.getContextSize(),
+ config.getMaxOutputTokens(),
+ config.getTemperature(),
+ config.getThreads(),
+ config.getTopP(),
+ config.getTopK(),
+ config.getMinP(),
+ config.getTopNSigma(),
+ config.getRepeatPenalty(),
+ config.isChatTemplateEnableThinking(),
+ config.isCachePrompt(),
+ config.isSwaFull(),
+ config.getCacheReuse(),
+ config.getGpuLayers(),
+ config.getMainGpu(),
+ config.getDevices(),
+ config.getReasoningEffort(),
+ config.getReasoningBudgetTokens(),
+ config.getDryMultiplier(),
+ config.getDryBase(),
+ config.getDryAllowedLength(),
+ config.getDryPenaltyLastN(),
+ drySequenceBreakers != null ? drySequenceBreakers : Collections.emptyList(),
+ stopStrings != null ? stopStrings : Collections.emptyList());
+ }
+
+ /**
+ * Builds a {@link LlamaCppJniConfig} from the small set of individual fallback parameters (used when
+ * no {@code fieldGenerations}/routing rule is configured), applying every other
+ * {@link AiGenerationConfig} default (sampling, DRY, GPU, …) unchanged.
+ *
+ * @param libraryPath native library path; may be {@code null} to use the bundled native library
+ * @param modelPath path to the GGUF model file
+ * @param contextSize context window size in tokens
+ * @param maxOutputTokens maximum number of output tokens per call
+ * @param temperature sampling temperature
+ * @param threads number of CPU threads
+ * @return the fully populated llama.cpp configuration
+ */
+ public static LlamaCppJniConfig fromFallbackParameters(
+ final @Nullable String libraryPath,
+ final String modelPath,
+ final int contextSize,
+ final int maxOutputTokens,
+ final float temperature,
+ final int threads) {
+ return new LlamaCppJniConfig(
+ libraryPath,
+ modelPath,
+ contextSize,
+ maxOutputTokens,
+ temperature,
+ threads,
+ AiGenerationConfig.DEFAULT_TOP_P,
+ AiGenerationConfig.DEFAULT_TOP_K,
+ AiGenerationConfig.DEFAULT_MIN_P,
+ AiGenerationConfig.DEFAULT_TOP_N_SIGMA,
+ AiGenerationConfig.DEFAULT_REPEAT_PENALTY,
+ AiGenerationConfig.DEFAULT_CHAT_TEMPLATE_ENABLE_THINKING,
+ AiGenerationConfig.DEFAULT_CACHE_PROMPT,
+ AiGenerationConfig.DEFAULT_SWA_FULL,
+ AiGenerationConfig.DEFAULT_CACHE_REUSE,
+ AiGenerationConfig.DEFAULT_GPU_LAYERS,
+ AiGenerationConfig.DEFAULT_MAIN_GPU,
+ AiGenerationConfig.DEFAULT_DEVICES,
+ AiGenerationConfig.DEFAULT_REASONING_EFFORT,
+ AiGenerationConfig.DEFAULT_REASONING_BUDGET_TOKENS,
+ AiGenerationConfig.DEFAULT_DRY_MULTIPLIER,
+ AiGenerationConfig.DEFAULT_DRY_BASE,
+ AiGenerationConfig.DEFAULT_DRY_ALLOWED_LENGTH,
+ AiGenerationConfig.DEFAULT_DRY_PENALTY_LAST_N,
+ Collections.emptyList(),
+ Collections.emptyList());
+ }
+}
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/MockAiGenerationProvider.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/provider/MockAiGenerationProvider.java
similarity index 93%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/MockAiGenerationProvider.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/provider/MockAiGenerationProvider.java
index feadf42..6958459 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/MockAiGenerationProvider.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/provider/MockAiGenerationProvider.java
@@ -1,12 +1,12 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.provider;
+package net.ladenthin.srcmorph.provider;
import java.io.IOException;
import java.nio.file.Path;
import lombok.ToString;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationRequest;
+import net.ladenthin.srcmorph.document.AiGenerationRequest;
/** Deterministic {@link AiGenerationProvider} that returns a mock summary; used for testing. */
@ToString
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/package-info.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/provider/package-info.java
similarity index 80%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/package-info.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/provider/package-info.java
index 0275a87..ca3475a 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/provider/package-info.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/provider/package-info.java
@@ -5,4 +5,4 @@
/**
* AI generation backends (mock + llama.cpp JNI) and the generation request/result carriers.
*/
-package net.ladenthin.maven.llamacpp.aiindex.provider;
+package net.ladenthin.srcmorph.provider;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiChecksumSupport.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiChecksumSupport.java
similarity index 96%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiChecksumSupport.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiChecksumSupport.java
index e7ee078..3433fc5 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiChecksumSupport.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiChecksumSupport.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.support;
+package net.ladenthin.srcmorph.support;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiDeterministicSummary.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiDeterministicSummary.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiDeterministicSummary.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiDeterministicSummary.java
index aeb4897..380679c 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiDeterministicSummary.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiDeterministicSummary.java
@@ -3,7 +3,7 @@
//
// SPDX-License-Identifier: Apache-2.0
// @formatter:on
-package net.ladenthin.maven.llamacpp.aiindex.support;
+package net.ladenthin.srcmorph.support;
/**
* Builds a model-free, deterministic Markdown body for files too large (or too uninteresting) to feed to
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiGenerationTimeEstimator.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiGenerationTimeEstimator.java
similarity index 99%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiGenerationTimeEstimator.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiGenerationTimeEstimator.java
index 96fc6bc..62ebd8d 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiGenerationTimeEstimator.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiGenerationTimeEstimator.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.support;
+package net.ladenthin.srcmorph.support;
import lombok.ToString;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiPathSupport.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiPathSupport.java
similarity index 96%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiPathSupport.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiPathSupport.java
index d50d90f..ec1a910 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiPathSupport.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiPathSupport.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.support;
+package net.ladenthin.srcmorph.support;
import java.nio.file.Path;
import lombok.ToString;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiProgressBar.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiProgressBar.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiProgressBar.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiProgressBar.java
index 5db1815..fed3c03 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiProgressBar.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiProgressBar.java
@@ -3,7 +3,7 @@
//
// SPDX-License-Identifier: Apache-2.0
// @formatter:on
-package net.ladenthin.maven.llamacpp.aiindex.support;
+package net.ladenthin.srcmorph.support;
/**
* Renders a simple ASCII progress bar from a completed/total ratio, e.g. {@code [##### ] 42%}.
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiSourceChunker.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiSourceChunker.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiSourceChunker.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiSourceChunker.java
index c5884b5..1da9883 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiSourceChunker.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiSourceChunker.java
@@ -3,7 +3,7 @@
//
// SPDX-License-Identifier: Apache-2.0
// @formatter:on
-package net.ladenthin.maven.llamacpp.aiindex.support;
+package net.ladenthin.srcmorph.support;
import java.util.ArrayList;
import java.util.LinkedHashSet;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiSourceExcludeFilter.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiSourceExcludeFilter.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiSourceExcludeFilter.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiSourceExcludeFilter.java
index 83d596e..48c9f41 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiSourceExcludeFilter.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiSourceExcludeFilter.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.support;
+package net.ladenthin.srcmorph.support;
import java.util.ArrayList;
import java.util.Collection;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiTimeSupport.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiTimeSupport.java
similarity index 96%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiTimeSupport.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiTimeSupport.java
index 60254f2..dcd197e 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/AiTimeSupport.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/support/AiTimeSupport.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.support;
+package net.ladenthin.srcmorph.support;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/ConvertToRecord.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/support/ConvertToRecord.java
similarity index 85%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/ConvertToRecord.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/support/ConvertToRecord.java
index 8490995..7a7dca2 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/ConvertToRecord.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/support/ConvertToRecord.java
@@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.support;
+package net.ladenthin.srcmorph.support;
/**
* Marker annotation for classes that should be migrated to a Java {@code record}
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/Java8CompatibilityHelper.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/support/Java8CompatibilityHelper.java
similarity index 99%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/Java8CompatibilityHelper.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/support/Java8CompatibilityHelper.java
index a7e7b70..f201d5e 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/Java8CompatibilityHelper.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/support/Java8CompatibilityHelper.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.support;
+package net.ladenthin.srcmorph.support;
import java.io.IOException;
import java.nio.charset.Charset;
diff --git a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/package-info.java b/srcmorph/src/main/java/net/ladenthin/srcmorph/support/package-info.java
similarity index 79%
rename from llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/package-info.java
rename to srcmorph/src/main/java/net/ladenthin/srcmorph/support/package-info.java
index 8028cb1..dc32c5b 100644
--- a/llamacpp-ai-index-maven-plugin/src/main/java/net/ladenthin/maven/llamacpp/aiindex/support/package-info.java
+++ b/srcmorph/src/main/java/net/ladenthin/srcmorph/support/package-info.java
@@ -5,4 +5,4 @@
/**
* Stateless foundation utilities (checksums, time, paths, Java-8 helpers).
*/
-package net.ladenthin.maven.llamacpp.aiindex.support;
+package net.ladenthin.srcmorph.support;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/CommonTestFixtures.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/CommonTestFixtures.java
similarity index 93%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/CommonTestFixtures.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/CommonTestFixtures.java
index c7c6bd8..0fb1fd1 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/CommonTestFixtures.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/CommonTestFixtures.java
@@ -1,15 +1,15 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex;
+package net.ladenthin.srcmorph;
import java.util.Arrays;
import java.util.List;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFieldGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinition;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinitionSupport;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptDefinition;
+import net.ladenthin.srcmorph.config.AiFieldGenerationConfig;
+import net.ladenthin.srcmorph.config.AiGenerationConfig;
+import net.ladenthin.srcmorph.config.AiModelDefinition;
+import net.ladenthin.srcmorph.config.AiModelDefinitionSupport;
+import net.ladenthin.srcmorph.prompt.AiPromptDefinition;
/**
* Shared test fixture factory methods used across multiple test classes.
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/PluginArchitectureTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/CoreArchitectureTest.java
similarity index 58%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/PluginArchitectureTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/CoreArchitectureTest.java
index 2c1b2dd..48f8818 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/PluginArchitectureTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/CoreArchitectureTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex;
+package net.ladenthin.srcmorph;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.fields;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
@@ -13,20 +13,27 @@
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import java.util.Random;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinition;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptDefinition;
import org.slf4j.Logger;
-@AnalyzeClasses(packages = "net.ladenthin.maven.llamacpp.aiindex", importOptions = ImportOption.DoNotIncludeTests.class)
-public class PluginArchitectureTest {
+/**
+ * Architecture rules for the srcmorph core library (extracted from the
+ * llamacpp-ai-index-maven-plugin's non-mojo layers: config/document/indexer/prompt/provider/
+ * support). This library is framework-free — no Maven plugin API dependency anywhere — so it can
+ * be unit-tested and consumed without a Maven runtime. The Mojo entry points that build on top of
+ * it live in the sibling {@code llamacpp-ai-index-maven-plugin} module, whose own
+ * {@code PluginArchitectureTest} carries the mojo-specific rules (Maven annotations confined to
+ * {@code mojo}, {@code AbstractMojo} inheritance, etc.) that have no meaning here.
+ */
+@AnalyzeClasses(packages = "net.ladenthin.srcmorph", importOptions = ImportOption.DoNotIncludeTests.class)
+public class CoreArchitectureTest {
/**
- * Maven plugins use Maven's Log interface for logging; java.util.logging must not be used.
+ * java.util.logging must not be used; all logging goes through SLF4J.
*/
@ArchTest
static final ArchRule noJavaUtilLogging = noClasses()
.that()
- .resideInAPackage("net.ladenthin.maven.llamacpp.aiindex..")
+ .resideInAPackage("net.ladenthin.srcmorph..")
.should()
.dependOnClassesThat()
.resideInAPackage("java.util.logging..");
@@ -37,21 +44,20 @@ public class PluginArchitectureTest {
@ArchTest
static final ArchRule noTestFrameworksInProduction = noClasses()
.that()
- .resideInAPackage("net.ladenthin.maven.llamacpp.aiindex..")
+ .resideInAPackage("net.ladenthin.srcmorph..")
.should()
.dependOnClassesThat()
.resideInAnyPackage("org.junit..", "net.jqwik..", "com.tngtech.archunit..");
/**
* Production code must not write to {@code System.out} / {@code System.err}; all output
- * goes through an SLF4J {@link Logger} (mojo-lifecycle messages still use Maven's
- * {@code Log} via {@code getLog()}). Currently vacuous (no usage); acts as a regression
+ * goes through an SLF4J {@link Logger}. Currently vacuous (no usage); acts as a regression
* guard.
*/
@ArchTest
static final ArchRule noSystemOutOrErrInProduction = noClasses()
.that()
- .resideInAPackage("net.ladenthin.maven.llamacpp.aiindex..")
+ .resideInAPackage("net.ladenthin.srcmorph..")
.should()
.accessField(System.class, "out")
.orShould()
@@ -64,7 +70,7 @@ public class PluginArchitectureTest {
@ArchTest
static final ArchRule noInternalJdkImports = noClasses()
.that()
- .resideInAPackage("net.ladenthin.maven.llamacpp.aiindex..")
+ .resideInAPackage("net.ladenthin.srcmorph..")
.should()
.dependOnClassesThat()
.resideInAnyPackage("sun..", "com.sun..", "jdk.internal..");
@@ -73,7 +79,7 @@ public class PluginArchitectureTest {
* No package cycles between the layered sub-packages.
*/
@ArchTest
- static final ArchRule noPackageCycles = slices().matching("net.ladenthin.maven.llamacpp.aiindex.(*)..")
+ static final ArchRule noPackageCycles = slices().matching("net.ladenthin.srcmorph.(*)..")
.should()
.beFreeOfCycles()
.allowEmptyShould(true);
@@ -84,48 +90,60 @@ public class PluginArchitectureTest {
* (verified against the compiled bytecode graph), so even intra-tier edges are governed
* (e.g. {@code provider} may use {@code document}+{@code prompt} but {@code document} and
* {@code prompt} may not use each other beyond what is listed). A new dependency between any
- * two packages fails the build unless this rule is updated to intend it. Conceptual tiers:
- * {@code Mojo} > {@code Indexer} > {@code Provider} > {@code Document}/{@code Prompt}
- * > {@code Config}/{@code Support}.
+ * two packages fails the build unless this rule is updated to intend it. Mirrors the pre-
+ * extraction {@code PluginArchitectureTest} layering with the {@code Mojo} layer removed (the
+ * mojo package lives in the sibling plugin module and is outside this test's analyzed
+ * classes; consideringOnlyDependenciesInLayers() means its former accesses to Indexer/
+ * Provider/Prompt/Config/Support are simply not part of this in-module rule anymore) — and with
+ * a new {@code Engine} layer on top, added for the per-phase orchestration engines (migration
+ * step 5): {@code engine} depends on {@code indexer} (the file/package/project indexers),
+ * {@code provider} (constructing/closing an {@code AiGenerationProvider} directly),
+ * {@code prompt} ({@code AiPromptSupport}/{@code AiPromptPreparationSupport}), {@code config}
+ * (the shared {@code SrcMorphConfiguration} plus the routing/model-lookup types), and
+ * {@code support} (the progress bar / time estimator / Java 8 compatibility helpers). The new
+ * {@code provider.LlamaCppJniConfigFactory} also introduced a {@code Provider -> Config} edge
+ * (it maps a {@code config.AiGenerationConfig} to a {@code provider.LlamaCppJniConfig}), and the
+ * new root {@code config.SrcMorphConfiguration} introduced a {@code Config -> Prompt} edge (it
+ * holds a {@code List}). Conceptual tiers: {@code Engine} >
+ * {@code Indexer} > {@code Provider} > {@code Document}/{@code Prompt} >
+ * {@code Config}/{@code Support}.
*/
@ArchTest
static final ArchRule layeredArchitecture = layeredArchitecture()
.consideringOnlyDependenciesInLayers()
- .layer("Mojo")
- .definedBy("net.ladenthin.maven.llamacpp.aiindex.mojo..")
+ .layer("Engine")
+ .definedBy("net.ladenthin.srcmorph.engine..")
.layer("Indexer")
- .definedBy("net.ladenthin.maven.llamacpp.aiindex.indexer..")
+ .definedBy("net.ladenthin.srcmorph.indexer..")
.layer("Provider")
- .definedBy("net.ladenthin.maven.llamacpp.aiindex.provider..")
+ .definedBy("net.ladenthin.srcmorph.provider..")
.layer("Document")
- .definedBy("net.ladenthin.maven.llamacpp.aiindex.document..")
+ .definedBy("net.ladenthin.srcmorph.document..")
.layer("Prompt")
- .definedBy("net.ladenthin.maven.llamacpp.aiindex.prompt..")
+ .definedBy("net.ladenthin.srcmorph.prompt..")
.layer("Config")
- .definedBy("net.ladenthin.maven.llamacpp.aiindex.config..")
+ .definedBy("net.ladenthin.srcmorph.config..")
.layer("Support")
- .definedBy("net.ladenthin.maven.llamacpp.aiindex.support..")
- .whereLayer("Mojo")
+ .definedBy("net.ladenthin.srcmorph.support..")
+ .whereLayer("Engine")
.mayNotBeAccessedByAnyLayer()
.whereLayer("Indexer")
- .mayOnlyBeAccessedByLayers("Mojo")
+ .mayOnlyBeAccessedByLayers("Engine")
.whereLayer("Provider")
- .mayOnlyBeAccessedByLayers("Mojo", "Indexer")
+ .mayOnlyBeAccessedByLayers("Indexer", "Engine")
.whereLayer("Document")
.mayOnlyBeAccessedByLayers("Indexer", "Prompt", "Provider")
.whereLayer("Prompt")
- .mayOnlyBeAccessedByLayers("Indexer", "Mojo", "Provider")
+ .mayOnlyBeAccessedByLayers("Indexer", "Provider", "Engine", "Config")
.whereLayer("Config")
- .mayOnlyBeAccessedByLayers("Indexer", "Mojo")
+ .mayOnlyBeAccessedByLayers("Indexer", "Engine", "Provider")
.whereLayer("Support")
- .mayOnlyBeAccessedByLayers("Config", "Document", "Indexer", "Mojo", "Prompt", "Provider");
+ .mayOnlyBeAccessedByLayers("Config", "Document", "Indexer", "Prompt", "Provider", "Engine");
/**
* Public mutable state forbidden: any non-static field declared
* {@code public} must also be {@code final}. Maven plugin configuration
- * POJOs use {@code private} fields with setters
- * ({@link AiPromptDefinition}, {@link AiModelDefinition}, etc.) and
- * therefore pass this rule.
+ * POJOs use {@code private} fields with setters and therefore pass this rule.
*/
@ArchTest
static final ArchRule noPublicMutableFields = fields().that()
@@ -137,15 +155,13 @@ public class PluginArchitectureTest {
.allowEmptyShould(true); // regression guard; passes vacuously today
/**
- * Production code must not call {@link System#exit(int)}; throw a
- * {@link org.apache.maven.plugin.MojoExecutionException} or
- * {@link org.apache.maven.plugin.MojoFailureException} instead so Maven
- * surfaces the failure to its caller.
+ * Production code must not call {@link System#exit(int)}; throw a domain exception instead
+ * so the calling Mojo (or any other embedder) surfaces the failure to its caller.
*/
@ArchTest
static final ArchRule noSystemExit = noClasses()
.that()
- .resideInAPackage("net.ladenthin.maven.llamacpp.aiindex..")
+ .resideInAPackage("net.ladenthin.srcmorph..")
.should()
.callMethod(System.class, "exit", int.class)
.allowEmptyShould(true);
@@ -158,7 +174,7 @@ public class PluginArchitectureTest {
@ArchTest
static final ArchRule noNewRandom = noClasses()
.that()
- .resideInAPackage("net.ladenthin.maven.llamacpp.aiindex..")
+ .resideInAPackage("net.ladenthin.srcmorph..")
.should()
.callConstructor(Random.class)
.orShould()
@@ -173,7 +189,7 @@ public class PluginArchitectureTest {
@ArchTest
static final ArchRule noThreadSleep = noClasses()
.that()
- .resideInAPackage("net.ladenthin.maven.llamacpp.aiindex..")
+ .resideInAPackage("net.ladenthin.srcmorph..")
.should()
.callMethod(Thread.class, "sleep", long.class)
.orShould()
@@ -182,58 +198,38 @@ public class PluginArchitectureTest {
// ---------------------------------------------------------------------------------------
// Per-module banned imports — confine the heavy / framework-specific dependencies to the
- // single layer that owns them, keeping the rest of the plugin decoupled and unit-testable.
+ // single layer that owns them, keeping the rest of the library decoupled and unit-testable.
// ---------------------------------------------------------------------------------------
/**
* The llama.cpp JNI binding ({@code net.ladenthin.llama..}) may only be referenced from the
* {@code provider} package, where {@code LlamaCppJniAiGenerationProvider} wraps it. Every
* other layer talks to the model exclusively through the {@code AiGenerationProvider}
- * interface, so the indexers, document/prompt/config and mojo code carry no JNI dependency
- * and stay testable with {@code MockAiGenerationProvider}.
+ * interface, so the indexers and document/prompt/config code carry no JNI dependency and
+ * stay testable with {@code MockAiGenerationProvider}.
*/
@ArchTest
static final ArchRule jniConfinedToProvider = noClasses()
.that()
- .resideOutsideOfPackage("net.ladenthin.maven.llamacpp.aiindex.provider..")
+ .resideOutsideOfPackage("net.ladenthin.srcmorph.provider..")
.should()
.dependOnClassesThat()
.resideInAPackage("net.ladenthin.llama..")
.allowEmptyShould(true);
/**
- * The Maven Mojo SPI annotations ({@code @Mojo} / {@code @Parameter} from
- * {@code org.apache.maven.plugins.annotations..}) may only appear in the {@code mojo}
- * package — the goal entry points. The lower layers are plain Java and must not be
- * annotated as Maven components.
+ * This library must be entirely framework-free: no Maven API at all (not even {@code Log}).
+ * Every class here is plain Java, so it can be unit-tested and consumed without a Maven
+ * runtime — that is the whole point of the extraction. The Maven Plugin API boundary
+ * ({@code AbstractMojo}, {@code @Parameter}, {@code MojoExecutionException}, the {@code mojo}
+ * package's Maven Mojo SPI annotations) lives entirely in the sibling
+ * {@code llamacpp-ai-index-maven-plugin} module, which is outside this test's analyzed
+ * classes and carries its own {@code mavenMojoAnnotationsConfinedToMojo} rule.
*/
@ArchTest
- static final ArchRule mavenMojoAnnotationsConfinedToMojo = noClasses()
+ static final ArchRule coreIsMavenFree = noClasses()
.that()
- .resideOutsideOfPackage("net.ladenthin.maven.llamacpp.aiindex.mojo..")
- .should()
- .dependOnClassesThat()
- .resideInAPackage("org.apache.maven.plugins.annotations..")
- .allowEmptyShould(true);
-
- /**
- * Every layer except {@code mojo} must be framework-free: no Maven API at all (not even
- * {@code Log} — logging in {@code indexer} goes through an SLF4J {@link Logger} instead).
- * They are plain Java, so they can be unit-tested and eventually extracted into a
- * standalone library without a Maven runtime. Only {@code mojo} is the Maven Plugin API
- * boundary ({@code AbstractMojo}, {@code @Parameter}, {@code MojoExecutionException}, and
- * the injected {@code getLog()} for mojo-lifecycle messages).
- */
- @ArchTest
- static final ArchRule nonMojoIsMavenFree = noClasses()
- .that()
- .resideInAnyPackage(
- "net.ladenthin.maven.llamacpp.aiindex.config..",
- "net.ladenthin.maven.llamacpp.aiindex.support..",
- "net.ladenthin.maven.llamacpp.aiindex.document..",
- "net.ladenthin.maven.llamacpp.aiindex.prompt..",
- "net.ladenthin.maven.llamacpp.aiindex.provider..",
- "net.ladenthin.maven.llamacpp.aiindex.indexer..")
+ .resideInAPackage("net.ladenthin.srcmorph..")
.should()
.dependOnClassesThat()
.resideInAPackage("org.apache.maven..")
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiCalibrationTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiCalibrationTest.java
similarity index 96%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiCalibrationTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiCalibrationTest.java
index 2b292c1..da22534 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiCalibrationTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiCalibrationTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiConditionEvaluatorTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiConditionEvaluatorTest.java
similarity index 99%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiConditionEvaluatorTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiConditionEvaluatorTest.java
index c94ab88..5d2a8b9 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiConditionEvaluatorTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiConditionEvaluatorTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiConditionGroupTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiConditionGroupTest.java
similarity index 96%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiConditionGroupTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiConditionGroupTest.java
index 3e1ff0f..8a7d405 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiConditionGroupTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiConditionGroupTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiConditionTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiConditionTest.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiConditionTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiConditionTest.java
index 9ece2e4..868fb4b 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiConditionTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiConditionTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.hasItem;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactCounterTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiFactCounterTest.java
similarity index 96%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactCounterTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiFactCounterTest.java
index 93d4b3d..c5e1c1e 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactCounterTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiFactCounterTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactDefinitionSupportTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiFactDefinitionSupportTest.java
similarity index 99%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactDefinitionSupportTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiFactDefinitionSupportTest.java
index f888d2a..b69193f 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactDefinitionSupportTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiFactDefinitionSupportTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactDefinitionTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiFactDefinitionTest.java
similarity index 96%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactDefinitionTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiFactDefinitionTest.java
index 0f07be0..c0ecc2f 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactDefinitionTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiFactDefinitionTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactExtractorTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiFactExtractorTest.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactExtractorTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiFactExtractorTest.java
index e2cf199..dd059b2 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFactExtractorTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiFactExtractorTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFieldGenerationConfigTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiFieldGenerationConfigTest.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFieldGenerationConfigTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiFieldGenerationConfigTest.java
index 9ad8a68..71e0bc7 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFieldGenerationConfigTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiFieldGenerationConfigTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFieldGenerationSelectorTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiFieldGenerationSelectorTest.java
similarity index 99%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFieldGenerationSelectorTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiFieldGenerationSelectorTest.java
index 10f08c5..649e537 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFieldGenerationSelectorTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiFieldGenerationSelectorTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFileContextTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiFileContextTest.java
similarity index 93%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFileContextTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiFileContextTest.java
index 63138a9..7c51b97 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiFileContextTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiFileContextTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiGenerationConfigTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiGenerationConfigTest.java
similarity index 99%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiGenerationConfigTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiGenerationConfigTest.java
index 1fa3bff..18e5400 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiGenerationConfigTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiGenerationConfigTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiGenerationKindLincheckTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiGenerationKindLincheckTest.java
similarity index 95%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiGenerationKindLincheckTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiGenerationKindLincheckTest.java
index aa2d9f2..20ca6f3 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiGenerationKindLincheckTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiGenerationKindLincheckTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import java.util.concurrent.atomic.AtomicInteger;
import org.jetbrains.kotlinx.lincheck.LinChecker;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiModelDefinitionSupportTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiModelDefinitionSupportTest.java
similarity index 99%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiModelDefinitionSupportTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiModelDefinitionSupportTest.java
index 656a5aa..0a7949b 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiModelDefinitionSupportTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiModelDefinitionSupportTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiModelDefinitionTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiModelDefinitionTest.java
similarity index 99%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiModelDefinitionTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiModelDefinitionTest.java
index d618635..62fd14b 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiModelDefinitionTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiModelDefinitionTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiOversizeStrategyTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiOversizeStrategyTest.java
similarity index 97%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiOversizeStrategyTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiOversizeStrategyTest.java
index d38f946..c730fb4 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiOversizeStrategyTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiOversizeStrategyTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiRangeConditionTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiRangeConditionTest.java
similarity index 97%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiRangeConditionTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiRangeConditionTest.java
index d597226..fcbf504 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/AiRangeConditionTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/AiRangeConditionTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/JavaStructureFactPatternsTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/JavaStructureFactPatternsTest.java
similarity index 99%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/JavaStructureFactPatternsTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/config/JavaStructureFactPatternsTest.java
index 8bec48d..78ea128 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/config/JavaStructureFactPatternsTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/JavaStructureFactPatternsTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.config;
+package net.ladenthin.srcmorph.config;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
diff --git a/srcmorph/src/test/java/net/ladenthin/srcmorph/config/SrcMorphConfigurationTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/SrcMorphConfigurationTest.java
new file mode 100644
index 0000000..bdcc32f
--- /dev/null
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/config/SrcMorphConfigurationTest.java
@@ -0,0 +1,231 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.config;
+
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.CoreMatchers.sameInstance;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.io.File;
+import java.util.Arrays;
+import net.ladenthin.srcmorph.prompt.AiPromptDefinition;
+import org.junit.jupiter.api.Test;
+
+public class SrcMorphConfigurationTest {
+
+ @Test
+ public void defaults_matchDocumentedConstants() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+
+ assertThat(c.getOutputDirectory(), is(new File(SrcMorphConfiguration.DEFAULT_OUTPUT_DIRECTORY)));
+ assertThat(c.isForce(), is(false));
+ assertThat(c.isPlanOnly(), is(false));
+ assertThat(c.getGenerationProvider(), is(SrcMorphConfiguration.DEFAULT_GENERATION_PROVIDER));
+ assertThat(c.getLlamaContextSize(), is(SrcMorphConfiguration.DEFAULT_LLAMA_CONTEXT_SIZE));
+ assertThat(c.getLlamaMaxOutputTokens(), is(SrcMorphConfiguration.DEFAULT_LLAMA_MAX_OUTPUT_TOKENS));
+ assertThat(c.getLlamaTemperature(), is(SrcMorphConfiguration.DEFAULT_LLAMA_TEMPERATURE));
+ assertThat(c.getLlamaThreads(), is(SrcMorphConfiguration.DEFAULT_LLAMA_THREADS));
+ assertThat(c.getAiVersion(), is(SrcMorphConfiguration.DEFAULT_AI_VERSION));
+ assertThat(c.getMinFileSizeBytes(), is(0L));
+ assertThat(c.getMaxFileSizeBytes(), is(0L));
+ assertThat(c.getPluginVersion(), is(""));
+ }
+
+ @Test
+ public void nullableCollectionsAndStringsAreNullByDefault() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+
+ assertThat(c.getSubtrees(), is(nullValue()));
+ assertThat(c.getExcludes(), is(nullValue()));
+ assertThat(c.getFileExtensions(), is(nullValue()));
+ assertThat(c.getPromptDefinitions(), is(nullValue()));
+ assertThat(c.getAiDefinitions(), is(nullValue()));
+ assertThat(c.getFieldGenerations(), is(nullValue()));
+ assertThat(c.getFactDefinitions(), is(nullValue()));
+ assertThat(c.getLlamaLibraryPath(), is(nullValue()));
+ assertThat(c.getLlamaModelPath(), is(nullValue()));
+ assertThat(c.getProjectName(), is(nullValue()));
+ }
+
+ @Test
+ public void baseDirectoryRoundTrips() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ final File dir = new File("/tmp/base");
+ c.setBaseDirectory(dir);
+ assertThat(c.getBaseDirectory(), is(sameInstance(dir)));
+ }
+
+ @Test
+ public void outputDirectoryRoundTrips() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ final File dir = new File("/tmp/out");
+ c.setOutputDirectory(dir);
+ assertThat(c.getOutputDirectory(), is(sameInstance(dir)));
+ }
+
+ @Test
+ public void forceRoundTrips() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ c.setForce(true);
+ assertThat(c.isForce(), is(true));
+ }
+
+ @Test
+ public void planOnlyRoundTrips() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ c.setPlanOnly(true);
+ assertThat(c.isPlanOnly(), is(true));
+ }
+
+ @Test
+ public void subtreesRoundTrips() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ c.setSubtrees(Arrays.asList("src/main/java/a"));
+ assertThat(c.getSubtrees(), hasItem("src/main/java/a"));
+ }
+
+ @Test
+ public void excludesRoundTrips() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ c.setExcludes(Arrays.asList("**/generated/**"));
+ assertThat(c.getExcludes(), hasItem("**/generated/**"));
+ }
+
+ @Test
+ public void fileExtensionsRoundTrips() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ c.setFileExtensions(Arrays.asList(".kt"));
+ assertThat(c.getFileExtensions(), hasItem(".kt"));
+ }
+
+ @Test
+ public void minFileSizeBytesRoundTrips() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ c.setMinFileSizeBytes(123L);
+ assertThat(c.getMinFileSizeBytes(), is(123L));
+ }
+
+ @Test
+ public void maxFileSizeBytesRoundTrips() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ c.setMaxFileSizeBytes(456L);
+ assertThat(c.getMaxFileSizeBytes(), is(456L));
+ }
+
+ @Test
+ public void generationProviderRoundTrips() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ c.setGenerationProvider("llamacpp-jni");
+ assertThat(c.getGenerationProvider(), is("llamacpp-jni"));
+ }
+
+ @Test
+ public void promptDefinitionsRoundTrips() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ final AiPromptDefinition p = new AiPromptDefinition();
+ p.setKey("k");
+ p.setTemplate("t");
+ c.setPromptDefinitions(Arrays.asList(p));
+ assertThat(c.getPromptDefinitions(), hasItem(p));
+ }
+
+ @Test
+ public void aiDefinitionsRoundTrips() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ final AiModelDefinition m = new AiModelDefinition();
+ m.setKey("m");
+ c.setAiDefinitions(Arrays.asList(m));
+ assertThat(c.getAiDefinitions(), hasItem(m));
+ }
+
+ @Test
+ public void fieldGenerationsRoundTrips() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ final AiFieldGenerationConfig f = new AiFieldGenerationConfig();
+ f.setPromptKey("p");
+ c.setFieldGenerations(Arrays.asList(f));
+ assertThat(c.getFieldGenerations(), hasItem(f));
+ }
+
+ @Test
+ public void factDefinitionsRoundTrips() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ final AiFactDefinition fd = new AiFactDefinition();
+ fd.setKey("fd");
+ c.setFactDefinitions(Arrays.asList(fd));
+ assertThat(c.getFactDefinitions(), hasItem(fd));
+ }
+
+ @Test
+ public void llamaLibraryPathRoundTrips() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ c.setLlamaLibraryPath("/opt/lib/libjllama.so");
+ assertThat(c.getLlamaLibraryPath(), is("/opt/lib/libjllama.so"));
+ }
+
+ @Test
+ public void llamaModelPathRoundTrips() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ c.setLlamaModelPath("model.gguf");
+ assertThat(c.getLlamaModelPath(), is("model.gguf"));
+ }
+
+ @Test
+ public void llamaContextSizeRoundTrips() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ c.setLlamaContextSize(8192);
+ assertThat(c.getLlamaContextSize(), is(8192));
+ }
+
+ @Test
+ public void llamaMaxOutputTokensRoundTrips() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ c.setLlamaMaxOutputTokens(4096);
+ assertThat(c.getLlamaMaxOutputTokens(), is(4096));
+ }
+
+ @Test
+ public void llamaTemperatureRoundTrips() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ c.setLlamaTemperature(0.9f);
+ assertThat(c.getLlamaTemperature(), is(0.9f));
+ }
+
+ @Test
+ public void llamaThreadsRoundTrips() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ c.setLlamaThreads(16);
+ assertThat(c.getLlamaThreads(), is(16));
+ }
+
+ @Test
+ public void pluginVersionRoundTrips() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ c.setPluginVersion("1.2.3");
+ assertThat(c.getPluginVersion(), is("1.2.3"));
+ }
+
+ @Test
+ public void aiVersionRoundTrips() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ c.setAiVersion("2.0.0");
+ assertThat(c.getAiVersion(), is("2.0.0"));
+ }
+
+ @Test
+ public void projectNameRoundTrips() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ c.setProjectName("my-project");
+ assertThat(c.getProjectName(), is("my-project"));
+ }
+
+ @Test
+ public void toStringContainsDistinguishingField() {
+ final SrcMorphConfiguration c = new SrcMorphConfiguration();
+ c.setGenerationProvider("llamacpp-jni");
+ assertThat(c.toString(), org.hamcrest.CoreMatchers.containsString("llamacpp-jni"));
+ }
+}
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdChildEntryLineFormatterTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/document/AiMdChildEntryLineFormatterTest.java
similarity index 94%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdChildEntryLineFormatterTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/document/AiMdChildEntryLineFormatterTest.java
index dcf938b..27d0b58 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdChildEntryLineFormatterTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/document/AiMdChildEntryLineFormatterTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.document;
+package net.ladenthin.srcmorph.document;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdDocumentCodecTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/document/AiMdDocumentCodecTest.java
similarity index 99%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdDocumentCodecTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/document/AiMdDocumentCodecTest.java
index 222b2ce..cdc40cf 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdDocumentCodecTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/document/AiMdDocumentCodecTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.document;
+package net.ladenthin.srcmorph.document;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdHeaderCodecProperties.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/document/AiMdHeaderCodecProperties.java
similarity index 94%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdHeaderCodecProperties.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/document/AiMdHeaderCodecProperties.java
index 5ea5448..a3b5c7b 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdHeaderCodecProperties.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/document/AiMdHeaderCodecProperties.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.document;
+package net.ladenthin.srcmorph.document;
import java.util.Arrays;
import net.jqwik.api.ForAll;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdHeaderCodecTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/document/AiMdHeaderCodecTest.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdHeaderCodecTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/document/AiMdHeaderCodecTest.java
index bcd06b7..77eba11 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdHeaderCodecTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/document/AiMdHeaderCodecTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.document;
+package net.ladenthin.srcmorph.document;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdHeaderSupportTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/document/AiMdHeaderSupportTest.java
similarity index 99%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdHeaderSupportTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/document/AiMdHeaderSupportTest.java
index d629c63..8c93745 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdHeaderSupportTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/document/AiMdHeaderSupportTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.document;
+package net.ladenthin.srcmorph.document;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdHeaderTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/document/AiMdHeaderTest.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdHeaderTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/document/AiMdHeaderTest.java
index 6fd6bf7..e1996fd 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdHeaderTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/document/AiMdHeaderTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.document;
+package net.ladenthin.srcmorph.document;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdLeadExtractorTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/document/AiMdLeadExtractorTest.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdLeadExtractorTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/document/AiMdLeadExtractorTest.java
index 1a38001..fe17e9c 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/document/AiMdLeadExtractorTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/document/AiMdLeadExtractorTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.document;
+package net.ladenthin.srcmorph.document;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
diff --git a/srcmorph/src/test/java/net/ladenthin/srcmorph/engine/AggregatePackagesEngineTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/engine/AggregatePackagesEngineTest.java
new file mode 100644
index 0000000..5069532
--- /dev/null
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/engine/AggregatePackagesEngineTest.java
@@ -0,0 +1,91 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.engine;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import net.ladenthin.srcmorph.CommonTestFixtures;
+import net.ladenthin.srcmorph.config.AiModelDefinition;
+import net.ladenthin.srcmorph.config.SrcMorphConfiguration;
+import net.ladenthin.srcmorph.document.AiMdDocument;
+import net.ladenthin.srcmorph.document.AiMdDocumentCodec;
+import net.ladenthin.srcmorph.document.AiMdHeader;
+import net.ladenthin.srcmorph.document.AiMdHeaderCodec;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+public class AggregatePackagesEngineTest {
+
+ @TempDir
+ Path tempDir;
+
+ private final AiMdDocumentCodec documentCodec = new AiMdDocumentCodec();
+
+ /**
+ * A model definition with a (dummy, never loaded) {@code modelPath} set: even with the mock
+ * provider, the engine always builds a
+ * {@link net.ladenthin.srcmorph.provider.LlamaCppJniConfig} value object, which requires a
+ * non-null model path.
+ */
+ private static AiModelDefinition mockModelDefinition() {
+ final AiModelDefinition definition = new AiModelDefinition();
+ definition.setKey(CommonTestFixtures.AI_DEFINITION_KEY_DEFAULT);
+ definition.setModelPath("mock.gguf");
+ definition.setCharsPerToken(0);
+ return definition;
+ }
+
+ @Test
+ public void execute_missingOutputDirectoryReturnsZeroWithoutError() throws Exception {
+ final SrcMorphConfiguration config = new SrcMorphConfiguration();
+ config.setBaseDirectory(tempDir.toFile());
+ config.setOutputDirectory(tempDir.resolve("out-does-not-exist").toFile());
+ config.setGenerationProvider("mock");
+
+ final int aggregated = new AggregatePackagesEngine(config).execute();
+
+ assertThat(aggregated, is(0));
+ }
+
+ @Test
+ public void execute_aggregatesOneChildIntoPackageAiMd() throws Exception {
+ final Path outputRoot = tempDir.resolve("ai");
+ final Path packageDirectory = outputRoot.resolve("com/example");
+ final Path childAiFile = packageDirectory.resolve("Test.java.ai.md");
+ Files.createDirectories(packageDirectory);
+
+ final AiMdHeader childHeader = new AiMdHeader(
+ "Test.java",
+ AiMdHeaderCodec.HEADER_VERSION_1_0,
+ "AAAAAAAA",
+ "2026-03-16T00:00:00Z",
+ "2026-03-16T00:00:10Z",
+ "1.0.0",
+ "0.0.0",
+ AiMdHeaderCodec.NODE_TYPE_FILE);
+ documentCodec.write(childAiFile, new AiMdDocument(childHeader, ""));
+
+ final SrcMorphConfiguration config = new SrcMorphConfiguration();
+ config.setBaseDirectory(tempDir.toFile());
+ config.setOutputDirectory(outputRoot.toFile());
+ config.setGenerationProvider("mock");
+ config.setPluginVersion("1.0.0");
+ config.setAiVersion("0.0.0");
+ config.setPromptDefinitions(CommonTestFixtures.createPackagePromptDefinitions());
+ config.setAiDefinitions(Collections.singletonList(mockModelDefinition()));
+ config.setFieldGenerations(CommonTestFixtures.createPackageFieldGenerations());
+
+ final int aggregated = new AggregatePackagesEngine(config).execute();
+
+ // One package.ai.md per directory from the output root down to the child's own package
+ // (root "ai/", "com/", and "com/example/"), matching PackageIndexer's recursive aggregation.
+ assertThat(aggregated, is(equalTo(3)));
+ assertThat(Files.exists(packageDirectory.resolve(AiMdHeaderCodec.PACKAGE_AI_MD_FILENAME)), is(true));
+ }
+}
diff --git a/srcmorph/src/test/java/net/ladenthin/srcmorph/engine/AggregateProjectEngineTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/engine/AggregateProjectEngineTest.java
new file mode 100644
index 0000000..6f2970c
--- /dev/null
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/engine/AggregateProjectEngineTest.java
@@ -0,0 +1,130 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.engine;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import net.ladenthin.srcmorph.CommonTestFixtures;
+import net.ladenthin.srcmorph.config.AiFieldGenerationConfig;
+import net.ladenthin.srcmorph.config.AiModelDefinition;
+import net.ladenthin.srcmorph.config.SrcMorphConfiguration;
+import net.ladenthin.srcmorph.document.AiMdDocument;
+import net.ladenthin.srcmorph.document.AiMdDocumentCodec;
+import net.ladenthin.srcmorph.document.AiMdHeader;
+import net.ladenthin.srcmorph.document.AiMdHeaderCodec;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+public class AggregateProjectEngineTest {
+
+ @TempDir
+ Path tempDir;
+
+ private final AiMdDocumentCodec documentCodec = new AiMdDocumentCodec();
+
+ /**
+ * A model definition with a (dummy, never loaded) {@code modelPath} set: even with the mock
+ * provider, the engine always builds a
+ * {@link net.ladenthin.srcmorph.provider.LlamaCppJniConfig} value object, which requires a
+ * non-null model path.
+ */
+ private static AiModelDefinition mockModelDefinition() {
+ final AiModelDefinition definition = new AiModelDefinition();
+ definition.setKey(CommonTestFixtures.AI_DEFINITION_KEY_DEFAULT);
+ definition.setModelPath("mock.gguf");
+ definition.setCharsPerToken(0);
+ return definition;
+ }
+
+ private void writePackageFile(final Path packageFile, final String title, final String body) throws Exception {
+ Files.createDirectories(packageFile.getParent());
+ final AiMdHeader header = new AiMdHeader(
+ title,
+ AiMdHeaderCodec.HEADER_VERSION_1_0,
+ "AAAAAAAA",
+ "2026-03-16T00:00:00Z",
+ "2026-03-16T00:00:10Z",
+ "1.0.0",
+ "0.0.0",
+ AiMdHeaderCodec.NODE_TYPE_PACKAGE);
+ documentCodec.write(packageFile, new AiMdDocument(header, body));
+ }
+
+ @Test
+ public void execute_missingOutputDirectoryReturnsZero() throws Exception {
+ final SrcMorphConfiguration config = new SrcMorphConfiguration();
+ config.setOutputDirectory(tempDir.resolve("out-does-not-exist").toFile());
+
+ assertThat(new AggregateProjectEngine(config).execute(), is(0));
+ }
+
+ @Test
+ public void execute_deterministicNoOverview_writesProjectIndex() throws Exception {
+ final Path outputRoot = tempDir.resolve("ai");
+ writePackageFile(
+ outputRoot.resolve("com/example/package.ai.md"), "com/example", "> Handles the example domain.\n");
+
+ final SrcMorphConfiguration config = new SrcMorphConfiguration();
+ config.setOutputDirectory(outputRoot.toFile());
+ config.setPluginVersion("1.0.0");
+ config.setAiVersion("0.0.0");
+ config.setProjectName("my-project");
+
+ final int written = new AggregateProjectEngine(config).execute();
+
+ assertThat(written, is(equalTo(1)));
+ final Path projectFile = outputRoot.resolve(AiMdHeaderCodec.PROJECT_AI_MD_FILENAME);
+ assertThat(Files.exists(projectFile), is(true));
+ final AiMdDocument document = documentCodec.read(projectFile);
+ assertThat(document.header().title(), is(equalTo("my-project")));
+ assertThat(document.body(), containsString("Handles the example domain."));
+ }
+
+ @Test
+ public void execute_blankProjectNameFallsBackToDefaultTitle() throws Exception {
+ final Path outputRoot = tempDir.resolve("ai");
+ writePackageFile(outputRoot.resolve("com/example/package.ai.md"), "com/example", "> Lead.\n");
+
+ final SrcMorphConfiguration config = new SrcMorphConfiguration();
+ config.setOutputDirectory(outputRoot.toFile());
+ config.setPluginVersion("1.0.0");
+ config.setAiVersion("0.0.0");
+ config.setProjectName(" ");
+
+ new AggregateProjectEngine(config).execute();
+
+ final AiMdDocument document = documentCodec.read(outputRoot.resolve(AiMdHeaderCodec.PROJECT_AI_MD_FILENAME));
+ assertThat(document.header().title(), is(equalTo("project")));
+ }
+
+ @Test
+ public void execute_withOverviewFieldGeneration_generatesOverviewSection() throws Exception {
+ final Path outputRoot = tempDir.resolve("ai");
+ writePackageFile(outputRoot.resolve("com/example/package.ai.md"), "com/example", "> Handles billing.\n");
+
+ final SrcMorphConfiguration config = new SrcMorphConfiguration();
+ config.setOutputDirectory(outputRoot.toFile());
+ config.setPluginVersion("1.0.0");
+ config.setAiVersion("0.0.0");
+ config.setProjectName("my-project");
+ config.setGenerationProvider("mock");
+ config.setPromptDefinitions(CommonTestFixtures.createPackagePromptDefinitions());
+ config.setAiDefinitions(Collections.singletonList(mockModelDefinition()));
+ final AiFieldGenerationConfig overview = new AiFieldGenerationConfig();
+ overview.setPromptKey(CommonTestFixtures.PROMPT_KEY_PACKAGE_BODY);
+ overview.setAiDefinitionKey(CommonTestFixtures.AI_DEFINITION_KEY_DEFAULT);
+ config.setFieldGenerations(Collections.singletonList(overview));
+
+ new AggregateProjectEngine(config).execute();
+
+ final AiMdDocument document = documentCodec.read(outputRoot.resolve(AiMdHeaderCodec.PROJECT_AI_MD_FILENAME));
+ assertThat(document.body(), containsString("#### Overview"));
+ }
+}
diff --git a/srcmorph/src/test/java/net/ladenthin/srcmorph/engine/CalibrateEngineTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/engine/CalibrateEngineTest.java
new file mode 100644
index 0000000..a12fdc9
--- /dev/null
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/engine/CalibrateEngineTest.java
@@ -0,0 +1,97 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.engine;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.Collections;
+import net.ladenthin.srcmorph.CommonTestFixtures;
+import net.ladenthin.srcmorph.config.AiFieldGenerationConfig;
+import net.ladenthin.srcmorph.config.AiModelDefinition;
+import net.ladenthin.srcmorph.config.SrcMorphConfiguration;
+import org.junit.jupiter.api.Test;
+
+public class CalibrateEngineTest {
+
+ /**
+ * A model definition with a (dummy, never loaded) {@code modelPath} set: even with the mock
+ * provider, the engine always builds a
+ * {@link net.ladenthin.srcmorph.provider.LlamaCppJniConfig} value object, which requires a
+ * non-null model path.
+ */
+ private static AiModelDefinition mockModelDefinition() {
+ final AiModelDefinition definition = new AiModelDefinition();
+ definition.setKey(CommonTestFixtures.AI_DEFINITION_KEY_DEFAULT);
+ definition.setModelPath("mock.gguf");
+ definition.setCharsPerToken(0);
+ return definition;
+ }
+
+ private SrcMorphConfiguration baseConfig() {
+ final SrcMorphConfiguration config = new SrcMorphConfiguration();
+ config.setGenerationProvider("mock");
+ config.setPromptDefinitions(CommonTestFixtures.createFilePromptDefinitions());
+ config.setAiDefinitions(Collections.singletonList(mockModelDefinition()));
+ return config;
+ }
+
+ @Test
+ public void execute_missingFieldGenerationsThrowsSrcMorphException() {
+ final SrcMorphConfiguration config = baseConfig();
+ config.setFieldGenerations(null);
+
+ final SrcMorphException e = assertThrows(SrcMorphException.class, () -> new CalibrateEngine(config).execute());
+ assertThat(e.getMessage(), containsString("nothing to calibrate"));
+ }
+
+ @Test
+ public void execute_noRoutableRuleThrowsSrcMorphException() {
+ final SrcMorphConfiguration config = baseConfig();
+ // A rule with neither promptKey nor aiDefinitionKey set is not routable.
+ config.setFieldGenerations(Collections.singletonList(new AiFieldGenerationConfig()));
+
+ final SrcMorphException e = assertThrows(SrcMorphException.class, () -> new CalibrateEngine(config).execute());
+ assertThat(e.getMessage(), containsString("No routable"));
+ }
+
+ @Test
+ public void execute_calibratesTheRoutedModelAndRendersXml() throws Exception {
+ final SrcMorphConfiguration config = baseConfig();
+ config.setFieldGenerations(CommonTestFixtures.createFileFieldGenerations());
+
+ final CalibrationReport report = new CalibrateEngine(config).execute();
+
+ assertThat(report.measurements().size(), is(1));
+ final CalibrationReport.ModelMeasurement measurement =
+ report.measurements().get(0);
+ assertThat(measurement.modelKey(), is(CommonTestFixtures.AI_DEFINITION_KEY_DEFAULT));
+ // The mock provider reports fixed synthetic throughput; the engine surfaces it unchanged.
+ assertThat(measurement.measurement().prefillTokensPerSecond(), is(1000.0d));
+ assertThat(measurement.measurement().decodeTokensPerSecond(), is(100.0d));
+
+ final String xml = report.renderXml();
+ assertThat(
+ xml, containsString("calibration for aiDefinition '" + CommonTestFixtures.AI_DEFINITION_KEY_DEFAULT));
+ assertThat(xml, containsString("1000.0"));
+ }
+
+ @Test
+ public void execute_dedupesMultipleRulesRoutedToTheSameModel() throws Exception {
+ final SrcMorphConfiguration config = baseConfig();
+ final AiFieldGenerationConfig ruleA = new AiFieldGenerationConfig();
+ ruleA.setPromptKey(CommonTestFixtures.PROMPT_KEY_FILE_BODY);
+ ruleA.setAiDefinitionKey(CommonTestFixtures.AI_DEFINITION_KEY_DEFAULT);
+ final AiFieldGenerationConfig ruleB = new AiFieldGenerationConfig();
+ ruleB.setPromptKey(CommonTestFixtures.PROMPT_KEY_FILE_BODY);
+ ruleB.setAiDefinitionKey(CommonTestFixtures.AI_DEFINITION_KEY_DEFAULT);
+ config.setFieldGenerations(java.util.Arrays.asList(ruleA, ruleB));
+
+ final CalibrationReport report = new CalibrateEngine(config).execute();
+
+ assertThat(report.measurements().size(), is(1));
+ }
+}
diff --git a/srcmorph/src/test/java/net/ladenthin/srcmorph/engine/CalibrationReportTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/engine/CalibrationReportTest.java
new file mode 100644
index 0000000..dafff90
--- /dev/null
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/engine/CalibrationReportTest.java
@@ -0,0 +1,92 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.engine;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import net.ladenthin.srcmorph.indexer.AiCalibrationMeasurement;
+import org.junit.jupiter.api.Test;
+
+public class CalibrationReportTest {
+
+ @Test
+ public void modelMeasurement_accessorsReflectConstructorArguments() {
+ final AiCalibrationMeasurement measurement = new AiCalibrationMeasurement(1.5d, 100.0d, 50.0d, 4.0d, 90.0d);
+ final CalibrationReport.ModelMeasurement m = new CalibrationReport.ModelMeasurement("my-model", measurement);
+
+ assertThat(m.modelKey(), is("my-model"));
+ assertThat(m.measurement(), is(measurement));
+ }
+
+ @Test
+ public void measurements_returnsDefensiveCopyInOrder() {
+ final CalibrationReport.ModelMeasurement m1 =
+ new CalibrationReport.ModelMeasurement("a", new AiCalibrationMeasurement(1.0d, 1.0d, 1.0d, 1.0d, 1.0d));
+ final CalibrationReport.ModelMeasurement m2 =
+ new CalibrationReport.ModelMeasurement("b", new AiCalibrationMeasurement(2.0d, 2.0d, 2.0d, 2.0d, 2.0d));
+ final List source = new ArrayList<>(Arrays.asList(m1, m2));
+ final CalibrationReport report = new CalibrationReport(source);
+
+ // Mutating the source list after construction must not affect the report (defensive copy).
+ source.clear();
+
+ assertThat(report.measurements(), is(equalTo(Arrays.asList(m1, m2))));
+ }
+
+ @Test
+ public void measurements_isUnmodifiable() {
+ final CalibrationReport report = new CalibrationReport(new ArrayList());
+ try {
+ report.measurements()
+ .add(new CalibrationReport.ModelMeasurement(
+ "x", new AiCalibrationMeasurement(0.0d, 0.0d, 0.0d, 0.0d, 0.0d)));
+ org.junit.jupiter.api.Assertions.fail("expected UnsupportedOperationException");
+ } catch (final UnsupportedOperationException expected) {
+ // expected: measurements() must not allow external mutation
+ }
+ }
+
+ @Test
+ public void renderXml_isEmptyForNoMeasurements() {
+ final CalibrationReport report = new CalibrationReport(new ArrayList());
+ assertThat(report.renderXml(), is(""));
+ }
+
+ @Test
+ public void renderXml_rendersOnePasteBlockPerModelInOrder() {
+ final AiCalibrationMeasurement measurementA =
+ new AiCalibrationMeasurement(1.234d, 1000.5d, 200.3d, 4.57d, 900.0d);
+ final AiCalibrationMeasurement measurementB = new AiCalibrationMeasurement(2.0d, 500.0d, 100.0d, 3.0d, 450.0d);
+ final CalibrationReport report = new CalibrationReport(Arrays.asList(
+ new CalibrationReport.ModelMeasurement("model-a", measurementA),
+ new CalibrationReport.ModelMeasurement("model-b", measurementB)));
+
+ final String expected = "\n"
+ + "\n"
+ + " 1000.5\n"
+ + " 200.3\n"
+ + " 4.57\n"
+ + "\n"
+ + "\n"
+ + "\n"
+ + " 500.0\n"
+ + " 100.0\n"
+ + " 3.00\n"
+ + "\n";
+
+ assertThat(report.renderXml(), is(expected));
+ }
+
+ @Test
+ public void toString_containsModelKey() {
+ final CalibrationReport report = new CalibrationReport(Arrays.asList(new CalibrationReport.ModelMeasurement(
+ "unique-model-key", new AiCalibrationMeasurement(0.0d, 0.0d, 0.0d, 0.0d, 0.0d))));
+ assertThat(report.toString(), org.hamcrest.CoreMatchers.containsString("unique-model-key"));
+ }
+}
diff --git a/srcmorph/src/test/java/net/ladenthin/srcmorph/engine/EngineSupportTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/engine/EngineSupportTest.java
new file mode 100644
index 0000000..a3dce2b
--- /dev/null
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/engine/EngineSupportTest.java
@@ -0,0 +1,175 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.engine;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import net.ladenthin.srcmorph.config.AiFieldGenerationConfig;
+import net.ladenthin.srcmorph.config.AiModelDefinition;
+import net.ladenthin.srcmorph.config.AiModelDefinitionSupport;
+import net.ladenthin.srcmorph.config.SrcMorphConfiguration;
+import net.ladenthin.srcmorph.prompt.AiPromptDefinition;
+import net.ladenthin.srcmorph.prompt.AiPromptSupport;
+import net.ladenthin.srcmorph.provider.LlamaCppJniConfig;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+public class EngineSupportTest {
+
+ @TempDir
+ Path tempDir;
+
+ //
+ @Test
+ public void resolveSubtrees_nullReturnsEmpty() {
+ assertThat(EngineSupport.resolveSubtrees(tempDir, null), is(Collections.emptyList()));
+ }
+
+ @Test
+ public void resolveSubtrees_emptyReturnsEmpty() {
+ assertThat(
+ EngineSupport.resolveSubtrees(tempDir, Collections.emptyList()),
+ is(Collections.emptyList()));
+ }
+
+ @Test
+ public void resolveSubtrees_earlyReturnResultIsMutable() {
+ // The early-return (null/empty subtrees) branch returns the same mutable ArrayList as the
+ // loop-completion branch, not an immutable Collections.emptyList(), so callers can treat every
+ // result uniformly. Mutating it here must not throw UnsupportedOperationException.
+ final List resolved = EngineSupport.resolveSubtrees(tempDir, null);
+ resolved.add(tempDir);
+ assertThat(resolved, hasItem(tempDir));
+ }
+
+ @Test
+ public void resolveSubtrees_existingSubtreeIsResolved() throws IOException {
+ final Path sub = tempDir.resolve("exists");
+ Files.createDirectories(sub);
+
+ final List resolved = EngineSupport.resolveSubtrees(tempDir, Arrays.asList("exists"));
+
+ assertThat(resolved, hasItem(sub.normalize()));
+ }
+
+ @Test
+ public void resolveSubtrees_missingSubtreeIsSkipped() {
+ final List resolved = EngineSupport.resolveSubtrees(tempDir, Arrays.asList("does-not-exist"));
+
+ assertThat(resolved, is(Collections.emptyList()));
+ }
+ //
+
+ //
+ @Test
+ public void buildPromptSupport_nullListBuildsEmptySupport() throws SrcMorphException {
+ final AiPromptSupport support = EngineSupport.buildPromptSupport(null);
+ assertThat(support, is(notNullValue()));
+ }
+
+ @Test
+ public void buildPromptSupport_missingKeyThrowsSrcMorphException() {
+ final AiPromptDefinition bad = new AiPromptDefinition();
+ bad.setTemplate("template with no key");
+
+ final SrcMorphException e =
+ assertThrows(SrcMorphException.class, () -> EngineSupport.buildPromptSupport(Arrays.asList(bad)));
+ assertThat(e.getMessage(), containsString("Invalid plugin configuration:"));
+ }
+ //
+
+ //
+ @Test
+ public void buildAiModelDefinitionSupport_nullListBuildsEmptySupport() throws SrcMorphException {
+ assertThat(EngineSupport.buildAiModelDefinitionSupport(null), is(notNullValue()));
+ }
+
+ @Test
+ public void buildAiModelDefinitionSupport_missingKeyThrowsSrcMorphException() {
+ final AiModelDefinition bad = new AiModelDefinition();
+ bad.setModelPath("model.gguf");
+
+ final SrcMorphException e = assertThrows(
+ SrcMorphException.class, () -> EngineSupport.buildAiModelDefinitionSupport(Arrays.asList(bad)));
+ assertThat(e.getMessage(), containsString("Invalid plugin configuration:"));
+ }
+ //
+
+ //
+ @Test
+ public void resolveLlamaCppJniConfig_noKey_usesFallbackWhenNoFieldGenerations() {
+ final SrcMorphConfiguration config = new SrcMorphConfiguration();
+ config.setLlamaModelPath("fallback.gguf");
+ config.setLlamaContextSize(2048);
+ config.setLlamaMaxOutputTokens(64);
+ config.setLlamaTemperature(0.2f);
+ config.setLlamaThreads(3);
+
+ final AiModelDefinitionSupport modelDefinitionSupport = new AiModelDefinitionSupport(null);
+ final LlamaCppJniConfig result = EngineSupport.resolveLlamaCppJniConfig(config, modelDefinitionSupport);
+
+ assertThat(result.modelPath(), is("fallback.gguf"));
+ assertThat(result.contextSize(), is(2048));
+ assertThat(result.maxOutputTokens(), is(64));
+ assertThat(result.temperature(), is(0.2f));
+ assertThat(result.threads(), is(3));
+ }
+
+ @Test
+ public void resolveLlamaCppJniConfig_noKey_usesFirstFieldGenerationsEntryWhenPresent() {
+ final SrcMorphConfiguration config = new SrcMorphConfiguration();
+ final AiFieldGenerationConfig rule = new AiFieldGenerationConfig();
+ rule.setPromptKey("prompt");
+ rule.setAiDefinitionKey("routed-model");
+ config.setFieldGenerations(Arrays.asList(rule));
+
+ final AiModelDefinition definition = new AiModelDefinition();
+ definition.setKey("routed-model");
+ definition.setModelPath("routed.gguf");
+ final AiModelDefinitionSupport modelDefinitionSupport = new AiModelDefinitionSupport(Arrays.asList(definition));
+
+ final LlamaCppJniConfig result = EngineSupport.resolveLlamaCppJniConfig(config, modelDefinitionSupport);
+
+ assertThat(result.modelPath(), is("routed.gguf"));
+ }
+
+ @Test
+ public void resolveLlamaCppJniConfig_byKey_looksUpNamedDefinition() {
+ final SrcMorphConfiguration config = new SrcMorphConfiguration();
+ config.setLlamaLibraryPath("/opt/lib");
+
+ final AiModelDefinition definition = new AiModelDefinition();
+ definition.setKey("named");
+ definition.setModelPath("named.gguf");
+ final AiModelDefinitionSupport modelDefinitionSupport = new AiModelDefinitionSupport(Arrays.asList(definition));
+
+ final LlamaCppJniConfig result =
+ EngineSupport.resolveLlamaCppJniConfig(config, modelDefinitionSupport, "named");
+
+ assertThat(result.libraryPath(), is("/opt/lib"));
+ assertThat(result.modelPath(), is("named.gguf"));
+ }
+
+ @Test
+ public void resolveLlamaCppJniConfig_byKey_missingKeyThrowsIllegalArgumentException() {
+ final SrcMorphConfiguration config = new SrcMorphConfiguration();
+ final AiModelDefinitionSupport modelDefinitionSupport = new AiModelDefinitionSupport(null);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> EngineSupport.resolveLlamaCppJniConfig(config, modelDefinitionSupport, "missing"));
+ }
+ //
+}
diff --git a/srcmorph/src/test/java/net/ladenthin/srcmorph/engine/GenerateEngineTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/engine/GenerateEngineTest.java
new file mode 100644
index 0000000..b1f5232
--- /dev/null
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/engine/GenerateEngineTest.java
@@ -0,0 +1,157 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.engine;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import net.ladenthin.srcmorph.CommonTestFixtures;
+import net.ladenthin.srcmorph.config.AiCondition;
+import net.ladenthin.srcmorph.config.AiFieldGenerationConfig;
+import net.ladenthin.srcmorph.config.AiModelDefinition;
+import net.ladenthin.srcmorph.config.SrcMorphConfiguration;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+public class GenerateEngineTest {
+
+ @TempDir
+ Path tempDir;
+
+ /**
+ * A routable rule matching {@code .java} files, with the {@code default} AI definition key. Unlike
+ * {@link CommonTestFixtures#createFileFieldGenerations()} (whose rule carries no condition — fine for
+ * the indexer-level tests that never call {@link net.ladenthin.srcmorph.config.AiFieldGenerationSelector#validate}),
+ * {@link GenerateEngine#execute()} does call {@code validate}, so the rule here needs a real condition.
+ */
+ private static AiFieldGenerationConfig javaFileRule() {
+ final AiFieldGenerationConfig rule = new AiFieldGenerationConfig();
+ rule.setPromptKey(CommonTestFixtures.PROMPT_KEY_FILE_BODY);
+ rule.setAiDefinitionKey(CommonTestFixtures.AI_DEFINITION_KEY_DEFAULT);
+ final AiCondition condition = new AiCondition();
+ condition.setExtensions(Arrays.asList(".java"));
+ rule.setCondition(condition);
+ return rule;
+ }
+
+ /**
+ * A model definition with a (dummy, never loaded) {@code modelPath} set: even with the mock provider,
+ * the engine always builds a {@link net.ladenthin.srcmorph.provider.LlamaCppJniConfig} value object
+ * before dispatching to {@link net.ladenthin.srcmorph.provider.AiGenerationProviderFactory}, and that
+ * value object requires a non-null model path.
+ */
+ private static AiModelDefinition mockModelDefinition() {
+ final AiModelDefinition definition = new AiModelDefinition();
+ definition.setKey(CommonTestFixtures.AI_DEFINITION_KEY_DEFAULT);
+ definition.setModelPath("mock.gguf");
+ // Disable automatic maxInputChars calculation so the 13-byte test source never trips the
+ // oversize path in the "happy path" tests below.
+ definition.setCharsPerToken(0);
+ return definition;
+ }
+
+ private SrcMorphConfiguration baseConfig() throws IOException {
+ final Path sourceRoot = tempDir.resolve("src/main/java");
+ Files.createDirectories(sourceRoot.resolve("com/example"));
+ Files.write(sourceRoot.resolve("com/example/Foo.java"), "class Foo {}\n".getBytes(StandardCharsets.UTF_8));
+
+ final SrcMorphConfiguration config = new SrcMorphConfiguration();
+ config.setBaseDirectory(tempDir.toFile());
+ config.setOutputDirectory(tempDir.resolve("out").toFile());
+ config.setGenerationProvider("mock");
+ config.setPluginVersion("1.0.0");
+ config.setAiVersion("0.0.0");
+ config.setPromptDefinitions(CommonTestFixtures.createFilePromptDefinitions());
+ config.setAiDefinitions(Collections.singletonList(mockModelDefinition()));
+ config.setFieldGenerations(Collections.singletonList(javaFileRule()));
+ return config;
+ }
+
+ @Test
+ public void execute_missingFieldGenerationsThrowsSrcMorphException() throws IOException {
+ final SrcMorphConfiguration config = baseConfig();
+ config.setFieldGenerations(null);
+
+ final SrcMorphException e = assertThrows(SrcMorphException.class, () -> new GenerateEngine(config).execute());
+ assertThat(e.getMessage(), containsString("No configured"));
+ }
+
+ @Test
+ public void execute_planOnlyStopsBeforeGenerating() throws Exception {
+ final SrcMorphConfiguration config = baseConfig();
+ config.setPlanOnly(true);
+
+ final GenerateResult result = new GenerateEngine(config).execute();
+
+ assertThat(result.planOnly(), is(true));
+ assertThat(result.written(), is(0));
+ assertThat(Files.exists(tempDir.resolve("out")), is(false));
+ }
+
+ @Test
+ public void execute_writesMatchedFileAndReportsCounts() throws Exception {
+ final SrcMorphConfiguration config = baseConfig();
+
+ final GenerateResult result = new GenerateEngine(config).execute();
+
+ assertThat(result.planOnly(), is(false));
+ assertThat(result.written(), is(1));
+ assertThat(result.unchanged(), is(0));
+ assertThat(result.skipped(), is(0));
+ // SourceFileIndexer relativises against the "src" root but keeps the "main/java/..." tail.
+ assertThat(Files.exists(tempDir.resolve("out/main/java/com/example/Foo.java.ai.md")), is(true));
+ }
+
+ @Test
+ public void execute_secondRunWithoutForceReportsUnchanged() throws Exception {
+ final SrcMorphConfiguration config = baseConfig();
+ new GenerateEngine(config).execute();
+
+ final GenerateResult second = new GenerateEngine(config).execute();
+
+ assertThat(second.written(), is(0));
+ assertThat(second.unchanged(), is(1));
+ }
+
+ @Test
+ public void execute_unmatchedFileWithNoFallbackThrowsSrcMorphException() throws IOException {
+ final SrcMorphConfiguration config = baseConfig();
+ final AiFieldGenerationConfig onlyMatchesTxt = new AiFieldGenerationConfig();
+ onlyMatchesTxt.setPromptKey(CommonTestFixtures.PROMPT_KEY_FILE_BODY);
+ onlyMatchesTxt.setAiDefinitionKey(CommonTestFixtures.AI_DEFINITION_KEY_DEFAULT);
+ final AiCondition condition = new AiCondition();
+ condition.setExtensions(Arrays.asList(".txt"));
+ onlyMatchesTxt.setCondition(condition);
+ config.setFieldGenerations(Arrays.asList(onlyMatchesTxt));
+
+ final SrcMorphException e = assertThrows(SrcMorphException.class, () -> new GenerateEngine(config).execute());
+ assertThat(e.getMessage(), containsString("matched no rule and no fallback"));
+ }
+
+ @Test
+ public void execute_oversizeFailThrowsSrcMorphException() throws Exception {
+ final SrcMorphConfiguration config = baseConfig();
+
+ // Force the routed model's window to be smaller than the source, with the default
+ // onOversize=fail strategy, so the file is a hard failure. The window check happens entirely
+ // during planning, before any LlamaCppJniConfig/provider is built, so modelPath is irrelevant here.
+ final AiModelDefinition tinyWindow = new AiModelDefinition();
+ tinyWindow.setKey(CommonTestFixtures.AI_DEFINITION_KEY_DEFAULT);
+ tinyWindow.setCharsPerToken(1);
+ tinyWindow.setContextSize(1);
+ tinyWindow.setMaxOutputTokens(0);
+ config.setAiDefinitions(Collections.singletonList(tinyWindow));
+
+ final SrcMorphException e = assertThrows(SrcMorphException.class, () -> new GenerateEngine(config).execute());
+ assertThat(e.getMessage(), containsString("exceed their routed model's context window"));
+ }
+}
diff --git a/srcmorph/src/test/java/net/ladenthin/srcmorph/engine/GenerateResultTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/engine/GenerateResultTest.java
new file mode 100644
index 0000000..3ecdb08
--- /dev/null
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/engine/GenerateResultTest.java
@@ -0,0 +1,54 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.engine;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+public class GenerateResultTest {
+
+ @Test
+ public void constructor_threadsEveryFieldThrough() {
+ final GenerateResult result = new GenerateResult(false, 3, 5, 7);
+
+ assertThat(result.planOnly(), is(false));
+ assertThat(result.written(), is(3));
+ assertThat(result.unchanged(), is(5));
+ assertThat(result.skipped(), is(7));
+ }
+
+ @Test
+ public void planned_isPlanOnlyWithEveryCountAtZero() {
+ final GenerateResult result = GenerateResult.planned();
+
+ assertThat(result.planOnly(), is(true));
+ assertThat(result.written(), is(0));
+ assertThat(result.unchanged(), is(0));
+ assertThat(result.skipped(), is(0));
+ }
+
+ @Test
+ public void equalsAndHashCode_reflectAllFields() {
+ final GenerateResult a = new GenerateResult(false, 1, 2, 3);
+ final GenerateResult b = new GenerateResult(false, 1, 2, 3);
+ final GenerateResult differentWritten = new GenerateResult(false, 9, 2, 3);
+ final GenerateResult differentPlanOnly = new GenerateResult(true, 1, 2, 3);
+
+ assertThat(a, is(equalTo(b)));
+ assertThat(a.hashCode(), is(equalTo(b.hashCode())));
+ assertThat(a, org.hamcrest.CoreMatchers.not(equalTo(differentWritten)));
+ assertThat(a, org.hamcrest.CoreMatchers.not(equalTo(differentPlanOnly)));
+ }
+
+ @Test
+ public void toString_containsDistinguishingCounts() {
+ final GenerateResult result = new GenerateResult(false, 11, 22, 33);
+ assertThat(result.toString(), org.hamcrest.CoreMatchers.containsString("11"));
+ assertThat(result.toString(), org.hamcrest.CoreMatchers.containsString("22"));
+ assertThat(result.toString(), org.hamcrest.CoreMatchers.containsString("33"));
+ }
+}
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiCalibrationMeasurementTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/AiCalibrationMeasurementTest.java
similarity index 93%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiCalibrationMeasurementTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/AiCalibrationMeasurementTest.java
index b3c5a00..9f273b5 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiCalibrationMeasurementTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/AiCalibrationMeasurementTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.indexer;
+package net.ladenthin.srcmorph.indexer;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiCalibrationRunnerTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/AiCalibrationRunnerTest.java
similarity index 81%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiCalibrationRunnerTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/AiCalibrationRunnerTest.java
index 01c0dab..c932cdf 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiCalibrationRunnerTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/AiCalibrationRunnerTest.java
@@ -1,19 +1,19 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.indexer;
+package net.ladenthin.srcmorph.indexer;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
-import net.ladenthin.maven.llamacpp.aiindex.CommonTestFixtures;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationRequest;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptPreparationSupport;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptSupport;
-import net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationProvider;
-import net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationTimings;
-import net.ladenthin.maven.llamacpp.aiindex.provider.MockAiGenerationProvider;
+import net.ladenthin.srcmorph.CommonTestFixtures;
+import net.ladenthin.srcmorph.config.AiGenerationConfig;
+import net.ladenthin.srcmorph.document.AiGenerationRequest;
+import net.ladenthin.srcmorph.prompt.AiPromptPreparationSupport;
+import net.ladenthin.srcmorph.prompt.AiPromptSupport;
+import net.ladenthin.srcmorph.provider.AiGenerationProvider;
+import net.ladenthin.srcmorph.provider.AiGenerationTimings;
+import net.ladenthin.srcmorph.provider.MockAiGenerationProvider;
import org.junit.jupiter.api.Test;
public class AiCalibrationRunnerTest {
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiFieldGenerationSupportRealModelTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/AiFieldGenerationSupportRealModelTest.java
similarity index 85%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiFieldGenerationSupportRealModelTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/AiFieldGenerationSupportRealModelTest.java
index 77e31a5..c8db908 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiFieldGenerationSupportRealModelTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/AiFieldGenerationSupportRealModelTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.indexer;
+package net.ladenthin.srcmorph.indexer;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
@@ -12,19 +12,19 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
-import net.ladenthin.maven.llamacpp.aiindex.CommonTestFixtures;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFactCounter;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFactExtractor;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFieldGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinition;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinitionSupport;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationResult;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeader;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptPreparationSupport;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptSupport;
-import net.ladenthin.maven.llamacpp.aiindex.provider.LlamaCppJniAiGenerationProvider;
-import net.ladenthin.maven.llamacpp.aiindex.provider.LlamaCppJniConfig;
+import net.ladenthin.srcmorph.CommonTestFixtures;
+import net.ladenthin.srcmorph.config.AiFactCounter;
+import net.ladenthin.srcmorph.config.AiFactExtractor;
+import net.ladenthin.srcmorph.config.AiFieldGenerationConfig;
+import net.ladenthin.srcmorph.config.AiGenerationConfig;
+import net.ladenthin.srcmorph.config.AiModelDefinition;
+import net.ladenthin.srcmorph.config.AiModelDefinitionSupport;
+import net.ladenthin.srcmorph.document.AiGenerationResult;
+import net.ladenthin.srcmorph.document.AiMdHeader;
+import net.ladenthin.srcmorph.prompt.AiPromptPreparationSupport;
+import net.ladenthin.srcmorph.prompt.AiPromptSupport;
+import net.ladenthin.srcmorph.provider.LlamaCppJniAiGenerationProvider;
+import net.ladenthin.srcmorph.provider.LlamaCppJniConfig;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiFieldGenerationSupportTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/AiFieldGenerationSupportTest.java
similarity index 96%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiFieldGenerationSupportTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/AiFieldGenerationSupportTest.java
index b0e62d2..ebbc865 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiFieldGenerationSupportTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/AiFieldGenerationSupportTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.indexer;
+package net.ladenthin.srcmorph.indexer;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
@@ -21,19 +21,19 @@
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
-import net.ladenthin.maven.llamacpp.aiindex.CommonTestFixtures;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFactCounter;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFactExtractor;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFieldGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinition;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiModelDefinitionSupport;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationRequest;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationResult;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeader;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeaderCodec;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptPreparationSupport;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptSupport;
-import net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationProvider;
+import net.ladenthin.srcmorph.CommonTestFixtures;
+import net.ladenthin.srcmorph.config.AiFactCounter;
+import net.ladenthin.srcmorph.config.AiFactExtractor;
+import net.ladenthin.srcmorph.config.AiFieldGenerationConfig;
+import net.ladenthin.srcmorph.config.AiModelDefinition;
+import net.ladenthin.srcmorph.config.AiModelDefinitionSupport;
+import net.ladenthin.srcmorph.document.AiGenerationRequest;
+import net.ladenthin.srcmorph.document.AiGenerationResult;
+import net.ladenthin.srcmorph.document.AiMdHeader;
+import net.ladenthin.srcmorph.document.AiMdHeaderCodec;
+import net.ladenthin.srcmorph.prompt.AiPromptPreparationSupport;
+import net.ladenthin.srcmorph.prompt.AiPromptSupport;
+import net.ladenthin.srcmorph.provider.AiGenerationProvider;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiIndexPlanTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/AiIndexPlanTest.java
similarity index 97%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiIndexPlanTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/AiIndexPlanTest.java
index 539484d..ad40277 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiIndexPlanTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/AiIndexPlanTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.indexer;
+package net.ladenthin.srcmorph.indexer;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
@@ -10,7 +10,7 @@
import java.nio.file.Path;
import java.nio.file.Paths;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFieldGenerationConfig;
+import net.ladenthin.srcmorph.config.AiFieldGenerationConfig;
import org.junit.jupiter.api.Test;
public class AiIndexPlanTest {
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiInputWindowCalculatorTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/AiInputWindowCalculatorTest.java
similarity index 94%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiInputWindowCalculatorTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/AiInputWindowCalculatorTest.java
index d7657b4..c93a35d 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/AiInputWindowCalculatorTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/AiInputWindowCalculatorTest.java
@@ -1,13 +1,13 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.indexer;
+package net.ladenthin.srcmorph.indexer;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptPreparationSupport;
+import net.ladenthin.srcmorph.config.AiGenerationConfig;
+import net.ladenthin.srcmorph.prompt.AiPromptPreparationSupport;
import org.junit.jupiter.api.Test;
public class AiInputWindowCalculatorTest {
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/PackageIndexerTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/PackageIndexerTest.java
similarity index 94%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/PackageIndexerTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/PackageIndexerTest.java
index a23c133..a8beea8 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/PackageIndexerTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/PackageIndexerTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.indexer;
+package net.ladenthin.srcmorph.indexer;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
@@ -17,15 +17,15 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
-import net.ladenthin.maven.llamacpp.aiindex.CommonTestFixtures;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationRequest;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdDocument;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdDocumentCodec;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeader;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeaderCodec;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptSupport;
-import net.ladenthin.maven.llamacpp.aiindex.provider.AiGenerationProvider;
-import net.ladenthin.maven.llamacpp.aiindex.provider.MockAiGenerationProvider;
+import net.ladenthin.srcmorph.CommonTestFixtures;
+import net.ladenthin.srcmorph.document.AiGenerationRequest;
+import net.ladenthin.srcmorph.document.AiMdDocument;
+import net.ladenthin.srcmorph.document.AiMdDocumentCodec;
+import net.ladenthin.srcmorph.document.AiMdHeader;
+import net.ladenthin.srcmorph.document.AiMdHeaderCodec;
+import net.ladenthin.srcmorph.prompt.AiPromptSupport;
+import net.ladenthin.srcmorph.provider.AiGenerationProvider;
+import net.ladenthin.srcmorph.provider.MockAiGenerationProvider;
import org.junit.jupiter.api.Test;
public class PackageIndexerTest {
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/ProjectIndexerTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/ProjectIndexerTest.java
similarity index 95%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/ProjectIndexerTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/ProjectIndexerTest.java
index 770d0dd..933e71a 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/ProjectIndexerTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/ProjectIndexerTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.indexer;
+package net.ladenthin.srcmorph.indexer;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
@@ -15,15 +15,15 @@
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
-import net.ladenthin.maven.llamacpp.aiindex.CommonTestFixtures;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFieldGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdDocument;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdDocumentCodec;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeader;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeaderCodec;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptDefinition;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptSupport;
-import net.ladenthin.maven.llamacpp.aiindex.provider.MockAiGenerationProvider;
+import net.ladenthin.srcmorph.CommonTestFixtures;
+import net.ladenthin.srcmorph.config.AiFieldGenerationConfig;
+import net.ladenthin.srcmorph.document.AiMdDocument;
+import net.ladenthin.srcmorph.document.AiMdDocumentCodec;
+import net.ladenthin.srcmorph.document.AiMdHeader;
+import net.ladenthin.srcmorph.document.AiMdHeaderCodec;
+import net.ladenthin.srcmorph.prompt.AiPromptDefinition;
+import net.ladenthin.srcmorph.prompt.AiPromptSupport;
+import net.ladenthin.srcmorph.provider.MockAiGenerationProvider;
import org.junit.jupiter.api.Test;
public class ProjectIndexerTest {
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/SourceFileIndexerTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/SourceFileIndexerTest.java
similarity index 93%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/SourceFileIndexerTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/SourceFileIndexerTest.java
index 709a98f..d3f48b7 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/indexer/SourceFileIndexerTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/indexer/SourceFileIndexerTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.indexer;
+package net.ladenthin.srcmorph.indexer;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.hasItem;
@@ -15,14 +15,14 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import net.ladenthin.maven.llamacpp.aiindex.CommonTestFixtures;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiCondition;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiFieldGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdDocument;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdDocumentCodec;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptPreparationSupport;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptSupport;
-import net.ladenthin.maven.llamacpp.aiindex.provider.MockAiGenerationProvider;
+import net.ladenthin.srcmorph.CommonTestFixtures;
+import net.ladenthin.srcmorph.config.AiCondition;
+import net.ladenthin.srcmorph.config.AiFieldGenerationConfig;
+import net.ladenthin.srcmorph.document.AiMdDocument;
+import net.ladenthin.srcmorph.document.AiMdDocumentCodec;
+import net.ladenthin.srcmorph.prompt.AiPromptPreparationSupport;
+import net.ladenthin.srcmorph.prompt.AiPromptSupport;
+import net.ladenthin.srcmorph.provider.MockAiGenerationProvider;
import org.junit.jupiter.api.Test;
public class SourceFileIndexerTest {
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/prompt/AiPreparedPromptTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/prompt/AiPreparedPromptTest.java
similarity index 96%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/prompt/AiPreparedPromptTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/prompt/AiPreparedPromptTest.java
index 0938d0f..7ac60b6 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/prompt/AiPreparedPromptTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/prompt/AiPreparedPromptTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.prompt;
+package net.ladenthin.srcmorph.prompt;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/prompt/AiPromptPreparationSupportTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/prompt/AiPromptPreparationSupportTest.java
similarity index 96%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/prompt/AiPromptPreparationSupportTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/prompt/AiPromptPreparationSupportTest.java
index f984c31..89ddddf 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/prompt/AiPromptPreparationSupportTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/prompt/AiPromptPreparationSupportTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.prompt;
+package net.ladenthin.srcmorph.prompt;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
@@ -10,9 +10,9 @@
import java.nio.file.Paths;
import java.util.Arrays;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationRequest;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeader;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeaderCodec;
+import net.ladenthin.srcmorph.document.AiGenerationRequest;
+import net.ladenthin.srcmorph.document.AiMdHeader;
+import net.ladenthin.srcmorph.document.AiMdHeaderCodec;
import org.junit.jupiter.api.Test;
public class AiPromptPreparationSupportTest {
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/prompt/AiPromptSupportTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/prompt/AiPromptSupportTest.java
similarity index 99%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/prompt/AiPromptSupportTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/prompt/AiPromptSupportTest.java
index 37c988b..e65e531 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/prompt/AiPromptSupportTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/prompt/AiPromptSupportTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.prompt;
+package net.ladenthin.srcmorph.prompt;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiCompletionParserTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/provider/AiCompletionParserTest.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiCompletionParserTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/provider/AiCompletionParserTest.java
index 09ce520..e633fc7 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiCompletionParserTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/provider/AiCompletionParserTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.provider;
+package net.ladenthin.srcmorph.provider;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiGenerationProviderDefaultTimingsTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/provider/AiGenerationProviderDefaultTimingsTest.java
similarity index 87%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiGenerationProviderDefaultTimingsTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/provider/AiGenerationProviderDefaultTimingsTest.java
index b63cf2d..6a3d479 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiGenerationProviderDefaultTimingsTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/provider/AiGenerationProviderDefaultTimingsTest.java
@@ -1,14 +1,14 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.provider;
+package net.ladenthin.srcmorph.provider;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import java.nio.file.Paths;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationRequest;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeader;
+import net.ladenthin.srcmorph.document.AiGenerationRequest;
+import net.ladenthin.srcmorph.document.AiMdHeader;
import org.junit.jupiter.api.Test;
public class AiGenerationProviderDefaultTimingsTest {
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiGenerationProviderFactoryTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/provider/AiGenerationProviderFactoryTest.java
similarity index 95%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiGenerationProviderFactoryTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/provider/AiGenerationProviderFactoryTest.java
index e55fec7..b8f6780 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiGenerationProviderFactoryTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/provider/AiGenerationProviderFactoryTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.provider;
+package net.ladenthin.srcmorph.provider;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
@@ -9,7 +9,7 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.Collections;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptSupport;
+import net.ladenthin.srcmorph.prompt.AiPromptSupport;
import org.junit.jupiter.api.Test;
public class AiGenerationProviderFactoryTest {
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiGenerationTimingsTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/provider/AiGenerationTimingsTest.java
similarity index 92%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiGenerationTimingsTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/provider/AiGenerationTimingsTest.java
index 9df4ec4..8a9db1b 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/provider/AiGenerationTimingsTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/provider/AiGenerationTimingsTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.provider;
+package net.ladenthin.srcmorph.provider;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/provider/LlamaCppJniAiGenerationProviderTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/provider/LlamaCppJniAiGenerationProviderTest.java
similarity index 92%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/provider/LlamaCppJniAiGenerationProviderTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/provider/LlamaCppJniAiGenerationProviderTest.java
index 9bb0b80..d06fb3a 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/provider/LlamaCppJniAiGenerationProviderTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/provider/LlamaCppJniAiGenerationProviderTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.provider;
+package net.ladenthin.srcmorph.provider;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
@@ -10,12 +10,12 @@
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collections;
-import net.ladenthin.maven.llamacpp.aiindex.CommonTestFixtures;
-import net.ladenthin.maven.llamacpp.aiindex.config.AiGenerationConfig;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationRequest;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeader;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeaderCodec;
-import net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptSupport;
+import net.ladenthin.srcmorph.CommonTestFixtures;
+import net.ladenthin.srcmorph.config.AiGenerationConfig;
+import net.ladenthin.srcmorph.document.AiGenerationRequest;
+import net.ladenthin.srcmorph.document.AiMdHeader;
+import net.ladenthin.srcmorph.document.AiMdHeaderCodec;
+import net.ladenthin.srcmorph.prompt.AiPromptSupport;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
diff --git a/srcmorph/src/test/java/net/ladenthin/srcmorph/provider/LlamaCppJniConfigFactoryTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/provider/LlamaCppJniConfigFactoryTest.java
new file mode 100644
index 0000000..9ac4bf4
--- /dev/null
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/provider/LlamaCppJniConfigFactoryTest.java
@@ -0,0 +1,143 @@
+// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+//
+// SPDX-License-Identifier: Apache-2.0
+package net.ladenthin.srcmorph.provider;
+
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.Arrays;
+import java.util.Collections;
+import net.ladenthin.srcmorph.config.AiGenerationConfig;
+import org.junit.jupiter.api.Test;
+
+public class LlamaCppJniConfigFactoryTest {
+
+ /**
+ * Every field set to a distinct, distinguishable value so a mutant that swaps two getters (or drops
+ * an argument) in {@link LlamaCppJniConfigFactory#fromGenerationConfig} is killed.
+ */
+ private static AiGenerationConfig fullConfig() {
+ final AiGenerationConfig config = new AiGenerationConfig();
+ config.setModelPath("model.gguf");
+ config.setContextSize(1111);
+ config.setMaxOutputTokens(222);
+ config.setTemperature(0.33f);
+ config.setThreads(4);
+ config.setTopP(0.55f);
+ config.setTopK(66);
+ config.setMinP(0.07f);
+ config.setTopNSigma(0.88f);
+ config.setRepeatPenalty(1.09f);
+ config.setChatTemplateEnableThinking(false);
+ config.setCachePrompt(false);
+ config.setSwaFull(false);
+ config.setCacheReuse(101);
+ config.setGpuLayers(12);
+ config.setMainGpu(3);
+ config.setDevices("Vulkan1");
+ config.setReasoningEffort("high");
+ config.setReasoningBudgetTokens(512);
+ config.setDryMultiplier(0.5f);
+ config.setDryBase(1.3f);
+ config.setDryAllowedLength(7);
+ config.setDryPenaltyLastN(999);
+ config.setDrySequenceBreakers(Arrays.asList("\n", "."));
+ config.setStopStrings(Arrays.asList(""));
+ return config;
+ }
+
+ @Test
+ public void fromGenerationConfig_threadsEveryFieldThrough() {
+ final LlamaCppJniConfig result = LlamaCppJniConfigFactory.fromGenerationConfig("libpath", fullConfig());
+
+ assertThat(result.libraryPath(), is("libpath"));
+ assertThat(result.modelPath(), is("model.gguf"));
+ assertThat(result.contextSize(), is(1111));
+ assertThat(result.maxOutputTokens(), is(222));
+ assertThat(result.temperature(), is(0.33f));
+ assertThat(result.threads(), is(4));
+ assertThat(result.topP(), is(0.55f));
+ assertThat(result.topK(), is(66));
+ assertThat(result.minP(), is(0.07f));
+ assertThat(result.topNSigma(), is(0.88f));
+ assertThat(result.repeatPenalty(), is(1.09f));
+ assertThat(result.chatTemplateEnableThinking(), is(false));
+ assertThat(result.cachePrompt(), is(false));
+ assertThat(result.swaFull(), is(false));
+ assertThat(result.cacheReuse(), is(101));
+ assertThat(result.gpuLayers(), is(12));
+ assertThat(result.mainGpu(), is(3));
+ assertThat(result.devices(), is("Vulkan1"));
+ assertThat(result.reasoningEffort(), is("high"));
+ assertThat(result.reasoningBudgetTokens(), is(512));
+ assertThat(result.dryMultiplier(), is(0.5f));
+ assertThat(result.dryBase(), is(1.3f));
+ assertThat(result.dryAllowedLength(), is(7));
+ assertThat(result.dryPenaltyLastN(), is(999));
+ assertThat(result.drySequenceBreakers(), hasItem("\n"));
+ assertThat(result.drySequenceBreakers(), hasItem("."));
+ assertThat(result.stopStrings(), hasItem(""));
+ }
+
+ @Test
+ public void fromGenerationConfig_libraryPathNullIsPreserved() {
+ final AiGenerationConfig config = fullConfig();
+ final LlamaCppJniConfig result = LlamaCppJniConfigFactory.fromGenerationConfig(null, config);
+ assertThat(result.libraryPath(), is(nullValue()));
+ }
+
+ @Test
+ public void fromGenerationConfig_nullStopStringsAndDrySequenceBreakersBecomeEmpty() {
+ final AiGenerationConfig config = fullConfig();
+ config.setStopStrings(null);
+ config.setDrySequenceBreakers(null);
+
+ final LlamaCppJniConfig result = LlamaCppJniConfigFactory.fromGenerationConfig("lib", config);
+
+ assertThat(result.stopStrings(), is(Collections.emptyList()));
+ assertThat(result.drySequenceBreakers(), is(Collections.emptyList()));
+ }
+
+ @Test
+ public void fromFallbackParameters_threadsFallbackArgumentsThrough() {
+ final LlamaCppJniConfig result =
+ LlamaCppJniConfigFactory.fromFallbackParameters("lib", "fallback.gguf", 4096, 256, 0.42f, 6);
+
+ assertThat(result.libraryPath(), is("lib"));
+ assertThat(result.modelPath(), is("fallback.gguf"));
+ assertThat(result.contextSize(), is(4096));
+ assertThat(result.maxOutputTokens(), is(256));
+ assertThat(result.temperature(), is(0.42f));
+ assertThat(result.threads(), is(6));
+ }
+
+ @Test
+ public void fromFallbackParameters_appliesEveryAiGenerationConfigDefault() {
+ final LlamaCppJniConfig result =
+ LlamaCppJniConfigFactory.fromFallbackParameters(null, "fallback.gguf", 4096, 256, 0.42f, 6);
+
+ assertThat(result.topP(), is(AiGenerationConfig.DEFAULT_TOP_P));
+ assertThat(result.topK(), is(AiGenerationConfig.DEFAULT_TOP_K));
+ assertThat(result.minP(), is(AiGenerationConfig.DEFAULT_MIN_P));
+ assertThat(result.topNSigma(), is(AiGenerationConfig.DEFAULT_TOP_N_SIGMA));
+ assertThat(result.repeatPenalty(), is(AiGenerationConfig.DEFAULT_REPEAT_PENALTY));
+ assertThat(result.chatTemplateEnableThinking(), is(AiGenerationConfig.DEFAULT_CHAT_TEMPLATE_ENABLE_THINKING));
+ assertThat(result.cachePrompt(), is(AiGenerationConfig.DEFAULT_CACHE_PROMPT));
+ assertThat(result.swaFull(), is(AiGenerationConfig.DEFAULT_SWA_FULL));
+ assertThat(result.cacheReuse(), is(AiGenerationConfig.DEFAULT_CACHE_REUSE));
+ assertThat(result.gpuLayers(), is(AiGenerationConfig.DEFAULT_GPU_LAYERS));
+ assertThat(result.mainGpu(), is(AiGenerationConfig.DEFAULT_MAIN_GPU));
+ assertThat(result.devices(), is(AiGenerationConfig.DEFAULT_DEVICES));
+ assertThat(result.reasoningEffort(), is(AiGenerationConfig.DEFAULT_REASONING_EFFORT));
+ assertThat(result.reasoningBudgetTokens(), is(AiGenerationConfig.DEFAULT_REASONING_BUDGET_TOKENS));
+ assertThat(result.dryMultiplier(), is(AiGenerationConfig.DEFAULT_DRY_MULTIPLIER));
+ assertThat(result.dryBase(), is(AiGenerationConfig.DEFAULT_DRY_BASE));
+ assertThat(result.dryAllowedLength(), is(AiGenerationConfig.DEFAULT_DRY_ALLOWED_LENGTH));
+ assertThat(result.dryPenaltyLastN(), is(AiGenerationConfig.DEFAULT_DRY_PENALTY_LAST_N));
+ assertThat(result.drySequenceBreakers(), is(Collections.emptyList()));
+ assertThat(result.stopStrings(), is(Collections.emptyList()));
+ }
+}
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/provider/MockAiGenerationProviderTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/provider/MockAiGenerationProviderTest.java
similarity index 92%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/provider/MockAiGenerationProviderTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/provider/MockAiGenerationProviderTest.java
index 602f851..b91c992 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/provider/MockAiGenerationProviderTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/provider/MockAiGenerationProviderTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.provider;
+package net.ladenthin.srcmorph.provider;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -9,8 +9,8 @@
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiGenerationRequest;
-import net.ladenthin.maven.llamacpp.aiindex.document.AiMdHeader;
+import net.ladenthin.srcmorph.document.AiGenerationRequest;
+import net.ladenthin.srcmorph.document.AiMdHeader;
import org.junit.jupiter.api.Test;
public class MockAiGenerationProviderTest {
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiChecksumSupportTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiChecksumSupportTest.java
similarity index 96%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiChecksumSupportTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiChecksumSupportTest.java
index f61b4e4..078c9d0 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiChecksumSupportTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiChecksumSupportTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.support;
+package net.ladenthin.srcmorph.support;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiDeterministicSummaryTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiDeterministicSummaryTest.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiDeterministicSummaryTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiDeterministicSummaryTest.java
index 34ecbfa..09753aa 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiDeterministicSummaryTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiDeterministicSummaryTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.support;
+package net.ladenthin.srcmorph.support;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.not;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiGenerationTimeEstimatorTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiGenerationTimeEstimatorTest.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiGenerationTimeEstimatorTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiGenerationTimeEstimatorTest.java
index e05a926..5914c5f 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiGenerationTimeEstimatorTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiGenerationTimeEstimatorTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.support;
+package net.ladenthin.srcmorph.support;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiPathSupportTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiPathSupportTest.java
similarity index 96%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiPathSupportTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiPathSupportTest.java
index 4c5242a..99a3cc8 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiPathSupportTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiPathSupportTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.support;
+package net.ladenthin.srcmorph.support;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiProgressBarTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiProgressBarTest.java
similarity index 97%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiProgressBarTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiProgressBarTest.java
index d0d05c6..7c448cc 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiProgressBarTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiProgressBarTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.support;
+package net.ladenthin.srcmorph.support;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiSourceChunkerTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiSourceChunkerTest.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiSourceChunkerTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiSourceChunkerTest.java
index 21d5895..a92d7bf 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiSourceChunkerTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiSourceChunkerTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.support;
+package net.ladenthin.srcmorph.support;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiSourceExcludeFilterTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiSourceExcludeFilterTest.java
similarity index 99%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiSourceExcludeFilterTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiSourceExcludeFilterTest.java
index 6ac5ce2..1c38649 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiSourceExcludeFilterTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiSourceExcludeFilterTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.support;
+package net.ladenthin.srcmorph.support;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiTimeSupportTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiTimeSupportTest.java
similarity index 96%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiTimeSupportTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiTimeSupportTest.java
index 0d5220e..093d173 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/AiTimeSupportTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/support/AiTimeSupportTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.support;
+package net.ladenthin.srcmorph.support;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/Java8CompatibilityHelperTest.java b/srcmorph/src/test/java/net/ladenthin/srcmorph/support/Java8CompatibilityHelperTest.java
similarity index 98%
rename from llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/Java8CompatibilityHelperTest.java
rename to srcmorph/src/test/java/net/ladenthin/srcmorph/support/Java8CompatibilityHelperTest.java
index b18f6bb..693d33a 100644
--- a/llamacpp-ai-index-maven-plugin/src/test/java/net/ladenthin/maven/llamacpp/aiindex/support/Java8CompatibilityHelperTest.java
+++ b/srcmorph/src/test/java/net/ladenthin/srcmorph/support/Java8CompatibilityHelperTest.java
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin
//
// SPDX-License-Identifier: Apache-2.0
-package net.ladenthin.maven.llamacpp.aiindex.support;
+package net.ladenthin.srcmorph.support;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
diff --git a/llamacpp-ai-index-maven-plugin/src/test/resources/SmolLM2-135M-Instruct-Q3_K_M.gguf b/srcmorph/src/test/resources/SmolLM2-135M-Instruct-Q3_K_M.gguf
similarity index 100%
rename from llamacpp-ai-index-maven-plugin/src/test/resources/SmolLM2-135M-Instruct-Q3_K_M.gguf
rename to srcmorph/src/test/resources/SmolLM2-135M-Instruct-Q3_K_M.gguf