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 @@ [![Coverage Status](https://coveralls.io/repos/github/bernardladenthin/llamacpp-ai-index-maven-plugin/badge.svg?branch=main)](https://coveralls.io/github/bernardladenthin/llamacpp-ai-index-maven-plugin?branch=main) [![codecov](https://codecov.io/gh/bernardladenthin/llamacpp-ai-index-maven-plugin/graph/badge.svg)](https://codecov.io/gh/bernardladenthin/llamacpp-ai-index-maven-plugin) [![JaCoCo](https://img.shields.io/codecov/c/github/bernardladenthin/llamacpp-ai-index-maven-plugin?label=JaCoCo&logo=java)](https://codecov.io/gh/bernardladenthin/llamacpp-ai-index-maven-plugin) -[![PIT Mutation](https://img.shields.io/badge/PIT%20mutation-100%25%20(24%20classes)-brightgreen)](https://github.com/bernardladenthin/llamacpp-ai-index-maven-plugin/actions/workflows/publish.yml) +[![PIT Mutation](https://img.shields.io/badge/PIT%20mutation-100%25%20(47%20classes)-brightgreen)](https://github.com/bernardladenthin/llamacpp-ai-index-maven-plugin/actions/workflows/publish.yml) **Quality:** [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=bernardladenthin_llamacpp-ai-index-maven-plugin&metric=alert_status)](https://sonarcloud.io/dashboard?id=bernardladenthin_llamacpp-ai-index-maven-plugin) @@ -36,12 +36,6 @@ [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=bernardladenthin_llamacpp-ai-index-maven-plugin&metric=security_rating)](https://sonarcloud.io/dashboard?id=bernardladenthin_llamacpp-ai-index-maven-plugin) **Security:** - [![Known Vulnerabilities](https://snyk.io/test/github/bernardladenthin/llamacpp-ai-index-maven-plugin/badge.svg?targetFile=pom.xml)](https://snyk.io/test/github/bernardladenthin/llamacpp-ai-index-maven-plugin?targetFile=pom.xml) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fbernardladenthin%2Fllamacpp-ai-index-maven-plugin.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fbernardladenthin%2Fllamacpp-ai-index-maven-plugin?ref=badge_shield) [![Dependencies](https://img.shields.io/librariesio/github/bernardladenthin/llamacpp-ai-index-maven-plugin)](https://libraries.io/github/bernardladenthin/llamacpp-ai-index-maven-plugin) @@ -72,594 +66,151 @@ using the assigned ID: [![Treeware](https://img.shields.io/badge/dynamic/json?color=brightgreen&label=Treeware&query=%24.total&url=https%3A%2F%2Fpublic.offset.earth%2Fusers%2Ftreeware%2Ftrees)](https://treeware.earth) [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](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 - - - - package-body - - - - - project-body - - - - +### 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.ladenthin llamacpp-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 - - - - file-body-sql - - - - file-body-fallback - - - - package-body - - - - project-body - - - - - file-body-java-v2 - - - - package-body-v2 - - - - - - - - - - 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.ladenthin srcmorph-parent - 1.1.0-SNAPSHOT + 1.1.0 pom ${project.groupId}:${project.artifactId} @@ -52,13 +52,38 @@ SPDX-License-Identifier: Apache-2.0
+ + + 2.22.0 + + + srcmorph + srcmorph-cli + srcmorph-maven-plugin llamacpp-ai-index-maven-plugin @@ -84,6 +109,25 @@ SPDX-License-Identifier: Apache-2.0 logback-classic 1.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 + + + + file-body-sql + + + + file-body-fallback + + + + package-body + + + + project-body + + + + + file-body-java-v2 + + + + package-body-v2 + + + + + + + + + + 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}:

    *
      - *
    1. The prompt is prepared via {@link net.ladenthin.maven.llamacpp.aiindex.prompt.AiPromptPreparationSupport#preparePrompt}.
    2. + *
    3. The prompt is prepared via {@link net.ladenthin.srcmorph.prompt.AiPromptPreparationSupport#preparePrompt}.
    4. *
    5. A trim warning is logged if the source was truncated and - * {@link net.ladenthin.maven.llamacpp.aiindex.config.AiGenerationConfig#isWarnOnTrim()} is {@code true}.
    6. + * {@link net.ladenthin.srcmorph.config.AiGenerationConfig#isWarnOnTrim()} is {@code true}. *
    7. The AI provider generates a value for the trimmed source.
    8. *
    9. 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