This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Artemis is an interactive learning platform for programming exercises, quizzes, modeling tasks, and exams with automatic and manual assessment. It integrates with AI services (Iris for virtual tutoring, Athena for automated assessment, Hyperion for exercise creation).
- Server: Spring Boot 4.1 (Java 25), MySQL, Hibernate, Hazelcast
- Client: Angular 22, TypeScript, SCSS
- Build: Gradle 9.6, pnpm 11 / Node 24 (pnpm version pinned via the
packageManagerfield in package.json; activate withcorepack enable) - Testing: JUnit 6, Vitest, Playwright
./gradlew bootRun # Start dev server (includes Angular build)
./gradlew bootRun -x webapp # Server only (use with pnpm start)
./gradlew -Pprod -Pwar clean bootWar # Production WAR (no SBOM, fast)
./gradlew -Pprod -Pwar -Psbom clean bootWar # Production WAR including server + client SBOMSBOM generation (cyclonedxBom + generateClientSbom) is gated behind the -Psbom Gradle property. CI release-eligible jobs (pushes to develop/main/release/*, version tags, and published releases) set it automatically in .github/workflows/ci-build.yml. Local builds and PR CI ship a WAR without the SBOM — AdminSbomResource returns 404 and the admin UI renders an informational banner in that case.
corepack enable # One-time: activate the pnpm version pinned in package.json
pnpm install --frozen-lockfile # Install dependencies (CI-style, asserts lockfile is authoritative)
pnpm install # Install + allow lockfile updates (for dependency changes)
pnpm start # Angular dev server with HMR (runs prebuild + ng serve)
pnpm run webapp:build # Development build
pnpm run webapp:prod # Production build
pnpm run build # Alternative production build- Client assets:
build/resources/main/static - Production WAR:
build/libs/Artemis-<version>.war
./gradlew spotlessCheck # Check Java formatting
./gradlew spotlessApply # Fix Java formatting
./gradlew checkstyleMain # Java linting
./gradlew modernizer # Check for legacy API usage
pnpm run lint # ESLint
pnpm run lint:fix # Fix ESLint issues
pnpm run stylelint # SCSS linting
pnpm run prettier:check # Check formatting
pnpm run prettier:write # Fix formatting# Server (requires Docker — tests run against PostgreSQL via Testcontainers by default)
./gradlew test -x webapp # All server tests (PostgreSQL)
./gradlew test --tests ExamIntegrationTest -x webapp # Single test class
./gradlew test --tests ExamIntegrationTest.testGetExamScore # Single test method
# Client (Vitest - preferred for new tests)
pnpm run vitest # Watch mode
pnpm run vitest:run # Single run
pnpm run vitest:coverage # With coverage
pnpm run vitest -- path/to/spec.ts # Single Vitest file
# E2E Tests (Playwright) — preferred way to run locally
# The script auto-kills processes on ports 8080/9000/7921, starts Postgres, server, and client.
./run-e2e-tests-local-fast.sh # Run all E2E tests
./run-e2e-tests-local-fast.sh --filter "Quiz" # Run tests matching "Quiz"
./run-e2e-tests-local-fast.sh --filter "ExamAssessment|SystemHealth" # Multiple patterns
./run-e2e-tests-local-fast.sh --stop # Stop all services
# Multi-node E2E (catches cluster / cache coherence regressions)
# Boots the full production-faithful stack: Postgres, JHipster Registry (Eureka),
# ActiveMQ, 3 Artemis nodes, nginx LB, containerised Playwright. Slower than the
# single-node fast script, but the only way to reproduce multi-node bugs locally.
./run-e2e-tests-local-multinode.sh # Full multi-node run (build WAR + image + stack + tests)
./run-e2e-tests-local-multinode.sh --filter "Quiz" # Multi-node, filtered
./run-e2e-tests-local-multinode.sh --middleware redis # Run the same suite on Redis instead of Hazelcast
./run-e2e-tests-local-multinode.sh --skip-build --skip-up # Quick re-run against an already-running stack
./run-e2e-tests-local-multinode.sh --stop # Tear everything down
# Multi-node E2E (fast variant) — same topology, host-launched JVMs instead of Docker images
# Skips the Docker image build that dominates the slow path (~5–8 min). Reuses the WAR built by
# Gradle and runs 3 java -jar processes on the host; Postgres/Eureka/ActiveMQ/nginx still run as
# containers. Use this for server-side iteration on multi-node bugs. Cold ~1–2 min, warm ~30 s.
./run-e2e-tests-local-multinode-fast.sh # Full run (build WAR + infra + 3 host JVMs + tests)
./run-e2e-tests-local-multinode-fast.sh --filter "Quiz" # Filter to a subset of tests
./run-e2e-tests-local-multinode-fast.sh --middleware redis # Same suite, Redis instead of Hazelcast
./run-e2e-tests-local-multinode-fast.sh --skip-build --skip-up # Re-run tests against the running stack
./run-e2e-tests-local-multinode-fast.sh --stop # Tear everything downWhich middleware? Both multi-node runners take --middleware hazelcast|redis. Hazelcast is the default because it is
what production runs; Redis is the supported alternative and has to pass the same suite. With redis no Hazelcast
instance is created at all, which is what makes the run a genuine check of the abstraction.
Which E2E runner should I use?
run-e2e-tests-local-fast.sh— single node, Angular dev server. Best for client (UI) iteration.run-e2e-tests-local-multinode-fast.sh— multi-node, WAR run from host. Best for server iteration that needs the cluster (distributed data, ActiveMQ STOMP, LB).run-e2e-tests-local-multinode.sh— full Docker image build, prod-faithful. Use this to reproduce a CI-only failure or before pushing a multi-node-sensitive change.
Organized by feature module:
core/- Configuration, security base, utilities, base entitiesaccount/- User, authority, passkey, account REST, authentication, LDAPexercise/- Base exercise functionalityprogramming/- Programming exercises (lifecycle, grading, repositories)jenkins/- Jenkins CI backend connectorlocalvc/- Embedded git server (HTTP + SSH), repo URI handling, VCS access tokenslocalci/- Local CI orchestration: build job queue, dispatch, result processingquiz/- Quiz exercisesmodeling/- UML diagram exercisestext/- Text exercisesfileupload/- File upload exercisesexam/- Exam modeassessment/- Grading and assessmentcommunication/- Channels, messaging, conversations, FAQs, saved postsnotification/- Course / global / system / push notifications, mail servicelecture/- Lecture managementcalendar/- Calendar events and iCal subscriptionsatlas/- Competency-based learning, learning analyticsiris/- LLM-based virtual tutorathena/- ML-based assessmenthyperion/- LLM-based exercise creation assistantplagiarism/- Plagiarism detection (JPlag)lti/- LTI integrationtutorialgroup/- Tutorial group managementglobalsearch/- Cross-entity search via Weaviatevideosource/- External video source integration (TUM Live)course/- Course management, registration, archive, dashboard, statisticsadmin/- Admin operations: data export, vulnerability scan, cleanup, telemetry, organization management, legal documents
core/- Core services (HTTP, auth, guards)shared/- Shared components, pipes, utilitiesopenapi/- Generated TypeScript client code- Feature modules mirror server structure
- Assets and translations in
content/ - Client tests are co-located with their TypeScript components
src/test/java/- JUnit server testssrc/test/playwright/- E2E tests
src/main/resources/- Spring profiles (config/application-*.yml), Liquibase changelogs, static filesdocumentation/- Project documentationdocker/- Deployment helpers
- Generated at runtime by springdoc:
/v3/api-docsand/swagger-ui
- PascalCase for classes, camelCase for fields/methods
- No wildcard imports (Spotless enforces)
- Package-by-feature organization
- 4-space indentation
- Avoid
@Transactionalscope - Do not inject
EntityManagerorEntityManagerFactorydirectly into services or controllers; all persistence operations must go through Spring Data repositories - Use DTOs (Java records) for REST endpoints
- Prefer constructor injection for Spring beans
- Use Java 25 features (records, sealed classes, pattern matching)
- Do not add
@Cache(Hibernate L2) annotations on entities or associations. Hibernate second-level cache is disabled cluster-wide and an ArchUnit rule (ArchitectureTest.testNoHibernateSecondLevelCacheAnnotation) fails the build if any reappears. Reason:@Modifying @Queryrepository methods bypass L2 invalidation, and the absence of service-level@Transactionalleaves no clean place to coordinate eviction within a REST call — both produced cross-node stale-read bugs in the multi-node cluster (issue #12574, fixed in PR #12578; further cleanup in PR #12579). - For DTO / projection caching, use Spring
@Cacheable. It resolves against theRoutingCacheManagerincore/config/cache/CacheManagerConfiguration, which serves blob caches (files,plantUmlPng,plantUmlSvg) from a bounded per-node Caffeine cache and every other cache from the distributed data provider. Always pair@Cacheablewith explicit eviction —@CacheEvicton the writer service, or a HibernatePostUpdateEventListener/PostDeleteEventListener. SeeTitleCacheEvictionServicefor the canonical pattern, andBlobCacheEvictionServicefor evicting a per-node blob cache across the cluster. - The bar for adding a new cache: a measured performance gain that justifies the eviction-correctness work. The default answer is: do not cache.
- Full rationale, history, and patterns:
documentation/docs/developer/guidelines/caching.mdx.
- Never use Hazelcast or Redis directly. All cross-node state — build job queue, feature toggles, scheduling messages, websocket broker status, LTI state, Pyris jobs,
@Cacheablecaches — goes throughDistributedDataProvider(core/service/distributed). An ArchUnit rule (DistributedDataProviderArchitectureTest) fails the build if a production class outside a small, explicitly named set of backend adapters depends oncom.hazelcast..,org.redisson..ororg.springframework.data.redis... - The backend is selected by
artemis.distributed-data.provider(Hazelcastdefault,Redis,Local). WithRedisno Hazelcast instance is created at all, so any direct usage silently loses that state instead of failing. - Request entry lifetimes at the call site with
getExpiringMap(name, ttl); a backend map configuration only applies to that backend.getMap(name)rejects a per-entry TTL for exactly this reason. - Missing capability? Add it to
DistributedDataProvider, implement it for all three backends, and add a case toAbstractDistributedDataTest— that suite is what keeps the backends in agreement. - Full rationale and patterns:
documentation/docs/developer/guidelines/distributed-data.mdx.
- kebab-case for filenames (
course-detail.component.ts) - PascalCase for classes, camelCase for members
- Single quotes, 4-space indentation
- Standalone components preferred
- Signal-based Angular APIs are mandatory for new code:
- Use
input()/input.required()instead of@Input() - Use
output()instead of@Output() - Use
viewChild()/viewChild.required()instead of@ViewChild() - Use
viewChildren()instead of@ViewChildren() - Use
signal(),computed(), andeffect()for reactive state management - Use
inject()for dependency injection instead of constructor injection - Legacy decorators (
@Input,@Output,@ViewChild,@ViewChildren,@ContentChild,@ContentChildren) must not be used in new code - In modules not yet fully migrated, prefer signal-based APIs for new components but maintain consistency within existing components
- An ESLint rule (
enforce-signal-apis-in-migrated-modules) enforces this in fully migrated modules ngOnChangesis banned — usecomputed()/effect()instead. An error-level rule (localRules/prefer-signal-reactivity-over-ngonchanges) enforces this acrosssrc/main/webapp/app,packages/tum-ui/src/lib, andsrc/test/javascript, including specs and undecorated base classes. Angular 21 does call inheritedngOnChangeshooks and fires them for signal inputs, so this is a consistency ban rather than a correctness fix. A genuinely unavoidable use ofSimpleChanges.previousValue/isFirstChange()or pre-child-initialization ordering needs a detailed comment and a justified line-leveleslint-disable-next-line.ngOnInitandngOnDestroyare unaffected. Seedocumentation/docs/developer/guidelines/client-development.mdx.
- Use
- Angular template control flow: use
@if,@for,@switch; never use*ngIf,*ngFor,*ngSwitch - Avoid
null, useundefinedwhere possible - Copy objects with
deepClone, never with object spread,Object.assign, orstructuredClone- Use
deepClonefromapp/foundation/util/deep-clone.util(a thin wrapper over lodashcloneDeep) whenever you copy an entity-like object — anything that may hold adayjsdate, a nested object, aMap/Set, or a circular reference structuredClone()is the worst option: it does not preserve prototypes, so a cloneddayjsdate comes back as a plain object without its methods- Object spread (
{ ...obj }) andObject.assign({}, obj)only copy one level deep, so nested objects and arrays stay shared with the original and later edits mutate both - This is why signal updates need care: a signal only notifies when the reference changes, so replace the object rather than mutating it —
const updated = deepClone(current); updated.field = value; return updated;SeeAccountService.setImageUrlfor the canonical pattern - Two companions live in the same file:
cloneWith(x, { a, b })replaces{ ...x, a, b }in a single expression (source deep-cloned, overrides applied by reference), andhydrate(new Course(), dto)replacesObject.assign(new Course(), dto)for giving a parsed server DTO a prototype - Enforced by
localRules/prefer-deep-clone(error, production client TS; specs exempt). ImportingcloneDeepfromlodash-esis blocked byno-restricted-importsso all copying goes through the wrappers - Array spread stays fine:
items.update((items) => [...items, newItem])is the documented way to append immutably, as does object rest in destructuring (const { id, ...rest } = post) - When you only need a signal to emit after mutating an object in place, do not copy it at all: declare the signal with
equal: () => falseand re-set the same reference (seeCourseUpdateComponent.commitCourse). Copying detaches the nested objects children hold and can end inNG0103 - Where the state is not signal-backed, build the replacement object explicitly field by field (see
MetisService.rebuildPostReference) rather than reaching for a shallow copy - Full rationale and examples:
documentation/docs/developer/guidelines/client-development.mdx(### Cloning objects)
- Use
- Prefer 100% type safety
- UI components: use TUM UI and Tailwind CSS
- TUM UI is the target component system; use an existing
@tumaet/ui-angularcomponent whenever it covers the required behavior. - Use Tailwind CSS v4 utilities for application layout. Do not introduce Bootstrap or ng-bootstrap in new work.
- If TUM UI lacks a reusable capability, add or evolve a package component around native HTML or stable Angular CDK primitives. Keep Artemis-specific composition and policy in the application.
- PrimeNG is a transitional fallback only when the TUM UI gap cannot reasonably be closed in the same change. Explain the contained fallback in the pull request.
- Colours use semantic tokens, never primitives or Bootstrap classes: use TUM UI component variants or
text-state-danger/text-state-success/text-state-warning/text-state-infofor plain markup. Never use--p-<color>-Nprimitives,text-red-500,text-danger, or the superseded arbitrarytext-(--danger)form. Full decision rules and the Bootstrap migration reference:documentation/docs/developer/guidelines/client-development.mdx(### Styling). - Never hand-write PrimeNG component root classes (
class="p-button",class="p-inputtext"). For a contained legacy fallback, render the real PrimeNG component so its styles load deterministically;localRules/no-primeng-component-classesenforces this. - See
documentation/docs/developer/guidelines/tum-ui-kit.mdxfor package ownership, public API, theming, stories, and integration rules.
- TUM UI is the target component system; use an existing
- LF line endings
- Final newlines required
- UTF-8 encoding
- YAML: 2-space indentation
- Server tests require Docker — tests run against PostgreSQL via Testcontainers by default (both locally and in CI).
- Keep tests deterministic; mock external services and WebSockets
- CI enforces coverage thresholds per module
- Use
pnpm run test-difffor incremental client work - Client tests: Vitest
- Use
vi.spyOn(),vi.fn(),vi.clearAllMocks()instead of Jest equivalents - Run Vitest:
pnpm run vitest(watch),pnpm run vitest:run(single run),pnpm run vitest:coverage
- Use
- Name server tests
*Test.java; reuse module base classes when present - When comparing
ZonedDateTimevalues in tests, usetoInstant()for comparisons since PostgreSQL stores timestamps as UTC (timezone offset is not preserved through database round-trips) - E2E tests: Use
./run-e2e-tests-local-fast.sh— this is the intended way to run Playwright E2E tests locally (for both developers and AI agents)- The script automatically kills processes on ports 8080, 9000, and 7921 before starting
- Use
--filter "TestName"to run specific tests; supports regex patterns (e.g.,--filter "Quiz|Exam") - After the first run, reuse running services with
--skip-server --skip-client --skip-db
- Add screenshots for UI changes in PRs
- Verify linting before submitting:
pnpm run lint,./gradlew checkstyleMain -x webapp
- Concise, imperative commit messages scoped where useful (e.g.,
Exam mode: adjust live updates,build: bump version); wrap bodies near 72 chars - PRs: include problem/solution summary, linked issue, commands/tests run, screenshots for UI, and doc updates if relevant
- Target
developbranch; rebase to reduce noise - Run lint and tests before submitting
- Follow
CONTRIBUTING.mdand the guidelines indocumentation/docs/developer/guidelines/ - Use the PR description template in
.github/PULL_REQUEST_TEMPLATE.md