From d8bcdce87976313afd77734efaf55c2c1f9c1938 Mon Sep 17 00:00:00 2001 From: Adrien Date: Thu, 9 Apr 2026 12:05:02 +0200 Subject: [PATCH] Squashed commit of the following: commit 0d624137c2557c6eeb87020749e4977b821c2b5c Author: Adrien Date: Thu Apr 9 11:55:22 2026 +0200 backend native image setup --- .env.example | 13 + CLAUDE.md | 4 +- README.md | 53 +++- backend/.dockerignore | 24 ++ backend/Dockerfile.native | 26 ++ backend/pom.xml | 85 ++++++ .../com/aiteacher/AiTeacherApplication.java | 4 + .../aiteacher/config/NativeHintsConfig.java | 76 +++++ backend/src/main/resources/application.yaml | 6 +- docker-compose.native.yml | 37 +++ .../contracts/build-contract.md | 47 ++++ .../005-native-image-deployment/data-model.md | 33 +++ specs/005-native-image-deployment/plan.md | 213 ++++++++++++++ .../005-native-image-deployment/quickstart.md | 81 ++++++ specs/005-native-image-deployment/research.md | 262 ++++++++++++++++++ specs/005-native-image-deployment/spec.md | 138 +++++++++ specs/005-native-image-deployment/tasks.md | 189 +++++++++++++ 17 files changed, 1285 insertions(+), 6 deletions(-) create mode 100644 .env.example create mode 100644 backend/.dockerignore create mode 100644 backend/Dockerfile.native create mode 100644 backend/src/main/java/com/aiteacher/config/NativeHintsConfig.java create mode 100644 docker-compose.native.yml create mode 100644 specs/005-native-image-deployment/contracts/build-contract.md create mode 100644 specs/005-native-image-deployment/data-model.md create mode 100644 specs/005-native-image-deployment/plan.md create mode 100644 specs/005-native-image-deployment/quickstart.md create mode 100644 specs/005-native-image-deployment/research.md create mode 100644 specs/005-native-image-deployment/spec.md create mode 100644 specs/005-native-image-deployment/tasks.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..cc0369d --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Copy this file to .env and fill in your values before running docker-compose.native.yml +# .env is gitignored — never commit real credentials + +# OpenAI +OPENAI_API_KEY=sk-... + +# AWS S3 (figure storage — leave blank if using local filesystem) +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_REGION=eu-west-1 + +# S3 bucket name (if S3 storage enabled) +APP_STORAGE_S3_BUCKET=ai-teacher-figures diff --git a/CLAUDE.md b/CLAUDE.md index 28ba62c..a29d9b6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,6 +14,8 @@ Auto-generated from all feature plans. Last updated: 2026-04-07 - PostgreSQL (sections, figures, messages — unchanged). No new tables needed. (004-rag-retrieval-quality) - Java 21 (backend), TypeScript / Node 20 (frontend) + Spring Boot 4.0.5, Spring AI 2.0.0-M4, OpenAI API (chat + embeddings), Vue 3.4, Pinia 2.1, Axios 1.7 (004-rag-retrieval-quality) - PostgreSQL (JPA + Flyway), pgvector (`VectorStore`) (004-rag-retrieval-quality) +- Java 25 (backend), TypeScript / Node 20 (frontend) + Spring Boot 4.0.5, Spring AI 2.0.0-M4, `native-maven-plugin` 0.10.6, (005-native-image-deployment) +- PostgreSQL 16 + pgvector (unchanged) (005-native-image-deployment) - Java 21 (backend), TypeScript / Node 20 (frontend) (001-neuro-rag-learning) @@ -33,9 +35,9 @@ npm test && npm run lint Java 21 (backend), TypeScript / Node 20 (frontend): Follow standard conventions ## Recent Changes +- 005-native-image-deployment: Added Java 25 (backend), TypeScript / Node 20 (frontend) + Spring Boot 4.0.5, Spring AI 2.0.0-M4, `native-maven-plugin` 0.10.6, - 004-rag-retrieval-quality: Added Java 21 (backend), TypeScript / Node 20 (frontend) + Spring Boot 4.0.5, Spring AI 2.0.0-M4, OpenAI API (chat + embeddings), Vue 3.4, Pinia 2.1, Axios 1.7 - 004-rag-retrieval-quality: Added Java 21 (backend), TypeScript / Node 20 (frontend) + Spring Boot 4.0.5, Spring AI 2.0.0-M4, OpenAI API (chat + embeddings), pgvector, Vue 3.4, Pinia 2.1 -- 003-basic-login: Added Java 21 (backend) / TypeScript + Node 20 (frontend) + Spring Boot 4.0.5, Spring Security (already included), Vue 3.4, Vue Router 4.3, Pinia 2.1, Axios 1.7 diff --git a/README.md b/README.md index 8ef2994..8c6aaca 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ PictureGroup ## Stack -- **Backend**: Spring Boot 4.0.5 + Spring AI 2.0.0-M4, Java 21, Maven +- **Backend**: Spring Boot 4.0.5 + Spring AI 2.0.0-M4, Java 25, Maven - **Frontend**: Vue.js 3 + Vite + TypeScript + Pinia + Axios - **Database**: PostgreSQL 16 + pgvector extension - **Auth**: HTTP Basic (single shared in-memory user) @@ -146,7 +146,7 @@ PictureGroup See [specs/001-neuro-rag-learning/quickstart.md](specs/001-neuro-rag-learning/quickstart.md) for full instructions. -### Local Dev +### Local Dev (JVM) ```bash # Start the database @@ -162,6 +162,55 @@ npm install npm run dev ``` +### Native Image Build + +Produces a GraalVM native binary packaged into a minimal Docker image via Jib. + +**Prerequisite**: GraalVM 25 must be installed and set as `JAVA_HOME`. + +```bash +# Install GraalVM 25 CE via sdkman (one-time) +sdk install java 25-graalce +sdk use java 25-graalce + +# Build native executable + Docker image (requires Docker daemon) +cd backend +mvn -Pnative package jib:dockerBuild -DskipTests +``` + +The image `ai-teacher-backend:latest` will appear in your local Docker. It starts in under 1 second and uses significantly less memory than the JVM image. + +### Run Native Stack (Docker Compose) + +```bash +# Copy and fill in secrets +cp .env.example .env +# edit .env — add OPENAI_API_KEY at minimum + +# Start PostgreSQL + native backend +docker compose -f docker-compose.native.yml up +``` + +App available at `http://localhost:8080`. + +### Build Pipeline (Native) + +```mermaid +graph LR + SRC["Source Code\n(Java 25)"] + AOT["Spring Boot AOT\n(process-aot)"] + NI["GraalVM native-image\n(native-maven-plugin)"] + EXE["Native Executable\ntarget/ai-teacher-backend"] + JIB["Jib\n(jib-native-image-extension)"] + IMG["Docker Image\nai-teacher-backend:latest\n(distroless base)"] + + SRC --> AOT + AOT --> NI + NI --> EXE + EXE --> JIB + JIB --> IMG +``` + ### Environment Variables #### Backend diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..f666d2d --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,24 @@ +# Java build artifacts +target/ +*.class +*.jar + +# Git +.git/ +.gitignore + +# Editor +.vscode/ +.idea/ +*.iml + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log + +# Environment +.env +.env.* diff --git a/backend/Dockerfile.native b/backend/Dockerfile.native new file mode 100644 index 0000000..fddf7e9 --- /dev/null +++ b/backend/Dockerfile.native @@ -0,0 +1,26 @@ +# ---- Pull Maven from its official image (avoids microdnf under QEMU) ---- +FROM maven:3.9.9-eclipse-temurin-21 AS maven-dist + +# ---- Build stage: GraalVM 25 + Maven ---- +ARG TARGETPLATFORM=linux/arm64 +FROM --platform=$TARGETPLATFORM ghcr.io/graalvm/native-image-community:25 AS build + +# Copy Maven from the official Maven image — no package installation needed +COPY --from=maven-dist /usr/share/maven /opt/maven +ENV PATH="/opt/maven/bin:$PATH" + +WORKDIR /app + +# Cache dependency resolution separately from source compilation +COPY pom.xml . +RUN mvn -Pnative dependency:resolve dependency:resolve-plugins -q + +# Build native executable +COPY src ./src +RUN mvn -Pnative package -DskipTests + +# ---- Runtime stage: minimal ARM64 distroless ---- +FROM --platform=$TARGETPLATFORM gcr.io/distroless/base-nossl-debian12 +COPY --from=build /app/target/ai-teacher-backend /app/ai-teacher-backend +EXPOSE 8080 +ENTRYPOINT ["/app/ai-teacher-backend"] diff --git a/backend/pom.xml b/backend/pom.xml index 1ca8141..c49997f 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -143,12 +143,97 @@ + + org.graalvm.buildtools + native-maven-plugin + + org.springframework.boot spring-boot-maven-plugin + + + + com.google.cloud.tools + jib-maven-plugin + 3.4.5 + + + + gcr.io/distroless/base-nossl-debian12 + + + + ai-teacher-backend + + + + 8080 + + + + + + com.google.cloud.tools.jib.maven.extension.nativeimage.JibNativeImageExtension + + + + + + + com.google.cloud.tools + jib-native-image-extension-maven + 0.1.0 + + + + + + native + + + + + + org.graalvm.buildtools + native-maven-plugin + 1.0.0 + + + add-reachability-metadata + + add-reachability-metadata + + + + compile + + compile-no-fork + + package + + + + ai-teacher-backend + + --initialize-at-build-time=org.slf4j,ch.qos.logback + -H:+ReportExceptionStackTraces + --gc=serial + -Os + -H:+RemoveUnusedSymbols + -H:-EnableLoggingFeature + -R:MaxHeapSize=128m + -R:MinHeapSize=32m + + + + + + + diff --git a/backend/src/main/java/com/aiteacher/AiTeacherApplication.java b/backend/src/main/java/com/aiteacher/AiTeacherApplication.java index 679f761..8b8c6c6 100644 --- a/backend/src/main/java/com/aiteacher/AiTeacherApplication.java +++ b/backend/src/main/java/com/aiteacher/AiTeacherApplication.java @@ -1,11 +1,15 @@ package com.aiteacher; +import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableAsync; +import com.aiteacher.config.NativeHintsConfig; + @SpringBootApplication @EnableAsync +@ImportRuntimeHints(NativeHintsConfig.class) public class AiTeacherApplication { public static void main(String[] args) { diff --git a/backend/src/main/java/com/aiteacher/config/NativeHintsConfig.java b/backend/src/main/java/com/aiteacher/config/NativeHintsConfig.java new file mode 100644 index 0000000..613c2de --- /dev/null +++ b/backend/src/main/java/com/aiteacher/config/NativeHintsConfig.java @@ -0,0 +1,76 @@ +package com.aiteacher.config; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeReference; + +/** + * GraalVM native-image runtime hints for third-party libraries that use reflection + * or classpath resource scanning not covered by Spring Boot's AOT processor. + * + * Registered via @ImportRuntimeHints on AiTeacherApplication. + */ +public class NativeHintsConfig implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + // PDFBox — font and encoding resources loaded via classpath scanning at runtime + hints.resources().registerPattern("org/apache/pdfbox/resources/*"); + hints.resources().registerPattern("org/apache/pdfbox/resources/afm/*"); + hints.resources().registerPattern("org/apache/pdfbox/resources/cmap/*"); + hints.resources().registerPattern("org/apache/pdfbox/resources/glyphlist/*"); + hints.resources().registerPattern("org/apache/pdfbox/resources/icc/*"); + hints.resources().registerPattern("org/apache/pdfbox/resources/ttf/*"); + hints.resources().registerPattern("org/apache/pdfbox/resources/version.properties"); + + // PDFBox — font encoding classes instantiated via reflection + hints.reflection().registerType( + org.apache.pdfbox.pdmodel.font.encoding.GlyphList.class, + MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, + MemberCategory.INVOKE_PUBLIC_METHODS + ); + hints.reflection().registerType( + org.apache.pdfbox.pdmodel.font.encoding.WinAnsiEncoding.class, + MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS + ); + hints.reflection().registerType( + org.apache.pdfbox.pdmodel.font.encoding.MacRomanEncoding.class, + MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS + ); + hints.reflection().registerType( + org.apache.pdfbox.pdmodel.font.encoding.MacExpertEncoding.class, + MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS + ); + hints.reflection().registerType( + org.apache.pdfbox.pdmodel.font.encoding.StandardEncoding.class, + MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS + ); + + // JPA / Hibernate — array types used in entity mappings + hints.reflection().registerType(java.util.UUID[].class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); + + // JBoss Logging — message logger implementations generated by annotation processor. + // JBoss Logging uses reflection to look up the generated *_$logger class by name. + registerJBossLogger(hints, "org.hibernate.jpa.internal.JpaLogger_$logger"); + registerJBossLogger(hints, "org.hibernate.internal.CoreMessageLogger_$logger"); + registerJBossLogger(hints, "org.hibernate.internal.EntityManagerMessageLogger_$logger"); + + // AWS SDK v2 — HTTP client and SdkPojo serialization + hints.resources().registerPattern("software/amazon/awssdk/global/handlers/execution.interceptors"); + hints.resources().registerPattern("software/amazon/awssdk/services/s3/execution.interceptors"); + hints.resources().registerPattern("codegen-resources/s3/*"); + hints.reflection().registerType( + software.amazon.awssdk.services.s3.S3Client.class, + MemberCategory.INVOKE_PUBLIC_METHODS + ); + } + + private void registerJBossLogger(RuntimeHints hints, String className) { + hints.reflection().registerType( + TypeReference.of(className), + MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, + MemberCategory.INVOKE_PUBLIC_METHODS + ); + } +} diff --git a/backend/src/main/resources/application.yaml b/backend/src/main/resources/application.yaml index 236ed95..b2113a1 100644 --- a/backend/src/main/resources/application.yaml +++ b/backend/src/main/resources/application.yaml @@ -27,7 +27,7 @@ spring: index-type: HNSW initialize-schema: false openai: - api-key: ${OPENAI_API_KEY} + api-key: ${OPENAI_API_KEY:} chat: options: model: gpt-4o-mini @@ -62,8 +62,8 @@ app: endpoint: https://s3.immich-ad.ovh region: garage bucket: ${S3_BUCKET:aiteacher} - access-key-id: ${S3_ACCESS_KEY_ID} - secret-access-key: ${S3_SECRET_ACCESS_KEY} + access-key-id: ${S3_ACCESS_KEY_ID:} + secret-access-key: ${S3_SECRET_ACCESS_KEY:} min-image-size-px: 100 embedding: batch-size: 20 diff --git a/docker-compose.native.yml b/docker-compose.native.yml new file mode 100644 index 0000000..9a60b70 --- /dev/null +++ b/docker-compose.native.yml @@ -0,0 +1,37 @@ +version: '3.9' + +services: + postgres: + image: pgvector/pgvector:pg16 + container_name: aiteacher-postgres-native + environment: + POSTGRES_DB: aiteacher + POSTGRES_USER: aiteacher + POSTGRES_PASSWORD: aiteacher + ports: + - "5432:5432" + volumes: + - pgdata_native:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U aiteacher -d aiteacher"] + interval: 10s + timeout: 5s + retries: 5 + + backend: + image: ai-teacher-backend:latest + container_name: aiteacher-backend-native + env_file: + - .env + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/aiteacher + SPRING_DATASOURCE_USERNAME: aiteacher + SPRING_DATASOURCE_PASSWORD: aiteacher + ports: + - "8080:8080" + depends_on: + postgres: + condition: service_healthy + +volumes: + pgdata_native: diff --git a/specs/005-native-image-deployment/contracts/build-contract.md b/specs/005-native-image-deployment/contracts/build-contract.md new file mode 100644 index 0000000..5e21faf --- /dev/null +++ b/specs/005-native-image-deployment/contracts/build-contract.md @@ -0,0 +1,47 @@ +# Build Contract: Native Image + +**Branch**: `005-native-image-deployment` | **Date**: 2026-04-07 + +## Overview + +This contract defines what the native build produces and how consumers (developers, CI, docker-compose) +interact with it. The REST API contract is **unchanged** — all existing endpoints remain identical. + +## Build Inputs + +| Input | Required | Description | +|-------|----------|-------------| +| GraalVM JDK 25 | Yes (native profile only) | `JAVA_HOME` must point to GraalVM 25 | +| `DOCKER_HOST` / Docker daemon | Optional | Required for `jib:dockerBuild`; not needed for `jib:build` | +| `jib.to.image` Maven property | Optional | Override target image name; defaults to `ai-teacher-backend` | + +## Build Outputs + +| Profile | Command | Output | +|---------|---------|--------| +| Default (JVM) | `mvn package` | `target/ai-teacher-backend-*.jar` | +| Native | `mvn -Pnative package` | `target/ai-teacher-backend` (native executable) | +| Native + Docker | `mvn -Pnative package jib:dockerBuild` | Local Docker image `ai-teacher-backend:latest` | +| Native + Registry | `mvn -Pnative package jib:build` | Remote Docker image (configured via properties) | + +## Docker Image Contract + +| Property | Value | +|----------|-------| +| Base image | `gcr.io/distroless/base-nossl-debian12` | +| Entrypoint | `/app/ai-teacher-backend` (native executable) | +| Exposed port | `8080` | +| Architecture | `linux/amd64` (matches build host) | +| Required env vars | Same as JVM mode (`SPRING_DATASOURCE_URL`, `OPENAI_API_KEY`, etc.) | + +## REST API Contract + +Unchanged — the native image exposes the same HTTP API as the JVM image: + +- `GET /api/v1/books` +- `POST /api/v1/books` (multipart PDF upload) +- `DELETE /api/v1/books/{id}` +- `POST /api/v1/chat` +- `GET /api/v1/chat/history` + +All endpoints use the same request/response schemas. diff --git a/specs/005-native-image-deployment/data-model.md b/specs/005-native-image-deployment/data-model.md new file mode 100644 index 0000000..2da7c34 --- /dev/null +++ b/specs/005-native-image-deployment/data-model.md @@ -0,0 +1,33 @@ +# Data Model: Native Image Deployment + +**Branch**: `005-native-image-deployment` | **Date**: 2026-04-07 + +## Summary + +This feature introduces **no new data entities**. It is a build and packaging change only. + +The existing data model (books, sections, figures, messages, pgvector embeddings) is unchanged. +All Flyway migrations run identically in native mode as in JVM mode. + +## Impact on Existing Entities + +| Entity | Change | +|--------|--------| +| Books | None | +| Sections | None | +| Figures | None | +| Messages | None | +| pgvector embeddings | None | + +## Configuration Properties (new/changed) + +The following new or updated configuration properties are added for native deployment: + +| Property | Description | Default | +|----------|-------------|---------| +| `jib.to.image` | Target Docker image name/tag | (set at build time) | +| `jib.to.auth.username` | Docker registry username | (CI env var) | +| `jib.to.auth.password` | Docker registry password | (CI env var) | + +All runtime configuration (database URL, OpenAI API key, S3 credentials) remains in +`application.properties` / environment variables — unchanged. diff --git a/specs/005-native-image-deployment/plan.md b/specs/005-native-image-deployment/plan.md new file mode 100644 index 0000000..2620cff --- /dev/null +++ b/specs/005-native-image-deployment/plan.md @@ -0,0 +1,213 @@ +# Implementation Plan: Native Image Deployment + +**Branch**: `005-native-image-deployment` | **Date**: 2026-04-07 | **Spec**: [spec.md](spec.md) +**Input**: Feature specification from `/specs/005-native-image-deployment/spec.md` + +## Summary + +Add a `native` Maven profile to the backend that compiles the Spring Boot 4.0.5 application to a +GraalVM 25 native executable, then packages it into a minimal Docker image via Jib. The JVM build +(default profile + existing Dockerfile) remains unchanged. A companion `docker-compose.native.yml` +enables local full-stack testing with the native image. README.md is updated with build +instructions and an updated architecture diagram. + +## Technical Context + +**Language/Version**: Java 25 (backend), TypeScript / Node 20 (frontend) +**Primary Dependencies**: Spring Boot 4.0.5, Spring AI 2.0.0-M4, `native-maven-plugin` 0.10.6, + `jib-maven-plugin` 3.4.5, `jib-native-image-extension-maven` 0.1.0 +**Storage**: PostgreSQL 16 + pgvector (unchanged) +**Testing**: Spring Boot Test / JUnit 5 (unchanged); native integration test via smoke-test on + produced Docker image +**Target Platform**: Linux x86_64 container (Docker), GraalVM 25 CE or Oracle GraalVM 25 +**Project Type**: Web application — backend API + frontend client (Option 2 structure) +**Performance Goals**: Backend container starts in < 1 s; idle RSS < 150 MB +**Constraints**: Native profile is opt-in; JVM mode unchanged; no new runtime dependencies; + cross-compilation out of scope +**Scale/Scope**: Single backend deployable unit (1 Docker image); frontend unchanged + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +| Principle | Status | Notes | +|-----------|--------|-------| +| I. KISS | ✅ Pass | Build-time change only; no new runtime layers or abstractions. | +| II. Easy to Change | ✅ Pass | `native` profile is additive; default JVM profile unchanged. Native hints isolated in one `NativeHintsConfig` class. | +| III. Web-First Architecture | ✅ Pass | REST API contract unchanged. | +| IV. Documentation as Architecture | ⚠️ Required | README.md MUST be updated with native build instructions and updated Mermaid diagram showing the build pipeline. | +| Technology Constraints | ✅ Pass | Still 1 backend + 1 frontend deployable unit; no new services. | + +**Post-design re-check**: After Phase 1 — Principle IV gate requires README update in same PR. + +## Project Structure + +### Documentation (this feature) + +```text +specs/005-native-image-deployment/ +├── plan.md # This file +├── spec.md # Feature specification +├── research.md # Phase 0 — toolchain & compatibility decisions +├── data-model.md # Phase 1 — no new entities (confirmed) +├── quickstart.md # Phase 1 — developer build guide +├── contracts/ +│ └── build-contract.md # Build inputs/outputs & unchanged REST API +└── tasks.md # Phase 2 output (/speckit.tasks — NOT created here) +``` + +### Source Code (repository root) + +```text +backend/ +├── pom.xml # +native profile, +Jib plugin, +NativeHintsConfig +├── src/ +│ └── main/ +│ └── java/com/aiteacher/ +│ └── config/ +│ └── NativeHintsConfig.java # NEW — RuntimeHints for PDFBox, AWS SDK gaps +└── src/ + └── main/ + └── resources/ + └── META-INF/ + └── native-image/ # Optional: manual JSON hints if AOT gaps remain + +docker-compose.yml # UNCHANGED (JVM mode) +docker-compose.native.yml # NEW — native image + postgres full stack +README.md # UPDATED — build instructions + Mermaid diagram +``` + +**Structure Decision**: Follows Option 2 (web application). Only backend is modified; frontend +is untouched. New files are minimal: one Java config class, one docker-compose file. + +## Complexity Tracking + +> No constitution violations. No entry required. + +--- + +## Implementation Phases + +### Phase 1: Maven `native` Profile + Native Maven Plugin + +**Goal**: `mvn -Pnative package` produces a native executable `target/ai-teacher-backend`. + +**Tasks**: + +1. Add `` to `backend/pom.xml`: + - Wire `spring-boot-maven-plugin` `process-aot` goal to `prepare-package` phase. + - Add `native-maven-plugin` 0.10.6 with `add-reachability-metadata` and `compile-no-fork` + executions. + - Set `ai-teacher-backend` and add essential build args: + `-H:+ReportExceptionStackTraces`, `--initialize-at-build-time=org.slf4j`. + +2. Create `backend/src/main/java/com/aiteacher/config/NativeHintsConfig.java`: + - Implements `RuntimeHintsRegistrar`. + - Registers PDFBox classpath resources: `org/apache/pdfbox/resources/**`. + - Registers PDFBox reflection entries for font/encoding classes. + - Registers AWS SDK `SdkPojo` subtypes used by S3 (discovered iteratively). + - Annotate main application class or a `@Configuration` class with + `@ImportRuntimeHints(NativeHintsConfig.class)`. + +3. Validate: run `mvn -Pnative package -DskipTests` and confirm executable is produced. + +**Acceptance**: `target/ai-teacher-backend` exists, `./target/ai-teacher-backend --help` +exits cleanly, basic startup reaches DB connection stage. + +--- + +### Phase 2: Jib Plugin — Package Native Image into Docker + +**Goal**: `mvn -Pnative package jib:dockerBuild` produces a local Docker image. + +**Tasks**: + +1. Add `jib-maven-plugin` 3.4.5 to the `` section (outside native profile, + but configured to package the native executable when the profile is active): + - Add `jib-native-image-extension-maven` 0.1.0 as a plugin dependency. + - Configure `gcr.io/distroless/base-nossl-debian12`. + - Set `ai-teacher-backend` (overridable via `-Djib.to.image`). + - Add `` pointing to `JibNativeImageExtension`. + - Set `8080`. + +2. Verify Jib picks up the native executable from `target/ai-teacher-backend` and sets it + as entrypoint. + +3. Run `mvn -Pnative package jib:dockerBuild` and confirm: + - Image exists in local Docker: `docker images | grep ai-teacher-backend`. + - Container starts: `docker run --rm -e SPRING_DATASOURCE_URL=... ai-teacher-backend`. + +**Acceptance**: Image starts, logs show "Started AiTeacherApplication in < 1.0 seconds", +and `GET /api/v1/books` returns 200 (with DB connected). + +--- + +### Phase 3: Integration Smoke Test + +**Goal**: All existing features work in native mode. + +**Tasks**: + +1. Start PostgreSQL locally (or via existing docker-compose.yml). +2. Start the native container with full environment variables. +3. Verify: + - Flyway migrations run successfully at startup. + - `POST /api/v1/books` (PDF upload + embedding) succeeds. + - `POST /api/v1/chat` (RAG chat) returns a non-empty answer. + - Spring Security HTTP Basic auth is enforced. + +4. Fix any `MissingResourceException` or `ClassNotFoundException` by adding entries to + `NativeHintsConfig` and rebuilding. + +**Acceptance**: All 5 REST API endpoints respond correctly with native image (FR-005). + +--- + +### Phase 4: Docker Compose for Native Stack + +**Goal**: `docker compose -f docker-compose.native.yml up` starts the full stack. + +**Tasks**: + +1. Create `docker-compose.native.yml` at repo root: + - `postgres` service: same as `docker-compose.yml` (copy). + - `backend` service: uses `ai-teacher-backend:latest` (the Jib-built native image). + - Wires all required env vars via `.env` file reference or inline secrets. + - Adds `depends_on` with health check for postgres. + +2. Document the `.env` file format in `quickstart.md`. + +**Acceptance**: `docker compose -f docker-compose.native.yml up` → all services healthy → +app responds at `http://localhost:8080/api/v1/books`. + +--- + +### Phase 5: README Update (Constitution IV Gate) + +**Goal**: README.md reflects the new build pipeline and updated deployment diagram. + +**Tasks**: + +1. Add "Native Image Build" section to README.md with: + - Prerequisite: GraalVM 25 install instructions (sdkman command). + - Build command: `mvn -Pnative package jib:dockerBuild`. + - Run command: `docker compose -f docker-compose.native.yml up`. + +2. Update the Mermaid architecture diagram in README.md to show: + - Build pipeline: Maven native profile → GraalVM native-image → Jib → Docker image. + - Keep the runtime diagram unchanged (same components, just a lighter container). + +**Acceptance**: README.md Mermaid diagram renders correctly on GitHub; build section +contains all commands from `quickstart.md`. + +--- + +## Risk Log + +| Risk | Likelihood | Mitigation | +|------|------------|------------| +| Spring AI 2.0.0-M4 missing native hints for some AI response types | Medium | Iterative hint discovery during Phase 3 smoke test | +| PDFBox font loading fails at runtime in native | Medium | Register full `org/apache/pdfbox/resources/**` in `NativeHintsConfig` | +| AWS SDK S3 reflection errors | Medium | Switch to `url-connection-client` (simpler); lazy-init S3Client | +| GraalVM 25 availability in CI | Low | Documented prerequisite; CI pipeline config out of scope (can be added as follow-up) | +| Native build time > 5 min | Low | Acceptable for a first build; not a blocker for POC | diff --git a/specs/005-native-image-deployment/quickstart.md b/specs/005-native-image-deployment/quickstart.md new file mode 100644 index 0000000..02d8b8a --- /dev/null +++ b/specs/005-native-image-deployment/quickstart.md @@ -0,0 +1,81 @@ +# Quickstart: Native Image Build & Deploy + +**Branch**: `005-native-image-deployment` | **Date**: 2026-04-07 + +## Prerequisites + +- GraalVM JDK 25 CE or Oracle GraalVM 25 installed and set as `JAVA_HOME` +- Docker daemon running (for `jib:dockerBuild`) +- Maven 3.9+ + +```bash +# Install GraalVM 25 CE via sdkman +sdk install java 25-graalce +sdk use java 25-graalce + +# Verify +java -version # should show GraalVM 25 +native-image --version # should show GraalVM 25 +``` + +## Build Native Docker Image (local) + +```bash +cd backend + +# Build native executable AND package into local Docker image +mvn -Pnative package jib:dockerBuild + +# The image is now available locally +docker images | grep ai-teacher-backend +``` + +## Run the Full Stack (native) + +```bash +# From repo root — starts PostgreSQL + native backend +docker compose -f docker-compose.native.yml up +``` + +Access the app at `http://localhost:8080`. + +## Build and Push to Registry (CI) + +```bash +mvn -Pnative package jib:build \ + -Djib.to.image=ghcr.io/your-org/ai-teacher-backend:native-latest \ + -Djib.to.auth.username=$REGISTRY_USER \ + -Djib.to.auth.password=$REGISTRY_TOKEN +``` + +## JVM Build (unchanged) + +```bash +# Default profile — no GraalVM required +cd backend +mvn package -DskipTests + +java -jar target/ai-teacher-backend-*.jar +``` + +## Environment Variables (both JVM and native) + +| Variable | Description | +|----------|-------------| +| `SPRING_DATASOURCE_URL` | PostgreSQL JDBC URL | +| `SPRING_DATASOURCE_USERNAME` | DB user | +| `SPRING_DATASOURCE_PASSWORD` | DB password | +| `OPENAI_API_KEY` | OpenAI API key | +| `AWS_ACCESS_KEY_ID` | S3 access key (if S3 storage enabled) | +| `AWS_SECRET_ACCESS_KEY` | S3 secret key | +| `AWS_REGION` | S3 region | + +## Troubleshooting + +**Build fails with "ImageGenerationFailed"**: Ensure `native-image` is on `PATH` and +`JAVA_HOME` points to GraalVM 25, not a regular JDK. + +**Missing resource at runtime**: Add the resource pattern to `NativeHintsConfig` and rebuild. + +**ClassNotFoundException at runtime**: Register the class in `NativeHintsConfig` with +`MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS` and rebuild. diff --git a/specs/005-native-image-deployment/research.md b/specs/005-native-image-deployment/research.md new file mode 100644 index 0000000..ced9e7f --- /dev/null +++ b/specs/005-native-image-deployment/research.md @@ -0,0 +1,262 @@ +# Research: Native Image Deployment + +**Branch**: `005-native-image-deployment` | **Date**: 2026-04-07 + +## Decision Log + +--- + +### 1. GraalVM Version and Java 25 Support + +**Decision**: Use GraalVM JDK 25 (Oracle GraalVM or GraalVM CE 25). + +**Rationale**: Oracle GraalVM 25 ships with Java 25 support and includes the +`native-image` tool. GraalVM CE 25 is available via sdkman (`sdk install java 25-graalce`). +Both support the `native-maven-plugin` 0.10.x workflow. + +**Alternatives considered**: +- GraalVM 21 with Java 21: would require downgrading `java.version` in pom.xml — rejected + because pom.xml already targets Java 25. +- Mandrel (Red Hat distribution): Red Hat Mandrel 25 may lag behind Oracle GraalVM 25 release; + acceptable fallback but Oracle GraalVM 25 is preferred. + +--- + +### 2. Native Image Build Toolchain + +**Decision**: Use `native-maven-plugin` (0.10.x) inside a Maven `native` profile, combined +with Spring Boot's AOT processing via `spring-boot-maven-plugin` `process-aot` execution. + +**Rationale**: This is the canonical Spring Boot 4.x native image approach: +1. `mvn -Pnative package` runs AOT → generates source under `target/spring-aot/` +2. `native:compile` compiles the AOT-generated sources + app into a native executable. + +The `native-maven-plugin` coordinates with GraalVM `native-image` CLI automatically. + +**Alternatives considered**: +- Paketo Buildpacks (`spring-boot:build-image -Pnative`): does not use Jib; rejected because + the user explicitly wants Jib. +- Manual `native-image` CLI: fragile, no Maven lifecycle integration — rejected. + +--- + +### 3. Jib + Native Image Integration + +**Decision**: Use `jib-maven-plugin` (3.4.x) with the `jib-native-image-extension-maven` +extension (0.1.0). + +**Rationale**: The Jib native image extension packages a pre-built native executable into a +Docker image without requiring a Docker daemon. The workflow is: + +``` +mvn -Pnative native:compile # step 1: build native executable +mvn -Pnative jib:dockerBuild # step 2: package into Docker image +``` + +Or combined (with `process-aot` wired in `native` profile): + +``` +mvn -Pnative package jib:dockerBuild +``` + +Jib uses the extension to locate the native executable under `target/` and set it as the +container entrypoint. + +**Key configuration**: +```xml + + com.google.cloud.tools + jib-maven-plugin + 3.4.5 + + + + gcr.io/distroless/base-nossl-debian12 + + + + + com.google.cloud.tools.jib.maven.extension.nativeimage.JibNativeImageExtension + + + + + + + com.google.cloud.tools + jib-native-image-extension-maven + 0.1.0 + + + +``` + +**Alternatives considered**: +- Custom Dockerfile for native image: more control but manual, diverges from Jib workflow — + rejected in favor of Jib as specified. +- `jib:build` (push to registry) vs `jib:dockerBuild` (local daemon): default to + `jib:dockerBuild` for dev; `jib:build` for CI with registry configured. + +--- + +### 4. Docker Base Image for Native Executable + +**Decision**: `gcr.io/distroless/base-nossl-debian12` (Debian 12 glibc variant). + +**Rationale**: GraalVM native images compiled on Linux link against glibc by default. +Distroless provides glibc without a shell, minimizing attack surface. + +Important: the native executable compiled on the host must target the same glibc ABI as the +base image. Since the build is on Linux x86_64 this matches Debian 12's glibc 2.36. + +**Alternatives considered**: +- `alpine` (musl libc): requires `--static` or `--libc=musl` native-image flag; more complex — + rejected for simplicity (KISS). +- `gcr.io/distroless/java-base`: includes JVM libs we don't need — rejected (pure native). +- `scratch`: too minimal; requires fully static binary with all libs included — rejected. + +--- + +### 5. Spring AI 2.0.0-M4 Native Image Compatibility + +**Decision**: Rely on Spring AI's built-in AOT hints; add manual `RuntimeHints` only for gaps +found during build. + +**Rationale**: Spring AI 2.0.0-M4 includes `@NativeHint` registrations for the OpenAI +integration (HTTP client, response DTOs) and pgvector store. Spring Boot's AOT processor +picks these up automatically. M4 milestone has known native image improvements. + +**Known gaps** (require manual RuntimeHints): +- `spring-ai-pdf-document-reader`: PDFBox font/resource loading uses `Class.forName()` and + classpath resource scanning. Need to register font resources and PDFBox parser classes. +- `spring-ai-advisors-vector-store`: generally AOT-compatible via Spring AI hints. + +**Approach**: Add a `NativeHintsConfig` class annotated with `@ImportRuntimeHints` that +registers PDFBox fonts and any other gaps found during a `-Ob` (quick) native build. + +--- + +### 6. PDFBox Reflection Hints + +**Decision**: Register PDFBox font resources and parser classes via a `RuntimeHintsRegistrar`. + +**Rationale**: PDFBox 3.x loads fonts from classpath resources (`/org/apache/pdfbox/resources/`) +and uses reflection for CMap/encoding classes. Without hints the native build fails at runtime +with `ClassNotFoundException` or missing resource errors. + +**Minimum hints needed**: +```java +hints.resources().registerPattern("org/apache/pdfbox/resources/**"); +hints.reflection().registerType(org.apache.pdfbox.pdmodel.font.encoding.GlyphList.class, + MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); +// Additional classes as discovered during smoke tests +``` + +--- + +### 7. AWS SDK v2 Native Image Support + +**Decision**: Add `software.amazon.awssdk:aws-crt-client` is NOT needed; instead add +`software.amazon.awssdk:netty-nio-client` or use URL connection client. +Add AWS SDK v2 native image support via `aws-sdk-java-v2-native-bridge` if available, +otherwise register reflection manually. + +**Rationale**: AWS SDK v2 2.30.x provides native image support via: +- `software.amazon.awssdk:apache-client` or `url-connection-client` (simpler, fewer reflection + needs than netty) +- The SDK's `SdkPojo` interfaces generate reflection entries; Spring Boot AOT may not cover all. + +**Minimum approach**: Switch S3 HTTP client from default (netty) to `url-connection-client` +for simplicity in native mode, register `SdkPojo` subtypes via `RuntimeHints`. + +**Alternative**: If S3 is used infrequently and only for figure uploads, mark S3Client as a +conditional bean initialized lazily — reflection issues then surface only on first use (not +startup), making native hints easier to discover iteratively. + +--- + +### 8. Flyway in Native Mode + +**Decision**: No special configuration needed; Flyway 10.x has native image support. + +**Rationale**: Spring Boot 4.x AOT includes Flyway native hints. PostgreSQL JDBC driver +and Flyway 10 have been tested with GraalVM native. No manual hints required. + +--- + +### 9. Spring Security in Native Mode + +**Decision**: No special configuration needed. + +**Rationale**: Spring Security 7.x (bundled with Spring Boot 4.x) includes AOT-compatible +configuration. HTTP Basic auth does not require dynamic proxies at runtime. + +--- + +### 10. Build Command Summary + +```bash +# Install GraalVM 25 +sdk install java 25-graalce + +# Build native executable + Docker image (local daemon) +cd backend +mvn -Pnative package jib:dockerBuild + +# Or push to registry (CI) +mvn -Pnative package jib:build \ + -Djib.to.image=ghcr.io/your-org/ai-teacher-backend:native-latest \ + -Djib.to.auth.username=$CI_USER \ + -Djib.to.auth.password=$CI_TOKEN +``` + +--- + +### 11. Maven Profile Structure + +```xml + + + native + + + + + org.springframework.boot + spring-boot-maven-plugin + + + process-aot + process-aot + + + + + + org.graalvm.buildtools + native-maven-plugin + 0.10.6 + + + add-reachability-metadata + add-reachability-metadata + + + compile + compile-no-fork + package + + + + ai-teacher-backend + + --initialize-at-build-time=org.slf4j + -H:+ReportExceptionStackTraces + + + + + + + +``` diff --git a/specs/005-native-image-deployment/spec.md b/specs/005-native-image-deployment/spec.md new file mode 100644 index 0000000..71fefbe --- /dev/null +++ b/specs/005-native-image-deployment/spec.md @@ -0,0 +1,138 @@ +# Feature Specification: Native Image Deployment + +**Feature Branch**: `005-native-image-deployment` +**Created**: 2026-04-07 +**Status**: Draft +**Input**: Prepare the application for deployment using GraalVM native image for the backend, built into a Docker image via Jib. + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Build Native Docker Image (Priority: P1) + +A developer or CI pipeline runs a single Maven command to produce a Docker image containing the +native-compiled backend binary. The image starts in under one second and uses significantly less +memory than the JVM fat-jar image. + +**Why this priority**: This is the core deliverable — without a working native Docker image the +entire feature has no value. + +**Independent Test**: Run `mvn -Pnative jib:dockerBuild` (or equivalent), start the resulting +container, hit `GET /api/v1/books`, and receive a valid response. Startup log must show the server +is ready in under 1 second. + +**Acceptance Scenarios**: + +1. **Given** a clean checkout with GraalVM installed, **When** the developer runs the native build + command, **Then** a Docker image is produced without manual steps. +2. **Given** the produced Docker image, **When** the container is started with the required + environment variables, **Then** the backend starts, connects to PostgreSQL, and serves HTTP + requests correctly. +3. **Given** the produced Docker image, **When** a RAG chat request is made, **Then** it responds + correctly (OpenAI call, pgvector retrieval, and Flyway migrations all work in native mode). + +--- + +### User Story 2 - JVM Mode Preserved (Priority: P2) + +A developer who has not installed GraalVM can still build and run the backend the same way as +today (fat-jar / existing Dockerfile), without any change to their workflow. + +**Why this priority**: Preserving the developer experience for non-native builds ensures the team +can keep iterating without a GraalVM prerequisite. + +**Independent Test**: Run `mvn package -DskipTests` and start the resulting jar with `java -jar` — +the application starts and serves requests identically to before this feature. + +**Acceptance Scenarios**: + +1. **Given** a standard JDK (no GraalVM), **When** `mvn package` is run, **Then** a fat-jar is + produced and runs normally. +2. **Given** the existing Dockerfile, **When** `docker build` is run, **Then** a JVM-mode image + is produced and works exactly as before. + +--- + +### User Story 3 - Docker Compose Full Stack (Priority: P3) + +A developer can bring up the complete stack (PostgreSQL + native backend) with a single +`docker compose up` command for local integration testing. + +**Why this priority**: Makes it easy to validate the native image against the real database before +pushing to any environment. + +**Independent Test**: Run `docker compose up` (with native image tag), wait for healthy status, +make a RAG request — all services communicate correctly. + +**Acceptance Scenarios**: + +1. **Given** the docker-compose.yml, **When** `docker compose up` is run with the native backend + image, **Then** all services start, health checks pass, and the application is reachable. + +--- + +### Edge Cases + +- What happens when a reflection-heavy library (PDFBox, AWS SDK) is called at runtime without + proper native hints? → Must be caught at build time via native-image test or detected early + via integration smoke test. +- What if GraalVM is not installed in CI? → Build must fail with a clear error message, not + silently produce a JVM image. +- What if an environment variable required at runtime is missing? → Container must fail fast with + a meaningful error (not a NullPointerException from missing reflection). + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: The project MUST provide a `native` Maven profile that compiles the backend to a + standalone native executable using GraalVM. +- **FR-002**: The native profile MUST include Spring Boot AOT processing to generate the + reflection, serialization, and proxy hints required at compile time. +- **FR-003**: The Jib Maven plugin MUST be configured to package the native executable into a + Docker image without requiring Docker daemon access during the build. +- **FR-004**: The resulting Docker image MUST start the backend in under 1 second on modern + hardware (2+ CPU cores, 2 GB RAM). +- **FR-005**: All existing REST endpoints MUST function correctly in native mode (book upload, + RAG chat, Flyway migrations, pgvector retrieval, Spring Security). +- **FR-006**: The JVM fat-jar build (default profile) MUST continue to work unchanged. +- **FR-007**: The README.md MUST be updated with native build instructions and updated + architecture/deployment diagram. +- **FR-008**: The `docker-compose.yml` MUST be updated (or a companion file added) to support + running the native Docker image alongside PostgreSQL. + +### Key Entities + +- **Native Profile**: Maven build profile (`native`) that activates AOT processing and + GraalVM native compilation. +- **Docker Image**: OCI image produced by Jib, containing the native executable and a minimal + base OS layer. +- **AOT Hints**: Compile-time metadata (reflection, serialization, proxy) that replace runtime + reflection for third-party libraries. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: The native Docker image starts and serves its first HTTP request in under 1 second + on a machine with 2+ CPU cores and 2 GB RAM. +- **SC-002**: The native container's idle memory footprint is at least 40% lower than the + equivalent JVM container running the same application. +- **SC-003**: All existing API endpoints return correct responses in native mode (0 regressions). +- **SC-004**: A developer with GraalVM installed can build the native Docker image with a single + command documented in README.md. +- **SC-005**: The default JVM build and Dockerfile continue to work without any changes to the + developer workflow. + +## Assumptions + +- GraalVM 25 (CE or Oracle) will be available in CI and on developer machines that want to build + native images. +- The native build is performed on Linux x86_64; cross-compilation for other architectures is out + of scope for this feature. +- Spring Boot 4.0.5 and Spring AI 2.0.0-M4 have sufficient native image support for the features + used (OpenAI chat/embedding, pgvector, Flyway, Spring Security, Spring Data JPA). +- The Docker registry target and image name/tag will be configured via Maven properties or + environment variables, not hardcoded. +- Frontend deployment is out of scope for this feature; only the backend native image is addressed. +- The existing Dockerfile is kept as a fallback JVM image build; the Jib-built native image is + the new primary artifact. diff --git a/specs/005-native-image-deployment/tasks.md b/specs/005-native-image-deployment/tasks.md new file mode 100644 index 0000000..3dd2982 --- /dev/null +++ b/specs/005-native-image-deployment/tasks.md @@ -0,0 +1,189 @@ +# Tasks: Native Image Deployment + +**Input**: Design documents from `/specs/005-native-image-deployment/` +**Prerequisites**: plan.md ✅, spec.md ✅, research.md ✅, data-model.md ✅, contracts/ ✅ + +**Tests**: No test tasks — this feature has no unit/integration test requirements in the spec. +Smoke testing is part of US1 implementation (T010, T011). + +**Organization**: Tasks grouped by user story for independent implementation and verification. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no incomplete-task dependencies) +- **[Story]**: Which user story this task belongs to (US1, US2, US3) +- Paths use `backend/` and repo root (web app structure per constitution) + +--- + +## Phase 1: Setup (Shared Infrastructure) + +**Purpose**: Verify prerequisites and baseline before touching any build files. + +- [ ] T001 Verify GraalVM 25 CE or Oracle GraalVM 25 is installed: run `native-image --version` and confirm output shows GraalVM 25; document install command (`sdk install java 25-graalce`) if missing +- [ ] T002 [P] Verify baseline JVM build passes: run `mvn package -DskipTests` inside `backend/` and confirm `target/ai-teacher-backend-*.jar` is produced + +**Checkpoint**: GraalVM available, existing JVM build green — safe to modify build files + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Add AOT processing and native compilation support to the Maven build. These changes +are prerequisites for all user stories. + +**⚠️ CRITICAL**: US1 cannot start until T007 confirms the native executable is produced. + +- [ ] T003 Add `` skeleton to `backend/pom.xml` and wire `spring-boot-maven-plugin` `process-aot` goal to the `prepare-package` phase inside that profile +- [ ] T004 Add `native-maven-plugin` 0.10.6 to the `native` profile in `backend/pom.xml` with executions `add-reachability-metadata` and `compile-no-fork` (phase `package`), image name `ai-teacher-backend`, and build args `--initialize-at-build-time=org.slf4j` and `-H:+ReportExceptionStackTraces` +- [ ] T005 [P] Create `backend/src/main/java/com/aiteacher/config/NativeHintsConfig.java` implementing `RuntimeHintsRegistrar`: register resource pattern `org/apache/pdfbox/resources/**` and reflection for `org.apache.pdfbox.pdmodel.font.encoding.GlyphList` with `MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS` +- [ ] T006 Add `@ImportRuntimeHints(NativeHintsConfig.class)` annotation to the main `@SpringBootApplication` class in `backend/src/main/java/com/aiteacher/` +- [ ] T007 Run `mvn -Pnative package -DskipTests` inside `backend/` and confirm `target/ai-teacher-backend` native executable is produced; fix any AOT compilation errors before proceeding + +**Checkpoint**: `target/ai-teacher-backend` exists and exits cleanly — US1 implementation can begin + +--- + +## Phase 3: User Story 1 — Build Native Docker Image (Priority: P1) 🎯 MVP + +**Goal**: One command (`mvn -Pnative package jib:dockerBuild`) produces a local Docker image +containing the native backend. Container starts in < 1 s and all REST endpoints work. + +**Independent Test**: Start native container with DB env vars → `GET /api/v1/books` returns 200 → +startup log shows ready in under 1 second. + +### Implementation for User Story 1 + +- [ ] T008 [US1] Add `jib-maven-plugin` 3.4.5 to `backend/pom.xml` `` section (default build, not inside `native` profile): configure `gcr.io/distroless/base-nossl-debian12`, `ai-teacher-backend`, exposed port `8080`, and `` referencing `com.google.cloud.tools.jib.maven.extension.nativeimage.JibNativeImageExtension`; add `jib-native-image-extension-maven` 0.1.0 as plugin `` +- [ ] T009 [US1] Run `mvn -Pnative package jib:dockerBuild -DskipTests` inside `backend/` and confirm `docker images | grep ai-teacher-backend` shows the produced image +- [ ] T010 [US1] Start the native container with required env vars (`SPRING_DATASOURCE_URL`, `SPRING_DATASOURCE_USERNAME`, `SPRING_DATASOURCE_PASSWORD`, `OPENAI_API_KEY`) pointing to a local PostgreSQL; verify startup log shows "Started … in < 1.0 seconds" and `GET /api/v1/books` returns HTTP 200 +- [ ] T011 [US1] Run full smoke test against the running native container: `POST /api/v1/books` (PDF upload), `POST /api/v1/chat` (RAG query), `GET /api/v1/chat/history`, and HTTP Basic auth enforcement; for each `MissingResourceException` or `ClassNotFoundException` found, add the corresponding entry to `backend/src/main/java/com/aiteacher/config/NativeHintsConfig.java` and rebuild (T009 → T010 loop) until all pass + +**Checkpoint**: Native Docker image starts < 1 s, all 5 endpoints work — US1 complete ✅ + +--- + +## Phase 4: User Story 2 — JVM Mode Preserved (Priority: P2) + +**Goal**: Default Maven build and existing Dockerfile continue to work with no changes required +from developers who do not have GraalVM installed. + +**Independent Test**: `mvn package -DskipTests` produces a fat-jar; `docker build` using +`backend/Dockerfile` produces a working JVM container. + +### Implementation for User Story 2 + +- [ ] T012 [P] [US2] Verify `mvn package -DskipTests` (no `-Pnative`) inside `backend/` still produces `target/ai-teacher-backend-*.jar`; confirm the jar starts correctly with `java -jar` +- [ ] T013 [P] [US2] Verify `docker build -t ai-teacher-backend-jvm backend/` using the existing `backend/Dockerfile` succeeds and the resulting JVM container starts and serves `GET /api/v1/books` correctly + +**Checkpoint**: JVM path fully unchanged — US2 complete ✅ + +--- + +## Phase 5: User Story 3 — Docker Compose Full Stack (Priority: P3) + +**Goal**: `docker compose -f docker-compose.native.yml up` starts PostgreSQL + native backend +together so developers can run the complete native stack locally with a single command. + +**Independent Test**: `docker compose -f docker-compose.native.yml up` → all services healthy → +`GET http://localhost:8080/api/v1/books` returns 200. + +### Implementation for User Story 3 + +- [ ] T014 [US3] Create `docker-compose.native.yml` at repo root: include `postgres` service (same image and config as `docker-compose.yml`), add `backend` service using image `ai-teacher-backend:latest` with all required env vars sourced from `.env`, `depends_on` postgres with health-check condition, and port mapping `8080:8080` +- [ ] T015 [P] [US3] Create `.env.example` at repo root listing all env vars needed by the native stack (`SPRING_DATASOURCE_URL`, `SPRING_DATASOURCE_USERNAME`, `SPRING_DATASOURCE_PASSWORD`, `OPENAI_API_KEY`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`) with placeholder values and inline comments + +**Checkpoint**: Full native stack starts via docker compose — US3 complete ✅ + +--- + +## Phase 6: Polish & Cross-Cutting Concerns + +**Purpose**: Constitution IV gate (README must be updated in the same PR as architectural change) +and final documentation alignment. + +- [ ] T016 Update `README.md` at repo root: add a "Native Image Build" section with GraalVM install command (`sdk install java 25-graalce`), build command (`mvn -Pnative package jib:dockerBuild`), and run command (`docker compose -f docker-compose.native.yml up`); update the Mermaid architecture diagram to show the native build pipeline (Maven native profile → GraalVM native-image → Jib → Docker image) +- [ ] T017 [P] Update `specs/005-native-image-deployment/quickstart.md` with any corrections or additions discovered during implementation (env var names, exact commands, troubleshooting entries) + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Setup (Phase 1)**: No dependencies — start immediately +- **Foundational (Phase 2)**: Requires Phase 1 completion — **BLOCKS all user stories** +- **US1 (Phase 3)**: Requires Phase 2 (T007 must pass) — primary deliverable +- **US2 (Phase 4)**: Requires Phase 2 (T003–T006 must not break JVM path) — can run in parallel with US1 after Phase 2 +- **US3 (Phase 5)**: Requires US1 completion (T009 must produce the Docker image) +- **Polish (Phase 6)**: Requires US1, US2, US3 all complete + +### User Story Dependencies + +- **US1 (P1)**: Unblocked after Foundational — critical path +- **US2 (P2)**: Unblocked after Foundational — can run in parallel with US1 (different verification commands, no file conflicts) +- **US3 (P3)**: Depends on US1 (needs the Docker image to exist for docker-compose) + +### Within US1 + +T008 (Jib config) → T009 (build image) → T010 (verify startup) → T011 (smoke test + hint fixes, may loop back to T009) + +### Parallel Opportunities + +- T001 and T002 (Phase 1): both can run in parallel +- T003/T004 (pom.xml edits) are sequential (same file); T005 (new Java file) can run in parallel with T003/T004 +- T012 and T013 (US2 verification): parallel, different commands +- T014 and T015 (US3 docker-compose + .env.example): parallel, different files +- T016 and T017 (Polish): parallel, different files +- US1 (Phase 3) and US2 (Phase 4) can run in parallel after Foundational completes + +--- + +## Parallel Example: Foundational Phase + +```bash +# In parallel — different files, no dependencies between them: +Task T005: "Create NativeHintsConfig.java in backend/src/main/java/com/aiteacher/config/" + +# Sequential — same file (pom.xml): +Task T003: "Add native profile skeleton with spring-boot-maven-plugin process-aot" +Task T004: "Add native-maven-plugin 0.10.6 to native profile" # after T003 +``` + +## Parallel Example: After Foundational Completes + +```bash +# US1 and US2 can start simultaneously: +Developer A → Phase 3 (US1): T008 → T009 → T010 → T011 +Developer B → Phase 4 (US2): T012, T013 (parallel) +``` + +--- + +## Implementation Strategy + +### MVP First (User Story 1 Only) + +1. Complete Phase 1: Setup (T001, T002) +2. Complete Phase 2: Foundational (T003–T007) — **CRITICAL, do not skip T007** +3. Complete Phase 3: US1 (T008–T011) +4. **STOP and VALIDATE**: Native image starts < 1 s, all endpoints work +5. This alone satisfies FR-001 through FR-006 and SC-001 through SC-003 + +### Incremental Delivery + +1. Phase 1 + Phase 2 → build toolchain ready +2. Phase 3 (US1) → native Docker image working (**MVP**) +3. Phase 4 (US2) → JVM fallback confirmed +4. Phase 5 (US3) → full native stack via docker compose +5. Phase 6 (Polish) → README updated (constitution IV gate — **required before merge**) + +--- + +## Notes + +- [P] tasks operate on different files or independent commands — safe to run concurrently +- US2 (T012, T013) can be verified at any time after Phase 2; they are quick sanity checks +- The hint-fix loop in T011 is the most likely source of iteration — budget extra time +- Constitution IV gate (README update, T016) is **mandatory** — PR cannot merge without it +- `.env.example` (T015) should be committed; actual `.env` must be in `.gitignore`