TEDY backend — validation/review brief
A self-contained task for an independent review agent. Read this end to end, then execute the
Validation objectives. Work read-only unless asked to fix; report findings ranked by severity with
file:line evidence. The branch is claude/terminology-editor-helex-migration-c79310; all TEDY backend
work lives under modules/tedy/backend/ and migration/tedy-migrate/.
1. Initial task (what was asked)
Migrate the terminology-editor backend from a reference terminology server into helex-tx, on the
Helex platform libraries. Hard requirements from the original brief:
- Create a backend under
modules/tedybased on an emr module template. - Add
run_frontend.sh tedy/run_backend.sh tedyupdate scripts. - Use helex-commons instead of the reference commons; no reference-product or vendor names
(“termx”/“kodality”) may remain in committed code, comments, commits, or PRs (real URLs only in a
gitignored
.env). - Use Forge (FHIR model lib) instead of the reference’s Zmei.
- Analyze the DB, rewrite changesets removing vendor references; every changeset must be
idempotent (
create if not exists, idempotent DML); new schema istedy; ship a separate jar that migrates the legacy DB →tedy, run manually before install; improve the model (a redundant table, an over-long table name). - (Phase 2) FHIR mapping between TEDY resources and FHIR + EMR FIS — out of scope for this review.
- Do not use Kefhir; recreate needed functionality in TEDY.
- Port all tests from the reference server. 9/10. Ensure endpoints work; note optimizations vs the reference.
- Be extensible for OAuth + user permissions.
Full plan: ~/.claude/plans/the-goal-is-migrate-spicy-shore.md.
2. Plan / decisions (locked with the user)
- Template: emr
modules/tx/backend(org.helex.tx) — the terminology-domain module — not genericmdm. - Build model: start standalone — the backend builds independently against published artifacts
org.helex.emr:commons-*(0.1.0-SNAPSHOT) andorg.helex.forge:*(0.20.0), no EMR checkout required; structured to also composite-build inside EMR later. - Package namespace:
org.helex.tx.tedy(emr’stxmodule is uniquelyorg.helex.tx). - Auth: TEDY-owned (a pluggable
SessionProviderchain +@Authorizedinterceptor) becausecore-backendis not published. ABAC ACL deferred untilcommons-tenantis published (a one-line emr change was made: it was added topublishableCoreLibrariesinemr/repo/build.gradle.kts, but the publish itself is a pending user action). Code Systems etc. are ACL-free for now (like emr’s globalmdm.practitioner). - DB idempotency: the
tedymodule changelog is idempotent (CREATE TABLE IF NOT EXISTS, guardedcreate_table_metadata,insert … on conflict) so a fresh install and a migrated legacy DB converge. It first includes the sharedcoreschema from the publishedcommons-db-core. - Data-model renames (agreed):
entity_property→code_system_property,entity_property_value→concept_property,defined_entity_property→defined_property,entity_version_code_system_version_membership→concept_version_membership,map_set*→concept_map*(Java packagemapset→conceptmap, modelsMapSet*→ConceptMap*; REST path stays/ts/map-sets).code_system.permissionsis dropped (ACL replaces it).code_system_entitykept (polymorphic supertype ofconcept+code_system_association). - Tests: two-stage (strengthen the reference suite first, then port to JUnit) — the reference-side strengthening (Stage A) and the full port (Stage B) are not yet done; TEDY ships its own JUnit Testcontainers ITs per slice instead.
- Jackson: commons-db is Jackson 2; Spring MVC (Boot 4) is Jackson 3 — loosely-structured fields are
Object/collections to cross that boundary.
3. Implemented part (what to review)
Commits on the branch (newest first):
| Commit | Content |
|---|---|
104cbf7 | migration extended to Value Sets + Spaces |
6a0c13a | run/contract-verification scripts |
44f51a2 | tedy-migrate standalone migration jar |
66989f5 | Spaces slice |
5f059e1 | Naming Systems + Defined Properties |
a184461 | Map Set slice (concept_map) |
3c9b3e5 | Value Set slice (compose + expansion) |
4e8c41b | concept associations |
3da59df | scaffold + auth core + Code System + concepts |
Backend (modules/tedy/backend, org.helex.tx.tedy)
- Standalone Gradle (JDK 25, Spring Boot 4.0.0);
build.gradle.kts,application.yml,application-dev.yml. - Auth core (
auth/):SessionProviderchain →SessionFilter→SessionStore;@Authorized+AuthorizationInterceptor;SecurityConfig(stateless, authz via the chain);GuestSessionProvidergrants*.*.*in dev (config-gated). Real OAuth/JWT provider is a documented follow-up. - Resources — each
model/ → repository/ (BaseRepository + forge-anvil) → service/ → api/: Code System (codesystem/, incl.concept/), Value Set (valueset/), Map Set (conceptmap/), Naming Systems (namingsystem/), Defined Properties (definedproperty/), Spaces (space/). - Changelog (
src/main/resources/tedy/db/changelog/): master includescore-dbthen the idempotenttedyDDL01…07. - REST:
/ts/code-systems(+/concepts, associations, versions,spaceIdfilter),/ts/value-sets(+/expand),/ts/map-sets(+/associations),/ts/naming-systems,/ts/defined-properties,/spaces(+/packages). - Optimization vs reference: Value Set compose
ruleSetand Map Setscopestored as one JSONB column instead of the reference’s normalized rule tables; expansion computed on demand.
Migration jar (migration/tedy-migrate)
- Standalone runnable jar.
TedyMigratorruns ordered idempotentINSERT … WHERE NOT EXISTSscripts (src/main/resources/migrate/*.sql) in one transaction, disabling the sys-column trigger to preservesys_*verbatim, applying every rename + thedefined_property_idbackfill + acore.seq_idfixup. Covers CS-family, associations, naming systems, concept maps, value sets, and spaces (sys.*→tedy.*).
Scripts (scripts/)
run_backend.sh tedy(Docker Postgres +devprofile Liquibase),run_frontend.sh tedy(VITE_TX_TARGET→ local backend, delegates todev.sh),verify-tedy.sh(curl smoke test),check-naming.sh(prohibited-term gate).
How it was verified (reproduce these)
- Backend ITs (Testcontainers Postgres, run Liquibase, hit HTTP):
CodeSystemControllerIT,ValueSetControllerIT,ConceptMapControllerIT,RegistryControllerIT,SpaceControllerIT—cd modules/tedy/backend && ./gradlew test(7 tests, all green). - Migration IT:
cd migration/tedy-migrate && ./gradlew test(2 tests: renames+backfill, idempotency). - Live smoke:
./scripts/run_backend.sh tedythen./scripts/verify-tedy.sh— all endpoints 200/204. - Naming gate:
./scripts/check-naming.sh— clean.
Known gaps / deferred (do NOT report as defects; confirm they’re correctly scoped out)
ABAC ACL wiring (pending commons-tenant publish); FHIR facade + FIS (Phase 2); the reference-suite test
port (Stage A/B); concept tree-search / cross-CS /ts/concepts / /entity-versions; Spaces GitHub/DevOps
sync + async diff; Map Set property-values / statistics / automap.
4. Validation objectives (the review task)
Assess correctness, faithfulness, and safety. Prioritize:
- Contract fidelity — do the controllers match the endpoint paths, params, and JSON shapes the
frontend clients expect? Cross-check
modules/tedy/backend/.../api/*againstlibs/tedy/src/lib/client/*.tsandlibs/tedy/src/lib/types/*.ts(paths,SearchResult={data,meta}, field names likemapSet/designationType/entityProperty, the/transactionbodies). - Persistence correctness — repositories: JSONB/
text[]/Forge-datatype column mappings (AnvilBeanProcessor/AnvilSqlBuilder), the client-supplied-text-id upsert, optimistic-lock/soft-delete handling, and thespaceIdjoin. Look for SQL injection (all use bound?), N+1 decoration, and Jackson 2/3 boundary bugs. - Renames & data model — confirm every agreed rename is applied consistently across DDL, models, repos,
and the migration; confirm
permissionsis dropped andcode_system_entityis retained as the supertype. - Migration safety — are all scripts truly idempotent (re-run = no change)? Are
sys_*preserved (trigger disabled)? Is thedefined_property_idbackfill correct and collision-free? Does the sequence fixup prevent future id collisions? Any source column referenced that wouldn’t exist in the real legacy schema? (The IT setup approximates it — flag mismatches.) - Auth extensibility — is the
SessionProviderchain genuinely pluggable, and is the guest*.*.*default safe (dev-gated, not on in prod)? Is@Authorizedenforced on every mutating endpoint? - Naming hygiene — independently grep committed sources for the prohibited terms.
- Idempotency of the module changelog — would a fresh install and a migrated DB converge? Any
create_table_metadata/index that isn’t guarded? - Build/verify reproducibility — do the ITs and the live smoke test actually pass on a clean checkout?
Deliver: a ranked findings list (severity, file:line, failure scenario), plus a short verdict on whether
the Phase-1 backend is faithful to the plan and safe to build on.