From 33d31c1a4c88e68caa6bff52d18f165b2041f67a Mon Sep 17 00:00:00 2001 From: Igor Kvasnicka Date: Thu, 16 Jul 2026 17:25:08 +0200 Subject: [PATCH 1/2] support for generating LMM.md for AI models --- LLM.md | 439 ++++++++++++++++++++++++++++++++++++++ docs/regenerate-llm-md.sh | 112 ++++++++++ 2 files changed, 551 insertions(+) create mode 100644 LLM.md create mode 100755 docs/regenerate-llm-md.sh diff --git a/LLM.md b/LLM.md new file mode 100644 index 0000000..1207bf0 --- /dev/null +++ b/LLM.md @@ -0,0 +1,439 @@ +# LLM.md — JAM (Java Annotation Mapper) + +Orientation document for LLMs / coding assistants. Every fact below was derived from this +repository's sources, poms, git history, and from the actual `*JAMImpl` code generated by +building `jam-tests` (JDK 11 profile). Snippets are copied from real files and attributed; +anything illustrative is labeled as such. + +## What this repository is + +JAM ("Java Annotation Mapper") is a **compile-time object-to-object mapper**: a Java +annotation processor that generates plain-Java mapping classes while you compile, so mapping +at runtime uses no reflection and no dependencies beyond the tiny `jam-common` jar. It is +inspired by SELMA (per `README.md`) and solves the same problem space as MapStruct. + +| | | +|---|---| +| Maven groupId | `sk.annotation.library.jam` | +| Modules | `jam-common` (runtime annotations + utils), `jam-processor` (the annotation processor), `jam-tests` (test/example modules `jam-tests-minimum` and `jam-tests-with-lombok`; only built with `-Prun-jam-tests` or directly from `jam-tests/`) | +| Current dev version | `0.9.22-SNAPSHOT` (`jam.version` in root `pom.xml`) | +| Latest release | `0.9.20` (commit `f1f4574` "release 0.9.20"; this clone has **no git tags**) | +| Java support | Maven profiles: `jdk11` (default, source/target 11) and `jdk8` (source/target 8, version suffix `-jdk8`, e.g. `0.9.20-jdk8`) | +| License | Apache License 2.0 | +| Upstream | (pom ``/``; this clone's `origin` is the fork `IgorKvasn/java-annotation-mapper`) | + +> **Stale README warning:** `README.md` says "last version … is 0.9.18". The poms and git +> history show 0.9.20 released and 0.9.22-SNAPSHOT in development — trust the poms, not the +> README. + +## When to use it + +Use JAM when you want: + +- **Compile-time generation** — mapping problems surface as compiler warnings/errors, not + runtime surprises; the generated code is readable Java you can step through. +- **No runtime reflection** — generated code calls getters/setters directly. Runtime + dependency is only `jam-common` (annotations + a few small util classes). +- **Cyclic object graphs** — generated methods use a per-thread instance cache so recursive / + self-referencing structures map without infinite loops (see ex7). +- **In-place updates** — `@Return` marks a destination parameter to be updated instead of + creating a new instance (see ex8, ex21). +- **Multi-source aggregation** — one target filled from several source parameters (ex10). +- **Spring / CDI integration** — `@EnableSpring` / `@EnableCDI` make the generated class a + `@Component` / `@Named` bean. +- **Lombok compatibility** — Lombok-generated accessors are recognized (whole + `jam-tests-with-lombok` module; fix commit `9a12e53` "Lombok Setters/Getters are not + recognized #35"). + +Comparable tools: **MapStruct**, **SELMA** (JAM's declared inspiration). If you need a large +ecosystem and documentation, MapStruct is the mainstream choice; JAM is small and +self-contained. + +## Installation + +From `README.md` (versions corrected against the poms/git history): + +```xml + + + + sk.annotation.library.jam + jam-common + 0.9.20 + + + sk.annotation.library.jam + jam-processor + provided + 0.9.20 + + +``` + +`jam-common` is the compile/runtime dependency; `jam-processor` is only needed at compile +time (`provided`). Note that `jam-tests/pom.xml` sets `full` on +`maven-compiler-plugin` in the `jdk11` profile — recent JDKs warn about (and eventually +stop) running annotation processors implicitly, so you may need the same setting. + +Building this repo itself: + +```bash +mvn -Pjdk11 install # builds jam-common + jam-processor (jam-tests excluded by default) +cd jam-tests && mvn -Pjdk11 install # builds & runs the example tests +# or from the root: mvn -Pjdk11,run-jam-tests install +``` + +## Quick start + +Mapper definition — copied from +`jam-tests/jam-tests-minimum/src/main/java/sk/annotation/library/jam/example/ex1/SimpleMapper.java`: + +```java +@Mapper +public interface SimpleMapper { + UserOutput toOutput(UserInput userInput); + UserInput toInput(UserOutput userOutput); +} +``` + +Usage — condensed from +`jam-tests/jam-tests-minimum/src/test/java/sk/annotation/library/jam/example/ex1/SimpleMapperTest.java` +(illustrative shape, same API): + +```java +SimpleMapper mapper = MapperUtil.getMapper(SimpleMapper.class); +UserOutput out = mapper.toOutput(input); +``` + +What happens: + +- The processor (`sk.annotation.library.jam.processor.AnnotationJamMapperProcessor`) + generates `JAMImpl` in the same package (suffix from + `MapperUtil.constPostFixClassName = "JAMImpl"`, + `jam-common/.../utils/MapperUtil.java`). An interface gets `class XJAMImpl implements X`, + an abstract class gets `class XJAMImpl extends X`. +- `MapperUtil.getMapper(Class)` loads `JAMImpl` reflectively once (with a + `ServiceLoader` fallback) and caches the instance in a static map. +- `MapperUtil.getMapper(Class, Object fromOtherMapper)` exists to break constructor cycles + when mappers reference each other (used in ex18 with `this`). +- Lombok `@Getter`/`@Setter`/`@Data` accessors are treated like handwritten ones + (`jam-tests-with-lombok`). + +## Public API (`jam-common`) + +All annotations live in `sk.annotation.library.jam.annotations` +(`jam-common/src/main/java/...`); all have `@Retention(CLASS)` except `@JamVisibility`, +which declares no retention (so it defaults to `RUNTIME`) and no target. + +| Annotation | @Target | Attributes (type, default) | +|---|---|---| +| `@Mapper` | TYPE | `defaultErrorConfig` (ConfigErrorReporting, `WARNINGS_ONLY`) | +| `@MapperConfig` | TYPE, PACKAGE, METHOD | `fieldMapping` (FieldMapping[], `{}`), `fieldIgnore` (FieldIgnore[], `{}`), `config` (ConfigGenerator[], `{}`), `immutable` (Class[], `{}`), `withCustom` (Class[], `{}`), `applyWhen` (ApplyFieldStrategy[], `{}`) | +| `@FieldMapping` | `{}` (only usable inside `@MapperConfig`) | `sPackages` (String[], `{}`), `sTypes` (Class[], `{}`), `s` (String[], required), `dPackages` (String[], `{}`), `dTypes` (Class[], `{}`), `d` (String[], required), `ignoreDirectionS2D` (boolean, `false`), `ignoreDirectionD2S` (boolean, `false`), `methodNameS2D` (String, `""`), `methodNameD2S` (String, `""`), `applyWhenS2D` (ApplyFieldStrategy[], `{}`), `applyWhenD2S` (ApplyFieldStrategy[], `{}`) | +| `@FieldIgnore` | `{}` (only inside `@MapperConfig`) | `types` (Class[], `{}`), `packages` (String[], `{}`), `value` (String[], required), `ignored` (IgnoreType, `IGNORE_ALL`) | +| `@ConfigGenerator` | `{}` (only inside `@MapperConfig`) | `fieldPackages` (Class[], `{}`), `fieldTypes` (Class[], `{}`), `field` (String[], `""`), `missingAsSource` (ConfigErrorReporting, `WARNINGS_ONLY`), `missingAsDestination` (ConfigErrorReporting, `WARNINGS_ONLY`) | +| `@Return` | PARAMETER | `value` (boolean, `true`) | +| `@Context` | PARAMETER | `value` (String, `"ctx"`); constants `jamConfif = "#JAM#conf"` (sic — typo in source), `jamContext = "#JAM#ctx"` | +| `@EnableSpring` | TYPE | `beanName` (String, `""`), `scope` (IocScope, `DEFAULT`) | +| `@EnableCDI` | TYPE | `beanName` (String, `""`), `scope` (IocScope, `DEFAULT`) | +| `@DisableMapperFeature` | TYPE | `value` (MapperFeature[], `{}`) | +| `@JamVisibility` | (none declared; default retention) | `value` (MapperVisibility, required) | +| `@JamGenerated` | (none declared) | `value` (String, required), `date` (String, `""`) — stamped on generated classes | + +Enums in `sk.annotation.library.jam.annotations.enums` (constants in declaration order): + +| Enum | Constants | +|---|---| +| `ConfigErrorReporting` | `NO_REPORT`, `WARNINGS_ONLY`, `COMPILATION_ERROR` | +| `ApplyFieldStrategy` | `ALWAYS`, `OLDVALUE_IS_NULL`, `NEWVALUE_IS_NOT_NULL` | +| `IgnoreType` | `DISABLED`, `IGNORE_ALL`, `IGNORE_READ`, `IGNORE_WRITE` | +| `IocScope` | `DEFAULT`, `SINGLETON`, `REQUEST`, `SESSION`, `APPLICATION` | +| `MapperFeature` | `PREVENT_CYCLIC_MAPPING`, `SHARED_CONTEXT_DATA_IN_SUB_MAPPER`, `ALL` | +| `MapperVisibility` | `IGNORED`, `THIS_MAPPER`, `ALL_MAPPERS` | + +Runtime utilities in `sk.annotation.library.jam.utils` (package `.cache` for the cache types): + +```java +// MapperUtil (abstract, static-only) +public static final String constPostFixClassName = "JAMImpl"; +static public T getMapper(Class clsMapper); +static public T getMapper(Class clsMapper, Object fromOtherMapper); +static public void doInMapperContext(IRunInMapper worker); // IRunInMapper: void run() throws Throwable + +// MapperRunCtxData +public void putContextValue(String ctxKey, Object ctxVal); +public T getContextValue(String ctxKey); +public InstanceCache getInstanceCache(); + +// MapperRunCtxDataHolder +static public final ThreadLocal data; +static public MapperRunCtxData createDefaultContext(); + +// InstanceCache (interface; DefaultInstanceCache is the HashMap-based impl) +default public InstanceCacheValue getCacheValues(String key, Object in); +public InstanceCacheValue getCacheValues(int hashKey, Object in); + +// InstanceCacheValue +public T getValue(); +public boolean isRegisteredAnyValue(); +public boolean isRegistered(T value); +public void registerValue(T value); +``` + +`@EnableSpring` puts `org.springframework.stereotype.Component` (plus +`@Scope("request"/"session"/"application")` for non-default scopes) on the generated class +and `@Autowired` on injected mapper fields; `@EnableCDI` uses `javax.inject.Named` / +CDI scope annotations and `@Inject` +(`jam-processor/.../data/IoCUtils.java`, `jam-processor/.../Constants.java`). + +## Examples (all from the jam-tests modules) + +Paths below are relative to `jam-tests/`. Each mapper has a matching `...Test` in the +module's `src/test/java`. + +**Renamed + flattened fields** — `jam-tests-minimum/.../ex4/NestedClassMapper.java`: + +```java +@Mapper +public interface NestedClassMapper { + @MapperConfig(fieldMapping = { @FieldMapping(s = "zipCode", d = "zip") }) + UserWithAddressOutput toOutput(UserWithAddressInput userWithAddressInput); + + @MapperConfig(fieldMapping = { + @FieldMapping(s = "address.street", d = "street"), + @FieldMapping(s = "address.number", d = "number"), + @FieldMapping(s = "address.city", d = "city"), + @FieldMapping(s = "address.zipCode", d = "zip") + }) + UserWithFlatAddressOutput toOutputFlatten(UserWithAddressInput userWithAddressInput); +} +``` + +**Field ignoring** — `jam-tests-with-lombok/.../ex2/IgnoreFieldMapper.java` (Lombok beans): + +```java +@MapperConfig(fieldIgnore = { + @FieldIgnore(value = {"id","id2"}, types = {Object.class}), + @FieldIgnore({"id","id2"}) +}) +UserOutput toOutput(UserInput userInput); +``` + +Directional variants (`IgnoreType.IGNORE_READ`/`IGNORE_WRITE`, overriding a class-level +ignore with `ignored = IgnoreType.DISABLED`) are in `jam-tests-with-lombok/.../ex15/`. + +**Aggregation (two sources → one target)** — `jam-tests-minimum/.../ex10/AggregationMapper.java`: + +```java +@Mapper +@MapperConfig(fieldMapping = { @FieldMapping(s = "zipCode", d = "zip") }) +public interface AggregationMapper { + UserWithFlatAddressOutput toOutput(UserInput userInput, AddressInput addressInput); +} +``` + +**Immutable types + package-scoped config** — `jam-tests-minimum/.../ex12/` +(`ExampleImmputableMapper.java` — typo is in the real class name — and `package-info.java`): + +```java +@Mapper +@MapperConfig(immutable = {Obj1.class}) // type level +public interface ExampleImmputableMapper { Obj to2(Obj obj); } + +// package-info.java +@MapperConfig(immutable = {Obj2.class}) // package level +package sk.annotation.library.jam.example.ex12; +``` + +Types listed in `immutable` are assigned by reference instead of being deep-copied. +`withCustom` (delegating to other mapper classes, including method-level overrides) is shown +in `jam-tests-minimum/.../ex6/CustomBeanMapper1.java`, `ex20/UseOtherMapper.java` and +`ex22/Ex22MapperWithCustom.java`. + +**@Return in-place update & auto-detection** — `jam-tests-minimum/.../ex21/AutoDetectReturnMapper.java` +(comments from the source): + +```java +public abstract ObjMerge updateLast0(ObjIn1 o1); // normal rezim +public abstract ObjMerge updateLast1(ObjIn1 o1, ObjMerge ret); // autodetected @Return(true) +public abstract ObjMerge updateLast2(ObjIn1 o1, @Return ObjMerge ret); // force enabled +public abstract ObjMerge updateLast3(ObjIn1 o1, @Return(false) ObjMerge ret); // force disabled +``` + +A trailing parameter whose type equals the return type is treated as `@Return(true)` +automatically. The simplest update mapper is `jam-tests-minimum/.../ex8/UpdateDestinationBeanMapper.java`. + +**Conditional field application** — `jam-tests-with-lombok/.../ex24/MapperNotNull1.java`: +`@MapperConfig(applyWhen = ApplyFieldStrategy.NEWVALUE_IS_NOT_NULL)` etc. on individual +methods. + +**Spring integration** — there is **no Spring example in the test modules** (the +`jam-tests-with-spring`/`-cdi` modules are commented out in `jam-tests/pom.xml`). The +README shows the intended (illustrative) usage: + +```java +@Mapper +@EnableSpring // generated class becomes a Spring @Component +public interface SimpleMapper { ... } + +@Autowired +private SimpleMapper mapper; // inject anywhere +``` + +## Custom conversion methods and interceptors + +These semantics were verified by building `jam-tests` and reading the generated code, plus +the processor sources. + +### Custom conversion / factory methods + +Any non-abstract method you write in the mapper is signature-matched against conversions the +processor needs (`MapperClassInfo.findBestMatchMethodApiFullSyntax`, +`jam-processor/.../data/MapperClassInfo.java`, using +`TypeMethodUtils.isMethodCallableForMapper`: same parameter count, parameters and return +type must match exactly after resolving generic type variables). Generic methods work — from +`jam-tests-minimum/.../ex23/MapperWithGenerics.java`: + +```java +@Mapper +@DisableMapperFeature(MapperFeature.ALL) +abstract public class MapperWithGenerics { + protected MyTypeObj createMyTypeObj(T value) { ... } // user factory/converter + + abstract public MyTypeObj convertLong(Long o); + abstract public MyTypeObj convertString(String o); + abstract public MyTypeObj convertUncompatible(Long o); // no user method matches + abstract public ObjOut convert(ObjIn o); + + protected void interceptor1(Object o1, Object o2) {cnt1++;} + protected void interceptor2(T o1, Object o2) {cnt2++;} + protected void interceptor3(T o1, Object o2) {cnt3++;} + protected , LST_OUT extends List, LST_IN extends List> + void interceptor4(LST_IN o1, LST_OUT o2) {cnt4++;} +} +``` + +### Interceptors + +Matching rules (`TypeMethodUtils.isMethodCallableForInterceptor`, +`jam-processor/src/main/java/sk/annotation/library/jam/processor/utils/TypeMethodUtils.java:47`): +a user method is an interceptor for a generated transform when it + +1. returns `void`, +2. has **exactly two parameters**, +3. the transform's **source** type is assignable to parameter 1, +4. the transform's **destination** type is assignable to parameter 2, +5. generic type variables are resolved against their upper bounds; **type arguments are + matched invariantly** (a destination `RefType1b` does *not* match a + parameter `RefType1b` — see ex19, where `test_interceptor2` is never + wired in). + +When they fire: at the **end of every generated `transf_*` method**, after field copying and +just before `return`, in **declaration order** +(`AbstractMethodSourceInfo.writeSourceCodeBodyReturn` → `writeInterceptors`, +`jam-processor/.../data/generator/method/AbstractMethodSourceInfo.java`). + +When they do **not** fire: if a mapper method is implemented by **directly delegating to a +user-provided method**, no transform is generated and no interceptors are called +(`DeclaredMethodSourceInfo.canCallInterceptors()` returns `false`, +`jam-processor/.../data/generator/method/DeclaredMethodSourceInfo.java`). + +Generated code — excerpt copied from +`jam-tests/jam-tests-minimum/target/generated-sources/annotations/sk/annotation/library/jam/example/ex23/MapperWithGenericsJAMImpl.java` +(build output, JDK 11 profile): + +```java +@Override +public MyTypeObj convertLong(Long o) { + // check null inputs + if (o==null) return null; + + MyTypeObj ret = null; + ret = createMyTypeObj(o); // direct delegation to user method — NO interceptors + return ret; +} + +protected ObjOut transf_toObjOut(ObjIn in) { + // check null inputs + if (in==null) return null; + + ObjOut out = new ObjOut(); + + // Copy Fields + out.setValueLong ( createMyTypeObj(in.getValueLong()) ); + out.setValueLongInList ( transf_toList_withMyTypeObj(in.getValueLongInList()) ); + out.setValueString ( createMyTypeObj(in.getValueString()) ); + + // Call Interceptors ... + interceptor1(in, out); // declaration order; interceptor3 skipped + interceptor2(in, out); // (source ObjIn is not a Long), interceptor4 skipped + return out; // (params are not Lists) +} +``` + +In the same file, `transf_toMyTypeObj_withString(Long in)` calls interceptors 1, 2 **and** 3 +(source `Long` satisfies ``), and the generated `List` transform calls 1, 2 +and 4 — signature matching, not naming, decides. + +## Feature catalog (example packages) + +`M` = `jam-tests/jam-tests-minimum`, `L` = `jam-tests/jam-tests-with-lombok` (ex2, ex15–17, +ex24 live **only** in the Lombok module; there is no ex2/ex15–17 in jam-tests-minimum). +Package root: `sk.annotation.library.jam.example.`. + +| Pkg | Mod | Feature | Key classes | +|---|---|---|---| +| ex1 | M | Minimal bidirectional bean mapping, `MapperUtil.getMapper` | `SimpleMapper`, `SimpleMapperTest` | +| ex2 | L | Field renaming + `@FieldIgnore` (with `types` filter), Lombok beans | `IgnoreFieldMapper`, `CustomFieldMapper` | +| ex3 | M | Copy/clone of the same type | `CopyMapper` | +| ex4 | M | `@FieldMapping` rename (`zipCode`→`zip`) and nested-path flattening (`address.street`→`street`) | `NestedClassMapper` | +| ex5 | M | `List
` collections, class-level `@MapperConfig` | `NestedClassListMapper` | +| ex6 | M | `withCustom` delegation to a handwritten mapper class | `CustomBeanMapper1`, `CustomBeanMapper2`, `CustomBeanMapperImpl` | +| ex7 | M | Cyclic graph (tree with children) via instance cache | `CyclicBeanMapper`, `TreeNodeInput/Output` | +| ex8 | M | `@Return` in-place update of an existing destination | `UpdateDestinationBeanMapper` | +| ex9 | M | Bean containing enum field (test largely commented out) | `BeanWithEnumMapper` | +| ex10 | M | Aggregation: two source params → one destination | `AggregationMapper` | +| ex11 | M | Enum→enum mapping by constant name | `EnumerationMapper` | +| ex12 | M | `immutable` types; package-level config in `package-info.java` | `ExampleImmputableMapper` | +| ex13 | M | `java.util.Date` ↔ `java.sql.Date`/`Time`/`Timestamp` conversions | `DateMapper` | +| ex14 | M | Primitive/wrapper/String/BigInteger/BigDecimal coercions | `SimpleTypesMapper` | +| ex15 | L | Directional/typed `@FieldIgnore`, `IgnoreType.DISABLED` overrides | `FieldIgnoresMapper`, `FieldIgnoresDirectionWith*Mapper` | +| ex16 | L | `@FieldMapping` direction flags `ignoreDirectionS2D`/`D2S`, `sTypes`/`dTypes` | `CustomFieldMapper` | +| ex17 | L | Collections: `List`/`Set`/`Map` incl. implementation changes | `CollectionsMapper` | +| ex18 | M | Mapper-in-mapper constructor cycle; `MapperUtil.getMapper(cls, this)` | `MapperConstructorCycleMapper` | +| ex19 | M | Interceptor matching with generic type hierarchies (invariant type args) | `RefTypeMapper`, `RefType1a/1b`, `SubType1aLong` | +| ex20 | M | Multiple `withCustom` mappers as fields; private fields ignored | `UseOtherMapper`, `OhterMapper1..4` (sic) | +| ex21 | M | `@Return` auto-detection / force-enable / force-disable | `AutoDetectReturnMapper` | +| ex22 | M | Class-level vs method-level `withCustom` override; package config | `Ex22MapperWithCustom` | +| ex23 | M | Generic factory methods + generic-bounded interceptors | `MapperWithGenerics`, `MyTypeObj` | +| ex24 | L | `applyWhen = ApplyFieldStrategy.*` conditional field writes | `MapperNotNull1`, `MapperNotNull2` | + +## Gotchas + +- **`@Return` semantics.** Generated update methods return the destination parameter when + the source is `null` (`if (in==null) return out;`), and create a new instance only when + the passed destination is `null` (verified in `ex8/UpdateDestinationBeanMapperJAMImpl`). + Auto-detection kicks in whenever the last parameter's type equals the return type — add + `@Return(false)` if you don't want update semantics. +- **Cyclic-mapping instance cache.** Each generated transform first consults a + `ThreadLocal` `MapperRunCtxData` instance cache keyed by generated-method name + source + instance and registers the destination *before* copying fields, so the same source object + maps to the same destination within one call tree. Disable per mapper with + `@DisableMapperFeature(MapperFeature.PREVENT_CYCLIC_MAPPING)` (or `ALL`, which also drops + the context plumbing entirely — compare `ex23`'s generated code, which has no `ctx` + parameter, against `ex7`'s). +- **Config scoping.** `@MapperConfig` can sit on a **package** (`package-info.java`), a + **type**, or a **method**; more specific wins (ex12, ex15, ex22). +- **Interceptor bypass.** Interceptors run only inside *generated* transforms. If a mapper + method's signature exactly matches a user-written method, the generated code delegates + straight to it and no interceptors fire (`convertLong` in ex23). +- **Interceptor generics are invariant.** `RefType1b` does not match a + `RefType1b` destination (ex19). +- **Stale README.** Version numbers in `README.md` (0.9.18) lag the actual state + (release 0.9.20, dev 0.9.22-SNAPSHOT). Regenerate this file with + `docs/regenerate-llm-md.sh` after releases. +- **Misspellings are real.** `Context.jamConfif`, `ExampleImmputableMapper`, `OhterMapper1` + — these typos exist in the source; don't "fix" them when referencing the API/examples. +- **`jam-tests` is not built by default.** The root pom excludes it; use + `-Prun-jam-tests` or build from the `jam-tests/` directory. Generated sources land in + `jam-tests/*/target/generated-sources/annotations/`. diff --git a/docs/regenerate-llm-md.sh b/docs/regenerate-llm-md.sh new file mode 100755 index 0000000..8254d5f --- /dev/null +++ b/docs/regenerate-llm-md.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# Regenerates LLM.md at the repository root using Claude Code. +# Usage: bin/regenerate-llm-md.sh +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +PROMPT=$(cat <<'EOF' +Create (overwrite) an LLM.md file at the repository root. Its purpose is to let LLM models / +coding assistants quickly understand what this repository is, when to use it, and how to use it. + +Requirements for the process (accuracy matters more than speed): + +1. Derive every fact from the repository itself — never from memory or the README alone. + Cross-check the README against pom.xml files and git tags; if they disagree (e.g. stale + version numbers), trust the poms/tags and flag the README as stale in the doc. +2. For every code example you include, state which module/package it comes from. If a snippet + is illustrative rather than copied from real code, label it as such. Do not attribute an + example to a location where it doesn't exist. +3. For non-obvious runtime semantics (e.g. interceptors), do not guess from the annotated + sources: build the test module (`mvn -P jdk11 install/compile`, parent poms first if needed) + and read the generated `*JAMImpl` sources under `target/generated-sources/annotations/`, + plus the relevant processor code (e.g. `TypeMethodUtils.isMethodCallableForInterceptor`). + Document what the generated code actually does. +4. After writing, verify the document against the source with fresh eyes (or a subagent): + every annotation name/attribute, enum constant, method signature, Maven coordinate, + version, and example attribution must match reality. Fix what doesn't. + +Required content/sections: + +- **What this repository is** — compile-time annotation-processor object mapper (JAM), + modules (jam-common runtime, jam-processor, jam-tests), Maven coordinates, current version + vs latest release tag, Java version support (jdk8/jdk11 profiles), license, upstream URL. +- **When to use it** — the problems it solves (compile-time generation, no reflection, + cyclic graphs, in-place updates, Spring/CDI integration); comparable tools (MapStruct/SELMA). +- **Installation** — Maven dependency snippet (jam-common compile, jam-processor provided). +- **Quick start** — minimal @Mapper interface + MapperUtil.getMapper usage, note the + generated `JAMImpl` naming and Lombok accessor support. +- **Public API** — table of all annotations in `sk.annotation.library.jam.annotations` with + their targets and attributes; the enums with all constants; MapperUtil / MapperRunCtxData / + InstanceCache signatures. +- **Examples** — representative snippets from jam-tests (renamed fields, field ignoring, + aggregation, immutable/withCustom, @Return updates, Spring integration), each with its + real source location. +- **Custom conversion methods and interceptors** — based on ex23 (MapperWithGenerics) and + ex19: signature-matched factory/converter methods; interceptor matching rules (void, exactly + two params, source assignable to param 1, destination to param 2, generic bounds); when they + fire (end of each generated transform, declaration order) and when they don't (direct + delegation to user methods). Include a generated-code excerpt. +- **Feature catalog** — one table row per jam-tests-minimum example package (ex1..ex23): + feature demonstrated + key classes, verified against each package's mapper and test. +- **Gotchas** — @Return semantics, cyclic-mapping instance cache, config scoping + (package/type/method), interceptor bypass, stale README versions. +EOF +) + +STREAM_LOG="$(mktemp -t regenerate-llm-md.XXXXXX.jsonl)" +trap 'rm -f "$STREAM_LOG"' EXIT + +# File modifications are restricted to LLM.md: the Edit(LLM.md) rule covers all +# file-editing tools (Write included) for that path only, +# git is limited to read-only subcommands, and no other write-capable tools are allowed +# (denied tool calls fail silently in -p mode). Exception: mvn still produces build output +# (target/, local ~/.m2 installs) — required to inspect generated *JAMImpl sources. +# +# --output-format stream-json emits one JSON event per line as Claude Code works; jq turns +# them into live progress messages so long silences don't look like a hang. The raw stream +# is kept in $STREAM_LOG to extract the final result afterwards. +claude -p "$PROMPT" \ + --permission-mode acceptEdits \ + --output-format stream-json --verbose \ + --allowedTools "Read Glob Grep Task Agent Edit(LLM.md) Bash(mvn:*) Bash(git log:*) Bash(git tag:*) Bash(git status) Bash(git diff:*) Bash(git show:*) Bash(ls:*) Bash(rg:*) Bash(fdfind:*) Bash(cat:*)" \ + | tee "$STREAM_LOG" \ + | jq --unbuffered -r ' + def brief: tostring | gsub("\\s+"; " ") | .[0:120]; + if .type == "system" and .subtype == "init" then + "session started (model: \(.model), session: \(.session_id))" + elif .type == "assistant" then + (.message.content // [])[] + | if .type == "tool_use" then + " tool \(.name): \((.input.description // .input.file_path // .input.pattern // .input.command // .input.prompt // "") | brief)" + elif .type == "text" and (.text | length) > 0 then + "* \(.text | brief)" + else empty + end + elif .type == "result" then + if .is_error then + "FAILED after \((.duration_ms / 1000) | floor)s: \((.result // "unknown error") | brief)" + else + "finished in \((.duration_ms / 1000) | floor)s, \(.num_turns) turns, cost $\(.total_cost_usd), denied tool calls: \(.permission_denials | length)" + end + else empty + end + ' \ + | while IFS= read -r message; do + printf '[%s] %s\n' "$(date +%H:%M:%S)" "$message" + done + +# The final "result" event is the authoritative outcome; a missing one means the run +# aborted before finishing. +RESULT_IS_ERROR=$(jq -r 'select(.type == "result") | .is_error' "$STREAM_LOG" | tail -1) +if [ "$RESULT_IS_ERROR" != "false" ]; then + echo "Claude Code did not report success — LLM.md may not have been regenerated." >&2 + exit 1 +fi + +echo +echo "--- Final report from Claude Code ---" +jq -r 'select(.type == "result") | .result // empty' "$STREAM_LOG" +echo +echo "Done. Review the result: git diff LLM.md" From a4739105bb66fa6861906e228a9cb20a19099ac8 Mon Sep 17 00:00:00 2001 From: Igor Kvasnicka Date: Thu, 16 Jul 2026 21:23:48 +0200 Subject: [PATCH 2/2] split script and prompt into two separate files --- docs/LLM.md.prompt | 45 ++++++++++++++++++++++++++++ docs/regenerate-llm-md.sh | 62 +++++++++------------------------------ 2 files changed, 59 insertions(+), 48 deletions(-) create mode 100644 docs/LLM.md.prompt diff --git a/docs/LLM.md.prompt b/docs/LLM.md.prompt new file mode 100644 index 0000000..bda4aa7 --- /dev/null +++ b/docs/LLM.md.prompt @@ -0,0 +1,45 @@ +Create (overwrite) an LLM.md file at the repository root. Its purpose is to let LLM models / +coding assistants quickly understand what this repository is, when to use it, and how to use it. + +Requirements for the process (accuracy matters more than speed): + +1. Derive every fact from the repository itself — never from memory or the README alone. + Cross-check the README against pom.xml files and git tags; if they disagree (e.g. stale + version numbers), trust the poms/tags and flag the README as stale in the doc. +2. For every code example you include, state which module/package it comes from. If a snippet + is illustrative rather than copied from real code, label it as such. Do not attribute an + example to a location where it doesn't exist. +3. For non-obvious runtime semantics (e.g. interceptors), do not guess from the annotated + sources: build the test module (`mvn -P jdk11 install/compile`, parent poms first if needed) + and read the generated `*JAMImpl` sources under `target/generated-sources/annotations/`, + plus the relevant processor code (e.g. `TypeMethodUtils.isMethodCallableForInterceptor`). + Document what the generated code actually does. +4. After writing, verify the document against the source with fresh eyes (or a subagent): + every annotation name/attribute, enum constant, method signature, Maven coordinate, + version, and example attribution must match reality. Fix what doesn't. + +Required content/sections: + +- **What this repository is** — compile-time annotation-processor object mapper (JAM), + modules (jam-common runtime, jam-processor, jam-tests), Maven coordinates, current version + vs latest release tag, Java version support (jdk8/jdk11 profiles), license, upstream URL. +- **When to use it** — the problems it solves (compile-time generation, no reflection, + cyclic graphs, in-place updates, Spring/CDI integration); comparable tools (MapStruct/SELMA). +- **Installation** — Maven dependency snippet (jam-common compile, jam-processor provided). +- **Quick start** — minimal @Mapper interface + MapperUtil.getMapper usage, note the + generated `JAMImpl` naming and Lombok accessor support. +- **Public API** — table of all annotations in `sk.annotation.library.jam.annotations` with + their targets and attributes; the enums with all constants; MapperUtil / MapperRunCtxData / + InstanceCache signatures. +- **Examples** — representative snippets from jam-tests (renamed fields, field ignoring, + aggregation, immutable/withCustom, @Return updates, Spring integration), each with its + real source location. +- **Custom conversion methods and interceptors** — based on ex23 (MapperWithGenerics) and + ex19: signature-matched factory/converter methods; interceptor matching rules (void, exactly + two params, source assignable to param 1, destination to param 2, generic bounds); when they + fire (end of each generated transform, declaration order) and when they don't (direct + delegation to user methods). Include a generated-code excerpt. +- **Feature catalog** — one table row per jam-tests-minimum example package (ex1..ex23): + feature demonstrated + key classes, verified against each package's mapper and test. +- **Gotchas** — @Return semantics, cyclic-mapping instance cache, config scoping + (package/type/method), interceptor bypass, stale README versions. diff --git a/docs/regenerate-llm-md.sh b/docs/regenerate-llm-md.sh index 8254d5f..606bf77 100755 --- a/docs/regenerate-llm-md.sh +++ b/docs/regenerate-llm-md.sh @@ -6,54 +6,19 @@ set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$REPO_ROOT" -PROMPT=$(cat <<'EOF' -Create (overwrite) an LLM.md file at the repository root. Its purpose is to let LLM models / -coding assistants quickly understand what this repository is, when to use it, and how to use it. - -Requirements for the process (accuracy matters more than speed): - -1. Derive every fact from the repository itself — never from memory or the README alone. - Cross-check the README against pom.xml files and git tags; if they disagree (e.g. stale - version numbers), trust the poms/tags and flag the README as stale in the doc. -2. For every code example you include, state which module/package it comes from. If a snippet - is illustrative rather than copied from real code, label it as such. Do not attribute an - example to a location where it doesn't exist. -3. For non-obvious runtime semantics (e.g. interceptors), do not guess from the annotated - sources: build the test module (`mvn -P jdk11 install/compile`, parent poms first if needed) - and read the generated `*JAMImpl` sources under `target/generated-sources/annotations/`, - plus the relevant processor code (e.g. `TypeMethodUtils.isMethodCallableForInterceptor`). - Document what the generated code actually does. -4. After writing, verify the document against the source with fresh eyes (or a subagent): - every annotation name/attribute, enum constant, method signature, Maven coordinate, - version, and example attribution must match reality. Fix what doesn't. - -Required content/sections: +PROMPT_FILE="$REPO_ROOT/docs/LLM.md.prompt" +if [ ! -f "$PROMPT_FILE" ]; then + echo "Prompt file not found: $PROMPT_FILE" >&2 + exit 1 +fi -- **What this repository is** — compile-time annotation-processor object mapper (JAM), - modules (jam-common runtime, jam-processor, jam-tests), Maven coordinates, current version - vs latest release tag, Java version support (jdk8/jdk11 profiles), license, upstream URL. -- **When to use it** — the problems it solves (compile-time generation, no reflection, - cyclic graphs, in-place updates, Spring/CDI integration); comparable tools (MapStruct/SELMA). -- **Installation** — Maven dependency snippet (jam-common compile, jam-processor provided). -- **Quick start** — minimal @Mapper interface + MapperUtil.getMapper usage, note the - generated `JAMImpl` naming and Lombok accessor support. -- **Public API** — table of all annotations in `sk.annotation.library.jam.annotations` with - their targets and attributes; the enums with all constants; MapperUtil / MapperRunCtxData / - InstanceCache signatures. -- **Examples** — representative snippets from jam-tests (renamed fields, field ignoring, - aggregation, immutable/withCustom, @Return updates, Spring integration), each with its - real source location. -- **Custom conversion methods and interceptors** — based on ex23 (MapperWithGenerics) and - ex19: signature-matched factory/converter methods; interceptor matching rules (void, exactly - two params, source assignable to param 1, destination to param 2, generic bounds); when they - fire (end of each generated transform, declaration order) and when they don't (direct - delegation to user methods). Include a generated-code excerpt. -- **Feature catalog** — one table row per jam-tests-minimum example package (ex1..ex23): - feature demonstrated + key classes, verified against each package's mapper and test. -- **Gotchas** — @Return semantics, cyclic-mapping instance cache, config scoping - (package/type/method), interceptor bypass, stale README versions. -EOF -) +# The prompt file starts with a human-facing preamble followed by a '---' separator line; +# everything after that separator is the actual prompt sent to Claude Code. +PROMPT=$(awk 'found { print } /^---$/ { found = 1 }' "$PROMPT_FILE") +if [ -z "$PROMPT" ]; then + echo "No prompt content found after the '---' separator in $PROMPT_FILE" >&2 + exit 1 +fi STREAM_LOG="$(mktemp -t regenerate-llm-md.XXXXXX.jsonl)" trap 'rm -f "$STREAM_LOG"' EXIT @@ -70,7 +35,7 @@ trap 'rm -f "$STREAM_LOG"' EXIT claude -p "$PROMPT" \ --permission-mode acceptEdits \ --output-format stream-json --verbose \ - --allowedTools "Read Glob Grep Task Agent Edit(LLM.md) Bash(mvn:*) Bash(git log:*) Bash(git tag:*) Bash(git status) Bash(git diff:*) Bash(git show:*) Bash(ls:*) Bash(rg:*) Bash(fdfind:*) Bash(cat:*)" \ + --allowedTools "Read Glob Grep Task Agent Edit(LLM.md) Bash(mvn:*) Bash(./gradlew:*) Bash(git log:*) Bash(git tag:*) Bash(git status) Bash(git diff:*) Bash(git show:*) Bash(ls:*) Bash(rg:*) Bash(fdfind:*) Bash(cat:*)" \ | tee "$STREAM_LOG" \ | jq --unbuffered -r ' def brief: tostring | gsub("\\s+"; " ") | .[0:120]; @@ -110,3 +75,4 @@ echo "--- Final report from Claude Code ---" jq -r 'select(.type == "result") | .result // empty' "$STREAM_LOG" echo echo "Done. Review the result: git diff LLM.md" +