Skip to content

fix(scaffold): generate and validate Compose structurally - #4

Merged
iflov merged 2 commits into
mainfrom
iflov/scaffold-compose-structural
Aug 31, 2026
Merged

iflov merged 2 commits into
mainfrom
iflov/scaffold-compose-structural

Conversation

@iflov

@iflov iflov commented Aug 31, 2026

Copy link
Copy Markdown
Owner

두 레인(Claude / Codex)이 각각 리뷰·검증하고 브리프로 토의한 결과를 구현했습니다.

무엇이 잘못돼 있었나

생성기가 compose 서비스를 조용히 삭제했습니다. materializeTypeOrmCompose의 정규식이 RDB 서비스 블록의 끝을 "redis:가 바로 뒤에 온다"로 정의했습니다. 문서화되지 않은 파일 순서 불변식이라, DB와 redis 사이에 서비스를 하나 끼우면 lazy 매칭이 그것까지 삼켜서 지웠습니다. 에러도 경고도 없었습니다.

check:scaffold가 그걸 잡지 못했습니다. compose 검사가 선택된 DB image 존재만 보고 선택되지 않은 것의 잔존은 보지 않았습니다. 드라이버·ORM 의존성·모듈 마커는 전부 양방향인데 compose만 단방향이었습니다. postgres와 mysql이 함께 있는 compose가 Scaffold selection is consistent로 통과했습니다. 잔존 profile 파일도 같았습니다.

문서의 로컬 E2E 명령이 성립하지 않았습니다. DB_PORT=15432 pnpm test:e2e의 15432는 CI service container 포트고 compose엔 없었습니다. override를 떼면 synchronize: true + beforeEach truncate가 개발 DB(5432)로 갔습니다.

무엇을 했나

  • 생성기와 검증기를 YAML document 위에서 다시 씁니다. 생성기는 서비스 노드를 수정하고 primary RDB가 정확히 하나일 것을 요구합니다. 다른 서비스·top-level section·주석이 보존됩니다.
  • 검증기가 두 compose를 구조적으로 읽고 image, app의 depends_on과 DB_* 환경변수, port, tmpfs, 잔존 RDB 서비스, 잔존 profile 파일을 검사합니다.
  • docker-compose.test.yml이 선택된 RDB를 자기 Compose project에서 tmpfs로 띄우고 15432를 publish합니다. E2E 기본 포트가 여기를 가리킵니다.
  • E2E datasource가 DB_TYPE에서 driver를 파생합니다. 엔진의 진실 원천이 셋에서 하나로 줄었습니다.
  • BullMQ root를 QueueModule로 옮겨 broker의 composition root를 한 파일에 모읍니다.

MySQL에서 처음 드러난 결함

실제 MySQL E2E를 처음 돌리자 PostgreSQL 전용 실행이 가려온 것이 나왔습니다.

QueryFailedError: BLOB/TEXT column 'name' used in key specification without a key length

공용 픽스처의 text unique 컬럼이 PostgreSQL에서만 성립했습니다. varchar(255)로 바꾸고 schema는 PostgreSQL에서만 전달합니다.

검증

  • check:scaffold / check:observability / lint / typecheck / build 통과
  • unit 35 files / 317 tests (기존 306 → 회귀 테스트 11개 추가)
  • base·test compose 모두 config --quiet stderr 없음
  • 문서에 적힌 절차 그대로 E2E: PostgreSQL 2 files / 20 tests, 생성된 TypeORM/MySQL 프로젝트 2 files / 20 tests
  • BULLMQ_ENABLED=true, REDIS_ENABLED=false 조합 E2E 통과
  • MikroORM/PostgreSQL, TypeORM/MySQL materialize → install / check / lint / typecheck 통과
  • 기존 재현이 이제 막힙니다: 끼워 넣은 서비스가 보존되고, 잔존 DB 서비스와 주석 위장 image는 exit 1

미검증

MikroORM·Prisma의 MySQL/MariaDB 조합, MariaDB E2E, 대체 ORM의 Docker E2E.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Qkwp4T95EwEXeoDozJ6Y4m

iflov and others added 2 commits August 31, 2026 15:36
BullModule.forRootAsync was assembled in AppModule while the queue,
adapter and processor were assembled in QueueModule, so one broker's
composition root was split across two files and swapping BullMQ for
another broker meant editing both.

Move the root connection into QueueModule so AppModule only selects a
module. This also narrows when the Bull root is registered: it used to
be created whenever BULLMQ_ENABLED was set, and now follows the
existing `bullmqEnabled && redisEnabled` gate, so BULLMQ_ENABLED=true
with REDIS_ENABLED=false no longer opens a Redis connection.

Verified: e2e passes with BULLMQ_ENABLED=true and REDIS_ENABLED=false
(2 files / 20 tests).

No queue generator profile is introduced. BullMQ is the only supported
broker today, and broker replacement changes delivery, retry and DLQ
policy rather than just a connection, so the profile axis waits for a
second broker and its delivery contract.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qkwp4T95EwEXeoDozJ6Y4m
The generator located the RDB service block with a regex whose end was
"the redis service comes next", an undocumented ordering invariant of
docker-compose.yml. Any service inserted between the database and redis
was swallowed by the lazy match and deleted, with no error. Reproduced
by calling materializeTypeOrmCompose directly: an added pgadmin service
did not survive materialization to mysql.

check:scaffold could not catch it. Its Compose check only asserted that
the selected database image was present; it never asserted that an
unselected one was absent, while every other axis in the validator
(drivers, ORM dependencies, module markers) is bidirectional. A compose
carrying both postgres and mysql reported "Scaffold selection is
consistent". Leftover profile files were unchecked the same way.

Rewrite both on top of a YAML document:

- materializeTypeOrmCompose edits service nodes instead of substituting
  text, and requires exactly one primary RDB service. Other services,
  top-level sections and comments are preserved.
- scaffoldConsistencyIssues reads both Compose files structurally and
  checks the selected image, the app service's depends_on and DB_* env,
  ports, tmpfs, leftover RDB services and leftover ORM profile files.

Also isolate the e2e database. README, CLAUDE.md and AGENTS.md all
documented `DB_PORT=15432 pnpm test:e2e`, but 15432 is the CI service
container port and nothing in docker-compose.yml published it, so the
documented command could not connect on a clean clone. Dropping the
override sent an e2e suite that runs `synchronize: true` and truncates
in beforeEach at the development database on 5432.

docker-compose.test.yml now runs the selected RDB in its own Compose
project on tmpfs and publishes 15432, and the e2e default port points
at it. The e2e datasource derives its driver from DB_TYPE instead of
hardcoding postgres, so the engine has one materialized source rather
than three.

Running the e2e against a real MySQL for the first time surfaced an
engine-specific defect the PostgreSQL-only runs had hidden:

  QueryFailedError: BLOB/TEXT column 'name' used in key specification
  without a key length

The shared fixture's `text` unique column only worked on PostgreSQL. It
is now varchar(255), and `schema` is passed for PostgreSQL only.

Verified: check:scaffold, check:observability, lint, typecheck and
build pass; 35 files / 317 tests; both Compose files produce no config
warnings; the documented e2e path passes on PostgreSQL (2 files / 20
tests) and on a generated TypeORM/MySQL project (2 files / 20 tests);
MikroORM/PostgreSQL and TypeORM/MySQL materialize and typecheck. The
original C1 and C2 reproductions now preserve the added service and
exit 1 respectively.

Not verified: MikroORM and Prisma on MySQL/MariaDB, MariaDB e2e, and
Docker e2e for the alternative ORMs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qkwp4T95EwEXeoDozJ6Y4m
@iflov
iflov merged commit a389221 into main Aug 31, 2026
1 check passed
@iflov
iflov deleted the iflov/scaffold-compose-structural branch August 31, 2026 06:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant