Files
Carsten Rehfeld b2a16a8be7 Implement apix-registry with IoT sunset/decommission lifecycle and full BDD suite
- REST API: register, patch, O-level, replacements, history, search endpoints
- IoT lifecycle validations: future sunset, lock-before-release, sunset-passed-before-decommission
- DB schema: Liquibase changesets 001–008 (services, versions, replacements, sunset-at column)
- @ColumnTransformer(write="?::jsonb") on bsm_payload fields to avoid JDBC varchar→jsonb rejection
- Jandex plugin on apix-common + quarkus.index-dependency so @NotBlank validators resolve at runtime
- quarkus-logging-json extension added; quarkus.log.console.json=false is now a recognised key
- Fix requireSunsetBeforeLockRelease: Boolean.TRUE.equals instead of !Boolean.FALSE.equals (null guard)
- BDD suite: 27 scenarios / 213 steps across 5 feature files (sunset-lock, decommission, replacement, discovery, anonymity)
- Test infrastructure: JDBC TRUNCATE in @Before for DB isolation, Arc.container() for clock control — no test endpoints in production code
- sunsetAt truncated to microseconds in BDD steps to match Postgres timestamptz precision
- Cucumber step fixes: singular/plural candidate(s), lastResponse propagation in replacementsReturnsNCandidates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 09:13:26 +02:00

572 lines
38 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# APIX MVP — Work Log
**Goal:** Deployable, publicly queryable APIX registry PoC on Hetzner by end of 2026.
**Constraint:** Solo development, LLM-assisted. Security hygiene (not hardening). Speed over completeness.
**Success criterion:** A reviewer can open a public URL, run a capability query, see a result from a registered service, and confirm the Spider has checked it.
---
## Status Legend
| Symbol | Meaning |
|---|---|
| `[ ]` | Not started |
| `[~]` | In progress |
| `[x]` | Done |
| `[!]` | Blocked / decision needed |
---
## Arc42 Documentation Deliverables
### 1. Introduction and Goals → `docs/arc42/01-introduction-goals.md`
| # | Deliverable | Status |
|---|---|---|
| D-01 | MVP goal statement (what must be provable at the end) | `[ ]` |
| D-02 | Quality goals table (top 35, measurable) | `[ ]` |
| D-03 | Stakeholder table (STF reviewer, agent developer, service registrant, BSF) | `[ ]` |
| D-04 | Explicit out-of-scope list for MVP (billing, full trust model, multi-region) | `[ ]` |
---
### 2. Architecture Constraints → `docs/arc42/02-constraints.md`
| # | Deliverable | Status |
|---|---|---|
| D-05 | Technical constraints (Hetzner, Docker Compose, Python, PostgreSQL, open source stack) | `[ ]` |
| D-06 | Organisational constraints (solo dev, LLM-assisted, public GitHub repo required) | `[ ]` |
| D-07 | Regulatory constraints (HTTPS mandatory, GDPR-lite: no PII stored beyond registrant email) | `[ ]` |
| D-08 | Convention constraints (HATEOAS API style, IETF Internet-Draft alignment) | `[ ]` |
---
### 3. Context and Scope → `docs/arc42/03-context-scope.md`
| # | Deliverable | Status |
|---|---|---|
| D-09 | System context diagram — PlantUML, external actors: Agent, Service Registrant, Spider, Admin | `[ ]` |
| D-10 | External interface table (what each actor sends/receives) | `[ ]` |
| D-11 | Technical context diagram — PlantUML, network boundary (Caddy → API → DB, Spider → external services) | `[ ]` |
---
### 4. Solution Strategy → `docs/arc42/04-solution-strategy.md`
| # | Deliverable | Status |
|---|---|---|
| D-12 | Tech stack decision table with rationale (FastAPI, PostgreSQL JSONB, Caddy, HTMX) | `[ ]` |
| D-13 | Architectural pattern decisions (REST + HATEOAS, async Spider, single-process portal) | `[ ]` |
| D-14 | Quality goal → architecture decision mapping | `[ ]` |
| D-15 | MVP shortcuts explicitly listed (manual O-level assignment, no billing, no rate limiting beyond Caddy) | `[ ]` |
---
### 5. Building Block View → `docs/arc42/05-building-blocks.md`
| # | Deliverable | Status |
|---|---|---|
| D-16 | Level 1 diagram — PlantUML: API service, Spider service, Portal, PostgreSQL, Caddy | `[ ]` |
| D-17 | Level 2 diagram — API internals: router, service layer, BSM validator, DB adapter | `[ ]` |
| D-18 | Level 2 diagram — Spider internals: scheduler, fetcher, liveness evaluator, DB writer | `[ ]` |
| D-19 | Component responsibility table (one sentence per component) | `[ ]` |
---
### 6. Runtime View → `docs/arc42/06-runtime-view.md`
| # | Deliverable | Status |
|---|---|---|
| D-20 | Scenario 1: Agent queries registry by capability — PlantUML sequence diagram | `[ ]` |
| D-21 | Scenario 2: Service registrant submits BSM via portal — PlantUML sequence diagram | `[ ]` |
| D-22 | Scenario 3: Spider runs liveness check and updates service status — PlantUML sequence diagram | `[ ]` |
| D-23 | Scenario 4: Agent navigates index via HATEOAS links — PlantUML sequence diagram | `[ ]` |
---
### 7. Deployment View → `docs/arc42/07-deployment-view.md`
| # | Deliverable | Status |
|---|---|---|
| D-24 | Hetzner deployment diagram — PlantUML: VPS, Docker Compose services, Caddy, volumes | `[ ]` |
| D-25 | Environment table (dev / staging / prod differences) | `[ ]` |
| D-26 | Backup and restore strategy (pg_dump, Hetzner volume snapshot) | `[ ]` |
| D-27 | Domain and DNS setup notes | `[ ]` |
---
### 8. Crosscutting Concepts → `docs/arc42/08-crosscutting-concepts.md`
| # | Deliverable | Status |
|---|---|---|
| D-28 | Logging concept (structured JSON, levels, what is logged per component) | `[ ]` |
| D-29 | Error handling concept (HTTP error codes, error response schema) | `[ ]` |
| D-30 | Security hygiene concept (HTTPS via Caddy, API key on write endpoints, rate limiting, no PII logging) | `[ ]` |
| D-31 | BSM validation concept (schema version, required fields, validation error format) | `[ ]` |
| D-32 | Liveness check concept (what "live" means, check frequency, status transition rules) | `[ ]` |
| D-33 | Idempotency concept (re-registration behaviour, Spider re-check behaviour) | `[ ]` |
| D-33a | i18n concept (locale resolution order, @MessageBundle keys, help content localisation, language switcher) | `[ ]` |
---
### 9. Architecture Decisions → `docs/arc42/09-architecture-decisions.md`
| # | ADR | Status |
|---|---|---|
| D-34 | ADR-001: Python + FastAPI over Node/Go — rationale: AI ecosystem, speed, solo dev | `[ ]` |
| D-35 | ADR-002: PostgreSQL + JSONB over MongoDB — rationale: relational integrity for registry + flexible BSM payload | `[ ]` |
| D-36 | ADR-003: Caddy over nginx/traefik — rationale: auto-TLS, zero config, solo maintainability | `[ ]` |
| D-37 | ADR-004: HTMX + Jinja2 over React/Vue — rationale: no JS build pipeline, speed, portal is admin-grade not consumer-grade | `[ ]` |
| D-38 | ADR-005: Automated O-1/O-2/O-3 verification in MVP — DNS + GLEIF + OpenCorporates + HTTP hygiene checks; O-4/O-5 post-MVP | `[ ]` |
| D-39 | ADR-006: Two-VPS Hetzner deployment — apix-app (Swarm) + apix-gitea (Gitea + CI) | `[ ]` |
| D-47 | ADR-007: Register verification APIs (GLEIF, OpenCorporates, EU Sanctions) as reference APIX entries | `[ ]` |
| D-48 | ADR-009: Maven multi-module with separated Spider module | `[ ]` |
| D-49 | ADR-010: Self-hosted Gitea primary; GitHub push mirror | `[ ]` |
| D-50 | ADR-011: Docker Swarm single-node for zero-downtime production deployment | `[ ]` |
| D-51 | ADR-012: Three-stage CI/CD pipeline (fast / native build / deploy) | `[ ]` |
| D-52 | ADR-013: Server-side i18n via Quarkus @MessageBundle; EN + DE; cookie + Accept-Language locale resolution | `[ ]` |
| D-53 | ADR-014: Client-side help overlay engine; server-rendered locale-aware tour content; 5 tours (register, search, trust, admin, agent-setup) | `[ ]` |
---
### 10. Quality Requirements → `docs/arc42/10-quality-requirements.md`
| # | Deliverable | Status |
|---|---|---|
| D-40 | Quality tree (functionality, reliability, security hygiene, operability) | `[ ]` |
| D-41 | Quality scenarios table (stimulus → response → measurable outcome) | `[ ]` |
| D-42 | MVP acceptance criteria (what "done" looks like — the STF reviewer test) | `[ ]` |
---
### 11. Risks and Technical Debt → `docs/arc42/11-risks-technical-debt.md`
| # | Deliverable | Status |
|---|---|---|
| D-43 | Risk register (chicken-and-egg, big tech competition, single point of failure, solo bus factor) | `[ ]` |
| D-44 | Technical debt log (MVP shortcuts accepted, with explicit exit path for each) | `[ ]` |
| D-45 | Mitigation actions per risk | `[ ]` |
---
### 12. Glossary → `docs/arc42/12-glossary.md`
| # | Deliverable | Status |
|---|---|---|
| D-46 | Glossary: APIX, BSM, Spider, O-level, S-level, Liveness, AE, HATEOAS, DC-1 | `[ ]` |
---
## Code Deliverables
**Stack:** Java 21 + Quarkus 3.x, Maven multi-module. Five modules: two plain Java libraries (`apix-common`, `apix-verification`) and three independently deployable Quarkus apps (`apix-registry`, `apix-spider`, `apix-portal`). Spider runs as its own process — separate module, separate container, separate lifecycle. Build tool: Maven with parent POM as BOM. Tests: plain JUnit 5 for library modules; JUnit 5 + `@QuarkusTest` + RestAssured + WireMock for Quarkus modules.
Source root per module: `<module>/src/main/java/org/botstandards/apix/<module-suffix>/`
---
### Parent POM — `pom.xml`
| # | Deliverable | Status |
|---|---|---|
| C-00 | `pom.xml` — parent; imports Quarkus BOM; declares all five modules; manages `maven-compiler-plugin` (Java 21 release); `quarkus-maven-plugin` config inherited by Quarkus modules | `[ ]` |
---
### apix-common — Shared Library (plain Java 21)
No Quarkus dependency. Shared enums and DTOs used by all modules.
| # | Deliverable | Status |
|---|---|---|
| C-01 | `OLevel.java` — enum: UNVERIFIED, IDENTITY_VERIFIED, LEGAL_ENTITY_VERIFIED, HYGIENE_VERIFIED, OPERATIONALLY_VERIFIED, AUDITED | `[ ]` |
| C-02 | `LivenessStatus.java` — enum: PENDING, LIVE, DEGRADED, UNREACHABLE | `[ ]` |
| C-03 | `BsmPayload.java` — Java record; all BSM fields per Internet-Draft; Bean Validation annotations (`@NotBlank`, `@Valid`, `@URL`) | `[ ]` |
| C-04 | `ServiceSummaryDto.java` — Java record; fields exposed in search results (id, name, capabilities, olevel, liveness_status, endpoint, last_checked_at) | `[ ]` |
| C-05 | `VerificationResult.java` — Java record: olevel achieved, step that blocked (if any), message | `[ ]` |
---
### apix-verification — Verification Library (plain Java 21)
No Quarkus dependency. Uses `java.net.http.HttpClient` and `dnsjava`. All classes are plain CDI-free POJOs — fully testable with plain JUnit. Depends on `apix-common`.
| # | Deliverable | What it does | O-level unlocked |
|---|---|---|---|
| C-06 | `O1DnsVerifier.java` | DNS TXT record lookup (dnsjava); business email MX check | O-1 |
| C-07 | `O2GleifVerifier.java` | GLEIF REST API via `java.net.http.HttpClient`; LEI lookup by company name + jurisdiction | O-2 |
| C-08 | `O2OpenCorporatesVerifier.java` | OpenCorporates REST API fallback for registrants without LEI; covers 130+ jurisdictions | O-2 (fallback) |
| C-09 | `O3HygieneVerifier.java` | HTTP fetch of `/.well-known/security.txt`; DNS DMARC + SPF check; policy URL reachability | O-3 |
| C-10 | `SanctionsScreener.java` | Screens name + jurisdiction against locally cached OFAC/EU/UN lists (CSV/XML read from file path) | Pre-condition for O-2 |
| C-11 | `VerificationPipeline.java` | Orchestrates O-1 → sanctions → O-2 → O-3 in sequence; returns `VerificationResult`; no I/O side effects (caller persists result) | All |
| C-12 | `VerificationConfig.java` | Plain POJO: gleifApiUrl, openCorporatesApiKey, sanctionsCachePath — injected by caller (Quarkus `@ConfigProperty` in registry) | — |
---
### apix-registry — REST API (Quarkus 3.x app)
Depends on `apix-common` + `apix-verification`. Owns the database schema (runs Liquibase at startup). Quarkus extensions: RESTEasy Reactive, Hibernate ORM + Panache, PostgreSQL JDBC, Liquibase, SmallRye Health, Quarkus Security.
#### Resource layer
| # | Deliverable | Status |
|---|---|---|
| C-13 | `resource/IndexResource.java``GET /` HATEOAS root; navigation links JSON | `[ ]` |
| C-14 | `resource/ServiceResource.java``GET /services` (capability search + filters), `GET /services/{id}` | `[ ]` |
| C-15 | `resource/RegisterResource.java``POST /register` (API-key protected); triggers `VerificationOrchestrator` asynchronously | `[ ]` |
#### Service layer
| # | Deliverable | Status |
|---|---|---|
| C-16 | `service/RegistryService.java` — register (UPSERT on endpoint URL), search by capability (JPQL + JSONB), dedup | `[ ]` |
| C-17 | `service/VerificationOrchestrator.java` — CDI bean; injects `VerificationConfig` from Quarkus `@ConfigProperty`; calls `VerificationPipeline`; persists `VerificationResult` to `ServiceRecord`; fires admin notification event if manual step needed | `[ ]` |
#### Model + repository
| # | Deliverable | Status |
|---|---|---|
| C-18 | `model/ServiceRecord.java` — Panache entity; `services` table; JSONB via `@JdbcTypeCode(SqlTypes.JSON)` for `bsm_payload` | `[ ]` |
| C-19 | `repository/ServiceRepository.java` — capability search query; upsert; O-level update | `[ ]` |
#### Liquibase migrations
| # | Deliverable | Status |
|---|---|---|
| C-20 | `resources/db/changelog/db.changelog-master.xml` | `[ ]` |
| C-21 | `changes/001-initial-schema.xml``services` table: id, endpoint_url (unique), bsm_payload (JSONB), olevel, slevel, liveness_status, registered_at | `[ ]` |
| C-22 | `changes/002-verification-columns.xml` — verification_status, olevel_checked_at, sanctions_cleared, gleif_lei | `[ ]` |
| C-23 | `changes/003-liveness-metrics.xml` — last_checked_at, uptime_30d_percent, avg_response_ms, consecutive_failures | `[ ]` |
#### Configuration
| # | Deliverable | Status |
|---|---|---|
| C-24 | `resources/application.properties` — datasource, Liquibase enabled, port 8180, GLEIF URL, OpenCorporates key, sanctions cache path, log level | `[ ]` |
---
### apix-spider — Liveness Scheduler (Quarkus 3.x app)
Separate module, separate container, separate lifecycle. Depends on `apix-common`. Connects to same PostgreSQL DB; Liquibase **disabled** (`quarkus.liquibase.migrate-at-start=false`). Quarkus extensions: Quarkus Scheduler, Hibernate ORM + Panache, PostgreSQL JDBC, REST Client Reactive, SmallRye Health.
| # | Deliverable | Status |
|---|---|---|
| C-25 | `SpiderScheduler.java``@Scheduled(every="${spider.interval:15m}")`; loads all active services; dispatches to `LivenessFetcher` via virtual thread pool | `[ ]` |
| C-26 | `LivenessFetcher.java``@RestClient`; async HTTP GET with 5s timeout; `@RunOnVirtualThread` | `[ ]` |
| C-27 | `LivenessEvaluator.java` — pure logic: HTTP status code + response time ms → `LivenessStatus`; no I/O; no Quarkus dependency | `[ ]` |
| C-28 | `OpenApiParser.java` — fetch + parse OpenAPI spec; verify declared capabilities present | `[ ]` |
| C-29 | `McpParser.java` — fetch + parse MCP spec URL | `[ ]` |
| C-30 | `model/SpiderServiceView.java` — Panache entity (read/write subset of `services` table): endpoint_url, liveness_status, last_checked_at, uptime_30d_percent, avg_response_ms, consecutive_failures | `[ ]` |
| C-31 | `repository/SpiderRepository.java` — load services due for check; write liveness result | `[ ]` |
| C-32 | `resources/application.properties` — datasource, Liquibase disabled, scheduler interval, spider HTTP timeout, port 8082 (internal only) | `[ ]` |
---
### apix-portal — Web Portal (Quarkus 3.x app)
Depends on `apix-common`. Calls `apix-registry` via REST Client — no direct DB access. Quarkus extensions: RESTEasy Reactive, Qute, REST Client Reactive, SmallRye Health.
| # | Deliverable | Status |
|---|---|---|
| C-33 | `resource/PortalResource.java``GET /`, `/services/{id}`, `/search`, `/register`; delegates to `RegistryClient` | `[ ]` |
| C-34 | `resource/AdminResource.java``GET /admin`, `POST /admin/olevel` (API-key protected) | `[ ]` |
| C-35 | `client/RegistryClient.java``@RegisterRestClient`; two methods: `search(capability, page)``List<ServiceSummaryDto>`; `getDetail(id)``ServiceDetailDto` (full BSM payload + all registry metadata); typed REST client calling `apix-registry` | `[ ]` |
| C-36 | `templates/index.html` (Qute, `@CheckedTemplate`) — registry stats, search box | `[ ]` |
| C-37 | `templates/register.html` (Qute) — BSM registration form; HTMX inline validation | `[ ]` |
| C-38 | `templates/service.html` (Qute) — human-targeted service detail page; see layout spec below | `[ ]` |
| C-39 | `templates/search.html` (Qute) — capability search results, HTMX pagination | `[ ]` |
| C-40 | `templates/admin.html` (Qute) — pending verifications, O-level assignment, reference registration flags | `[ ]` |
| C-41 | `resources/META-INF/resources/style.css` — minimal CSS, no framework | `[ ]` |
| C-42 | `resources/application.properties` — registry base URL, port 8081, log level | `[ ]` |
#### service.html — human-readable service detail page (C-38 layout spec)
A human visiting `/services/{id}` is evaluating whether to use this service. The page answers four questions in order: *Who is this? Can I trust them? What exactly does it do? How do I call it?*
**Section 1 — Identity hero**
- Service `name` (h1, prominent)
- `description` (full text, not truncated)
- Two inline trust chips: O-level badge (colored pill: grey=O-0, blue=O-1/O-2/O-3, green=O-4/O-5) + liveness dot (green/amber/red) — both visible above the fold without scrolling
**Section 2 — Trust verification card**
- O-level: large badge icon + level name (e.g. "Legal Entity Verified") + 2-sentence plain-English explanation (e.g. *"The provider's legal incorporation has been confirmed against the GLEIF global legal entity database. This means APIX has independently verified that the registrant is a real legal entity — not self-declared."*) + GLEIF LEI link if present
- S-level: same pattern (badge + name + explanation)
- "Verified on" date — relative ("3 days ago") with absolute date as tooltip; "Reference entry by BSF" badge if applicable (grey label "Registered by Bot Standards Foundation — operator not yet self-registered")
**Section 3 — Liveness status card**
- Colored status row: dot + label (LIVE / DEGRADED / UNREACHABLE / PENDING)
- Uptime last 30 days: percentage bar + number (e.g. "98.4%")
- Average response time: "142 ms"
- Last checked: relative time ("8 minutes ago")
- Check frequency note: "Automatically verified every 15 minutes by the APIX Spider"
**Section 4 — Capabilities**
- Each `capabilities[]` entry as a visual chip (tag-style)
- If BSM includes a description per capability, show it beneath the chip
**Section 5 — Pricing** (conditional; omit section if BSM has no pricing object)
- Per-call price + currency + billing unit (e.g. "€0.02 / call")
- Billing model if declared
- "Pricing not declared — contact provider" if the BSM pricing object is absent
**Section 6 — Contact & registration**
- Registrant contact email (mailto link)
- Registered since (human date)
- BSM version
**Section 7 — Integration** (`<details>` collapsible; closed by default)
- Endpoint URL with one-click copy button
- OpenAPI spec URL link + MCP spec URL link (if declared in BSM)
- Minimal HTTP example snippet:
```
GET {endpoint}
X-APIX-Caller: your-agent-id
```
- Link to machine-readable entry: `GET /api/services/{id}` (JSON)
#### ServiceDetailViewModel — portal-internal view model
| # | Deliverable | Status |
|---|---|---|
| C-65 | `model/ServiceDetailViewModel.java` — Java record; portal-internal (not in `apix-common`); carries: all BSM payload fields; `oLevelLabel` (e.g. "Legal Entity Verified"); `oLevelDescription` (2-sentence plain-English, locale-resolved from Messages); `oLevelColorClass` (CSS class: `trust-unverified`/`trust-basic`/`trust-strong`); same pattern for `sLevel*`; `livenessLabel`, `livenessColorClass`, `livenessExplanation`; `formattedUptime` ("98.4%"), `formattedAvgResponseMs` ("142 ms"); `registeredAtRelative`, `registeredAtIso`; `lastCheckedAtRelative`, `lastCheckedAtIso`; `gleifLeiUrl` (null or `https://search.gleif.org/#/record/{lei}`); `isReferenceEntry` boolean | `[ ]` |
| C-66 | Update `PortalResource.java` — `GET /services/{id}`: call `RegistryClient.getDetail(id)`, build `ServiceDetailViewModel` via `ServiceDetailViewModelFactory`, pass to `service.html` template | `[ ]` |
| C-67 | `service/ServiceDetailViewModelFactory.java` — CDI bean; receives `ServiceDetailDto` + `Locale` + injected `Messages`; resolves O-level description string by level index (keys: `service.oLevel.0.description` … `service.oLevel.5.description`); computes relative timestamps via `java.time`; builds `ServiceDetailViewModel` | `[ ]` |
**O-level description message keys** (to be added to C-54 `Messages.java` + C-55/C-56 properties files):
| Key | EN value |
|---|---|
| `service.oLevel.0.name` | Unverified |
| `service.oLevel.0.description` | Self-declared only. No identity claims have been independently confirmed by APIX. Treat output with appropriate caution. |
| `service.oLevel.1.name` | Identity Verified |
| `service.oLevel.1.description` | The provider's domain ownership has been confirmed via DNS verification and their business email is reachable. APIX has confirmed this registrant controls the declared domain. |
| `service.oLevel.2.name` | Legal Entity Verified |
| `service.oLevel.2.description` | The provider's legal incorporation has been confirmed against the GLEIF global legal entity database or an authoritative company register. This is a real, registered legal entity — not self-declared. |
| `service.oLevel.3.name` | Hygiene Verified |
| `service.oLevel.3.description` | The provider publishes a security contact, enforces email authentication (DMARC/SPF), and maintains reachable legal documents. They meet baseline operational transparency standards. |
| `service.oLevel.4.name` | Operationally Verified |
| `service.oLevel.4.description` | An accredited APIX Verifier has assessed this provider's operational practices, incident response, and SLA track record. This level requires a human review process. |
| `service.oLevel.5.name` | Audited |
| `service.oLevel.5.description` | The provider holds a third-party security audit certificate (SOC 2 Type II or ISO 27001) confirmed by an APIX Accredited Verifier. The highest trust level available. |
Same key pattern for `service.sLevel.*`. German equivalents go in `messages_de.properties`.
#### i18n — server-side message bundles (Quarkus @MessageBundle)
| # | Deliverable | Status |
|---|---|---|
| C-54 | `Messages.java` — `@MessageBundle` interface; one method per translatable string key; sections: `nav.*`, `home.*`, `register.*`, `search.*`, `service.*`, `admin.*`, `help.*`, `error.*` | `[ ]` |
| C-55 | `resources/i18n/messages.properties` — English strings (all keys defined; this is the build-time default) | `[ ]` |
| C-56 | `resources/i18n/messages_de.properties` — German strings (same key set as EN) | `[ ]` |
| C-57 | `LocaleResolver.java` — CDI bean; reads `apix-locale` cookie first, then `Accept-Language` header, then falls back to `Locale.ENGLISH`; returns `java.util.Locale` | `[ ]` |
| C-58 | `resource/LocaleResource.java` — `POST /locale`; validates `lang` param against `["en","de"]`; sets `apix-locale` cookie (HttpOnly, SameSite=Lax, path `/`); redirects to `Referer` | `[ ]` |
#### Help system — overlay engine + tour content
| # | Deliverable | Status |
|---|---|---|
| C-59 | `resources/META-INF/resources/help.js` — client-side tour engine; spotlight four-wing dimming (`help-dim-top/left/right/bottom`) + highlight ring; draggable tour card (header grip); progress dots; state indicator; page-help drawer (slide-in right); context filter (shows only tours whose `pages` includes current `<body data-page-id>`); reads `window.PAGE_TOURS` + `window.PAGE_HELP`; no external dependency | `[ ]` |
| C-60 | `templates/layout.html` — Qute base layout extended by all portal pages; contains: nav bar with help button (?) and language switcher form; overlay HTML (4 wing divs + highlight ring + tour card shell + progress dots); help drawer HTML (guided tour list + page help section); `<script src="/help.js">`; all nav/chrome strings via `{inject:msg.*}` | `[ ]` |
| C-61 | `model/TourDefinition.java` + `model/TourStep.java` — Java records; `TourDefinition`: tourId, pages (list of page IDs), title, steps; `TourStep`: targetSelector (CSS selector), title, body, checkFn (optional JS function name for step validation) | `[ ]` |
| C-62 | `service/HelpContentService.java` — CDI bean; builds locale-resolved list of `TourDefinition` using injected `Messages`; serializes to JSON (Jackson); defines 5 MVP tours: `tour-agent-setup` (3 steps, home), `tour-register` (5 steps, register), `tour-search` (3 steps, search), `tour-trust` (4 steps, service detail), `tour-admin` (4 steps, admin) | `[ ]` |
---
### Tests
Tests live in each module's `src/test/`. Library module tests are plain JUnit (fast). Quarkus module tests use `@QuarkusTest` with `@QuarkusTestResource` for Testcontainers (PostgreSQL).
#### apix-verification tests (plain JUnit 5 + WireMock)
| # | Deliverable | Status |
|---|---|---|
| C-43 | `O1DnsVerifierTest` — valid domain, missing TXT record, wrong value | `[ ]` |
| C-44 | `O2GleifVerifierTest` — LEI match, unknown LEI, timeout (WireMock HTTP server) | `[ ]` |
| C-45 | `O3HygieneVerifierTest` — security.txt present/absent, DMARC/SPF checks | `[ ]` |
| C-46 | `SanctionsScreenerTest` — match, no match, false positive (local CSV fixture) | `[ ]` |
| C-47 | `VerificationPipelineTest` — O-0 → O-3 happy path; blocked at O-2 (sanctions); O-3 failure preserves O-2 | `[ ]` |
| C-48 | `LivenessEvaluatorTest` — pure logic: 200+fast=LIVE, 200+slow=DEGRADED, 503=UNREACHABLE, timeout=UNREACHABLE | `[ ]` |
#### apix-registry tests (@QuarkusTest + Testcontainers + RestAssured)
| # | Deliverable | Status |
|---|---|---|
| C-49 | `RegistryServiceTest` — register happy path, duplicate UPSERT, invalid BSM rejected | `[ ]` |
| C-50 | `ServiceResourceTest` — capability search match/no-match/partial; HATEOAS root links present | `[ ]` |
| C-51 | `RegisterResourceTest` — valid registration 201, missing API key 401, invalid payload 400 | `[ ]` |
#### apix-spider tests (@QuarkusTest + WireMock)
| # | Deliverable | Status |
|---|---|---|
| C-52 | `SpiderSchedulerTest` — trigger one check cycle; verify liveness written to DB | `[ ]` |
#### apix-portal tests (@QuarkusTest + WireMock for registry)
| # | Deliverable | Status |
|---|---|---|
| C-53 | `PortalResourceTest` — homepage renders; search form submits; registration form renders | `[ ]` |
| C-63 | `LocaleResolverTest` — DE `Accept-Language` header → `Locale.GERMAN`; EN cookie overrides DE header → `Locale.ENGLISH`; absent header + absent cookie → `Locale.ENGLISH` default | `[ ]` |
| C-64 | `HelpContentServiceTest` — register page returns exactly 5 tour steps; home page tour excludes `tour-admin`; DE locale produces DE strings in tour title/body JSON; page filter returns only tours referencing current page ID | `[ ]` |
| C-68 | `ServiceDetailViewModelFactoryTest` — O-0 produces colorClass `trust-unverified` + EN description; O-2 with LEI → `gleifLeiUrl` non-null and correct; `registeredAtRelative` computed from fixed `Instant`; `isReferenceEntry` true when flag set; DE locale → DE description strings from properties | `[ ]` |
---
## Infrastructure Deliverables
### Docker / Compose — `infra/`
| # | Deliverable | Status |
|---|---|---|
| I-01 | `docker-compose.yml` — five services: registry (:8180), spider (:8082 internal), portal (:8081), db (postgres:16-alpine), caddy (:80/:443) | `[ ]` |
| I-02 | `docker-compose.override.yml` — dev overrides: JVM mode for all three Quarkus apps (`quarkus dev`); all ports exposed; no TLS; hot reload | `[ ]` |
| I-03 | `Caddyfile` — HTTPS auto-cert; `/api/*` → registry:8180; `/*` → portal:8081; spider has no public route | `[ ]` |
| I-04 | `apix-registry/Dockerfile` — multi-stage: GraalVM 21 Maven builder → UBI Minimal 8 runtime; non-root user; exposes 8180 | `[ ]` |
| I-05 | `apix-spider/Dockerfile` — multi-stage: GraalVM 21 Maven builder → UBI Minimal 8 runtime; non-root user; no exposed port (internal only) | `[ ]` |
| I-06 | `apix-portal/Dockerfile` — multi-stage: GraalVM 21 Maven builder → UBI Minimal 8 runtime; non-root user; exposes 8081 | `[ ]` |
| I-07 | `.env.example` — QUARKUS_DATASOURCE_JDBC_URL, API_KEY, GLEIF_API_URL, OPENCORPORATES_API_KEY, SANCTIONS_CACHE_PATH, SPIDER_INTERVAL, LOG_LEVEL, REGISTRY_BASE_URL (portal→registry) | `[ ]` |
### Hetzner — `infra/hetzner/apix-app/` (APIX application VPS)
| # | Deliverable | Status |
|---|---|---|
| I-08 | `provision.sh` — VPS bootstrap: Docker install + Swarm init (`docker swarm init`), firewall (80/443 only), swap, non-root user, Hetzner volume mount | `[ ]` |
| I-09 | `backup.sh` — pg_dump to Hetzner volume; retain 7 dumps; run via cron at 03:00 UTC | `[ ]` |
| I-10 | DNS setup notes — A record for `registry.botstandards.org` (or agreed domain) → apix-app VPS IP | `[ ]` |
| I-11 | `sanctions/download.sh` — download OFAC SDN, EU consolidated, UN SC sanctions lists; run weekly via cron | `[ ]` |
| I-12 | `docker-stack.yml` — production Swarm stack; all five services (registry, spider, portal, db, caddy); `deploy.update_config.order: start-first`; `deploy.rollback_config`; health check references; image tags pulled from Gitea registry | `[ ]` |
### Hetzner — `infra/hetzner/apix-gitea/` (Gitea VPS)
| # | Deliverable | Status |
|---|---|---|
| I-13 | `provision.sh` — Gitea VPS bootstrap: Docker install, firewall (22/80/443), swap, non-root user | `[ ]` |
| I-14 | `docker-compose.yml` — Gitea + Caddy on Gitea VPS; Gitea data volume; SQLite (no external DB) | `[ ]` |
| I-15 | `gitea-config/app.ini` — Gitea configuration: container registry enabled, Gitea Actions enabled, organisation `bot-standards-foundation`, domain `gitea.botstandards.org` | `[ ]` |
| I-16 | GitHub push mirror setup — configure Gitea push mirror to `github.com/bot-standards-foundation/*` for all repositories; triggered on every push to `main` | `[ ]` |
| I-17 | act_runner JVM install — Gitea Actions runner on Gitea VPS; Java 21 + Maven; handles Stage 1 (fast cycle) and Stage 3 (deploy) CI jobs | `[ ]` |
| I-18 | act_runner GraalVM install — Gitea Actions runner on Gitea VPS (or apix-app VPS off-hours); GraalVM 21; handles Stage 2 (native image build); configured to run max 1 concurrent native job | `[ ]` |
| I-19 | DNS setup notes — A record `gitea.botstandards.org` → Gitea VPS IP | `[ ]` |
### CI/CD Pipelines — `.gitea/workflows/`
| # | Deliverable | Trigger | Status |
|---|---|---|---|
| I-20 | `ci-fast.yml` — Stage 1: `mvn verify` (JVM); all module unit tests + `@QuarkusTest`; WireMock-based verification tests; ~35 min | Every push | `[ ]` |
| I-21 | `ci-native.yml` — Stage 2: `mvn package -Pnative` per Quarkus module; Docker multi-stage build; `@QuarkusIntegrationTest` against native container; push to Gitea registry as `<module>:main-<sha>` | Merge to `main` | `[ ]` |
| I-22 | `deploy.yml` — Stage 3: SSH to apix-app VPS; `docker service update --image <new> apix_<service>` for registry + spider + portal; poll `/q/health` until UP; fail + alert on timeout (Swarm auto-rollbacks) | Git tag `v*` | `[ ]` |
### Local Developer Scripts — `scripts/`
| # | Deliverable | Status |
|---|---|---|
| I-23 | `setup-dev.sh` — idempotent local setup: check/install Java 21 (SDKMAN prompt), Maven, Docker; copy `.env.example` → `.env` if missing; start PostgreSQL container; run Liquibase migrations via Maven; print next steps | `[ ]` |
| I-24 | `dev.sh` — start all three Quarkus modules in dev mode concurrently; each in its own named tmux pane (or background with log files if tmux absent); prints URLs on start | `[ ]` |
| I-25 | `restart.sh [registry|spider|portal|all]` — kill and restart the specified dev-mode process; defaults to `all`; reads PID file or tmux session name | `[ ]` |
| I-26 | `stop.sh` — stop all dev-mode Quarkus processes; stop PostgreSQL container; clean PID files | `[ ]` |
| I-27 | `reset.sh` — stop everything; drop and recreate local DB; re-run migrations; restart dev mode | `[ ]` |
| I-28 | `logs.sh [registry|spider|portal]` — tail logs for a specific dev-mode service; or `all` for multiplexed output | `[ ]` |
---
## Build Sequence
Develop in JVM mode (`quarkus dev`) throughout — fast reload, continuous testing. Native build only in Block 5 for the production image.
### Block 1 — Multi-Module Foundation (Weeks 12)
- C-00 (parent `pom.xml` — BOM, module declarations, plugin config)
- Create five Maven modules: `apix-common`, `apix-verification`, `apix-registry`, `apix-spider`, `apix-portal`
- Generate three Quarkus modules via code.quarkus.io (or `mvn quarkus:create`):
- `apix-registry`: RESTEasy Reactive, Hibernate ORM + Panache, PostgreSQL JDBC, Liquibase, SmallRye Health, Quarkus Security
- `apix-spider`: Quarkus Scheduler, Hibernate ORM + Panache, PostgreSQL JDBC, REST Client Reactive, SmallRye Health
- `apix-portal`: RESTEasy Reactive, Qute, REST Client Reactive, SmallRye Health
- C-01 to C-05 (apix-common: enums + DTOs)
- C-20 to C-24 (Liquibase changelogs + registry application.properties)
- I-01, I-07 (docker-compose skeleton + .env.example)
- D-05 to D-08, D-34 to D-47 (constraints + ADRs)
### Block 2 — Core Registry API (Weeks 34)
- C-06 to C-12 (apix-verification: all verifiers + pipeline + config)
- C-13 to C-19 (apix-registry: resources, services, model, repository)
- C-43 to C-48 (apix-verification tests — plain JUnit, no Quarkus context needed)
- C-49 to C-51 (apix-registry tests — @QuarkusTest + Testcontainers)
- D-09 to D-11 (context + technical diagrams)
### Block 3 — Spider Module (Weeks 56)
- C-25 to C-32 (apix-spider: scheduler, fetcher, evaluator, parsers, entity, repository, config)
- C-52 (SpiderSchedulerTest)
- I-05 (spider Dockerfile — multi-stage native)
- D-20 to D-23 (runtime view sequence diagrams)
### Block 4 — Portal Module + i18n + Help System + Human-Readable Service Detail (Weeks 79)
- C-33 to C-42 (apix-portal: resources, REST client, Qute templates, CSS, config)
- C-65 to C-67 (ServiceDetailViewModel, PortalResource update, ServiceDetailViewModelFactory)
- C-54 to C-58 (i18n: Messages interface with O/S-level description keys, EN/DE properties files, LocaleResolver, LocaleResource)
- C-59 to C-62 (help system: help.js, base layout, tour model records, HelpContentService with 5 tours)
- Update C-36 to C-40 (Qute templates: extend layout.html; replace hardcoded strings with `{inject:msg.*}`; add `<body data-page-id="...">` attribute; inject PAGE_TOURS + PAGE_HELP script block via HelpContentService)
- C-38 service.html: 7-section human-friendly layout (identity hero, trust card, liveness card, capabilities, pricing, contact, integration collapsible)
- C-53 (PortalResourceTest)
- C-63, C-64, C-68 (LocaleResolverTest, HelpContentServiceTest, ServiceDetailViewModelFactoryTest)
- I-03 (Caddyfile)
- I-06 (portal Dockerfile — multi-stage native)
- D-52 to D-53 (ADR-013 i18n, ADR-014 help system)
### Block 5 — Gitea + CI/CD Infrastructure (Weeks 910)
- I-13 to I-19 (provision Gitea VPS, install Gitea + Caddy, configure container registry, GitHub mirror, act_runners)
- I-20 (ci-fast.yml — Stage 1 pipeline; verify it passes on first push)
- I-21 (ci-native.yml — Stage 2; first successful native build + push to Gitea registry)
- I-23 to I-28 (local dev scripts: setup-dev, dev, restart, stop, reset, logs)
- I-02 (docker-compose.override.yml — JVM dev mode for all three Quarkus modules)
### Block 6 — Production Deployment + Zero Downtime (Weeks 1011)
- I-08 to I-12 (provision apix-app VPS as Swarm node, backup, sanctions download, docker-stack.yml)
- I-04 to I-06 (Dockerfiles for all three Quarkus modules — multi-stage native)
- I-22 (deploy.yml — Stage 3; first tagged release deployed with zero-downtime verification)
- D-24 to D-27 (deployment view — two-VPS diagram + Swarm rolling update sequence)
- Fix any GraalVM reflection hints discovered during native integration tests
### Block 7 — Arc42 Completion + Real Services (Weeks 12)
- Remaining arc42 docs (D-01 to D-04, D-40 to D-46)
- Register RS-01 to RS-08 (self + Lexnexum + reference registrations)
- Manual outreach to founding member candidates (RS-09)
---
## Real Services Target List
At least 5 live registered services required for a credible PoC. Candidates:
| # | Service | Type | Source |
|---|---|---|---|
| RS-01 | APIX index itself (`/health`, `/services`) | Self-registration | Day 1 |
| RS-02 | Public OpenAPI test service (Petstore or equivalent) | Demo | Day 1 |
| RS-03 | BSF website bot endpoint | Self-registration | Week 4 |
| RS-04 | Lexnexum — legal search / legal portal bot endpoint | innoit.de / lexnexum.ai — controlled registrant | Week 4 |
| RS-05 | GLEIF API — `legal-entity.lookup` capability | Reference registration by BSF; invite GLEIF to self-upgrade | Week 5 |
| RS-06 | OpenCorporates API — `company.lookup` capability | Reference registration by BSF | Week 5 |
| RS-07 | EU Sanctions API (eu-sanctions.io or direct list endpoint) — `sanctions.screen` capability | Reference registration by BSF | Week 5 |
| RS-08 | Companies House UK API — `org.verify.uk` capability | Reference registration by BSF | Week 5 |
| RS-09 | Early adopter from founding member outreach | Real | Week 8+ |
| RS-10 | Developer community volunteer (HN / LangChain) | Community | Week 10+ |
---
## Open Questions
| # | Question | Owner | Status |
|---|---|---|---|
| OQ-MVP-01 | Domain name for public PoC — `registry.apix.dev`, `index.botstandards.org`, other? | Carsten | `[ ]` |
| OQ-MVP-02 | GitHub organisation for open source repo — `bot-standards-foundation` or personal? | Carsten | `[ ]` |
| OQ-MVP-03 | API key distribution for write access during PoC — how are early registrants onboarded? | Carsten | `[ ]` |
| OQ-MVP-04 | Spider check frequency — every 15 min is reasonable for PoC; confirm acceptable load on registrant services | Carsten | `[ ]` |
| OQ-MVP-05 | Hetzner server size — CX22 (2 vCPU, 4GB) sufficient for PoC; upgrade path if needed | Carsten | `[ ]` |
| OQ-MVP-06 | OpenCorporates API key — free tier sufficient for PoC volume? Confirm rate limits before build | Carsten | `[ ]` |
| OQ-MVP-07 | Sanctions list update cadence — weekly download of OFAC/EU/UN lists acceptable? Check if any list requires a licence for automated use | Carsten | `[ ]` |
| OQ-MVP-08 | GLEIF API — confirm no API key required for public LEI lookup (currently free/open); check terms of use for automated batch use | Carsten | `[ ]` |
| OQ-MVP-09 | Reference registrations (RS-05 to RS-08): BSF registers third-party APIs at O-0. Confirm BSF ToS allows this. Prepare outreach template inviting self-registration upgrade. | Carsten | `[ ]` |
| OQ-MVP-10 | Gitea domain — `gitea.botstandards.org` or separate domain (e.g. `code.apix.dev`)? Confirm before provisioning Caddy TLS cert | Carsten | `[ ]` |
| OQ-MVP-11 | GitHub org name — `bot-standards-foundation` exists? Create before configuring push mirror | Carsten | `[ ]` |
| OQ-MVP-12 | Native build runner placement — on Gitea VPS or apix-app VPS? apix-app VPS risks resource contention during native build; Gitea VPS is cleaner but requires GraalVM installed there | Carsten | `[ ]` |
| OQ-MVP-13 | Zero-downtime validation — how to confirm health check passes before declaring deploy successful in CI? Poll `/q/health` with retry loop or use Swarm service inspection | Carsten | `[ ]` |