--- 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 기준을 정의합니다. ## 부모 (필수) - **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]] > ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다. ## 브랜치 계약 패킷 - **생성 시 프로젝트 개정**: `1` - **패킷 스키마**: `contract_packet: 1` - **완료 조건**: sample domain fixture가 module contract를 검증하고 제거 smoke가 통과한다 ### 상속한 프로젝트 결정 | 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]] | ### 브랜치 지역 결정 > 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다. | Decision ID | Decision | Relation | Supporting Claims | Status | |---|---|---|---|---| ### 선언한 예외 | Override ID | Overrides | Reason | Approval | Status | |---|---|---|---|---| ## 목표 도메인/비즈니스 로직은 제거하지만, 샘플 도메인이 없으면 경계 validation, mapper, repository capability, transaction, response contract를 실제 흐름으로 검증할 수 없습니다. sample은 기능이 아니라 contract fixture입니다. - 이슈: - PR: ## 범위 ### 포함 범위 - `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` L576–581) | `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` L20–50). - 권고: §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]] ## 묶음 - [[raw/official-docs/sample-microservices-spring-cloud-github]] - [[raw/official-docs/sample-realworld-gothinkster-github]] - [[raw/official-docs/sample-spring-petclinic-github]] - [[raw/interviews/sample-domain-contract-fixture-clean-architecture]] - [[raw/errors/gradle-wrapper-lock-read-only-sandbox-2026-06-10]] - [[raw/blog-topics/sample-domain-contract-fixture-clean-architecture-2026-06-10]] > 본 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):