Thanks for considering a contribution. This guide covers what to read first, how to set up locally, and how to propose changes that land cleanly.
- README — what we're building and why.
- Project state — what phase we're in and what's next on the roadmap.
- Compatibility matrix — the lighthouse: what's core, what's framework-layer, what's Fanar-exclusive. Every scope question traces back here.
- ADRs — every non-obvious decision, grouped into five categories. Don't deviate from an ADR without superseding it.
- API sketch — the target code shape. Living document.
- Architecture — module layout, request-flow diagrams, "where does X live?".
- Library best practices — hygiene every PR must respect.
- Glossary — Fanar-specific and project-specific terminology.
If you read those in order, you have the full context. Expect ~30 minutes.
Prerequisites: JDK 21 or later on PATH. Nothing else.
git clone <your fork>
cd fanar-java
./mvnw verifyverify passes with two expected warnings about module-name terminal digits (documented in ADR-010).
If it fails for any other reason, that is a bug — please open an issue.
./mvnw -pl core verify # core only
./mvnw -pl json-jackson3 -am verify # adapter + its dependencies
./mvnw -pl spring-ai-starter verify # Spring AI adapter + starter chainThe fanar-java-e2e module is gated on FANAR_API_KEY — without it, the live tests skip silently.
With a real key in scope:
FANAR_API_KEY=… ./mvnw -pl e2e -am verifyPR CI does not run live tests (no key in PR scope); the nightly job will (planned).
- Discuss first for substantial changes. Open an issue or comment on an existing one. Use the appropriate issue template — particularly the Scope dropdown on feature requests, which forces the core-vs-framework-layer conversation up front.
- Fork → branch from
main→ push → PR. - Fill the PR template. The scope-split checklists are not decorative. Reviewers will ask about unchecked items.
- Keep PRs focused. One design decision per PR. One bug fix per PR. Large multi-concern PRs get broken up in review — saving both sides time.
If your change touches the public API, adds or alters an SPI, changes scope, or affects stability, it needs an ADR.
- Pick the next unused number (look at the INDEX — last entry is highest). Numbers are assigned in creation order and never renumbered.
- Copy an existing ADR as a template (they all follow the extended Michael Nygard format):
cp docs/adr/019-pre-10-stability-policy.md docs/adr/020-my-decision.md - Fill in the sections: Status (
Proposedinitially), Date, Deciders, Context, Decision, Alternatives considered, Consequences, References. - Open a PR containing the ADR and (if applicable) the code change it motivates.
- When the PR merges, flip the ADR's Status to
Accepted. - If the change supersedes an existing ADR, set the old one's Status to
Superseded by ADR-XYZand add a cross- reference both ways.
Short, imperative, module-prefixed:
core: add ChatRequest and ChatResponse records
json-jackson3: wire ServiceLoader descriptor
docs: update ADR-008 with Central Portal note
ci: bump Java matrix to 21 and 25
Reference ADRs where relevant: core: implement retry chain (ADR-012, ADR-014).
The full set lives in Library best practices. Highlights:
- Core module has zero runtime dependencies. Any new dep is an ADR conversation, not a PR.
- No third-party types on the public API surface. JDK types (
Flow.Publisher,CompletableFuture, etc.) and our own DTOs only. - Top-level package = public API,
.spi= extension interfaces,.internal= implementation (not exported). - Records for DTOs, sealed interfaces for unions, no
Optionalfields. See ADR-015. - Javadoc on every public type and method.
-Xdoclint:all,-missingis enforced at compile time. module-info.javaexports only public packages. Never internal ones.-parametersis enabled globally. Spring MVC's@PathVariable String foobinds by parameter name reflectively; the flag must be on for that to work without explicit name args.- Test patterns to mimic:
ApplicationContextRunner+ AssertJ for auto-config tests;FilteredClassLoader(ChatModel.class)to assert "without spring-ai on classpath, the bean isn't registered"; realHttpServer(no mocks) for adapter wire-format tests — seeFanarChatModelTestandFanarHealthIndicatorTest.
Every shipping module enforces:
- JaCoCo 100 % on instructions, lines, branches, methods, complexity. Sample apps and
e2e*modules setjacoco.skip=true. dependency:analyzestrict — fails on undeclared or unused direct deps. Sample apps disable it.- Doclint at javac time.
When CI flakes on a coverage gate, the failing job uploads the JaCoCo HTML report as an artifact named
jacoco-java-{21,25} — drill into the package row at < 100 % and the highlighted source line tells you
which branch is missed. Concurrency-flake fixes belong on the test (deterministic ordering, latches),
not on the threshold.
We use the release-and-bump flow: every tagged commit's pom.xml matches the release version
exactly — no -SNAPSHOT suffix appears in published artifacts. The release workflow guards this
by failing fast if the pom doesn't match the resolved version.
Step by step:
-
Cut a release branch from
main:git switch -c release/0.1.0 main ./mvnw -B versions:set -DnewVersion=0.1.0 -DgenerateBackupPoms=false git commit -am "release: 0.1.0" git push -u origin release/0.1.0 -
Dry-run the release workflow from the release branch:
Actions → Release → Run workflow → Branch: release/0.1.0, version: 0.1.0, dry_run: true. The workflow builds, verifies, stages 10 consumable artifacts (9 library jars + BOM.pom), uploads them to the workflow run page, and stops short of creating a GitHub Release. Inspect the artifact list — every filename should end in0.1.0.jar/0.1.0.pom. (Sample apps are not shipped — they're cloneable demos, not pinnable artifacts.) -
Open a PR
release/0.1.0 → main. Review checks the version bump and changelog. -
Merge — squash or merge-commit, doesn't matter. The release branch only has one commit (the version bump), so both produce equivalent history on main.
-
Tag the merged commit:
git switch main && git pull git tag -a v0.1.0 -m "Release 0.1.0" git push origin v0.1.0
Tag-push fires the release workflow again, this time creating the GitHub Release with the 12 artifacts attached and auto-generated PR notes.
-
Bump main back to the next snapshot — mandatory follow-up:
git switch -c bump/0.2.0-SNAPSHOT main ./mvnw -B versions:set -DnewVersion=0.2.0-SNAPSHOT -DgenerateBackupPoms=false git commit -am "build: bump to 0.2.0-SNAPSHOT"PR → main. Without this, every subsequent dev build still says
0.1.0and trying to release0.1.0again will fail noisily.
The Verify pom version matches release version step in release.yml enforces step 1 — if you
forget the versions:set commit, the workflow fails with a one-line fix instruction.
- GitHub Discussions — questions, ideas, help.
- Issue tracker — bugs and concrete feature requests only.
- SECURITY.md — private vulnerability reporting. Never open a public issue for a vuln.
- Strong narrow core; adaptable edges. Most of our decisions aim at a small, evolvable, framework-agnostic core.
- Internals are not a contract. Anything under
.internal.*can change freely. Only the public API and.spicarry stability guarantees (see ADR-018). - Every decision is reasoned, not decreed. If an ADR doesn't explain why, that's a bug — please open a PR to strengthen it.