diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml
index ce1e63a..8da7663 100644
--- a/.gitea/workflows/build.yaml
+++ b/.gitea/workflows/build.yaml
@@ -19,21 +19,23 @@ jobs:
uses: https://github.com/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
persist-credentials: false
+ - name: Verify pinned engineering resources
+ run: python3 scripts/engineering.py --check
- name: Identify workflow attempt
# Gitea 1.25 does not populate gitea.run_attempt. Use a unique value for
# container names and diagnostic artifacts, including reruns of one SHA.
- run: java ci/Ci.java attempt
+ run: java generated/ci/Ci.java attempt
- name: Build and test both images
- run: java ci/Ci.java verify
+ run: java generated/ci/Ci.java verify
- name: Publish verified main images
if: success() && gitea.ref == 'refs/heads/main'
env:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
- run: java ci/Ci.java publish
+ run: java generated/ci/Ci.java publish
- name: Collect diagnostics and clean up test containers
if: always()
- run: java ci/Ci.java cleanup
+ run: java generated/ci/Ci.java cleanup
- name: Retain diagnostics for fourteen days
if: always()
uses: https://github.com/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5
diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml
index b0418cd..ad23a78 100644
--- a/.gitea/workflows/deploy.yaml
+++ b/.gitea/workflows/deploy.yaml
@@ -42,8 +42,10 @@ jobs:
uses: https://github.com/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
persist-credentials: false
+ - name: Verify pinned engineering resources
+ run: python3 scripts/engineering.py --check
- name: Identify workflow attempt
- run: java ci/Ci.java attempt
+ run: java generated/ci/Ci.java attempt
- name: Release verified images — persistent PostgreSQL storage
env:
RELEASE_MODE: ${{ inputs.mode }}
@@ -57,7 +59,7 @@ jobs:
DEPLOY_REGISTRY: ${{ vars.DEPLOY_REGISTRY }}
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
- run: java ci/Ci.java deploy
+ run: java generated/ci/Ci.java deploy
- name: Retain deployment diagnostics for fourteen days
if: always()
uses: https://github.com/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5
diff --git a/.gitea/workflows/rollback.yaml b/.gitea/workflows/rollback.yaml
index feff460..bd91070 100644
--- a/.gitea/workflows/rollback.yaml
+++ b/.gitea/workflows/rollback.yaml
@@ -19,8 +19,10 @@ jobs:
uses: https://github.com/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
persist-credentials: false
+ - name: Verify pinned engineering resources
+ run: python3 scripts/engineering.py --check
- name: Identify workflow attempt
- run: java ci/Ci.java attempt
+ run: java generated/ci/Ci.java attempt
- name: Restore previous successful release — requires a compatible database schema
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
@@ -31,7 +33,7 @@ jobs:
DEPLOY_REGISTRY: ${{ vars.DEPLOY_REGISTRY }}
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
- run: java ci/Ci.java rollback
+ run: java generated/ci/Ci.java rollback
- name: Retain deployment diagnostics for fourteen days
if: always()
uses: https://github.com/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5
diff --git a/AGENTS.md b/AGENTS.md
index fb11189..355bfd9 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,22 +1,31 @@
# Project skills and specifications
-- Project skills are stored in `skills/`; specifications belong in `specs/`.
-- For code reviews, read and follow `skills/code-review/SKILL.md`. If no
+- Project skills are stored in `generated/skills/`; specifications belong in [landscape](http://192.168.2.28:3000/wanjakrah/landscape/src/branch/main/specs).
+- For code reviews, read and follow `generated/skills/code-review/SKILL.md`. If no
dedicated Skill tool is available, load the file directly.
- Use `grill-me` only when the user explicitly requests it, for example
`Use grill-me to review this plan` or `Gebruik grill-me voor dit ontwerp`.
-- Before using it, read `skills/grill-me/SKILL.md` and its dependency
- `skills/grilling/SKILL.md`. If no dedicated Skill tool is available, load these
+- Before using it, read `generated/skills/grill-me/SKILL.md` and its dependency
+ `generated/skills/grilling/SKILL.md`. If no dedicated Skill tool is available, load these
files directly and follow their instructions within the available tools.
- Installing or mentioning a skill does not start an interview. Do not apply
`grilling` automatically to ordinary development requests.
- Keep the upstream skill files unchanged. Source revision and license are
- recorded in `skills/upstream.json` and `skills/LICENSE.mattpocock`.
+ recorded in `generated/skills/upstream.json` and `generated/skills/LICENSE.mattpocock`.
# Backend changes and reviews
- Before changing backend Java, read `CODING_STANDARDS.md`.
-- For reviews, use `skills/code-review/SKILL.md` together with
+- For reviews, use `generated/skills/code-review/SKILL.md` together with
`docs/backend-review.md`. Keep upstream skills unchanged.
- Obtain maintainer agreement before adding any standards waiver; record its
narrow scope and reason alongside the affected code or configuration.
+
+# Landscape and engineering
+
+- Read current [landscape guidance](http://192.168.2.28:3000/wanjakrah/landscape),
+ including applicable ADRs and specifications. This repository owns its implementation.
+- Engineering resources are pinned by `engineering.lock.json`. Do not edit
+ `generated/`; change engineering and deliberately adopt a released version.
+- Regenerate with `mvn -N -Pregenerate-engineering validate`; check with
+ `python3 scripts/engineering.py --check`. No sibling checkout is needed.
diff --git a/CODING_STANDARDS.md b/CODING_STANDARDS.md
index 0ea25bf..e5073a5 100644
--- a/CODING_STANDARDS.md
+++ b/CODING_STANDARDS.md
@@ -1,277 +1,11 @@
-# Backend coding standards
+# Agile Dancers coding standards
-This document is the authoritative policy for handwritten production and test Java
-in `agiledancers-be`. Generated code is out of scope. The Java conventions below also apply to
-handwritten Java helpers outside that module. Existing frontend conventions in README.md and agiledancers-fe remain in force.
+Read [the pinned engineering standards](generated/CODING_STANDARDS.md) before
+changing Java. Their backend-module placeholder means `agiledancers-be` here.
+Frontend conventions remain in README.md and agiledancers-fe.
-## Scope and authority
+The existing maintainer-approved exception for Group's eight-parameter canonical
+constructor remains in `tooling/standards/waivers.tsv`. No new waiver is introduced
+by S19. For reviews also read [the local companion](docs/backend-review.md).
-- Cover handwritten production and test Java in `agiledancers-be`. Exclude
- generated code. Dependency injection and production architecture rules apply
- where relevant. The Java conventions also cover Java helpers outside the backend; backend
- architecture, metrics, coverage and automated checks remain backend-only.
- Frontend code is out of scope.
-- [Java coding guidelines](https://www.geeksforgeeks.org/java/coding-guidelines-in-java/)
- supplied source material. Only the rules adopted in this local document apply;
- changes to the article do not change project policy.
-- The project's pinned Palantir formatter wins formatting conflicts, including
- comment layout. S8 remains responsible for formatter integration.
-- Do not adopt unrelated static-analysis rule bundles.
-
-## General language guidance
-
-- Always prefer newer stable official APIs, language features and officially
- recommended coding practices over older alternatives when they suit the task
- and are supported by the project's configured language/runtime version.
- Preserve required behavior and compatibility; document concrete reasons for
- retaining an older approach. This guidance applies to every language used in
- the project.
-
-## Java conventions
-
-- Types use UpperCamelCase; methods and variables use lowerCamelCase; constants
- use UPPER_SNAKE_CASE; package names are lowercase.
-- One top-level type per file; explicit member visibility; `@Override` on
- overriding methods. Do not invent visibility modifiers where Java forbids them.
-- Opening braces stay on the declaration/control-statement line. All `if`, `for`
- and `while` bodies require braces, including enhanced for and do/while bodies.
-- No wildcard imports. Static imports are allowed only in test classes.
-- Prefer `java.nio` APIs (including `java.nio.file` and `java.nio.charset`) over
- equivalent `java.io` APIs. Use stream APIs where no suitable NIO equivalent
- exists, such as process pipes or APIs requiring streams.
-- Use varargs (`Type... values`) when an array is the last method or constructor
- parameter, including `main(String... args)`. Preserve required override signatures
- and framework/API compatibility.
-- Prefer Java 25 `IO.print`/`IO.println` for direct standard output and `IO.readln`
- for line-oriented standard input. `IO` has no standard-error variant; retain
- `System.err` for error output. Use application logging where appropriate.
-- Comments explain intent or constraints. Require Javadoc on public classes and
- public methods that do more than basic POJO/data-carrier work. Trivial accessors
- and plain data carriers do not need boilerplate documentation. Describe behavior,
- constraints and failure conditions where relevant.
-- Block comments use the following layout, subject to Palantir formatting:
-
- ```java
- /*
- This is an example of a block comment.
- It may have more than one line.
- */
- ```
-- Prefer records for immutable data carriers. Classes are allowed when mutation,
- framework requirements or inheritance make records unsuitable. Preserve API and
- serialization behavior. Record immutability is shallow: protect mutable
- components appropriately.
-- Require `final` for parameters, local variables and fields wherever they can
- remain assigned once, including loop variables and catch parameters where Java
- permits it. Field finality must remain compatible with framework injection and
- lifecycle requirements. Permit reassignment when the algorithm needs it; do not
- introduce artificial holder objects to avoid reassignment. This rule does not
- require final classes or methods.
-- Use explicit types unless the initializer visibly identifies the type. `var` is
- allowed with `new School(...)` or an explicit cast, but not for method results,
- literals or other inferred expressions. For example,
- `final var school = new School(...)` is allowed, while a repository result uses
- `final School school = repository.find(id)`. Borderline cases require review.
-- No additional member-ordering or line-width policy beyond Palantir.
-
-## Metrics
-
-Targets guide development. Review signals require assessment, not automatic
-failure. Blocking limits fail verification. Strict inequalities matter: a value
-exactly at a blocking limit passes.
-
-| Metric | Target | Review signal | Blocking limit |
-| --- | ---: | ---: | ---: |
-| Cyclomatic complexity per method/constructor | ≤ 5 | > 7 | > 10 |
-| Cognitive complexity per method/constructor | ≤ 10 | > 10 | > 15 |
-| Method/constructor length, code lines | ≤ 20 | > 30 | > 50 |
-| Parameters per method | ≤ 3 | > 4 | > 5 |
-| Parameters per explicitly written constructor | ≤ 4 | > 5 | > 7 |
-| Injected dependencies per class | ≤ 4 | > 5 | > 7 |
-| Fan-out per class, unique used types | ≤ 10 | > 15 | Review only |
-| Own superclass depth, excluding Object | ≤ 1 | > 2 | > 3 |
-| Control-flow nesting | ≤ 2 | > 3 | > 4 |
-| Production duplicated lines density | ≤ 1% | > 1% | > 3% |
-| Class length, code lines | ≤ 200 | > 300 | > 500 |
-| Declared public methods per class, excluding trivial accessors | ≤ 10 | > 15 | Review only |
-
-### Measurement definitions
-
-- Use the pinned PMD algorithms as authoritative for cyclomatic and cognitive
- complexity. Document their exact configuration, including switch and boolean
- treatment. Do not accidentally introduce a class-level aggregate complexity gate.
-- Measure each method, constructor and explicitly written record constructor
- independently. Lambda bodies contribute to their enclosing executable;
- methods in nested/local classes are separate executables.
-- Parameter limits exclude implicit record constructors and generated members.
- Record component counts remain a design review concern.
-- Length means physical source lines after Palantir formatting, excluding blank
- and comment-only lines. Declaration and brace-only lines count; a line containing
- both code and comments counts. Do not substitute statement counts or NCSS.
- Measure nested types separately and exclude them from enclosing class length.
-- Count distinct injected collaborators by resolved type and qualifier. Include
- constructor, field, setter, inherited and implicit constructor injection.
- Repeated injection of the same type and qualifier counts once; differently
- qualified instances of the same type count separately.
-- Fan-out counts distinct explicitly referenced resolved types, including JDK and
- framework types. Exclude primitives, the class itself and generated references.
-- Inheritance depth counts project-owned superclass layers; exclude Object,
- interfaces and external framework ancestry.
-- Nesting combines `if`, loops, `switch` and `catch`, with the first structure at
- depth one. An `else if` chain stays at one level. `else`, `try` and `finally`
- add no level themselves. Lambdas retain surrounding depth; methods in nested
- classes start fresh.
-- Public-method counts include explicitly declared public instance and static
- methods; exclude constructors, inherited methods and trivial accessors. A
- trivial accessor only reads or assigns its corresponding field, with no
- validation, branching or other work.
-- Duplication limits apply only to production code. Duplication in tests is allowed
- and has no target, review signal or blocking limit.
-- Detect production clones with CPD, minimum 100 tokens, without ignoring
- identifier or literal differences. Density is
- `100 × distinct duplicated code-line locations / all production code-line locations`.
- Exclude blank/comment-only lines from numerator and denominator. Count every
- occurrence, including the first; merge overlapping ranges to count each source
- line once. This project metric need not equal a future Sonar metric.
-
-## Enforcement and developer workflow
-
-- Use explicitly pinned Java-25-compatible Checkstyle and PMD/CPD versions.
- Retain ArchUnit for architecture checks and add small targeted custom checks
- where standard rules cannot accurately express the agreed semantics.
-- Automate every blocking metric and explicit rule where reliable checking is
- available. Identify rules requiring manual review explicitly; do not claim
- they are automated. Review signals must appear in reports even when builds pass.
-- Run standards checks during `mvn verify`; provide documented standalone commands.
- Ordinary compile/test commands retain the current S8 formatting behavior.
-- Standards checks report violations without rewriting code. CI passes these
- checks before image publication. Parser failures or unsupported Java constructs
- fail with the affected path and actionable diagnostics, rather than being skipped.
-- Add a root AGENTS.md instruction requiring agents to read CODING_STANDARDS.md
- before changing backend Java. Project review instructions must explicitly cover
- manual rules, review signals in changed code and approved waivers, without
- repeating checks already enforced by tooling.
-- Keep upstream skill files unchanged. Put project-specific code-review guidance
- in a companion file referenced by project instructions and used alongside
- `skills/code-review/SKILL.md`.
-- Waivers must be explicit, narrowly scoped and carry a recorded reason alongside
- code or configuration. Agents need maintainer agreement before introducing one.
- No blanket suppressions, exclusions of difficult files or weakened thresholds
- merely to make verification pass.
-
-## Coverage
-
-- Require at least 80% backend line coverage across handwritten production code
- for the module as a whole, not separately per class or package. Compare the
- unrounded percentage against 80%.
-- Local `mvn verify` must reach 80% using backend unit/integration tests alone.
- This intentionally tightens the original combined-tests-only requirement.
-- CI additionally instruments the backend exercised by Playwright, merges that
- coverage with backend unit/integration coverage and gates the combined result.
- The Playwright contribution need not separately reach 80%.
-- Exclude generated code only; do not broadly exclude DTOs, configuration or
- difficult code. Publish line and branch coverage reports; branch coverage has
- no agreed blocking percentage.
-- PIT measures mutation-test effectiveness, not line coverage, and is outside
- S7's required implementation. Configuring Sonar in the pipeline remains out of
- scope for S14. FindBugs/SpotBugs is not an additional required check suite.
-
-
-## Implemented checks and manual review
-
-Checkstyle **12.3.1**, PMD/CPD **7.28.0**, and JaCoCo **0.8.14** are pinned.
-`tooling/standards/checkstyle.xml` selects individual naming, structure, import and brace checks; no general analysis bundle is enabled.
-`tooling/standards/src/main/java/Standards.java` supplies the targeted measurements.
-ArchUnit remains in the backend test suite for architectural boundaries.
-
-PMD uses Java 25, the running JDK's `jrt-fs.jar`, compiled backend classes and the
-resolved test dependency classpath. Cyclomatic complexity uses PMD's default
-options: boolean paths count, assertions do not. Decisions include if/loops,
-catch, ternary and throw; non-default switch alternatives count individually,
-including comma-separated labels. There is no aggregate class-complexity gate.
-Cognitive complexity uses PMD's boolean-operation sequences, nesting, switch,
-recursion and flow rules. The scoped visitors retain those algorithms while
-including lambdas in their enclosing executable and excluding nested/local type
-bodies, whose methods are measured independently. Compact record constructors
-use the same visitors and start with cyclomatic complexity one.
-
-Custom checks measure physical code lines from regular parser tokens, including
-brace/declaration lines and mixed code/comment lines. They count multiline literal
-lines while excluding physically blank lines. Class length excludes nested-type
-tokens, preserving lines shared with enclosing code. Other custom measurements
-implement the definitions above: nesting, resolved type fan-out, project-owned
-superclass depth, public non-accessor methods, and injected collaborators.
-Injection includes inherited fields/setters, explicit `@Inject` constructors,
-single constructors on bean classes, records' implicit canonical constructors,
-and Lombok required/all-argument constructor injection. Qualifier keys use resolved
-annotation names and values, including defaults; repeat sites are deduplicated.
-The CPD configuration preserves identifiers/literals, uses 100 tokens and disables
-skip blocks. Its line-location union counts overlapping occurrences only once.
-
-Checkstyle and targeted checks enforce import/brace/naming/visibility rules,
-`@Override`, visible constructor/cast/new-array `var` initializer forms, final parameters, locals (including
-loops/catches and lambdas) and initialized fields. PMD's three selected finality
-rules additionally cover control-flow assignment and private constructor-initialized
-fields. Framework/lifecycle requirements, generated Lombok setters, and external
-writes to public/protected fields need review before changing field finality.
-Comment intent, Javadoc adequacy for nontrivial public contracts, record suitability
-and protection of mutable components require manual review. These are **manual
-rules**, as are ambiguous type inference, architecture/DI design decisions, NIO
-API selection, trailing-array varargs, console I/O selection and the POJO
-documentation boundary. Review all `DESIGN` record-component entries
-and `REVIEW` signals, even when a build passes, using `docs/backend-review.md`.
-
-Reports never rewrite Java. Formatter application remains S8's separate validate
-execution. Generated output under `target` and generated source directories are
-outside the handwritten `src/main/java` and `src/test/java` scope. No maintained
-file, DTO, configuration class or package is excluded from standards or coverage.
-JaCoCo filters compiler/Lombok-generated members; no handwritten-code exclusions
-are configured. Line and branch results are published, with only lines gated.
-
-Exact metric waivers live in `tooling/standards/waivers.tsv`; each records a metric,
-fully resolved executable signature (or class), exact retained value and the
-maintainer agreement/reason. No wildcard waivers are allowed, and a changed value
-fails again. A waived blocking result remains visible as `WAIVER`. PMD suppression
-annotations/comments cannot silently waive a rule. The only approved waiver is the eight-parameter `Group` canonical constructor,
-which preserves its JSON contract while defensively copying both lists. Adding a waiver always requires maintainer agreement.
-
-## Commands and reports
-
-Run from the root with JDK 25:
-
-```sh
-mvn -Dformat.mode=check clean verify
-# Prepare standalone analysis without running tests:
-mvn -pl agiledancers-be test-compile dependency:build-classpath@standards-classpath
-mvn -f tooling/standards/pom.xml test exec:java
-# Exact local coverage gate after a test/report build:
-java tooling/coverage/Coverage.java agiledancers-be/target/site/jacoco/jacoco.xml
-```
-
-`verify` runs backend tests, builds resolved analysis inputs, generates JaCoCo
-reports, runs checker regression tests and standards analysis, then gates exact
-module-wide line coverage. `compile` and `test` retain existing formatting behavior
-and do not run the verify-only standards checks. Standalone analysis requires its
-classpath preparation; unresolved types fail with the path and preparation advice.
-
-Full metrics and findings: `agiledancers-be/target/standards/report.txt`.
-Checker regression results: `tooling/standards/target/surefire-reports`.
-Coverage HTML/XML/CSV: `agiledancers-be/target/site/jacoco` (local backend tests)
-and `agiledancers-be/target/site/jacoco-combined` (merged browser contribution).
-The gate compares integer line counts (`5 * covered >= 4 * total`), rejecting an
-empty report and percentages that would round up to 80%.
-
-CI first passes local verify. It mounts the pinned JaCoCo agent into its disposable
-backend test container, exposes the agent only on the isolated Docker network,
-and uses the pinned CLI to dump `target/playwright.exec` after Playwright. A missing
-or empty dump fails. The following commands merge both datasets and gate the result
-before the verified-commit marker can authorize publication:
-
-```sh
-mvn -pl agiledancers-be jacoco:merge@combined jacoco:report@combined-report exec:exec@combined-coverage
-```
-
-CI retains both coverage reports, execution data, standards findings and regression
-results with its usual diagnostics artifacts. The agent is mounted only for tests;
-published images contain no agent. PIT and Sonar configuration remain outside S7.
-See `specs/S7-validation.md` for migration evidence and retained-signal assessment.
+Validation evidence: [S7](docs/validation/S7-validation.md).
diff --git a/Dockerfile b/Dockerfile
index fe44e43..cf06501 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,8 +2,9 @@ FROM maven:3.9-eclipse-temurin-25 AS build
WORKDIR /workspace
COPY pom.xml ./
COPY .mvn .mvn
+COPY generated generated
COPY tooling tooling
-COPY ci ci
+COPY engineering.properties engineering.properties
COPY agiledancers-be/pom.xml agiledancers-be/pom.xml
COPY agiledancers-be/src agiledancers-be/src
# Source builds run the same PostgreSQL tests as local/CI verification.
diff --git a/README.md b/README.md
index ecbebc5..3bb9557 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,18 @@
+# Landscape and shared engineering
+
+Architecture decisions, specifications and project onboarding are maintained in
+[landscape](http://192.168.2.28:3000/wanjakrah/landscape). Reusable tooling, skills,
+standards and the project generator are maintained in
+[engineering](http://192.168.2.28:3000/wanjakrah/engineering).
+
+This project commits the engineering version recorded in `engineering.lock.json`
+under `generated/`. Ordinary builds need no other repository checkout. Regenerate
+with `mvn -N -Pregenerate-engineering validate`; CI checks the selected tag and
+commit with `python3 scripts/engineering.py --check`. To upgrade, update both tag
+and commit, follow engineering's UPGRADING.md, regenerate and review the diff.
+Project configuration in `engineering.properties`, workflows and `deploy/` remains
+application-owned. Read CODING_STANDARDS.md and AGENTS.md for project guidance.
+
# agiledancers
To map dance groups to performance days. The backend runs on Quarkus with Java 25.
@@ -299,7 +314,7 @@ during the one-time transition from an in-memory release, and [S6 evidence](docs
Maven is the authoritative formatting path. The root build runs the actual
Palantir Java Format **2.101.0**, Palantir style, through a custom OpenRewrite
-**8.63.4** text recipe in `tooling/format/Format.java`. The pinned
+**8.63.4** text recipe in `generated/tooling/format/Format.java`. The pinned
`exec-maven-plugin` **3.6.3** launches that helper with JDK 25. Native import
ordering and unused-import removal are enabled; extra Javadoc formatting is off.
No OpenRewrite AutoFormat or additional refactoring recipes run.
@@ -344,9 +359,9 @@ format-on-save must not override Maven's result. Other editors can invoke the
standalone Maven apply command as an external tool.
Verification and measurements are recorded in
-[the S8 evidence](specs/S8-formatting-evidence.md). Reproduce the isolated failure,
+[the S8 evidence](docs/validation/S8-formatting-evidence.md). Reproduce the isolated failure,
UTF-8, import, cache and idempotence checks with
-`python3 tooling/format/test.py`, and the sequential end-to-end build measurements
+the formatter integration test in engineering (`python3 tests/format_integration.py`), and the sequential end-to-end build measurements
with `python3 tooling/format/benchmark.py` (requires the ordinary backend test
prerequisites). Results are written under `target/`.
@@ -357,7 +372,7 @@ It defines the agreed rules, exact metric limits, manual review duties, pinned
analysis tools, coverage policy and standalone commands. `mvn verify` runs standards
checks and requires at least 80% module-wide backend line coverage. CI also collects
and merges coverage from Playwright before publication. Reports are retained with
-CI diagnostics. See [S7 validation](specs/S7-validation.md) for current migration status.
+CI diagnostics. See [S7 validation](docs/validation/S7-validation.md) for current migration status.
Existing-school mutations require `X-School-Revision` with the revision loaded by
diff --git a/adr/README.md b/adr/README.md
deleted file mode 100644
index f06618a..0000000
--- a/adr/README.md
+++ /dev/null
@@ -1,33 +0,0 @@
-
-The adr-folder contains Architecture Decision Records.
-An Architecture Decision Record is a file that contains a decision that should be applied to all new and altered code and systems.
-ADR's can only be added or altered on specific request by the user. Agents should never add or modify ADR's without explicit request to do so.
-ADR's are immutable in essence. This means that the only modifications allowed are in its state, or when it applies to typo's, adding clarifications, or removing ambiguity.
-An ADR MUST have the following properties:
-
-- ID
-- Title
-- Supersedes
-- Status (NEW, REFINED, APPROVED, REJECTED, EXPIRED)
-- Expiration date
-- Context
-- Decisions
-- Options considered with pro's and cons
-- Consequences.
-An _ID_ MUST be a unique number, prefixed with ADR (e.g. ADR1, ADR2, ADR3, etc.). This makes it easy to reference an ADR.
-A _title_ is a short, unique description of the ADR. It should make it easy to know in a glance what the ADR is about.
-The name of the file SHOULD contain the ID and the title, or a short variant of the title, of the ADR.
-_Supersedes_ is an optional property that MAY reference another, expired ADR that it supersedes.
-When _supersedes_ references another ADR, it MUST do so by referencing its ID.
-When _supersedes_ references another ADR, the status of that other ADR MUST equal to EXPIRED.
-_Status_ contains the state of the ADR. In the readme, when we mention state, we mean the value of this status-property. ADR's are always in a subfolder of the adr-folder, with the same name as the state, but in lowercase ('adr/new' for ADR's with state NEW, 'adr/refined' for ADR's with state REFINED, etc.).
-Each created ADR will start with the state NEW. Apart from the ID and the state, each property can still be modified. Nothing that is in an ADR with state NEW, should be considered when writing or evaluating anything within the software landscape.
-An RFD will get the state REFINED, when it is fully worked out. Only RFD's with the state NEW can be changed to REFINED. This means an effort has been made to get to a final version of the RFD. Getting an RFD from NEW to REFINED can be done with or without the help of an agent. However, an agent should always verify mutual understanding of the content with the human owner, before changing the state from NEW to REFINED.
-An ADR with the state REFINED can be moved to either APPROVED or REJECTED. This step must be done manually, not by any agent. APPROVED means the decision in the ADR applies to all code and/or software added or altered as far as the content of the ADR applies to the situation. Exceptions are possible with explicit accord. Such an accord should be given by a person. REJECTED means that the ADR should not be considered anywhere when writing or evaluating anything within the software landscape.
-An ADR with the state EXPIRED is no longer valid. It should no longer be considered anywhere when writing or evaluating anything within the software landscape, even though it had been the case previously. It MAY have been superseded by a new ADR, but that is not necessary.
-An _expiration date_ indicated from when an ADR should be notified for reevaluation. Reaching the _expiration date_ does not mean the ADR is expired. It only means the ADR should be reevaluated by a person, with or without the help of an agent. As a result of the reevaluation, this ADR may get EXPIRED - only after explicit agreement of a person - and it MAY be superseded by a new one. Another option is that only the expiration date itself gets updated to a new expiration date. This also only after explicit agreement of a person.
-_Context_ should explain the context of the problem where a decision had to be made for.
-The _decision_ describes the decision. This is what all code MUST apply to, unless an explicit exception is approved by a person.
-The _options_ describes the various alternatives that were considered, all with its pro's and con's, and why the chosen decision whas chosen.
-_Consequences_ describes the consequences to the current state of the application landscape at the time of writing. This could be about a migration timeline, a new process, different tooling, or a change in the stack, or something else.
-
diff --git a/adr/approved/ADR1_application-size.md b/adr/approved/ADR1_application-size.md
deleted file mode 100644
index d102d5d..0000000
--- a/adr/approved/ADR1_application-size.md
+++ /dev/null
@@ -1,215 +0,0 @@
-**ID:** ADR1 \
-**Title:** Application size \
-**Supersedes:** - \
-**Status:** APPROVED \
-**Expiration Date:** Six months after approval; record the calendar date upon manual approval.
-
-## Context
-
-Agile Dancers currently keeps its frontend, backend, CI pipeline, skills,
-specifications, ADRs, and other documentation in one repository. As the application
-grows, humans and agents need more context to understand individual tasks.
-Functional dependencies can become entangled, making changes and reuse harder.
-
-The primary goal is to reduce the context required for a task. Supporting goals
-are preventing uncontrolled coupling and enabling reuse and replacement of
-capabilities. Repository organization, module boundaries, and deployment
-boundaries are separate architectural choices.
-
-Business capabilities stand on their own, while an application system may support
-multiple capabilities. Loosely coupled capability boundaries allow implementations
-to change between custom software, libraries, and externally supplied components.
-Scheduling and school administration are candidate areas to examine; their exact
-system boundaries have not yet been decided.
-
-Code-level modularity alone is insufficient when shared tables, materialized
-views, SQL scripts, or other database access bypass module boundaries. Separate
-services would have the same weakness without enforced data ownership.
-
-Systems can initially run on the same physical server. Independent workload
-placement is nevertheless useful: computational work, such as runners and future
-scheduling calculations, may run on a mini-PC rather than the Synology. Independent
-scaling and release cycles are future possibilities rather than universal current
-requirements. Colocation reduces network exposure but process restarts, timeouts,
-and resource contention still require defined failure behavior.
-
-Reusable engineering resources should also support other projects. Specifications
-can affect several projects, and ADRs apply across the application landscape.
-
-## Decisions
-
-### Capabilities and systems
-
-Organize the landscape into independently deployable systems. Group capabilities
-into systems according to cohesion, data ownership, replacement needs, and
-operational requirements, including workload placement. Capability size alone does
-not justify a separate system.
-
-Each system has its own repository and independent deployment lifecycle. Within a
-system, use modules to keep its capabilities loosely coupled. A capability does
-not automatically require its own service or deployment. Compute workers may be
-deployed separately from the business system that delegates work to them.
-
-Capability contracts should allow implementations to be replaced by custom code,
-libraries, or external products, using adapters where necessary.
-
-### Data ownership
-
-Each system exclusively owns its data. Other systems access that data through
-explicit contracts, never through direct access to its storage. This restriction
-also applies to reporting queries, views, migrations, and maintenance scripts.
-Cross-system reporting requires explicit contracts or a separately owned
-projection.
-
-PostgreSQL-backed systems initially share one PostgreSQL server/container, with
-separate databases or schemas and separate credentials whose permissions enforce
-ownership. Sharing physical infrastructure does not permit sharing storage access.
-Systems may choose other storage technologies when justified. This permits future
-CQRS designs without adopting CQRS through this ADR.
-
-### Contracts and communication
-
-Systems communicate through explicit, versioned contracts. Internal domain models
-and persistence entities remain private to their owning system and are not shared
-through common libraries. Contract definitions and generated clients may be shared.
-
-Choose synchronous APIs or asynchronous messaging for each interaction according
-to its needs. Define failure behavior for each interaction. This ADR mandates
-neither a landscape-wide communication mechanism nor a message broker.
-
-### Landscape and engineering resources
-
-Maintain a landscape repository containing ADRs, specifications, and the capability
-and system map. Specifications identify the projects they affect and may span
-multiple systems. ADRs govern the application landscape according to their scope.
-Each system retains a README describing its implementation and operation.
-
-Maintain reusable engineering resources, including pipeline components, skills,
-and general engineering documentation, separately from the landscape repository
-and individual systems. Group these resources according to maintenance and release
-needs; a separate repository per resource type is not required. Version shared
-resources so consuming projects can adopt updates deliberately.
-
-### Adoption and review
-
-Use this ADR as guidance and the starting point for follow-up specifications.
-Detailed system boundaries, contracts, and migration designs are follow-up work.
-Migrate incrementally, keeping the application working after each extraction.
-
-Perform an initial review six months after approval. Review earlier if contract
-coordination, operational overhead, or persistent cross-system coupling becomes a
-significant burden. The expiration date triggers reevaluation and does not
-automatically invalidate the decision.
-
-## Options considered
-
-### 1. Improve navigation within the current structure
-
-Keep the existing projects and deployment structure, improving focused
-documentation, task guidance, and file organization.
-
-Pros:
-
-- Requires little disruption or migration work.
-- Makes relevant context easier to find.
-- Retains simple local development and coordinated changes.
-
-Cons:
-
-- Offers limited protection against functional entanglement.
-- Does not enforce data ownership or support independent workload placement.
-- Leaves reuse and replacement dependent on existing application boundaries.
-
-Not chosen because navigation improvements alone do not establish the ownership
-and replacement boundaries needed as the landscape grows.
-
-### 2. Modular monolith in one repository
-
-Organize capabilities into modules with explicit interfaces and dependency rules,
-while retaining coordinated deployment and one repository.
-
-Pros:
-
-- Reduces the code area needed to understand a capability.
-- Enables dependency checks and straightforward changes across modules.
-- Keeps deployment and in-process communication simple.
-
-Cons:
-
-- Shared database access can bypass code-level boundaries through tables, views,
- and scripts unless storage ownership is separately enforced.
-- Workload placement and deployment remain coupled.
-- Independent replacement and reuse may require later extraction.
-
-Not chosen as the landscape-wide approach because independently enforced ownership
-and deployment boundaries are desired. Modular design remains part of the chosen
-approach inside each system.
-
-### 3. Separate projects and repositories with coordinated deployment
-
-Extract capabilities into separately built libraries or applications with
-versioned contracts, while retaining a coordinated deployment lifecycle.
-
-Pros:
-
-- Separates source and build context.
-- Gives reusable components their own versioned releases.
-- Retains a coordinated operational release process.
-
-Cons:
-
-- Changes spanning repositories require dependency release coordination.
-- Deployment remains coupled despite repository separation.
-- Repository separation alone does not enforce storage ownership or runtime
- independence.
-
-Not chosen because it incurs repository coordination costs while retaining a
-deployment constraint that would limit workload placement and replacement.
-
-### 4. Independently deployable systems with internal capability modules — chosen
-
-Give systems their own repositories, deployment lifecycles, contracts, and enforced
-data ownership. Keep capabilities loosely coupled through modules within a system.
-Place compute workers independently where useful.
-
-Pros:
-
-- Supports focused development context and independent system evolution.
-- Enforces data ownership beyond code dependency rules through storage permissions.
-- Allows workloads to move between the Synology and mini-PC.
-- Supports capability replacement and reuse through explicit contracts.
-- Allows storage technology choices per system.
-
-Cons:
-
-- Requires contract compatibility and coordination for changes spanning systems.
-- Adds deployment, configuration, and operational maintenance for multiple processes.
-- Requires explicit failure handling and cross-system consistency decisions.
-- Sharing one server or PostgreSQL instance retains common infrastructure failure
- points despite logical isolation.
-
-Chosen because ownership, replaceability, focused context, and independent workload
-placement outweigh the additional operational and contract coordination work.
-
-## Consequences
-
-- Write follow-up specifications to map capabilities and dependencies, choose
- initial system boundaries, define contracts and data ownership, and plan
- extraction. This ADR does not itself settle those boundaries or implement them.
-- Establish the landscape repository and the repositories and versioning needed
- for reusable engineering resources. Preserve links between specifications and
- affected projects, and between system READMEs and landscape guidance.
-- Extract systems one at a time, establishing contracts and enforcing storage
- ownership as part of each extraction. Moving tables into separate schemas is
- insufficient while cross-system SQL access remains possible.
-- Adapt build and deployment pipelines to release systems independently and
- consume versioned shared engineering resources.
-- Add appropriate checks for module dependencies, contract compatibility, and
- storage isolation. Include views and scripts in boundary enforcement.
-- Specify how systems exchange information, handle failed interactions, and keep
- derived information consistent. These choices belong to the relevant follow-up
- specifications rather than a universal messaging requirement.
-- Keep one PostgreSQL container initially. Detailed backup and failover design is
- future work; logical data separation does not provide infrastructure failover.
-- Record the calendar review date when a maintainer manually approves this ADR.
- REFINED records the agreed proposal; approval remains a separate manual step.
diff --git a/adr/approved/ADR2_http-api-contracts-and-tooling.md b/adr/approved/ADR2_http-api-contracts-and-tooling.md
deleted file mode 100644
index 6c509b3..0000000
--- a/adr/approved/ADR2_http-api-contracts-and-tooling.md
+++ /dev/null
@@ -1,176 +0,0 @@
-**ID:** ADR2 \
-**Title:** HTTP API contracts and tooling \
-**Supersedes:** - \
-**Status:** APPROVED \
-**Expiration Date:** Six months after approval; record the calendar date upon manual approval.
-
-## Context
-
-Agile Dancers currently uses REST without an explicit architectural decision.
-The landscape needs agreed defaults for communication across system boundaries,
-contract documentation, client generation, compatibility, and error handling.
-
-Interactions include frontend access, requests between business systems,
-delegation of long-running calculations to workers, change notifications, and
-external consumers. Most clients and servers are developed by the same maintainer.
-Consumers must nevertheless be able to upgrade independently. The independently
-deployable systems proposed in ADR1 are relevant context; ADR1 remains REFINED
-and is not approved through this decision.
-
-The current frontend uses Angular, but future clients may use other frameworks
-or languages. Contracts and tooling should support that flexibility.
-
-For now, interactions use a defined alternative path when another system is
-offline. Durable queued delivery is not a current requirement. More specialized
-communication mechanisms may become appropriate for particular interactions.
-
-## Decisions
-
-### Scope and communication style
-
-Use HTTP APIs with JSON payloads as the default across system boundaries.
-Use resource-oriented endpoints where natural and action-oriented endpoints
-where they express business intent more clearly. Do not force business actions
-into CRUD operations. For example:
-
-```text
-GET /api/v1/schedules/{id}
-POST /api/v1/schedules/{id}/calculate
-```
-
-Action endpoints still follow HTTP method and status semantics. For long-running
-work, use an explicit job or operation resource with a status endpoint when the
-caller needs to track progress. A notification can initially use a direct HTTP
-request with documented failure behavior; this does not promise durable delivery.
-
-These are landscape defaults. Document justified exceptions alongside the
-relevant interface, including the reason and the alternative contract and failure
-behavior. This ADR does not mandate GraphQL, RPC, or messaging infrastructure.
-
-### Contracts and documentation
-
-Describe HTTP APIs using OpenAPI. Normally generate the description from server
-code, adding explicit descriptions and annotations where needed. Review the
-resulting contract as a public interface; generation alone does not ensure
-completeness or backward compatibility.
-
-Use self-hosted Scalar API Reference as the default interactive documentation UI.
-Keep the OpenAPI document independently available so consumers and tooling do not
-depend on the UI. Publish a contract artifact for each supported major version.
-
-Choose an OpenAPI specification version supported by the server tooling, Scalar,
-and the selected client generators. Do not automatically adopt the latest version
-without checking that compatibility.
-
-### Client generation
-
-Generate client transport code and contract types from OpenAPI by default, using
-OpenAPI Generator. Select the generator appropriate to the consuming technology;
-use `typescript-angular` for Angular clients. This choice does not require future
-frontends to use Angular.
-
-Pin generator versions and configuration for reproducible generation. Generate
-against a selected contract artifact rather than requiring a running server during
-the client build. Keep generated code separate from handwritten application logic
-and do not manually edit generated files. Generated static types do not by
-themselves provide runtime response validation.
-
-### Versioning and compatibility
-
-Put the major contract version in the URL, such as `/api/v1/schedules`.
-Keep backward-compatible changes within that major version. Introduce a new
-major version for breaking changes. Software release and generated client package
-versions may advance independently of the major contract version.
-
-Allow consumers to upgrade independently. Normally support one contract version;
-during transitions, aim to support two. This is a guideline rather than a hard
-cap: additional versions may coexist when justified. Document supported versions,
-deprecation, and the transition plan, coordinating retirement with affected
-consumers. Check compatibility before releasing contract changes.
-
-### Errors and offline behavior
-
-Standardize API error bodies on RFC 9457 Problem Details using
-`application/problem+json` and appropriate HTTP status codes. Use stable problem
-`type` identifiers for programmatic handling and readable explanations for humans.
-Clients must not depend on parsing human-readable messages. Document expected
-errors in OpenAPI, including validation details where applicable. Do not expose
-internal exception details or sensitive information in error responses.
-
-Define timeouts and the offline alternative path for each interaction. A network
-failure may produce no HTTP response, so consumers must handle that separately
-from a Problem Details response. Avoid automatic retries of actions unless
-repeating them is safe. Do not assume durable delivery or eventual replay.
-
-### Adoption and review
-
-Adopt incrementally for new interfaces and interfaces being substantially changed.
-Migrate existing interfaces through planned work while preserving compatibility
-for existing consumers. No immediate landscape-wide rewrite is required.
-
-Review six months after approval, or earlier if integration needs expose
-limitations. The review date triggers reevaluation rather than automatic expiry.
-
-## Options considered
-
-### Communication approach
-
-| Option | Pros | Cons | Outcome |
-| --- | --- | --- | --- |
-| Resource-oriented HTTP/JSON with action endpoints | Readable resources and business actions; supports different client technologies; retains HTTP semantics. | Requires consistent endpoint conventions and explicit handling of long-running work and failures. | Chosen as the default. |
-| GraphQL | Flexible data selection; typed schema; supports different consumer views of related data. | Adds query authorization and performance concerns; requires additional conventions for work delegation and notifications. | Not chosen: current needs and common client/server ownership do not justify the added complexity. |
-| RPC, including JSON-RPC and gRPC | Explicit operation contracts; JSON-RPC is lightweight; gRPC supports strongly typed generated clients. | Operation-oriented contracts require careful evolution; gRPC adds browser integration requirements. | Not chosen as the default: resource-oriented HTTP with optional actions better matches the desired interface flexibility. RPC remains a possible justified exception. |
-
-### Contract description and authoring
-
-| Option | Pros | Cons | Outcome |
-| --- | --- | --- | --- |
-| OpenAPI, normally generated from server code | Language-independent HTTP contracts; supports documentation and client generation; avoids separately maintaining a design language. | Needs annotations, review, and compatibility checks; generated output can be incomplete. | Chosen. |
-| TypeSpec generating OpenAPI | Dedicated contract authoring language; can generate descriptions and other artifacts. | Adds a language and compiler to maintain. | Not chosen: no current benefit sufficient to justify the additional tooling. |
-| RAML | API description language with reusable modeling constructs. | Its specification repository is archived, creating a maintenance concern for a new standard. | Not chosen. |
-
-### Interactive documentation
-
-| Option | Pros | Cons | Outcome |
-| --- | --- | --- | --- |
-| Scalar API Reference | Readable interactive documentation; integrated request testing; self-hosting and configuration options. | Requires integration and maintenance alongside the server framework. | Chosen: readability and testability are the primary purposes of the UI. |
-| Swagger UI | Interactive requests and authentication support; direct integration with Quarkus OpenAPI tooling. | Its expandable operation layout is less aligned with the preferred reading experience. | Not chosen as the landscape default. |
-
-### Client generation
-
-| Option | Pros | Cons | Outcome |
-| --- | --- | --- | --- |
-| OpenAPI Generator | Supports Angular and other client technologies in one tool family; configurable generation. | Generator-specific options and upgrades need management. | Chosen to support future technology changes. |
-| ng-openapi-gen | Angular-focused generation; npm-based tooling. | Other client technologies require another generator. | Not chosen as the landscape default. |
-| Handwritten clients | Direct control over client code and application integration. | Duplicates contract types and request details; increases drift risk. | Not chosen as the default. |
-
-## Consequences
-
-- Apply the defaults to new and substantially changed interfaces. Plan migration
- of existing interfaces incrementally, preserving consumer compatibility.
-- Generate and publish versioned OpenAPI artifacts. Add Scalar to provide
- interactive documentation and request testing for supported contracts.
-- Establish pinned OpenAPI Generator configurations for client technologies,
- initially Angular. Keep generated code separate from application logic.
-- Add contract validation and compatibility checks to release workflows. Verify
- generated clients against the actual contract before adopting generator upgrades.
-- Introduce URL major versions and coordinate deprecation with consumers. Normally
- maintain one version and temporarily two, documenting justified deviations.
-- Align API error responses with Problem Details and document expected failures.
- Define timeout and offline behavior per interaction, including safe retry rules.
-- Record justified exceptions alongside their interfaces rather than imposing an
- unsuitable communication mechanism on every interaction.
-- Record the calendar review date upon manual approval. REFINED captures the
- agreed proposal; approval remains a separate manual step.
-
-### References
-
-- [OpenAPI specification](https://spec.openapis.org/oas/)
-- [Scalar API Reference configuration](https://scalar.com/products/api-references/configuration)
-- [OpenAPI Generator: TypeScript Angular](https://openapi-generator.tech/docs/generators/typescript-angular/)
-- [RFC 9457: Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc9457.html)
-- [Swagger UI configuration](https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/)
-- [TypeSpec overview](https://learn.microsoft.com/en-us/azure/developer/typespec/overview/)
-- [RAML specification repository](https://github.com/raml-org/raml-spec)
-- [JSON-RPC specification](https://www.jsonrpc.org/specification)
-- [ng-openapi-gen](https://github.com/cyclosproject/ng-openapi-gen)
diff --git a/agiledancers-be/pom.xml b/agiledancers-be/pom.xml
index 5f45e1f..a58ea87 100644
--- a/agiledancers-be/pom.xml
+++ b/agiledancers-be/pom.xml
@@ -97,20 +97,20 @@
${maven.home}/bin/mvn${maven.multiModuleProjectDirectory}-B-ntp-f
- ${maven.multiModuleProjectDirectory}/tooling/standards/pom.xml
+ ${maven.multiModuleProjectDirectory}/generated/tooling/standards/pom.xmltestexec:javacoverageverifyexec${java.home}/bin/java
- ${maven.multiModuleProjectDirectory}/tooling/coverage/Coverage.java
+ ${maven.multiModuleProjectDirectory}/generated/tooling/coverage/Coverage.java${project.build.directory}/site/jacoco/jacoco.xmlcombined-coveragenoneexec${java.home}/bin/java
- ${maven.multiModuleProjectDirectory}/tooling/coverage/Coverage.java
+ ${maven.multiModuleProjectDirectory}/generated/tooling/coverage/Coverage.java${project.build.directory}/site/jacoco-combined/jacoco.xml
diff --git a/ci/runner/agiledancers-runner.service b/ci/runner/runner.service
similarity index 100%
rename from ci/runner/agiledancers-runner.service
rename to ci/runner/runner.service
diff --git a/deploy/health.sh b/deploy/health.sh
new file mode 100644
index 0000000..47393f7
--- /dev/null
+++ b/deploy/health.sh
@@ -0,0 +1,7 @@
+# Application-specific readiness, retained with each release by engineering.
+application_health() {
+ frontend=$(compose ps -q frontend)
+ docker_cmd exec "$frontend" wget -q -T 5 -O /dev/null http://backend:8080/q/health/ready || return 1
+ curl --noproxy '*' --fail --silent --show-error --max-time 5 "http://$BIND_IP:$FRONTEND_PORT/" -o /dev/null || return 1
+ curl --noproxy '*' --fail --silent --show-error --max-time 5 "http://$BIND_IP:$FRONTEND_PORT/api/dance-schools" -o /dev/null
+}
diff --git a/docs/S19-classification.md b/docs/S19-classification.md
new file mode 100644
index 0000000..84e047f
--- /dev/null
+++ b/docs/S19-classification.md
@@ -0,0 +1,3 @@
+# S19 resource ownership
+
+The complete [migration inventory](http://192.168.2.28:3000/wanjakrah/landscape/src/branch/main/S19-classification.md) records every source resource and its destination.
diff --git a/docs/S19-validation.md b/docs/S19-validation.md
new file mode 100644
index 0000000..61d66d9
--- /dev/null
+++ b/docs/S19-validation.md
@@ -0,0 +1,36 @@
+# S19 validation
+
+The [landscape specification](http://192.168.2.28:3000/wanjakrah/landscape/src/branch/main/specs/S19.md)
+and [resource inventory](http://192.168.2.28:3000/wanjakrah/landscape/src/branch/main/S19-classification.md)
+describe the repository split. Engineering owns the shared source; this project
+consumes its selected tag and commit through engineering.lock.json.
+
+Validated on 2026-11-08:
+
+- Full Maven verification: 73 backend tests and 19 standards regression tests.
+ Existing approved waiver retained; no new waiver or threshold change.
+- Full shared pipeline: frontend lint/build, 32 frontend tests, 4 Playwright
+ tests, both prebuilt Docker images and combined backend coverage
+ 1771/1841 (96.20%).
+- Standalone backend and frontend Docker source builds passed. The backend used
+ a disposable PostgreSQL container and BuildKit secrets for test configuration.
+- Shared CI/release regression suites: 19 and 34 tests respectively.
+- Engineering bootstrap/generator suite: 7 tests, covering all component choices,
+ actual Maven regeneration with generated/ absent, drift including new files,
+ obsolete-file removal, moved tags and preservation of project-owned files.
+- Formatter integration checks passed. Upstream skills and attribution files
+ match their source snapshot byte for byte; only the skills index was adapted.
+- A separate generated example passed its common pipeline and a real isolated
+ deployment of two synthetic revisions followed by rollback. Production was
+ not used. See engineering's VALIDATION.md and tests/acceptance.py.
+
+Gitea build-run evidence for the migrated application commit is recorded with
+S19 in landscape. Its workflow checks the pinned release before invoking the
+same shared pipeline. Checkout and committed generated resources require no
+sibling repository checkout.
+
+Production deployment is a separate action. Live NAS SSH authentication,
+production credentials and rollback against pre-S19 retained releases were not
+exercised. The existing Compose project, database volume and release-state schema
+remain compatible. Legacy releases without retained health.sh use the current
+application health function; review that compatibility before production rollback.
diff --git a/docs/backend-review.md b/docs/backend-review.md
index 1cf4e9d..c6fb0b0 100644
--- a/docs/backend-review.md
+++ b/docs/backend-review.md
@@ -1,19 +1,10 @@
-# Backend review companion
+# Agile Dancers backend review companion
-Use alongside `skills/code-review/SKILL.md` and `CODING_STANDARDS.md`.
+Use [the shared review guidance](../generated/docs/backend-review.md),
+[code-review skill](../generated/skills/code-review/SKILL.md), and
+[project standards](../CODING_STANDARDS.md).
-Review changed code for intent-revealing comments, missing public-contract Javadoc,
-appropriate record use, defensive copying of mutable record components, and
-compatibility with APIs, JSON and persistence. Assess ambiguous `var` initializers
-and framework/lifecycle exceptions to field finality. Confirm architecture and
-injection decisions make sense for production and tests. Check NIO API preference,
-varargs for trailing array parameters, and Java 25 IO use for direct console
-input/output; retain System.err for diagnostics. Require Javadoc for public
-classes and methods beyond basic POJO/data-carrier behavior.
-
-Read standards reports even when verification passes. Assess every review signal
-in changed code, including fan-out, public methods and record component counts.
-Record a rationale for retained signals in the S7 evidence or change description.
-Check narrowly scoped waivers against recorded maintainer agreement and reason.
-Never introduce blanket exclusions or weaken thresholds to pass a build.
-Do not repeat mechanical checks already enforced by tooling; verify their result.
+The backend module is agiledancers-be. The approved Group constructor waiver
+remains application-owned in tooling/standards/waivers.tsv. Assess retained
+signals against the implementation evidence in docs/validation and the change
+description. S19 introduces no standards waiver.
diff --git a/docs/gitea-ci.md b/docs/gitea-ci.md
index 78649cc..65199df 100644
--- a/docs/gitea-ci.md
+++ b/docs/gitea-ci.md
@@ -12,7 +12,7 @@ zoals `[skip ci]`; dit is expliciet toegestaan.
## Java-bronbestanden uitvoeren
De CI- en setuphulpmiddelen zijn Java 25-bronbestanden. Start ze direct met
-`java ci/Ci.java ` vanuit de repositoryroot. De Java-launcher compileert
+`java generated/ci/Ci.java ` vanuit de repositoryroot. De Java-launcher compileert
de benodigde bronbestanden in het geheugen. Er is geen aparte `javac`-stap,
Maven-module, Python-installatie of scriptingframework nodig. De helper gebruikt
alleen de JDK, inclusief de HTTP-client, process builder en bestandsloten.
@@ -76,7 +76,7 @@ Je kunt de instelling ook samenvoegen met behoud van bestaande instellingen en
backup via:
```sh
-java ci/Ci.java configure-docker "$HOME/.docker/daemon.json"
+java generated/ci/Ci.java configure-docker "$HOME/.docker/daemon.json"
```
Voor Docker Engine op Linux is dat dezelfde toevoeging aan
@@ -112,7 +112,7 @@ Het gewone jobtoken van Gitea 1.25 biedt geen packagepublicatie; gebruik een PAT
Gebruik een repository-runnerregistratietoken, geen instancebrede registratie.
Sla tokenwaarden buiten Git op en plaats ze niet in opdrachten, screenshots of logs.
-Voor reproduceerbare configuratie bestaat `java ci/Ci.java configure-gitea`. Maak twee
+Voor reproduceerbare configuratie bestaat `java generated/ci/Ci.java configure-gitea`. Maak twee
bestanden buiten de repository met mode `600`: één voor een PAT met voldoende
repositorybeheerrechten (repository Actions, secrets, variabelen en runnerregistratie),
en één voor de publicatie-PAT. De beheer-PAT hoeft niet als workflowsecret te worden
@@ -124,7 +124,7 @@ export GITEA_ADMIN_TOKEN_FILE="$HOME/.config/agiledancers-runner/admin-token"
export REGISTRY_TOKEN_FILE="$HOME/.config/agiledancers-runner/registry-token"
export REGISTRY_USER=wanjakrah
export TARGET_PLATFORM=linux/amd64 # alleen als dit op de Synology is vastgesteld
-java ci/Ci.java configure-gitea
+java generated/ci/Ci.java configure-gitea
```
Het script stelt de repository-instellingen in via de API en schrijft het
@@ -143,9 +143,9 @@ testdiagnostiek; gepubliceerde containerimages worden niet automatisch verwijder
```sh
export JAVA_HOME=/usr/lib/jvm/java-25-openjdk-amd64
-java ci/Ci.java install-runner --prepare-only
+java generated/ci/Ci.java install-runner --prepare-only
# Na Dockerconfiguratie en het opslaan van het registratietoken:
-java ci/Ci.java install-runner
+java generated/ci/Ci.java install-runner
systemctl --user status agiledancers-runner
journalctl --user -u agiledancers-runner -f
```
@@ -200,8 +200,8 @@ De frontend luistert op containerpoort 8080, ondersteunt SPA-routes en proxy't
8080. Voorbeelden voor eventueel handmatig starten staan los van de pipeline;
de pipeline start geen blijvende applicatie op de NAS.
-`java ci/Ci.java verify` maakt een succesmarker pas na alle builds en tests.
-`java ci/Ci.java publish` vereist die marker en hoofdbranchref. Onder een gedeeld Java-bestandslot (`FileChannel`)
+`java generated/ci/Ci.java verify` maakt een succesmarker pas na alle builds en tests.
+`java generated/ci/Ci.java publish` vereist die marker en hoofdbranchref. Onder een gedeeld Java-bestandslot (`FileChannel`)
buiten de checkout worden bestaande commitimages hergebruikt en ontbrekende
images geüpload. Iedere image wordt teruggehaald, op revisielabel gecontroleerd en
met digest vastgelegd in de diagnostiek. Authenticatie- of netwerkfouten tellen
@@ -238,7 +238,7 @@ blijven de committags bruikbaar en worden de oude `main`-updates overgeslagen.
Lokale controle van publicatie-/foutgedrag zonder registrywijzigingen:
```sh
-java ci/Ci.java test
+java generated/ci/Ci.java test
```
Volledige lokale verificatie met Docker en de vastgestelde platformwaarde:
@@ -246,7 +246,7 @@ Volledige lokale verificatie met Docker en de vastgestelde platformwaarde:
```sh
export JAVA_HOME=/usr/lib/jvm/java-25-openjdk-amd64
export TARGET_PLATFORM=linux/amd64 # eerst de Synology controleren
-java ci/Ci.java verify
+java generated/ci/Ci.java verify
```
## Operationele oplevering
diff --git a/docs/production-deployment.md b/docs/production-deployment.md
index 8c68c41..06ffc2f 100644
--- a/docs/production-deployment.md
+++ b/docs/production-deployment.md
@@ -136,8 +136,9 @@ sudo -n /usr/local/bin/docker compose version
sudo -n /usr/local/bin/docker info --format '{{.OSType}}/{{.Architecture}}'
```
-Als DSM geen `visudo` bevat, is `deploy/enable-docker.sh` het eenmalige alternatief.
-Kopieer het naar de eigen NAS-thuismap en voer het daar met sudo uit. Het maakt
+Als DSM geen `visudo` bevat, is `generated/deploy/enable-docker.sh` het eenmalige alternatief.
+Kopieer het naar de eigen NAS-thuismap en voer daar
+`sudo sh enable-docker.sh agiledancers-deploy /usr/local/bin/docker` uit. Het maakt
uitsluitend de bovengenoemde regel, controleert die door als deploymentaccount
Docker te starten en verwijdert een nieuwe regel als die controle faalt. Een
afwijkende bestaande regel wordt niet overschreven. De implementatiesessie heeft
@@ -177,7 +178,7 @@ inrichten. Bewaar `admin-token`, `s6-release-token`, `s6-pull-token` en de priva
`~/.ssh/known_hosts` en voer uit:
```sh
-java ci/Ci.java configure-deployment
+java generated/ci/Ci.java configure-deployment
```
Defaults zijn de bevestigde NAS/account/SSH-poort, repository
@@ -269,8 +270,9 @@ nieuwe release door te drukken.
controlescript uitvoeren met bestaande lockowner en gekozen release-directory:
```sh
- ssh -i -p @ \
- 'timeout -k 5 300 sh -s -- apply ' < deploy/remote.sh
+ cat deploy/health.sh generated/deploy/remote.sh | \
+ ssh -i -p @ \
+ 'COMPOSE_PROJECT=agiledancers-production DEPLOY_ROLES="backend frontend" DEPENDENCY_SERVICES=database REQUIRED_SECRETS=DATABASE_PASSWORD TARGET_PLATFORM=linux/amd64 timeout -k 5 300 sh -s -- apply '
```
Gebruik de eerder gepinde hostkey en gecontroleerde waarden uit het journal.
@@ -294,9 +296,9 @@ apart: imageherstel maakt geen migratie ongedaan. Rollback naar een oude release
van vóór S16 kan de database niet lezen. Backup/restore blijven S17-werk.
```sh
-java ci/Ci.java test
-java ci/Ci.java release-test
-sh -n deploy/remote.sh
+java generated/ci/Ci.java test
+java generated/ci/Ci.java release-test
+sh -n generated/deploy/remote.sh
mvn -N -Dformat.mode=check exec:exec@format
```
diff --git a/docs/s18-validation.md b/docs/s18-validation.md
index 0e09ab4..4126f37 100644
--- a/docs/s18-validation.md
+++ b/docs/s18-validation.md
@@ -1,3 +1,5 @@
+> Historical validation evidence from application commit `bd898e92d22f6c412e671908770f4d89c8ae12bc`. Commands retain their original paths; current tooling is documented in README.md.
+
# S18 implementation validation
Validated on 2026-11-03 against the confirmed S18 specification.
diff --git a/docs/s2-validation.md b/docs/s2-validation.md
index 7b2a6df..efac27e 100644
--- a/docs/s2-validation.md
+++ b/docs/s2-validation.md
@@ -1,3 +1,5 @@
+> Historical validation evidence from application commit `bd898e92d22f6c412e671908770f4d89c8ae12bc`. Commands retain their original paths; current tooling is documented in README.md.
+
# S2 implementation evidence
Validated on the development mini-PC on 2026-10-29: AMD Ryzen 7 7730U (8 cores,
@@ -58,7 +60,7 @@ Prettier. The selected configuration uses only Prettier. Staged scopes are:
- All: all 52 maintained frontend files, including non-applicable files that the
hook must skip, plus a small TS preservation-test fixture; generated lockfile
excluded. Exact paths and raw measurements are in
- [the timing report](s2-hook-timings.json).
+ [the timing report](http://192.168.2.28:3000/wanjakrah/agiledancers/src/commit/bd898e92d22f6c412e671908770f4d89c8ae12bc/docs/s2-hook-timings.json).
| Candidate | Staged scope | Run 1 | Run 2 | Run 3 |
| --- | --- | --- | --- | --- |
diff --git a/docs/s4-validation.md b/docs/s4-validation.md
index 2d65209..19edb64 100644
--- a/docs/s4-validation.md
+++ b/docs/s4-validation.md
@@ -1,3 +1,5 @@
+> Historical validation evidence from application commit `bd898e92d22f6c412e671908770f4d89c8ae12bc`. Commands retain their original paths; current tooling is documented in README.md.
+
# S4 verification evidence
Updated: 2026-10-29. S4 is **complete**. Main publication/pullback passed in run
diff --git a/docs/s6-validation.md b/docs/s6-validation.md
index 88696f0..8c72f02 100644
--- a/docs/s6-validation.md
+++ b/docs/s6-validation.md
@@ -1,3 +1,5 @@
+> Historical validation evidence from application commit `bd898e92d22f6c412e671908770f4d89c8ae12bc`. Commands retain their original paths; current tooling is documented in README.md.
+
# S6 implementation and operational evidence
Status: IN_REVIEW. Production `v1.0.0` is running on the Synology at
diff --git a/specs/S12-validation.md b/docs/validation/S12-validation.md
similarity index 96%
rename from specs/S12-validation.md
rename to docs/validation/S12-validation.md
index ca746ee..0175bd1 100644
--- a/specs/S12-validation.md
+++ b/docs/validation/S12-validation.md
@@ -1,3 +1,5 @@
+> Historical validation evidence from application commit `bd898e92d22f6c412e671908770f4d89c8ae12bc`. Commands retain their original paths; current tooling is documented in README.md.
+
# S12 implementation validation
## Delivered behavior
diff --git a/specs/S16-validation.md b/docs/validation/S16-validation.md
similarity index 98%
rename from specs/S16-validation.md
rename to docs/validation/S16-validation.md
index 4979c80..da46f24 100644
--- a/specs/S16-validation.md
+++ b/docs/validation/S16-validation.md
@@ -1,3 +1,5 @@
+> Historical validation evidence from application commit `bd898e92d22f6c412e671908770f4d89c8ae12bc`. Commands retain their original paths; current tooling is documented in README.md.
+
# S16 implementation validation
## Delivered behavior
diff --git a/specs/implemented/S7-review-signals.txt b/docs/validation/S7-review-signals.txt
similarity index 100%
rename from specs/implemented/S7-review-signals.txt
rename to docs/validation/S7-review-signals.txt
diff --git a/specs/implemented/S7-validation.md b/docs/validation/S7-validation.md
similarity index 98%
rename from specs/implemented/S7-validation.md
rename to docs/validation/S7-validation.md
index 2a2ae48..874da50 100644
--- a/specs/implemented/S7-validation.md
+++ b/docs/validation/S7-validation.md
@@ -1,3 +1,5 @@
+> Historical validation evidence from application commit `bd898e92d22f6c412e671908770f4d89c8ae12bc`. Commands retain their original paths; current tooling is documented in README.md.
+
# S7 implementation and validation evidence
Status: IN_REVIEW — implementation complete; awaiting project review.
diff --git a/specs/implemented/S8-formatting-evidence.md b/docs/validation/S8-formatting-evidence.md
similarity index 96%
rename from specs/implemented/S8-formatting-evidence.md
rename to docs/validation/S8-formatting-evidence.md
index 0725125..a3fb208 100644
--- a/specs/implemented/S8-formatting-evidence.md
+++ b/docs/validation/S8-formatting-evidence.md
@@ -1,3 +1,5 @@
+> Historical validation evidence from application commit `bd898e92d22f6c412e671908770f4d89c8ae12bc`. Commands retain their original paths; current tooling is documented in README.md.
+
# S8 implementation evidence
Measured on 2026-10-30 on Linux x86_64 (kernel 7.0.0-38-generic), AMD Ryzen 7
diff --git a/engineering.lock.json b/engineering.lock.json
new file mode 100644
index 0000000..646ff93
--- /dev/null
+++ b/engineering.lock.json
@@ -0,0 +1,5 @@
+{
+ "repository": "http://192.168.2.28:3000/wanjakrah/engineering.git",
+ "tag": "v1.0.0",
+ "commit": "c25ce11a1bb8bf97f24d5e2b12d98680a9c573ca"
+}
diff --git a/engineering.properties b/engineering.properties
new file mode 100644
index 0000000..5e51de6
--- /dev/null
+++ b/engineering.properties
@@ -0,0 +1,30 @@
+# Project-owned configuration; environment variables override these defaults.
+PROJECT_NAME=agiledancers
+BACKEND_MODULE=agiledancers-be
+FRONTEND_MODULE=agiledancers-fe
+FORMAT_DIRECTORIES=agiledancers-be/src/main/java,agiledancers-be/src/test/java
+BACKEND_CONTEXT=.
+BROWSER_TESTS=true
+CI_DATABASE=true
+DATABASE_NAME=agiledancers
+DATABASE_USER=agiledancers
+CI_READINESS_URL=http://127.0.0.1:8080/api/dance-schools
+COMPOSE_PROJECT=agiledancers-production
+DEPENDENCY_SERVICES=database
+REQUIRED_DEPLOY_SECRETS=DATABASE_PASSWORD
+TARGET_PLATFORM=linux/amd64
+GITEA_SERVER_URL=http://192.168.2.28:3000
+CI_REPOSITORY=wanjakrah/agiledancers
+INSECURE_REGISTRY=192.168.2.28:3000
+RUNNER_LABEL=agiledancers-mini-pc
+DEPLOY_HOST=192.168.2.28
+DEPLOY_USER=agiledancers-deploy
+DEPLOY_SSH_PORT=22
+DEPLOY_DIRECTORY=/volume1/docker/agiledancers-production
+DEPLOY_BIND_IP=192.168.2.28
+DEPLOY_FRONTEND_PORT=8088
+DEPLOY_REGISTRY=127.0.0.1:3000
+DEPLOY_DOCKER_BIN=/usr/local/bin/docker
+RELEASE_TOKEN_FILE=${user.home}/.config/agiledancers-runner/s6-release-token
+PULL_TOKEN_FILE=${user.home}/.config/agiledancers-runner/s6-pull-token
+DEPLOY_SSH_KEY_FILE=${user.home}/.config/agiledancers-runner/s6-deploy-key
diff --git a/specs/implemented/S7_define-coding-standards-to-be b/generated/CODING_STANDARDS.md
similarity index 50%
rename from specs/implemented/S7_define-coding-standards-to-be
rename to generated/CODING_STANDARDS.md
index 6e1ef97..aaad96e 100644
--- a/specs/implemented/S7_define-coding-standards-to-be
+++ b/generated/CODING_STANDARDS.md
@@ -1,28 +1,32 @@
-# S7 — Backend coding standards
+# Backend coding standards
-Status: IMPLEMENTED
-
-Shared understanding confirmed after the grill-me review.
-
-## User story
-
-As a developer, I want developers and agents to follow explicit coding standards
-for `agiledancers-be`, so that the code stays readable and maintainable.
+This document is the authoritative policy for handwritten production and test Java
+in ``. Generated code is out of scope. The Java conventions below also apply to
+handwritten Java helpers outside that module. Frontend conventions are owned by each consuming project.
## Scope and authority
-- Create root `CODING_STANDARDS.md` with the complete agreed backend rules.
- Preserve any existing frontend instructions.
-- Cover handwritten production and test Java in `agiledancers-be`. Exclude
+- Cover handwritten production and test Java in ``. Exclude
generated code. Dependency injection and production architecture rules apply
- where relevant. Frontend and Java helpers outside the backend are out of scope.
-- Use https://www.geeksforgeeks.org/java/coding-guidelines-in-java/ as source
- material, not as a changing normative dependency. The local document is complete
- and authoritative for the adopted rules.
+ where relevant. The Java conventions also cover Java helpers outside the backend; backend
+ architecture, metrics, coverage and automated checks remain backend-only.
+ Frontend code is out of scope.
+- [Java coding guidelines](https://www.geeksforgeeks.org/java/coding-guidelines-in-java/)
+ supplied source material. Only the rules adopted in this local document apply;
+ changes to the article do not change project policy.
- The project's pinned Palantir formatter wins formatting conflicts, including
comment layout. S8 remains responsible for formatter integration.
- Do not adopt unrelated static-analysis rule bundles.
+## General language guidance
+
+- Always prefer newer stable official APIs, language features and officially
+ recommended coding practices over older alternatives when they suit the task
+ and are supported by the project's configured language/runtime version.
+ Preserve required behavior and compatibility; document concrete reasons for
+ retaining an older approach. This guidance applies to every language used in
+ the project.
+
## Java conventions
- Types use UpperCamelCase; methods and variables use lowerCamelCase; constants
@@ -32,9 +36,19 @@ for `agiledancers-be`, so that the code stays readable and maintainable.
- Opening braces stay on the declaration/control-statement line. All `if`, `for`
and `while` bodies require braces, including enhanced for and do/while bodies.
- No wildcard imports. Static imports are allowed only in test classes.
-- Comments explain intent or constraints. Document public contracts with Javadoc
- where their meaning is not clear from the declaration; do not require boilerplate
- documentation for every accessor.
+- Prefer `java.nio` APIs (including `java.nio.file` and `java.nio.charset`) over
+ equivalent `java.io` APIs. Use stream APIs where no suitable NIO equivalent
+ exists, such as process pipes or APIs requiring streams.
+- Use varargs (`Type... values`) when an array is the last method or constructor
+ parameter, including `main(String... args)`. Preserve required override signatures
+ and framework/API compatibility.
+- Prefer Java 25 `IO.print`/`IO.println` for direct standard output and `IO.readln`
+ for line-oriented standard input. `IO` has no standard-error variant; retain
+ `System.err` for error output. Use application logging where appropriate.
+- Comments explain intent or constraints. Require Javadoc on public classes and
+ public methods that do more than basic POJO/data-carrier work. Trivial accessors
+ and plain data carriers do not need boilerplate documentation. Describe behavior,
+ constraints and failure conditions where relevant.
- Block comments use the following layout, subject to Palantir formatting:
```java
@@ -162,50 +176,101 @@ exactly at a blocking limit passes.
S7's required implementation. Configuring Sonar in the pipeline remains out of
scope for S14. FindBugs/SpotBugs is not an additional required check suite.
-## Existing-code migration
-- Bring existing backend production and test Java into compliance, including
- structural refactoring where necessary.
-- Migration is complete when all blocking rules pass and all review signals have
- been assessed, with rationale recorded for retained signals. Reaching every
- target is not required.
-- Preserve public APIs, JSON contracts, persistence formats and application
- behavior. Any necessary contract change requires a separate decision.
-- Separate enforcement/configuration changes from code refactoring for review.
- Strengthen tests where migration affects behavior-sensitive code.
+## Implemented checks and manual review
-## Acceptance criteria
+Checkstyle **12.3.1**, PMD/CPD **7.28.0**, and JaCoCo **0.8.14** are pinned.
+`generated/tooling/standards/checkstyle.xml` selects individual naming, structure, import and brace checks; no general analysis bundle is enabled.
+`generated/tooling/standards/src/main/java/Standards.java` supplies the targeted measurements.
+ArchUnit remains in the backend test suite for architectural boundaries.
-- [x] CODING_STANDARDS.md contains all agreed rules and preserves frontend guidance.
-- [x] Agent and project code-review instructions enforce discovery and review of
- these standards without modifying upstream skills.
-- [x] Pinned tools, configurations and targeted checks implement the exact agreed
- metrics and identify manual-only rules.
-- [x] Verify and standalone checks report actionable blocking failures and review
- signals; CI prevents publication after a failure.
-- [x] Representative violations demonstrate that their checks fail, including
- relevant threshold boundaries and custom measurement semantics.
-- [x] Existing handwritten backend production and test code passes all blocking
- rules; retained review signals and approved waivers are documented.
-- [x] Local backend tests and combined CI coverage meet the agreed line-coverage
- gates; line and branch reports are available.
-- [x] Clean verification and behavior-preservation tests pass after migration.
-- [x] Evidence records tool versions, commands, scope, exclusions, metric and
- coverage reports, approved waivers and verification results.
+PMD uses Java 25, the running JDK's `jrt-fs.jar`, compiled backend classes and the
+resolved test dependency classpath. Cyclomatic complexity uses PMD's default
+options: boolean paths count, assertions do not. Decisions include if/loops,
+catch, ternary and throw; non-default switch alternatives count individually,
+including comma-separated labels. There is no aggregate class-complexity gate.
+Cognitive complexity uses PMD's boolean-operation sequences, nesting, switch,
+recursion and flow rules. The scoped visitors retain those algorithms while
+including lambdas in their enclosing executable and excluding nested/local type
+bodies, whose methods are measured independently. Compact record constructors
+use the same visitors and start with cyclomatic complexity one.
-## Implementation handoff
+Custom checks measure physical code lines from regular parser tokens, including
+brace/declaration lines and mixed code/comment lines. They count multiline literal
+lines while excluding physically blank lines. Class length excludes nested-type
+tokens, preserving lines shared with enclosing code. Other custom measurements
+implement the definitions above: nesting, resolved type fan-out, project-owned
+superclass depth, public non-accessor methods, and injected collaborators.
+Injection includes inherited fields/setters, explicit `@Inject` constructors,
+single constructors on bean classes, records' implicit canonical constructors,
+and Lombok required/all-argument constructor injection. Qualifier keys use resolved
+annotation names and values, including defaults; repeat sites are deduplicated.
+The CPD configuration preserves identifiers/literals, uses 100 tokens and disables
+skip blocks. Its line-location union counts overlapping occurrences only once.
-This specification records the agreed design. Updating it does not implement
-checks, change agent instructions, refactor application code or establish current
-coverage. Those are implementation work, with evidence required above.
+Checkstyle and targeted checks enforce import/brace/naming/visibility rules,
+`@Override`, visible constructor/cast/new-array `var` initializer forms, final parameters, locals (including
+loops/catches and lambdas) and initialized fields. PMD's three selected finality
+rules additionally cover control-flow assignment and private constructor-initialized
+fields. Framework/lifecycle requirements, generated Lombok setters, and external
+writes to public/protected fields need review before changing field finality.
+Comment intent, Javadoc adequacy for nontrivial public contracts, record suitability
+and protection of mutable components require manual review. These are **manual
+rules**, as are ambiguous type inference, architecture/DI design decisions, NIO
+API selection, trailing-array varargs, console I/O selection and the POJO
+documentation boundary. Review all `DESIGN` record-component entries
+and `REVIEW` signals, even when a build passes, using `docs/backend-review.md`.
-## Implementation evidence
+Reports never rewrite Java. Formatter application remains S8's separate validate
+execution. Generated output under `target` and generated source directories are
+outside the handwritten `src/main/java` and `src/test/java` scope. No maintained
+file, DTO, configuration class or package is excluded from standards or coverage.
+JaCoCo filters compiler/Lombok-generated members; no handwritten-code exclusions
+are configured. Line and branch results are published, with only lines gated.
-Implementation and verification are recorded in `specs/S7-validation.md`; retained
-review signals and the sole approved waiver are recorded in
-`specs/S7-review-signals.txt` and `tooling/standards/waivers.tsv`.
+Exact metric waivers live in `tooling/standards/waivers.tsv`; each records a metric,
+fully resolved executable signature (or class), exact retained value and the
+maintainer agreement/reason. No wildcard waivers are allowed, and a changed value
+fails again. A waived blocking result remains visible as `WAIVER`. PMD suppression
+annotations/comments cannot silently waive a rule. Adding a waiver always requires maintainer agreement.
-The maintainer approved GroupDetails-based Java service signatures and the exact
-eight-parameter Group constructor waiver in the implementation conversation.
-REST/JSON contracts remain unchanged. Clean verify and browser/combined coverage
-checks pass. The implementation is ready for project review.
+## Commands and reports
+
+Run from the root with JDK 25:
+
+```sh
+mvn -Dformat.mode=check clean verify
+# Prepare standalone analysis without running tests:
+mvn -pl test-compile dependency:build-classpath@standards-classpath
+mvn -f generated/tooling/standards/pom.xml test exec:java
+# Exact local coverage gate after a test/report build:
+java generated/tooling/coverage/Coverage.java /target/site/jacoco/jacoco.xml
+```
+
+`verify` runs backend tests, builds resolved analysis inputs, generates JaCoCo
+reports, runs checker regression tests and standards analysis, then gates exact
+module-wide line coverage. `compile` and `test` retain existing formatting behavior
+and do not run the verify-only standards checks. Standalone analysis requires its
+classpath preparation; unresolved types fail with the path and preparation advice.
+
+Full metrics and findings: `/target/standards/report.txt`.
+Checker regression results: `generated/tooling/standards/target/surefire-reports`.
+Coverage HTML/XML/CSV: `/target/site/jacoco` (local backend tests)
+and `/target/site/jacoco-combined` (merged browser contribution).
+The gate compares integer line counts (`5 * covered >= 4 * total`), rejecting an
+empty report and percentages that would round up to 80%.
+
+CI first passes local verify. It mounts the pinned JaCoCo agent into its disposable
+backend test container, exposes the agent only on the isolated Docker network,
+and uses the pinned CLI to dump `target/playwright.exec` after Playwright. A missing
+or empty dump fails. The following commands merge both datasets and gate the result
+before the verified-commit marker can authorize publication:
+
+```sh
+mvn -pl jacoco:merge@combined jacoco:report@combined-report exec:exec@combined-coverage
+```
+
+CI retains both coverage reports, execution data, standards findings and regression
+results with its usual diagnostics artifacts. The agent is mounted only for tests;
+published images contain no agent. PIT and Sonar configuration remain outside S7.
+Application validation evidence and retained-signal assessments stay in the consuming project.
diff --git a/generated/SOURCE.json b/generated/SOURCE.json
new file mode 100644
index 0000000..d833c0a
--- /dev/null
+++ b/generated/SOURCE.json
@@ -0,0 +1,5 @@
+{
+ "commit": "c25ce11a1bb8bf97f24d5e2b12d98680a9c573ca",
+ "repository": "http://192.168.2.28:3000/wanjakrah/engineering.git",
+ "tag": "v1.0.0"
+}
diff --git a/ci/Ci.java b/generated/ci/Ci.java
similarity index 94%
rename from ci/Ci.java
rename to generated/ci/Ci.java
index 48e3c97..a7b369e 100644
--- a/ci/Ci.java
+++ b/generated/ci/Ci.java
@@ -9,8 +9,8 @@ public class Ci {
try {
if (Runtime.version().feature() < 25) throw new IllegalArgumentException("JDK 25 or newer is required");
if (args.length == 0) throw new IllegalArgumentException(usage());
- var env = new HashMap<>(System.getenv());
Path root = Path.of("").toAbsolutePath();
+ var env = Recipe.environment(root, System.getenv());
var commands = new Commands(Duration.ofMinutes(args[0].equals("publish") ? 15 : 75));
switch (args[0]) {
case "verify", "publish", "cleanup" -> {
@@ -32,7 +32,7 @@ public class Ci {
case "configure-gitea" -> Setup.configureGitea(env);
case "configure-docker" -> {
if (args.length != 2) throw new IllegalArgumentException("Supply the daemon.json path");
- Setup.configureDocker(Path.of(args[1]));
+ Setup.configureDocker(Path.of(args[1]), Setup.required(env, "INSECURE_REGISTRY"));
}
case "install-runner" ->
Setup.installRunner(root, env, commands, args.length == 2 && args[1].equals("--prepare-only"));
diff --git a/ci/CiTests.java b/generated/ci/CiTests.java
similarity index 89%
rename from ci/CiTests.java
rename to generated/ci/CiTests.java
index e1e4456..d33485f 100644
--- a/ci/CiTests.java
+++ b/generated/ci/CiTests.java
@@ -32,6 +32,26 @@ class CiTests {
static void run() throws Exception {
count = 0;
+ test("recipe supports backend-only and frontend-only projects", () -> {
+ equal(List.of("backend"), Recipe.roles(Map.of("BACKEND_MODULE", "server")));
+ equal(List.of("frontend"), Recipe.roles(Map.of("FRONTEND_MODULE", "web")));
+ failure(() -> Recipe.roles(Map.of()));
+ failure(() -> Recipe.project(Map.of("PROJECT_NAME", "../unsafe")));
+ });
+ test("project configuration is overridden by environment and rejects escaping modules", () -> {
+ final Path root = Files.createTempDirectory("recipe-config-");
+ try {
+ Files.writeString(
+ root.resolve("engineering.properties"), "PROJECT_NAME=sample\nBACKEND_MODULE=server\n");
+ equal(
+ "override",
+ Recipe.environment(root, Map.of("PROJECT_NAME", "override"))
+ .get("PROJECT_NAME"));
+ failure(() -> Recipe.environment(root, Map.of("BACKEND_MODULE", "../outside")));
+ } finally {
+ Pipeline.deleteTree(root);
+ }
+ });
test("JSON preserves configuration values and escaped text", () -> {
String json = "{\"text\":\"line\\nquote\\\"\\u20ac\",\"values\":[null,true,false,1,-2.5e2],\"nested\":{}}";
Object data = Json.parse(json);
@@ -49,14 +69,14 @@ class CiTests {
Path file = dir.resolve("daemon.json");
String original = "{\"features\":{\"buildkit\":true},\"insecure-registries\":[\"existing:5000\"]}";
Files.writeString(file, original);
- Setup.configureDocker(file);
+ Setup.configureDocker(file, "registry.example:3000");
String first = Files.readString(file);
- Setup.configureDocker(file);
+ Setup.configureDocker(file, "registry.example:3000");
equal(first, Files.readString(file));
equal(
- List.of("existing:5000", "192.168.2.28:3000"),
+ List.of("existing:5000", "registry.example:3000"),
Json.object(Json.parse(first)).get("insecure-registries"));
- equal(original, Files.readString(dir.resolve("daemon.json.before-agiledancers")));
+ equal(original, Files.readString(dir.resolve("daemon.json.before-engineering")));
} finally {
Pipeline.deleteTree(dir);
}
@@ -67,7 +87,7 @@ class CiTests {
Path file = dir.resolve("daemon.json");
String original = "{\"insecure-registries\":\"invalid\"}";
Files.writeString(file, original);
- failure(() -> Setup.configureDocker(file));
+ failure(() -> Setup.configureDocker(file, "registry.example:3000"));
equal(original, Files.readString(file));
} finally {
Pipeline.deleteTree(dir);
@@ -253,17 +273,20 @@ class CiTests {
"CI_REF",
"refs/heads/main",
"CI_REPOSITORY",
- "wanjakrah/agiledancers",
+ "owner/recipe-demo",
"TARGET_PLATFORM",
"linux/amd64",
"GITEA_SERVER_URL",
- "http://192.168.2.28:3000",
+ "http://registry.example:3000",
"REGISTRY_USER",
"test",
"REGISTRY_TOKEN",
"dummy-token",
"CI_PUBLISH_LOCK_DIR",
root.resolve("lock").toString()));
+ env.put("PROJECT_NAME", "recipe-demo");
+ env.put("BACKEND_MODULE", "backend");
+ env.put("FRONTEND_MODULE", "frontend");
pipeline = new Pipeline(root, env, fake) {
@Override
String mainHead() {
diff --git a/ci/Commands.java b/generated/ci/Commands.java
similarity index 100%
rename from ci/Commands.java
rename to generated/ci/Commands.java
diff --git a/ci/Json.java b/generated/ci/Json.java
similarity index 100%
rename from ci/Json.java
rename to generated/ci/Json.java
diff --git a/ci/Pipeline.java b/generated/ci/Pipeline.java
similarity index 58%
rename from ci/Pipeline.java
rename to generated/ci/Pipeline.java
index ad53b66..d1ac2da 100644
--- a/ci/Pipeline.java
+++ b/generated/ci/Pipeline.java
@@ -14,15 +14,15 @@ class Pipeline {
Pipeline(Path root, Map environment, Commands commands) throws Exception {
this.root = root;
- this.env = new HashMap<>(environment);
+ this.env = Recipe.environment(root, environment);
this.commands = commands;
commit = value("CI_COMMIT", () -> capture("git", "rev-parse", "HEAD"));
String id = value("CI_RUN_ID", () -> "local"), attempt = value("CI_RUN_ATTEMPT", () -> "1");
check(commit.matches("[0-9a-f]{40}"), "Invalid commit SHA");
check((id + "-" + attempt).matches("[a-zA-Z0-9-]+"), "Invalid run identifier");
- runName = "agiledancers-" + id + "-" + attempt;
- backendImage = "agiledancers-ci-backend:" + commit + "-" + id + "-" + attempt;
- frontendImage = "agiledancers-ci-frontend:" + commit + "-" + id + "-" + attempt;
+ runName = Recipe.project(env) + "-" + id + "-" + attempt;
+ backendImage = Recipe.project(env) + "-ci-backend:" + commit + "-" + id + "-" + attempt;
+ frontendImage = Recipe.project(env) + "-ci-frontend:" + commit + "-" + id + "-" + attempt;
artifacts = root.resolve("ci/artifacts");
Files.createDirectories(artifacts);
}
@@ -71,7 +71,7 @@ class Pipeline {
void isolateDocker() throws Exception {
value("DOCKER_HOST", () -> capture("docker", "context", "inspect", "--format", "{{.Endpoints.docker.Host}}"));
env.remove("DOCKER_CONTEXT");
- isolatedConfig = Files.createTempDirectory("agiledancers-docker-");
+ isolatedConfig = Files.createTempDirectory(Recipe.project(env) + "-docker-");
env.put("DOCKER_CONFIG", isolatedConfig.toString());
}
@@ -98,175 +98,180 @@ class Pipeline {
isolateDocker();
check(commit.equals(capture("git", "rev-parse", "HEAD")), "Checkout does not match pushed commit");
run("java", "-version");
- run("node", "--version");
- run("mvn", "-B", "-ntp", "-Dformat.mode=check", "clean", "verify");
+ if (Recipe.roles(env).contains("frontend")) run("node", "--version");
+ if (Recipe.roles(env).contains("backend"))
+ run("mvn", "-B", "-ntp", "-Dformat.mode=check", "clean", "verify");
CiTests.run();
ReleaseTests.run();
- Path frontend = root.resolve("agiledancers-fe");
- verifyFrontend();
+ Path frontend = root.resolve(env.getOrDefault("FRONTEND_MODULE", "."));
+ if (Recipe.roles(env).contains("frontend")) verifyFrontend();
String platform = env.get("TARGET_PLATFORM");
- run(
- "docker",
- "build",
- "--platform",
- platform,
- "--target",
- "prebuilt",
- "--label",
- "org.opencontainers.image.revision=" + commit,
- "-t",
- backendImage,
- ".");
- run(
- "docker",
- "build",
- "--platform",
- platform,
- "--target",
- "prebuilt",
- "--label",
- "org.opencontainers.image.revision=" + commit,
- "-t",
- frontendImage,
- "agiledancers-fe");
- Files.writeString(artifacts.resolve("containers-started"), "");
- run("docker", "network", "create", "--label", "agiledancers.ci.run=" + runName, runName);
- run(
- "docker",
- "run",
- "-d",
- "--name",
- runName + "-database",
- "--network",
- runName,
- "--network-alias",
- "database",
- "--env",
- "POSTGRES_USER=agiledancers",
- "--env",
- "POSTGRES_PASSWORD=ci-only",
- "--env",
- "POSTGRES_DB=agiledancers",
- "postgres:17.6");
- for (int attempt = 0; attempt < 60; attempt++) {
- if (result(false, "docker", "exec", runName + "-database", "pg_isready", "-U", "agiledancers")
- .code()
- == 0) break;
- Thread.sleep(1000);
+ for (String role : Recipe.roles(env)) {
+ run(
+ "docker",
+ "build",
+ "--platform",
+ platform,
+ "--target",
+ "prebuilt",
+ "--label",
+ "org.opencontainers.image.revision=" + commit,
+ "-t",
+ role.equals("backend") ? backendImage : frontendImage,
+ env.getOrDefault(
+ role.toUpperCase() + "_CONTEXT",
+ role.equals("backend") ? "." : env.get("FRONTEND_MODULE")));
+ }
+ if (Recipe.enabled(env, "BROWSER_TESTS")) {
+ Files.writeString(artifacts.resolve("containers-started"), "");
+ run("docker", "network", "create", "--label", Recipe.project(env) + ".ci.run=" + runName, runName);
+ if (Recipe.enabled(env, "CI_DATABASE")) {
+ run(
+ "docker",
+ "run",
+ "-d",
+ "--name",
+ runName + "-database",
+ "--network",
+ runName,
+ "--network-alias",
+ "database",
+ "--env",
+ "POSTGRES_USER=" + env.get("DATABASE_USER"),
+ "--env",
+ "POSTGRES_PASSWORD=ci-only",
+ "--env",
+ "POSTGRES_DB=" + env.get("DATABASE_NAME"),
+ "postgres:17.6");
+ for (int attempt = 0; attempt < 60; attempt++) {
+ if (result(
+ false,
+ "docker",
+ "exec",
+ runName + "-database",
+ "pg_isready",
+ "-U",
+ env.get("DATABASE_USER"))
+ .code()
+ == 0) break;
+ Thread.sleep(1000);
+ }
+ }
+ run(
+ "docker",
+ "run",
+ "-d",
+ "--name",
+ runName + "-backend",
+ "--label",
+ Recipe.project(env) + ".ci.run=" + runName,
+ "--platform",
+ platform,
+ "--network",
+ runName,
+ "--network-alias",
+ "backend",
+ "--memory",
+ "1g",
+ "--cpus",
+ "2",
+ "--volume",
+ root.resolve(env.get("BACKEND_MODULE") + "/target/coverage-tools/agent.jar")
+ + ":/jacoco-agent.jar:ro",
+ "--env",
+ "JAVA_TOOL_OPTIONS=-javaagent:/jacoco-agent.jar=output=tcpserver,address=*,port=6300",
+ "--env",
+ "DATABASE_URL=jdbc:postgresql://database:5432/" + env.get("DATABASE_NAME"),
+ "--env",
+ "DATABASE_USER=" + env.get("DATABASE_USER"),
+ "--env",
+ "DATABASE_PASSWORD=ci-only",
+ backendImage);
+ run(
+ "docker",
+ "run",
+ "-d",
+ "--name",
+ runName + "-frontend",
+ "--label",
+ Recipe.project(env) + ".ci.run=" + runName,
+ "--platform",
+ platform,
+ "--network",
+ runName,
+ "--network-alias",
+ "frontend",
+ "--memory",
+ "256m",
+ "--cpus",
+ "1",
+ frontendImage);
+ waitForApplication();
+ String user = capture("id", "-u") + ":" + capture("id", "-g");
+ run(
+ "docker",
+ "run",
+ "--name",
+ runName + "-browser",
+ "--label",
+ Recipe.project(env) + ".ci.run=" + runName,
+ "--platform",
+ platform,
+ "--network",
+ runName,
+ "--memory",
+ "2g",
+ "--cpus",
+ "2",
+ "--shm-size=1g",
+ "--user",
+ user,
+ "--env",
+ "HOME=/tmp",
+ "--env",
+ "CI=true",
+ "--env",
+ "PLAYWRIGHT_BASE_URL=http://frontend:8080",
+ "--volume",
+ frontend + ":/workspace",
+ "--workdir",
+ "/workspace",
+ env.getOrDefault("PLAYWRIGHT_IMAGE", "mcr.microsoft.com/playwright:v1.64.1-noble"),
+ "npm",
+ "run",
+ "e2e");
+ run(
+ "docker",
+ "run",
+ "--rm",
+ "--network",
+ runName,
+ "--user",
+ user,
+ "--volume",
+ root.resolve(env.get("BACKEND_MODULE") + "/target") + ":/coverage",
+ "eclipse-temurin:25-jre",
+ "java",
+ "-jar",
+ "/coverage/coverage-tools/cli.jar",
+ "dump",
+ "--address",
+ "backend",
+ "--destfile",
+ "/coverage/playwright.exec");
+ check(
+ Files.size(root.resolve(env.get("BACKEND_MODULE") + "/target/playwright.exec")) > 0,
+ "Playwright backend coverage is missing");
+ run(
+ "mvn",
+ "-B",
+ "-ntp",
+ "-pl",
+ env.get("BACKEND_MODULE"),
+ "jacoco:merge@combined",
+ "jacoco:report@combined-report",
+ "exec:exec@combined-coverage");
}
- run(
- "docker",
- "run",
- "-d",
- "--name",
- runName + "-backend",
- "--label",
- "agiledancers.ci.run=" + runName,
- "--platform",
- platform,
- "--network",
- runName,
- "--network-alias",
- "backend",
- "--memory",
- "1g",
- "--cpus",
- "2",
- "--volume",
- root.resolve("agiledancers-be/target/coverage-tools/agent.jar") + ":/jacoco-agent.jar:ro",
- "--env",
- "JAVA_TOOL_OPTIONS=-javaagent:/jacoco-agent.jar=output=tcpserver,address=*,port=6300",
- "--env",
- "DATABASE_URL=jdbc:postgresql://database:5432/agiledancers",
- "--env",
- "DATABASE_USER=agiledancers",
- "--env",
- "DATABASE_PASSWORD=ci-only",
- backendImage);
- run(
- "docker",
- "run",
- "-d",
- "--name",
- runName + "-frontend",
- "--label",
- "agiledancers.ci.run=" + runName,
- "--platform",
- platform,
- "--network",
- runName,
- "--network-alias",
- "frontend",
- "--memory",
- "256m",
- "--cpus",
- "1",
- frontendImage);
- waitForApplication();
- String user = capture("id", "-u") + ":" + capture("id", "-g");
- run(
- "docker",
- "run",
- "--name",
- runName + "-browser",
- "--label",
- "agiledancers.ci.run=" + runName,
- "--platform",
- platform,
- "--network",
- runName,
- "--memory",
- "2g",
- "--cpus",
- "2",
- "--shm-size=1g",
- "--user",
- user,
- "--env",
- "HOME=/tmp",
- "--env",
- "CI=true",
- "--env",
- "PLAYWRIGHT_BASE_URL=http://frontend:8080",
- "--volume",
- frontend + ":/workspace",
- "--workdir",
- "/workspace",
- "mcr.microsoft.com/playwright:v1.64.1-noble",
- "npm",
- "run",
- "e2e");
- run(
- "docker",
- "run",
- "--rm",
- "--network",
- runName,
- "--user",
- user,
- "--volume",
- root.resolve("agiledancers-be/target") + ":/coverage",
- "eclipse-temurin:25-jre",
- "java",
- "-jar",
- "/coverage/coverage-tools/cli.jar",
- "dump",
- "--address",
- "backend",
- "--destfile",
- "/coverage/playwright.exec");
- check(
- Files.size(root.resolve("agiledancers-be/target/playwright.exec")) > 0,
- "Playwright backend coverage is missing");
- run(
- "mvn",
- "-B",
- "-ntp",
- "-pl",
- "agiledancers-be",
- "jacoco:merge@combined",
- "jacoco:report@combined-report",
- "exec:exec@combined-coverage");
Files.writeString(artifacts.resolve("verified-commit"), commit + "\n");
} catch (Exception e) {
failure = e;
@@ -289,23 +294,17 @@ class Pipeline {
}
void verifyFrontend() throws Exception {
- Path frontend = root.resolve("agiledancers-fe");
+ Path frontend = root.resolve(env.get("FRONTEND_MODULE"));
commands.execute(List.of("npm", "ci"), frontend, env, null, true).checked();
commands.execute(List.of("npm", "run", "lint"), frontend, env, null, true)
.checked();
- commands.execute(
- List.of(
- "npm",
- "test",
- "--",
- "--reporters=default",
- "--reporters=junit",
- "--output-file=unit-results/junit.xml"),
- frontend,
- env,
- null,
- true)
- .checked();
+ final Object configured = Json.parse(
+ env.getOrDefault(
+ "FRONTEND_TEST_COMMAND",
+ "[\"npm\",\"test\",\"--\",\"--reporters=default\",\"--reporters=junit\",\"--output-file=unit-results/junit.xml\"]"));
+ final List test =
+ ((List>) configured).stream().map(Object::toString).toList();
+ commands.execute(test, frontend, env, null, true).checked();
commands.execute(List.of("npm", "run", "build"), frontend, env, null, true)
.checked();
}
@@ -321,7 +320,7 @@ class Pipeline {
"-q",
"-O",
"-",
- "http://127.0.0.1:8080/api/dance-schools")
+ Setup.required(env, "CI_READINESS_URL"))
.code()
== 0) return;
Thread.sleep(2000);
@@ -334,15 +333,15 @@ class Pipeline {
commands.renew(Duration.ofMinutes(2));
List failures = new ArrayList<>();
for (String path : List.of(
- "agiledancers-be/target/surefire-reports",
- "agiledancers-be/target/standards",
- "tooling/standards/target/surefire-reports",
- "agiledancers-be/target/site/jacoco",
- "agiledancers-be/target/site/jacoco-combined",
- "agiledancers-be/target/failsafe-reports",
- "agiledancers-fe/unit-results",
- "agiledancers-fe/test-results",
- "agiledancers-fe/playwright-report")) {
+ env.get("BACKEND_MODULE") + "/target/surefire-reports",
+ env.get("BACKEND_MODULE") + "/target/standards",
+ "generated/tooling/standards/target/surefire-reports",
+ env.get("BACKEND_MODULE") + "/target/site/jacoco",
+ env.get("BACKEND_MODULE") + "/target/site/jacoco-combined",
+ env.get("BACKEND_MODULE") + "/target/failsafe-reports",
+ env.get("FRONTEND_MODULE") + "/unit-results",
+ env.get("FRONTEND_MODULE") + "/test-results",
+ env.get("FRONTEND_MODULE") + "/playwright-report")) {
if (Files.isDirectory(root.resolve(path))) {
try {
copyTree(root.resolve(path), artifacts.resolve(path));
@@ -352,7 +351,8 @@ class Pipeline {
}
}
for (String data : List.of("jacoco.exec", "playwright.exec", "jacoco-combined.exec")) {
- final Path source = root.resolve("agiledancers-be/target").resolve(data);
+ final Path source =
+ root.resolve(env.get("BACKEND_MODULE") + "/target").resolve(data);
if (Files.isRegularFile(source))
Files.copy(source, artifacts.resolve(data), StandardCopyOption.REPLACE_EXISTING);
}
@@ -404,7 +404,7 @@ class Pipeline {
"CI_PUBLISH_LOCK_DIR",
env.getOrDefault(
"XDG_STATE_HOME",
- Setup.home().resolve(".local/state").toString()) + "/agiledancers-ci"));
+ Setup.home().resolve(".local/state").toString()) + "/" + Recipe.project(env) + "-ci"));
Files.createDirectories(lockDir);
// Shared host lock: Gitea 1.25 ignores workflow concurrency.
try (FileChannel channel = FileChannel.open(
@@ -419,10 +419,11 @@ class Pipeline {
token,
true)
.checked();
- String prefix = registry + "/" + owner + "/agiledancers-";
- ensureImage(prefix + "backend", backendImage);
- ensureImage(prefix + "frontend", frontendImage);
- for (String role : List.of("backend", "frontend")) {
+ String prefix = registry + "/" + owner + "/" + Recipe.project(env) + "-";
+ for (String role : Recipe.roles(env)) {
+ ensureImage(prefix + role, role.equals("backend") ? backendImage : frontendImage);
+ }
+ for (String role : Recipe.roles(env)) {
if (!commit.equals(mainHead())) {
System.out.println("Newer main exists; moving tags unchanged");
return;
diff --git a/generated/ci/Recipe.java b/generated/ci/Recipe.java
new file mode 100644
index 0000000..6e46c70
--- /dev/null
+++ b/generated/ci/Recipe.java
@@ -0,0 +1,61 @@
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+/** Project-owned configuration for the shared engineering mechanisms. */
+final class Recipe {
+ private Recipe() {}
+
+ static Map environment(final Path root, final Map input) throws Exception {
+ final var result = new HashMap(input);
+ final Path file = root.resolve("engineering.properties");
+ if (Files.exists(file)) {
+ final var properties = new Properties();
+ try (final var stream = Files.newInputStream(file)) {
+ properties.load(stream);
+ }
+ for (final String key : properties.stringPropertyNames()) {
+ result.putIfAbsent(
+ key, properties.getProperty(key).replace("${user.home}", System.getProperty("user.home")));
+ }
+ }
+ if (result.containsKey("PROJECT_NAME")) {
+ project(result);
+ for (final String role : roles(result)) {
+ final Path module = Path.of(result.get(role.toUpperCase() + "_MODULE"));
+ Pipeline.check(
+ !module.isAbsolute() && !module.normalize().startsWith(".."),
+ "Module must stay in the project");
+ }
+ Pipeline.check(
+ !enabled(result, "BROWSER_TESTS") || roles(result).size() == 2,
+ "Browser coverage requires both backend and frontend");
+ }
+ return result;
+ }
+
+ static String project(final Map env) {
+ final String name = Setup.required(env, "PROJECT_NAME");
+ Pipeline.check(name.matches("[a-z][a-z0-9-]*"), "Invalid project name");
+ return name;
+ }
+
+ static List roles(final Map env) {
+ final var roles = new ArrayList();
+ for (final String role : List.of("backend", "frontend")) {
+ if (!env.getOrDefault(role.toUpperCase() + "_MODULE", "").isBlank()) {
+ roles.add(role);
+ }
+ }
+ Pipeline.check(!roles.isEmpty(), "Configure BACKEND_MODULE, FRONTEND_MODULE, or both");
+ return roles;
+ }
+
+ static boolean enabled(final Map env, final String key) {
+ return Boolean.parseBoolean(env.getOrDefault(key, "false"));
+ }
+}
diff --git a/ci/Release.java b/generated/ci/Release.java
similarity index 97%
rename from ci/Release.java
rename to generated/ci/Release.java
index 043fef8..395574c 100644
--- a/ci/Release.java
+++ b/generated/ci/Release.java
@@ -153,6 +153,7 @@ class Release {
"A newer release has already succeeded; use explicit rollback for older versions");
String build = api.verified(commit);
remote.call("stage", Files.readString(root.resolve("deploy/compose.yaml")));
+ remote.call("stage-health", Files.readString(root.resolve("deploy/health.sh")));
String prepared = remote.call("prepare", null, version.toString(), commit, prefix());
return new ReleaseState.Deployment(
version, commit, digest(prepared, "BACKEND"), digest(prepared, "FRONTEND"), attempt, build);
@@ -168,7 +169,7 @@ class Release {
registry.equals(authority) || registry.equals("127.0.0.1:" + port),
"Deployment registry must be the existing Gitea registry or its NAS loopback alias");
String owner = Setup.required(env, "CI_REPOSITORY").split("/")[0].toLowerCase(java.util.Locale.ROOT);
- return registry + "/" + owner + "/agiledancers-";
+ return registry + "/" + owner + "/" + Recipe.project(env) + "-";
}
void ensure(ReleaseState.Deployment release) throws Exception {
@@ -183,6 +184,7 @@ class Release {
}
String digest(String response, String role) {
+ if (!Recipe.roles(env).contains(role.toLowerCase(java.util.Locale.ROOT))) return "-";
String value = response.lines()
.filter(line -> line.startsWith("S6_" + role + "="))
.map(line -> line.substring(("S6_" + role + "=").length()))
diff --git a/ci/ReleaseApi.java b/generated/ci/ReleaseApi.java
similarity index 98%
rename from ci/ReleaseApi.java
rename to generated/ci/ReleaseApi.java
index 2971e18..2bcc185 100644
--- a/ci/ReleaseApi.java
+++ b/generated/ci/ReleaseApi.java
@@ -82,7 +82,7 @@ class ReleaseApi {
"target",
commit,
"message",
- "Agiledancers " + version + "\nCommit: " + commit + "\nDeployment attempt: " + run));
+ "Release " + version + "\nCommit: " + commit + "\nDeployment attempt: " + run));
Tag tag = tags().stream()
.filter(t -> t.version().equals(version))
.findFirst()
diff --git a/ci/ReleaseRemote.java b/generated/ci/ReleaseRemote.java
similarity index 85%
rename from ci/ReleaseRemote.java
rename to generated/ci/ReleaseRemote.java
index e493d9c..cc77b10 100644
--- a/ci/ReleaseRemote.java
+++ b/generated/ci/ReleaseRemote.java
@@ -32,8 +32,14 @@ class ReleaseRemote implements AutoCloseable {
&& Integer.parseInt(port) > 0
&& Integer.parseInt(port) <= 65535,
"Invalid deployment SSH host, user or port");
- script = Files.readString(root.resolve("deploy/remote.sh"));
- credentials = Files.createTempDirectory("agiledancers-deploy-ssh-");
+ script = "COMPOSE_PROJECT=" + quote(Setup.required(env, "COMPOSE_PROJECT")) + "\n"
+ + "DEPLOY_ROLES=" + quote(String.join(" ", Recipe.roles(env))) + "\n"
+ + "DEPENDENCY_SERVICES=" + quote(env.getOrDefault("DEPENDENCY_SERVICES", "")) + "\n"
+ + "TARGET_PLATFORM=" + quote(env.getOrDefault("TARGET_PLATFORM", "linux/amd64")) + "\n"
+ + "REQUIRED_SECRETS=" + quote(env.getOrDefault("REQUIRED_DEPLOY_SECRETS", "")) + "\n"
+ + Files.readString(root.resolve("deploy/health.sh")) + "\n"
+ + Files.readString(root.resolve("generated/deploy/remote.sh"));
+ credentials = Files.createTempDirectory("engineering-deploy-ssh-");
try {
writePrivate("identity", Setup.required(env, "DEPLOY_SSH_KEY") + "\n");
writePrivate("known_hosts", Setup.required(env, "DEPLOY_KNOWN_HOSTS") + "\n");
diff --git a/ci/ReleaseSetup.java b/generated/ci/ReleaseSetup.java
similarity index 81%
rename from ci/ReleaseSetup.java
rename to generated/ci/ReleaseSetup.java
index 412966d..de8fa26 100644
--- a/ci/ReleaseSetup.java
+++ b/generated/ci/ReleaseSetup.java
@@ -10,20 +10,12 @@ import java.util.Map;
class ReleaseSetup {
static void configure(Path root, Map input) throws Exception {
var env = new HashMap<>(input);
- env.putIfAbsent("GITEA_SERVER_URL", "http://192.168.2.28:3000");
- env.putIfAbsent("CI_REPOSITORY", "wanjakrah/agiledancers");
- env.putIfAbsent("DEPLOY_HOST", "192.168.2.28");
- env.putIfAbsent("DEPLOY_USER", "agiledancers-deploy");
env.putIfAbsent("DEPLOY_SSH_PORT", "22");
- env.putIfAbsent("DEPLOY_DIRECTORY", "/volume1/docker/agiledancers-production");
- env.putIfAbsent("DEPLOY_BIND_IP", "192.168.2.28");
- env.putIfAbsent("DEPLOY_FRONTEND_PORT", "8088");
- env.putIfAbsent("DEPLOY_REGISTRY", "127.0.0.1:3000");
- Path config = Setup.home().resolve(".config/agiledancers-runner");
+ Path config = Setup.home().resolve(".config/" + Recipe.project(env) + "-runner");
env.putIfAbsent("GITEA_ADMIN_TOKEN_FILE", config.resolve("admin-token").toString());
- env.putIfAbsent("RELEASE_TOKEN_FILE", config.resolve("s6-release-token").toString());
- env.putIfAbsent("PULL_TOKEN_FILE", config.resolve("s6-pull-token").toString());
- env.putIfAbsent("DEPLOY_SSH_KEY_FILE", config.resolve("s6-deploy-key").toString());
+ env.putIfAbsent("RELEASE_TOKEN_FILE", config.resolve("release-token").toString());
+ env.putIfAbsent("PULL_TOKEN_FILE", config.resolve("pull-token").toString());
+ env.putIfAbsent("DEPLOY_SSH_KEY_FILE", config.resolve("deploy-key").toString());
String admin = Setup.secret(env, "GITEA_ADMIN_TOKEN_FILE");
String pull = Setup.secret(env, "PULL_TOKEN_FILE");
env.put("RELEASE_TOKEN", Setup.secret(env, "RELEASE_TOKEN_FILE"));
@@ -70,10 +62,16 @@ class ReleaseSetup {
Pipeline.check(registryUser.matches("[a-zA-Z0-9_.-]+"), "Invalid registry user");
try (var remote = new ReleaseRemote(root, env, commands, "setup")) {
System.out.println(remote.executeScript(
- Files.readString(root.resolve("deploy/provision.sh")),
+ Files.readString(root.resolve("generated/deploy/provision.sh")),
pull + "\n",
300,
- List.of(remote.directory, registryUser, registry, bind, port)));
+ List.of(
+ remote.directory,
+ registryUser,
+ registry,
+ bind,
+ port,
+ Setup.required(env, "DEPLOY_DOCKER_BIN"))));
remote.acquire();
try {
remote.call("preflight", null);
diff --git a/ci/ReleaseShellTests.java b/generated/ci/ReleaseShellTests.java
similarity index 90%
rename from ci/ReleaseShellTests.java
rename to generated/ci/ReleaseShellTests.java
index bfb5be9..e16d58a 100644
--- a/ci/ReleaseShellTests.java
+++ b/generated/ci/ReleaseShellTests.java
@@ -17,7 +17,7 @@ class ReleaseShellTests {
CiTests.equal("S6_HEALTHY", f.call("apply", null, "one", "one").checked());
String calls = Files.readString(f.directory.resolve("calls"));
CiTests.equal(true, calls.contains("http://backend:8080/q/health/ready"));
- CiTests.equal(true, calls.contains("http://127.0.0.1:8088/api/dance-schools"));
+ CiTests.equal(true, calls.contains("http://127.0.0.1:8088/api/items"));
CiTests.equal(false, calls.contains(" down"));
CiTests.equal(false, calls.contains("prune"));
}
@@ -111,7 +111,19 @@ class ReleaseShellTests {
static class Fixture implements AutoCloseable {
final Path directory = Files.createTempDirectory("nas-script-test-");
final Map env = new HashMap<>(System.getenv());
- final String script = Files.readString(Path.of("deploy/remote.sh"));
+ final String health = "application_health() {\n"
+ + "frontend=$(compose ps -q frontend)\n"
+ + "docker_cmd exec \"$frontend\" wget -q -T 5 -O /dev/null http://backend:8080/q/health/ready || return"
+ + " 1\n"
+ + "curl --fail http://127.0.0.1:8088/api/items -o /dev/null\n"
+ + "}\n";
+ final String script = "COMPOSE_PROJECT=recipe-demo-test\n"
+ + "DEPLOY_ROLES='backend frontend'\n"
+ + "DEPENDENCY_SERVICES=database\n"
+ + "TARGET_PLATFORM=linux/amd64\n"
+ + "REQUIRED_SECRETS=DATABASE_PASSWORD\n"
+ + health + "\n"
+ + Files.readString(Path.of("generated/deploy/remote.sh"));
final Commands commands = new Commands(Duration.ofMinutes(1));
Fixture() throws Exception {
@@ -150,19 +162,19 @@ class ReleaseShellTests {
}
String backend() {
- return "nas:3000/owner/agiledancers-backend@sha256:" + "1".repeat(64);
+ return "nas:3000/owner/recipe-demo-backend@sha256:" + "1".repeat(64);
}
String frontend() {
- return "nas:3000/owner/agiledancers-frontend@sha256:" + "2".repeat(64);
+ return "nas:3000/owner/recipe-demo-frontend@sha256:" + "2".repeat(64);
}
void prepare() throws Exception {
call("lock", null, "one").checked();
call("preflight", null, "one").checked();
- call("stage", Files.readString(Path.of("deploy/compose.yaml")), "one")
- .checked();
- String result = call("prepare", null, "one", "v1.0.0", ReleaseTests.SHA, "nas:3000/owner/agiledancers-")
+ call("stage", "services: {}\n", "one").checked();
+ call("stage-health", health, "one").checked();
+ String result = call("prepare", null, "one", "v1.0.0", ReleaseTests.SHA, "nas:3000/owner/recipe-demo-")
.checked();
CiTests.equal(true, result.contains("S6_BACKEND=" + backend()));
CiTests.equal(true, result.contains("S6_FRONTEND=" + frontend()));
diff --git a/ci/ReleaseState.java b/generated/ci/ReleaseState.java
similarity index 91%
rename from ci/ReleaseState.java
rename to generated/ci/ReleaseState.java
index 3b526e2..d49eeba 100644
--- a/ci/ReleaseState.java
+++ b/generated/ci/ReleaseState.java
@@ -9,8 +9,11 @@ record ReleaseState(ReleaseState.Deployment current, ReleaseState.Deployment pre
Deployment {
ReleaseApi.sha(sha);
Pipeline.check(directory.matches("[a-zA-Z0-9-]+"), "Invalid retained release directory");
+ Pipeline.check(!backend.equals("-") || !frontend.equals("-"), "A release must contain an image");
for (String image : new String[] {backend, frontend})
- Pipeline.check(image.matches("[a-zA-Z0-9.:/_-]+@sha256:[0-9a-f]{64}"), "Invalid retained image digest");
+ Pipeline.check(
+ (image.equals("-") || image.matches("[a-zA-Z0-9.:/_-]+@sha256:[0-9a-f]{64}")),
+ "Invalid retained image digest");
}
Map json() {
diff --git a/ci/ReleaseTests.java b/generated/ci/ReleaseTests.java
similarity index 96%
rename from ci/ReleaseTests.java
rename to generated/ci/ReleaseTests.java
index bc0e907..0f733b2 100644
--- a/ci/ReleaseTests.java
+++ b/generated/ci/ReleaseTests.java
@@ -37,7 +37,7 @@ class ReleaseTests {
"GITEA_SERVER_URL",
"http://nas:3000",
"CI_REPOSITORY",
- "owner/agiledancers",
+ "owner/recipe-demo",
"RELEASE_TOKEN",
"test"));
env.putAll(Map.of(
@@ -58,8 +58,8 @@ class ReleaseTests {
return new ReleaseState.Deployment(
ReleaseVersion.parse(version),
sha,
- "nas:3000/owner/agiledancers-backend@sha256:" + "1".repeat(64),
- "nas:3000/owner/agiledancers-frontend@sha256:" + "2".repeat(64),
+ "nas:3000/owner/recipe-demo-backend@sha256:" + "1".repeat(64),
+ "nas:3000/owner/recipe-demo-frontend@sha256:" + "2".repeat(64),
directory,
"10");
}
@@ -353,8 +353,14 @@ class ReleaseTests {
Fixture(String mode) throws Exception {
Files.createDirectories(root.resolve("deploy"));
- Files.writeString(root.resolve("deploy/remote.sh"), "");
+ Files.createDirectories(root.resolve("generated/deploy"));
+ env.put("PROJECT_NAME", "recipe-demo");
+ env.put("BACKEND_MODULE", "backend");
+ env.put("FRONTEND_MODULE", "frontend");
+ env.put("COMPOSE_PROJECT", "recipe-demo-test");
+ Files.writeString(root.resolve("generated/deploy/remote.sh"), "");
Files.writeString(root.resolve("deploy/compose.yaml"), "services: {}\n");
+ Files.writeString(root.resolve("deploy/health.sh"), "application_health() { true; }\n");
env.put("RELEASE_MODE", mode);
if (mode.equals("retry")) env.put("RELEASE_VERSION", "v1.0.0");
api = new FakeApi(env, events);
@@ -379,7 +385,7 @@ class ReleaseTests {
test("NAS registry alias keeps the same repository and rejects unrelated registries", () -> {
try (var f = new Fixture()) {
f.env.put("DEPLOY_REGISTRY", "127.0.0.1:3000");
- CiTests.equal("127.0.0.1:3000/owner/agiledancers-", f.release.prefix());
+ CiTests.equal("127.0.0.1:3000/owner/recipe-demo-", f.release.prefix());
f.env.put("DEPLOY_REGISTRY", "other-registry:3000");
CiTests.failure(f.release::prefix);
}
@@ -476,7 +482,7 @@ class ReleaseTests {
test("real remote shell atomically excludes a second owner and retains unresolved journal", () -> {
Path directory = Files.createTempDirectory("release-shell-test-");
try {
- String script = Files.readString(Path.of("deploy/remote.sh"));
+ String script = Files.readString(Path.of("generated/deploy/remote.sh"));
var commands = new Commands(Duration.ofSeconds(30));
var env = environment();
var first = commands.execute(
diff --git a/ci/ReleaseVersion.java b/generated/ci/ReleaseVersion.java
similarity index 100%
rename from ci/ReleaseVersion.java
rename to generated/ci/ReleaseVersion.java
diff --git a/ci/Setup.java b/generated/ci/Setup.java
similarity index 91%
rename from ci/Setup.java
rename to generated/ci/Setup.java
index ad4f550..0963d5b 100644
--- a/ci/Setup.java
+++ b/generated/ci/Setup.java
@@ -65,7 +65,7 @@ class Setup {
Pipeline.check(
Set.of("linux/amd64", "linux/arm64").contains(platform),
"Set TARGET_PLATFORM from the verified Synology architecture");
- String server = "http://192.168.2.28:3000", base = "/api/v1/repos/wanjakrah/agiledancers";
+ String server = required(env, "GITEA_SERVER_URL"), base = "/api/v1/repos/" + required(env, "CI_REPOSITORY");
api(server, admin, "PATCH", base, Map.of("has_actions", true));
api(server, admin, "PUT", base + "/actions/secrets/REGISTRY_USER", Map.of("data", user));
api(server, admin, "PUT", base + "/actions/secrets/REGISTRY_TOKEN", Map.of("data", registry));
@@ -81,29 +81,29 @@ class Setup {
api(server, admin, exists ? "PUT" : "POST", variable, Map.of("value", platform));
Map response =
Json.object(api(server, admin, "POST", base + "/actions/runners/registration-token", null));
- Path directory = home().resolve(".config/agiledancers-runner");
+ Path directory = home().resolve(".config/" + Recipe.project(env) + "-runner");
privateDirectory(directory);
privateWrite(directory.resolve("registration-token"), Objects.toString(response.get("token")));
System.out.println("Actions, registry secrets and platform configured; runner token saved outside Git.");
}
- static void configureDocker(Path file) throws IOException {
+ static void configureDocker(Path file, String registry) throws IOException {
Map data =
Files.exists(file) ? Json.object(Json.parse(Files.readString(file))) : new LinkedHashMap<>();
Object entry = data.getOrDefault("insecure-registries", new ArrayList<>());
if (!(entry instanceof List>))
throw new IllegalArgumentException("Existing insecure-registries must be an array; unchanged");
List