From 7569b11fe41caafe3da9ccecdf26a80a063e1715 Mon Sep 17 00:00:00 2001 From: Romain Manni-Bucau Date: Thu, 16 Jul 2026 19:51:44 +0200 Subject: [PATCH 1/3] [agents] add agents.md everywhere --- AGENTS.md | 94 +++++ CLAUDE.md | 11 + fusion-api/AGENTS.md | 58 +++ fusion-api/pom.xml | 1 + fusion-build-api/AGENTS.md | 44 +++ fusion-build-internal/AGENTS.md | 37 ++ fusion-build-internal/pom.xml | 161 ++++++++ .../agent/AgentsFileSynchronizer.java | 344 ++++++++++++++++++ .../src/main/resources/agents/claude.md | 6 + .../main/resources/agents/module/_default.md | 8 + .../main/resources/agents/module/_footer.md | 12 + .../resources/agents/module/fusion-api.md | 47 +++ .../agents/module/fusion-build-api.md | 20 + .../agents/module/fusion-build-internal.md | 26 ++ .../resources/agents/module/fusion-cli.md | 49 +++ .../agents/module/fusion-documentation.md | 25 ++ .../agents/module/fusion-handlebars.md | 20 + .../agents/module/fusion-http-server.md | 48 +++ .../agents/module/fusion-httpclient-parent.md | 10 + .../agents/module/fusion-httpclient.md | 18 + .../resources/agents/module/fusion-json.md | 38 ++ .../resources/agents/module/fusion-jsonrpc.md | 36 ++ .../resources/agents/module/fusion-jwt.md | 20 + .../agents/module/fusion-kubernetes-client.md | 19 + .../module/fusion-kubernetes-operator-base.md | 20 + .../agents/module/fusion-observability.md | 19 + .../agents/module/fusion-persistence.md | 44 +++ .../agents/module/fusion-processor.md | 28 ++ .../resources/agents/module/fusion-testing.md | 30 ++ .../resources/agents/module/fusion-tracing.md | 20 + .../src/main/resources/agents/root.md | 72 ++++ .../agent/AgentsFileSynchronizerTest.java | 80 ++++ fusion-cli/AGENTS.md | 60 +++ fusion-cli/pom.xml | 1 + fusion-documentation/AGENTS.md | 36 ++ fusion-documentation/pom.xml | 11 +- fusion-handlebars/AGENTS.md | 31 ++ fusion-handlebars/pom.xml | 1 + fusion-http-server/AGENTS.md | 59 +++ fusion-http-server/pom.xml | 1 + fusion-httpclient-parent/AGENTS.md | 21 ++ .../fusion-httpclient/AGENTS.md | 29 ++ .../fusion-httpclient/pom.xml | 1 + .../fusion-kubernetes-client/AGENTS.md | 30 ++ .../fusion-kubernetes-client/pom.xml | 1 + fusion-httpclient-parent/pom.xml | 1 + fusion-json/AGENTS.md | 49 +++ fusion-json/pom.xml | 1 + fusion-jsonrpc/AGENTS.md | 47 +++ fusion-jsonrpc/pom.xml | 1 + fusion-jwt/AGENTS.md | 62 ++++ fusion-jwt/pom.xml | 1 + fusion-kubernetes-operator-base/AGENTS.md | 45 +++ fusion-kubernetes-operator-base/pom.xml | 1 + fusion-observability/AGENTS.md | 30 ++ fusion-observability/pom.xml | 1 + fusion-persistence/AGENTS.md | 55 +++ fusion-persistence/pom.xml | 1 + fusion-processor/AGENTS.md | 39 ++ fusion-processor/pom.xml | 1 + fusion-testing/AGENTS.md | 41 +++ fusion-testing/pom.xml | 1 + fusion-tracing/AGENTS.md | 31 ++ fusion-tracing/pom.xml | 1 + pom.xml | 1 + 65 files changed, 2155 insertions(+), 2 deletions(-) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 fusion-api/AGENTS.md create mode 100644 fusion-build-api/AGENTS.md create mode 100644 fusion-build-internal/AGENTS.md create mode 100644 fusion-build-internal/pom.xml create mode 100644 fusion-build-internal/src/main/java/io/yupiik/fusion/build/internal/agent/AgentsFileSynchronizer.java create mode 100644 fusion-build-internal/src/main/resources/agents/claude.md create mode 100644 fusion-build-internal/src/main/resources/agents/module/_default.md create mode 100644 fusion-build-internal/src/main/resources/agents/module/_footer.md create mode 100644 fusion-build-internal/src/main/resources/agents/module/fusion-api.md create mode 100644 fusion-build-internal/src/main/resources/agents/module/fusion-build-api.md create mode 100644 fusion-build-internal/src/main/resources/agents/module/fusion-build-internal.md create mode 100644 fusion-build-internal/src/main/resources/agents/module/fusion-cli.md create mode 100644 fusion-build-internal/src/main/resources/agents/module/fusion-documentation.md create mode 100644 fusion-build-internal/src/main/resources/agents/module/fusion-handlebars.md create mode 100644 fusion-build-internal/src/main/resources/agents/module/fusion-http-server.md create mode 100644 fusion-build-internal/src/main/resources/agents/module/fusion-httpclient-parent.md create mode 100644 fusion-build-internal/src/main/resources/agents/module/fusion-httpclient.md create mode 100644 fusion-build-internal/src/main/resources/agents/module/fusion-json.md create mode 100644 fusion-build-internal/src/main/resources/agents/module/fusion-jsonrpc.md create mode 100644 fusion-build-internal/src/main/resources/agents/module/fusion-jwt.md create mode 100644 fusion-build-internal/src/main/resources/agents/module/fusion-kubernetes-client.md create mode 100644 fusion-build-internal/src/main/resources/agents/module/fusion-kubernetes-operator-base.md create mode 100644 fusion-build-internal/src/main/resources/agents/module/fusion-observability.md create mode 100644 fusion-build-internal/src/main/resources/agents/module/fusion-persistence.md create mode 100644 fusion-build-internal/src/main/resources/agents/module/fusion-processor.md create mode 100644 fusion-build-internal/src/main/resources/agents/module/fusion-testing.md create mode 100644 fusion-build-internal/src/main/resources/agents/module/fusion-tracing.md create mode 100644 fusion-build-internal/src/main/resources/agents/root.md create mode 100644 fusion-build-internal/src/test/java/io/yupiik/fusion/build/internal/agent/AgentsFileSynchronizerTest.java create mode 100644 fusion-cli/AGENTS.md create mode 100644 fusion-documentation/AGENTS.md create mode 100644 fusion-handlebars/AGENTS.md create mode 100644 fusion-http-server/AGENTS.md create mode 100644 fusion-httpclient-parent/AGENTS.md create mode 100644 fusion-httpclient-parent/fusion-httpclient/AGENTS.md create mode 100644 fusion-httpclient-parent/fusion-kubernetes-client/AGENTS.md create mode 100644 fusion-json/AGENTS.md create mode 100644 fusion-jsonrpc/AGENTS.md create mode 100644 fusion-jwt/AGENTS.md create mode 100644 fusion-kubernetes-operator-base/AGENTS.md create mode 100644 fusion-observability/AGENTS.md create mode 100644 fusion-persistence/AGENTS.md create mode 100644 fusion-processor/AGENTS.md create mode 100644 fusion-testing/AGENTS.md create mode 100644 fusion-tracing/AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..16a14d4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,94 @@ + +# Yupiik Fusion - agent guide + +Fusion is a reflectionless, self-contained Java framework designed to be GraalVM-native friendly. +Business code never depends on jakarta/spring/guice. Everything heavy (bean/model discovery, DI wiring, +JSON codecs, configuration binding, persistence models, proxies) is generated at COMPILE TIME by an +annotation processor; only bean resolution happens at runtime. + +- Documentation site: https://yupiik.github.io/fusion/ +- Example applications: https://github.com/yupiik/fusion-examples + +## Requirements and build commands + +Requires Java >= 17 and Maven >= 3.8 (no wrapper is committed, use your own Maven). + +| Command | Purpose | +|---|---| +| `mvn install` | Full build with tests (also regenerates all AGENTS.md/CLAUDE.md files). | +| `mvn install -pl -am` | Build one module and its dependencies. | +| `mvn test -pl -Dtest=MyTest` | Run a single test class. | +| `mvn license:format` | Add/fix the mandatory Apache-2.0 Yupiik license headers. | +| `mvn install -DskipTests ossindex:audit` | Dependency security audit. | +| `mvn compile yupiik-tools:serve-minisite -e` | Preview the documentation site (run in `fusion-documentation`). | + +## Module map + +Each module has its own `AGENTS.md` with entry points and module specific rules. + +| Module | Description | +|---|---| +| [fusion-api](fusion-api/AGENTS.md) | Fusion runtime API: IoC container, scopes, lifecycle, events and configuration. | +| [fusion-processor](fusion-processor/AGENTS.md) | Fusion annotation processor generating beans, JSON codecs, configuration binding and persistence models at build time. | +| [fusion-build-api](fusion-build-api/AGENTS.md) | API which is used by the annotation processor but not the runtime. | +| [fusion-json](fusion-json/AGENTS.md) | Reflectionless JSON mapping with compile time generated codecs, JSON-Patch/Pointer/diff utilities. | +| [fusion-http-server](fusion-http-server/AGENTS.md) | HTTP server based on Apache Tomcat with a light request/response API and endpoint matchers. | +| [fusion-testing](fusion-testing/AGENTS.md) | JUnit 5 integration to test Fusion applications (container lifecycle, injection, CLI launcher). | +| [fusion-jsonrpc](fusion-jsonrpc/AGENTS.md) | JSON-RPC 2.0 server on top of the Fusion HTTP server with OpenRPC support. | +| [fusion-cli](fusion-cli/AGENTS.md) | Command line application support with compile time generated commands. | +| [fusion-httpclient-parent](fusion-httpclient-parent/AGENTS.md) | Parent of the Fusion HTTP client extensions. | +| [fusion-httpclient](fusion-httpclient-parent/fusion-httpclient/AGENTS.md) | Extended java.net.http client: routing, throttling, retries, logging listeners. | +| [fusion-kubernetes-client](fusion-httpclient-parent/fusion-kubernetes-client/AGENTS.md) | Lightweight Kubernetes client based on the JDK HTTP client (in-cluster and kubeconfig support). | +| [fusion-tracing](fusion-tracing/AGENTS.md) | Zipkin oriented tracing for the Fusion HTTP server and clients. | +| [fusion-observability](fusion-observability/AGENTS.md) | Observability HTTP server exposing health checks and OpenMetrics metrics. | +| [fusion-persistence](fusion-persistence/AGENTS.md) | Lightweight reflectionless JDBC persistence with compile time generated entity models. | +| [fusion-documentation](fusion-documentation/AGENTS.md) | AsciiDoc minisite of the project and documentation generators (configuration, OpenRPC converters). | +| [fusion-handlebars](fusion-handlebars/AGENTS.md) | Handlebars templating engine implementation without any dependency. | +| [fusion-jwt](fusion-jwt/AGENTS.md) | JWT signing and validation based on JDK cryptography. | +| [fusion-kubernetes-operator-base](fusion-kubernetes-operator-base/AGENTS.md) | Base to build Kubernetes operators with Fusion (watch/reconcile runtime, CRD support). | +| [fusion-build-internal](fusion-build-internal/AGENTS.md) | Internal build tooling keeping repository agent files (AGENTS.md/CLAUDE.md) in sync. Never published. | + +How consumers use Fusion (the "triad"): applications depend on `fusion-build-api` + `fusion-processor` +in `provided` scope (compile time only) and `fusion-api` in `compile` scope (runtime). Opt-in modules +(json, http-server, jsonrpc, cli, persistence, ...) are added as needed. + +## Design rules (do not break these) + +- NO REFLECTION in runtime code paths: user code is bound through processor-generated classes, never + `java.lang.reflect` lookups on business classes. This is the core promise of the framework. +- New user-facing annotations go in `fusion-build-api` (package `io.yupiik.fusion.framework.build.api.*`) + and MUST be handled in `fusion-processor` (they are compile-time only, `provided` scope for consumers). +- Package conventions: public API in `...api`, extension points in `...spi`, implementation details in + `...internal` or `...impl` (never referenced from user documentation or other modules' public API). +- There is deliberately NO declarative interceptor support - this is a documented design choice, do not add one. +- Keep modules dependency-light: no new third party runtime dependency without strong justification + (Tomcat in `fusion-http-server` is the notable exception). + +## Coding conventions + +- Every `.java`, `.xml`, `.properties` and `.yaml` file starts with the Apache-2.0 Yupiik header + (checked at `validate` phase, fix with `mvn license:format`). +- Java 17 level: records, switch expressions, `var` are idiomatic here; parameters and locals are + typically `final`. +- ASCII-only content in sources and documentation. + +## Testing conventions + +- JUnit 5 (Jupiter), test classes are package-private, named `*Test`, mirroring the main package layout. +- Surefire runs tests IN PARALLEL (classes and methods, `junit.jupiter.execution.parallel.enabled=true`): + tests must not rely on shared mutable global state, fixed network ports or the working directory. +- Use `fusion-testing` helpers: `@FusionSupport`/`@MonoFusionSupport` (container lifecycle), `@Fusion` + (injection in tests), `@FusionCLITest` with `Stdout`/`Stderr` for CLI, `TestClient` for HTTP. + +## Documentation workflow + +- The site is AsciiDoc, sources in `fusion-documentation/src/main/minisite/content/fusion/*.adoc`. +- `fusion-documentation/src/main/minisite/content/_partials/generated/**` is GENERATED from the + annotation metadata - never edit by hand. +- User-facing feature changes need a matching `.adoc` update. +- All `AGENTS.md` files and `CLAUDE.md` are GENERATED by `fusion-build-internal`: edit the templates in + `fusion-build-internal/src/main/resources/agents/` then run `mvn install -pl fusion-build-internal`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..06c4b62 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,11 @@ + +@AGENTS.md + +Per-module guides live in each module directory (`/AGENTS.md`), read the one matching the module you work on. + +Note: this file and all `AGENTS.md` files are generated by `fusion-build-internal`. To change them, edit the +templates in `fusion-build-internal/src/main/resources/agents/` and run `mvn install -pl fusion-build-internal`. diff --git a/fusion-api/AGENTS.md b/fusion-api/AGENTS.md new file mode 100644 index 0000000..9cf8a8e --- /dev/null +++ b/fusion-api/AGENTS.md @@ -0,0 +1,58 @@ + +# Fusion :: API (`fusion-api`) + +Fusion runtime API: IoC container, scopes, lifecycle, events and configuration. + +Runtime side of the framework: the IoC container consumers embed in their application. + +## Entry points + +- `io.yupiik.fusion.framework.api.ConfiguringContainer`: bootstrap API (`ConfiguringContainer.of().start()`). +- `io.yupiik.fusion.framework.api.RuntimeContainer` / `Instance`: running container and bean instance handles. +- `io.yupiik.fusion.framework.api.scope`: `@ApplicationScoped` (lazy singleton) and `@DefaultScoped` (instance per lookup). +- `io.yupiik.fusion.framework.api.event`: event bus (`Emitter`), container lifecycle events `Start`/`Stop`. +- `io.yupiik.fusion.framework.api.configuration.Configuration`: runtime configuration lookup abstraction. +- `io.yupiik.fusion.framework.api.main.Launcher`: default `main` entry point for applications + (`CliLauncher` variant for CLI applications - it skips the command name argument, prefer it with `fusion-cli`). +- `io.yupiik.fusion.framework.api.spi.FusionModule`: SPI the generated code implements to register beans/listeners. + +## Declaring and consuming beans + +```java +@ApplicationScoped +public class MyService { + @Injection + OtherBean dependency; // or constructor injection; wired by generated code, no reflection + + @Init + public void start() { /* post-construct */ } + + @Destroy + public void stop() { /* pre-destroy */ } + + public void onStart(@OnEvent final Start start) { /* container started */ } +} + +// embedding the container manually (Launcher does it for you): +try (final var container = ConfiguringContainer.of().start(); + final var lookup = container.lookup(MyService.class)) { + lookup.instance().doSomething(); +} +``` + +## Module rules + +- This is the API consumers code against at runtime: any signature change is a breaking change, be conservative. +- No reflection: bean registration flows through `FusionModule` implementations generated by `fusion-processor`. + + + +## Working in this module + +- Build it: `mvn install -pl fusion-api -am` (from the repository root). +- The root [AGENTS.md](/AGENTS.md) holds the global rules: reflectionless design, license headers, parallel JUnit 5 tests, ASCII-only. + diff --git a/fusion-api/pom.xml b/fusion-api/pom.xml index b5adbfb..5d1140b 100644 --- a/fusion-api/pom.xml +++ b/fusion-api/pom.xml @@ -26,6 +26,7 @@ fusion-api Fusion :: API + Fusion runtime API: IoC container, scopes, lifecycle, events and configuration. diff --git a/fusion-build-api/AGENTS.md b/fusion-build-api/AGENTS.md new file mode 100644 index 0000000..c4f1df1 --- /dev/null +++ b/fusion-build-api/AGENTS.md @@ -0,0 +1,44 @@ + +# Fusion :: Build API (`fusion-build-api`) + +API which is used by the annotation processor but not the runtime. + +All user-facing annotations live here, under `io.yupiik.fusion.framework.build.api.*`. Consumers declare this +module in `provided` scope: it is only needed at compile time, the annotation processor (`fusion-processor`) +translates the annotations into generated code. + +## API catalog (generated from the classpath) + +- `cli`: `@Command` +- `configuration`: `@ConfigurationModel`, `@Property`, `@RootConfiguration` +- `container`: `@DetectableContext`, `@LazyContext` +- `event`: `@OnEvent` +- `http`: `@HttpJavaMatcher`, `@HttpMatcher` +- `json`: `@JsonModel`, `@JsonOthers`, `@JsonProperty` +- `jsonrpc`: `@JsonRpc`, `@JsonRpcError`, `@JsonRpcParam` +- `kubernetes.crd`: `@CustomResourceDefinition` +- `lifecycle`: `@Destroy`, `@Init` +- `metadata`: `@BeanMetadata`, `@BeanMetadataAlias` +- `metadata.spi`: `MetadataContributor` +- `order`: `@Order` +- `persistence`: `@Column`, `@Id`, `@OnDelete`, `@OnInsert`, `@OnLoad`, `@OnUpdate`, `@Table` +- `scanning`: `@Bean`, `@Injection` + +## Module rules + +- Compile-time only: NEVER make runtime code require this jar. +- Adding a type here without handling it in `fusion-processor` ships a no-op API: both always change together + (plus documentation in `fusion-documentation`). +- Annotations must stay dependency-free (JDK only). + + + +## Working in this module + +- Build it: `mvn install -pl fusion-build-api -am` (from the repository root). +- The root [AGENTS.md](/AGENTS.md) holds the global rules: reflectionless design, license headers, parallel JUnit 5 tests, ASCII-only. + diff --git a/fusion-build-internal/AGENTS.md b/fusion-build-internal/AGENTS.md new file mode 100644 index 0000000..9c88242 --- /dev/null +++ b/fusion-build-internal/AGENTS.md @@ -0,0 +1,37 @@ + +# Fusion :: Build Internal (`fusion-build-internal`) + +Internal build tooling keeping repository agent files (AGENTS.md/CLAUDE.md) in sync. Never published. + +This module generates the file you are reading: all `AGENTS.md` files and the root `CLAUDE.md` are rendered +from handlebars templates at `process-classes` phase (exec-maven-plugin) and committed to git. + +## Layout and entry points + +- `io.yupiik.fusion.build.internal.agent.AgentsFileSynchronizer`: the generator (also a plain `main`). +- `src/main/resources/agents/root.md` and `agents/claude.md`: root `AGENTS.md`/`CLAUDE.md` templates. +- `src/main/resources/agents/module/.md`: per-module templates (`_default.md` fallback, + `_footer.md` shared tail). +- Data available in templates: pom data (module list, names, descriptions, java release), per-module + configuration tables from `META-INF/fusion/configuration/documentation.json`, and the `fusion-build-api` + annotation catalog. + +## Module rules + +- To change any AGENTS.md/CLAUDE.md: edit the template, then run `mvn install -pl fusion-build-internal` + and commit both the template and the regenerated files. +- `-Dfusion.build-internal.agents.mode=check` makes the build fail on stale files instead of rewriting them (CI friendly). +- This module is never published (deploy and central publishing are skipped). +- When a module is added to the reactor, add its template here (the build warns and falls back to `_default.md`). + + + +## Working in this module + +- Build it: `mvn install -pl fusion-build-internal -am` (from the repository root). +- The root [AGENTS.md](/AGENTS.md) holds the global rules: reflectionless design, license headers, parallel JUnit 5 tests, ASCII-only. + diff --git a/fusion-build-internal/pom.xml b/fusion-build-internal/pom.xml new file mode 100644 index 0000000..2b5fcdd --- /dev/null +++ b/fusion-build-internal/pom.xml @@ -0,0 +1,161 @@ + + + + 4.0.0 + + io.yupiik.fusion + fusion + 1.0.38-SNAPSHOT + + + fusion-build-internal + Fusion :: Build Internal + Internal build tooling keeping repository agent files (AGENTS.md/CLAUDE.md) in sync. Never published. + + + true + sync + + + + + ${project.groupId} + fusion-documentation + ${project.version} + + + ${project.groupId} + fusion-handlebars + ${project.version} + + + ${project.groupId} + fusion-json + ${project.version} + + + ${project.groupId} + fusion-api + ${project.version} + + + ${project.groupId} + fusion-build-api + ${project.version} + + + ${project.groupId} + fusion-http-server + ${project.version} + + + ${project.groupId} + fusion-testing + ${project.version} + + + ${project.groupId} + fusion-jsonrpc + ${project.version} + + + ${project.groupId} + fusion-cli + ${project.version} + + + ${project.groupId} + fusion-httpclient + ${project.version} + + + ${project.groupId} + fusion-kubernetes-client + ${project.version} + + + ${project.groupId} + fusion-tracing + ${project.version} + + + ${project.groupId} + fusion-observability + ${project.version} + + + ${project.groupId} + fusion-persistence + ${project.version} + + + ${project.groupId} + fusion-jwt + ${project.version} + + + ${project.groupId} + fusion-kubernetes-operator-base + ${project.version} + + + + + + + org.codehaus.mojo + exec-maven-plugin + + + sync-agents-md + process-classes + + java + + + io.yupiik.fusion.build.internal.agent.AgentsFileSynchronizer + + ${maven.multiModuleProjectDirectory} + ${project.version} + ${fusion.build-internal.agents.mode} + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + ${maven.multiModuleProjectDirectory} + ${project.version} + + + + + org.sonatype.central + central-publishing-maven-plugin + + true + + + + + diff --git a/fusion-build-internal/src/main/java/io/yupiik/fusion/build/internal/agent/AgentsFileSynchronizer.java b/fusion-build-internal/src/main/java/io/yupiik/fusion/build/internal/agent/AgentsFileSynchronizer.java new file mode 100644 index 0000000..4d0ff69 --- /dev/null +++ b/fusion-build-internal/src/main/java/io/yupiik/fusion/build/internal/agent/AgentsFileSynchronizer.java @@ -0,0 +1,344 @@ +/* + * Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.yupiik.fusion.build.internal.agent; + +import io.yupiik.fusion.documentation.DocumentationGenerator; +import io.yupiik.fusion.framework.handlebars.HandlebarsCompiler; +import io.yupiik.fusion.json.internal.JsonMapperImpl; +import org.w3c.dom.Element; + +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.File; +import java.io.IOException; +import java.net.JarURLConnection; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.logging.Logger; +import java.util.stream.Stream; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Optional.ofNullable; +import static java.util.stream.Collectors.joining; + +/** + * Keeps the repository agent instruction files (root {@code AGENTS.md}/{@code CLAUDE.md} and per module + * {@code AGENTS.md}) in sync with the build. + *

+ * Inputs are the maven poms (module list, names, descriptions, java release), the annotation processor + * configuration metadata ({@code META-INF/fusion/configuration/documentation.json} found on the classpath) + * and the handlebars templates in {@code src/main/resources/agents}. + */ +public class AgentsFileSynchronizer implements Runnable { + private final Logger logger = Logger.getLogger(getClass().getName()); + + private final Path repositoryRoot; + private final Path outputRoot; + private final String version; + private final boolean checkOnly; + + public AgentsFileSynchronizer(final Path repositoryRoot, final Path outputRoot, + final String version, final boolean checkOnly) { + this.repositoryRoot = repositoryRoot; + this.outputRoot = outputRoot; + this.version = version; + this.checkOnly = checkOnly; + } + + public static void main(final String... args) { + if (args.length < 2) { + throw new IllegalArgumentException("Usage: AgentsFileSynchronizer [sync|check]"); + } + final var root = Path.of(args[0]).toAbsolutePath().normalize(); + new AgentsFileSynchronizer(root, root, args[1], args.length > 2 && "check".equals(args[2])).run(); + } + + @Override + public void run() { + try { + final var rootPom = readPom(repositoryRoot.resolve("pom.xml")); + final var modules = collectModules(rootPom); + final var configurations = readConfigurationTables(); + final var compiler = new HandlebarsCompiler(); + + final var globalData = new HashMap(); + globalData.put("javaRelease", rootPom.properties().getOrDefault("maven.compiler.release", "17")); + globalData.put("modules", modules.stream() + .map(module -> Map.of( + "artifactId", module.artifactId(), + "path", module.path(), + "name", module.name(), + "description", module.description())) + .toList()); + + final var stale = new ArrayList(); + write(outputRoot.resolve("AGENTS.md"), banner("agents/root.md") + render(compiler, "agents/root.md", globalData), stale); + write(outputRoot.resolve("CLAUDE.md"), banner("agents/claude.md") + render(compiler, "agents/claude.md", globalData), stale); + for (final var module : modules) { + final var data = new HashMap(globalData); + data.put("artifactId", module.artifactId()); + data.put("path", module.path()); + data.put("name", module.name()); + data.put("description", module.description()); + data.put("configuration", configurations.getOrDefault(module.artifactId(), "")); + if ("fusion-build-api".equals(module.artifactId())) { + data.put("annotations", buildApiAnnotationCatalog()); + } + data.put("footer", render(compiler, "agents/module/_footer.md", data)); + + final var template = ofNullable(getClass().getClassLoader().getResource("agents/module/" + module.artifactId() + ".md")) + .map(url -> "agents/module/" + module.artifactId() + ".md") + .orElseGet(() -> { + logger.warning(() -> "No template for module '" + module.artifactId() + "', " + + "using agents/module/_default.md, please add agents/module/" + module.artifactId() + ".md"); + return "agents/module/_default.md"; + }); + write(outputRoot.resolve(module.path()).resolve("AGENTS.md"), banner(template) + render(compiler, template, data), stale); + } + + if (checkOnly && !stale.isEmpty()) { + throw new IllegalStateException( + "Stale agent files, refresh them with 'mvn install -pl fusion-build-internal': " + stale); + } + } catch (final IOException e) { + throw new IllegalStateException(e); + } + } + + private String banner(final String template) { + return "\n"; + } + + private String render(final HandlebarsCompiler compiler, final String template, final Map data) throws IOException { + try (final var in = getClass().getClassLoader().getResourceAsStream(template)) { + if (in == null) { + throw new IllegalStateException("Missing template '" + template + "'"); + } + final var rendered = compiler + .compile(new HandlebarsCompiler.CompilationContext(new String(in.readAllBytes(), UTF_8))) + .render(data); + return rendered.endsWith("\n") ? rendered : rendered + '\n'; + } + } + + private void write(final Path target, final String content, final List stale) throws IOException { + if (Files.exists(target) && content.equals(Files.readString(target))) { + return; + } + if (checkOnly) { + stale.add(target); + return; + } + if (target.getParent() != null) { + Files.createDirectories(target.getParent()); + } + Files.writeString(target, content); + logger.info(() -> "Updated '" + target + "'"); + } + + private List collectModules(final Pom rootPom) { + return rootPom.modules().stream() + .flatMap(module -> collectModules(module, repositoryRoot)) + .toList(); + } + + private Stream collectModules(final String relativePath, final Path base) { + final var directory = base.resolve(relativePath); + final var pom = readPom(directory.resolve("pom.xml")); + final var path = repositoryRoot.relativize(directory).toString().replace(File.separatorChar, '/'); + final var self = new Module(pom.artifactId(), path, pom.name(), pom.description() == null || pom.description().isBlank() ? pom.name() : pom.description()); + return Stream.concat( + Stream.of(self), + pom.modules().stream().flatMap(child -> collectModules(child, directory))); + } + + private Pom readPom(final Path pom) { + try { + final var factory = DocumentBuilderFactory.newInstance(); + factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + final var project = factory.newDocumentBuilder().parse(pom.toFile()).getDocumentElement(); + final var properties = new HashMap(); + directChild(project, "properties").ifPresent(props -> { + final var children = props.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + if (children.item(i) instanceof Element property) { + properties.put(property.getTagName(), property.getTextContent().strip()); + } + } + }); + final var modules = new ArrayList(); + directChild(project, "modules").ifPresent(mods -> { + final var children = mods.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + if (children.item(i) instanceof Element module) { + modules.add(module.getTextContent().strip()); + } + } + }); + return new Pom( + directChild(project, "artifactId").map(Element::getTextContent).map(String::strip).orElseThrow(), + directChild(project, "name").map(Element::getTextContent).map(String::strip).orElse(""), + directChild(project, "description").map(Element::getTextContent).map(String::strip).orElse(""), + modules, properties); + } catch (final RuntimeException re) { + throw re; + } catch (final Exception e) { + throw new IllegalStateException("Can't read '" + pom + "'", e); + } + } + + private Optional directChild(final Element parent, final String name) { + final var children = parent.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + if (children.item(i) instanceof Element element && name.equals(element.getTagName())) { + return Optional.of(element); + } + } + return Optional.empty(); + } + + private Map readConfigurationTables() throws IOException { + final var tables = new TreeMap(); + final var parameters = new ConfigurationParameters(); + try (final var json = new JsonMapperImpl(List.of(), c -> Optional.empty())) { + final var docs = getClass().getClassLoader().getResources("META-INF/fusion/configuration/documentation.json"); + while (docs.hasMoreElements()) { + final var url = docs.nextElement(); + try (final var in = url.openStream()) { + if (!(json.fromBytes(Object.class, in.readAllBytes()) instanceof Map doc) || + !(doc.get("classes") instanceof Map classes)) { + continue; + } + @SuppressWarnings("unchecked") final var roots = ofNullable((List) doc.get("roots")).orElse(List.of()); + if (roots.isEmpty()) { + continue; + } + tables.put(moduleOf(url), asMarkdownTable(parameters.parameters(classes, roots))); + } + } + } + return tables; + } + + private String moduleOf(final URL url) { + final var file = url.getFile(); + final var target = file.indexOf("/target/classes/"); + if (target > 0) { // reactor case: ...//target/classes/META-INF/... + return file.substring(file.lastIndexOf('/', target - 1) + 1, target); + } + // jar case: .../-.jar!/META-INF/... + final var end = file.indexOf(".jar!/"); + if (end > 0) { + final var jarName = file.substring(file.lastIndexOf('/', end) + 1, end); + final var versionSuffix = "-" + version; + return jarName.endsWith(versionSuffix) ? jarName.substring(0, jarName.length() - versionSuffix.length()) : jarName; + } + return file; + } + + private String asMarkdownTable(final List parameters) { + return "| Name | Env variable | Description | Default | Required |\n" + + "|---|---|---|---|---|\n" + + parameters.stream() + .map(parameter -> "| `" + cell(parameter.name()) + "` " + + "| `" + cell(parameter.envName()) + "` " + + "| " + cell(ofNullable(parameter.documentation()).orElse("-")) + " " + + "| " + (parameter.defaultValue() == null ? "-" : "`" + cell(String.valueOf(parameter.defaultValue())) + "`") + " " + + "| " + (parameter.required() ? "yes" : "no") + " |") + .collect(joining("\n")); + } + + private String cell(final String value) { + return value.replace("|", "\\|").replace("\r\n", "\n").replace("\n", "
"); + } + + private String buildApiAnnotationCatalog() throws IOException { + final var basePackage = "io/yupiik/fusion/framework/build/api/"; + final var marker = basePackage + "scanning/Bean.class"; + final var url = getClass().getClassLoader().getResource(marker); + if (url == null) { + throw new IllegalStateException("Can't locate fusion-build-api on the classpath"); + } + final var byPackage = new TreeMap>(); + if ("jar".equals(url.getProtocol())) { + final var jar = ((JarURLConnection) url.openConnection()).getJarFile(); + jar.stream() + .map(java.util.jar.JarEntry::getName) + .filter(name -> name.startsWith(basePackage) && name.endsWith(".class")) + .forEach(name -> addToCatalog(byPackage, name.substring(basePackage.length()))); + } else { + final var root = Path.of(url.getPath().substring(0, url.getPath().length() - marker.length())).resolve(basePackage); + try (final var walk = Files.walk(root)) { + walk.filter(Files::isRegularFile) + .map(path -> root.relativize(path).toString().replace(File.separatorChar, '/')) + .filter(name -> name.endsWith(".class")) + .forEach(name -> addToCatalog(byPackage, name)); + } + } + return byPackage.entrySet().stream() + .map(entry -> "- `" + entry.getKey() + "`: " + entry.getValue().stream() + .map(type -> "`" + (isAnnotation("io.yupiik.fusion.framework.build.api." + entry.getKey() + "." + type) ? "@" : "") + type + "`") + .collect(joining(", "))) + .collect(joining("\n")); + } + + private boolean isAnnotation(final String fqn) { + try { + return getClass().getClassLoader().loadClass(fqn).isAnnotation(); + } catch (final ClassNotFoundException e) { + return false; + } + } + + private void addToCatalog(final Map> byPackage, final String relativeClassName) { + final var className = relativeClassName.substring(0, relativeClassName.length() - ".class".length()); + if (className.contains("$") || className.endsWith("package-info") || !className.contains("/")) { + return; + } + final var sep = className.lastIndexOf('/'); + byPackage.computeIfAbsent(className.substring(0, sep).replace('/', '.'), k -> new TreeSet<>()) + .add(className.substring(sep + 1)); + } + + private record Module(String artifactId, String path, String name, String description) { + } + + private record Pom(String artifactId, String name, String description, + List modules, Map properties) { + } + + // just opens the protected extraction logic of the base generator + private static class ConfigurationParameters extends DocumentationGenerator { + private ConfigurationParameters() { + super(Path.of("."), Map.of()); + } + + private List parameters(final Map classes, final List roots) { + return findParameters(classes, roots); + } + } +} diff --git a/fusion-build-internal/src/main/resources/agents/claude.md b/fusion-build-internal/src/main/resources/agents/claude.md new file mode 100644 index 0000000..f9c8806 --- /dev/null +++ b/fusion-build-internal/src/main/resources/agents/claude.md @@ -0,0 +1,6 @@ +@AGENTS.md + +Per-module guides live in each module directory (`/AGENTS.md`), read the one matching the module you work on. + +Note: this file and all `AGENTS.md` files are generated by `fusion-build-internal`. To change them, edit the +templates in `fusion-build-internal/src/main/resources/agents/` and run `mvn install -pl fusion-build-internal`. diff --git a/fusion-build-internal/src/main/resources/agents/module/_default.md b/fusion-build-internal/src/main/resources/agents/module/_default.md new file mode 100644 index 0000000..76deb76 --- /dev/null +++ b/fusion-build-internal/src/main/resources/agents/module/_default.md @@ -0,0 +1,8 @@ +# {{{name}}} (`{{{artifactId}}}`) + +{{{description}}} + +No specific template was written for this module yet - add +`fusion-build-internal/src/main/resources/agents/module/{{{artifactId}}}.md` to document its entry points. + +{{{footer}}} diff --git a/fusion-build-internal/src/main/resources/agents/module/_footer.md b/fusion-build-internal/src/main/resources/agents/module/_footer.md new file mode 100644 index 0000000..d7c5bf5 --- /dev/null +++ b/fusion-build-internal/src/main/resources/agents/module/_footer.md @@ -0,0 +1,12 @@ +{{#if configuration}} +## Configuration properties + +Generated from the annotation processor metadata (`META-INF/fusion/configuration/documentation.json`): + +{{{configuration}}} +{{/if}} + +## Working in this module + +- Build it: `mvn install -pl {{{path}}} -am` (from the repository root). +- The root [AGENTS.md](/AGENTS.md) holds the global rules: reflectionless design, license headers, parallel JUnit 5 tests, ASCII-only. diff --git a/fusion-build-internal/src/main/resources/agents/module/fusion-api.md b/fusion-build-internal/src/main/resources/agents/module/fusion-api.md new file mode 100644 index 0000000..57364e5 --- /dev/null +++ b/fusion-build-internal/src/main/resources/agents/module/fusion-api.md @@ -0,0 +1,47 @@ +# {{{name}}} (`{{{artifactId}}}`) + +{{{description}}} + +Runtime side of the framework: the IoC container consumers embed in their application. + +## Entry points + +- `io.yupiik.fusion.framework.api.ConfiguringContainer`: bootstrap API (`ConfiguringContainer.of().start()`). +- `io.yupiik.fusion.framework.api.RuntimeContainer` / `Instance`: running container and bean instance handles. +- `io.yupiik.fusion.framework.api.scope`: `@ApplicationScoped` (lazy singleton) and `@DefaultScoped` (instance per lookup). +- `io.yupiik.fusion.framework.api.event`: event bus (`Emitter`), container lifecycle events `Start`/`Stop`. +- `io.yupiik.fusion.framework.api.configuration.Configuration`: runtime configuration lookup abstraction. +- `io.yupiik.fusion.framework.api.main.Launcher`: default `main` entry point for applications + (`CliLauncher` variant for CLI applications - it skips the command name argument, prefer it with `fusion-cli`). +- `io.yupiik.fusion.framework.api.spi.FusionModule`: SPI the generated code implements to register beans/listeners. + +## Declaring and consuming beans + +```java +@ApplicationScoped +public class MyService { + @Injection + OtherBean dependency; // or constructor injection; wired by generated code, no reflection + + @Init + public void start() { /* post-construct */ } + + @Destroy + public void stop() { /* pre-destroy */ } + + public void onStart(@OnEvent final Start start) { /* container started */ } +} + +// embedding the container manually (Launcher does it for you): +try (final var container = ConfiguringContainer.of().start(); + final var lookup = container.lookup(MyService.class)) { + lookup.instance().doSomething(); +} +``` + +## Module rules + +- This is the API consumers code against at runtime: any signature change is a breaking change, be conservative. +- No reflection: bean registration flows through `FusionModule` implementations generated by `fusion-processor`. + +{{{footer}}} diff --git a/fusion-build-internal/src/main/resources/agents/module/fusion-build-api.md b/fusion-build-internal/src/main/resources/agents/module/fusion-build-api.md new file mode 100644 index 0000000..8269813 --- /dev/null +++ b/fusion-build-internal/src/main/resources/agents/module/fusion-build-api.md @@ -0,0 +1,20 @@ +# {{{name}}} (`{{{artifactId}}}`) + +{{{description}}} + +All user-facing annotations live here, under `io.yupiik.fusion.framework.build.api.*`. Consumers declare this +module in `provided` scope: it is only needed at compile time, the annotation processor (`fusion-processor`) +translates the annotations into generated code. + +## API catalog (generated from the classpath) + +{{{annotations}}} + +## Module rules + +- Compile-time only: NEVER make runtime code require this jar. +- Adding a type here without handling it in `fusion-processor` ships a no-op API: both always change together + (plus documentation in `fusion-documentation`). +- Annotations must stay dependency-free (JDK only). + +{{{footer}}} diff --git a/fusion-build-internal/src/main/resources/agents/module/fusion-build-internal.md b/fusion-build-internal/src/main/resources/agents/module/fusion-build-internal.md new file mode 100644 index 0000000..a4a8cbe --- /dev/null +++ b/fusion-build-internal/src/main/resources/agents/module/fusion-build-internal.md @@ -0,0 +1,26 @@ +# {{{name}}} (`{{{artifactId}}}`) + +{{{description}}} + +This module generates the file you are reading: all `AGENTS.md` files and the root `CLAUDE.md` are rendered +from handlebars templates at `process-classes` phase (exec-maven-plugin) and committed to git. + +## Layout and entry points + +- `io.yupiik.fusion.build.internal.agent.AgentsFileSynchronizer`: the generator (also a plain `main`). +- `src/main/resources/agents/root.md` and `agents/claude.md`: root `AGENTS.md`/`CLAUDE.md` templates. +- `src/main/resources/agents/module/.md`: per-module templates (`_default.md` fallback, + `_footer.md` shared tail). +- Data available in templates: pom data (module list, names, descriptions, java release), per-module + configuration tables from `META-INF/fusion/configuration/documentation.json`, and the `fusion-build-api` + annotation catalog. + +## Module rules + +- To change any AGENTS.md/CLAUDE.md: edit the template, then run `mvn install -pl fusion-build-internal` + and commit both the template and the regenerated files. +- `-Dfusion.build-internal.agents.mode=check` makes the build fail on stale files instead of rewriting them (CI friendly). +- This module is never published (deploy and central publishing are skipped). +- When a module is added to the reactor, add its template here (the build warns and falls back to `_default.md`). + +{{{footer}}} diff --git a/fusion-build-internal/src/main/resources/agents/module/fusion-cli.md b/fusion-build-internal/src/main/resources/agents/module/fusion-cli.md new file mode 100644 index 0000000..e958c41 --- /dev/null +++ b/fusion-build-internal/src/main/resources/agents/module/fusion-cli.md @@ -0,0 +1,49 @@ +# {{{name}}} (`{{{artifactId}}}`) + +{{{description}}} + +Classes annotated with `@Command` (from `fusion-build-api`) become executable CLI commands with their +parameters mapped from a `@RootConfiguration` record. + +## Declaring a command + +```java +@Command(name = "my-command", description = "A super command.") +public class MyCommand implements Runnable { + private final Conf conf; + private final JsonMapper jsonMapper; // any other constructor parameter is an injected bean + + public MyCommand(final Conf conf, final JsonMapper jsonMapper) { + this.conf = conf; + this.jsonMapper = jsonMapper; + } + + @Override + public void run() { + // command implementation + } + + @RootConfiguration("my-command") // options become --my-command-xxx (use prefix "-" for plain --xxx) + public record Conf(String name) {} +} +``` + +- The command must be a `Runnable`; its FIRST constructor parameter must be the configuration record. +- Constructor injections are supported after the configuration parameter: any simple-type bean can be + injected (no lists - wrap a list in a dedicated bean if needed). + +## Launching + +- `io.yupiik.fusion.framework.api.main.CliLauncher` (in `fusion-api`) is the PREFERRED main: unlike plain + `Launcher` it skips the first argument (the command name), so `my-app my-command --my-command-name foo` + maps the remaining args to the command configuration. +- Without the launcher integration, call `io.yupiik.fusion.cli.CliAwaiter` yourself and register an + `Args` instance in the container. + +## Module rules + +- Command parameter binding reuses the configuration subsystem: options are documented/validated like any + `@RootConfiguration`. +- Test commands with `@FusionCLITest` from `fusion-testing` (captures `Stdout`/`Stderr`). + +{{{footer}}} diff --git a/fusion-build-internal/src/main/resources/agents/module/fusion-documentation.md b/fusion-build-internal/src/main/resources/agents/module/fusion-documentation.md new file mode 100644 index 0000000..06436b6 --- /dev/null +++ b/fusion-build-internal/src/main/resources/agents/module/fusion-documentation.md @@ -0,0 +1,25 @@ +# {{{name}}} (`{{{artifactId}}}`) + +{{{description}}} + +Two things live here: the AsciiDoc minisite (published to https://yupiik.github.io/fusion/) and the doc +generators run as `yupiik-tools-maven-plugin` pre-actions. + +## Layout and entry points + +- `src/main/minisite/content/fusion/*.adoc`: hand-written documentation pages (one per feature/module). +- `src/main/minisite/content/_partials/generated/`: GENERATED configuration docs - never edit by hand. +- `io.yupiik.fusion.documentation.ConfigurationDocumentationGenerator` (and legacy `DocumentationGenerator`): + renders `META-INF/fusion/configuration/documentation.json` metadata to AsciiDoc. +- `io.yupiik.fusion.documentation.OpenRpcGenerator` / `OpenRPC2Adoc` / `OpenRPC2OpenAPI` / `OpenRPC2Postman`: + JSON-RPC documentation converters. +- Preview the site locally: `mvn compile yupiik-tools:serve-minisite -e` (in this module). + +## Module rules + +- AsciiDoc conventions: tables with inline rows (a full row on one line when possible), `opts=headers` on + tables, inline admonitions instead of blocks. +- Any user-facing feature change in other modules needs its `.adoc` page updated here. +- The default profile regenerates content on every build (`skipRendering=true`): commit the regenerated files. + +{{{footer}}} diff --git a/fusion-build-internal/src/main/resources/agents/module/fusion-handlebars.md b/fusion-build-internal/src/main/resources/agents/module/fusion-handlebars.md new file mode 100644 index 0000000..97dc0b2 --- /dev/null +++ b/fusion-build-internal/src/main/resources/agents/module/fusion-handlebars.md @@ -0,0 +1,20 @@ +# {{{name}}} (`{{{artifactId}}}`) + +{{{description}}} + +Small handlebars-compatible template engine (each/if/unless/with, partials, helpers, `@first`/`@last`/`@index` +data variables) used for server-side rendering without any third party dependency. + +## Entry points + +- `io.yupiik.fusion.framework.handlebars.HandlebarsCompiler`: compile a template + (`compile(new CompilationContext(content)).render(data)`); `Settings` carries helpers and partials. +- `io.yupiik.fusion.framework.handlebars.spi.Accessor` / `Template`: data access and rendering SPI. +- `io.yupiik.fusion.framework.handlebars.helper`: built-in helpers. + +## Module rules + +- Double-brace interpolation HTML-escapes, triple-brace renders raw: preserve that handlebars semantic. +- Data access goes through `Accessor` (maps/lists by default): no reflection on user objects. + +{{{footer}}} diff --git a/fusion-build-internal/src/main/resources/agents/module/fusion-http-server.md b/fusion-build-internal/src/main/resources/agents/module/fusion-http-server.md new file mode 100644 index 0000000..fe4cd02 --- /dev/null +++ b/fusion-build-internal/src/main/resources/agents/module/fusion-http-server.md @@ -0,0 +1,48 @@ +# {{{name}}} (`{{{artifactId}}}`) + +{{{description}}} + +Embeds Apache Tomcat behind a small reactive-friendly `Request`/`Response` API; endpoints are declared with +`@HttpMatcher` (compiled to `Endpoint` beans by the processor) or implemented as `Endpoint` beans directly. + +```java +// preferred: declarative matcher on a bean method (Request parameter and CompletionStage are optional) +@HttpMatcher(method = "GET", pathMatching = EXACT, path = "/greet") +public CompletionStage greet(final Request request) { + return completedFuture(Response.of() + .status(200) + .header("content-type", "application/json") + .body("{\"hello\":true}") + .build()); +} + +// alternative: implement Endpoint directly as a bean +@Bean +public class Greeting implements Endpoint { + @Override + public boolean matches(final Request request) { + return "GET".equals(request.method()); + } + + @Override + public CompletionStage handle(final Request request) { + return completedFuture(Response.of().body("{\"hello\":true}").build()); + } +} +``` + +## Entry points + +- `io.yupiik.fusion.http.server.api.WebServer`: server bootstrap/configuration (`WebServer.Configuration` bean). +- `io.yupiik.fusion.http.server.api.Request` / `Response` / `Cookie` / `Body`: HTTP abstraction. +- `io.yupiik.fusion.http.server.spi.Endpoint` / `BaseEndpoint`: endpoint SPI the generated matchers implement. +- `io.yupiik.fusion.http.server.spi.MonitoringEndpoint`: endpoints for the observability server. +- `io.yupiik.fusion.http.server.impl.tomcat`: Tomcat wiring (internal). + +## Module rules + +- Tomcat (`tomcat-*` artifacts) is the ONLY third party runtime dependency family here; keep it that way. +- Response bodies use `java.util.concurrent.Flow` publishers: never block Tomcat threads in helpers. +- Tests run in parallel: always bind test servers to port `0` (random port), never a fixed port. + +{{{footer}}} diff --git a/fusion-build-internal/src/main/resources/agents/module/fusion-httpclient-parent.md b/fusion-build-internal/src/main/resources/agents/module/fusion-httpclient-parent.md new file mode 100644 index 0000000..174c77b --- /dev/null +++ b/fusion-build-internal/src/main/resources/agents/module/fusion-httpclient-parent.md @@ -0,0 +1,10 @@ +# {{{name}}} (`{{{artifactId}}}`) + +{{{description}}} + +Aggregator pom only, no code. See the children guides: + +- [fusion-httpclient](fusion-httpclient/AGENTS.md): extended `java.net.http` client. +- [fusion-kubernetes-client](fusion-kubernetes-client/AGENTS.md): lightweight Kubernetes client. + +{{{footer}}} diff --git a/fusion-build-internal/src/main/resources/agents/module/fusion-httpclient.md b/fusion-build-internal/src/main/resources/agents/module/fusion-httpclient.md new file mode 100644 index 0000000..db45900 --- /dev/null +++ b/fusion-build-internal/src/main/resources/agents/module/fusion-httpclient.md @@ -0,0 +1,18 @@ +# {{{name}}} (`{{{artifactId}}}`) + +{{{description}}} + +Decorators around the JDK `java.net.http.HttpClient` - no custom HTTP stack. + +## Entry points + +- `io.yupiik.fusion.httpclient.core.ExtendedHttpClient` + `ExtendedHttpClientConfiguration`: main entry point + (retries, logging, listeners). +- `io.yupiik.fusion.httpclient.core.RoutingHttpClient` / `ThrottledHttpClient` / `DelegatingHttpClient`: composable decorators. +- `listener` package: request/response listeners (used by `fusion-tracing`). + +## Module rules + +- Stay a thin layer over the JDK client: no third party HTTP dependency. + +{{{footer}}} diff --git a/fusion-build-internal/src/main/resources/agents/module/fusion-json.md b/fusion-build-internal/src/main/resources/agents/module/fusion-json.md new file mode 100644 index 0000000..e98f3fa --- /dev/null +++ b/fusion-build-internal/src/main/resources/agents/module/fusion-json.md @@ -0,0 +1,38 @@ +# {{{name}}} (`{{{artifactId}}}`) + +{{{description}}} + +Records annotated with `@JsonModel` get a codec generated at compile time; this module provides the runtime +mapper executing those codecs plus a streaming parser. + +```java +@JsonModel +public record MyModel( + @JsonProperty("boolean") boolean aBool, // renames the attribute in the JSON payload + String simplest, + LocalDate date, + AnotherModel nested, + List list, + @JsonOthers Map extensions) { // catch-all for unknown attributes +} + +// JsonMapper is a bean once fusion-json is on the classpath, inject it +final MyModel model = jsonMapper.fromString(MyModel.class, "{\"simplest\":\"value\"}"); +final String json = jsonMapper.toString(model); +``` + +## Entry points + +- `io.yupiik.fusion.json.JsonMapper`: main user API (a bean once `FusionJsonModule` is on the classpath). +- `io.yupiik.fusion.json.serialization.JsonCodec`: SPI implemented by generated codecs. +- `io.yupiik.fusion.json.spi.Parser`: streaming parser abstraction. +- `io.yupiik.fusion.json.pretty.PrettyJsonMapper`: formatting decorator. +- `io.yupiik.fusion.json.patch`/`pointer`/`diff`: JSON-Patch, JSON-Pointer and diff utilities. +- `io.yupiik.fusion.json.internal.JsonMapperImpl`: implementation (internal, do not expose in signatures). + +## Module rules + +- Reflectionless: no reflection-based (de)serialization fallback, unsupported types must fail clearly. +- Codec generation lives in `fusion-processor` (`internal/json`), only the runtime belongs here. + +{{{footer}}} diff --git a/fusion-build-internal/src/main/resources/agents/module/fusion-jsonrpc.md b/fusion-build-internal/src/main/resources/agents/module/fusion-jsonrpc.md new file mode 100644 index 0000000..06fee5c --- /dev/null +++ b/fusion-build-internal/src/main/resources/agents/module/fusion-jsonrpc.md @@ -0,0 +1,36 @@ +# {{{name}}} (`{{{artifactId}}}`) + +{{{description}}} + +Methods annotated with `@JsonRpc` (from `fusion-build-api`) become JSON-RPC 2.0 methods served over the +Fusion HTTP server (single endpoint, default `/jsonrpc`). + +```java +public class Endpoints { // a bean, methods can be sync or return a CompletionStage + @JsonRpc("copy") + public MyResult result(final MyInput input) { // parameters/results are @JsonModel records + return new MyResult(input.name()); + } + + @JsonModel + public record MyInput(String name) {} + + @JsonModel + public record MyResult(String name) {} +} +``` + +## Entry points + +- `io.yupiik.fusion.jsonrpc.JsonRpcEndpoint`: the HTTP endpoint dispatching requests. +- `io.yupiik.fusion.jsonrpc.JsonRpcHandler` / `JsonRpcRegistry`: method resolution and execution. +- `io.yupiik.fusion.jsonrpc.JsonRpcException` / `Response`: error and response modeling. +- `io.yupiik.fusion.jsonrpc.bean.OpenRPCEndpoint` / `OpenRPCHttpEndpoint`: openrpc.json exposure of the registered methods. +- `io.yupiik.fusion.jsonrpc.event.BeforeRequest`: hook event fired before method execution (validation/auth). + +## Module rules + +- Protocol behavior (error codes, batch handling) follows JSON-RPC 2.0: check the spec before changing responses. +- The OpenRPC document is generated from the processor metadata: keep it consistent when touching method binding. + +{{{footer}}} diff --git a/fusion-build-internal/src/main/resources/agents/module/fusion-jwt.md b/fusion-build-internal/src/main/resources/agents/module/fusion-jwt.md new file mode 100644 index 0000000..9c040f1 --- /dev/null +++ b/fusion-build-internal/src/main/resources/agents/module/fusion-jwt.md @@ -0,0 +1,20 @@ +# {{{name}}} (`{{{artifactId}}}`) + +{{{description}}} + +JWT creation (signer) and validation (validator) on top of JDK crypto, configured through +`@RootConfiguration` records - no third party JOSE dependency. + +## Entry points + +- `io.yupiik.fusion.jwt.JwtSignerFactory` + `JwtSignerConfiguration`: token creation. +- `io.yupiik.fusion.jwt.JwtValidatorFactory` + `JwtValidatorConfiguration`: token validation, returns `Jwt`. +- `io.yupiik.fusion.jwt.PublicKeyLoader` / `PrivateKeyLoader`: PEM key loading. +- `io.yupiik.fusion.jwt.bean`: default beans wiring the factories from the configuration. + +## Module rules + +- Security sensitive: algorithm/claim validation changes need tests for the negative cases + (expired, wrong issuer, wrong signature, `none` algorithm rejection). + +{{{footer}}} diff --git a/fusion-build-internal/src/main/resources/agents/module/fusion-kubernetes-client.md b/fusion-build-internal/src/main/resources/agents/module/fusion-kubernetes-client.md new file mode 100644 index 0000000..12cdd6f --- /dev/null +++ b/fusion-build-internal/src/main/resources/agents/module/fusion-kubernetes-client.md @@ -0,0 +1,19 @@ +# {{{name}}} (`{{{artifactId}}}`) + +{{{description}}} + +An `HttpClient` facade preconfigured for the Kubernetes API server: in-cluster token/CA handling, kubeconfig +support, and websocket helpers. JSON payloads are handled by the caller (typically with `fusion-json`). + +## Entry points + +- `io.yupiik.fusion.kubernetes.client.KubernetesClient`: the client (implements `HttpClient`). +- `io.yupiik.fusion.kubernetes.client.KubernetesClientConfiguration`: token/certificates/API base configuration. +- `io.yupiik.fusion.kubernetes.client.WebSocketBuilderDelegate`: websocket support (exec/watch style usage). + +## Module rules + +- Keep it reflectionless and dependency-light: it must stay usable in GraalVM native operators. +- Token and certificates are refreshed from files: preserve that behavior for in-cluster long-running processes. + +{{{footer}}} diff --git a/fusion-build-internal/src/main/resources/agents/module/fusion-kubernetes-operator-base.md b/fusion-build-internal/src/main/resources/agents/module/fusion-kubernetes-operator-base.md new file mode 100644 index 0000000..00e1360 --- /dev/null +++ b/fusion-build-internal/src/main/resources/agents/module/fusion-kubernetes-operator-base.md @@ -0,0 +1,20 @@ +# {{{name}}} (`{{{artifactId}}}`) + +{{{description}}} + +Base to write Kubernetes operators with Fusion: watch/reconcile loop, CRD-typed events and awaiter, built on +`fusion-kubernetes-client`. Note the package root is `io.yupiik.kubernetes.operator.base` (not `io.yupiik.fusion`). + +## Entry points + +- `io.yupiik.kubernetes.operator.base.spi.Operator` / `BulkingOperator`: the SPI an operator implements. +- `io.yupiik.kubernetes.operator.base.impl.OperatorRuntime` / `OperatorsLifecycle`: watch/dispatch runtime. +- `io.yupiik.kubernetes.operator.base.impl.OperatorConfiguration`: runtime configuration. +- `@CustomResourceDefinition` (from `fusion-build-api`, `kubernetes/crd` package): generates the CRD JSON model. + +## Module rules + +- Must stay GraalVM native friendly (operators ship as small native images): no reflection, no dynamic loading. +- Reconciliation is event-driven and potentially concurrent: keep operator state handling thread-safe. + +{{{footer}}} diff --git a/fusion-build-internal/src/main/resources/agents/module/fusion-observability.md b/fusion-build-internal/src/main/resources/agents/module/fusion-observability.md new file mode 100644 index 0000000..daddb8a --- /dev/null +++ b/fusion-build-internal/src/main/resources/agents/module/fusion-observability.md @@ -0,0 +1,19 @@ +# {{{name}}} (`{{{artifactId}}}`) + +{{{description}}} + +Dedicated monitoring HTTP server (separate port from the business server) exposing health checks and +OpenMetrics/Prometheus-format metrics. + +## Entry points + +- `io.yupiik.fusion.observability.http.ObservabilityServer`: the monitoring server. +- `io.yupiik.fusion.observability.health.HealthCheck` / `HealthRegistry`: health SPI and registry. +- `io.yupiik.fusion.observability.metrics.MetricsRegistry` / `OpenMetricsFormatter`: metrics SPI and rendering. +- Endpoints implement `MonitoringEndpoint` from `fusion-http-server`. + +## Module rules + +- Health endpoints are used by orchestrators (Kubernetes probes): keep them non-blocking and cheap. + +{{{footer}}} diff --git a/fusion-build-internal/src/main/resources/agents/module/fusion-persistence.md b/fusion-build-internal/src/main/resources/agents/module/fusion-persistence.md new file mode 100644 index 0000000..15df644 --- /dev/null +++ b/fusion-build-internal/src/main/resources/agents/module/fusion-persistence.md @@ -0,0 +1,44 @@ +# {{{name}}} (`{{{artifactId}}}`) + +{{{description}}} + +Records annotated with `@Table`/`@Id`/`@Column` (from `fusion-build-api`) get their entity model generated at +compile time; this module executes SQL over plain JDBC with those models - no lazy loading, no dirty checking, +just typed statements and mapping. + +```java +@Table("CUSTOMER") +public record CustomerEntity( + @Id(autoIncremented = true) Integer id, + @Column String firstname, + @Column(name = "LAST_NAME") String lastname) { + + @OnInsert // lifecycle callbacks return a new instance (records are immutable) + public CustomerEntity onInsert() { + return this; + } +} + +// Database is created from a DataSource (DatabaseFactory) or injected as a bean +final CustomerEntity saved = database.insert(new CustomerEntity(null, "John", "Doe")); +final List all = database.query( + CustomerEntity.class, "select * from CUSTOMER where LAST_NAME = ?", b -> b.bind("Doe")); +``` + +## Entry points + +- `io.yupiik.fusion.persistence.api.Database` / `ContextLessDatabase` / `BaseDatabase`: main query APIs + (with or without a thread-bound connection). +- `io.yupiik.fusion.persistence.api.DatabaseFactory`: creation from a `DataSource`. +- `io.yupiik.fusion.persistence.api.TransactionManager`: transaction handling. +- `io.yupiik.fusion.persistence.api.Entity` / `StatementBinder` / `ResultSetWrapper` / `SqlBuilder`: generated-model SPI and helpers. +- `io.yupiik.fusion.persistence.spi.DatabaseTranslation`: per-database SQL dialect SPI (see `impl.translation` for the provided ones). +- `io.yupiik.fusion.persistence.impl.datasource`: simple and Tomcat-pool backed datasources (internal). + +## Module rules + +- Entity model generation lives in `fusion-processor` (`internal/persistence`); only runtime execution belongs here. +- New database dialects go through `DatabaseTranslation` implementations, not conditionals in the core. +- Tests use H2 and run in parallel: use per-test database names. + +{{{footer}}} diff --git a/fusion-build-internal/src/main/resources/agents/module/fusion-processor.md b/fusion-build-internal/src/main/resources/agents/module/fusion-processor.md new file mode 100644 index 0000000..2209acc --- /dev/null +++ b/fusion-build-internal/src/main/resources/agents/module/fusion-processor.md @@ -0,0 +1,28 @@ +# {{{name}}} (`{{{artifactId}}}`) + +{{{description}}} + +The heart of the framework: a `javax.annotation.processing.Processor` which turns the `fusion-build-api` +annotations into reflectionless runtime code (beans, DI wiring, JSON codecs, configuration binding, +persistence entities, doc metadata). + +## Entry points + +- `io.yupiik.fusion.framework.processor.FusionProcessor`: the processor, registered through + `src/main/resources/META-INF/services/javax.annotation.processing.Processor`. +- `internal/generator`: code generators (beans, listeners, subclasses/proxies). +- `internal/json`: JSON codec generation for `@JsonModel` records. +- `internal/persistence`: entity/table model generation. +- `internal/meta`: metadata emission, including `META-INF/fusion/configuration/documentation.json` + (consumed by `fusion-documentation` and `fusion-build-internal`). + +## Module rules + +- ANY new annotation added to `fusion-build-api` must be handled here, plus documentation and tests. +- Generated code must stay reflectionless and readable: it is the runtime, users debug through it. +- Tests are compilation-based: see `FusionProcessorTest` (compiles fixture sources from + `src/test/resources` and asserts on the generated output/behavior). Add a fixture per new feature. +- Supported annotations are processed per compilation unit: mind incremental compilation semantics + when changing rounds handling. + +{{{footer}}} diff --git a/fusion-build-internal/src/main/resources/agents/module/fusion-testing.md b/fusion-build-internal/src/main/resources/agents/module/fusion-testing.md new file mode 100644 index 0000000..ebbff8e --- /dev/null +++ b/fusion-build-internal/src/main/resources/agents/module/fusion-testing.md @@ -0,0 +1,30 @@ +# {{{name}}} (`{{{artifactId}}}`) + +{{{description}}} + +```java +@FusionSupport // starts/stops a container around the test class (@MonoFusionSupport: one per JVM, faster) +class MyTest { + @Test + void run(@Fusion final MyService service) { // beans are injected as test method parameters + // assertions on service + } +} +``` + +## Entry points + +- `io.yupiik.fusion.testing.FusionSupport`: JUnit 5 extension starting/stopping a container per test class. +- `io.yupiik.fusion.testing.MonoFusionSupport`: single shared container for the whole surefire execution (faster). +- `io.yupiik.fusion.testing.Fusion`: parameter injection of beans in test methods. +- `io.yupiik.fusion.testing.launcher.FusionCLITest` + `Stdout`/`Stderr`: run a CLI command and capture output. +- `io.yupiik.fusion.testing.task.Task` / `TaskResult`: run logic around the container lifecycle. +- `io.yupiik.fusion.testing.assertion.Asserts` / `JsonAsserts`: assertion helpers. +- `io.yupiik.fusion.testing.module.TestingModule`: programmatic bean replacement/addition for tests. + +## Module rules + +- This module is consumers' test-facing API: keep it JUnit 5 only, no other test framework dependency. +- Everything must be safe under parallel execution (it is the default in this build and in consumer setups). + +{{{footer}}} diff --git a/fusion-build-internal/src/main/resources/agents/module/fusion-tracing.md b/fusion-build-internal/src/main/resources/agents/module/fusion-tracing.md new file mode 100644 index 0000000..2eb2549 --- /dev/null +++ b/fusion-build-internal/src/main/resources/agents/module/fusion-tracing.md @@ -0,0 +1,20 @@ +# {{{name}}} (`{{{artifactId}}}`) + +{{{description}}} + +Zipkin-format span collection and flushing for the Fusion HTTP server (Tomcat valve) and the extended HTTP +client (listener), with OpenTelemetry-compatible export. + +## Entry points + +- `io.yupiik.fusion.tracing.server.TracingValve` + `ServerTracingConfiguration`: server side span creation. +- `io.yupiik.fusion.tracing.client.TracingListener` + `ClientTracingConfiguration`: client side propagation. +- `io.yupiik.fusion.tracing.collector.AccumulatingSpanCollector`: buffering collector. +- `io.yupiik.fusion.tracing.zipkin.ZipkinFlusher`: HTTP flusher of the accumulated spans. +- `io.yupiik.fusion.tracing.span.Span`: span model. + +## Module rules + +- Span flushing is asynchronous and batched: mind thread-safety, the collector is shared. + +{{{footer}}} diff --git a/fusion-build-internal/src/main/resources/agents/root.md b/fusion-build-internal/src/main/resources/agents/root.md new file mode 100644 index 0000000..437e301 --- /dev/null +++ b/fusion-build-internal/src/main/resources/agents/root.md @@ -0,0 +1,72 @@ +# Yupiik Fusion - agent guide + +Fusion is a reflectionless, self-contained Java framework designed to be GraalVM-native friendly. +Business code never depends on jakarta/spring/guice. Everything heavy (bean/model discovery, DI wiring, +JSON codecs, configuration binding, persistence models, proxies) is generated at COMPILE TIME by an +annotation processor; only bean resolution happens at runtime. + +- Documentation site: https://yupiik.github.io/fusion/ +- Example applications: https://github.com/yupiik/fusion-examples + +## Requirements and build commands + +Requires Java >= {{{javaRelease}}} and Maven >= 3.8 (no wrapper is committed, use your own Maven). + +| Command | Purpose | +|---|---| +| `mvn install` | Full build with tests (also regenerates all AGENTS.md/CLAUDE.md files). | +| `mvn install -pl -am` | Build one module and its dependencies. | +| `mvn test -pl -Dtest=MyTest` | Run a single test class. | +| `mvn license:format` | Add/fix the mandatory Apache-2.0 Yupiik license headers. | +| `mvn install -DskipTests ossindex:audit` | Dependency security audit. | +| `mvn compile yupiik-tools:serve-minisite -e` | Preview the documentation site (run in `fusion-documentation`). | + +## Module map + +Each module has its own `AGENTS.md` with entry points and module specific rules. + +| Module | Description | +|---|---| +{{#each modules}}| [{{{artifactId}}}]({{{path}}}/AGENTS.md) | {{{description}}} | +{{/each}} + +How consumers use Fusion (the "triad"): applications depend on `fusion-build-api` + `fusion-processor` +in `provided` scope (compile time only) and `fusion-api` in `compile` scope (runtime). Opt-in modules +(json, http-server, jsonrpc, cli, persistence, ...) are added as needed. + +## Design rules (do not break these) + +- NO REFLECTION in runtime code paths: user code is bound through processor-generated classes, never + `java.lang.reflect` lookups on business classes. This is the core promise of the framework. +- New user-facing annotations go in `fusion-build-api` (package `io.yupiik.fusion.framework.build.api.*`) + and MUST be handled in `fusion-processor` (they are compile-time only, `provided` scope for consumers). +- Package conventions: public API in `...api`, extension points in `...spi`, implementation details in + `...internal` or `...impl` (never referenced from user documentation or other modules' public API). +- There is deliberately NO declarative interceptor support - this is a documented design choice, do not add one. +- Keep modules dependency-light: no new third party runtime dependency without strong justification + (Tomcat in `fusion-http-server` is the notable exception). + +## Coding conventions + +- Every `.java`, `.xml`, `.properties` and `.yaml` file starts with the Apache-2.0 Yupiik header + (checked at `validate` phase, fix with `mvn license:format`). +- Java {{{javaRelease}}} level: records, switch expressions, `var` are idiomatic here; parameters and locals are + typically `final`. +- ASCII-only content in sources and documentation. + +## Testing conventions + +- JUnit 5 (Jupiter), test classes are package-private, named `*Test`, mirroring the main package layout. +- Surefire runs tests IN PARALLEL (classes and methods, `junit.jupiter.execution.parallel.enabled=true`): + tests must not rely on shared mutable global state, fixed network ports or the working directory. +- Use `fusion-testing` helpers: `@FusionSupport`/`@MonoFusionSupport` (container lifecycle), `@Fusion` + (injection in tests), `@FusionCLITest` with `Stdout`/`Stderr` for CLI, `TestClient` for HTTP. + +## Documentation workflow + +- The site is AsciiDoc, sources in `fusion-documentation/src/main/minisite/content/fusion/*.adoc`. +- `fusion-documentation/src/main/minisite/content/_partials/generated/**` is GENERATED from the + annotation metadata - never edit by hand. +- User-facing feature changes need a matching `.adoc` update. +- All `AGENTS.md` files and `CLAUDE.md` are GENERATED by `fusion-build-internal`: edit the templates in + `fusion-build-internal/src/main/resources/agents/` then run `mvn install -pl fusion-build-internal`. diff --git a/fusion-build-internal/src/test/java/io/yupiik/fusion/build/internal/agent/AgentsFileSynchronizerTest.java b/fusion-build-internal/src/test/java/io/yupiik/fusion/build/internal/agent/AgentsFileSynchronizerTest.java new file mode 100644 index 0000000..d28bb61 --- /dev/null +++ b/fusion-build-internal/src/test/java/io/yupiik/fusion/build/internal/agent/AgentsFileSynchronizerTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.yupiik.fusion.build.internal.agent; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AgentsFileSynchronizerTest { + @Test + void synchronize(@TempDir final Path output) throws IOException { + new AgentsFileSynchronizer(repositoryRoot(), output, version(), false).run(); + + final var root = output.resolve("AGENTS.md"); + assertTrue(Files.exists(root), "missing root AGENTS.md"); + final var rootContent = Files.readString(root); + assertTrue(rootContent.contains("GENERATED FILE"), "missing banner"); + assertTrue(rootContent.contains("fusion-json"), "missing module map"); + assertTrue(rootContent.contains("mvn install"), "missing build commands"); + + assertTrue(Files.readString(output.resolve("CLAUDE.md")).contains("@AGENTS.md"), "missing AGENTS.md import"); + assertTrue(Files.exists(output.resolve("fusion-api/AGENTS.md")), "missing module AGENTS.md"); + assertTrue(Files.exists(output.resolve("fusion-httpclient-parent/fusion-httpclient/AGENTS.md")), "missing nested module AGENTS.md"); + assertTrue( + Files.readString(output.resolve("fusion-build-api/AGENTS.md")).contains("`@RootConfiguration`"), + "missing annotation catalog"); + assertTrue( + Files.readString(output.resolve("fusion-jwt/AGENTS.md")).contains("| Name | Env variable |"), + "missing configuration table"); + + // rerunning does not modify anything (idempotent) + final var content = Files.readString(output.resolve("fusion-jwt/AGENTS.md")); + new AgentsFileSynchronizer(repositoryRoot(), output, version(), false).run(); + assertEquals(content, Files.readString(output.resolve("fusion-jwt/AGENTS.md"))); + } + + @Test + void checkModeDetectsStaleFiles(@TempDir final Path output) throws IOException { + final var synchronizer = new AgentsFileSynchronizer(repositoryRoot(), output, version(), false); + synchronizer.run(); + + final var check = new AgentsFileSynchronizer(repositoryRoot(), output, version(), true); + assertDoesNotThrow(check::run); + + Files.writeString(output.resolve("AGENTS.md"), "stale"); + final var error = assertThrows(IllegalStateException.class, check::run); + assertTrue(error.getMessage().contains("AGENTS.md"), error::getMessage); + } + + private Path repositoryRoot() { + return Path.of(System.getProperty("fusion.build-internal.repositoryRoot", "..")) + .toAbsolutePath() + .normalize(); + } + + private String version() { + return System.getProperty("project.version", "unknown"); + } +} diff --git a/fusion-cli/AGENTS.md b/fusion-cli/AGENTS.md new file mode 100644 index 0000000..2ec4bda --- /dev/null +++ b/fusion-cli/AGENTS.md @@ -0,0 +1,60 @@ + +# Fusion :: CLI (`fusion-cli`) + +Command line application support with compile time generated commands. + +Classes annotated with `@Command` (from `fusion-build-api`) become executable CLI commands with their +parameters mapped from a `@RootConfiguration` record. + +## Declaring a command + +```java +@Command(name = "my-command", description = "A super command.") +public class MyCommand implements Runnable { + private final Conf conf; + private final JsonMapper jsonMapper; // any other constructor parameter is an injected bean + + public MyCommand(final Conf conf, final JsonMapper jsonMapper) { + this.conf = conf; + this.jsonMapper = jsonMapper; + } + + @Override + public void run() { + // command implementation + } + + @RootConfiguration("my-command") // options become --my-command-xxx (use prefix "-" for plain --xxx) + public record Conf(String name) {} +} +``` + +- The command must be a `Runnable`; its FIRST constructor parameter must be the configuration record. +- Constructor injections are supported after the configuration parameter: any simple-type bean can be + injected (no lists - wrap a list in a dedicated bean if needed). + +## Launching + +- `io.yupiik.fusion.framework.api.main.CliLauncher` (in `fusion-api`) is the PREFERRED main: unlike plain + `Launcher` it skips the first argument (the command name), so `my-app my-command --my-command-name foo` + maps the remaining args to the command configuration. +- Without the launcher integration, call `io.yupiik.fusion.cli.CliAwaiter` yourself and register an + `Args` instance in the container. + +## Module rules + +- Command parameter binding reuses the configuration subsystem: options are documented/validated like any + `@RootConfiguration`. +- Test commands with `@FusionCLITest` from `fusion-testing` (captures `Stdout`/`Stderr`). + + + +## Working in this module + +- Build it: `mvn install -pl fusion-cli -am` (from the repository root). +- The root [AGENTS.md](/AGENTS.md) holds the global rules: reflectionless design, license headers, parallel JUnit 5 tests, ASCII-only. + diff --git a/fusion-cli/pom.xml b/fusion-cli/pom.xml index 357ad58..617ffe4 100644 --- a/fusion-cli/pom.xml +++ b/fusion-cli/pom.xml @@ -26,6 +26,7 @@ fusion-cli Fusion :: CLI + Command line application support with compile time generated commands. diff --git a/fusion-documentation/AGENTS.md b/fusion-documentation/AGENTS.md new file mode 100644 index 0000000..1fc11c2 --- /dev/null +++ b/fusion-documentation/AGENTS.md @@ -0,0 +1,36 @@ + +# Fusion :: Documentation (`fusion-documentation`) + +AsciiDoc minisite of the project and documentation generators (configuration, OpenRPC converters). + +Two things live here: the AsciiDoc minisite (published to https://yupiik.github.io/fusion/) and the doc +generators run as `yupiik-tools-maven-plugin` pre-actions. + +## Layout and entry points + +- `src/main/minisite/content/fusion/*.adoc`: hand-written documentation pages (one per feature/module). +- `src/main/minisite/content/_partials/generated/`: GENERATED configuration docs - never edit by hand. +- `io.yupiik.fusion.documentation.ConfigurationDocumentationGenerator` (and legacy `DocumentationGenerator`): + renders `META-INF/fusion/configuration/documentation.json` metadata to AsciiDoc. +- `io.yupiik.fusion.documentation.OpenRpcGenerator` / `OpenRPC2Adoc` / `OpenRPC2OpenAPI` / `OpenRPC2Postman`: + JSON-RPC documentation converters. +- Preview the site locally: `mvn compile yupiik-tools:serve-minisite -e` (in this module). + +## Module rules + +- AsciiDoc conventions: tables with inline rows (a full row on one line when possible), `opts=headers` on + tables, inline admonitions instead of blocks. +- Any user-facing feature change in other modules needs its `.adoc` page updated here. +- The default profile regenerates content on every build (`skipRendering=true`): commit the regenerated files. + + + +## Working in this module + +- Build it: `mvn install -pl fusion-documentation -am` (from the repository root). +- The root [AGENTS.md](/AGENTS.md) holds the global rules: reflectionless design, license headers, parallel JUnit 5 tests, ASCII-only. + diff --git a/fusion-documentation/pom.xml b/fusion-documentation/pom.xml index b1c18cb..8032c75 100644 --- a/fusion-documentation/pom.xml +++ b/fusion-documentation/pom.xml @@ -26,9 +26,10 @@ fusion-documentation Fusion :: Documentation + AsciiDoc minisite of the project and documentation generators (configuration, OpenRPC converters). - 1.2.12 + 1.2.15-SNAPSHOT @@ -128,7 +129,8 @@ Fusion framework aims at providing a very light java framework and graal native. Fusion Documentation - © 2025 <strong><a href="https://www.yupiik.com">Yupiik</a></strong>. All Rights Reserved + © 2026 <strong><a href="https://www.yupiik.com">Yupiik</a></strong>. All Rights Reserved + true ${project.basedir}/src/main/minisite/content/_partials @@ -154,6 +156,11 @@ asciidoc-java ${yupiik-tools.version} + + dev.langchain4j + langchain4j-embeddings-all-minilm-l6-v2 + 1.16.1-beta26 + diff --git a/fusion-handlebars/AGENTS.md b/fusion-handlebars/AGENTS.md new file mode 100644 index 0000000..afb08cf --- /dev/null +++ b/fusion-handlebars/AGENTS.md @@ -0,0 +1,31 @@ + +# Fusion :: Handlebars (`fusion-handlebars`) + +Handlebars templating engine implementation without any dependency. + +Small handlebars-compatible template engine (each/if/unless/with, partials, helpers, `@first`/`@last`/`@index` +data variables) used for server-side rendering without any third party dependency. + +## Entry points + +- `io.yupiik.fusion.framework.handlebars.HandlebarsCompiler`: compile a template + (`compile(new CompilationContext(content)).render(data)`); `Settings` carries helpers and partials. +- `io.yupiik.fusion.framework.handlebars.spi.Accessor` / `Template`: data access and rendering SPI. +- `io.yupiik.fusion.framework.handlebars.helper`: built-in helpers. + +## Module rules + +- Double-brace interpolation HTML-escapes, triple-brace renders raw: preserve that handlebars semantic. +- Data access goes through `Accessor` (maps/lists by default): no reflection on user objects. + + + +## Working in this module + +- Build it: `mvn install -pl fusion-handlebars -am` (from the repository root). +- The root [AGENTS.md](/AGENTS.md) holds the global rules: reflectionless design, license headers, parallel JUnit 5 tests, ASCII-only. + diff --git a/fusion-handlebars/pom.xml b/fusion-handlebars/pom.xml index 5209a0d..a8a1fba 100644 --- a/fusion-handlebars/pom.xml +++ b/fusion-handlebars/pom.xml @@ -26,6 +26,7 @@ fusion-handlebars Fusion :: Handlebars + Handlebars templating engine implementation without any dependency. diff --git a/fusion-http-server/AGENTS.md b/fusion-http-server/AGENTS.md new file mode 100644 index 0000000..9159161 --- /dev/null +++ b/fusion-http-server/AGENTS.md @@ -0,0 +1,59 @@ + +# Fusion :: Http Server (`fusion-http-server`) + +HTTP server based on Apache Tomcat with a light request/response API and endpoint matchers. + +Embeds Apache Tomcat behind a small reactive-friendly `Request`/`Response` API; endpoints are declared with +`@HttpMatcher` (compiled to `Endpoint` beans by the processor) or implemented as `Endpoint` beans directly. + +```java +// preferred: declarative matcher on a bean method (Request parameter and CompletionStage are optional) +@HttpMatcher(method = "GET", pathMatching = EXACT, path = "/greet") +public CompletionStage greet(final Request request) { + return completedFuture(Response.of() + .status(200) + .header("content-type", "application/json") + .body("{\"hello\":true}") + .build()); +} + +// alternative: implement Endpoint directly as a bean +@Bean +public class Greeting implements Endpoint { + @Override + public boolean matches(final Request request) { + return "GET".equals(request.method()); + } + + @Override + public CompletionStage handle(final Request request) { + return completedFuture(Response.of().body("{\"hello\":true}").build()); + } +} +``` + +## Entry points + +- `io.yupiik.fusion.http.server.api.WebServer`: server bootstrap/configuration (`WebServer.Configuration` bean). +- `io.yupiik.fusion.http.server.api.Request` / `Response` / `Cookie` / `Body`: HTTP abstraction. +- `io.yupiik.fusion.http.server.spi.Endpoint` / `BaseEndpoint`: endpoint SPI the generated matchers implement. +- `io.yupiik.fusion.http.server.spi.MonitoringEndpoint`: endpoints for the observability server. +- `io.yupiik.fusion.http.server.impl.tomcat`: Tomcat wiring (internal). + +## Module rules + +- Tomcat (`tomcat-*` artifacts) is the ONLY third party runtime dependency family here; keep it that way. +- Response bodies use `java.util.concurrent.Flow` publishers: never block Tomcat threads in helpers. +- Tests run in parallel: always bind test servers to port `0` (random port), never a fixed port. + + + +## Working in this module + +- Build it: `mvn install -pl fusion-http-server -am` (from the repository root). +- The root [AGENTS.md](/AGENTS.md) holds the global rules: reflectionless design, license headers, parallel JUnit 5 tests, ASCII-only. + diff --git a/fusion-http-server/pom.xml b/fusion-http-server/pom.xml index d576a24..20b0de6 100644 --- a/fusion-http-server/pom.xml +++ b/fusion-http-server/pom.xml @@ -26,6 +26,7 @@ fusion-http-server Fusion :: Http Server + HTTP server based on Apache Tomcat with a light request/response API and endpoint matchers. diff --git a/fusion-httpclient-parent/AGENTS.md b/fusion-httpclient-parent/AGENTS.md new file mode 100644 index 0000000..302b397 --- /dev/null +++ b/fusion-httpclient-parent/AGENTS.md @@ -0,0 +1,21 @@ + +# Fusion :: HTTP Client Parent (`fusion-httpclient-parent`) + +Parent of the Fusion HTTP client extensions. + +Aggregator pom only, no code. See the children guides: + +- [fusion-httpclient](fusion-httpclient/AGENTS.md): extended `java.net.http` client. +- [fusion-kubernetes-client](fusion-kubernetes-client/AGENTS.md): lightweight Kubernetes client. + + + +## Working in this module + +- Build it: `mvn install -pl fusion-httpclient-parent -am` (from the repository root). +- The root [AGENTS.md](/AGENTS.md) holds the global rules: reflectionless design, license headers, parallel JUnit 5 tests, ASCII-only. + diff --git a/fusion-httpclient-parent/fusion-httpclient/AGENTS.md b/fusion-httpclient-parent/fusion-httpclient/AGENTS.md new file mode 100644 index 0000000..03e469a --- /dev/null +++ b/fusion-httpclient-parent/fusion-httpclient/AGENTS.md @@ -0,0 +1,29 @@ + +# Fusion :: HTTP Client (`fusion-httpclient`) + +Extended java.net.http client: routing, throttling, retries, logging listeners. + +Decorators around the JDK `java.net.http.HttpClient` - no custom HTTP stack. + +## Entry points + +- `io.yupiik.fusion.httpclient.core.ExtendedHttpClient` + `ExtendedHttpClientConfiguration`: main entry point + (retries, logging, listeners). +- `io.yupiik.fusion.httpclient.core.RoutingHttpClient` / `ThrottledHttpClient` / `DelegatingHttpClient`: composable decorators. +- `listener` package: request/response listeners (used by `fusion-tracing`). + +## Module rules + +- Stay a thin layer over the JDK client: no third party HTTP dependency. + + + +## Working in this module + +- Build it: `mvn install -pl fusion-httpclient-parent/fusion-httpclient -am` (from the repository root). +- The root [AGENTS.md](/AGENTS.md) holds the global rules: reflectionless design, license headers, parallel JUnit 5 tests, ASCII-only. + diff --git a/fusion-httpclient-parent/fusion-httpclient/pom.xml b/fusion-httpclient-parent/fusion-httpclient/pom.xml index 5ff12bb..f6d2768 100644 --- a/fusion-httpclient-parent/fusion-httpclient/pom.xml +++ b/fusion-httpclient-parent/fusion-httpclient/pom.xml @@ -26,6 +26,7 @@ fusion-httpclient Fusion :: HTTP Client + Extended java.net.http client: routing, throttling, retries, logging listeners. diff --git a/fusion-httpclient-parent/fusion-kubernetes-client/AGENTS.md b/fusion-httpclient-parent/fusion-kubernetes-client/AGENTS.md new file mode 100644 index 0000000..09b3b3b --- /dev/null +++ b/fusion-httpclient-parent/fusion-kubernetes-client/AGENTS.md @@ -0,0 +1,30 @@ + +# Fusion :: HTTP Client Parent :: Kubernetes Client (`fusion-kubernetes-client`) + +Lightweight Kubernetes client based on the JDK HTTP client (in-cluster and kubeconfig support). + +An `HttpClient` facade preconfigured for the Kubernetes API server: in-cluster token/CA handling, kubeconfig +support, and websocket helpers. JSON payloads are handled by the caller (typically with `fusion-json`). + +## Entry points + +- `io.yupiik.fusion.kubernetes.client.KubernetesClient`: the client (implements `HttpClient`). +- `io.yupiik.fusion.kubernetes.client.KubernetesClientConfiguration`: token/certificates/API base configuration. +- `io.yupiik.fusion.kubernetes.client.WebSocketBuilderDelegate`: websocket support (exec/watch style usage). + +## Module rules + +- Keep it reflectionless and dependency-light: it must stay usable in GraalVM native operators. +- Token and certificates are refreshed from files: preserve that behavior for in-cluster long-running processes. + + + +## Working in this module + +- Build it: `mvn install -pl fusion-httpclient-parent/fusion-kubernetes-client -am` (from the repository root). +- The root [AGENTS.md](/AGENTS.md) holds the global rules: reflectionless design, license headers, parallel JUnit 5 tests, ASCII-only. + diff --git a/fusion-httpclient-parent/fusion-kubernetes-client/pom.xml b/fusion-httpclient-parent/fusion-kubernetes-client/pom.xml index d5aa54e..1db8a03 100644 --- a/fusion-httpclient-parent/fusion-kubernetes-client/pom.xml +++ b/fusion-httpclient-parent/fusion-kubernetes-client/pom.xml @@ -26,6 +26,7 @@ fusion-kubernetes-client Fusion :: HTTP Client Parent :: Kubernetes Client + Lightweight Kubernetes client based on the JDK HTTP client (in-cluster and kubeconfig support). diff --git a/fusion-httpclient-parent/pom.xml b/fusion-httpclient-parent/pom.xml index f9dac12..fdab36f 100644 --- a/fusion-httpclient-parent/pom.xml +++ b/fusion-httpclient-parent/pom.xml @@ -26,6 +26,7 @@ fusion-httpclient-parent Fusion :: HTTP Client Parent + Parent of the Fusion HTTP client extensions. pom diff --git a/fusion-json/AGENTS.md b/fusion-json/AGENTS.md new file mode 100644 index 0000000..031610b --- /dev/null +++ b/fusion-json/AGENTS.md @@ -0,0 +1,49 @@ + +# Fusion :: JSON (`fusion-json`) + +Reflectionless JSON mapping with compile time generated codecs, JSON-Patch/Pointer/diff utilities. + +Records annotated with `@JsonModel` get a codec generated at compile time; this module provides the runtime +mapper executing those codecs plus a streaming parser. + +```java +@JsonModel +public record MyModel( + @JsonProperty("boolean") boolean aBool, // renames the attribute in the JSON payload + String simplest, + LocalDate date, + AnotherModel nested, + List list, + @JsonOthers Map extensions) { // catch-all for unknown attributes +} + +// JsonMapper is a bean once fusion-json is on the classpath, inject it +final MyModel model = jsonMapper.fromString(MyModel.class, "{\"simplest\":\"value\"}"); +final String json = jsonMapper.toString(model); +``` + +## Entry points + +- `io.yupiik.fusion.json.JsonMapper`: main user API (a bean once `FusionJsonModule` is on the classpath). +- `io.yupiik.fusion.json.serialization.JsonCodec`: SPI implemented by generated codecs. +- `io.yupiik.fusion.json.spi.Parser`: streaming parser abstraction. +- `io.yupiik.fusion.json.pretty.PrettyJsonMapper`: formatting decorator. +- `io.yupiik.fusion.json.patch`/`pointer`/`diff`: JSON-Patch, JSON-Pointer and diff utilities. +- `io.yupiik.fusion.json.internal.JsonMapperImpl`: implementation (internal, do not expose in signatures). + +## Module rules + +- Reflectionless: no reflection-based (de)serialization fallback, unsupported types must fail clearly. +- Codec generation lives in `fusion-processor` (`internal/json`), only the runtime belongs here. + + + +## Working in this module + +- Build it: `mvn install -pl fusion-json -am` (from the repository root). +- The root [AGENTS.md](/AGENTS.md) holds the global rules: reflectionless design, license headers, parallel JUnit 5 tests, ASCII-only. + diff --git a/fusion-json/pom.xml b/fusion-json/pom.xml index 5fee5f2..eec1785 100644 --- a/fusion-json/pom.xml +++ b/fusion-json/pom.xml @@ -26,6 +26,7 @@ fusion-json Fusion :: JSON + Reflectionless JSON mapping with compile time generated codecs, JSON-Patch/Pointer/diff utilities. diff --git a/fusion-jsonrpc/AGENTS.md b/fusion-jsonrpc/AGENTS.md new file mode 100644 index 0000000..1a3bcbb --- /dev/null +++ b/fusion-jsonrpc/AGENTS.md @@ -0,0 +1,47 @@ + +# Fusion :: JSON-RPC (`fusion-jsonrpc`) + +JSON-RPC 2.0 server on top of the Fusion HTTP server with OpenRPC support. + +Methods annotated with `@JsonRpc` (from `fusion-build-api`) become JSON-RPC 2.0 methods served over the +Fusion HTTP server (single endpoint, default `/jsonrpc`). + +```java +public class Endpoints { // a bean, methods can be sync or return a CompletionStage + @JsonRpc("copy") + public MyResult result(final MyInput input) { // parameters/results are @JsonModel records + return new MyResult(input.name()); + } + + @JsonModel + public record MyInput(String name) {} + + @JsonModel + public record MyResult(String name) {} +} +``` + +## Entry points + +- `io.yupiik.fusion.jsonrpc.JsonRpcEndpoint`: the HTTP endpoint dispatching requests. +- `io.yupiik.fusion.jsonrpc.JsonRpcHandler` / `JsonRpcRegistry`: method resolution and execution. +- `io.yupiik.fusion.jsonrpc.JsonRpcException` / `Response`: error and response modeling. +- `io.yupiik.fusion.jsonrpc.bean.OpenRPCEndpoint` / `OpenRPCHttpEndpoint`: openrpc.json exposure of the registered methods. +- `io.yupiik.fusion.jsonrpc.event.BeforeRequest`: hook event fired before method execution (validation/auth). + +## Module rules + +- Protocol behavior (error codes, batch handling) follows JSON-RPC 2.0: check the spec before changing responses. +- The OpenRPC document is generated from the processor metadata: keep it consistent when touching method binding. + + + +## Working in this module + +- Build it: `mvn install -pl fusion-jsonrpc -am` (from the repository root). +- The root [AGENTS.md](/AGENTS.md) holds the global rules: reflectionless design, license headers, parallel JUnit 5 tests, ASCII-only. + diff --git a/fusion-jsonrpc/pom.xml b/fusion-jsonrpc/pom.xml index bf75a77..c81a686 100644 --- a/fusion-jsonrpc/pom.xml +++ b/fusion-jsonrpc/pom.xml @@ -26,6 +26,7 @@ fusion-jsonrpc Fusion :: JSON-RPC + JSON-RPC 2.0 server on top of the Fusion HTTP server with OpenRPC support. diff --git a/fusion-jwt/AGENTS.md b/fusion-jwt/AGENTS.md new file mode 100644 index 0000000..2fec5e9 --- /dev/null +++ b/fusion-jwt/AGENTS.md @@ -0,0 +1,62 @@ + +# Fusion :: JWT (`fusion-jwt`) + +JWT signing and validation based on JDK cryptography. + +JWT creation (signer) and validation (validator) on top of JDK crypto, configured through +`@RootConfiguration` records - no third party JOSE dependency. + +## Entry points + +- `io.yupiik.fusion.jwt.JwtSignerFactory` + `JwtSignerConfiguration`: token creation. +- `io.yupiik.fusion.jwt.JwtValidatorFactory` + `JwtValidatorConfiguration`: token validation, returns `Jwt`. +- `io.yupiik.fusion.jwt.PublicKeyLoader` / `PrivateKeyLoader`: PEM key loading. +- `io.yupiik.fusion.jwt.bean`: default beans wiring the factories from the configuration. + +## Module rules + +- Security sensitive: algorithm/claim validation changes need tests for the negative cases + (expired, wrong issuer, wrong signature, `none` algorithm rejection). + +## Configuration properties + +Generated from the annotation processor metadata (`META-INF/fusion/configuration/documentation.json`): + +| Name | Env variable | Description | Default | Required | +|---|---|---|---|---| +| `jwt-signer.algorithm` | `JWT_SIGNER_ALGORITHM` | Default JWT `alg` value if no `keys` is set (mainly useful for Hmac case). | `"RS256"` | no | +| `jwt-signer.expRequired` | `JWT_SIGNER_EXPREQUIRED` | Is `exp` (expiry) required. | `true` | no | +| `jwt-signer.expValidity` | `JWT_SIGNER_EXPVALIDITY` | Is `exp` is required the validity used in milliseconds. | `0` | no | +| `jwt-signer.iatRequired` | `JWT_SIGNER_IATREQUIRED` | Is `iat` (issued at) required. | `false` | no | +| `jwt-signer.issuer` | `JWT_SIGNER_ISSUER` | JWT issuer. | - | yes | +| `jwt-signer.key` | `JWT_SIGNER_KEY` | Private key. | - | yes | +| `jwt-signer.kid` | `JWT_SIGNER_KID` | KID header kid. | `"k001"` | no | +| `jwt-signer.nbfRequired` | `JWT_SIGNER_NBFREQUIRED` | Is `nbf` (not before) required. | `false` | no | +| `jwt.algo` | `JWT_ALGO` | Default JWT `alg` value if no `keys` is set (mainly useful for Hmac case). | `"RS256"` | no | +| `jwt.expRequired` | `JWT_EXPREQUIRED` | Is `exp` (expiry) validation required of can it be skipped if claim is missing. | `true` | no | +| `jwt.iatRequired` | `JWT_IATREQUIRED` | Is `iat` (issued at) validation required of can it be skipped if claim is missing. | `false` | no | +| `jwt.issuer` | `JWT_ISSUER` | JWT issuer, validation is ignored if null. | - | no | +| `jwt.jtiRequired` | `JWT_JTIREQUIRED` | Is `jti` (JWT ID) validation required of can it be skipped if claim is missing. | `true` | no | +| `jwt.key` | `JWT_KEY` | Default public key to use to validate the incoming JWT if no `keys` is set else `kid` is matched against the `keys` set (mainly useful for Hmac case which can't be in `jwk_uri`). | - | yes | +| `jwt.keys.$index.alg` | `JWT_KEYS_INDEX_ALG` | | - | no | +| `jwt.keys.$index.crv` | `JWT_KEYS_INDEX_CRV` | | - | no | +| `jwt.keys.$index.e` | `JWT_KEYS_INDEX_E` | | - | no | +| `jwt.keys.$index.kid` | `JWT_KEYS_INDEX_KID` | | - | no | +| `jwt.keys.$index.kty` | `JWT_KEYS_INDEX_KTY` | | - | no | +| `jwt.keys.$index.n` | `JWT_KEYS_INDEX_N` | | - | no | +| `jwt.keys.$index.use` | `JWT_KEYS_INDEX_USE` | | - | no | +| `jwt.keys.$index.x` | `JWT_KEYS_INDEX_X` | | - | no | +| `jwt.keys.$index.x5c` | `JWT_KEYS_INDEX_X5C` | | - | no | +| `jwt.keys.$index.y` | `JWT_KEYS_INDEX_Y` | | - | no | +| `jwt.nbfRequired` | `JWT_NBFREQUIRED` | Is `nbf` (not before) validation required of can it be skipped if claim is missing. | `false` | no | +| `jwt.tolerance` | `JWT_TOLERANCE` | Tolerance for date validation (in seconds). | `30` | no | + +## Working in this module + +- Build it: `mvn install -pl fusion-jwt -am` (from the repository root). +- The root [AGENTS.md](/AGENTS.md) holds the global rules: reflectionless design, license headers, parallel JUnit 5 tests, ASCII-only. + diff --git a/fusion-jwt/pom.xml b/fusion-jwt/pom.xml index d8b1c6b..8a8d922 100644 --- a/fusion-jwt/pom.xml +++ b/fusion-jwt/pom.xml @@ -26,6 +26,7 @@ fusion-jwt Fusion :: JWT + JWT signing and validation based on JDK cryptography. diff --git a/fusion-kubernetes-operator-base/AGENTS.md b/fusion-kubernetes-operator-base/AGENTS.md new file mode 100644 index 0000000..29d3d8b --- /dev/null +++ b/fusion-kubernetes-operator-base/AGENTS.md @@ -0,0 +1,45 @@ + +# Fusion :: K8s Operator Base (`fusion-kubernetes-operator-base`) + +Base to build Kubernetes operators with Fusion (watch/reconcile runtime, CRD support). + +Base to write Kubernetes operators with Fusion: watch/reconcile loop, CRD-typed events and awaiter, built on +`fusion-kubernetes-client`. Note the package root is `io.yupiik.kubernetes.operator.base` (not `io.yupiik.fusion`). + +## Entry points + +- `io.yupiik.kubernetes.operator.base.spi.Operator` / `BulkingOperator`: the SPI an operator implements. +- `io.yupiik.kubernetes.operator.base.impl.OperatorRuntime` / `OperatorsLifecycle`: watch/dispatch runtime. +- `io.yupiik.kubernetes.operator.base.impl.OperatorConfiguration`: runtime configuration. +- `@CustomResourceDefinition` (from `fusion-build-api`, `kubernetes/crd` package): generates the CRD JSON model. + +## Module rules + +- Must stay GraalVM native friendly (operators ship as small native images): no reflection, no dynamic loading. +- Reconciliation is event-driven and potentially concurrent: keep operator state handling thread-safe. + +## Configuration properties + +Generated from the annotation processor metadata (`META-INF/fusion/configuration/documentation.json`): + +| Name | Env variable | Description | Default | Required | +|---|---|---|---|---| +| `operator.await` | `OPERATOR_AWAIT` | Should operator await process termination, keep it `true` until you embed it. | `true` | no | +| `operator.event-thread-count` | `OPERATOR_EVENT_THREAD_COUNT` | How many threads are handling events, take care that more than one require a specific concurrency handling. | `1` | no | +| `operator.kubernetes.certificates` | `OPERATOR_KUBERNETES_CERTIFICATES` | Kubernetes certificate to connect to its API. | `"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"` | no | +| `operator.kubernetes.master` | `OPERATOR_KUBERNETES_MASTER` | The kubernetes API base URL. | `java.util.Optional.ofNullable(System.getenv("KUBERNETES_SERVICE_HOST")).map(host -> "https://" + host + ':' + java.util.Optional.ofNullable(System.getenv("KUBERNETES_SERVICE_PORT")).orElse("443")).orElse("https://kubernetes.default.svc")` | no | +| `operator.kubernetes.tls-skip` | `OPERATOR_KUBERNETES_TLS_SKIP` | Should TLS validations be skipped. | `false` | no | +| `operator.kubernetes.token` | `OPERATOR_KUBERNETES_TOKEN` | Kubernetes token (service account). | `"/var/run/secrets/kubernetes.io/serviceaccount/token"` | no | +| `operator.probe-port` | `OPERATOR_PROBE_PORT` | Server for healthchecks, set to a negative value to disable (when embedded for ex). | `8081` | no | +| `operator.storage` | `OPERATOR_STORAGE` | Operator can store locally (on the filesystem) the latest resource version it saw. This must be a directory, ignored if `null`. | - | no | +| `operator.use-bookmarks` | `OPERATOR_USE_BOOKMARKS` | If `true`, `BOOKMARK` events are enabled. | `true` | no | + +## Working in this module + +- Build it: `mvn install -pl fusion-kubernetes-operator-base -am` (from the repository root). +- The root [AGENTS.md](/AGENTS.md) holds the global rules: reflectionless design, license headers, parallel JUnit 5 tests, ASCII-only. + diff --git a/fusion-kubernetes-operator-base/pom.xml b/fusion-kubernetes-operator-base/pom.xml index 854b493..2b43402 100644 --- a/fusion-kubernetes-operator-base/pom.xml +++ b/fusion-kubernetes-operator-base/pom.xml @@ -26,6 +26,7 @@ fusion-kubernetes-operator-base Fusion :: K8s Operator Base + Base to build Kubernetes operators with Fusion (watch/reconcile runtime, CRD support). diff --git a/fusion-observability/AGENTS.md b/fusion-observability/AGENTS.md new file mode 100644 index 0000000..e21b5ba --- /dev/null +++ b/fusion-observability/AGENTS.md @@ -0,0 +1,30 @@ + +# Fusion :: Observability (`fusion-observability`) + +Observability HTTP server exposing health checks and OpenMetrics metrics. + +Dedicated monitoring HTTP server (separate port from the business server) exposing health checks and +OpenMetrics/Prometheus-format metrics. + +## Entry points + +- `io.yupiik.fusion.observability.http.ObservabilityServer`: the monitoring server. +- `io.yupiik.fusion.observability.health.HealthCheck` / `HealthRegistry`: health SPI and registry. +- `io.yupiik.fusion.observability.metrics.MetricsRegistry` / `OpenMetricsFormatter`: metrics SPI and rendering. +- Endpoints implement `MonitoringEndpoint` from `fusion-http-server`. + +## Module rules + +- Health endpoints are used by orchestrators (Kubernetes probes): keep them non-blocking and cheap. + + + +## Working in this module + +- Build it: `mvn install -pl fusion-observability -am` (from the repository root). +- The root [AGENTS.md](/AGENTS.md) holds the global rules: reflectionless design, license headers, parallel JUnit 5 tests, ASCII-only. + diff --git a/fusion-observability/pom.xml b/fusion-observability/pom.xml index dce2b33..3c7abc6 100644 --- a/fusion-observability/pom.xml +++ b/fusion-observability/pom.xml @@ -26,6 +26,7 @@ fusion-observability Fusion :: Observability + Observability HTTP server exposing health checks and OpenMetrics metrics. diff --git a/fusion-persistence/AGENTS.md b/fusion-persistence/AGENTS.md new file mode 100644 index 0000000..f2a9731 --- /dev/null +++ b/fusion-persistence/AGENTS.md @@ -0,0 +1,55 @@ + +# Fusion :: Persistence (`fusion-persistence`) + +Lightweight reflectionless JDBC persistence with compile time generated entity models. + +Records annotated with `@Table`/`@Id`/`@Column` (from `fusion-build-api`) get their entity model generated at +compile time; this module executes SQL over plain JDBC with those models - no lazy loading, no dirty checking, +just typed statements and mapping. + +```java +@Table("CUSTOMER") +public record CustomerEntity( + @Id(autoIncremented = true) Integer id, + @Column String firstname, + @Column(name = "LAST_NAME") String lastname) { + + @OnInsert // lifecycle callbacks return a new instance (records are immutable) + public CustomerEntity onInsert() { + return this; + } +} + +// Database is created from a DataSource (DatabaseFactory) or injected as a bean +final CustomerEntity saved = database.insert(new CustomerEntity(null, "John", "Doe")); +final List all = database.query( + CustomerEntity.class, "select * from CUSTOMER where LAST_NAME = ?", b -> b.bind("Doe")); +``` + +## Entry points + +- `io.yupiik.fusion.persistence.api.Database` / `ContextLessDatabase` / `BaseDatabase`: main query APIs + (with or without a thread-bound connection). +- `io.yupiik.fusion.persistence.api.DatabaseFactory`: creation from a `DataSource`. +- `io.yupiik.fusion.persistence.api.TransactionManager`: transaction handling. +- `io.yupiik.fusion.persistence.api.Entity` / `StatementBinder` / `ResultSetWrapper` / `SqlBuilder`: generated-model SPI and helpers. +- `io.yupiik.fusion.persistence.spi.DatabaseTranslation`: per-database SQL dialect SPI (see `impl.translation` for the provided ones). +- `io.yupiik.fusion.persistence.impl.datasource`: simple and Tomcat-pool backed datasources (internal). + +## Module rules + +- Entity model generation lives in `fusion-processor` (`internal/persistence`); only runtime execution belongs here. +- New database dialects go through `DatabaseTranslation` implementations, not conditionals in the core. +- Tests use H2 and run in parallel: use per-test database names. + + + +## Working in this module + +- Build it: `mvn install -pl fusion-persistence -am` (from the repository root). +- The root [AGENTS.md](/AGENTS.md) holds the global rules: reflectionless design, license headers, parallel JUnit 5 tests, ASCII-only. + diff --git a/fusion-persistence/pom.xml b/fusion-persistence/pom.xml index 045528e..9ee2596 100644 --- a/fusion-persistence/pom.xml +++ b/fusion-persistence/pom.xml @@ -26,6 +26,7 @@ fusion-persistence Fusion :: Persistence + Lightweight reflectionless JDBC persistence with compile time generated entity models. diff --git a/fusion-processor/AGENTS.md b/fusion-processor/AGENTS.md new file mode 100644 index 0000000..6028861 --- /dev/null +++ b/fusion-processor/AGENTS.md @@ -0,0 +1,39 @@ + +# Fusion :: Processor (`fusion-processor`) + +Fusion annotation processor generating beans, JSON codecs, configuration binding and persistence models at build time. + +The heart of the framework: a `javax.annotation.processing.Processor` which turns the `fusion-build-api` +annotations into reflectionless runtime code (beans, DI wiring, JSON codecs, configuration binding, +persistence entities, doc metadata). + +## Entry points + +- `io.yupiik.fusion.framework.processor.FusionProcessor`: the processor, registered through + `src/main/resources/META-INF/services/javax.annotation.processing.Processor`. +- `internal/generator`: code generators (beans, listeners, subclasses/proxies). +- `internal/json`: JSON codec generation for `@JsonModel` records. +- `internal/persistence`: entity/table model generation. +- `internal/meta`: metadata emission, including `META-INF/fusion/configuration/documentation.json` + (consumed by `fusion-documentation` and `fusion-build-internal`). + +## Module rules + +- ANY new annotation added to `fusion-build-api` must be handled here, plus documentation and tests. +- Generated code must stay reflectionless and readable: it is the runtime, users debug through it. +- Tests are compilation-based: see `FusionProcessorTest` (compiles fixture sources from + `src/test/resources` and asserts on the generated output/behavior). Add a fixture per new feature. +- Supported annotations are processed per compilation unit: mind incremental compilation semantics + when changing rounds handling. + + + +## Working in this module + +- Build it: `mvn install -pl fusion-processor -am` (from the repository root). +- The root [AGENTS.md](/AGENTS.md) holds the global rules: reflectionless design, license headers, parallel JUnit 5 tests, ASCII-only. + diff --git a/fusion-processor/pom.xml b/fusion-processor/pom.xml index f065ae0..b32834c 100644 --- a/fusion-processor/pom.xml +++ b/fusion-processor/pom.xml @@ -26,6 +26,7 @@ fusion-processor Fusion :: Processor + Fusion annotation processor generating beans, JSON codecs, configuration binding and persistence models at build time. +# Fusion :: Testing (`fusion-testing`) + +JUnit 5 integration to test Fusion applications (container lifecycle, injection, CLI launcher). + +```java +@FusionSupport // starts/stops a container around the test class (@MonoFusionSupport: one per JVM, faster) +class MyTest { + @Test + void run(@Fusion final MyService service) { // beans are injected as test method parameters + // assertions on service + } +} +``` + +## Entry points + +- `io.yupiik.fusion.testing.FusionSupport`: JUnit 5 extension starting/stopping a container per test class. +- `io.yupiik.fusion.testing.MonoFusionSupport`: single shared container for the whole surefire execution (faster). +- `io.yupiik.fusion.testing.Fusion`: parameter injection of beans in test methods. +- `io.yupiik.fusion.testing.launcher.FusionCLITest` + `Stdout`/`Stderr`: run a CLI command and capture output. +- `io.yupiik.fusion.testing.task.Task` / `TaskResult`: run logic around the container lifecycle. +- `io.yupiik.fusion.testing.assertion.Asserts` / `JsonAsserts`: assertion helpers. +- `io.yupiik.fusion.testing.module.TestingModule`: programmatic bean replacement/addition for tests. + +## Module rules + +- This module is consumers' test-facing API: keep it JUnit 5 only, no other test framework dependency. +- Everything must be safe under parallel execution (it is the default in this build and in consumer setups). + + + +## Working in this module + +- Build it: `mvn install -pl fusion-testing -am` (from the repository root). +- The root [AGENTS.md](/AGENTS.md) holds the global rules: reflectionless design, license headers, parallel JUnit 5 tests, ASCII-only. + diff --git a/fusion-testing/pom.xml b/fusion-testing/pom.xml index 15920e3..d20a982 100644 --- a/fusion-testing/pom.xml +++ b/fusion-testing/pom.xml @@ -26,6 +26,7 @@ fusion-testing Fusion :: Testing + JUnit 5 integration to test Fusion applications (container lifecycle, injection, CLI launcher). diff --git a/fusion-tracing/AGENTS.md b/fusion-tracing/AGENTS.md new file mode 100644 index 0000000..353e782 --- /dev/null +++ b/fusion-tracing/AGENTS.md @@ -0,0 +1,31 @@ + +# Fusion :: Tracing (`fusion-tracing`) + +Zipkin oriented tracing for the Fusion HTTP server and clients. + +Zipkin-format span collection and flushing for the Fusion HTTP server (Tomcat valve) and the extended HTTP +client (listener), with OpenTelemetry-compatible export. + +## Entry points + +- `io.yupiik.fusion.tracing.server.TracingValve` + `ServerTracingConfiguration`: server side span creation. +- `io.yupiik.fusion.tracing.client.TracingListener` + `ClientTracingConfiguration`: client side propagation. +- `io.yupiik.fusion.tracing.collector.AccumulatingSpanCollector`: buffering collector. +- `io.yupiik.fusion.tracing.zipkin.ZipkinFlusher`: HTTP flusher of the accumulated spans. +- `io.yupiik.fusion.tracing.span.Span`: span model. + +## Module rules + +- Span flushing is asynchronous and batched: mind thread-safety, the collector is shared. + + + +## Working in this module + +- Build it: `mvn install -pl fusion-tracing -am` (from the repository root). +- The root [AGENTS.md](/AGENTS.md) holds the global rules: reflectionless design, license headers, parallel JUnit 5 tests, ASCII-only. + diff --git a/fusion-tracing/pom.xml b/fusion-tracing/pom.xml index 46698ea..a8d0933 100644 --- a/fusion-tracing/pom.xml +++ b/fusion-tracing/pom.xml @@ -26,6 +26,7 @@ fusion-tracing Fusion :: Tracing + Zipkin oriented tracing for the Fusion HTTP server and clients. diff --git a/pom.xml b/pom.xml index 0dba8f7..0cd808a 100644 --- a/pom.xml +++ b/pom.xml @@ -55,6 +55,7 @@ fusion-handlebars fusion-jwt fusion-kubernetes-operator-base + fusion-build-internal From 765d31b52714ed9c5dbebfc0c122581b771a6f62 Mon Sep 17 00:00:00 2001 From: Romain Manni-Bucau Date: Fri, 17 Jul 2026 18:56:02 +0200 Subject: [PATCH 2/3] [dep] upgrade tools plugin to 1.2.15 --- fusion-documentation/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fusion-documentation/pom.xml b/fusion-documentation/pom.xml index 8032c75..0982d25 100644 --- a/fusion-documentation/pom.xml +++ b/fusion-documentation/pom.xml @@ -29,7 +29,7 @@ AsciiDoc minisite of the project and documentation generators (configuration, OpenRPC converters). - 1.2.15-SNAPSHOT + 1.2.15 From 5d5db5697cc3db0003d763a52bcb6aef47512965 Mon Sep 17 00:00:00 2001 From: Romain Manni-Bucau Date: Mon, 20 Jul 2026 07:27:28 +0200 Subject: [PATCH 3/3] [agents.md] fix junit ref (version) --- AGENTS.md | 2 +- fusion-build-internal/src/main/resources/agents/root.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 16a14d4..35ad475 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -78,7 +78,7 @@ in `provided` scope (compile time only) and `fusion-api` in `compile` scope (run ## Testing conventions -- JUnit 5 (Jupiter), test classes are package-private, named `*Test`, mirroring the main package layout. +- JUnit Jupiter, test classes are package-private, named `*Test`, mirroring the main package layout. - Surefire runs tests IN PARALLEL (classes and methods, `junit.jupiter.execution.parallel.enabled=true`): tests must not rely on shared mutable global state, fixed network ports or the working directory. - Use `fusion-testing` helpers: `@FusionSupport`/`@MonoFusionSupport` (container lifecycle), `@Fusion` diff --git a/fusion-build-internal/src/main/resources/agents/root.md b/fusion-build-internal/src/main/resources/agents/root.md index 437e301..e3e8306 100644 --- a/fusion-build-internal/src/main/resources/agents/root.md +++ b/fusion-build-internal/src/main/resources/agents/root.md @@ -56,7 +56,7 @@ in `provided` scope (compile time only) and `fusion-api` in `compile` scope (run ## Testing conventions -- JUnit 5 (Jupiter), test classes are package-private, named `*Test`, mirroring the main package layout. +- JUnit Jupiter, test classes are package-private, named `*Test`, mirroring the main package layout. - Surefire runs tests IN PARALLEL (classes and methods, `junit.jupiter.execution.parallel.enabled=true`): tests must not rely on shared mutable global state, fixed network ports or the working directory. - Use `fusion-testing` helpers: `@FusionSupport`/`@MonoFusionSupport` (container lifecycle), `@Fusion`