Files
llm-wiki/raw/branch-notes/feature-sample-domain-contract-fixture.md

384 lines
37 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.
---
title: branch / feature-sample-domain-contract-fixture
source_type: branch-note
status: raw
branch: feature-sample-domain-contract-fixture
parent_branch:
related_projects: [ca-skeleton]
governing_docs: [wiki/projects/ca-tmpl/sample-fixture-and-adoption]
tags: [branch, ca-skeleton, sample-domain, contract-fixture]
created: 2026-05-21
target_merge:
status_label: in-progress
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-014
kind: project-work-item
project: ca-skeleton-operational-contract
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-014
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MODULE-LAYOUT-001@1]
refines: []
overrides: []
depends_on: []
contract_packet: 1
contract_packet_sha256: 6d4baf1c3d8e37e66b1a0ad293a732d9e1bf0462c598959d9dc0ccdc12e49964
---
# branch: feature-sample-domain-contract-fixture
> Layer: `raw/branch-notes/` — skeleton 계약 검증을 위한 sample domain fixture 기준을 정의합니다.
<!-- section-id: branch-parent -->
## 부모 (필수)
- **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]]
> ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다.
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: sample domain fixture가 module contract를 검증하고 제거 smoke가 통과한다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MODULE-LAYOUT-001@1` | Gradle multi-module에서 domain-core·application-core·adapter-*·shared-contract·app-bootstrap·sample-portfolio 책임을 분리한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
<!-- section-id: branch-local-decisions -->
### 브랜치 지역 결정
> 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
<!-- section-id: declared-overrides -->
### 선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|---|---|---|---|
<!-- GENERATED: branch-contract:end -->
<!-- section-id: branch-goal -->
## 목표
도메인/비즈니스 로직은 제거하지만, 샘플 도메인이 없으면 경계 validation, mapper, repository capability, transaction, response contract를 실제 흐름으로 검증할 수 없습니다. sample은 기능이 아니라 contract fixture입니다.
- 이슈:
- PR:
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- `sample-portfolio` fixture.
- create/read/update/delete 최소 흐름.
- validation/not found/conflict/optimistic lock fixture.
- pagination fixture.
- repository capability fixture.
- idempotent command fixture.
- sample package/module/profile 격리 기준.
### 제외 범위
- 실제 서비스 도메인 기능.
- portfolio/blog/interview 직접 파생.
- production feature로 노출.
## 근거 (필수, 최소 1개+)
> 본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.
| Source | 정당화하는 결정 |
|---|---|
| [[raw/official-docs/sample-spring-petclinic-github]] | Spring 공식; "demo지 best-practice 아님" 본인 선언 |
| [[raw/official-docs/sample-realworld-gothinkster-github]] | cross-stack spec; 풍부하나 minimum 아니고 contract scenario 부재 |
| [[raw/official-docs/sample-microservices-spring-cloud-github]] | fixture 수준 초과; microservices variant |
## 외부 근거 / 대안 조사 (2026-05-22 — Group G-H: Sample Domain Fixture)
본 branch의 sample-portfolio (12 scenario matrix + 6-field minimum model + OPEN→IN_PROGRESS→CLOSED state machine + optimistic lock + idempotency key) 결정에 대한 외부 source.
- **채택 결정 (skeleton contract 검증 fixture로서 sample-portfolio)**:
- (ca-tmpl 고유; sample은 demo/tutorial이 아닌 contract 검증 도구라는 목적 정의)
- **검토한 대안**:
- **대안 1: Spring Petclinic** — [[raw/official-docs/sample-spring-petclinic-github]] (Spring 공식; "demo지 best-practice 아님" 본인 선언)
- **대안 2: RealWorld (gothinkster Conduit)** — [[raw/official-docs/sample-realworld-gothinkster-github]] (cross-stack spec; 풍부하나 minimum 아니고 contract scenario 부재)
- **대안 3: Spring Cloud Microservices sample** — [[raw/official-docs/sample-microservices-spring-cloud-github]] (fixture 수준 초과; microservices variant)
- **대안 4: Shopping cart (Stripe testmode)** — payment domain 한정, ca-tmpl 일반 skeleton 부적합
- **대안 5: No fixture (unit tests only)** — contract 검증 도구 부재로 거부
- **비교 핵심**: ca-tmpl sample-portfolio은 12 scenario × 6 model × state machine이 skeleton contract(envelope/error/capability/transaction/idempotency)를 모두 트리거하는 minimal fixture. Petclinic/RealWorld는 demo/teaching 목적이라 contract 검증 매트릭스 부재.
## TODO
> TODO drained 2026-05-22 — 결정은 아래 "결정 사항" / "Sample-portfolio Matrix" / "Minimum Model" / "테스트 계약" 참조. package/module/profile 격리, CRUD fixture, validation/not found/conflict/lock fixture, pagination, repository capability, idempotent command, production runtime 비활성화 구조 모두 Sample-portfolio Matrix row 또는 결정 라인으로 반영됨. 잔존 TODO 없음.
## Work Item Contract
각 TODO는 아래 판정 단위로 재작성되어야 canonical 승급 가능합니다. TODO가 단순히 `기준 작성`으로 남아 있으면 이 branch는 완료로 보지 않습니다.
| field | required | rule |
| --- | --- | --- |
| Decision | yes | 구현자가 선택해야 하는 기본값 |
| Allowed | yes | 허용되는 예외와 조건 |
| Forbidden | yes | 절대 금지되는 구현/문서 상태 |
| Required registry update | conditional | error/env/header/log/metric/capability 변경 시 필수 |
| Required contract test | yes | 계약 위반 시 실패해야 하는 테스트 |
| Failure condition | yes | review/build에서 실패로 판정할 상태 |
| Canonical extraction target | yes | `wiki/projects` 승급 위치 |
## 진행 중 메모
- sample은 business feature가 아니라 skeleton contract를 보여주는 living example입니다.
- 2026-06-10 구현: delegated 항목(idempotency dedup/storage, sample-off/profile isolation, dual-mode CI)은 제외하고, 본 branch covered-here gap이던 `WorkLogStatus` 상태 머신과 `WorkLogOwner` minimum model을 코드에 반영했다. 상태 전이는 `OPEN -> IN_PROGRESS -> CLOSED`만 허용하며, `status=OPEN` 되돌리기와 `CLOSED` 상태의 내용 변경은 domain invariant conflict로 실패한다.
- 2026-06-10 구현 파일: `WorkLog.java`, `WorkLogStatus.java`, `WorkLogOwner.java`, `WorkLogInvariantException.java`, `UpdateWorkLogCommand.java`, `UpdateWorkLogUseCase.java`, `UpdateWorkLogRequest.java`, `WorkLogController.java`, `WorkLogWebMapper.java`, response DTO 2종, `WorkLogEntity.java`, `WorkLogPersistenceMapper.java`, `V2__work_log.sql`, 관련 domain/application/persistence/web tests.
- 2026-06-10 검증: focused RED는 `:sample-portfolio:compileTestJava`에서 missing `WorkLogStatus`/`WorkLogOwner`/status accessor/command status patch/`CLOSED_WORKLOG_MUTATION`으로 실패 확인. GREEN 후 `:sample-portfolio:test`, `verifyCleanArchitectureDependencies`, `:app-bootstrap:test --tests '*CleanArchitectureTest'`, `test`, `check` 모두 exit 0.
- 2026-06-10 owner=principal capture (Option A, spec `ca-tmpl docs/superpowers/specs/2026-06-10-worklog-owner-principal-capture-design.md`): `WorkLogOwner` 가 더 이상 상수 `sample-owner` 가 아니라 **create 시점 인증 principal 의 subject**. `CreateWorkLogCommand.owner`(String) 신설 → `WorkLogController``SecurityContextHolder``AuthenticatedUser.idpUserId()` 를 주입(`currentOwnerSubject()`, `RateLimitKeyResolver` 와 동일 null-safe idiom) → `CreateWorkLogUseCase`/`BatchCreateWorkLogsUseCase``WorkLogOwner.of(cmd.owner())` 로 생성. **privacy**: `WorkLogResponse`/`WorkLogSummaryResponse` 에서 raw `owner` 제거(`WorkLogWebMapper` 정합) — raw principal id 응답 비노출. pseudonymization([[raw/branch-notes/feature-data-retention-privacy-contract]])·owner-scoped authz([[raw/branch-notes/feature-authentication-authorization-contract]] D2 ABAC 보류)는 sibling SSOT 위임. TDD RED→GREEN: `WorkLogUseCasesTest.create_sets_owner_from_command_principal`, `WorkLogControllerWireTest.create_captures_authenticated_principal_as_owner`, `..._response_does_not_expose_owner`(privacy). 게이트 `:sample-portfolio:test`·`verifyCleanArchitectureDependencies`·`*CleanArchitectureTest` exit 0.
## 결정 사항 (decisions)
- 2026-05-21: sample domain fixture는 필요.
- 2026-05-22: sample domain 이름은 `sample-portfolio`을 기본값으로 둠.
- 2026-05-22: sample은 production feature가 아니라 contract fixture이며 prod profile에서는 기본 비활성화.
- 2026-05-22: worklog status transition, owner/assignee policy, optimistic lock, idempotent create, pagination을 검증 대상으로 둠.
- 2026-05-22: sample-portfolio fixture의 SSOT는 이 branch. verification/DX/scorecard/onboarding branch는 scenario와 minimum model을 소비만 함.
- 2026-05-22: sample-off smoke scenario는 `feature-sample-removal-adoption-contract`와 함께 release-blocking verification 대상.
## 판정 기준
| 구분 | 기준 |
| --- | --- |
| Decision | `sample-portfolio`을 skeleton contract fixture로 사용 |
| Allowed | 실제 프로젝트 생성 시 sample-off profile로 runtime 노출 차단. fork cleanup은 선택 사항이며 template의 `sample-portfolio` module은 fixture/reference로 유지 |
| Forbidden | sample 결과를 portfolio/blog/interview 산출물로 직접 파생 |
| Required fixture | create/read/update/close, validation, not found, conflict, optimistic lock, pagination, repository capability, idempotency |
| Failure condition | sample 없이 boundary/repo/transaction/error contract test를 검증하려 하면 실패 |
## 결정-근거 매핑
> 본 branch 의 핵심 결정 (sample-portfolio = contract fixture, 12 scenario matrix, 6-field minimum model, state machine, optimistic lock, idempotency) 은 ca-tmpl 고유 모델. 외부 raw 는 "기존 sample 들이 contract fixture 목적에는 부적합" 이라는 대조 근거만 제공.
| Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|
| D1 | sample domain fixture 가 필요 (skeleton 계약 검증 도구) | `raw/official-docs/sample-spring-petclinic-github.md#SAMPLE-PC-C4` (PetClinic 의 "best practice 아님" disclaimer — 기존 sample 차용 위험 근거) | `official-vendor-doc` (대조 근거) | "fixture 가 필요" 자체는 ca-tmpl 고유 결정 — 외부 raw 는 기존 sample 의 한계만 증명 |
| D2 | sample domain 이름 = `sample-portfolio` (기본값) | (ca-tmpl 고유 명명; 외부 raw 가 worklog 도메인을 권장하지 않음) | UNSUPPORTED_DECISION | naming 자체 외부 근거 없음 |
| D3 | sample 은 production feature 가 아닌 contract fixture, prod profile 기본 비활성화 | `raw/official-docs/sample-spring-petclinic-github.md#SAMPLE-PC-C1` (PetClinic 이 demo 목적임을 시인), `raw/official-docs/sample-realworld-gothinkster-github.md#SMP-RW-C1` (RealWorld 가 demo apps 묶음임) | `official-vendor-doc` + `engineering-blog` (RealWorld 는 OSS community spec → 본 branch 가 `engineering-blog` 로 분류; **공식 best practice 격상 금지**) | "sample = fixture" 정의 자체는 ca-tmpl 고유. 외부 raw 는 기존 sample 들이 demo 라는 사실만 증명 |
| D4 | worklog status transition / owner/assignee policy / optimistic lock / idempotent create / pagination 을 검증 대상 | `raw/official-docs/sample-spring-petclinic-github.md#SAMPLE-PC-C4` ("not a best practice" — PetClinic 에 이 시나리오 없음을 대조), `raw/official-docs/sample-realworld-gothinkster-github.md#SMP-RW-C2` (RealWorld API spec 이 corner case cover 안 함 시사), `raw/official-docs/sample-realworld-gothinkster-github.md#SMP-RW-C3` (modularity 만 보장 — concurrency / optimistic lock 등 corner case 미보장은 "Does not prove" 컬럼) | `official-vendor-doc` + `engineering-blog` (대조 근거만) | optimistic lock / idempotency 가 본 branch 가 필수로 둔다는 사실은 ca-tmpl 고유 — 외부 표준이 권장하지 않음 |
| D5 | sample-portfolio fixture SSOT 는 본 branch. verification/DX/scorecard/onboarding branch 는 scenario / minimum model consume only | (ca-tmpl 고유 SSOT 정책; 외부 raw 직접 근거 없음) | UNSUPPORTED_DECISION | sibling branch 간 governance 결정 |
| D6 | sample-off smoke scenario 는 `feature-sample-removal-adoption-contract` 와 함께 release-blocking verification | (sibling branch 간 contract; 외부 raw 직접 근거 없음) | UNSUPPORTED_DECISION | sibling branch 의 dual-mode CI matrix 와 일관성 외 외부 표준 없음 |
## 구현 가이드
> *결정 (D1~D6)* 이 "*무엇* 을" 이라면, 본 §는 sample-portfolio fixture 가 ca-tmpl `/home/donghyeon/workspace/ca-tmpl/src/sample-portfolio/` 에 *실제로 어떻게* 구현됐는지의 **코드 정합 명세**다 (2026-06-10 ground-truth 대조). 노트(2026-05-22)가 설계로 적은 모델/명명과 코드가 어긋난 지점은 §Audit & Findings 에 drift 로 분리하고, 본 §에는 *코드로 확인된 사실(actually-implemented)* 과 *아직 코드 없는 항목(planned/delegated)* 만 남긴다.
>
> **3-rule (CLAUDE.md §15.5)**: 각 row 는 `Decision ID` + 근거(코드 anchor 또는 Claim ID). 근거 없는 임의 detail = `UNSUPPORTED_IMPL_DECISION`. 본 branch 결정 범위 밖(다른 owner branch 소유 계약) detail = `OUT_OF_BRANCH_SCOPE` 로 owner 에 위임.
### 1. Fixture scenario → 트리거 계약 → 실제 검증 test (D1, D4)
> **Trace**: D1 (fixture 필요) + D4 (worklog 검증 시나리오) → ca-tmpl `src/sample-portfolio`. 등급은 `src/` grep 으로 확정(2026-06-10; note 자기보고 아님).
>
> - **UNSUPPORTED_IMPL_DECISION**: test 클래스 명명 규약. 노트는 `Sample{ScenarioName}ContractTest.java` / `SampleIdempotentReplayContractTest` 로 추정했으나 실제 규약은 `{Domain}{Layer}{Type}Test` (`WorkLogControllerWireTest`, `WorkLogAuthorizationContractTest`, `GetRepoStatsUseCaseTest`). trade-off: 추정 명명을 따르면 신규 test 가 기존 규약과 어긋남 → 실제 규약 채택.
| 노트 scenario | 트리거 계약 unit | 실제 검증 (file · method, ca-tmpl@2026-06-10) | 등급 |
|---|---|---|---|
| create success | request mapper · command validation · WRITE capability · transaction · response mapper | `WorkLogControllerWireTest.batch_create_all_ok_returns_array` (L286) · `WorkLogTest.create_keeps_assigned_id_and_fields` (L21) · `CreateWorkLogUseCase @UseCaseCapability(WRITE, NOT_IDEMPOTENT)` (L23) | `actually-implemented` |
| create validation failure | structured validation details · client-safe msg | `WorkLogControllerWireTest.create_with_blank_title_fails_validation` (L231) / `create_with_unknown_field_is_rejected_b1` (L239) / `..._unmappable_link_routes_to_mapping_failed_b3` (L248) · domain `WorkLogInvariantTest.blank_title_is_rejected_with_a_safe_reason_on_create` (L28) | `actually-implemented` |
| get not found | `WORKLOG_NOT_FOUND` · 404 · retryable=false | `WorkLogControllerWireTest.get_missing_returns_404_worklog_not_found` (L208) · `PortfolioErrorCode.WORKLOG_NOT_FOUND(Category.NOT_FOUND,404,false)` (`adapter/web/error/PortfolioErrorCode.java`) | `actually-implemented` |
| list pagination | page meta · sort/filter | `WorkLogControllerWireTest.list_is_wrapped_in_envelope_with_page_meta` (L79) / `empty_list_is_data_array_not_null_with_zero_total` (L92) / `size_over_cap_is_400_validation_with_field_and_code` (L102) | `actually-implemented` |
| optimistic lock conflict | version → ETag/If-Match → HTTP 412 | `WorkLogControllerWireTest.patch_with_stale_if_match_returns_412` (L152) / `get_emits_etag_header` (L136) / `patch_with_matching_if_match_applies_update` (L161) · `WorkLog.version:Long` (`domain/worklog/WorkLog.java` L37) | `actually-implemented` · **메커니즘 `OUT_OF_BRANCH_SCOPE` → [[raw/branch-notes/feature-api-contract-baseline]] D15** |
| unauthorized update | auth/authz 분리 · fail-closed | `WorkLogAuthorizationContractTest.unauthenticated_caller_is_denied_fail_closed` (L125) / `authenticated_user_without_close_permission_is_denied_delete` (L102) · `WorkLogAuthorizationE2ETest` | `actually-implemented` |
| outbound forbidden use case | `EXTERNAL_OUTBOUND_ALLOWED` capability gate | `GetRepoStatsUseCaseTest.delegates_to_port` (L14) · `GetRepoStatsUseCase @UseCaseCapability(READ_ONLY, externalOutboundAllowed=true)` (L15) | `actually-implemented` · **capability `OUT_OF_BRANCH_SCOPE` → `feature-repository-access-permission-contract`** |
| idempotent create replay | Idempotency-Key 헤더 수용 · dedup storage | 헤더 수용: `WorkLogControllerWireTest.post_accepts_idempotency_key_header` (L196) — `actually-implemented`. **dedup/replay storage: 코드 없음**`planned` · `OUT_OF_BRANCH_SCOPE``feature-rate-limit-idempotency-contract` (Idempotency-Key header owner) | `planned` (dedup) |
| update invalid transition | (노트: status state machine) | **코드에 status state machine 없음** — 도메인은 `rename()`/`recategorize()` (`WorkLog.java`) 만, `OPEN→IN_PROGRESS→CLOSED` 부재 → §Audit `MODEL_DRIFT`. 상태전이 검증 test 없음 | `planned` / drift |
| sample disabled startup | prod profile isolation | **코드에 `@Profile`/`@ConditionalOnProperty` 없음**`APP_SAMPLE_ENABLED` env 만 registry 선언 → `planned` · `OUT_OF_BRANCH_SCOPE``feature-sample-removal-adoption-contract` | `planned` |
| sample-off smoke | sample/core decoupling · dual-mode CI | **CI workflow / smoke test 코드 없음**`planned` · `OUT_OF_BRANCH_SCOPE``feature-sample-removal-adoption-contract` (dual-mode CI matrix owner) | `planned` |
> **코드가 노트 matrix 를 초과 cover (SCENARIO_EXPANSION, §Audit)**: 실제 fixture 는 12-scenario 외에 ETag 304 (`get_with_matching_if_none_match_returns_304_no_body` L145), HEAD 지원 (L173), batch atomic (`batch_create_is_atomic_one_bad_item_fails_whole_batch` L310), sort syntax 검증 (native accept / jsonapi-prefix reject L111/L119), ULID 정규화 (L223) 도 검증한다 — 노트 §Sample-portfolio Matrix 갱신 시 반영 권고.
### 2. Sample module 격리 + SSOT governance 정적 강제 (D2, D5)
> **Trace**: D2 (이름 `sample-portfolio`) + D5 (본 branch 가 fixture SSOT) → 실제 package + ArchUnit.
>
> - **UNSUPPORTED_IMPL_DECISION**: 격리 강제 *메커니즘* 선택(ArchUnit vs Gradle module 경계 vs `@Profile`). 노트는 원칙(production 노출 차단)만 결정 — 실제 코드는 ArchUnit 채택. trade-off: 컴파일 차단(Gradle 경계)보다 약하나 단일 test 모듈에서 검증 가능.
| 강제 대상 | 메커니즘 (실제) | 등급 |
|---|---|---|
| production code 가 sample import 금지 (D5 SSOT, D2 격리) | ArchUnit `production_code_does_not_depend_on_sample_portfolio` = `noClasses().that().resideOutsideOfPackage("..sample.portfolio..").should().dependOnClassesThat().resideInAPackage("..sample.portfolio..")` (`app-bootstrap/.../architecture/CleanArchitectureTest.java` L576581) | `actually-implemented` |
| sample package 명명 (D2) | `dev.caskeleton.sample.portfolio.*` (domain/application/adapter 4-layer) | `actually-implemented` |
| prod runtime 노출 차단 (D3) | `APP_SAMPLE_ENABLED` (default true, `prod_profile_must_be_false`) — **owner_branch=`feature-sample-removal-adoption-contract`**`OUT_OF_BRANCH_SCOPE`. 런타임 `@Profile`/`@ConditionalOnProperty` 코드 아직 없음 | `documented-only` · delegated |
| sibling 이 scenario/minimum model 독자 변경 금지 (D5) | 자동 강제 메커니즘 없음 — wikilink cross-ref + review. `UNSUPPORTED_IMPL_DECISION`: 사회적 강제만, 정적 분석 불가 | `documented-only` |
## 엣지·실패·의존
> R4 캡처. sample-portfolio fixture 는 *자체 계약을 거의 소유하지 않고* sibling branch 계약을 **소비/트리거**한다 — 그 계약이 바뀌면 fixture test 가 깨진다.
- **실패·엣지 경로**:
- **stale If-Match → 412**: 동시 update 시 version 불일치. 기대: `patch_with_stale_if_match_returns_412` (412 PRECONDITION_FAILED, Category.CONFLICT). raw JPA optimistic-lock exception 이 presentation 까지 전파되면 실패.
- **blank/누락 title**: web `@NotBlank` (syntax) + domain `requireValidTitle()` (invariant, `WorkLogInvariantException.Reason.TITLE_BLANK`) 2중 방어 — domain 검증이 web 뒤에서도 독립 동작 (`WorkLogInvariantTest.null_title_stays_a_null_check_not_an_invariant_violation` L43).
- **unknown/unmappable field**: `create_with_unknown_field_is_rejected_b1` / `..._unmappable_link_routes_to_mapping_failed_b3` — boundary-validation 계약 위반 시 실패.
- **fail-closed authz**: 미인증 호출이 deny 안 되면 (`unauthenticated_caller_is_denied_fail_closed`) 실패.
- **batch 부분 실패**: `batch_create_is_atomic_one_bad_item_fails_whole_batch` — 1건 실패가 전체 롤백 안 되면 transaction 경계 위반.
- **idempotency dedup 미구현**: 동일 Idempotency-Key 재요청 시 현재 헤더만 수용, 중복 생성 방지 storage 없음 → replay 시 worklog 중복 가능 (planned gap).
- **owner raw 노출 금지 (privacy)**: owner 는 raw principal id 이므로 응답 payload 에 노출되면 실패 — `create_response_does_not_expose_owner` / patch 응답 `$.data.owner` 부재로 강제. pseudonymized 형태 준비 시(privacy branch) 재노출 가능.
- **다른 계약 의존** (owner branch + 소비 대상):
- [[raw/branch-notes/feature-api-contract-baseline]] `D15` — version/ETag/If-Match/412 optimistic-lock 메커니즘. 바뀌면 conflict scenario 전부 영향.
- [[raw/branch-notes/feature-rate-limit-idempotency-contract]] — `Idempotency-Key` 헤더 + dedup scope. idempotent replay scenario 의존.
- [[raw/branch-notes/feature-repository-access-permission-contract]] — `EXTERNAL_OUTBOUND_ALLOWED` capability. outbound forbidden scenario 의존.
- [[raw/branch-notes/feature-domain-modeling-guardrails]] `D3/D6` — title invariant. validation scenario 의존.
- [[raw/branch-notes/feature-resource-identifier-contract]] `D4/D5` — WorkLogId factory (ULID). id 정규화 scenario 의존.
- [[raw/branch-notes/feature-boundary-validation-mapping-contract]] — request/response mapper + unknown-field/mapping-failed 계약. **본 fixture 의 WorkLog 도메인이 이 branch sub-project B(2026-05-29)에서 실제 구현됨.**
- [[raw/branch-notes/feature-sample-removal-adoption-contract]] — `APP_SAMPLE_ENABLED` + dual-mode CI + sample-off smoke. sample-disabled/sample-off scenario (D3/D6) 위임처.
- [[raw/branch-notes/feature-data-retention-privacy-contract]] `D2/D7` — principal pseudonymization (HMAC-SHA-256 + rotating salt). owner 는 현재 raw 저장 + 응답 비노출이며, pseudonymized 표현은 이 branch 소유 → 준비 시 consume.
- [[raw/branch-notes/feature-authentication-authorization-contract]] `D2` — permission 기반 RBAC. owner-scoped authz(`worklog.owner==principal`)는 이 branch 가 YAGNI 로 보류한 ABAC 확장점 — fixture 는 permission 기반만 소비(owner 로 authz 안 함).
## Audit & Findings (2026-06-10 ca-tmpl ground-truth 대조)
> `/branch-spec` 가 ca-tmpl `src/sample-portfolio` + registry + 거버닝 canonical 과 대조해 발견한 drift. **사용자/canonical 결정 영역이므로 자동 rewrite 하지 않고 정합 권고만 남긴다** (CLAUDE.md §11, branch-spec §2).
- **MODEL_DRIFT (3-layer — 가장 중요)** — 동일 fixture 가 세 층에서 다른 모델:
- 거버닝 canonical [[wiki/projects/ca-tmpl/sample-fixture-and-adoption]]: `sample-ticket` · `TicketStatus`/`TicketOwner` · state machine `OPEN→IN_PROGRESS→CLOSED`.
- 본 노트 (2026-05-22, §Minimum Model): `sample-portfolio` · `WorkLogStatus`(OPEN/IN_PROGRESS/CLOSED)/`WorkLogOwner`.
- **실제 코드 (2026-06-10)**: `sample-portfolio` · `WorkLog{title:String, category:WorkCategory(INFRASTRUCTURE/DATABASE/BACKEND/PLATFORM), summary, content, techStack, links, period:Period, version:Long}`**status state machine 없음, owner 필드 없음, title 은 VO 아닌 String invariant** (`domain/worklog/WorkLog.java` L2050).
- 권고: §Minimum Model 과 canonical 6-field(Ticket*) 을 실제 WorkLog 모델로 정합 갱신. "update invalid transition" scenario 는 state machine 부재로 `rename`/`recategorize` + 412 conflict 로 재정의.
- 2026-06-10 구현 갱신: 실제 코드에 `WorkLogStatus(OPEN, IN_PROGRESS, CLOSED)``WorkLogOwner`가 추가되어 본 노트의 minimum model/state-machine drift 중 sample-portfolio 코드 gap은 해소됨. **owner 후속 갱신**: owner 가 create 시점 인증 principal subject 로 capture(상수 `sample-owner` 아님), pseudonymization·owner-scoped authz 는 sibling 위임, 응답 payload 비노출 — §Minimum Model owner 목적도 이에 맞춰 갱신함(§진행 중 메모 2026-06-10 owner=principal capture 참조). canonical `sample-ticket` 명명 drift는 별도 `/ingest`/canonical 갱신 영역으로 남음.
- **NAME_DRIFT**: canonical 은 `sample-ticket`, 노트/코드는 `sample-portfolio`. canonical(status=draft) `/ingest` 재실행 시 정합 권고.
- **TEST_NAMING_DRIFT**: 노트 추정 `Sample{Scenario}ContractTest.java` ≠ 코드 실제 `WorkLogControllerWireTest`/`WorkLogAuthorizationContractTest`/`GetRepoStatsUseCaseTest` (§구현 가이드 1 에 실제 규약 반영).
- **ERROR_CODE_DRIFT**: 노트의 `RESOURCE_NOT_FOUND`/`RESOURCE_CONFLICT` ≠ 코드 `PortfolioErrorCode.WORKLOG_NOT_FOUND(Category.NOT_FOUND,404,false)` (sample 전용 enum, registry row 아님). conflict 는 별도 코드가 아니라 412/If-Match 경로.
- **SCENARIO_EXPANSION (Should-fix)**: 실제 fixture 가 노트 12-scenario 초과 — ETag/304, HEAD, batch atomic, sort syntax, ULID 정규화 추가 검증 (§구현 가이드 1 하단).
- **STATUS_DRIFT**: 노트 §Claims To Verify / §Sample-portfolio Matrix 가 전부 `planned` 이나 대다수 이미 `actually-implemented` (§구현 가이드 1 등급). 머지/`/ingest` 전 등급 재판정 필요.
- **무근거 미수정**: 위는 전부 정합 *권고*. 실제 갱신은 사용자가 모델 SSOT(노트 vs canonical) 방향을 확정한 뒤 — `/branch-spec` 는 drift surface 만 수행.
## 검증해야 할 주장
> 외부 sample 들과의 비교는 대조 근거이지 ca-tmpl sample-portfolio 의 동작 보장이 아님. 실제 구현 후 검증 대상.
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| sample-portfolio 의 12 scenario 가 skeleton 의 모든 contract (envelope / error / capability / transaction / idempotency / boundary / lock) 를 누락 없이 트리거 | 12 scenario 망라성은 본 branch 가 정의 — 외부 표준이 권장 scenario 목록을 제공하지 않음 | 각 scenario 별 contract test 작성 → contract 단위 (envelope / error / etc) coverage matrix 작성 → 누락 항목 발견 시 scenario 추가 | `planned` |
| sample controller 가 request DTO 를 application 으로 직접 넘기지 않는다 | architecture rule 위반은 ArchUnit 등으로만 자동 차단 가능 | ArchUnit rule + contract test (`Sample{ScenarioName}ContractTest.java`) 작성 → CI 에서 실행 | `planned` |
| sample domain object 가 response 로 직접 노출되지 않는다 | 직렬화 mapper 가 누락되어도 컴파일은 통과 — 별도 검증 필요 | response mapper 강제 ArchUnit rule + integration test 에서 response payload 검사 | `planned` |
| sample write use case 가 repository capability 없이 write 하면 실패한다 | capability 선언이 없어도 코드는 동작 가능 — gate 명시 필요 | `EXTERNAL_OUTBOUND_ALLOWED` 등 capability annotation + ArchUnit rule + contract test | `planned` |
| optimistic lock / conflict / not found 가 structured error 로 분류된다 | raw JPA exception 전파 위험 — error mapping layer 가 없으면 누락 가능 | error registry 의 `RESOURCE_NOT_FOUND` / `RESOURCE_CONFLICT` 등 매핑 + contract test | `planned` |
| sample-off 상태에서 core app smoke test 와 core contract test 가 유지된다 | sample-on / sample-off dual-mode 가 본 branch + `feature-sample-removal-adoption-contract` 가 정의 | CI matrix.profile = [sample-on, sample-off] 양쪽 green | `planned` |
| 다른 branch 가 sample-portfolio scenario / minimum model 을 독자 변경하지 않는다 | SSOT 정책 (D5) 의 사회적 강제 — 자동 차단 메커니즘 없음 | wikilink 기반 cross-reference + branch note review 시 차이 확인. 정적 분석은 어려움 | `planned` |
| PetClinic / RealWorld / Microservices sample 과 ca-tmpl sample-portfolio 의 비교 매트릭스가 wiki/concepts 에 추출 가능 | 비교는 본 branch 외부 근거 / 대안 조사 섹션에 있으나 wiki 변환 시 PetClinic disclaimer (`SAMPLE-PC-C4`) 의 강한 부정 표현이 외부 산출물에 보존되어야 함 | `/ingest` 시 PetClinic 의 "not a best practice" 인용 verbatim 유지 + RealWorld 의 `engineering-blog` 강도 표시 유지 | `planned` |
| sample disabled startup 시 prod profile 에서 sample endpoint 가 노출되지 않는다 | profile isolation 은 Spring 의 `@Profile` 만으로는 실수 가능 — 별도 contract test 필요 | sample-off profile 로 startup → endpoint 목록에 `sample.worklog` 부재 검사 | `planned` |
| idempotency key 메커니즘이 retry 시 worklog 중복 생성을 막는다 | idempotency storage 가 없거나 잘못 구현되면 중복 발생 — 외부 표준 부재 (자체 정책) | replay 시뮬레이션 contract test (`SampleIdempotentReplayContractTest`) | `planned` |
## Sample-portfolio Matrix
| scenario | verifies | failure condition |
| --- | --- | --- |
| create worklog success | request mapper, command validation, write capability, transaction, response mapper | controller가 domain/entity를 직접 생성하거나 반환 |
| create worklog validation failure | structured validation details, client-safe message | malformed request가 raw exception 또는 500으로 노출 |
| idempotent create replay | idempotency storage, duplicate write 방지, replay meta | retry 시 worklog 중복 생성 |
| get worklog not found | `RESOURCE_NOT_FOUND`, 404, retryable false | not found가 500으로 변환 |
| list worklogs pagination | pagination meta, sorting/filtering | pagination 정보가 data payload에 섞임 |
| update worklog invalid transition | domain invariant, conflict mapping | `CLOSED` worklog update가 성공 |
| optimistic lock conflict | persistence failure mapping | raw JPA exception이 presentation까지 전파 |
| unauthorized update | auth/authz separation, privacy log | token/principal raw value가 log에 남음 |
| outbound forbidden use case | `EXTERNAL_OUTBOUND_ALLOWED` capability | capability 없이 외부 adapter 호출 |
| sample disabled startup | prod profile isolation | prod profile에서 sample endpoint 노출 |
| sample-off smoke | sample/core decoupling | sample-off 상태에서 core contract test 실패 |
## Minimum Model
| model | required fields | purpose |
| --- | --- | --- |
| `WorkLogId` | opaque id | path variable mapping, value object |
| `WorkLogTitle` | normalized non-empty string | syntax validation + domain invariant |
| `WorkLogStatus` | `OPEN`, `IN_PROGRESS`, `CLOSED` | enum serialization + conflict |
| `WorkLogVersion` | numeric version | optimistic locking |
| `WorkLogOwner` | create 시점 인증 principal subject (raw IdP `sub`) | 신원 capture (`actually-implemented`, 2026-06-10). pseudonymization·owner-scoped authz 는 sibling SSOT 위임(미적용), 응답 payload 비노출(privacy) |
| `IdempotencyKey` | opaque key | duplicate write prevention |
## 테스트 계약
- sample controller가 request DTO를 application으로 직접 넘기면 실패.
- sample domain object가 response로 직접 노출되면 실패.
- sample write use case가 repository capability 없이 write하면 실패.
- sample optimistic lock/conflict/not found가 structured error로 분류되어야 함.
- sample 제거 후 core app smoke test와 core contract test가 유지되어야 함.
- 다른 branch가 sample-portfolio scenario/minimum model을 독자 변경하면 실패.
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
> `/coverage` 가 채우는 **생성물** (coverage-auditor, 2026-06-10). governing_docs = [[wiki/projects/ca-tmpl/sample-fixture-and-adoption]]. 기준: `rules/coverage-gate.md`.
> 상태: `covered-here`(이 브랜치 결정) / `delegated`(다른 owner 브랜치) / `missing`(아무도 안 맡음 → Blocking). **Verdict: Covered (Blocking 0).**
| 관심사 (governing doc) | 상태 | owner | 심각도 | 근거 |
|--------|------|-------|--------|------|
| C1: 12 scenario matrix | covered-here | — | — | D4 + §Sample-portfolio Matrix |
| C2: 6-field minimum model | covered-here | — | — | §Minimum Model; 필드명 drift 는 §Audit `MODEL_DRIFT` |
| C3: state machine OPEN→IN_PROGRESS→CLOSED | covered-here | — | Advisory | D4; 코드 gap 은 §Audit `MODEL_DRIFT` (depth 영역) |
| C4: optimistic lock | covered-here | — | — | D4 + §구현 가이드 1 (`WorkLog.version` L37, WireTest L152) |
| C5a: idempotency key 헤더 수용 | covered-here | — | — | D4 + `WorkLogController.java` L156/199 |
| C5b: idempotency dedup/storage | delegated | [[raw/branch-notes/feature-rate-limit-idempotency-contract]] | OK | §구현 가이드 1 `OUT_OF_BRANCH_SCOPE` + §엣지 위임 링크 |
| C6: sample-off / profile isolation | delegated | [[raw/branch-notes/feature-sample-removal-adoption-contract]] | OK | D3 + §구현 가이드 2 `OUT_OF_BRANCH_SCOPE` + env-keys.yaml L1269 |
| C7: dual-mode CI matrix (sample-on/off release-blocking) | delegated | [[raw/branch-notes/feature-sample-removal-adoption-contract]] | OK | D6 + §구현 가이드 1 `OUT_OF_BRANCH_SCOPE` |
| C8: multi-module adoption checklist | delegated | [[raw/branch-notes/feature-domain-feature-onboarding-contract]] (via [[raw/branch-notes/feature-sample-removal-adoption-contract]]) | Advisory | governing doc L54; sibling D5 consume 구조 |
## 마주친 문제
- 2026-06-10 Gradle wrapper lock sandbox 권한 문제.
- 원인: Codex sandbox 기본 권한에서 `~/.gradle/wrapper/dists/...zip.lck` 쓰기가 read-only로 차단.
- 시도: 동일 Gradle 명령을 승인 실행으로 재시도.
- 해결: 승인 실행 후 RED/GREEN 검증 및 full `check` 통과.
- 별도 에러 노트로 분리됨: [[raw/errors/gradle-wrapper-lock-read-only-sandbox-2026-06-10]]
## 묶음
<!-- GENERATED: sources:start -->
- [[raw/official-docs/sample-microservices-spring-cloud-github]]
- [[raw/official-docs/sample-realworld-gothinkster-github]]
- [[raw/official-docs/sample-spring-petclinic-github]]
<!-- GENERATED: sources:end -->
<!-- GENERATED: interviews:start -->
- [[raw/interviews/sample-domain-contract-fixture-clean-architecture]]
<!-- GENERATED: interviews:end -->
<!-- GENERATED: errors:start -->
- [[raw/errors/gradle-wrapper-lock-read-only-sandbox-2026-06-10]]
<!-- GENERATED: errors:end -->
<!-- GENERATED: blog-topics:start -->
- [[raw/blog-topics/sample-domain-contract-fixture-clean-architecture-2026-06-10]]
<!-- GENERATED: blog-topics:end -->
> 본 feature branch 는 leaf — 자식 자료 없음. Phase C2 실 코드 작성 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.
### 오류 기록 (본 feature 작업 중 발생)
- [[raw/errors/gradle-wrapper-lock-read-only-sandbox-2026-06-10]] — Codex sandbox에서 Gradle wrapper가 `~/.gradle` lock 파일을 쓰지 못해 테스트 명령을 승인 실행으로 재시도.
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- [[raw/interviews/sample-domain-contract-fixture-clean-architecture]] — 샘플 도메인을 production 기능이 아니라 계약 fixture로 두는 이유와 계층 경계 설명.
### job-posting tie-ins (이 작업에서 파생된 글감)
- [[raw/blog-topics/sample-domain-contract-fixture-clean-architecture-2026-06-10]] — Clean Architecture 템플릿에서 sample domain fixture로 계약을 검증하는 구현 글감.
## 관련 일일 노트
> 이 브랜치를 작업한 날짜. 양방향 nav 유지.
- (아직 연결된 일일 노트 없음 — Phase C2 실 작업일 기록 시 `[[raw/daily-notes/YYYY-MM-DD]]` 추가)
## 완료 후 정리
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
- `actually-implemented` 항목:
- `locally-verified` 항목:
- `prod-verified` 항목:
- **추출하지 않을 항목** (planned / documented-only / abandoned):