init: llm-wiki-haness 하네스 설계
This commit is contained in:
+289
@@ -0,0 +1,289 @@
|
||||
---
|
||||
title: branch / feature-accessibility-baseline-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
branch: feature-accessibility-baseline-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract]
|
||||
tags: [branch, ca-skeleton, frontend, testing, react, static-analysis]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-019
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-019
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-STYLING-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-TEST-STACK-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-013]
|
||||
contract_packet: 1
|
||||
contract_packet_sha256: c7f5f84ade7d63ed9663a6988f27472d1bef629d546049c86ab104ba2314fcac
|
||||
imports: [FE-GATE-006@1, FE-OC-001@1, FE-OC-011@1, FE-OC-020@1, FE-OC-021@1, FE-OC-026@1]
|
||||
delegates: [DELEG-FE-004@1]
|
||||
|
||||
---
|
||||
|
||||
# branch: feature-accessibility-baseline-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — TODO·결정·진행 기록. 구현 결과는 검증 뒤 `/ingest`로만 추출한다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
[[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
형제 branch (같은 project 의 다른 자식, 본 branch 가 의존/기여):
|
||||
|
||||
- [[raw/branch-notes/feature-async-ui-state-contract]] — async surface state 모델 owner (`FE-OC-011`). 본 branch 가 그 state 위에 a11y semantics 를 얹음(그 branch 가 명시적으로 위임).
|
||||
- [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] — gate/fixture/artifact 분리 owner (`FE-OC-020`). a11y gate 는 그 taxonomy 의 한 gate.
|
||||
- [[raw/branch-notes/feature-sample-feature-slice-contract-fixture]] — sample route owner (`FE-OC-024`). a11y 증거를 측정할 대상 route 제공.
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: sample route에서 axe·keyboard·focus evidence가 남는다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-STYLING-001@1` | styling default는 Tailwind theme token과 component primitive다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-TEST-STACK-001@1` | test stack default는 Vitest, RTL, MSW, Playwright, axe다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-frontend-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 -->
|
||||
## 목표
|
||||
|
||||
- 이 branch 는 hub §10.3 Accessibility baseline 의 planned 요구를 *되묻지 않고 구현·검증 가능한* 계약으로 내린다. 스스로 Primary `FE-OC-*` 를 소유하지 않고 (§20 branch 분해표: Primary `—`), `FE-OC-019`·`FE-OC-020`·`FE-OC-021`·`FE-OC-024` 에 **기여**한다: (a) `FE-OC-020` 의 gate/fixture/artifact 분리에 a11y gate(`FE-GATE-009`) 와 그 fixture·artifact 를 공급, (b) `FE-OC-021` 의 context 동반 측정 NFR 에 `FE-NFR-009`(axe critical/serious 0) 를 공급, (c) `FE-OC-024` sample route 를 a11y 증거의 측정 대상으로 사용, (d) `FE-OC-019` browser 안전 경계(untrusted HTML 금지) 위에서만 접근 가능한 콘텐츠를 렌더한다는 전제를 명문화.
|
||||
- 완료의 measurable 정의(§20): **axe + keyboard/focus manual evidence for sample routes**. automated(axe) 와 manual(keyboard/focus/screen-reader) 두 증거를 모두 요구한다.
|
||||
- 이슈: 없음 (스캐폴딩 단계)
|
||||
- PR: 없음
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- hub §10.3 accessibility baseline 요구의 계약화: keyboard 도달성, visible focus, route 변경 후 deterministic focus target, loading state 의 live region + 반복 announcement 억제, error 의 programmatic association, color 단독 금지, modal focus trap/restore, reduced-motion 존중.
|
||||
- automated axe gate 설정: severity threshold(critical/serious = 0), 측정 대상(sample route), artifact(`pnpm test:a11y` → `artifacts/tests/a11y.json`), 컴포넌트 수준 a11y fixture(`pnpm test:component` 의 a11y fixtures → `artifacts/tests/component.xml`).
|
||||
- manual keyboard/focus/screen-reader 체크리스트 + 증거 형식(`FE-GATE-009` 의 "signed manual review").
|
||||
- hub §9.1 async surface state(§9.1 표)의 **a11y 표현 semantics**(live-region/focus attribute) — state 모델 자체가 아니라 그 위의 a11y hook.
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외. 다른 owner branch 소유이므로 여기서 detail 을 정하지 않고 그 branch 를 가리킨다(CLAUDE.md §15.5 R3).
|
||||
|
||||
- async surface 에 *어떤 state 가 존재하고 언제 전이하는가* → [[raw/branch-notes/feature-async-ui-state-contract]] (`FE-OC-011`) 소유. 본 branch 는 state 목록을 consume 만 한다.
|
||||
- CI gate orchestration / gate·fixture·artifact 분리 프레임워크 → [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] (`FE-OC-020`) 소유. 본 branch 는 a11y gate 의 내용물만 공급.
|
||||
- untrusted HTML injection 금지·sanitization·CSP → [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] (`FE-OC-019`) 소유. a11y 는 "정제된 콘텐츠" 전제만 소비.
|
||||
- color contrast token 값 / design token → [[raw/branch-notes/feature-tailwind-design-token-styling-contract]] (`FE-OC-021` 기여) 소유. 본 branch 는 "color 를 state 의 유일 신호로 쓰지 않는다" 규칙만.
|
||||
- render/error boundary 배치 → `feature-frontend-render-recovery-boundary-contract` (`FE-OC-015`) 소유.
|
||||
- Web Vitals/performance NFR 측정 machinery → `feature-web-vitals-performance-budget-contract` (`FE-OC-021`) 소유. axe NFR 은 a11y 가, 측정 컨텍스트 규약은 그 branch 가.
|
||||
- 제품별 실제 화면 구현과 실제 audit 결과의 verified 승격.
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/official-docs/react-ui-library-official]] | D3·D4 — a11y attribute 가 부착되는 React 컴포넌트 구조의 source. **a11y 규칙 자체의 근거는 아님**(a11y 규칙은 hub §10.3). |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §10.3 | D1~D6 — accessibility baseline planned 요구(keyboard/focus/live-region/programmatic association/color/focus trap/reduced-motion/axe threshold)의 primary 근거 + "automated axe ≠ manual review" + "WCAG 미주장" 경계. |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §9.1 | D3 — async surface required/non-blocking state 모델(a11y hook 을 부착할 대상). |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §14.2·§14.3·§15 | D1·D2·D4 — `FE-NFR-009`(axe critical/serious 0, sample routes), `pnpm test:a11y`→`artifacts/tests/a11y.json`, `FE-GATE-009`(axe + signed manual review). |
|
||||
| axe-core `doc/API.md` (external research, 2026-07-19) — https://github.com/dequelabs/axe-core/blob/develop/doc/API.md | D1 — impact severity taxonomy. verbatim: *"How serious the violation is. Can be one of 'minor', 'moderate', 'serious', or 'critical'."* 또한 verbatim: *"Axe does not test hidden regions, such as inactive menus or modal windows."* ⚠️ 아직 `raw/official-docs/` 미아카이브 → follow-up: `wiki-source-summarizer` 로 `raw/official-docs/axe-core-official.md` 아카이브 권고. |
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] automated axe gate 설정(severity threshold critical/serious=0 + sample-route scope + `a11y.json` artifact) 명세 — 등급: `planned`
|
||||
- [ ] manual keyboard/focus/screen-reader 체크리스트 + signed evidence 형식 설계 — 등급: `planned`
|
||||
- [ ] hub §9.1 async state 별 live-region/focus a11y semantics 표 작성 — 등급: `planned`
|
||||
- [ ] reduced-motion + color-signal 규칙 명세 — 등급: `planned`
|
||||
- [ ] evidence-grade boundary(WCAG 미주장, planned 유지) 문서화 — 등급: `planned`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
- `/branch-spec` 로 hub §10.3/§9.1/§14/§15 + axe-core `doc/API.md`(1건 bounded research) 근거로 채움. frontend 코드는 아직 존재하지 않으므로 모든 항목 `planned`.
|
||||
- axe severity(critical/serious/moderate/minor) 정의는 axe-core 문서로 grounding. axe 는 hidden region(inactive menu/modal)을 검사하지 않는다는 점이 manual review 필수성의 기술적 근거 하나.
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> 아래 Decision Evidence Map 의 prose mirror. 근거는 hub §10.3/§9.1/§14/§15 + axe-core `doc/API.md`.
|
||||
|
||||
- **D1**: automated a11y gate 는 axe 를 사용하고 impact `critical`·`serious` violation 0 을 sample route 에서 blocking default 로 한다(`moderate`/`minor` 는 report-only backlog). / 이유: hub §10.3 이 axe critical/serious 0 을 blocking 으로 규정하고 `FE-NFR-009` 가 이를 NFR 로 고정 / 검토한 대안: 전면 manual audit(느리고 결정론 재현 불가) / 근거: hub §10.3·§14.2 + [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D022 + axe-core `doc/API.md`.
|
||||
- **D2**: automated axe 통과는 완료 판정을 단독으로 만들 수 없다 — axe(automated) + keyboard/focus/screen-reader(manual) 두 증거를 모두 요구한다. / 이유: hub §10.3 "automated axe 통과는 manual review 를 대체하지 않는다" + axe 가 hidden region 을 검사하지 않음 / 검토한 대안: automated-only(위양성 안심) — 거부 / 근거: hub §10.3·§20.
|
||||
- **D3**: async surface(§9.1)의 각 visible state 에 a11y 표현 semantics 를 부착한다(initial-loading = skeleton, focus theft 금지 / refreshing = subtle live region, 반복 announcement 억제 / terminal-error = programmatic 연결 + action focus). state 모델 자체는 async-ui branch 소유이고 본 branch 는 그 hook 만 소유. / 이유: hub §9.1 state 표 + §10.3 live-region/association 요구 + async-ui branch 의 명시적 위임 / 근거: hub §9.1·§10.3 + [[raw/branch-notes/feature-async-ui-state-contract]] (`FE-OC-011`).
|
||||
- **D4**: keyboard/focus baseline — 모든 interactive action 이 keyboard 로 도달, visible focus indicator, route 변경 후 deterministic focus target, modal focus trap + restore. / 이유: hub §10.3 planned 요구 + `FE-GATE-006` 컴포넌트 gate 의 keyboard 축 / 근거: hub §10.3·§15.
|
||||
- **D5**: axe 로 잡히지 않는 신호 — prefers-reduced-motion 존중 + color 를 state 의 유일 신호로 쓰지 않음(icon/text 병행). color token 값 자체는 tailwind branch 위임. / 이유: hub §10.3 / 근거: hub §10.3.
|
||||
- **D6**: evidence-grade boundary — repo 실행 증거 없이는 WCAG 적합을 주장하지 않고 모든 a11y 주장을 `planned` 로 유지하며, 외부 답변에서 목표 수치를 측정 결과처럼 말하지 않는다(`FE-OC-001`·`FE-OC-026`·§16 answer boundary). / 이유: hub §10.3 "WCAG 적합성은 실제 audit 없이 주장 금지" / 근거: hub §10.3·§2.1.
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
> 각 결정의 근거 claim 과 선택 조건. `Decision ID` 는 이 note 안에서 안정.
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | axe automated gate: impact `critical`·`serious` violation 0 을 sample route 에서 blocking default, `moderate`/`minor` 는 report-only backlog (`FE-OC-020`·`FE-OC-021` 기여, `FE-NFR-009`) | sample route 가 존재하는 한 axe blocking default / organization test platform 이 axe 를 대체하거나 더 엄격한 threshold 를 강제하면 재검토(test stack revisit trigger) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §10.3 (axe critical/serious 0 blocking)·§14.2 `FE-NFR-009`·FE-D022 (test stack incl. axe); axe-core `doc/API.md` impact taxonomy("minor/moderate/serious/critical") | `project-decision` + `conditional-default (test stack)` + `official-doc (axe severity)` | axe automated 는 a11y 이슈의 일부만 포착(→ D2 manual 필수). `moderate`/`minor` backlog 처리 정책과 rule-set 튜닝 미확정 |
|
||||
| D2 | 완료 판정 = axe(automated) **AND** keyboard/focus/screen-reader(manual) 이중 증거. automated pass 단독으로 완료 주장 금지 (`FE-OC-020` 기여) | 모든 a11y 완료 판정에서 불변 — 대안 없음(hub §10.3 문장 + axe 가 hidden region 미검사) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §10.3 ("automated axe 통과는 manual keyboard/screen-reader review 를 대체하지 않는다")·§20 measurable completion("axe + keyboard/focus manual evidence"); axe-core `doc/API.md`("Axe does not test hidden regions") | `project-decision` + `official-doc (axe scope 한계)` | manual review 는 사람 판단 → `FE-GATE-009` 의 "signed manual review" artifact 형식/서명 메커니즘 미확정 |
|
||||
| D3 | async surface(§9.1) state 별 a11y 표현: initial-loading=skeleton·focus theft 금지 / refreshing=subtle live region·반복 announcement 억제 / stale-degraded=stale 안내·manual retry 도달 / terminal-error=programmatic 연결·action focus / mutation-pending=aria-busy·중복 차단 (`FE-OC-011` consume) | async surface(원격 데이터 view)가 존재하는 한 적용 / 순수 정적 view(원격 데이터 없음)엔 async a11y hook 불필요. state 목록/전이가 바뀌면 async-ui owner 를 따라 재정렬 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §9.1 (visible state 표)·§10.3 (loading live region + 반복 announcement 억제, error programmatic association); [[raw/branch-notes/feature-async-ui-state-contract]] `FE-OC-011` 위임(async 는 a11y hook point 만 노출) | `project-decision` (cross-branch delegation) | aria-live politeness(polite vs assertive) 와 announcement debounce 메커니즘은 hub 미규정 → §구현 가이드 UNSUPPORTED_IMPL |
|
||||
| D4 | keyboard/focus baseline: 모든 interactive action keyboard 도달 + visible focus + route 변경 후 deterministic focus target + modal focus trap/restore (`FE-OC-020` 기여, `FE-GATE-006` keyboard 축) | 모든 interactive/route surface 에 적용 / 대안 없음(§10.3 planned 요구) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §10.3 (keyboard/visible focus/deterministic route focus/modal trap·restore)·§15 `FE-GATE-006` | `project-decision` | manual keyboard walk-through 는 automated 로 완전 대체 불가. route 변경 시 focus target 선택 규칙(main landmark vs heading)은 §10.3 미규정 → UNSUPPORTED_IMPL |
|
||||
| D5 | prefers-reduced-motion 존중 + color 단독 state 신호 금지(icon/text 병행). color contrast token 값은 tailwind branch 위임 | 항상 적용 / 대안 없음(§10.3) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §10.3 (reduced-motion 존중, color 만으로 state 구분 금지) | `project-decision` | reduced-motion 적용 범위(어떤 animation/transition)는 컴포넌트별. color contrast 값은 [[raw/branch-notes/feature-tailwind-design-token-styling-contract]] (`FE-OC-021` 기여) 소유 — 위임 |
|
||||
| D6 | evidence-grade boundary: repo 증거 없이 WCAG 적합 미주장, a11y 주장 `planned` 유지, 외부 답변에서 목표를 측정치처럼 표현 금지 (`FE-OC-001`·`FE-OC-026`) | repo evidence 없는 한 불변 / 실제 audit 후에만 conformance 주장 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §10.3 ("WCAG 적합성은 실제 audit 없이 주장하지 않는다")·§2.1 `FE-OC-001`("repo evidence 없이 완료 주장 MUST NOT")·`FE-OC-026` | `project-decision` (evidence invariant) | N/A (usage boundary). 다만 §16 answer boundary 를 파생 산출물에서 준수해야 함 |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> `planned` blueprint. frontend 코드가 없으므로 경로/명령은 hub §14/§15 가 고정한 planned anchor 다. 3-rule(R1 Trace / R2 UNSUPPORTED_IMPL_DECISION / R3 OUT_OF_BRANCH_SCOPE) 준수.
|
||||
|
||||
### 1. Automated axe gate — severity threshold · scope · artifact
|
||||
|
||||
> **Trace**: D1 + `FE-OC-020`·`FE-OC-021` (기여) + [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §10.3·§14.2 `FE-NFR-009`·§14.3 `pnpm test:a11y`·FE-D022.
|
||||
>
|
||||
> - **`a11y.json` 스키마는 해소됨(2026-07-21)**: hub §2.1.3 `ART-FE-004@1` 로 등록됐고 **Schema Owner 는 본 branch** 다(`harness/source/artifact-schemas/ca-skeleton-frontend/a11y-report.schema.json`). impact 어휘는 axe-core 4단계를 그대로 쓰고, `blockingCount`(serious·critical)가 0 이 아니면 `FE-GATE-009` FAIL 이다.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: axe integration 메커니즘(@axe-core/playwright 로 route-level e2e-a11y + vitest-axe 로 component-level) 과 rule-set config 는 hub 가 규정하지 않음. Trade-off: FE-D022 의 Playwright+RTL 스택과 정합을 위해 위 조합을 제안하되, 최종 runner binding 은 test-taxonomy owner 확정에 위임.
|
||||
|
||||
| 항목 | planned 값 | 근거 |
|
||||
|---|---|---|
|
||||
| 대상 scope | sample route (제품 route 아님) | §14.2 `FE-NFR-009` context = sample routes |
|
||||
| blocking severity | impact ∈ {`critical`, `serious`} → fail | §10.3 + axe-core impact taxonomy |
|
||||
| non-blocking severity | impact ∈ {`moderate`, `minor`} → report-only backlog | axe-core impact taxonomy(4단계) |
|
||||
| route-level 실행 | `pnpm test:a11y` → `artifacts/tests/a11y.json` | §14.3 planned command 표 |
|
||||
| component-level 실행 | `pnpm test:component` 의 a11y fixtures → `artifacts/tests/component.xml` | §14.3(component = async/error/**a11y** fixtures) |
|
||||
|
||||
> **R3 위임**: a11y gate 를 CI 파이프라인에 blocking gate 로 배선하는 orchestration 은 [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] (`FE-OC-020`) 소유. 본 절은 gate 의 *내용물*(scope/severity/artifact)만 확정.
|
||||
|
||||
### 2. Manual keyboard / focus / screen-reader checklist + evidence format
|
||||
|
||||
> **Trace**: D2 + D4 + [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §10.3·§15 `FE-GATE-009`("signed manual review").
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: manual review record 의 경로/포맷(예: `artifacts/tests/a11y-manual/<route>.md`)과 "signed" 메커니즘(리뷰어 서명 방식)은 hub 가 "signed manual review" 라고만 하고 스키마를 규정하지 않음. Trade-off: sample route 당 markdown record 를 `a11y.json` 옆에 co-locate 제안, 최종 경로는 test-taxonomy owner 확정에 위임.
|
||||
|
||||
체크리스트 항목(§10.3 요구와 1:1):
|
||||
|
||||
| # | 수동 검증 항목 | 통과 기준 |
|
||||
|---|---|---|
|
||||
| M1 | keyboard 로 모든 interactive action 도달 | 마우스 없이 전 action 실행 가능 |
|
||||
| M2 | visible focus indicator | 모든 focusable 요소에 시각적 focus 표시 |
|
||||
| M3 | route 변경 후 deterministic focus target | route 전환 시 focus 가 정해진 지점으로 이동 |
|
||||
| M4 | modal focus trap + restore | modal 내부 trap, 닫으면 트리거로 focus 복귀 |
|
||||
| M5 | error 의 programmatic association | error 메시지가 관련 control 과 aria 로 연결 |
|
||||
| M6 | color 단독 금지 | state 가 색 외 신호(icon/text)도 가짐 |
|
||||
| M7 | reduced-motion 존중 | prefers-reduced-motion 시 애니메이션 축소 |
|
||||
|
||||
### 3. Async surface a11y semantics (live-region + focus for §9.1 states)
|
||||
|
||||
> **Trace**: D3 + `FE-OC-011` (async-ui branch 에서 consume) + [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §9.1·§10.3.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: aria-live politeness(polite/assertive) 와 반복 announcement 억제(debounce/dedupe) 메커니즘은 §10.3 이 "적절한 live region, 반복 announcement 억제" 원칙만 규정하고 구현 detail 미규정. Trade-off: 기본 `polite` + message-key dedupe, `terminal-error` 만 `assertive` 제안.
|
||||
|
||||
| §9.1 state | a11y 표현 요구 | 근거 |
|
||||
|---|---|---|
|
||||
| `initial-loading` | 안정적 skeleton, focus theft 금지 | §9.1·§10.3 |
|
||||
| `refreshing` | 기존 콘텐츠 유지 + subtle live region, 반복 announcement 억제 | §9.1·§10.3 |
|
||||
| `stale-degraded` | stale 안내 announce + manual retry 를 keyboard 로 도달 | §9.1·§10.3 |
|
||||
| `terminal-error` | 안전 메시지의 programmatic 연결 + registry action 에 focus | §9.1·§10.3 |
|
||||
| `mutation-pending` | `aria-busy`/disabled 로 중복 action 차단 announce | §9.1 |
|
||||
|
||||
> **R3 위임**: 위 state 가 *존재하는지·언제 전이하는지*는 [[raw/branch-notes/feature-async-ui-state-contract]] (`FE-OC-011`) 소유. 본 절은 그 state 위의 a11y attribute 만 명세(그 branch 가 "a11y hook point 만 노출"이라 위임함).
|
||||
|
||||
### 4. Reduced-motion + color-signal (axe 로 잡히지 않는 신호)
|
||||
|
||||
> **Trace**: D5 + [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §10.3.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: reduced-motion 을 적용할 animation 범위는 §10.3 이 원칙만 규정하고 열거하지 않음. Trade-off: loading skeleton + route transition 에 우선 적용, 컴포넌트별 애니메이션은 각 컴포넌트 owner 에 위임.
|
||||
|
||||
- `prefers-reduced-motion: reduce` 시 skeleton/route transition 애니메이션 축소 또는 제거.
|
||||
- state 는 색 외에 icon/text 신호를 병행(color 단독 금지).
|
||||
|
||||
> **R3 위임**: color contrast token 값(대비비 등)은 [[raw/branch-notes/feature-tailwind-design-token-styling-contract]] (`FE-OC-021` 기여) 소유. 본 절은 "color 를 유일 신호로 쓰지 않는다" 규칙만.
|
||||
|
||||
### 5. Evidence-grade boundary
|
||||
|
||||
> **Trace**: D6 + [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §10.3·§2.1 `FE-OC-001`·`FE-OC-026`.
|
||||
|
||||
- repo 가 axe + manual 을 실행해 artifact 를 낼 때까지 모든 a11y 주장은 `planned`. WCAG 적합(conformance) 문구를 쓰지 않는다.
|
||||
- 파생 산출물/외부 답변에서 목표 수치(axe 0, WCAG AA 등)를 측정 결과처럼 표현하지 않는다(§16 answer boundary). (본 절은 boundary 규칙이므로 별도 impl detail 없음.)
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- dynamic content 변경(route 전환·async state 전이) 시 focus/live-region 이 결정론적으로 발화하지 않으면 screen-reader 사용자가 맥락을 잃음 → §구현 가이드 3 의 live-region + M3 deterministic focus 로 방지.
|
||||
- modal 닫힘 시 focus restore 실패 → 트리거 복귀 검증(M4).
|
||||
- hidden region(inactive menu/modal)은 axe 가 검사하지 않음(axe-core `doc/API.md`) → 렌더/활성화 후 재실행하는 fixture 필요.
|
||||
- 잦은 refetch 시 live-region announcement storm → politeness/dedupe(§구현 가이드 3 UNSUPPORTED_IMPL).
|
||||
- reduced-motion 미존중 → vestibular 부담(M7).
|
||||
- **다른 계약 의존**:
|
||||
- [[raw/branch-notes/feature-async-ui-state-contract]] `FE-OC-011` 에 의존 — async surface state 목록/전이를 consume. 그 state 모델이 바뀌면 본 branch 의 a11y hook 이 재정렬됨.
|
||||
- [[raw/branch-notes/feature-sample-feature-slice-contract-fixture]] `FE-OC-024` 에 의존 — a11y 증거를 측정할 sample route 가 생기기 전엔 검증 불가.
|
||||
- [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] `FE-OC-020` 에 의존 — a11y gate 를 blocking gate 로 배선/artifact 보존하는 orchestration owner.
|
||||
- [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] `FE-OC-019` 에 의존 — untrusted HTML 금지 전제. a11y 는 정제된 콘텐츠만 렌더한다고 가정.
|
||||
- [[raw/branch-notes/feature-tailwind-design-token-styling-contract]] — color contrast token 값 소유(`FE-OC-021` 기여). color-not-sole 규칙만 본 branch.
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| sample route 에서 axe critical/serious violation 0 | 구현·gate 미존재 | `pnpm test:a11y` → `artifacts/tests/a11y.json` 결과가 critical/serious 0 | `needs-confirmation` |
|
||||
| 모든 interactive action 이 keyboard 로 도달 | 화면 미구현 | sample route manual keyboard walk-through + signed record(M1) | `needs-confirmation` |
|
||||
| route 변경 후 focus 가 deterministic target 으로 이동 | 라우팅 a11y 미구현 | component/e2e focus 이동 test(M3) | `needs-confirmation` |
|
||||
| async state 전이가 live-region 으로 announce 되되 storm 없음 | live-region 정책 미확정 | component a11y fixture(aria-live assertion + dedupe) `pnpm test:component` | `needs-confirmation` |
|
||||
| modal focus trap + restore 동작 | modal 미구현 | component test(trap 내부 + 닫힘 시 트리거 복귀, M4) | `needs-confirmation` |
|
||||
| prefers-reduced-motion 이 존중됨 | 애니메이션 미구현 | media-query 기반 manual/자동 test(M7) | `needs-confirmation` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
- 스캐폴딩 단계: `/coverage` 실행 전 수동 행을 만들지 않는다.
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
- 없음 — 스캐폴딩 단계.
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-GATE-006@1` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | component 레벨이 실패하면 merge 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-OC-001@1` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | 모든 구현 주장은 evidence grade를 MUST 표시하고 repo evidence가 없는 상태에서 구현 완료를 MUST NOT 주장 | import 참조로 적용 |
|
||||
| `FE-OC-011@1` | [[raw/branch-notes/feature-async-ui-state-contract]] | async surface는 initial-loading, success, empty, terminal-error를 MUST 표현 | import 참조로 적용 |
|
||||
| `FE-OC-020@1` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | gate 종류별 책임·fixture·artifact를 분리하고 실패를 warning으로 낮추면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-021@1` | [[raw/branch-notes/feature-web-vitals-performance-budget-contract]] | NFR은 device/network/cache/build context와 함께 MUST 측정 | import 참조로 적용 |
|
||||
| `FE-OC-026@1` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | 외부 답변은 evidence grade를 MUST 보존하고 목표 수치를 측정 결과처럼 말하면 안 됨 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
- 없음 — 자식 자료는 생성 후 controller가 parent Cluster와 함께 등록한다.
|
||||
|
||||
### 블로그·채용공고 연계 글감
|
||||
|
||||
- follow-up 후보: `raw/official-docs/axe-core-official.md` 아카이브(현재 inline research 로만 인용). 생성 시 D1·D2 Supporting Claim 을 wikilink 로 승격.
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
- 없음 — daily note는 이 작업에서 수정하지 않는다.
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크: 없음
|
||||
- 리뷰 메모: 없음
|
||||
- 머지 결과 / 배포 환경: `planned`
|
||||
- **wiki 추출 대상** (verified만): 없음
|
||||
- **추출하지 않을 항목** (planned / documented-only / abandoned): 현재 전체
|
||||
+401
@@ -0,0 +1,401 @@
|
||||
---
|
||||
title: branch / feature-api-client-response-envelope-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
branch: feature-api-client-response-envelope-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract.md]
|
||||
tags: [branch, ca-skeleton, frontend, api-design, integration, javascript, api-contract]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-005
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-005
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-TIMEOUT-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-RETRY-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-IDEMPOTENCY-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-004, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-002]
|
||||
contract_packet: 1
|
||||
contract_packet_sha256: e96673571270de028cc143c2c7b806cc8e9e434fbfbaeb7a9f3cac4e86f78505
|
||||
imports: [FE-GATE-004@1, FE-GATE-005@1, FE-GATE-007@1, FE-OC-002@1, FE-OC-007@1, FE-OC-010@1, FE-OC-022@1, FE-OC-023@1, FLOW-FE-RESP-004@1, FLOW-FE-RESP-005@1, FLOW-FE-RESP-006@1, FLOW-FE-RESP-007@1, FLOW-FE-RESP-008@1]
|
||||
delegates: [DELEG-FE-005@1]
|
||||
|
||||
---
|
||||
|
||||
# branch: feature-api-client-response-envelope-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — 단일 브랜치의 TODO·결정·진행 기록. 머지/종료 후 verified 결과는 `/ingest`로 `wiki/projects/`에 추출한다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: API registry와 timeout·abort·retry·idempotency deterministic tests가 통과한다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-TIMEOUT-001@1` | request total timeout default는 10초이며 별도 browser connect timeout은 주장하지 않는다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-RETRY-001@1` | retry는 initial call 이후 최대 2회, exponential backoff와 full jitter, 2초 cap을 사용한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-IDEMPOTENCY-001@1` | mutation auto-retry는 stable idempotency key와 backend replay contract가 있을 때만 허용한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-frontend-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 -->
|
||||
## 목표
|
||||
|
||||
이 브랜치는 project hub 의 두 계약 `FE-OC-006`(모든 HTTP 는 shared client 를 통과하고 timeout·abort·response parsing 을 page 에서 구현하지 않는다)과 `FE-OC-009`(retry 는 safe/idempotent request 에 한정하며 cap·jitter·`Retry-After` 를 적용)을 **구현 착수 가능한 명세**로 내린다. 구체적으로 (1) shared HTTP client boundary 와 request context(hub §7.1~7.2), (2) response envelope 처리 순서(hub §7.3), (3) timeout·abort 분류(hub §7.4), (4) retry 알고리즘·`Retry-After`·retry decision order(hub §7.5·§7.6·§8.3), (5) idempotency 와 mutation replay(hub §7.7·§7.8), (6) `FE-REG-API` operation registry(hub §5.3)를 owner 로서 확정한다. 근거 결정은 `FE-D014`(total timeout 10s), `FE-D015`(retry ≤2 · exponential backoff + full jitter · cap 2s), `FE-D016`(mutation auto-retry 는 idempotency key + backend replay contract 있을 때만). 현재 frontend 구현 repository 가 식별되지 않았으므로(hub §0.3 `NOT_READY`) 본 노트의 모든 구현 항목은 `planned` 이며, 이 브랜치의 완료 측정치는 hub §20 의 "API operation registry + timeout/abort/retry/idempotency deterministic tests" 다.
|
||||
|
||||
- 이슈: (없음 — 구현 repository·이슈 트래커 미생성)
|
||||
- PR: (없음 — scaffolding/spec 단계)
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
Primary contract IDs `FE-OC-006` + `FE-OC-009` 가 소유하는 것만:
|
||||
|
||||
- **Shared HTTP client boundary** — 모든 API request 가 통과하는 application output port 구현 adapter, page/component 의 직접 `fetch`·timeout 복제·envelope 해석·retry loop·auth token 읽기 금지 규칙(hub §7.1).
|
||||
- **Request context** — `operationId`/`method`/`routeId`/`timeoutMs`/`idempotency`/`attempt`/`abortReason`/`authMode` 필드 계약(hub §7.2).
|
||||
- **Response envelope 처리 순서** — transport→content-type→JSON parse→envelope schema→success/failure branch→payload schema→DTO→application model mapper→application result/normalized failure 의 8단계 total order 와 success/failure envelope shape 판별(hub §7.3).
|
||||
- 이 중 **stage 1~3 의 owner 는 본 branch** 다 — hub §2.1.4 Flow Stage Registry `FLOW-FE-RESP-001@1`(transport 완료 대기; timeout·abort 를 이 단계가 소유하고 이후 단계로 예외를 넘기지 않는다) · `FLOW-FE-RESP-002@1`(content-type 기대값 검사; 기대와 다르면 본문을 파싱하지 않고 실패 전환) · `FLOW-FE-RESP-003@1`(JSON parse; parse 실패는 raw body 를 버리고 실패 전환). 이 세 단계의 Invariants 를 바꾸려면 본 branch 가 revision 을 올려야 하고, 인접 단계 branch 의 pin 이 낡아 `STALE_IMPORTED_CONTRACT` 로 잡힌다. stage 4~8 은 남의 소유라 `imports` 로만 pin 한다.
|
||||
- **Timeout 과 abort 분류** — total 10s timeout(`FE-D014`), navigation/user/superseded/external abort 의 분류·retry·telemetry·UX(hub §7.4).
|
||||
- **Retry 정책** — 알고리즘(max 2 · exponential backoff + full jitter · base 250ms · cap 2s, `FE-D015`), retry candidate status 집합, retry decision order, `Retry-After` 처리(hub §7.5·§7.6·§8.3).
|
||||
- **Idempotency 와 mutation replay** — keyed mutation 만 자동 retry(`FE-D016`), key lifecycle(memory-only default), 401 recovery 후 replay policy(hub §7.7·§7.8).
|
||||
- **`FE-REG-API` operation registry** — method/path/operationId/auth/timeoutMs/idempotency/requestSchema/responseSchema/owner 필드 스키마와 registry-first 강제(hub §5.3, registry owner map §5.1).
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외 — 인접 계약이지만 다른 owner branch/외부 시스템이 소유. 여기서 detail 을 정하지 않고 owner 를 가리킨다(CLAUDE.md §15.5 R3).
|
||||
|
||||
- **Envelope/payload runtime schema 정의(Zod)** — `FE-OC-007` owner [[raw/branch-notes/feature-runtime-schema-validation-contract]]. 본 client 는 §7.3 step 4·6 에서 그 schema 를 *호출*만 한다.
|
||||
- **Frontend error kind enum·`FE-REG-ERROR`·normalized failure shape** — `FE-OC-008` owner [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]]. 본 client 는 그 kind 를 *방출*하고 retryability 만 결정한다.
|
||||
- **Auth token lifecycle** — 발급·저장·refresh·rotation·logout·revocation·IdP redirect 는 `FE-OC-010` owner [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] + 외부 Keycloak. 본 client 는 `AuthSessionPort.attach` 호출과 401/403 정규화만.
|
||||
- **Query cache/invalidation 배선** — `FE-OC-012` owner [[raw/branch-notes/feature-server-state-caching-contract]]. 본 client 는 retry policy 를 *callback* 으로 노출할 뿐 TanStack Query client 를 import 하지 않는다.
|
||||
- **Runtime config 로딩·검증** — `REQUEST_TIMEOUT_MS`/`MAX_RETRY_ATTEMPTS`/`API_BASE_URL`/`API_CONTRACT_VERSION` 의 존재·검증은 `FE-OC-004` owner [[raw/branch-notes/feature-frontend-env-runtime-config-contract]]. 본 client 는 검증된 값을 *소비*.
|
||||
- **API/schema breaking change 의 migration·version bump 판정** — `FE-OC-023` owner [[raw/branch-notes/feature-frontend-contract-compatibility-governance]].
|
||||
- **Registry snapshot·orphan token scan 강제** — `FE-OC-022` owner [[raw/branch-notes/feature-frontend-contract-registry-governance]]. 본 branch 는 `FE-REG-API` 스키마만 소유.
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
> 주의: 본 branch 의 primary 결정(`FE-D014`/`FE-D015`/`FE-D016`)은 hub 가 명시적으로 기록한 **project decision / conditional-default** 이며 외부 official-doc 이 근거가 아니다(hub §3.2 Evidence/rationale 열: "project-local initial limit", "retry storm 억제를 위한 project default", "duplicate write 방지 invariant"). 따라서 이들의 SSOT 는 governing hub 자체다. 아래 official-doc 은 envelope 처리 파이프라인이 *위임 호출*하는 schema 계층의 근거로만 매핑된다.
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] (§7.4 `FE-D014`) | D4 — total 10s timeout, 별도 connect timeout 미주장 |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] (§7.5·§8.3 `FE-D015`) | D5 — retry ≤2 · exponential backoff + full jitter · cap 2s · retry candidate 집합 |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] (§7.6) | D6 — `Retry-After` 파싱·30s 상한·terminal `RATE_LIMITED` |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] (§7.7·§7.8 `FE-D016`) | D7·D8 — keyed mutation 만 retry, 401 recovery replay policy |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] (§5.3 `FE-D018`) | D9 — `FE-REG-API` registry-first 강제 |
|
||||
| [[raw/official-docs/zod-runtime-schema-validation-official]] (`ZOD-VALID-C3`,`ZOD-VALID-C4`,`ZOD-VALID-C5`) | D3 — envelope/payload 검증을 runtime schema(Zod) 계층에 위임: `.parse()` 검증 관문·`ZodError`·`.safeParse()` discriminated union. 단 schema *정의* 는 `FE-OC-007` sibling 소유 |
|
||||
|
||||
## TODO
|
||||
|
||||
측정 완료 기준(hub §20): "API operation registry + timeout/abort/retry/idempotency deterministic tests". 아래는 모두 `planned`(frontend 코드 부재).
|
||||
|
||||
- [ ] `FE-REG-API` operation registry 모듈 `src/contracts/api-operations.js` 정의(9개 필드 스키마 + `LIST_SAMPLE_RESOURCES`/`CREATE_SAMPLE_RESOURCE` 초기 row) — 등급: `planned`
|
||||
- [ ] shared HTTP adapter(`ResourceQueryPort`/`ResourceCommandPort` 구현) `src/adapters/http/` 작성 — 등급: `planned`
|
||||
- [ ] response envelope 8단계 처리 파이프라인 구현(200 이어도 invalid 면 success 반환 금지) — 등급: `planned`
|
||||
- [ ] `AbortController` 기반 total 10s timeout + abort 5분류(`REQUEST_TIMEOUT`/`REQUEST_ABORTED`/external signal reason 해소 → 미해소 시 `UNKNOWN_FAILURE`) 구현 — 등급: `planned`
|
||||
- [ ] retry scheduler(exponential backoff + full jitter, cap 2s, `ClockPort` + injectable random) 구현 — 등급: `planned`
|
||||
- [ ] `Retry-After` 처리(30s 상한 → terminal `RATE_LIMITED`) 구현 — 등급: `planned`
|
||||
- [ ] idempotency key lifecycle(memory-only) + mutation replay policy 구현 — 등급: `planned`
|
||||
- [ ] `AuthSessionPort.attach` 호출 + 401/403 정규화 + bounded 1회 recovery 배선(port 정의는 sibling) — 등급: `planned`
|
||||
- [ ] deterministic retry unit test(fake clock) + MSW integration taxonomy + negative fixture("POST without idempotency key receives 503") 작성 — 등급: `planned`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> 아래 Decision Evidence Map(D1~D9)의 산문 요약. 모든 결정은 governing hub 또는 archived official-doc 근거를 가진다(근거 없는 결정 없음 → `UNSUPPORTED_DECISION` 0건).
|
||||
|
||||
- **D1** shared HTTP client 를 모든 HTTP 의 단일 boundary 로 강제. 이유: page 마다 fetch/timeout/retry 재구현 시 동일 status 가 서로 다른 UX 로 갈라짐(hub §1.3-1). 대안: per-page fetch — route 1개·외부 API 0개 throwaway prototype 에서만(hub §0.4 반대 논거). 근거: hub `FE-OC-006`·§7.1.
|
||||
- **D2** response envelope 처리를 8단계 total order 로 고정하고 200 이어도 JSON/envelope/payload invalid 면 success 로 반환하지 않음. 근거: hub §7.3.
|
||||
- **D3** envelope/payload 검증을 runtime schema(Zod) 계층에 위임(client 는 순서·envelope discriminator gate 소유, schema 정의는 sibling `FE-OC-007`). 근거: [[raw/official-docs/zod-runtime-schema-validation-official]] `ZOD-VALID-C3/C4/C5` + hub `FE-D007`.
|
||||
- **D4** default total request timeout 10s, 별도 connect timeout 미주장(browser fetch 가 portable 하게 제공 안 함). abort 는 hub §7.4 의 5분류를 그대로 소유 — timeout 은 `REQUEST_TIMEOUT`, navigation/user/superseded 는 `REQUEST_ABORTED`(non-retryable), **external signal abort 는 signal reason 을 앞의 4분류 중 하나로 해소해 그 kind 로 귀속**하고 해소 불가 시 `UNKNOWN_FAILURE`; timeout owner 로 해소될 때만 retry 하며 telemetry 는 redacted reason category 만 남긴다. 근거: hub `FE-D014`·§7.4(5 rows)·§8.2.
|
||||
- **D5** retry 는 initial 이후 max 2회, exponential backoff + full jitter, base 250ms, cap 2s; network/timeout/429/502/503/504 만 후보이고 parse/envelope/schema/auth/authz/404/409/422 와 generic 500 은 non-retryable default. 근거: hub `FE-D015`·§7.5·§8.3.
|
||||
- **D6** `Retry-After` 파싱 후 유효 delay >30s 면 자동 retry 하지 않고 terminal `RATE_LIMITED`, ≤30s 면 local backoff 와 비교해 큰 값 사용. 근거: hub §7.6.
|
||||
- **D7** mutation 자동 retry 는 stable idempotency key + active backend replay contract 있을 때만; unkeyed(`none`) mutation 은 recovery 성공 후에도 replay 금지. 근거: hub `FE-D016`·§7.7·§8.5.
|
||||
- **D8** auth 는 consume-only: `AuthSessionPort.attach` 호출 + 401→`AUTH_REQUIRED`/403→`FORBIDDEN` 정규화 + logical request 당 bounded 1회 recovery callback + replay policy; token lifecycle 은 외부 owner. 근거: hub §7.8·`FE-D017`.
|
||||
- **D9** 모든 shared-client request 는 `FE-REG-API` registry row(9필드)를 먼저 가져야 하며 call site raw config 는 violation. 근거: hub §5.3·`FE-D018`.
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
> 각 결정과 raw source Claim ID의 연결은 `/branch-spec`에서 작성한다.
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | Shared HTTP client 를 모든 API 호출의 단일 boundary 로 강제; page/component 는 fetch·timeout·envelope 해석·retry·auth token 읽기 금지 (`FE-OC-006`) | client-only SPA 가 공유 backend 계약을 소비하는 한 이 default 유지 / 대안(per-page fetch)은 route 1개·외부 API 0개·배포 0회 throwaway prototype 일 때만(hub §0.4) | `raw/project-notes/ca-skeleton-frontend-operational-contract.md` `FE-OC-006`·§7.1 | `project-decision` | boundary 강제는 architecture lint(`FE-OC-002`)에 의존 — 그 gate 미구현 시 우회 가능 |
|
||||
| D2 | Response envelope 처리를 8단계 total order 로 고정; 200 이어도 JSON/envelope/payload invalid 면 success 반환 금지 (`FE-OC-006`) | backend 가 structured JSON envelope 를 제공(hub 가정 C)하는 한 유지 / 여러 backend 가 상이한 protocol·schema 이고 통합 adapter 불가면 재설계(가정 C 무효 조건) | `raw/project-notes/ca-skeleton-frontend-operational-contract.md` §7.3 | `project-decision` | backend envelope/OpenAPI source 미확정(`FE-Q-005`) — 실제 shape 이 §7.3 과 다를 수 있음 |
|
||||
| D3 | Envelope/payload 검증을 runtime schema(Zod) 계층에 위임; client 는 처리 순서와 top-level envelope discriminator gate 만 소유 (`FE-OC-006`→`FE-OC-007` 기여) | `FE-D007`(Zod 채택)이 유효한 한 위임 / bundle budget 또는 generated schema pipeline 이 대체안을 요구하면 전환(`FE-D007` revisit) | `raw/official-docs/zod-runtime-schema-validation-official.md#ZOD-VALID-C3`, `#ZOD-VALID-C4`, `#ZOD-VALID-C5`; `...frontend-operational-contract.md` `FE-D007` | `official-vendor-doc + project-decision` | 경계별 `.parse()`(throw) vs `.safeParse()`(non-throw) 선택은 sibling 소유(`ZOD-VALID-C5` "does not prove") — 본 파이프라인은 결과 계약만 소비 |
|
||||
| D4 | Default total request timeout 10s; 별도 connect timeout 미주장(browser fetch 가 portable 하게 분리 제공 안 함); override 는 registry row 의 owner 결정 필요. abort 는 §7.4 5분류 전부 소유 — timeout→`REQUEST_TIMEOUT`, navigation/user/superseded→`REQUEST_ABORTED`(no retry), external signal abort→reason 을 앞 4분류로 해소한 kind 로 귀속(해소 불가 시 `UNKNOWN_FAILURE`), timeout owner 로 해소될 때만 retry, telemetry 는 redacted reason category (`FE-D014`, `FE-OC-006`/`FE-OC-009`) | measured p95 가 10s 를 정당하게 초과하거나 streaming 이 도입되기 전까지 10s 유지 / 그 트리거 발생 시 `FE-D014` 재검토. external abort 는 외부 `AbortSignal` 을 client 에 전달하는 caller 가 존재하는 한 유지 / 그런 caller 가 없으면 dead branch | `...frontend-operational-contract.md` `FE-D014`·§7.4(5 rows)·§8.2 abort/unknown row; `FE-REG-ENV` `REQUEST_TIMEOUT_MS`; `FE-NFR-007` | `conditional-default` | measured latency baseline 없음(`FE-NFR-007` current evidence none) — 10s 는 initial limit. external abort 의 `abortReason` 토큰이 §7.2 enum 에 없음 — §3 `UNSUPPORTED_IMPL_DECISION` 참조 |
|
||||
| D5 | Retry: initial 이후 max 2회, exponential backoff + full jitter(base 250ms, cap 2s); network/timeout/429/502/503/504 만 후보, parse/envelope/schema/auth/authz/404/409/422·generic 500 은 non-retryable default (`FE-D015`, `FE-OC-009`) | retry storm 억제를 위한 project default; backend SLO·rate-limit contract 가 확정되기 전까지 유지 / 확정 시 `FE-D015` 재검토, generic 500 opt-in 은 operation owner 가 safe 증명 시 | `...frontend-operational-contract.md` `FE-D015`·§7.5·§8.3·§8.2; `FE-NFR-008` | `conditional-default` | `FE-RISK-006` — retry 가 backend overload 를 증폭. mitigation: cap/jitter/`Retry-After`+telemetry; load/degradation test 로 해소 |
|
||||
| D6 | `Retry-After` 파싱(delta-seconds 또는 HTTP-date); invalid/negative→local backoff, 유효 >30s→terminal `RATE_LIMITED`(자동 retry 안 함), ≤30s→local backoff 와 max (`FE-OC-009`) | 30s 상한이 project default; backend rate-limit contract 없이는 긴 대기를 자동 소비하지 않음 / contract 확정 시 `FE-D015` 와 함께 재검토 | `...frontend-operational-contract.md` §7.6; §8.2 `429` row | `project-decision` | 30s 임계는 hub 가 준 상수지만 근거 measured 아님 — retry decision order 와 함께 통합 테스트 필요 |
|
||||
| D7 | Mutation 자동 retry 는 stable idempotency key + active backend replay contract 있을 때만; `none`(unkeyed) mutation 은 recovery 성공 후에도 replay 금지, 명시적 user 재시도 요구 (`FE-D016`, `FE-OC-009`/`FE-OC-023`) | mutation 이 schema 로 naturally idempotent 임이 증명되기 전까지 keyed-only 유지 / 증명 시 `FE-D016` 재검토 | `...frontend-operational-contract.md` `FE-D016`·§7.7·§7.8 replay·§8.5 fixture | `accepted-documented-only` | backend `Idempotency-Key`·replay contract 미확정(`FE-Q-005`) — 없으면 mutation retry 는 영구 off |
|
||||
| D8 | Auth 는 consume-only: request 전 `AuthSessionPort.attach` 호출, 401→`AUTH_REQUIRED`/403→`FORBIDDEN` 정규화, logical request 당 bounded 1회 recovery callback, safe≤1·keyed≤1·none=0 replay; token lifecycle 미소유 (`FE-OC-006`→`FE-OC-010` 기여) | auth lifecycle 이 외부 owner 인 한 consume-only(`FE-D017`) / skeleton 이 독립 auth product 로 scope 변경 시 `FE-D017` 재검토 | `...frontend-operational-contract.md` §7.8 state machine·`FE-D017` | `project-decision (delegated boundary)` | auth adapter owner·interface 미정(`FE-Q-006`); `FE-RISK-005`(guard 를 security control 로 오해) — backend authz 가 최종 판단 |
|
||||
| D9 | 모든 shared-client request 는 `FE-REG-API` row(method/path/operationId/auth/timeoutMs/idempotency/requestSchema/responseSchema/owner)를 먼저 가져야 하며 call site raw config 는 violation (`FE-REG-API`, `FE-D018`) | 8-registry governance(`FE-D018`)가 유효한 한 registry-first / code generation SSOT 채택 시 `FE-D018` 재검토 | `...frontend-operational-contract.md` §5.3·§5.1·`FE-D018` | `project-decision` | registry snapshot·orphan token scan 강제는 `FE-OC-022` sibling 소유 — 본 branch 는 스키마만 |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> 전체 `planned` — frontend 구현 repository 가 아직 없다(hub §0.3 `NOT_READY`). 아래 경로는 hub §4.6 Planned directory blueprint + §5.1 registry owner map 에서 온 grounded anchor 이며, 실제 repository 생성 시 확정된다(`FE-D009` 변경 절차). 클래스·함수·파일명 중 hub 가 규정하지 않은 것은 `UNSUPPORTED_IMPL_DECISION` 으로 표기한다.
|
||||
|
||||
### 1. Shared HTTP client boundary 와 request context
|
||||
|
||||
> **Trace**: D1(`FE-OC-006`·§7.1) + D2(§7.3 진입) . `application` 이 `ResourceQueryPort`/`ResourceCommandPort` 를 소유(hub §4.4)하고 `adapters/http` 가 구현(hub §4.2). 배선은 `bootstrap/composition-root.js` 하나(hub §4.5·`FE-D011`).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: adapter 파일명(`src/adapters/http/http-client.js`)·클래스명(`SharedHttpClient`)·request context 객체 필드 순서는 hub 가 규정하지 않음 — blueprint 디렉토리(`src/adapters/http/`)만 grounded, 파일/식별자 명명은 구현자 임의 trade-off(가독성 우선, `FE-REG-API` operationId 와 1:1 연결 유지).
|
||||
|
||||
| 항목 | 명세 | 근거 |
|
||||
|---|---|---|
|
||||
| 진입점 | `adapters/http` 가 `application` 의 `ResourceQueryPort`·`ResourceCommandPort` 를 구현, presentation 은 facade 만 호출 | hub §4.2·§4.4 |
|
||||
| page 금지 목록 | 직접 `fetch` / `AbortController` timeout 복제 / status→user copy 변환 / raw body log / page-local retry / storage 에서 auth token 읽기 | hub §7.1 |
|
||||
| request context 필드 | `operationId`,`method`,`routeId`,`timeoutMs`,`idempotency`,`attempt`(initial=0),`abortReason?`,`authMode` | hub §7.2 |
|
||||
| 주입 | `composition-root` 가 `ClockPort`·injectable random·`AuthSessionPort`·validated config 를 client 에 주입 | hub §4.5·§7.5 |
|
||||
|
||||
### 2. Response envelope 처리 파이프라인
|
||||
|
||||
> **Trace**: D2(§7.3) + D3(§7.3 step 4·6 → Zod 위임, `ZOD-VALID-C3/C4/C5`). success/failure envelope shape 는 hub §7.3, 위반 시 kind 는 §8.2(error 계층 소유).
|
||||
>
|
||||
> - **해소됨(2026-07-21) — 근거 있는 결정**: envelope discriminator 의 throw/non-throw 는 owner sibling 이 이미 정했다. `FE-OC-007` owner [[raw/branch-notes/feature-runtime-schema-validation-contract]] D3 = "경계에서 `.safeParse()`(non-throwing) default, `.parse()`(throw)는 정규화 catch 내부 한정". 같은 사실이 hub §2.1.4 `FLOW-FE-RESP-004@1` Invariants("경계 검증은 `.safeParse()` non-throwing — throw 를 상위로 누출하지 않는다")로 고정되어 있고, 본 문서는 그 stage 를 `imports` 로 pin 한다. 본 파이프라인의 요구("invalid→normalized failure")는 그 결정과 정합이다.
|
||||
|
||||
처리 순서(총 8단계, 각 실패 지점의 normalized kind 는 §8.2 owner 소유):
|
||||
|
||||
| # | 단계 | 실패 시 kind(§8.2, 위임) |
|
||||
|---|---|---|
|
||||
| 1 | HTTP transport 완료 | `NETWORK_UNREACHABLE`/`REQUEST_TIMEOUT`/`REQUEST_ABORTED` |
|
||||
| 2 | content-type 기대 확인 | `CONTENT_TYPE_MISMATCH` |
|
||||
| 3 | JSON parse | `MALFORMED_JSON` |
|
||||
| 4 | envelope schema 검증(Zod 위임) | `ENVELOPE_MISMATCH` |
|
||||
| 5 | success/failure branch 판별 | HTTP status 기반 §8.2 row |
|
||||
| 6 | payload schema 검증(Zod 위임) | `SCHEMA_MISMATCH` |
|
||||
| 7 | DTO→application model mapper (`FLOW-FE-RESP-007@1`) | mapper 실패 시 catch-all `UNKNOWN_FAILURE` |
|
||||
| 8 | application result 또는 normalized failure 반환 | — |
|
||||
|
||||
- 불변식: `200` 이어도 3~6 중 하나가 invalid 면 success 로 반환하지 않는다. `4xx/5xx` body 가 invalid 면 status 기반 safe fallback error 를 만들고 raw body 는 폐기(hub §7.3).
|
||||
|
||||
### 3. Timeout 과 abort 분류
|
||||
|
||||
> **Trace**: D4(`FE-D014`·§7.4 5 rows). total 10s(`REQUEST_TIMEOUT_MS` 소비, env branch 검증). abort 분류·retry·UX 는 §7.4 표 전체(external signal abort 포함), 미해소 catch-all 은 §8.2 마지막 문단.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: `AbortController` 하나로 timeout·navigation·user·superseded abort 를 모두 표현할지, timeout 용 별도 controller 를 둘지는 hub 미규정 — 구현자 trade-off(단일 controller + `abortReason` 태깅 권장). connect/read timeout 분리는 **금지**(browser fetch 가 portable 제공 안 함, §7.4 마지막 문단).
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: hub §7.4 는 external signal abort 를 5번째 행으로 요구하지만 §7.2 `abortReason` 허용값은 `navigation`/`user`/`timeout`/`superseded` 4개뿐이라 이 상황을 표현할 토큰이 없다 — 본 branch 는 `abortReason` 에 `external` 값 1개를 추가하고, 해소된 원인은 별도 필드가 아니라 기존 4값으로 *재분류*해 기록한다(구현자 trade-off: enum 1값 확장이 telemetry·registry 계약 변경 폭이 가장 작다. 대안인 별도 `abortSource` 필드는 §7.2 스키마를 넓히고 §8.1 normalized failure 와 정보가 이중화된다). `external` 추가는 §7.2 스키마 변경이므로 실제 도입 시 hub §3.3 decision change protocol 로 승격한다.
|
||||
|
||||
| 상황 | kind | retry | telemetry | UX |
|
||||
|---|---|---|---|---|
|
||||
| 10s total timeout | `REQUEST_TIMEOUT` | safe/keyed 만 | terminal 시 1 event, elapsed bucket | retry action |
|
||||
| navigation cancel | `REQUEST_ABORTED` | no | debug counter, error event 금지 | stale surface 제거 |
|
||||
| user cancel | `REQUEST_ABORTED` | no | optional interaction event | neutral canceled |
|
||||
| superseded query | `REQUEST_ABORTED` | no | none | latest 유지 |
|
||||
| external signal abort (caller 가 넘긴 외부 `AbortSignal`) | reason 해소 결과에 귀속 — timeout owner 면 `REQUEST_TIMEOUT`, navigation/user/superseded 로 해소되면 `REQUEST_ABORTED`, 해소 불가면 `UNKNOWN_FAILURE` | no — 단 timeout owner 로 해소된 경우에만 timeout 정책(safe/keyed max 2) 적용 | redacted reason category 만(raw signal `reason` 값·message·stack 금지), 해소된 kind 의 telemetry rule 을 그대로 상속 | context-specific — 해소된 kind 의 UX 를 상속(timeout→`retry`, abort→`none`, 미해소→generic reference) |
|
||||
|
||||
### 4. Retry 알고리즘 · decision order · `Retry-After`
|
||||
|
||||
> **Trace**: D5(`FE-D015`·§7.5·§8.3) + D6(§7.6). `ClockPort` + injectable random source 로 결정론 테스트 가능(§7.5 normative).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: retry scheduler 파일/클래스명(`src/adapters/http/retry-policy.js`, `RetryScheduler`)은 hub 미규정 — blueprint 디렉토리만 grounded, 명명은 구현자 trade-off. `MAX_RETRY_ATTEMPTS` 는 env registry(§5.4 default `2`)에서 소비하되 상수 fallback 은 `FE-D015` 값.
|
||||
|
||||
```text
|
||||
maxRetries = 2 # initial 제외, hub §7.5 / FE-D015
|
||||
baseDelayMs = 250 # hub §7.5
|
||||
maxDelayMs = 2000 # cap, hub §7.5 / FE-D015
|
||||
delay(i) = min(maxDelayMs, baseDelayMs * 2^retryIndex) * random(0,1) # full jitter
|
||||
```
|
||||
|
||||
Retry decision order(hub §8.3, 위→아래 우선):
|
||||
|
||||
```text
|
||||
if aborted (navigation/user/superseded) -> no retry
|
||||
else if parse/envelope/schema/auth/authz/404/409/422 -> no retry
|
||||
else if method is safe -> apply status/network policy
|
||||
else if idempotency == keyed AND backend replay active -> apply status/network policy
|
||||
else -> no retry
|
||||
```
|
||||
|
||||
- external signal abort 는 위 순서의 **첫 줄 이전에 reason 해소 단계**가 선행한다: 해소 결과가 navigation/user/superseded 면 1번째 줄에 걸려 no retry, timeout 이면 3~4번째 줄의 status/network policy 로 내려가고, 해소 불가면 `UNKNOWN_FAILURE`(non-retryable)로 종결한다. 해소 단계 자체는 hub §7.4 row 5("reason에 따라" / "no unless timeout owner")에서 도출되며 §8.3 의 문장 순서를 바꾸지 않는다(§3 표 참조).
|
||||
- **UNSUPPORTED_IMPL_DECISION**: 위 순서 4번째 줄의 조건 "backend replay contract active" 를 표현하는 필드가 `FE-REG-API` 9필드에 없다 — hub §7.7 은 "backend contract 가 `Idempotency-Key` 를 지원한다고 registry 에 명시" 를 요구하지만 §5.3 스키마에서 이를 담을 수 있는 후보는 `idempotency` 하나뿐이다. 본 branch 는 `idempotency=keyed` 를 *backend key 지원 선언* 으로 읽고, §7.8 이 별도로 요구하는 *active replay contract* 는 `FE-Q-005` 해소 전까지 keyed 와 동일시한다(구현자 trade-off: 미검증 backend 정보로 registry 스키마를 늘리지 않는 대신, key 는 수용하지만 replay 결과를 반환하지 않는 backend 를 과신할 위험을 진다 — mutation auto-retry 자체가 `FE-Q-005` 해소 전까지 off 이므로 safe-path 작업은 막히지 않는다). backend 가 key 수용과 replay 반환을 구분하는 것으로 확인되면 10번째 필드(예: `replayContract`)를 hub §5.10 registry change protocol 로 추가한 뒤 이 분기를 두 조건으로 분리한다.
|
||||
- retry candidate status: network failure·timeout·`429`·`502`·`503`·`504`(safe/keyed 만). generic `500` 은 default off, operation owner 가 safe 증명 시 opt-in(hub §7.5).
|
||||
- backend `error.retryable=true` 는 hint 일 뿐 unsafe mutation 자동 retry 의 충분조건 아님(hub §8.3).
|
||||
- `Retry-After`: parse → invalid/negative 면 local backoff → 유효 >30s 면 automatic retry 안 하고 terminal `RATE_LIMITED` → ≤30s 면 local backoff 와 max → abort 시 wait 취소. raw value 는 telemetry 금지, normalized delay bucket 만(hub §7.6).
|
||||
- unmount/superseded 시 남은 timer 와 request 취소(hub §7.5 마지막 bullet).
|
||||
|
||||
### 5. Idempotency 와 401 recovery replay
|
||||
|
||||
> **Trace**: D7(`FE-D016`·§7.7) + D8(§7.8). key lifecycle 은 auth token lifecycle 과 분리, memory-only default(§7.7).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: idempotency key 생성 방식(UUID v4 vs client-side hash)·single-flight dedup key 도출은 hub 미규정 — 구현자 trade-off(logical action 당 1 key·retry 간 재사용·telemetry/URL/message 노출 금지 제약만 grounded, §7.7). key persistence 가 필요해지면 본 branch 가 아니라 storage registry(`FE-REG-STORAGE`)에 TTL/classification/migration 추가 후.
|
||||
|
||||
401 recovery state machine(hub §7.8, client 소비 부분만):
|
||||
|
||||
| 현재 상태 | 이벤트 | 다음 상태 | client 동작 |
|
||||
|---|---|---|---|
|
||||
| `authenticated` | first `401` | `recovery-pending` | 외부 owner bounded recovery callback 1회 |
|
||||
| `recovery-pending` | session restored | `authenticated` | replay policy 적용 |
|
||||
| `recovery-pending` | no session | `unauthenticated` | terminal `AUTH_REQUIRED` |
|
||||
| `recovery-pending` | adapter throw/reject/invalid | `integration-failed` | terminal `AUTH_INTEGRATION_FAILURE` |
|
||||
| any | same request 2nd `401` | `unauthenticated` | 추가 recovery 없이 terminal `AUTH_REQUIRED` |
|
||||
|
||||
Replay policy(recovery 성공 후): `safe`=최대 1회 replay / `keyed`=같은 key + active replay contract 시 최대 1회 / `none`=replay 금지, 명시적 user 재시도 요구(hub §7.8·§8.5 fixture).
|
||||
|
||||
### 6. `FE-REG-API` operation registry
|
||||
|
||||
> **Trace**: D9(§5.3·`FE-D018`). registry owner map §5.1 이 본 branch 를 `FE-REG-API` single owner 로 지정. planned path `src/contracts/api-operations.js`.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: registry 를 plain object map 으로 둘지 factory 함수로 둘지, operationId→row lookup API 모양은 hub 미규정 — 구현자 trade-off(9필드 스키마·`UPPER_SNAKE_CASE` operationId·call-site raw config 금지 제약만 grounded).
|
||||
|
||||
| Field | Required | Rule(hub §5.3) |
|
||||
|---|---|---|
|
||||
| `method` | yes | uppercase HTTP method |
|
||||
| `path` | yes | path template, query value·host 미포함 |
|
||||
| `operationId` | yes | stable `UPPER_SNAKE_CASE`, telemetry·test·owner key |
|
||||
| `auth` | yes | `none` 또는 `external-session` |
|
||||
| `timeoutMs` | yes | default `10000`, override 는 decision change |
|
||||
| `idempotency` | yes | `safe`/`keyed`/`none` |
|
||||
| `requestSchema` | yes | body 없으면 explicit `none`, params/search 도 검증 |
|
||||
| `responseSchema` | yes | success envelope payload schema reference |
|
||||
| `owner` | yes | owning feature/branch slug |
|
||||
|
||||
초기 planned row(hub §5.3): `LIST_SAMPLE_RESOURCES`(GET `/api/sample/resources`, safe), `CREATE_SAMPLE_RESOURCE`(POST `/api/sample/resources`, keyed) — owner 는 [[raw/branch-notes/feature-sample-feature-slice-contract-fixture]](`FE-OC-024`; 본 branch 는 스키마 소유, sample row 는 fixture branch 가 채움).
|
||||
|
||||
- `idempotency=keyed` 는 위 9필드 안에서 hub §7.7 의 "backend 가 `Idempotency-Key` 를 지원한다고 registry 에 명시" 요구를 담는 유일한 필드이며, §7.8 의 *active backend replay contract* 조건도 `FE-Q-005` 해소 전까지 여기에 겹쳐 읽는다 — 필드 분리 조건과 trade-off 는 §4 의 `UNSUPPORTED_IMPL_DECISION` 참조.
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로** (hub §8.2 matrix 중 본 client 가 방출/분기하는 것):
|
||||
- `NETWORK_UNREACHABLE`(DNS/offline/CORS-like): safe/keyed max 2 retry, 가능 시 cached safe data, raw URL telemetry 금지.
|
||||
- `REQUEST_TIMEOUT`(10s total): safe/keyed max 2, stale data 유지 가능, elapsed bucket.
|
||||
- `REQUEST_ABORTED`(navigation/user/superseded): no retry, error toast/event 금지, latest 유지.
|
||||
- external signal abort(caller 가 넘긴 외부 `AbortSignal`): reason 을 해소해 `REQUEST_TIMEOUT`(timeout owner) 또는 `REQUEST_ABORTED`(navigation/user/superseded)로 귀속, 해소 불가 시 catch-all `UNKNOWN_FAILURE`. timeout 으로 해소된 경우에만 safe/keyed retry, 그 외 no retry. telemetry 는 redacted reason category 만(raw `reason` 값 금지), UX 는 해소된 kind 를 상속(hub §7.4 row 5·§8.2).
|
||||
- `RATE_LIMITED`(`429`): `Retry-After` bounded, >30s 면 terminal, delay bucket.
|
||||
- `SERVER_FAILURE`(`502/503/504` safe/keyed max 2; `500` default off; 기타 5xx default off): stale safe data fallback.
|
||||
- Non-retryable: `MALFORMED_JSON`·`ENVELOPE_MISMATCH`·`SCHEMA_MISMATCH`·`AUTH_REQUIRED`·`FORBIDDEN`·`NOT_FOUND`·`CONFLICT`·`VALIDATION_REJECTED` — retry 하지 않고 normalized failure 반환.
|
||||
- **Total-function normalization**: response/adapter/browser exception 이 named branch 와 안 맞거나 mapper 자체가 실패하면 최종 catch-all 이 raw value 폐기 후 `UNKNOWN_FAILURE` 반환 — normalized failure 를 못 만든 채 throw 를 presentation 으로 통과시키는 경로 금지(hub §8.2 마지막 문단). enum·shape 는 `FE-OC-008` 소유이나 "leak 금지" 불변식은 본 client 책임.
|
||||
- 동시성: retry 중 component unmount / query superseded 시 남은 timer·request 취소(hub §7.5).
|
||||
- **다른 계약 의존** (대상 branch + consume 하는 contract; hub §20 Dependency·§4.3 dependency matrix):
|
||||
- [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] (`FE-OC-004`, 본 노트 D4·D5) — 검증된 `REQUEST_TIMEOUT_MS`·`MAX_RETRY_ATTEMPTS`·`API_BASE_URL`·`API_CONTRACT_VERSION` 소비. 그 config 검증 계약이 바뀌면 client boot 입력 변경.
|
||||
- [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] (`FE-OC-002`, 본 노트 D1·D5) — `ResourceQueryPort`/`ResourceCommandPort` + `ClockPort` + injectable random source 의 application-owned port 정의와 composition-root 주입(port ownership 결정은 hub 소유, §4.4 port matrix·§4.5 boot order). `ClockPort` 와 random source 는 본 branch 의 완료 측정치인 deterministic retry test 의 전제이며, injectable random 은 §4.4 port matrix 에 행이 없어 주입 형태(별도 port vs adapter 생성자 인자)는 layering branch 가 확정한다. port shape 변경 시 adapter 시그니처 영향.
|
||||
- [[raw/branch-notes/feature-runtime-schema-validation-contract]] (`FE-OC-007`, 본 노트 D3) — envelope/payload Zod schema; §7.3 step 4·6 이 호출. schema 계약 변경 시 파이프라인 검증 지점 영향.
|
||||
- [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] (`FE-OC-008`, 본 노트 D2·§8.2) — `FE-REG-ERROR` kind enum·normalized failure shape; client 가 emit·retryability 결정.
|
||||
- [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] (`FE-OC-010`, 본 노트 D8) — `AuthSessionPort.attach`·bounded recovery(auth lifecycle 은 외부 owner). port/lifecycle 변경 시 §7.8 소비 영향.
|
||||
- [[raw/branch-notes/feature-server-state-caching-contract]] (`FE-OC-012`, 본 노트 D5) — `QueryCachePort`/TanStack adapter 가 client retry policy 를 callback 으로 소비. page-local retry 숫자 금지.
|
||||
- [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] (`FE-OC-023`, 본 노트 D7) — `apiContractVersion`·API schema breaking change migration/version bump 판정.
|
||||
- [[raw/branch-notes/feature-frontend-operational-runbook-contract]] (`FE-OC-025`, `FE-RB-003`) — backend API degradation 시 technical escalation 이 본 branch → backend operation owner 경로(hub §16.3).
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| retry 는 initial 이후 정확히 ≤2회, backoff sequence 가 deterministic | 코드·fake clock 없음 | deterministic retry unit test(`ClockPort`+injectable random) — `FE-GATE-005`, `FE-NFR-008` | `needs-confirmation` |
|
||||
| POST(`idempotency=none`) 가 `503` 을 받아도 자동 retry 하지 않음 | 정책은 문서, 코드 미검증 | negative fixture "POST without idempotency key receives 503"(hub §15.2) + MSW integration — `FE-GATE-007` | `needs-confirmation` |
|
||||
| `200` + malformed JSON/invalid envelope 가 success 로 새지 않고 normalized failure 반환 | envelope 파이프라인 미구현 | runtime-schema/integration fixture(success envelope without `data`) — `FE-GATE-004`/`007` | `needs-confirmation` |
|
||||
| total timeout 이 10s 에 발화하고 `REQUEST_TIMEOUT` 으로 분류 | `AbortController` timeout 배선 미구현 | fake-clock unit + MSW delay integration — `FE-NFR-007` | `needs-confirmation` |
|
||||
| `Retry-After` >30s → 자동 retry 없이 terminal `RATE_LIMITED` | 30s 상한 로직 미구현 | integration fixture(`429` + `Retry-After: 60`) | `needs-confirmation` |
|
||||
| navigation/superseded abort 가 in-flight timer·request 취소 + error event 미방출 | 취소 경로 미구현 | component/integration abort fixture | `needs-confirmation` |
|
||||
| 외부 `AbortSignal` 로 끊긴 request 가 reason 해소 결과의 kind 로 귀속되고(미해소 시 `UNKNOWN_FAILURE`) timeout 으로 해소된 경우에만 retry, telemetry 에 raw reason 미노출 | reason 해소 로직 미구현 + `abortReason` 에 `external` 토큰 부재(§3 `UNSUPPORTED_IMPL_DECISION`) | external signal abort fixture 3종(timeout owner / navigation reason / 미해소 임의 reason) + telemetry redaction assertion — `FE-GATE-007` | `needs-confirmation` |
|
||||
| first `401` 이 bounded 1회 recovery callback, second `401` 은 terminal `AUTH_REQUIRED` | auth adapter·state machine 미구현 | MSW auth-recovery taxonomy integration — `FE-GATE-007` | `needs-confirmation` |
|
||||
| unkeyed mutation 은 recovery 성공 후에도 replay 안 함 | replay policy 미구현 | integration fixture(hub §8.5 "recovery succeeds for unkeyed mutation") | `needs-confirmation` |
|
||||
| normalization 이 total — 미매핑 exception 이 `UNKNOWN_FAILURE` 로 귀결, throw 가 presentation 으로 새지 않음 | catch-all 경로 미구현 | integration fixture(thrown non-`Error`/mapper exception) | `needs-confirmation` |
|
||||
| backend 가 `Idempotency-Key` + replay contract 를 실제 제공 | backend envelope/OpenAPI source 미확정(`FE-Q-005`) | backend owner 확인 + captured fixture 대조 | `needs-confirmation` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
> `/coverage`가 채우는 생성물이며 손으로 유지하지 않는다.
|
||||
|
||||
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| TODO — `/coverage` 실행 전 | missing | (없음) | 미평가 | TODO |
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: flow:start -->
|
||||
### 가져온 흐름 단계
|
||||
|
||||
| Stage Ref | Order | Owner | Input | Action | Output |
|
||||
|---|---:|---|---|---|---|
|
||||
| `FLOW-FE-RESP-004@1` | 4 | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | unvalidated JSON | envelope 공유 스키마 검증 | discriminated envelope |
|
||||
| `FLOW-FE-RESP-005@1` | 5 | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | discriminated envelope | success/failure 분기 검증 | 분기 확정 envelope |
|
||||
| `FLOW-FE-RESP-006@1` | 6 | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | 분기 확정 envelope | payload per-operation 스키마 검증 | 검증된 payload(deep clone) |
|
||||
| `FLOW-FE-RESP-007@1` | 7 | [[raw/branch-notes/feature-boundary-mapper-viewmodel-contract]] | 검증된 payload | DTO → application model 매핑 | application model |
|
||||
| `FLOW-FE-RESP-008@1` | 8 | [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] | application model 또는 실패 신호 | 정규화된 결과 반환 | application result 또는 normalized failure |
|
||||
<!-- GENERATED: flow:end -->
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-GATE-004@1` | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | invalid fixture 가 예상 kind 로 거부되지 않으면 merge 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-005@1` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | unit 레벨이 실패하면 merge 를 MUST 차단하고 warning 으로 낮추면 안 됨 | import 참조로 적용 |
|
||||
| `FE-GATE-007@1` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | MSW 기반 integration 매트릭스가 미충족이면 merge 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-OC-002@1` | [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] | `domain <- application <- presentation` 의존 방향과 application-owned output port를 MUST 지킴 | import 참조로 적용 |
|
||||
| `FE-OC-007@1` | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | JSON envelope와 payload는 boundary에서 runtime schema를 MUST 통과 | import 참조로 적용 |
|
||||
| `FE-OC-010@1` | [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] | skeleton은 session state를 소비하되 token lifecycle을 MUST 소유하지 않음 | import 참조로 적용 |
|
||||
| `FE-OC-022@1` | [[raw/branch-notes/feature-frontend-contract-registry-governance]] | 8개 registry는 single primary owner와 compatibility impact를 MUST 기록 | import 참조로 적용 |
|
||||
| `FE-OC-023@1` | [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] | API/config/storage/release schema의 breaking change는 migration 또는 version bump 없이 배포하면 안 됨 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
### Sub-branches (세부 작업)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 오류 기록 (이 branch 작업 중 발생)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 강의 (이 작업을 위해 학습한 강의)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### job-posting tie-ins (이 작업에서 파생된 글감)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크:
|
||||
- 리뷰 메모:
|
||||
- 머지 결과 / 배포 환경:
|
||||
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
|
||||
- `actually-implemented` 항목:
|
||||
- `locally-verified` 항목:
|
||||
- `prod-verified` 항목:
|
||||
- **추출하지 않을 항목** (planned / documented-only / abandoned):
|
||||
+364
@@ -0,0 +1,364 @@
|
||||
---
|
||||
title: branch / feature-async-ui-state-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
branch: feature-async-ui-state-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract.md]
|
||||
tags: [branch, ca-skeleton, frontend, application, react, error-handling]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-013
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-013
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-UI-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-SERVER-STATE-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-007, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-010]
|
||||
contract_packet: 1
|
||||
contract_packet_sha256: 106ffdb88a0c1937bbc4d9387b99c06b90050d7e375ab4e29c7ed2b1a0dfe570
|
||||
imports: [FE-OC-002@1, FE-OC-008@1, FE-OC-012@1, FE-OC-015@1, FE-OC-020@1]
|
||||
delegates: [DELEG-FE-006@1]
|
||||
|
||||
---
|
||||
|
||||
# branch: feature-async-ui-state-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — 단일 브랜치의 TODO·결정·진행 기록. 현재는 scaffolding 단계다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: required와 non-blocking state matrix component test가 통과한다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-UI-001@1` | UI composition은 React를 사용한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-SERVER-STATE-001@1` | application-owned QueryCachePort와 TanStack Query adapter를 사용하고 client store에 server state를 복제하지 않는다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-frontend-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 -->
|
||||
## 목표
|
||||
|
||||
이 브랜치는 project-wide contract `FE-OC-011`(async surface는 initial-loading·success·empty·terminal-error를 MUST 표현)의 **single owner**로서, hub §9.1 Async surface state model을 *되묻지 않고 구현할 수 있는 spec*으로 내린다. 원격 데이터에 의존하는 모든 view는 `loading` boolean 하나로 상태를 뭉개지 않고 required 4-state + non-blocking 4-state를 discrete하게 표현해야 하며(§9.1), 이 상태들을 React 함수형 컴포넌트 + 단방향 props 흐름으로 렌더한다(`FE-D004`, [[raw/official-docs/react-ui-library-official]] `REACT-UI-C1`·`REACT-UI-C5`). 부수적으로 `FE-OC-015`(operational failure를 state로 반환·render defect만 boundary throw), `FE-OC-020`(component state matrix test artifact), `FE-OC-024`(sample slice가 async surface를 fixture로 exercise)에 기여한다. 현재 frontend repository가 없으므로 본 노트의 모든 구현 주장은 `planned`이며 코드 evidence는 0건이다.
|
||||
|
||||
- 이슈: (없음 — repository 생성 전)
|
||||
- PR: (없음)
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- `FE-OC-011` 소유: async surface의 **required visible state** 4종(`initial-loading`/`success`/`empty`/`terminal-error`)과 **non-blocking state** 4종(`refreshing`/`stale-degraded`/`mutation-pending`/`mutation-conflict`)의 discrete 표현 계약 (hub §9.1).
|
||||
- 단일 `loading` boolean 금지 규칙의 코드 표현(base tagged union + non-blocking overlay flag 2축, §1) + base×overlay 합법 조합·indicator 우선순위 규칙.
|
||||
- server query/mutation 신호 → `{base, overlay}` 파생 계약의 **presentation 측 소비 형태**(view-model만 소비, TanStack Query client 직접 import 금지 — §4.3/§9.2).
|
||||
- `terminal-error` state가 normalized failure의 `userMessageKey` + `action`만 렌더하는 계약(§8.1/§8.4 소비).
|
||||
- `FE-D004`(UI composition = React) 소유 — 함수형 컴포넌트·props 단방향 흐름을 async state 렌더 기반으로 채택.
|
||||
- measurable completion: state matrix component test(base 4 + overlay 4 + 교차 2 + latch 전이 1 = 11 fixtures, `pnpm test:component` async fixtures).
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외. "이건 다른 owner 브랜치 범위"라고 답할 근거.
|
||||
|
||||
- **failure의 정규화(raw → 26-kind)**: `feature-frontend-error-classification-boundary-contract`(`FE-OC-008`) 소유. 본 브랜치는 normalized failure를 *소비*만 한다.
|
||||
- **server state의 fetch/cache/invalidation·QueryCachePort 정의**: `feature-server-state-caching-contract`(`FE-OC-012`) 소유. 본 브랜치는 port가 노출하는 상태 신호를 *소비*한다.
|
||||
- **error boundary topology·recovery 배치(boot/route/feature/async boundary 소유권)**: `feature-frontend-render-recovery-boundary-contract`(`FE-OC-015`) 소유. 본 브랜치는 "operational failure는 throw하지 않는다"는 계약만 제공.
|
||||
- **component test 하네스 구성(Vitest/RTL/MSW 설정·gate 분리)**: `feature-frontend-test-taxonomy-contract`(`FE-OC-020`) 소유. 본 브랜치는 async fixture 목록·기대치만 제공.
|
||||
- **loading/error live region·focus 관리의 axe 검증**: `feature-accessibility-baseline-contract` 소유(이 브랜치에 depend). async state는 a11y hook point만 노출하고 axe 규칙을 정의하지 않는다.
|
||||
- **auth token lifecycle / 401 replay**: 외부 auth owner + [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] `FE-OC-010`.
|
||||
- **telemetry event 정의·redaction·sink 정책**: 본 브랜치는 async state 전용 telemetry event를 정의하지 않으며 [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] `FE-OC-014`가 소유한다(hub §2.2 Q7 응답). error 표기 state가 남기는 telemetry rule은 §8.2 failure matrix의 kind별 rule을 그대로 따르고, 본 브랜치는 §8.1 금지 필드(raw body/token/stack)를 UI·telemetry 양쪽에 노출하지 않는 계약만 제공한다.
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/official-docs/react-ui-library-official]] `REACT-UI-C1` | D1 — UI composition을 React로 채택(재사용 컴포넌트 단위로 async surface 구성). `FE-D004`의 official 근거. |
|
||||
| [[raw/official-docs/react-ui-library-official]] `REACT-UI-C5` | D1·D4 — 부모 state를 props로 자식에 전달하는 단방향 흐름을, discrete async state의 렌더/전파 모델로 채택. |
|
||||
| [[raw/official-docs/tanstack-query-server-state-official]] `TSQ-C1` | D4 — async surface가 소비하는 "server state"(loading/staleness/refetch 신호)의 정의적 근거. 단, port 소유·구현은 `FE-OC-012`에 위임(delegated). |
|
||||
|
||||
## TODO
|
||||
|
||||
각 항목 옆 증거 등급 표기.
|
||||
|
||||
- [ ] base 4-state tagged union + non-blocking overlay flag 집합(2축) + `deriveAsyncState` selector 계약 정의 — 등급: `planned`
|
||||
- [ ] base × overlay 합법 조합표(§1.2, 20조합) + 단일 슬롯 indicator 우선순위(§1.3) 확정 — 등급: `planned`
|
||||
- [ ] server query/mutation 신호 → `{base, overlay}` 파생 매핑표 확정(server-state 계약 fix 후) — 등급: `planned`
|
||||
- [ ] `terminal-error` 렌더 컴포넌트(`userMessageKey` + `action` only, raw body/stack 금지) — 등급: `planned`
|
||||
- [ ] non-throw 규율 + async→render boundary handoff 계약 문서화 — 등급: `planned`
|
||||
- [ ] `{base, overlay}` state matrix fixture 11종(base 4 + overlay 4 + 교차 2 + latch 전이 1, `pnpm test:component`) — 등급: `planned`
|
||||
- [ ] 구현 repository·검증 evidence 식별 — 등급: `needs-confirmation`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
- 본 노트는 `/branch-spec` self-map으로 hub `FE-OC-011` owner scope에서 도출. frontend 코드는 아직 없음 → 전부 `planned` blueprint.
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> 아래 Decision Evidence Map의 prose mirror. 근거는 hub §9.1/§8/§10.1/§4 + `raw/official-docs/react-ui-library-official`.
|
||||
|
||||
- **D1**: UI composition은 React 함수형 컴포넌트 + 단방향 props 흐름으로 하고, async surface의 discrete state를 그 위에 렌더한다(`FE-D004`). 대안: native custom-element / 다른 framework fork(revisit trigger).
|
||||
- **D2**: 원격 데이터에 의존하는 모든 async surface는 required 4-state(`initial-loading`/`success`/`empty`/`terminal-error`)를 MUST 표현한다(§9.1). 대안: 없음(surface당 불변).
|
||||
- **D3**: non-blocking 4-state(`refreshing`/`stale-degraded`/`mutation-pending`/`mutation-conflict`)를 required state와 **별개 축으로** 표현한다. hub §9.1이 금지하는 것은 "`loading` boolean 하나로 empty/error/refreshing을 합치는 것"이므로, 금지 대상은 *상태 개수를 1개 boolean으로 붕괴시키는 것*이지 다축 구조 표현이 아니다.
|
||||
- **D8**: async surface 상태는 **`base` (required 4 중 정확히 1개) + `overlay` (non-blocking 4의 flag 집합)** 2축으로 표현한다. required 4는 §9.1 Data 열이 상호배타(none / present / valid empty / none-or-unusable)이므로 한 시점에 정확히 하나이고, non-blocking 4는 §9.1이 "Additional"로 분류하며 `refreshing`이 "existing content 유지"를 요구하므로 base를 *대체하지 않고 겹친다*. 단일 flat 8-union은 `success`+`refreshing` 동시 성립을 표현할 수 없어 기각. overlay 간 동시 성립 시 단일 슬롯 indicator 우선순위는 `mutation-conflict` > `mutation-pending` > `stale-degraded` > `refreshing`(§구현 가이드 §1.3).
|
||||
- **D4**: async state는 §1의 2축 구조(base tagged union + overlay flag set)로 표현하고 presentation은 application facade view-model만 소비한다. server query/mutation 신호 → `{base, overlay}` 파생은 application/adapter 경계에서 하며 presentation은 TanStack Query client를 직접 import하지 않는다(§4.3/§9.2).
|
||||
- **D5**: `terminal-error`(및 stale-degraded/mutation-conflict의 error 표기)는 error-classification이 낸 normalized failure의 `userMessageKey` + closed `action`만 렌더하고 raw body/stack을 노출하지 않는다(§8.1/§8.4 소비).
|
||||
- **D6**: async surface는 operational failure를 normal state로 반환하고 render boundary로 throw하지 않는다; render defect(programmer error/invariant breach)만 boundary가 잡는다(§10.1).
|
||||
- **D7**: 완료 판정은 base 4 + overlay 4 + 교차 2 + latch 전이 1(총 11 fixture)을 결정론적으로 재현하는 component matrix test(Vitest + RTL, `pnpm test:component`)다(§20 measurable completion).
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | UI composition은 React 함수형 컴포넌트 + 단방향 props 흐름을 채택하고, async surface의 discrete state를 그 위에 렌더한다 (`FE-OC-011` / `FE-OC-002`) | component-based UI를 유지하는 한 React default / native custom-element·다른 framework로 project fork 시 재검토(`FE-D004` revisit trigger) | `raw/official-docs/react-ui-library-official.md#REACT-UI-C1`, `#REACT-UI-C5`; [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D004` | `official-vendor-doc` + `project-decision (accepted-documented-only)` | React 채택은 code evidence 없음(문서상 채택). 실제 컴포넌트 트리가 nesting/props 패턴을 따르는지 로컬 검증 필요(react-ui doc Usage Boundaries) |
|
||||
| D2 | 원격 데이터 의존 async surface는 required 4-state(`initial-loading`/`success`/`empty`/`terminal-error`)를 MUST 표현 (`FE-OC-011`) | async surface(원격 데이터 view)가 존재하는 한 항상 4-state / 순수 정적 view(원격 데이터 없음)엔 async state 계약 불필요 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §9.1 required visible states 표 | `project-decision` | exhaustive coverage는 `{base, overlay}` state matrix test 11종(base 4 + overlay 4 + 교차 2 + latch 전이 1)으로만 증명(measurable completion) — 미구현 시 empty/error 누락 경로 leak |
|
||||
| D3 | non-blocking 4-state(`refreshing`/`stale-degraded`/`mutation-pending`/`mutation-conflict`)를 required state와 **분리된 축**으로 표현; 금지 대상은 "`loading` boolean 하나로 empty/error/refreshing 합치기"로 한정 (`FE-OC-011`) | background activity·write-in-flight·retry-exhausted·conflict가 발생 가능한 surface에 적용 / 발생 불가한 surface는 해당 overlay 생략(단 required 4-state는 유지) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §9.1 "Additional non-blocking states" 표 + 인용 "`loading` boolean 하나로 empty/error/refreshing을 합치면 contract violation이다" | `project-decision` | 어떤 surface가 어떤 non-blocking state를 갖는지는 operation semantics에 의존 — surface별 적용 범위 판단 필요 |
|
||||
| D8 | async surface 상태는 `base`(required 4 중 1개) + `overlay`(non-blocking 4의 flag 집합) 2축으로 표현하고, overlay 동시 성립 시 단일 슬롯 indicator 우선순위는 `mutation-conflict` > `mutation-pending` > `stale-degraded` > `refreshing` (`FE-OC-011`) | §9.1이 required/additional 2표를 유지하고 `refreshing`이 기존 content를 유지하는 한 2축 / 만약 hub가 non-blocking state를 base와 상호배타로 재정의하면 flat union으로 회귀 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §9.1 required 표(Data 열 none/present/valid empty/none-or-unusable = 상호배타) + "Additional non-blocking states" 표 + `refreshing` UI 요구 "existing content 유지" + `mutation-pending` Data "current view" | `project-decision` (구조) + `UNSUPPORTED_IMPL_DECISION` (표현 shape·indicator 우선순위) | §9.1은 overlay 동시 성립 시 렌더 우선순위를 규정하지 않음 — §1.3 우선순위는 사용자 trade-off. base×overlay 합법 조합표(§1.2)는 §9.1 Data 열에서 도출한 해석이며 hub가 명시한 표가 아님 |
|
||||
| D4 | async state는 §1의 2축 `{base, overlay}`(base tagged union 1개 + non-blocking overlay flag 집합)로 표현, presentation은 application facade view-model만 소비하고 TanStack Query client를 직접 import하지 않음; server 신호 → `{base, overlay}` 파생은 application/adapter 경계 (`FE-OC-011` → `FE-OC-012` 소비) | server state가 `QueryCachePort`로 소유되는 한(`FE-D006`) 유지 / presentation 직접 import는 §4.3 dependency rule 위반이라 대안 아님 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §4.3 dependency matrix·§9.2 (presentation·application은 TanStack Query 직접 import 안 함); `raw/official-docs/react-ui-library-official.md#REACT-UI-C5`; `raw/official-docs/tanstack-query-server-state-official.md#TSQ-C1` | `project-decision` + `official-vendor-doc` | server 신호(status/fetchStatus) → `{base, overlay}` 매핑 함수 shape는 hub가 규정 안 함(§구현 가이드 UNSUPPORTED_IMPL). port 신호 형태는 `FE-OC-012` owner 소유 — 계약 fix 전엔 매핑 잠정 |
|
||||
| D5 | `terminal-error`(및 error 표기 state)는 normalized failure의 `userMessageKey` + closed `action`만 렌더, raw body/stack 노출 금지 (`FE-OC-011` ← `FE-OC-008` 소비) | 모든 error 표기에서 불변 / 예외 없음 — raw 노출은 `FE-OC-008`이 금지 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §8.1 normalized failure shape·§8.4 UX action vocabulary | `project-decision (delegated consume)` | `kind → action` 계약 shape은 error-classification(`FE-OC-008`) 소유 — 그 계약 미확정 시 렌더 계약 모호(해당 브랜치 D6이 "action 실제 UI 실행은 async-ui 소유"라고 위임함) |
|
||||
| D6 | async surface는 operational failure를 normal state(`terminal-error`/`stale-degraded`)로 반환하고 render boundary로 throw하지 않음; render defect만 boundary가 catch (`FE-OC-011` → `FE-OC-015` 기여) | normalized operational failure는 항상 state 반환 / programmer defect·invariant breach만 throw | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §10.1 error boundary ownership(async boundary는 normalized state를 catch, thrown render defect는 catch 안 함) | `project-decision` | boundary topology·recovery 배치는 render-recovery(`FE-OC-015`) 소유 — async surface는 "throw 안 함" 계약만 제공. 경계 계약이 어긋나면 operational failure가 render boundary로 새어 reload loop 위험 |
|
||||
| D7 | 완료 판정은 `{base, overlay}` 11 fixture(base 4 + overlay 4 + 교차 2 + latch 전이 1)를 결정론적으로 재현하는 component matrix test(Vitest + RTL, `pnpm test:component`) (`FE-OC-011` → `FE-OC-020` 기여) | 2축 `{base, overlay}` 계약이 유효한 한 매트릭스 test / 대안 없음 — 완료의 유일 evidence(§20 measurable completion) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §20 measurable completion·§16 `pnpm test:component`(async fixtures)·`FE-D022` test stack | `project-decision` + `conditional-default (test stack)` | RTL/Vitest 하네스·fixture 구조는 test-taxonomy(`FE-OC-020`) 소유 — 본 브랜치는 async fixture 목록·기대치만 확정 |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> 전부 `planned` blueprint. 경로는 hub §4.6 Planned directory blueprint에서 도출(repository 생성 시 변경 가능). 코드는 존재하지 않는다.
|
||||
|
||||
### 1. Async surface state machine (base tagged union + non-blocking overlay flags)
|
||||
|
||||
> **Trace**: D2·D3·D8 + `FE-OC-011` + hub §9.1. §9.1은 두 개의 표를 유지한다 — "Required visible states"(4) 와 "Additional non-blocking states"(4). 후자는 전자를 *대체하지 않는다*: `refreshing`의 UI 요구가 "existing content 유지"이고 `mutation-pending`의 Data가 "current view"이므로, 이 state들은 데이터를 이미 가진 base 위에 겹친다. 따라서 8개를 하나의 상호배타 union으로 뭉치면 `success`+`refreshing` 또는 `success`+`mutation-pending` 동시 성립을 표현할 수 없다. 본 절은 이를 **2축**(base 1개 + overlay flag 집합)으로 계약화한다.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 2축 값 객체의 구체 shape(`{ base: 'success', overlay: { refreshing: false, staleDegraded: false, mutationPending: false, mutationConflict: false } }`)·tag 필드명·모듈 경로(`src/presentation/components/async/async-surface-state.js`)·`isValidEmpty(data)` 판별자 — hub §9.1은 state 이름과 UI 요구만 규정하고 JS 표현 shape/파일 경로/empty 판별 predicate를 규정하지 않음. Trade-off: base를 tagged union으로 두어 exhaustive `switch` + RTL fixture addressability를 유지하고, overlay는 flag 집합으로 두어 동시 성립을 손실 없이 표현. hub §9.1이 실제로 금지하는 문장은 "`loading` boolean 하나로 empty/error/refreshing을 합치면 contract violation이다"이므로 금지 대상은 *단일 boolean으로의 붕괴*이고, base+overlay 구조 표현은 그 금지에 해당하지 않는다(오히려 empty/error/refreshing이 서로 구분 가능하게 남는다).
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: §1.3 indicator 우선순위 — §9.1은 overlay가 동시에 성립할 때 어떤 UI 요구를 우선할지 규정하지 않음. Trade-off: "사용자 조치를 요구하는 것이 조용한 배경 신호보다 우선"이라는 단일 원칙으로 정렬.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: §1.1.1 `staleFailure` latch — hub §9.1은 `stale-degraded`의 진입 조건만 주고 clear/exit 조건을 규정하지 않으며, latch의 보관 위치·수명(query key 단위 / adapter 내부 vs selector 인자)과 refetch 진행 중 stale label 유지 여부도 규정하지 않음. Trade-off: latch를 `!refetchInFlight`와 곱해 read 축 두 overlay를 *정의상* 배타로 만들어(§9.2 focus refetch가 발동하는 정상 경로에서 invariant throw 회피), refetch 진행 중에는 stale label은 유지하되 manual retry affordance만 비활성화한다("아직 stale이지만 재시도 중"을 전달). alt = "동시 성립을 합법으로 허용"은 §9.1의 두 UI 요구(subtle indicator vs stale label + manual retry)가 같은 슬롯에서 충돌해 기각.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: §1.2 query-less(mutation-only) surface의 base 규칙 — hub §9.1은 "원격 데이터에 의존하는 surface"만 다루고 read query가 없는 write-only surface의 base를 규정하지 않음. Trade-off: base = `success` 고정 + write 축 overlay만 허용해, form이 항상 렌더 가능하다는 사실과 §9.1 required-state 표현 의무를 동시에 만족. alt = 이런 surface를 계약 밖으로 배제하면 `mutation-pending`의 "duplicate action 차단"(§9.1) 근거가 submit form에서 사라져 기각.
|
||||
> - **해석 주의(§1.2 조합표의 지위)**: §1.2 base×overlay 합법 조합표는 hub가 명시한 표가 **아니라** §9.1 Data 열(none / present / valid empty / none-or-unusable, 그리고 overlay 4종의 데이터 전제)에서 도출한 *해석*이다. 위반 시 render defect로 취급해 throw하는 근거(D6 invariant breach 경로)도 이 해석 위에 서 있다. hub가 §9.1에 조합표를 명시하면 본 절이 그것으로 대체된다.
|
||||
|
||||
#### 1.1 두 축
|
||||
|
||||
Base state (§9.1 required 표 — 한 시점에 **정확히 1개**, Data 열이 상호배타):
|
||||
|
||||
| base | data | activity | UI 요구 (§9.1) | 파생 신호(소비) |
|
||||
|---|---|---|---|---|
|
||||
| `initial-loading` | none | first request | 안정적 skeleton, focus theft 금지 | query pending & no cached data |
|
||||
| `success` | present | idle | view-model render | query success & non-empty |
|
||||
| `empty` | valid empty | idle | empty 사유 + 가능 시 primary action | query success & `isValidEmpty` |
|
||||
| `terminal-error` | none/unusable | stopped | safe message + registry action(§3 참조) | normalized failure(retry 소진/비재시도) |
|
||||
|
||||
Overlay flags (§9.1 "Additional non-blocking states" 표 — **0개 이상 동시 성립**, base를 대체하지 않음):
|
||||
|
||||
| overlay | data 전제 | activity | UI 요구 (§9.1) | 파생 신호(소비) |
|
||||
|---|---|---|---|---|
|
||||
| `refreshing` | stale/present | background | 기존 content 유지 + subtle indicator | `refetchInFlight` — background refetch가 진행 중 |
|
||||
| `stale-degraded` | cached | retry exhausted | stale label + manual retry | `staleFailure` latch set(직전 refetch가 재시도 소진/비재시도로 실패 & cached 존재) **AND** 현재 refetch in-flight 아님 |
|
||||
| `mutation-pending` | current view | write in flight | 중복 action 차단 | mutation pending |
|
||||
| `mutation-conflict` | authoritative refetch 필요 | stopped | conflict action | `CONFLICT`(409) normalized failure |
|
||||
|
||||
#### 1.1.1 read-overlay latch 전이 (`refreshing` ⊕ `stale-degraded`의 배타성 근거)
|
||||
|
||||
`stale-degraded`는 독립 flag가 아니라 **latch 1개 + in-flight 부정**의 파생값이다. read 축 전체를 `refetchInFlight`(현재 refetch 진행 여부)와 `staleFailure`(직전 refetch 실패가 아직 해소되지 않음) 두 신호로 계산한다:
|
||||
|
||||
```text
|
||||
refreshing := refetchInFlight
|
||||
stale-degraded := staleFailure && !refetchInFlight
|
||||
```
|
||||
|
||||
`staleFailure` latch가 필요한 이유: hub §9.1은 `stale-degraded`의 진입 조건(retry exhausted)만 규정하고 **exit 조건을 규정하지 않는데**, hub §9.2의 cache default가 "refetch on focus = enabled for stale query"이므로 `stale-degraded` surface는 window focus만으로 자동 background refetch에 진입한다. latch 없이 `stale-degraded`를 "직전 실패 & cached"로만 정의하면 그 정상 경로에서 `refreshing`과 동시 성립해 §1.2 배타 불변식이 깨진다. 위 정의는 두 flag를 `refetchInFlight` 하나의 참/거짓으로 갈라 **정의상(구조적으로)** 배타로 만든다 — 런타임 검사에 의존하지 않으므로 focus refetch 경로에서 invariant가 throw되지 않는다.
|
||||
|
||||
| 전이 | 트리거 | latch 변화 | 결과 read overlay |
|
||||
|---|---|---|---|
|
||||
| `stale-degraded` → `refreshing` | 재refetch **진입** — window focus 자동 refetch(§9.2) 또는 stale label의 manual retry | `staleFailure` **유지**(clear하지 않음) | `refreshing` only |
|
||||
| `refreshing` → ∅ | refetch **성공** | `staleFailure` clear | ∅ (base가 `success`/`empty`로 갱신) |
|
||||
| `refreshing` → `stale-degraded` | refetch **실패** & cached 존재 | `staleFailure` set(유지) | `stale-degraded` only |
|
||||
| `refreshing` → (base 전환) | refetch **실패** & cached 없음 | — | read overlay ∅ — §1.2에 따라 base = `terminal-error` |
|
||||
| ∅ → `refreshing` | 최초 background refetch(직전 실패 없음) | 변화 없음(unset) | `refreshing` only |
|
||||
|
||||
manual retry와 focus 자동 refetch는 **같은 전이**를 쓴다(둘 다 refetch 진입). "manual retry는 foreground라 `refreshing`이 아니다"라는 구분은 두지 않는다 — 그 구분은 §9.1에 근거가 없고, focus refetch 경로가 자동이므로 배타성을 구제하지도 못한다.
|
||||
|
||||
#### 1.2 합법 조합 (base × overlay)
|
||||
|
||||
§9.1 Data 열에서 도출: 4개 overlay 모두 *이미 렌더 가능한 데이터가 존재함*을 전제(stale/present · cached · current view · authoritative refetch 필요)하므로, 데이터가 없는 base에는 붙을 수 없다.
|
||||
|
||||
| base | 허용 overlay | 근거 |
|
||||
|---|---|---|
|
||||
| `initial-loading` | 없음 (∅) | Data = none — 유지할 기존 content가 없어 "existing content 유지"·"current view"가 성립 불가 |
|
||||
| `success` | 4종 모두 | Data = present |
|
||||
| `empty` | 4종 모두 | Data = valid empty(유효한 데이터) — refetch·mutation 모두 성립 가능 |
|
||||
| `terminal-error` | 없음 (∅) | Data = none/unusable, Activity = stopped — cached content가 남아 있다면 base는 `terminal-error`가 아니라 `success`/`empty` + `stale-degraded` |
|
||||
|
||||
Overlay 내부 상호배타(정의상 도출):
|
||||
|
||||
- `refreshing` ⊕ `stale-degraded` — §1.1.1 latch 정의(`stale-degraded := staleFailure && !refetchInFlight`)에서 **구조적으로** 도출. 직전 refetch 실패 후 focus 자동 refetch(§9.2)가 다시 걸리면 `stale-degraded → refreshing`으로 *전이*하며 동시 성립하지 않는다. 이 배타성은 런타임 assert가 아니라 파생식의 성질이다.
|
||||
- `mutation-pending` ⊕ `mutation-conflict` — 전자는 "write in flight", 후자는 Activity "stopped". 동시 성립 불가.
|
||||
|
||||
query-less(mutation-only) surface 규칙: read query가 없는 surface(제출 전용 form 등)는 base를 `initial-loading`으로 두지 않는다. 읽을 원격 데이터가 없어 "first request 대기"가 성립하지 않고 렌더 가능한 form view가 항상 존재하므로 **base = `success` 고정**이며, write 축 overlay(`mutation-pending`/`mutation-conflict`)만 사용한다. read 축 overlay(`refreshing`/`stale-degraded`)는 성립하지 않는다. `deriveAsyncState`는 `queryResult`가 `undefined`일 때 이 규칙을 적용한다.
|
||||
|
||||
→ 따라서 동시 성립하는 overlay는 최대 2개(read 축 1 + write 축 1)이며, 전체 합법 조합 수는 `initial-loading`(1) + `terminal-error`(1) + (`success`·`empty`) × 3(read: none/refreshing/stale-degraded) × 3(write: none/pending/conflict) = 20이다.
|
||||
|
||||
#### 1.3 동시 성립 시 우선순위 (indicator precedence)
|
||||
|
||||
read overlay와 write overlay는 서로 다른 affordance를 점유하므로(read = content 영역 indicator/stale label, write = action 영역 차단/conflict action) **기본은 동시 렌더**다. 단일 슬롯(예: surface 헤더의 status indicator 1칸)만 있는 경우에만 다음 순서로 하나를 고른다:
|
||||
|
||||
```text
|
||||
mutation-conflict > mutation-pending > stale-degraded > refreshing
|
||||
```
|
||||
|
||||
원칙: 사용자 조치를 요구하며 activity가 stopped인 것 → 사용자 조작을 차단하는 것 → 수동 retry를 요구하는 것 → 조용한 배경 신호. `deriveAsyncState`는 이 우선순위를 *렌더 힌트*(`overlay.primary`)로만 계산하고, overlay flag 자체는 절대 삭제하지 않는다(삭제하면 §9.1 요구가 유실됨).
|
||||
|
||||
### 2. Server-signal → state 파생 (consume, not define)
|
||||
|
||||
> **Trace**: D4·D8 + `FE-OC-011` → `FE-OC-012` 소비. TanStack Query query/mutation 신호를 §1의 2축 상태(`{base, overlay}`)로 파생하는 순수 selector를 application/adapter 경계에 둔다. query 신호는 base + read overlay를, mutation 신호는 write overlay를 결정하며, 두 축은 독립적으로 계산된 뒤 §1.2 합법 조합표로 검증된다. presentation은 결과 view-model만 받는다.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: query `{status, fetchStatus, data, isPlaceholderData}` 및 mutation `{status}` 튜플 → `{base, overlay}`의 구체 매핑표와 selector signature(`deriveAsyncState(queryResult, mutationResult, { isValidEmpty, staleFailure })`) — hub는 state 집합만 정의하고 TanStack 필드→state 매핑은 규정하지 않음. signature는 §1.1.1의 `staleFailure` latch를 명시 입력으로 받고(selector를 순수 함수로 유지), `queryResult`가 `undefined`이면 §1.2 query-less 규칙(base = `success`, write 축 overlay만)을 적용한다. Trade-off: 파생을 경계에 두어 presentation을 framework-neutral로 유지(§4.3), alt = page-local 파생은 dependency rule 위반이라 기각.
|
||||
> - **R3(위임)**: `QueryCachePort`가 노출하는 실제 신호 형태·query key·invalidation은 `feature-server-state-caching-contract`(`FE-OC-012`)가 소유한다. 본 절은 그 신호를 *소비*하는 매핑만 명세하며, port 신호 shape이 확정되면 매핑표를 fix한다.
|
||||
|
||||
### 3. error-표기 state 렌더 계약
|
||||
|
||||
> **Trace**: D5 + `FE-OC-011` ← `FE-OC-008` 소비. error를 표기하는 state(`terminal-error`, `stale-degraded`, `mutation-conflict`)는 normalized failure의 safe 필드만 사용한다.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: `action`(6-closed: `retry`/`reauth`/`navigate`/`reload-once`/`contact-support`/`none`) → 구체 버튼/handler 컴포넌트(`AsyncErrorSurface`) 매핑, `userMessageKey` → copy 카탈로그 lookup — hub §8.4는 action 어휘와 allowed-when/MUST-NOT만 규정하고 컴포넌트/카피 구현은 규정 안 함. Trade-off: action별 단일 presentational 컴포넌트로 고정해 테스트 대상을 좁힘; copy 카탈로그(i18n)는 본 브랜치 밖.
|
||||
> - **R3(위임)**: `kind → action`·`kind → userMessageKey` 매핑 계약은 `feature-frontend-error-classification-boundary-contract`(`FE-OC-008`)가 소유(그 브랜치 D6이 "action의 실제 UI 실행은 async-ui가 소유"라고 위임). 본 절은 소비/렌더만.
|
||||
|
||||
렌더 불변식: raw response body·token·authorization header·full URL/query·stack·storage value를 error state UI에 노출하지 않는다(§8.1).
|
||||
|
||||
### 4. Non-throw 규율 + async→render boundary handoff
|
||||
|
||||
> **Trace**: D6 + `FE-OC-011` → `FE-OC-015` 기여. async surface는 normalized operational failure를 반드시 state로 반환하고 render boundary로 throw하지 않는다.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: "state로 반환됐고 throw되지 않았음"을 강제하는 test 어서션 형태(예: failure 주입 후 nearest error boundary 미발동 assert) — hub는 원칙만 규정. Trade-off: integration test에서 boundary render 여부로 검증(별도 boundary mock 대신 실제 boundary 미발동 관찰).
|
||||
> - **R3(위임)**: boundary 배치·소유권(boot/route/feature/async boundary)·reload loop 방지(§10.2)는 `feature-frontend-render-recovery-boundary-contract`(`FE-OC-015`)가 소유. 본 절은 async surface가 그 boundary를 발동시키지 않는다는 계약만 제공.
|
||||
|
||||
### 5. Component state matrix tests (measurable completion)
|
||||
|
||||
> **Trace**: D7·D8 + `FE-OC-011` → `FE-OC-020` 기여. base 4종과 overlay 4종을 각각 결정론적으로 재현하는 component fixture(8종) + overlay 동시 성립 우선순위 fixture(2종) + §1.1.1 read-overlay latch 전이 fixture(1종)를 작성하고 `pnpm test:component` gate(`artifacts/tests/component.xml`)에 편입.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: fixture 파일명·경로(`tests/component/async-surface.state-matrix.test.jsx`)·RTL query 전략(role/label 기준) — hub §20은 "state matrix component tests" 결과만 요구하고 파일 배치·query 전략은 규정 안 함. Trade-off: base/overlay당 최소 1 fixture로 1:1 addressable하게 배치하고, §1.2의 20개 합법 조합 전수 대신 축별 1개 + 교차 2개 + latch 전이 1개로 축소(전수는 fixture 유지비가 계약 가치를 넘어섬). latch 전이만 예외적으로 fixture를 추가한 이유는 그것이 정적 조합이 아니라 §9.2 focus refetch가 발동시키는 *시간 축* 경로여서 정적 조합 fixture로는 재현되지 않기 때문.
|
||||
> - **R3(위임)**: Vitest/RTL/MSW 하네스 구성·gate 분리·artifact 규약은 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D022`가 규정하고, 구현 소유자는 [[raw/branch-notes/feature-frontend-test-taxonomy-contract]](`FE-OC-020`)다. 본 절은 async fixture 목록(11종)과 각 fixture의 기대 `{base, overlay}`만 확정.
|
||||
|
||||
| fixture | 주입 조건 | 기대 base | 기대 overlay |
|
||||
|---|---|---|---|
|
||||
| initial-loading | pending & no cache | `initial-loading` | ∅ |
|
||||
| success | success & non-empty | `success` | ∅ |
|
||||
| empty | success & valid empty payload | `empty` | ∅ |
|
||||
| terminal-error | normalized failure(비재시도/재시도 소진) & cache 없음 | `terminal-error` | ∅ |
|
||||
| refreshing | success & background refetch in flight | `success` | `refreshing` |
|
||||
| stale-degraded | refetch 실패 & cached 존재 | `success` | `stale-degraded` |
|
||||
| mutation-pending | success & mutation in flight | `success` | `mutation-pending` |
|
||||
| mutation-conflict | success & `CONFLICT`(409) normalized failure | `success` | `mutation-conflict` |
|
||||
| overlay-cross | refetch in flight + mutation in flight 동시 | `success` | `refreshing` + `mutation-pending`(단일 슬롯 = `mutation-pending`) |
|
||||
| overlay-precedence | stale-degraded + mutation-conflict 동시 | `success` | `stale-degraded` + `mutation-conflict`(단일 슬롯 = `mutation-conflict`) |
|
||||
| stale-degraded → 재refetch | `stale-degraded` 상태에서 window focus 자동 refetch 진입(§9.2) — 이어서 (a) 성공 / (b) 실패 & cached 존재 | 진입 중 `success` → (a) `success` / (b) `success` | 진입 중 `refreshing` **only**(`stale-degraded` false, `staleFailure` latch는 유지) → (a) ∅ / (b) `stale-degraded` only. 세 시점 모두 두 flag 동시 true 아님을 assert(§1.1.1 latch 전이) |
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- `empty` vs `terminal-error` 오분류: valid empty payload를 error로 렌더하면 안 됨 → per-operation `isValidEmpty` predicate 필요(§9.1 empty = "valid empty").
|
||||
- `initial-loading`: skeleton 안정성 유지 + focus theft 금지(§9.1). live region 반복 announcement 억제는 a11y 브랜치 위임.
|
||||
- `refreshing` 중 background refetch 실패 → `stale-degraded`로 전이 + stale label + manual retry(§9.1), 기존 content 유지(§1.1.1 전이표).
|
||||
- `stale-degraded` surface가 window focus를 되찾아 자동 refetch(§9.2 "refetch on focus = enabled for stale query")에 진입 → `staleFailure` latch는 유지한 채 `stale-degraded → refreshing`으로 전이한다. 두 flag가 동시에 true가 되지 않으므로 §1.2 read 축 배타 불변식은 이 정상 경로에서 깨지지 않는다(§1.1.1).
|
||||
- `mutation-pending` 중 중복 submit → duplicate action 차단(§9.1).
|
||||
- `mutation-conflict`(409) → authoritative refetch를 요구하는 conflict action(§8.2 `CONFLICT` row).
|
||||
- async surface가 normalized failure를 못 만든 채 throw되는 경로: 이는 error-classification total-function(§8.2) 위반이며, 만약 새면 render boundary(`FE-OC-015`)가 최후로 catch — async surface는 이를 유발하지 않아야 함.
|
||||
- **다른 계약 의존**:
|
||||
- [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] `FE-OC-008` — normalized failure(`userMessageKey`/`action`/safe 필드)를 consume. 그 계약이 바뀌면 error 표기 state 렌더가 영향.
|
||||
- [[raw/branch-notes/feature-server-state-caching-contract]] `FE-OC-012` — `QueryCachePort`의 query/mutation 상태 신호를 consume해 §1의 2축 `{base, overlay}`를 파생. port 신호 shape 변경 시 매핑 재조정. 특히 cache default "refetch on focus = enabled for stale query"(hub §9.2)가 §1.1.1 read-overlay latch 전이(`stale-degraded → refreshing`)를 발동시키는 경로이므로, focus refetch를 opt-out하는 surface는 그 전이가 manual retry로만 일어난다.
|
||||
- [[raw/branch-notes/feature-frontend-render-recovery-boundary-contract]] `FE-OC-015` — boundary ownership에 의존. async surface는 throw하지 않는다는 계약을 제공하고 boundary 배치는 위임.
|
||||
- [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] `FE-OC-020` — component test 하네스·gate를 consume해 matrix fixture를 편입.
|
||||
- [[raw/branch-notes/feature-sample-feature-slice-contract-fixture]] `FE-OC-024` — sample slice가 async surface를 fixture로 exercise(sample removal smoke 대상).
|
||||
- [[raw/branch-notes/feature-accessibility-baseline-contract]] — loading/error live region·focus(§10.3)를 소유. async state는 a11y hook point만 노출.
|
||||
- [[raw/branch-notes/feature-tailwind-design-token-styling-contract]] `FE-OC-011` — 본 브랜치가 요구하는 시각 primitive(안정적 skeleton, `refreshing`의 subtle indicator, `stale-degraded`의 stale label, 단일 슬롯 status indicator)의 token-driven 어휘를 소유하는 co-tenant(hub Decision Register `FE-D005`가 `FE-OC-011`에 영향). 본 브랜치는 *어떤 state가 존재하고 언제 성립하는지*를 소유하고, 그 state의 시각 표현 어휘는 위임한다. 해당 브랜치 D4가 동일 경계를 반대편에서 명시("state machine·required-state는 위임"). primitive 어휘가 §1.2 조합표의 동시 렌더(read overlay + write overlay)를 표현하지 못하면 §1.3 단일 슬롯 fallback으로 축약된다.
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| component matrix가 base 4 + overlay 4 + 교차 2 + latch 전이 1을 결정론적으로 재현한다 | 코드·fixture가 아직 없음 | `pnpm test:component` async state matrix fixtures(`artifacts/tests/component.xml`) — base/overlay당 최소 1 fixture + `stale-degraded → 재refetch` 전이 fixture exit 0 | `needs-confirmation` |
|
||||
| `stale-degraded` 상태에서 focus 자동 refetch(§9.2)가 걸려도 `refreshing`·`stale-degraded`가 동시 true가 되지 않는다 | hub §9.1이 `stale-degraded`의 exit 조건을 규정하지 않아 latch 정의(§1.1.1)는 본 브랜치의 해석 | `deriveAsyncState` unit test — `staleFailure` latch set 상태에서 `refetchInFlight` true/false를 토글하며 두 flag의 동시 true 부재 assert + §5 `stale-degraded → 재refetch` fixture | `needs-confirmation` |
|
||||
| server 신호(status/fetchStatus/data + mutation status) → `{base, overlay}` 파생이 gap 없이 exhaustive하다 | hub가 매핑표를 규정하지 않아 잠정 | `deriveAsyncState` selector unit test(모든 튜플 조합 → base 정확히 1개 + overlay flag 집합이 §1.2 합법 조합에 속함) | `needs-confirmation` |
|
||||
| §1.2 합법 조합표가 실제 surface에서 위반되지 않는다(예: `terminal-error` + `refreshing` 동시 방출 없음) | 조합표는 §9.1 Data 열에서 도출한 해석이며 hub 명시 표가 아님 | `deriveAsyncState` invariant test — 불법 조합 방출 시 throw(render defect로 취급, D6의 "invariant breach" 경로). **단 read 축 배타(`refreshing` ⊕ `stale-degraded`)는 §1.1.1 파생식의 성질이라 런타임 throw 대상이 아니다** — throw가 걸리는 것은 base×overlay 조합(데이터 없는 base에 overlay 부착) 위반뿐이며, read 축은 `stale-degraded := staleFailure && !refetchInFlight`가 성립하는지 unit test로 확인한다 | `needs-confirmation` |
|
||||
| error 표기 state가 raw body/stack/token을 노출하지 않는다 | 렌더 경로가 미구현 | negative test — 직렬화 후 금지 필드 부재 assert(§8.1, error-classification D2 패턴 mirror) | `needs-confirmation` |
|
||||
| async surface가 operational failure에 render boundary로 throw하지 않는다 | boundary 계약·구현 미확정 | integration test — failure 주입 후 nearest error boundary 미발동 assert(§10.1) | `needs-confirmation` |
|
||||
| `isValidEmpty` predicate가 valid-empty를 error로 오분류하지 않는다 | per-operation empty 판별자가 미정 | component fixture(empty payload) → `empty` state assert | `planned` |
|
||||
| React 컴포넌트 트리가 nesting/props 단방향 흐름을 준수한다 | react-ui doc Usage Boundaries가 로컬 검증 요구 | 구현 후 architecture lint(`FE-OC-002` dependency-cruiser/ESLint) | `planned` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-OC-002@1` | [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] | `domain <- application <- presentation` 의존 방향과 application-owned output port를 MUST 지킴 | import 참조로 적용 |
|
||||
| `FE-OC-008@1` | [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] | 모든 failure는 stable frontend error kind로 MUST 정규화하고 raw body·stack을 UI에 노출하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-012@1` | [[raw/branch-notes/feature-server-state-caching-contract]] | query key와 invalidation은 registry factory만 MUST 사용 | import 참조로 적용 |
|
||||
| `FE-OC-015@1` | [[raw/branch-notes/feature-frontend-render-recovery-boundary-contract]] | expected operational error와 render defect를 MUST 분리하고 reload loop를 금지 | import 참조로 적용 |
|
||||
| `FE-OC-020@1` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | gate 종류별 책임·fixture·artifact를 분리하고 실패를 warning으로 낮추면 안 됨 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
### Sub-branches (세부 작업)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
### 오류 기록 (이 branch 작업 중 발생)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
### 강의 (이 작업을 위해 학습한 강의)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
### job-posting tie-ins (이 작업에서 파생된 글감)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크: TODO
|
||||
- 리뷰 메모: TODO
|
||||
- 머지 결과 / 배포 환경: TODO
|
||||
- **wiki 추출 대상**: 없음 — scaffolding 단계
|
||||
- **추출하지 않을 항목**: 없음 — scaffolding 단계
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
---
|
||||
title: branch / feature-boundary-mapper-viewmodel-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
branch: feature-boundary-mapper-viewmodel-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract.md]
|
||||
tags: [branch, ca-skeleton, frontend, mapper, react, clean-architecture]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-014
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-014
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-VALIDATION-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-ARCHITECTURE-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-006]
|
||||
contract_packet: 1
|
||||
contract_packet_sha256: 6a29a98487f6cf6afb2a40f0dd7b31f4f895e00a2b536821ce0de9fc6aded104
|
||||
imports: [FE-OC-002@1, FE-OC-007@1, FE-OC-008@1, FE-OC-024@1, FLOW-FE-RESP-001@1, FLOW-FE-RESP-002@1, FLOW-FE-RESP-003@1, FLOW-FE-RESP-004@1, FLOW-FE-RESP-005@1, FLOW-FE-RESP-006@1, FLOW-FE-RESP-008@1]
|
||||
---
|
||||
|
||||
# branch: feature-boundary-mapper-viewmodel-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — 단일 브랜치의 TODO·결정·진행 기록. 현재는 `/branch-spec` 로 채운 `planned` 설계 단계다 (frontend 코드 저장소 아직 없음 — 모든 구현 주장은 `planned`).
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: raw DTO direct use가 차단되고 mapper negative fixture가 실패한다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-VALIDATION-001@1` | boundary runtime validation은 Zod schema로 수행한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-ARCHITECTURE-001@1` | domain, application, presentation, adapters, bootstrap 책임을 분리한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-frontend-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 -->
|
||||
## 목표
|
||||
|
||||
이 브랜치는 hub §20 기준 **Primary contract owner 가 없는 기여(contribute) 브랜치**다. project-wide 계약 `FE-OC-007`(경계에서 JSON envelope·payload 를 runtime schema 로 검증) 과 `FE-OC-024`(sample 은 제거 가능한 contract fixture) 의 교집합인 **"raw DTO 직접 사용 금지 → boundary mapper 가 application model 을 생산하고 application 이 view-model 로 투영"** 책임을, 되묻지 않고 코드를 쓸 수 있는 implementation-ready spec 으로 내린다. 근거 축은 hub §4.2/§4.3 Clean Architecture layering(presentation 은 raw API DTO 를 소유·소비하면 안 되고 application 이 view-model 계약을 소유) + §7.3 응답 처리 순서 stage 7 `DTO → application model mapper`(§2.1.4 `FLOW-FE-RESP-007`) + §9.1 async `success` state 의 `view-model render` 요구다. 측정 가능한 완료 조건(hub §20): **raw DTO 직접 사용 금지 + mapper negative fixture**.
|
||||
|
||||
- 이슈: (아직 없음 — 저장소 생성 전)
|
||||
- PR: (아직 없음)
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- raw backend DTO 가 http-adapter 경계를 넘어 application/presentation 으로 흐르지 못하게 하는 **containment 규칙**과 그 경계에 놓이는 **DTO → application model mapper** 의 위치·계약(§7.3 stage 7, `FLOW-FE-RESP-007`).
|
||||
- application 이 소유하는 **view-model 계약**(render-ready shape)의 소유 위치·소비 규칙(§4.2/§4.3/§9.1).
|
||||
- mapper 를 **total/guarded function** 으로 만드는 규칙: mapper 자체 throw → `UNKNOWN_FAILURE` catch-all (§8.2 total function, §8.5 fixture).
|
||||
- 위 규칙을 증명하는 **mapper negative fixture** 와, sample slice 안의 제거 가능한 mapper 시연부(`FE-OC-024` 기여분).
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외 — 다른 owner 브랜치 소유. 여기서 detail 을 재정의하지 않고 owner 로 위임한다.
|
||||
|
||||
- **payload/envelope schema 정의·검증 메커니즘 자체 (Zod `.parse()`, schema 파일)** → `FE-OC-007` owner [[raw/branch-notes/feature-runtime-schema-validation-contract]]. 본 브랜치는 그 검증된 output(validated clone)을 mapper 입력으로 **소비만** 한다.
|
||||
- **normalized failure kind 카탈로그와 `UNKNOWN_FAILURE` 의 정규화 shape** → `FE-OC-008` owner [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]]. 본 브랜치는 mapper throw 를 그 catch-all 로 넘길 뿐, kind 목록을 정의하지 않는다.
|
||||
- **shared HTTP client·응답 envelope 파싱 파이프라인(§7.3 stage 1~6)** → `FE-OC-006` owner [[raw/branch-notes/feature-api-client-response-envelope-contract]].
|
||||
- **import 방향 정적 강제(dependency-cruiser/ESLint restricted import) 규칙 엔진** → `FE-OC-002` owner [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] / architecture lint 브랜치. 본 브랜치는 forbidden-import fixture case 만 제공.
|
||||
- **sample feature slice 의 실제 route/page/필드 내용** → `FE-OC-024` owner [[raw/branch-notes/feature-sample-feature-slice-contract-fixture]]. 본 브랜치는 그 slice 안의 mapper stage 만 소유.
|
||||
- **async surface state(`initial-loading`/`empty`/`terminal-error`) 렌더링** → `FE-OC-011` owner [[raw/branch-notes/feature-async-ui-state-contract]].
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/official-docs/react-ui-library-official]] `#REACT-UI-C1`, `#REACT-UI-C5` | D3 — presentation 이 view-model 을 컴포넌트 props(단방향 데이터 흐름)로 소비한다는 초기 근거. React component 모델·props 전달이 "presentation 은 view-model type 만 import" 규칙과 정합. **간접 근거**(component 모델 일반론이며 mapper 전용 계약은 아님). |
|
||||
| [[raw/official-docs/zod-runtime-schema-validation-official]] `#ZOD-VALID-C3`, `#ZOD-VALID-C4` | D2 — `.parse()` 가 반환하는 "strongly-typed deep clone" 이 mapper 의 입력(검증된 payload)이라는 근거. mapper 는 unvalidated JSON 이 아니라 검증 통과한 clone 만 받는다. `.parse()` 실패 throw 는 검증 계층(FE-OC-007) 소관이며 mapper 실행 전이다. |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §4.2/§4.3, `FE-D009`, `FE-D010` | D1 — presentation 은 raw API DTO 를 소유·import 하면 안 되고 application 이 view-model 계약을 소유(dependency rule). |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §7.3 stage 7 · §2.1.4 `FLOW-FE-RESP-007` | D2 — `DTO → application model mapper` 가 응답 처리 순서 stage 7(검증 stage 4~6 이후, application 결과 반환 stage 8 이전)이라는 위치 근거. stage 7 산출물이 model 이고 view-model 이 아니라는 것도 같은 근거. |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §4.2/§9.1 | D3 — application 이 view-model 계약 소유 + async `success` state 는 `view-model render`. |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §8.2/§8.5 | D4 — normalization 은 total function; mapper 예외는 `UNKNOWN_FAILURE` catch-all 로 흡수하고 raw value 폐기. negative fixture 필수. |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-024`, `FE-D025` | D5 — sample 은 제거 가능한 contract fixture 이며 product import 금지. mapper 시연부는 이 slice 안에 둔다. |
|
||||
|
||||
## TODO
|
||||
|
||||
각 항목 옆에 증거 등급 표기. 저장소 미생성이므로 전부 `planned` / `needs-confirmation`.
|
||||
|
||||
- [ ] http-adapter 경계에서 raw DTO 가 application/presentation 으로 새지 않게 하는 containment 규칙과 mapper 위치(stage 7) 확정 — 등급: `planned`
|
||||
- [ ] application 소유 view-model 계약(render-ready shape)의 위치·소비 규칙 명세 — 등급: `planned`
|
||||
- [ ] mapper 를 guarded total function 으로 구현(예외 → `UNKNOWN_FAILURE` 위임) — 등급: `planned`
|
||||
- [ ] mapper negative fixture(예외 유발 → `UNKNOWN_FAILURE` 기대) + presentation-imports-raw-DTO forbidden fixture case 작성 — 등급: `planned`
|
||||
- [ ] sample slice 안 mapper 시연부가 제거 가능하고 product import 0건임을 확인 — 등급: `needs-confirmation`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
- hub §20 상 본 브랜치는 Primary owner 없음 + `FE-OC-007`·`FE-OC-024` 기여, dependency = [[raw/branch-notes/feature-runtime-schema-validation-contract]] (검증된 payload 를 stage 7 로 넘겨받음). 그 sibling 노트가 이미 stage 7 mapper 를 본 브랜치로 위임(`FE-OC-007`·`FE-OC-024` 기여)하고 있어 정합 확인됨 — drift 없음.
|
||||
- ~~hub 내부 경미한 표현 불일치: §7.3 은 stage 7 을 "DTO → view-model mapper"(adapter 경계) 로, §4.2/§4.4 는 adapter 가 "validated model" 을 반환하고 application 이 "view-model 계약" 을 소유한다고 기술.~~ → **해소됨(2026-07-21)**: hub §7.3 stage 7 이 `DTO → application model mapper` 로 정정되고 "view-model 투영은 application 소유" 가 본문에 명시됐다. 같은 사실이 hub §2.1.4 Flow Stage Registry `FLOW-FE-RESP-007` 의 Invariants 로 고정되어, 본 브랜치가 채택한 2-stage 해석이 이제 hub 결정이다.
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> 아래 Decision Evidence Map 의 산문형 요약. 각 결정의 근거는 Sources 표 및 hub 참조.
|
||||
|
||||
- 2026-07-18: **raw DTO containment** — raw backend DTO 는 http-adapter 경계를 넘지 못하고, presentation/use-case 는 application 소유 view-model 만 소비한다. 이유: hub §4.2/§4.3 dependency rule(presentation MUST NOT own raw DTO). 검토한 대안: presentation 이 DTO 에서 직접 파생 — layering(`FE-D009`/`FE-D010`) 위반이라 기각.
|
||||
- 2026-07-18: **mapper 위치 = stage 7** — DTO → application model mapper 는 §7.3 처리 순서 stage 7(schema 검증 이후, 결과 반환 이전)에 놓이며 입력은 검증된 clone 이다. 대안: 검증 전 raw JSON 매핑 — 검증 우회라 기각. (2026-07-21 정정: stage 7 산출물은 model 이고 view-model 이 아니다 — hub §7.3 · §2.1.4 `FLOW-FE-RESP-007`.)
|
||||
- 2026-07-18: **view-model 소유 = application** — view-model 계약은 application 이 소유(`application/view-models/`), presentation 은 type 만 import. 대안: presentation-local view-model — `FE-D010`(application-owned contract) 위반이라 기각.
|
||||
- 2026-07-18: **mapper = total/guarded function** — mapper 예외는 presentation 으로 throw 되지 않고 `UNKNOWN_FAILURE` catch-all 로 흡수(§8.2). negative fixture 로 증명. 대안: 예외 전파 — §8.2 total function 요구 위반이라 기각.
|
||||
- 2026-07-18: **mapper 시연부 = 제거 가능한 sample fixture** — mapper 데모 + fixture 는 `sample/contract-fixture/` 안에 두고 product 는 import 금지(`FE-OC-024`/`FE-D025`). 대안: 공용 product util — sample 제거 smoke 위반이라 기각.
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | raw API DTO 는 http-adapter 경계를 넘지 못하고 presentation/use-case 는 application 소유 view-model 만 소비 (raw DTO 직접 사용 금지) — `FE-OC-007`·`FE-OC-024` 기여 | 스켈레톤의 모든 read/query 응답에 항상 적용되는 invariant. 대안(presentation 이 DTO 에서 직접 파생)은 layering 결정 `FE-D009`/`FE-D010` 가 뒤집힐 때만 가능하고 그건 `FE-OC-002` owner 브랜치 소관 — 본 브랜치에서 바꾸지 않음 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §4.2/§4.3, `FE-D009`, `FE-D010`, `FE-OC-007` | project-decision | DTO→model 경계를 물리적으로 adapter 에 둘지 application 에 둘지 미세 미확정 → 구현 §1 |
|
||||
| D2 | DTO → application model mapper 는 §7.3 처리 순서 stage 7(검증 stage 4~6 이후, 결과 반환 stage 8 이전)에 위치하고 입력은 검증된 payload(deep clone); view-model 투영은 이 단계가 아니라 application 소유 | success branch(검증 통과)일 때만 mapper 실행. 검증 실패면 mapper 실행 안 하고 `SCHEMA_MISMATCH`/normalized-failure 경로(FE-OC-008)로 감 — 즉 대안은 "실행 안 함" | `raw/official-docs/zod-runtime-schema-validation-official.md#ZOD-VALID-C3`; [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §7.3, `FE-OC-007` | official-doc + project-decision | stage 7 라벨이 dependency sibling 과 일치(확인됨). 검증계층 output 형태 변경 시 mapper 입력 계약 재확인 필요 |
|
||||
| D3 | view-model 계약(render-ready shape)은 application 이 소유(`application/view-models/`); presentation 은 view-model type 만 import 하고 async `success` state 가 이를 render | 모든 slice 에서 application 소유가 default. 대안(presentation-local 또는 adapter 소유 view-model)은 `FE-D010`(application-owned contract) 를 layering owner 가 개정할 때만 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §4.2/§9.1, `FE-D010`; `raw/official-docs/react-ui-library-official.md#REACT-UI-C5` | project-decision + official-doc | adapter 의 validated-model 과 application 의 view-model 2-stage 분리 세부 미확정 → 구현 §1 |
|
||||
| D4 | mapper 는 total/guarded function — 예외(누락/renamed 필드, non-Error throw)는 presentation 으로 전파되지 않고 `UNKNOWN_FAILURE` catch-all 로 흡수(raw value 폐기) | mapper 예외는 항상 `UNKNOWN_FAILURE` 로. mapper throw 가 presentation 에 도달하도록 허용하는 조건은 없음(N/A) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §8.2, §8.5, `FE-OC-008` | project-decision | `UNKNOWN_FAILURE` 정규화 shape 자체는 `FE-OC-008` owner 소유 → 위임 |
|
||||
| D5 | mapper 시연부 + negative fixture 는 제거 가능한 sample slice(`sample/contract-fixture/`) 안에 두고 product feature 는 import 금지 | fixture 는 항상 sample 안. mapper 가 실제 product feature 에 필요해지면 sample 밖으로 graduate 하고 그 feature 브랜치가 소유(대안) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-024`, `FE-D025`, §4.2 | project-decision | sample slice 내용/route 는 `FE-OC-024` owner 소유 → 위임; 본 브랜치는 mapper stage 만 |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> `planned` blueprint (frontend 저장소 없음). CLAUDE.md §15.5 3-rule 준수: R1 Trace 필수, R2 UNSUPPORTED_IMPL_DECISION, R3 OUT_OF_BRANCH_SCOPE 정제. 경로는 hub §4.6 Planned directory blueprint + §5.1 에서 도출된 `planned` anchor.
|
||||
|
||||
### 1. 경계 배치 & mapping 파이프라인 (planned)
|
||||
|
||||
> **Trace**: D1 + D2 + D3 → hub §4.2/§4.3/§4.6, §7.3 stage 7, `FE-OC-007`. raw DTO 는 adapter 에서 멈추고, 검증된 clone 이 model 로, model 이 view-model 로 이어진다.
|
||||
>
|
||||
> - **(a) 2-stage 매핑 — 근거 있는 결정(2026-07-21 확정)**: mapper 는 stage 7 에서 `application model` 까지만 만들고 view-model 투영은 `application/view-models/` 가 소유한다. 근거: hub §7.3 stage 7 + §2.1.4 `FLOW-FE-RESP-007@1`(Invariants: "이 단계 산출물은 model 이고 view-model 이 아니다"). 본 브랜치가 임의로 고른 trade-off 가 아니라 hub 가 결정한 계약이므로 `UNSUPPORTED_IMPL_DECISION` 라벨을 뗀다.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: (b) mapper 모듈 파일 경로·명명(`src/adapters/http/<op>-model-mapper.js`, `src/application/view-models/<slice>-view-model.js`)은 hub §4.6 이 디렉터리(`adapters/http/`, `application/view-models/`)만 고정하고 파일명은 미규정 — trade-off: op/slice 접미사 convention 을 임의 채택(저장소 생성 시 조정 가능).
|
||||
|
||||
| 파이프라인 단계 | 입력 | 출력 | 소유 layer (planned 경로) | 규칙 |
|
||||
|---|---|---|---|---|
|
||||
| raw DTO 수신 | backend 응답 body | (경계 내부에서만 존재) | `adapters/http/` | raw DTO 는 이 layer 밖으로 반환·재노출 금지 |
|
||||
| schema 검증 | raw DTO | validated clone | `adapters/http/` (검증 메커니즘은 `FE-OC-007` owner 위임) | 검증 통과분만 다음 단계로 |
|
||||
| model 매핑 (2-stage 中 1) | validated clone | domain/application model | `adapters/http/` | validated payload → application-facing model |
|
||||
| view-model 투영 (2-stage 中 2) | application model | view-model | `application/view-models/` | render-ready shape 생산; raw status code·DTO 필드 1:1 노출 금지 |
|
||||
| 소비 | view-model | 렌더 | `presentation/` | view-model type 만 import (§4.3), raw DTO schema import 금지 |
|
||||
|
||||
### 2. mapper 함수 계약 (planned)
|
||||
|
||||
> **Trace**: D2 + D4 → hub §7.3 stage 7, §8.2 total function, `raw/official-docs/zod-runtime-schema-validation-official.md#ZOD-VALID-C3`.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: mapper 함수 signature/형태(순수 함수 `mapToModel(validatedPayload) → model` vs 클래스) 는 hub 가 미규정 — **순수 함수 채택**, trade-off: 테스트·treeshake 용이하나 stateful 전처리가 필요해지면 재검토. field 투영 방식(explicit allowlist 매핑 vs spread) 도 미규정 — **explicit 매핑 채택**, trade-off: 새 필드가 자동 노출되지 않아 안전하나 필드 추가 시 수기 갱신 필요.
|
||||
|
||||
- 입력: schema 검증을 통과한 payload(= `.parse()` 의 deep clone, `#ZOD-VALID-C3`). unvalidated JSON 을 입력으로 받는 경로 없음.
|
||||
- 출력: application model(성공) **또는** 정규화 실패로의 위임(§8.2). mapper 는 실패를 직접 만들지 않고 catch-all 로 넘긴다. view-model 투영은 이 단계가 아니라 `application/view-models/` 소유(2-stage 中 2).
|
||||
- guard: mapper 본문은 예외 안전 경계(try 경로) 안에서 실행되어 예외/누락 필드/비-Error throw 시 raw value 를 폐기하고 `UNKNOWN_FAILURE` 로 흡수(§8.2 마지막 문단, §8.5). presentation 으로 throw 통과 금지.
|
||||
|
||||
### 3. view-model shape 규칙 (planned)
|
||||
|
||||
> **Trace**: D3 → hub §4.2, §9.1. view-model 은 render-ready 이며 정규 shape 은 async success 렌더의 입력.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 일반 shape convention(중첩 DTO flatten, 날짜/숫자 포맷팅, optional 필드 부재 표현) 은 hub 가 원칙만 두고 detail 미규정 — **"raw status/DTO 필드명 비노출 + optional 부재는 throw 대신 안전 default/absent 표기" 원칙만 고정**, trade-off: 구체 포맷 규칙은 sample view-model 이 생길 때 확정.
|
||||
|
||||
- view-model 은 raw HTTP status·backend error code·DTO 필드명을 그대로 노출하지 않는다(§8.1/§8.2 원칙과 정합: raw body/status 로 UI 분기 금지).
|
||||
- **OUT_OF_BRANCH_SCOPE**: sample slice 의 **구체 view-model 필드 목록**은 `FE-OC-024` owner [[raw/branch-notes/feature-sample-feature-slice-contract-fixture]] 소유 — 여기서 필드를 열거하지 않고 그 브랜치로 위임.
|
||||
|
||||
### 4. negative fixture & 강제 (planned)
|
||||
|
||||
> **Trace**: D4 + D5 → hub §8.5, §15.2, `FE-OC-024`, `FE-OC-008`. 규칙이 실제 동작함을 deliberately failing fixture 로 증명.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 테스트 파일 경로·명명(`tests/unit/mapper-throws-maps-to-unknown-failure.test.js` 등)과 harness 는 hub 가 test stack(`FE-D022` Vitest+RTL+MSW) 만 고정하고 파일명 미규정 — **Vitest unit 채택**, trade-off: 저장소 생성 시 test-taxonomy 브랜치 convention 에 맞춰 조정.
|
||||
|
||||
| Fixture | 목적 | 기대 결과 | 소유/위임 |
|
||||
|---|---|---|---|
|
||||
| mapper 강제 throw(누락 필드/비-Error) | mapper total function 증명 | `UNKNOWN_FAILURE` 반환, raw value·stack 비노출 | 본 브랜치 소유(§8.5 "thrown non-Error object, symbol, or mapper exception → UNKNOWN_FAILURE") |
|
||||
| presentation 이 raw DTO schema import | raw DTO 직접 사용 금지 강제 증명 | architecture gate FAIL | fixture case 제공(본 브랜치) + 강제 엔진은 `FE-OC-002` owner 위임 [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]] |
|
||||
| sample 제거 후 product 빌드 | mapper 시연부가 제거 가능 fixture 임을 증명 | product import 0건, smoke PASS | `FE-OC-024` owner 위임 [[raw/branch-notes/feature-sample-feature-slice-contract-fixture]] |
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- mapper 가 **valid-but-empty payload** 수신(검증은 통과했으나 빈 결과) → application model 은 정상 생산하되 async `empty` state 로 표현(렌더 판단은 `FE-OC-011` owner 위임, mapper 는 throw 하지 않음).
|
||||
- mapper 가 **예상외 추가 필드** 수신 → 실패 아님. explicit allowlist 투영이므로 추가 필드는 무시(검증계층이 이미 shape 통과시킴).
|
||||
- mapper **자체 throw**(누락 필드, `null` 접근, non-Error throw) → raw value 폐기 후 `UNKNOWN_FAILURE`(§8.2). presentation 으로 throw 통과 경로 없음.
|
||||
- **nested optional 필드 부재** → application model 은 안전 default/absent 로 표기, throw 금지.
|
||||
- **다른 계약 의존**:
|
||||
- [[raw/branch-notes/feature-runtime-schema-validation-contract]] `FE-OC-007` 에 의존 — 검증된 payload(stage 6 output)를 mapper 입력으로 consume. 그 검증 output 형태가 바뀌면 mapper 입력 계약 영향.
|
||||
- [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] `FE-OC-008` 에 의존 — mapper throw 흡수 대상인 `UNKNOWN_FAILURE` 정규화 shape 을 consume.
|
||||
- [[raw/branch-notes/feature-api-client-response-envelope-contract]] `FE-OC-006` 에 의존 — mapper 가 꽂히는 §7.3 처리 순서 파이프라인(stage 1~8)을 소유.
|
||||
- [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] `FE-OC-002` 에 의존 — raw DTO 를 presentation 에서 금지하는 import 규칙 소유(본 브랜치는 fixture case 제공).
|
||||
- [[raw/branch-notes/feature-sample-feature-slice-contract-fixture]] `FE-OC-024` 에 기여 — mapper 시연부를 그 sample slice 안에 둠.
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| mapper 자체 throw 가 실제로 `UNKNOWN_FAILURE` 로 라우팅되고 raw value/stack 을 흘리지 않는다 | 저장소·mapper 코드 없음; §8.2 는 원칙만 규정 | mapper negative fixture(Vitest unit) — 강제 throw → `UNKNOWN_FAILURE` 단언, stack 비노출 assert (hub §8.5 fixture) | `needs-confirmation` |
|
||||
| presentation 의 raw DTO schema import 가 architecture gate 를 실제로 FAIL 시킨다 | 정적 강제 엔진 미구현 | forbidden-import fixture(dependency-cruiser/ESLint) — 강제 엔진은 `FE-OC-002` owner, fixture case 는 본 브랜치 | `needs-confirmation` |
|
||||
| 2-stage 매핑(adapter validated-model → application view-model)이 중복 할당 없이 테스트 가능하다 | 2-stage 자체는 hub 결정(§7.3 · `FLOW-FE-RESP-007`)이며 남은 불확실성은 hop 추가에 따른 중복 할당·성능뿐 | 저장소 생성 후 mapper 단위 테스트 + 성능/할당 프로파일로 확인 | `planned` |
|
||||
| view-model 에 raw status/DTO 필드 leakage 가 없다 | sample view-model 필드 미확정(다른 브랜치 소유) | sample view-model 확정 후 component/unit 테스트로 raw status·backend code 비노출 assert | `planned` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
- 없음 — `planned` 설계 단계
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: flow:start -->
|
||||
### 가져온 흐름 단계
|
||||
|
||||
| Stage Ref | Order | Owner | Input | Action | Output |
|
||||
|---|---:|---|---|---|---|
|
||||
| `FLOW-FE-RESP-001@1` | 1 | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | HTTP 요청 | transport 완료 대기 | raw Response |
|
||||
| `FLOW-FE-RESP-002@1` | 2 | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | raw Response | content-type 기대값 검사 | 본문 판독 가능 Response |
|
||||
| `FLOW-FE-RESP-003@1` | 3 | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | 본문 판독 가능 Response | JSON parse | unvalidated JSON |
|
||||
| `FLOW-FE-RESP-004@1` | 4 | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | unvalidated JSON | envelope 공유 스키마 검증 | discriminated envelope |
|
||||
| `FLOW-FE-RESP-005@1` | 5 | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | discriminated envelope | success/failure 분기 검증 | 분기 확정 envelope |
|
||||
| `FLOW-FE-RESP-006@1` | 6 | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | 분기 확정 envelope | payload per-operation 스키마 검증 | 검증된 payload(deep clone) |
|
||||
| `FLOW-FE-RESP-008@1` | 8 | [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] | application model 또는 실패 신호 | 정규화된 결과 반환 | application result 또는 normalized failure |
|
||||
<!-- GENERATED: flow:end -->
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-OC-002@1` | [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] | `domain <- application <- presentation` 의존 방향과 application-owned output port를 MUST 지킴 | import 참조로 적용 |
|
||||
| `FE-OC-007@1` | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | JSON envelope와 payload는 boundary에서 runtime schema를 MUST 통과 | import 참조로 적용 |
|
||||
| `FE-OC-008@1` | [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] | 모든 failure는 stable frontend error kind로 MUST 정규화하고 raw body·stack을 UI에 노출하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-024@1` | [[raw/branch-notes/feature-sample-feature-slice-contract-fixture]] | sample은 contract fixture이며 production feature가 의존하면 안 됨 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
### Sub-branches (세부 작업)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
### 오류 기록 (이 branch 작업 중 발생)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
### 강의 (이 작업을 위해 학습한 강의)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
### job-posting tie-ins (이 작업에서 파생된 글감)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크: TODO
|
||||
- 리뷰 메모: TODO
|
||||
- 머지 결과 / 배포 환경: TODO
|
||||
- **wiki 추출 대상**: 없음 — `planned` 단계(구현 증거 생성 후 재평가)
|
||||
- **추출하지 않을 항목**: 현재 전 항목 `planned` — 외부 산출물 파생 금지
|
||||
+312
@@ -0,0 +1,312 @@
|
||||
---
|
||||
title: branch / feature-frontend-architecture-enforcement-lint-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-003
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-003
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-ARCHITECTURE-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-002, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-017]
|
||||
contract_packet: 1
|
||||
branch: feature-frontend-architecture-enforcement-lint-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract.md]
|
||||
tags: [branch, ca-skeleton, frontend, architecture, testing, javascript, static-analysis]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
contract_packet_sha256: 12187592363c519ab92cd3b73e1e4b135b2515f0421bd4c671ca45c7e30b2340
|
||||
imports: [FE-GATE-013@1, FE-OC-002@1, FE-OC-014@1, FE-OC-019@1, FE-OC-020@1]
|
||||
delegates: [DELEG-FE-002@1, DELEG-FE-003@1]
|
||||
|
||||
---
|
||||
|
||||
# branch: feature-frontend-architecture-enforcement-lint-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — 단일 브랜치의 TODO·결정·진행 기록. 머지/종료 후 verified 결과는 `/ingest`로 `wiki/projects/`에 추출한다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
형제 branch (같은 부모, 본 branch 가 의존/위임하는 대상):
|
||||
|
||||
- [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] — 본 branch 가 *기계화*할 allowed-import matrix 의 정의 owner (`FE-OC-002`)
|
||||
- [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] — gate 등록·artifact 보존·"실패→warning 금지" 정책 owner (`FE-OC-020`)
|
||||
- [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] — ESLint·dependency-cruiser 의 *설치* + base flat-config substrate owner (`FE-OC-003`). 본 branch 의 D1 은 `eslint.config.js`·`.dependency-cruiser.cjs` 가 이미 존재함을 전제하고 거기에 **규칙만 추가**한다.
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: allowed fixture는 통과하고 forbidden fixture는 실패하며 lint report가 생성된다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-ARCHITECTURE-001@1` | domain, application, presentation, adapters, bootstrap 책임을 분리한다 | allowed-import matrix의 lint·dependency graph 규칙에 적용 | [[raw/project-notes/ca-skeleton-frontend-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 -->
|
||||
## 목표
|
||||
|
||||
이 branch 는 `FE-OC-002`(의존 방향 `domain <- application <- presentation` 과 application-owned output port 를 MUST 유지)와 `FE-OC-020`(gate 별 책임·fixture·artifact 분리, 실패를 warning 으로 낮추지 않음)를 **구현 착수 가능한 강제(enforcement) 명세로 내리는** 브랜치다. 본 branch 는 자체 소유 contract 가 없다(§20 `Primary contract IDs = —`) — 대신 hub §4.3 dependency matrix 를 기계 검증 가능하게 만드는 **architecture gate (`FE-GATE-010`)** 을 build 한다: dependency-cruiser 그래프 규칙 + ESLint restricted-import 규칙 + allowed/forbidden fixture + `artifacts/quality/` 로의 dependency report 산출. 즉 layering branch 가 *정의*한 경계를 이 branch 가 *자동으로 집행*하고, test-taxonomy/CI branch 가 소비할 evidence artifact 를 emit 한다. 현재 frontend 코드는 존재하지 않으므로 아래 모든 구현 주장은 등급 `planned` 이다.
|
||||
|
||||
- 이슈:
|
||||
- PR:
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- dependency-cruiser 설정 — hub §4.3 dependency matrix 를 그래프 reachability 규칙으로 encoding (transitive/indirect 위반 포착) — 등급: `planned`
|
||||
- ESLint flat-config restricted-import 규칙 — 동일 matrix 를 import-statement(module) 레벨로 encoding — 등급: `planned`
|
||||
- allowed + forbidden fixture set — `presentation → adapters/http`, 직접 TanStack Query client import, `application → adapter 구체`, `domain → React/browser global` 등 — 등급: `planned`
|
||||
- **`test fixtures` 행(hub §4.3 row 6)의 import 경계 규칙 + 짝 fixture** — `tests/**` 는 public contract + 명시 test helper 만 import 가능, production secret 모듈·real telemetry endpoint 설정 import 는 fail (D6; 지금까지 owner 미지정이던 행) — 등급: `planned`
|
||||
- dependency/enforcement **report artifact** 를 `artifacts/quality/` 로 emit + 위반 시 non-zero exit(warning 강등 금지) — 등급: `planned`
|
||||
- gate pass 조건: allowed fixture pass · forbidden fixture fail · report emitted (`FE-GATE-010` — §20 Measurable completion) — 등급: `planned`
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외 — 다른 owner branch 가 소유. 여기서 detail 을 정하지 않고 그 branch 를 가리킨다(CLAUDE.md §15.5 R3, `OUT_OF_BRANCH_SCOPE`).
|
||||
|
||||
- **allowed-import matrix 의 *정의* 자체 + layer/port 책임 분해** → [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] 소유 (`FE-OC-002`). 정의 근거 결정은 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §3.2 `FE-D009`~`FE-D011`. 본 branch 는 그 matrix 를 *기계화*할 뿐 정의하지 않는다.
|
||||
- **gate 정의(blocking scope·pass condition·evidence artifact)** → hub §15.1 소유, gate 별 Owner 는 hub §2.1.1. **test level 슬롯 · artifact 보존 정책 · "실패→warning 금지" 정책** → [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] 소유 (`FE-OC-020`). **CI wiring** → [[raw/branch-notes/feature-frontend-ci-quality-gates-contract]] 가 orchestration.
|
||||
- **forbidden-API(browser-global) lint** (`window`/`localStorage`/`fetch` 직접 사용 금지 — cross-layer import 금지와 별개) → [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] 소유 (`FE-OC-019` 가 `FE-GATE-002` lint 의 forbidden-API 부분). 본 branch 는 forbidden-**import**/layer 부분만.
|
||||
- **checkJs/type 강제** (`FE-GATE-003`) → [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] 소유 (`FE-OC-003`).
|
||||
- **ESLint / dependency-cruiser 의 *설치* 와 base flat-config substrate** (`eslint.config.js`·`.dependency-cruiser.cjs` 파일 자체의 존재·engine·script wiring) → [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] 소유 (`FE-OC-003`). 본 branch 는 그 config 에 **규칙을 추가**할 뿐 toolchain 을 세우지 않는다.
|
||||
- **QueryCachePort 설계** (`FE-D006`) → [[raw/branch-notes/feature-server-state-caching-contract]]. 본 branch 는 TanStack import 경계만 강제.
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | §4.3 dependency matrix + §15.1 `FE-GATE-010` + §15.2 negative fixture — 본 branch 강제 명세의 SSOT (D1·D2·D3·D5) |
|
||||
| [[raw/project-notes/ca-skeleton-operational-contract]] | backend Clean-Architecture 운영 계약 철학(왜 port 를 application 이 소유하고 adapter 가 구현하는가, 왜 layer 를 강제하는가) — `FE-D009` 의 rationale, D2 가 기계화하는 대상 |
|
||||
| [[raw/official-docs/tanstack-query-server-state-official]] | `TSQ-C1`(server-state 전용 라이브러리로 자기 정의) — D4 의 "직접 TanStack import 금지" fixture 근거(TanStack 은 `QueryCachePort` 뒤에 격리) |
|
||||
| [[raw/official-docs/vite-build-tool-official]] | `VITE-C1`(native ES modules 위에서 동작) — dependency-cruiser·ESLint 가 분석하는 ESM import 그래프의 substrate(`FE-D002` JS ESM / `FE-D003` Vite baseline) — D1 도구 적용 가능성의 맥락 근거 |
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] dependency-cruiser 설정으로 §4.3 matrix + forbidden-edge 규칙 encoding — 등급: `planned`
|
||||
- [ ] ESLint flat-config restricted-import 규칙을 matrix 와 1:1 mirror — 등급: `planned`
|
||||
- [ ] allowed + forbidden fixture suite 작성 (`presentation→adapters/http`, 직접 TanStack import, `application→adapter 구체`, `domain→React`) — 등급: `planned`
|
||||
- [ ] dependency/enforcement report 를 `artifacts/quality/` 로 emit + 위반 시 non-zero exit wiring — 등급: `planned`
|
||||
- [ ] `test fixtures` 행(D6) 규칙 encoding + allowed/forbidden fixture 쌍 작성 — 등급: `planned`
|
||||
- [ ] `FE-OC-019`(production secret 목록)·`FE-OC-014`(real telemetry endpoint 목록) owner 에게 금지 대상 모듈 목록 발행 요청 — 미발행 동안 D6 fixture 는 placeholder — 등급: `planned`
|
||||
- [ ] 규칙 catalog 를 layering branch 의 allowed-import matrix 와 cross-check(drift 방지) — 등급: `planned`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
없음 — `/branch-spec` 채움 단계. 모든 항목 `planned`(frontend repo 미생성).
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> 각 결정의 근거·대안은 아래 Decision Evidence Map 과 1:1. 여기 prose 는 그 요약이다.
|
||||
|
||||
- 2026-07-18: **이중 도구 강제(dependency-cruiser 그래프 + ESLint restricted-import), 둘 다 merge-blocking** / 이유: import-statement 레벨(빠름·에디터 내)과 whole-graph reachability(transitive/barrel re-export 포착)를 함께 커버 / 대안: 단일 도구 / 근거: hub §4.3 "Planned enforcement" 열이 두 도구를 명시, `FE-OC-002`. (D1)
|
||||
- 2026-07-18: **§4.3 dependency matrix 를 규칙의 single source-of-truth 로 강제** (`domain ← application ← presentation`; adapter 는 application port 구현; bootstrap 만 composition root) / 이유: `FE-OC-002` owner 가 정의한 경계를 코드로 집행 / 대안: N/A(matrix 는 layering branch 소유) / 근거: hub §4.3 + §3.2 결정. (D2)
|
||||
- 2026-07-18: **forbidden fixture 는 반드시 fail, allowed fixture 는 반드시 pass — 실행된 실패 fixture 없는 규칙은 증거 불충분** / 이유: gate 가 실제로 동작함을 증명하려면 deliberately failing fixture 필요 / 대안: rule 존재만 확인 / 근거: hub §15.2 + §15.1 `FE-GATE-010` pass 조건. (D3)
|
||||
- 2026-07-18: **"직접 TanStack Query client import" forbidden fixture — `adapters/query-cache` 만 TanStack import 허용, presentation/application 직접 import 은 fail** / 이유: `QueryCachePort`(application-owned) 뒤로 TanStack 격리 / 대안: 전역 허용 / 근거: hub §15.1 `FE-GATE-010`("including direct TanStack client import") + §3.2 결정 + `TSQ-C1`. (D4)
|
||||
- 2026-07-18: **machine-readable dependency/enforcement report 를 `artifacts/quality/` 로 emit, 위반은 warning 으로 강등 금지** / 이유: gate 가 "실행됐다" 인정받으려면 evidence artifact 필요 / 대안: 콘솔 출력만 / 근거: hub §15.1 `FE-GATE-010` evidence artifact + §4.6 blueprint + `FE-OC-020`. (D5)
|
||||
- 2026-07-20: **hub §4.3 `test fixtures` 행(6번째)의 import 경계 규칙 + 짝 fixture 를 본 branch 가 소유** — `tests/**` 는 public contract + 명시 test helper 만 import 가능, production secret 모듈·real telemetry endpoint 설정 import 는 fail / 이유: §4.3 matrix 의 한 행이고 그 matrix 기계화가 본 branch 책임(`FE-GATE-010`)인데 지금까지 어떤 branch 도 owner 로 잡지 않아 owner-less 였음 / 대안: browser-security(`FE-OC-019`) 또는 observability(`FE-OC-014`)에 전부 위임 — 그러나 두 branch 는 *무엇이 secret/endpoint 인가* 를 정의할 뿐 import 그래프 규칙을 집행하지 않으므로 부적합 / 근거: hub §4.3 row 6 (`test config guard`) + `FE-OC-002`. (D6)
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
> 각 결정과 raw source Claim ID 의 연결. `Decision ID` 는 이 branch-note 안에서 안정적으로 유지한다.
|
||||
> `Supporting Claims`: hub 결정(project decision)은 `[[hub]] §·FE-D` 로, 외부 스펙은 `raw/official-docs/<slug>.md#<CLAIM>` 로 가리킨다. (`FE-D*` 는 hub §3.2 소유 — 본 branch 는 그 결정을 *기계화*한다.)
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | 이중 도구 강제: dependency-cruiser(그래프 reachability) + ESLint restricted-import(module 레벨), 둘 다 merge-blocking (`FE-GATE-010`/`FE-GATE-002` → `FE-OC-002`) | **이 결정:** 경계를 import-statement 레벨 *과* whole-graph 레벨 *양쪽*에서 강제해야 할 때(transitive/indirect 위반은 ESLint 단독으로 못 잡음). **대안(단일 도구):** 한 도구가 완전히 redundant 임이 fixture 로 증명될 때 → revisit | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §4.3 "Planned enforcement" 열 + §15.1 `FE-GATE-010`·`FE-GATE-002`; `raw/official-docs/vite-build-tool-official.md#VITE-C1` (ESM 그래프 substrate) | `project-decision` (+contextual official-doc) | hub 는 *도구* 만 명시, 정확한 rule config 는 미명시 → 규칙 상세는 `UNSUPPORTED_IMPL_DECISION` |
|
||||
| D2 | §4.3 dependency matrix 를 규칙의 SSOT 로 강제 (`domain ← application ← presentation`; adapter 는 application port 구현; bootstrap 만 composition root) | **N/A** — matrix 는 `FE-OC-002` owner(layering branch)가 고정. 본 branch 는 기계화만. layer taxonomy 가 바뀌면(FSD fork 승인) 규칙 재생성 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §4.3 dependency matrix + §4.2 responsibility + §3.2 `FE-D009`·`FE-D010`·`FE-D011`; 철학 근거 [[raw/project-notes/ca-skeleton-operational-contract]] | `project-decision` (delegated from layering branch) | layering branch 의 concrete allowed-import matrix 발행에 의존 — 그것이 바뀌면 규칙 drift (§엣지·의존 참조) |
|
||||
| D3 | forbidden fixture 는 MUST fail, allowed fixture 는 MUST pass — 실행된 실패 fixture 없는 규칙은 증거 불충분 | **N/A(invariant)** — canonical negative fixture = `presentation` imports `adapters/http` (§15.2). rule 존재만 확인한 결과는 `locally-verified` 증거로 불충분 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.2 ("최소 하나의 deliberately failing fixture 필요") + §15.1 `FE-GATE-010` pass 조건("allowed passes, forbidden fails") + §20 Measurable completion | `project-decision` (hub §15.1/§15.2) | fixture set 이 rule set 과 동기 유지돼야 함 — 짝 fixture 없이 rule 추가 시 gate 조용히 degrade |
|
||||
| D4 | "직접 TanStack Query client import" forbidden fixture: `adapters/query-cache` 만 import 허용, presentation/application 직접 import 은 fail | **이 결정:** `QueryCachePort` 뒤에 TanStack 을 격리하는 동안 유지. **대안:** 그 경계 결정 변경(offline-first normalized cache) 시 재검토 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.1 `FE-GATE-010` ("including direct TanStack client import") + §3.2 `FE-D006`; `raw/official-docs/tanstack-query-server-state-official.md#TSQ-C1` | `official-doc` (`TSQ-C1`) + `project-decision` (`FE-D006`) | 금지할 정확한 import specifier(`@tanstack/react-query`)는 hub 미명시 → `UNSUPPORTED_IMPL_DECISION` |
|
||||
| D5 | machine-readable dependency/enforcement report 를 `artifacts/quality/` 로 emit, 위반은 warning 강등 금지, blocking scope=merge | **N/A** — artifact 없으면 gate 가 "실행됨" 으로 인정 안 됨. report format/보존은 test-taxonomy branch(`FE-OC-020`)에 위임 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.1 `FE-GATE-010` evidence artifact("dependency report") + §4.6 blueprint(`artifacts/quality/`) + §2.1 `FE-OC-020` ("실패를 warning 으로 낮추면 안 됨") | `project-decision` (hub §15.1 + `FE-OC-020`) | 정확한 report filename/format 은 `UNSUPPORTED_IMPL_DECISION`; 보존 정책은 test-taxonomy/CI branch 소유 |
|
||||
| D6 | hub §4.3 `test fixtures` 행의 import 경계 규칙 + 짝 fixture 를 본 branch 가 소유: `tests/**` 는 public contract + 명시 test helper 만 import 가능, production secret 모듈·real telemetry endpoint 설정 import 는 MUST fail (`test config guard` → `FE-GATE-010`) | **이 결정:** §4.3 matrix 의 행이고 집행 수단이 import 그래프 규칙인 동안(= 정적 분석으로 판정 가능한 동안) 본 branch 소유. **대안(위임):** 집행이 런타임 값 검사나 secret scanning 으로 바뀌면 `FE-GATE-013` security gate 소유로 이관 → revisit | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §4.3 dependency matrix row 6(`test fixtures` \| allowed: public contracts and explicit test helpers \| forbidden: production secret, real telemetry endpoint \| enforcement: `test config guard`) + §15.1 `FE-GATE-010`(forbidden import fixtures) + §2.1 `FE-OC-002` | `project-decision` (hub §4.3 row 6) | *무엇이* production secret / real telemetry endpoint 인가의 목록은 `FE-OC-019`·`FE-OC-014` owner 미발행 → 발행 전까지 fixture 대상 모듈이 placeholder. 식별 메커니즘(경로 기반)은 `UNSUPPORTED_IMPL_DECISION` |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> 전부 `planned` blueprint — frontend repo 미생성. 경로는 hub §4.6 Planned directory blueprint + §5.1 registry owner map 에서 유래(grounded)하나 코드는 없다. 3-rule(R1 Trace / R2 UNSUPPORTED_IMPL_DECISION / R3 no OUT_OF_BRANCH_SCOPE) 준수.
|
||||
|
||||
### 1. 강제 도구 wiring (dependency-cruiser + ESLint)
|
||||
|
||||
> **Trace**: D1 (hub §4.3 "Planned enforcement", `FE-OC-002`) + D2.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**:
|
||||
> - dependency-cruiser 설정 파일명/형식(`.dependency-cruiser.cjs` 가정) — hub 는 *도구* 만 명시, 파일명 미권고. Trade-off: `.cjs` 는 dependency-cruiser `--init` 의 문서화된 기본 출력.
|
||||
> - ESLint 규칙 선택(`import/no-restricted-paths`(eslint-plugin-import) vs 빌트인 `no-restricted-imports`) — hub 미권고. Trade-off: `import/no-restricted-paths` 가 zone→zone 금지를 직접 표현해 matrix 대응이 명확; `no-restricted-imports` 는 빌트인이나 pattern 기반. 둘 다 동일 matrix 를 encoding — 최종 선택은 first-impl 로 유예.
|
||||
|
||||
| 도구 | 역할(무엇을 잡나) | planned 위치 | 근거 |
|
||||
|---|---|---|---|
|
||||
| dependency-cruiser | whole-graph reachability — transitive/indirect/barrel re-export 를 통한 layer 위반 | `.dependency-cruiser.cjs` (repo root) | hub §4.3 "Planned enforcement" 열 |
|
||||
| ESLint (flat config) | import-statement 레벨 즉시 위반 + 에디터 피드백 | `eslint.config.js` restricted-import 블록 | hub §4.3; `FE-GATE-002` lint |
|
||||
|
||||
### 2. Layer boundary 규칙 catalog (matrix 의 기계화)
|
||||
|
||||
> **Trace**: D2 (hub §4.3 dependency matrix; §3.2 결정 `FE-D009`·`FE-D010`·`FE-D011`; `FE-OC-002`) + D6 (hub §4.3 `test fixtures` 행).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: glob 경로 패턴(`src/domain/**` 등)의 정확한 문법 — §4.6 blueprint 는 디렉토리 *이름* 만 주고 glob 은 미명시. Trade-off: blueprint 디렉토리명을 그대로 `src/<layer>/**` glob 으로 승격(가장 단순한 1:1 매핑).
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: `test fixtures` 행의 glob(`tests/**`) — §4.6 blueprint 는 `tests/{unit,component,integration,e2e}` 만 주고 fixture glob 을 미명시. Trade-off: blueprint 의 `tests/` 루트를 그대로 승격해 4개 레벨을 한 번에 덮음(레벨별 분기 없이 가장 단순).
|
||||
|
||||
**집행 유형** 열은 hub §4.3 `Planned enforcement` 열의 각 항목이 *자동 규칙*(gate 가 exit code 로 판정)인지 *수동/자동화 밖*(사람 리뷰)인지 구분한다 — hub 는 두 종류를 한 열에 섞어 적고 구분하지 않으므로, `FE-GATE-010` 의 forbidden-fixture 범위가 어디까지인지 여기서 명시한다.
|
||||
|
||||
| From (source) | MUST NOT import (금지 대상) | 집행 도구(§4.3) | 집행 유형 | `FE-GATE-010` fixture 범위 | planned glob |
|
||||
|---|---|---|---|---|---|
|
||||
| `domain` | application, presentation, adapters, bootstrap, React, browser globals | dependency-cruiser + ESLint restricted imports | **자동 규칙** | 포함 | `src/domain/**` |
|
||||
| `application` | presentation, adapters 구체, bootstrap, React, `window`/`localStorage`/`fetch` | architecture fixture | **자동 규칙** | 포함 | `src/application/**` |
|
||||
| `presentation` | adapters, raw DTO schema, registry storage 구현 | restricted import rule | **자동 규칙** | 포함 | `src/presentation/**` |
|
||||
| `adapters/*` | presentation, bootstrap internals, 다른 adapter 구체 구현 | dependency graph snapshot | **자동 규칙** | 포함 | `src/adapters/**` |
|
||||
| `bootstrap` | page-specific business rule | composition-root review | **수동 / 자동화 밖** | **제외** (아래 주석) | `src/bootstrap/**` |
|
||||
| `test fixtures` | production secret, real telemetry endpoint (허용: public contract + 명시 test helper) | test config guard | **자동 규칙** (import 경계 부분만) | 포함 (D6) | `tests/**` |
|
||||
|
||||
> **`bootstrap` 행이 `FE-GATE-010` forbidden-fixture 범위 밖인 이유**: hub §4.3 이 이 행에만 `composition-root review`(사람 리뷰)를 배정했고, 금지 대상이 "page-specific business rule" 이라는 *의미론적* 판정이라 import specifier 로 표현되지 않는다 — 어떤 모듈을 import 했는가가 아니라 그 모듈 안에 무엇을 썼는가의 문제다. 따라서 짝 forbidden fixture 를 만들 수 없고, D3 의 "모든 규칙은 짝 fixture 필요" 불변식은 이 행에 적용되지 않는다. `FE-GATE-010` pass 조건은 나머지 5개 행으로만 판정한다. **UNSUPPORTED_IMPL_DECISION**: bootstrap 행을 자동 gate 에서 제외한 이 판단 자체 — hub 는 "composition-root review" 라고만 적고 gate 범위 포함/제외를 명시하지 않는다. Trade-off: 기계 판정 불가한 행을 gate 에 넣으면 gate 가 항상 vacuous pass 가 되어 D3 증거 기준이 무의미해지므로, 명시적으로 제외하고 수동 리뷰 항목으로 남긴다. bootstrap 의 business-rule 혼입은 코드 리뷰 체크리스트로 다루며, 그 체크리스트 소유는 [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] (`FE-OC-002`).
|
||||
|
||||
> `application → adapter 구체` 는 `MUST NOT`; output port 정의는 `application` 이 `MUST` 소유; adapter 는 application 이름을 알면 안 됨 (hub §4.3 normative summary — D2).
|
||||
> `application` 의 browser-global 직접 사용(`window`/`localStorage`/`fetch`) 금지 중 **browser-API 표면 자체의 금지 규칙 카탈로그**는 `FE-OC-019` 소유 → 여기선 layer-cross import 관점만, API 표면 detail 은 [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] 로 위임(R3).
|
||||
>
|
||||
> **`test fixtures` 행 (D6) — 본 branch 가 소유**: hub §4.3 의 6번째 행은 지금까지 어떤 branch 도 owner 로 잡지 않았다. 이 행은 §4.3 dependency matrix 의 일부이고 그 matrix 의 *기계화* 가 본 branch 의 정의된 책임(`FE-GATE-010`)이므로, **test 코드에서의 import 경계 규칙 + 짝 fixture 는 본 branch 가 소유**한다. 규칙: `tests/**` 는 public contract(`src/contracts/**`)와 명시 test helper 만 import 할 수 있고, production secret 모듈과 real telemetry endpoint 설정은 import 할 수 없다. 즉 다른 layer 행과 동일한 종류의 forbidden-import 규칙으로 encoding 되며 `FE-GATE-010` 의 allowed/forbidden fixture 쌍을 갖는다.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: "production secret" 을 test config 에서 *어떻게 식별* 하는가(모듈 경로 기반 vs 환경변수 이름 패턴 vs secret registry 조회) — hub §4.3 은 금지 *대상* 만 적고 식별 메커니즘을 권고하지 않는다. Trade-off: 본 branch 는 정적 import 그래프만 볼 수 있으므로 **모듈 경로 기반**(secret 을 노출하는 모듈로 향하는 import edge 금지)으로 좁힌다 — 런타임 값 검사는 정적 분석 밖이고 `FE-GATE-013` security scan 영역이다.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: rule id / 규칙 이름 — hub 미명시. Trade-off: §2 의 다른 5개 행과 같은 rule 계열(zone→zone 금지)로 표현해 catalog 일관성을 유지하고, 별도 rule 계열을 만들지 않는다.
|
||||
> - **위임(reference-only)**: *무엇이* production secret 인가의 정의(어떤 값·어떤 모듈이 secret 인가)는 [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] (`FE-OC-019`, browser bundle 에 secret 금지) 소유이고, *무엇이* real telemetry endpoint 인가는 [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] (`FE-OC-014`) 소유다. 본 branch 는 그 두 owner 가 발행하는 목록을 **입력으로 받아 import 금지 규칙으로 encoding** 할 뿐 정의하지 않는다(R3). 두 목록 중 하나라도 미발행이면 본 행의 fixture 는 placeholder 대상 모듈로만 검증되고 상태는 `needs-confirmation` 이다.
|
||||
|
||||
### 3. Fixture set (allowed + forbidden)
|
||||
|
||||
> **Trace**: D3 (hub §15.2 + §15.1 `FE-GATE-010`) + D4 (hub §15.1 + `FE-D006` + `TSQ-C1`) + D6 (hub §4.3 `test fixtures` 행).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: fixture 파일 배치(`tests/architecture/fixtures/…` 가정) — hub §4.6 는 `tests/` 하위 레벨(unit/component/integration/e2e)만 주고 architecture-fixture subfolder 미명시. Trade-off: `tests/` 아래 전용 architecture 서브트리로 colocate(다른 gate fixture 와 동일 관례).
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 금지할 TanStack import specifier(`@tanstack/react-query`) — hub 는 "direct TanStack client import" 라고만 표현, 패키지명 미명시. Trade-off: TanStack Query 의 표준 React 엔트리 패키지명을 사용, 확정은 first-impl.
|
||||
|
||||
| Fixture | 종류 | 기대 결과 | 근거 |
|
||||
|---|---|---|---|
|
||||
| `presentation` imports `adapters/http` | forbidden | MUST fail | hub §15.2 canonical negative fixture |
|
||||
| presentation/application imports `@tanstack/react-query` 직접 | forbidden | MUST fail | hub §15.1 `FE-GATE-010`; `FE-D006`; `TSQ-C1` |
|
||||
| `application` imports adapter 구체 | forbidden | MUST fail | hub §4.3 normative summary |
|
||||
| `domain` imports React/browser global | forbidden | MUST fail | hub §4.2/§4.3 |
|
||||
| `presentation` imports application facade | allowed | MUST pass | hub §4.3 (presentation → application facade) |
|
||||
| `adapters/query-cache` imports `@tanstack/react-query` | allowed | MUST pass | hub §4.2 (`adapters/query-cache` consumes TanStack Query) |
|
||||
| test fixture imports public contract + 명시 test helper | allowed | MUST pass (false-positive 방지) | hub §4.3 `test fixtures` 행 (D6) |
|
||||
| test helper imports production secret 모듈 | forbidden | MUST fail | hub §4.3 `test fixtures` 행 forbidden 열 (D6); secret 목록 소유 `FE-OC-019` |
|
||||
| test helper imports real telemetry endpoint 설정 | forbidden | MUST fail | hub §4.3 `test fixtures` 행 forbidden 열 (D6); endpoint 목록 소유 `FE-OC-014` |
|
||||
|
||||
> gate 를 CI 에 배선하고 artifact 를 보존하는 workflow(YAML/retention)는 본 branch 범위 밖 → [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] + [[raw/branch-notes/feature-frontend-ci-quality-gates-contract]] (R3).
|
||||
|
||||
### 4. Report artifact 산출 + 위반 시 exit 정책
|
||||
|
||||
> **Trace**: D5 (hub §15.1 `FE-GATE-010` evidence "dependency report" + §4.6 `artifacts/quality/` + `FE-OC-020`).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: report 파일명/형식(`json` vs `html`/`dot`) — hub 미명시. Trade-off: gate 파싱용 machine-readable(`json`) 을 primary 로, 선택적 `dot`/`svg` 를 human review 용으로 병행.
|
||||
|
||||
- dependency-cruiser 가 그래프 report 를 `artifacts/quality/` 로 emit(§4.6 blueprint).
|
||||
- forbidden fixture 가 pass 하거나 allowed fixture 가 fail 하면 **non-zero exit** → hub §15.1 `FE-GATE-010@1` 의 pass 조건에 매핑(조건 원문은 §15.1 소유). warning 강등 금지(`FE-OC-020`).
|
||||
- report 형식/보존 기간의 최종 계약은 test-taxonomy branch(`FE-OC-020`)에 위임(R3).
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- *Rule false-negative (transitive/barrel):* `presentation → shared/index.js → adapters/http` 처럼 barrel re-export 로 우회하면 ESLint 단독은 놓칠 수 있음 → dependency-cruiser 그래프가 잡아야 함(이것이 D1 이중 도구의 이유). 검증 필요.
|
||||
- *Rule false-positive:* test helper / shared UI primitive 가 layer 를 가로질러 import 하는 정당 케이스 → §4.3 `test fixtures` 행(public contract + 명시 helper 허용)으로 scope-out 필요. over-match 시 정상 코드 block. 이 행의 allowed/forbidden 규칙은 D6 으로 본 branch 가 소유한다.
|
||||
- *정적 분석 한계:* `import()` 동적 import 로 우회하면 두 도구 모두 정적 그래프에서 못 볼 수 있음 → 잔여 위험으로 기록, `needs-confirmation`.
|
||||
- *규칙-fixture 비동기:* rule 추가 시 짝 forbidden fixture 미추가 → gate 가 조용히 약화(D3 Open Risk).
|
||||
- **다른 계약 의존**:
|
||||
- [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] 의 allowed-import matrix(`FE-OC-002`)에 의존 — 그 matrix 가 본 branch 규칙의 입력. 바뀌면 규칙 재생성(D2).
|
||||
- hub §15.1·§2.1.1 의 `FE-GATE-010@1` 정의(Owner = 본 branch)와 [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] 의 test level 슬롯 + artifact 보존 + "실패→warning 금지" 정책(`FE-OC-020`)에 의존 — report 소비처.
|
||||
- [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] 의 toolchain 설치 + base flat-config(`FE-OC-003`)에 의존 — D1 은 `eslint.config.js`·`.dependency-cruiser.cjs` 와 그 실행 script 가 *이미 존재*함을 전제하고 규칙만 추가한다. 그 branch 가 lint runner/flat-config 형식(또는 package manager script 이름)을 바꾸면 본 branch 의 규칙 블록 배치·실행 진입점이 함께 바뀐다.
|
||||
- [[raw/branch-notes/feature-server-state-caching-contract]] (QueryCachePort 경계 owner)에 의존 — hub [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §3.2 `FE-D006` 이 D4 TanStack import 금지 fixture 의 근거. 그 결정 변경 시 fixture 재정의.
|
||||
- hub §4.6 Planned directory blueprint 에 의존 — glob 경로가 디렉토리 layout 을 전제. layout 변경 시 glob 갱신(`FE-D009` 변경 절차).
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| dependency-cruiser + ESLint 가 §4.3 모든 forbidden edge 를 함께 포착 | 도구별 blind spot(동적 import, barrel re-export) | forbidden fixture(직접+transitive+dynamic-import 케이스) 실행 → 각각 fail 확인 (`FE-GATE-010` "forbidden fails") | `needs-confirmation` |
|
||||
| allowed fixture 가 false-positive 0 으로 pass | 규칙이 test helper/shared primitive 를 over-match 할 수 있음 | allowed fixture(presentation→facade, adapter→TanStack, test-helper cross-import) 실행 → pass 확인 | `needs-confirmation` |
|
||||
| 직접 TanStack import 금지가 presentation/application 에서만 발화, `adapters/query-cache` 는 예외 | 패키지명 기반 금지는 mis-scope 위험 | forbidden: presentation imports `@tanstack/react-query` → fail; allowed: `adapters/query-cache` import → pass | `needs-confirmation` |
|
||||
| report artifact 가 `artifacts/quality/` 로 emit 되고 위반 시 gate 가 fail(warning 강등 없음) | artifact wiring + CI exit code 미검증 | seeded 위반으로 gate 실행 → non-zero exit + report 파일 존재 확인 | `needs-confirmation` |
|
||||
| `tests/**` 가 production secret 모듈·real telemetry endpoint 설정을 import 하면 gate 가 fail (D6) | 금지 대상 모듈 목록이 `FE-OC-019`·`FE-OC-014` owner 미발행 상태 — 현재는 placeholder 경로로만 규칙 표현 가능 | 두 owner 발행 후 실제 경로로 forbidden fixture 실행 → fail 확인; allowed(public contract + test helper) fixture → pass 확인 | `needs-confirmation` |
|
||||
| 규칙 catalog 가 layering branch allowed-import matrix 와 동기 유지 | matrix 가 외부 소유라 drift 가능 | 변경마다 규칙 catalog vs `FE-OC-002` owner 발행 matrix cross-check | `needs-confirmation` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
> `/coverage`가 채우는 생성물이며 손으로 유지하지 않는다.
|
||||
|
||||
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| TODO — `/coverage` 실행 전 | missing | (없음) | 미평가 | TODO |
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
없음 — `/branch-spec` 채움 단계.
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-GATE-013@1` | [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] | secret·vulnerability·license·dependency review 정책 위반이면 merge·release 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-OC-002@1` | [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] | `domain <- application <- presentation` 의존 방향과 application-owned output port를 MUST 지킴 | import 참조로 적용 |
|
||||
| `FE-OC-014@1` | [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] | telemetry는 best-effort이며 render·API success를 차단하면 안 되고 PII·token을 전송하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-019@1` | [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] | browser bundle에 secret을 넣지 않고 untrusted HTML injection을 기본 금지 | import 참조로 적용 |
|
||||
| `FE-OC-020@1` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | gate 종류별 책임·fixture·artifact를 분리하고 실패를 warning으로 낮추면 안 됨 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
### Sub-branches (세부 작업)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 오류 기록 (이 branch 작업 중 발생)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 강의 (이 작업을 위해 학습한 강의)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### job-posting tie-ins (이 작업에서 파생된 글감)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크:
|
||||
- 리뷰 메모:
|
||||
- 머지 결과 / 배포 환경:
|
||||
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
|
||||
- `actually-implemented` 항목:
|
||||
- `locally-verified` 항목:
|
||||
- `prod-verified` 항목:
|
||||
- **추출하지 않을 항목** (planned / documented-only / abandoned):
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
---
|
||||
title: branch / feature-frontend-auth-session-integration-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-008
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-008
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-AUTH-BOUNDARY-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-PORT-OWNERSHIP-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-002]
|
||||
contract_packet: 1
|
||||
branch: feature-frontend-auth-session-integration-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract.md]
|
||||
tags: [branch, ca-skeleton, frontend, auth, security, javascript, oauth2]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
contract_packet_sha256: ec06d05939cbbe7c12c1a581115a07830894c4328344e6a5053c2162df756ab0
|
||||
imports: [FE-OC-002@1, FE-OC-005@1, FE-OC-006@1, FE-OC-008@1, FE-OC-019@1]
|
||||
|
||||
---
|
||||
|
||||
# branch: feature-frontend-auth-session-integration-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — 단일 브랜치의 TODO·결정·진행 기록. 머지/종료 후 verified 결과는 `/ingest`로 `wiki/projects/`에 추출한다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: AuthSessionPort·bounded 401 replay와 token lifecycle import 금지가 test로 고정된다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-AUTH-BOUNDARY-001@1` | auth lifecycle은 외부 owner가 소유하고 skeleton은 AuthSessionPort만 소비한다 | token lifecycle 비소유와 bounded session recovery 경계에 적용 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-PORT-OWNERSHIP-001@1` | output port interface는 application이 소유하고 adapter가 구현한다 | application-owned AuthSessionPort와 외부 auth adapter 경계에 적용 | [[raw/project-notes/ca-skeleton-frontend-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 -->
|
||||
## 목표
|
||||
|
||||
이 브랜치는 hub의 `FE-OC-010`("skeleton은 session state를 소비하되 token lifecycle을 MUST NOT 소유")을 구현 착수 가능한 명세로 내린다. 구체적으로 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D017`(auth lifecycle은 외부 owner, skeleton은 `AuthSessionPort`만 소비)을 근거로, application이 소유하는 `AuthSessionPort` 경계 · bounded 401 recovery state machine · recovery replay policy · auth 실패 정규화 · no-token-lifecycle 불변식을 `planned` 청사진으로 확정한다. 또한 이 경계가 라우팅·API client·browser security에 닿는 지점(`FE-OC-005`·`FE-OC-006`·`FE-OC-019`)에 대해 "무엇을 기여하고 무엇을 다른 owner 브랜치에 위임하는지"를 못박는다. token 발급/저장/refresh/rotation/logout은 외부 auth owner(Keycloak 등, [[raw/project-notes/keycloak-patterns-overview]])가 소유하므로 이 노트는 그것을 *명명만* 하고 명세하지 않는다. 현재 frontend 구현 코드가 없어 모든 항목은 `planned`다.
|
||||
|
||||
- 이슈:
|
||||
- PR:
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- `AuthSessionPort`(application 소유 integration boundary port)의 소비 계약: opaque session state 읽기 + request 전 `attach(request)` + unauthenticated transition 통지 콜백 (`FE-OC-010`).
|
||||
- Bounded 401 recovery state machine: `authenticated → recovery-pending → {authenticated | unauthenticated | integration-failed}`, logical request당 recovery callback 최대 1회, 두 번째 401은 terminal (hub §7.8).
|
||||
- Recovery 이후 replay policy: `safe` 1회 replay / `keyed` mutation은 stable idempotency key + backend replay contract일 때만 1회 / `none`(unkeyed) mutation은 replay 금지 (hub §7.8·§7.7).
|
||||
- Auth 실패 정규화 기여: `401→AUTH_REQUIRED`, `403→FORBIDDEN`, attach/recovery adapter fault→`AUTH_INTEGRATION_FAILURE` (hub §8.2·§8.5).
|
||||
- no-token-lifecycle 불변식: skeleton은 token/secret을 browser storage·bundle·env·telemetry·error body에 저장/노출하지 않는다 (`FE-OC-019` 기여, hub §5.5·§6.1).
|
||||
- session-required route가 `AuthSessionPort` state를 UX hint로만 소비하고 backend authorization을 최종 판단으로 두는 규칙 (`FE-OC-005` 기여, hub §9.3).
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외 — 외부 auth owner 또는 다른 owner 브랜치 소유. 여기서는 *명명*만 하고 명세하지 않는다.
|
||||
|
||||
- **Token lifecycle 전체** — authorization code exchange, token 저장 위치, access token refresh, refresh token rotation, logout propagation, revocation, IdP redirect detail, backend permission decision. 외부 auth owner 소유([[raw/project-notes/keycloak-patterns-overview]], hub §7.8 "Skeleton does not own").
|
||||
- **Route registry / navigation guard 메커니즘** (route ID/path/param validation/404/redirect-loop) — [[raw/branch-notes/feature-routing-navigation-guard-contract]] 의 `FE-OC-005` 소유. 나는 session-state hint 소비만 기여.
|
||||
- **Shared HTTP client transport 및 timeout/abort/retry algorithm** — [[raw/branch-notes/feature-api-client-response-envelope-contract]] 의 `FE-OC-006`·`FE-OC-009` 소유. attach·정규화는 그 client 안에서 실행되나 client 뼈대는 그 브랜치가 소유.
|
||||
- **Error registry 구조 및 total-function 정규화** — [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] 의 `FE-OC-008` 소유. 나는 3개 auth kind의 기대 매핑만 기여.
|
||||
- **Storage registry 스키마** — [[raw/branch-notes/feature-frontend-storage-registry-contract]] 의 `FE-OC-013` 소유. `AUTH_TOKEN` forbidden 행의 불변식만 기여.
|
||||
- **CSP/header/secret-scan browser boundary 메커니즘** — [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] 의 `FE-OC-019` 소유. 나는 no-token-storage 불변식만 기여.
|
||||
- **Telemetry redaction 인프라** — [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] 의 `FE-OC-014` 소유. auth 이벤트의 forbidden attribute(token/principal) 규칙만 기여.
|
||||
- **Architecture import-lint 강제 메커니즘** — [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]] 의 `FE-OC-002` 소유. 나는 "token lifecycle 심볼은 auth adapter 밖에서 import 금지"라는 *검사 대상 불변식*만 정의.
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `FE-D017`/`FE-OC-010` + §4.4(Port ownership) · §7.8(Auth integration boundary) · §8.2(failure matrix) · §5.5·§6.1(browser security) · §9.3(route behavior) — D1~D7의 1차 근거(project decision SSOT) |
|
||||
| [[raw/project-notes/keycloak-patterns-overview]] | 외부 auth owner가 token 발급·저장·refresh·rotation·logout을 소유한다는 external-owner context(§3 token 종류, §2.1 token 위치·인증 강제 주체) — D1(경계 설정)·D6(token 브라우저 미저장)의 배경 근거 |
|
||||
| [[raw/official-docs/react-router-official]] | route composition(`<Routes>`/`<Route>`, nested `<Outlet/>`; `REACT-ROUTER-C1`/`C2`)이 session-required route surface가 얹히는 라우팅 모드에 부합 — D7의 라우팅 context. ⚠ navigation **guard 보안**(`REACT-ROUTER-C3` boundary)은 이 자료가 **정당화하지 않음** → guard=UX hint는 project decision(D7) |
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] `AuthSessionPort` 인터페이스 정의(application 소유): opaque session state getter + `attach(request)` header/credential callback + unauthenticated-transition callback — 등급: `planned`
|
||||
- [ ] 외부 auth adapter placeholder(`adapters/auth/`)가 port 구현, composition-root boot step 6에서 주입 — 등급: `planned`
|
||||
- [ ] shared HTTP client 경유 bounded 401 state machine + replay policy 구현 — 등급: `planned`
|
||||
- [ ] auth 실패 정규화 매핑(401/403/attach·recovery fault)을 error registry에 기여 — 등급: `planned`
|
||||
- [ ] no-token-lifecycle import test(architecture fixture): token 저장/refresh 심볼을 `adapters/auth/` 밖에서 import 시 실패 — 등급: `planned`
|
||||
- [ ] session-required route UX hint + backend authz 최종성 e2e(guarded route 403 처리) — 등급: `planned`
|
||||
- [ ] negative fixtures: attach throw/reject·recovery invalid state → `AUTH_INTEGRATION_FAILURE`; unkeyed mutation recovery → no replay — 등급: `planned`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
`/branch-spec` 자체 채움(self-map). frontend 구현 repository 미식별 → 전 항목 `planned`. hub와 6개 archived official-doc이 유일 SSOT이며, auth lifecycle 근거는 [[raw/project-notes/keycloak-patterns-overview]]. 인라인 웹 리서치 0건(hub가 이미 충분).
|
||||
|
||||
## 결정 사항
|
||||
|
||||
- 2026-07-19: auth lifecycle은 외부 owner, skeleton은 `AuthSessionPort`만 소비 / 이유: token 발급·저장·refresh·rotation·logout·revocation은 IdP·backend가 소유하는 관심사이며 client-only SPA가 이를 소유하면 보안·release 경계가 흐려짐 / 검토한 대안: skeleton이 token lifecycle을 직접 소유(독립 auth product) / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D017`·`FE-OC-010`, [[raw/project-notes/keycloak-patterns-overview]].
|
||||
- 2026-07-19: `AuthSessionPort`는 application이 정의 소유, 외부 adapter가 구현, token 문자열을 domain/application model로 반환하지 않고 opaque state + attach callback 형태 우선 / 이유: dependency inversion 유지 + token이 layer 내부로 스며들지 않게 / 검토한 대안: adapter가 직접 token을 반환해 use case가 소비 / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D010`·`FE-D009`·`FE-OC-002`·§4.4.
|
||||
- 2026-07-19: 401 recovery는 bounded state machine, logical request당 recovery 1회, 2nd 401 terminal / 이유: recovery loop 차단 / 검토한 대안: 무제한 재인증 재시도 / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §7.8·`FE-OC-006`.
|
||||
- 2026-07-19: recovery replay는 `safe` 1회 / `keyed`(+backend replay contract) 1회 / `none` 금지 / 이유: duplicate write 방지 / 검토한 대안: 성공 후 무조건 replay / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §7.8·§7.7·`FE-D016`.
|
||||
- 2026-07-19: auth 실패 정규화 `401→AUTH_REQUIRED` / `403→FORBIDDEN` / attach·recovery fault→`AUTH_INTEGRATION_FAILURE`, raw body·token 미노출 / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §8.2·§8.5·§5.6·`FE-OC-008`.
|
||||
- 2026-07-19: token/secret은 browser storage·bundle·env·telemetry·error body에 미저장·미노출; `AUTH_TOKEN` key forbidden / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.5·§6.1·§5.8·`FE-OC-019`.
|
||||
- 2026-07-19: session-required route는 `AuthSessionPort` state를 UX hint로만 사용, backend authorization이 최종 / 이유: guard를 security control로 오해 방지 / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §9.3·§7.8·`FE-OC-005`. (react-router `REACT-ROUTER-C3`는 guard를 정당화하지 않음.)
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
> 각 결정과 raw source claim ID의 연결. `FE-D###`/`§` 참조는 hub project 링크에 붙인다(consistency hook 규약).
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | auth lifecycle은 외부 owner; skeleton은 `AuthSessionPort`만 소비하고 token 발급/저장/refresh/rotation/logout/revocation을 MUST NOT 소유 (`FE-OC-010`) | client-only SPA + 외부 auth owner가 session interface를 제공하는 한 이 결정 유지 / skeleton이 독립 auth product로 scope 변경되면 재검토 (`FE-D017` revisit trigger) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D017`·`FE-OC-010`; [[raw/project-notes/keycloak-patterns-overview]] (외부 owner가 token 종류·위치 소유) | `project-decision` | 통합 adapter owner 미정 (`FE-Q-006`); guard가 security로 오해될 위험 (`FE-RISK-005`) |
|
||||
| D2 | `AuthSessionPort`는 application이 정의 소유, 외부 adapter가 구현, token 문자열을 model로 반환하지 않고 opaque state + attach callback 형태 우선 | dependency inversion 유지(adapter가 port 구현)하는 한 유지 / port가 domain invariant 자체를 표현해야 하는 concrete case면 재검토 (`FE-D010` revisit) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D010`·`FE-D009`·`FE-OC-002` §4.4 (AuthSessionPort row) | `project-decision` | attach 구현 세부(header supplier vs opaque credential)는 auth owner 결정 — §4.4가 "구현 세부는 auth owner가 정한다"로 유보 |
|
||||
| D3 | 401 recovery는 bounded state machine(`authenticated→recovery-pending→{authenticated\|unauthenticated\|integration-failed}`), logical request당 recovery 콜백 ≤1회, 같은 request의 2nd 401은 terminal `AUTH_REQUIRED` | 외부 owner가 bounded recovery callback을 제공하면 이 machine 사용 / owner가 recovery를 안 하면 첫 401이 곧 terminal(unauthenticated). recovery loop 금지 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §7.8 (session state table)·`FE-OC-010`·`FE-OC-006` | `project-decision` | recovery callback의 timeout/bound 세부는 owner 계약에 의존 (`FE-Q-006`) |
|
||||
| D4 | recovery 성공 후 replay: `safe`=같은 context 1회 / `keyed`=stable idempotency key + active backend replay contract일 때만 1회 / `none`(unkeyed)=MUST NOT replay(명시적 재시도 요구) | idempotency mode로 분기 — backend replay contract 없으면 `keyed`도 replay 금지 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §7.8 (replay policy)·§7.7 (idempotency)·`FE-D016`·`FE-OC-006` | `project-decision` | backend replay contract 존재 여부 미확정 (`FE-Q-005`); unsafe duplicate write 위험 (`FE-RISK-006` 인접) |
|
||||
| D5 | auth 실패 정규화: `401→AUTH_REQUIRED`, `403→FORBIDDEN`, attach/recovery adapter가 throw/reject/invalid state→`AUTH_INTEGRATION_FAILURE`; raw body·token·principal은 failure·telemetry에 미포함 | 이 3 kind는 stable enum. backend가 다른 auth 상태를 쓰면 error 브랜치가 registry에 추가 후 매핑(재정의 아님) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §8.2 (failure matrix 3 auth rows)·§8.5 (negative fixtures)·§5.6 (error enum)·`FE-OC-008` | `project-decision` | error registry 구조는 error 브랜치 owner — 나는 매핑 값만 기여(경계 이탈 주의) |
|
||||
| D6 | token/secret은 browser storage·bundle·env·telemetry·error body에 저장/노출 MUST NOT; `AUTH_TOKEN` storage key는 forbidden(`sensitive-forbidden`), token 저장은 외부 auth owner만 | default off(브라우저 token storage 금지) / auth owner가 browser storage를 반드시 써야 하면 별도 threat model + owner evidence 필요(§6.1), skeleton default 아님 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.5 (`AUTH_TOKEN` forbidden)·§6.1 (secret config)·§5.8 (telemetry forbiddenAttributes)·§8.1·`FE-OC-019` | `project-decision` | XSS surface 시 token이 브라우저에 없어야 완화(keycloak note P2A 함정); CSP/scan은 browser-security 브랜치 owner |
|
||||
| D7 | session-required route는 `AuthSessionPort` state를 UX hint로만 사용, backend authorization이 최종 권한 판단 — navigation guard는 보안 control이 아님 | route access가 `session-required`/`integration-defined`일 때 hint 적용 / `public`이면 미적용. 최종 authz는 항상 backend(`403→FORBIDDEN`) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §9.3 (route behavior)·§7.8 ("guard는 UX hint")·`FE-OC-005`. (react-router `REACT-ROUTER-C3`는 guard 보안 미정당화) | `project-decision` | guard가 security로 오해 (`FE-RISK-005`) → e2e에서 403 처리 확인; route registry/redirect는 routing 브랜치 owner |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> 전 항목 `planned` — frontend 코드 없음. 경로는 hub §4.6 Planned directory blueprint / §5.1 registry owner map에서 도출된 `planned` anchor다.
|
||||
|
||||
### 1. `AuthSessionPort` 인터페이스 (application 소유)
|
||||
|
||||
> **Trace**: D1 + D2 · `FE-OC-010`/`FE-OC-002` · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §4.4 (Port ownership matrix) · §4.6 (blueprint).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 아래 메서드 명(`getSessionState`/`attach`/`onUnauthenticated`)과 "header-supplier callback" 표현은 내가 임의 선택 — hub §4.4는 "구현 세부는 auth owner가 정한다"로 shape만 유보(opaque state + attach callback, token 문자열 미반환). trade-off: §7.8의 attach·unauthenticated-transition 어휘를 거울 삼아 되묻기를 줄이되, 최종 signature는 auth owner 계약 확정 시 조정.
|
||||
|
||||
| 항목 | `planned` 값 | 근거 |
|
||||
|---|---|---|
|
||||
| Definition owner | `application` (integration boundary) | §4.4 |
|
||||
| Planned 위치 | `src/application/ports/auth-session-port.js` (정의), `src/adapters/auth/` (구현) | §4.6 |
|
||||
| Consumer | routing (session hint) + API client interceptor(attach) | §4.4 |
|
||||
| Input/Output | opaque session state / request-header attach callback (token 문자열 미반환) | §4.4 |
|
||||
| Failure vocabulary | `AuthRequired`, `AuthIntegrationFailure` | §4.4 |
|
||||
| 주입 시점 | composition-root boot step 6 (auth integration adapter 주입) | §4.5 |
|
||||
|
||||
### 2. Bounded 401 recovery state machine
|
||||
|
||||
> **Trace**: D3 · `FE-OC-006`/`FE-OC-010` · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §7.8 (session state table). 전부 hub 계약에서 도출.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — 상태·전이·행동이 §7.8 표에 명시됨. 명시돼 있다는 것은 곧 **owner 가 hub §7.8 이라는 뜻**이므로 표를 여기에 복제하지 않는다.
|
||||
|
||||
**state machine 본문은 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §7.8 소유다.** 요약 한 줄: `authenticated` 에서 첫 `401` 이 bounded recovery 를 1회만 트리거하고, 같은 logical request 의 2번째 `401` 은 추가 recovery 없이 terminal `AUTH_REQUIRED` 로 끝난다. 본 브랜치가 소유하는 것은 그 전이를 `AuthSessionPort` 계약으로 내리는 부분이다.
|
||||
|
||||
### 3. Recovery replay policy
|
||||
|
||||
> **Trace**: D4 · `FE-OC-006`/`FE-OC-009` · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §7.8 (replay policy) · §7.7 (idempotency) · `FE-D016`. hub 계약에서 도출.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음.
|
||||
|
||||
**replay policy 본문은 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §7.8 소유다.** 요약 한 줄: idempotency mode 별로 recovery 성공 후 replay 는 최대 1회이고 `none`(unkeyed) 는 replay 금지다.
|
||||
|
||||
replay + 일반 retry의 총 시도는 operation registry·test fixture가 추적하며 recovery loop를 만들 수 없다.
|
||||
|
||||
### 4. Auth 실패 정규화 매핑 (error registry 기여)
|
||||
|
||||
> **Trace**: D5 · `FE-OC-008`(error 브랜치 owner에 기여) · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §8.2 (failure matrix) · §8.5 (negative fixtures) · §5.6 (error enum).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — kind/action/telemetry가 §8.2·§5.6에 고정.
|
||||
> - **OUT_OF_BRANCH_SCOPE**: error registry의 스키마·total-function 정규화 뼈대는 [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] 의 `FE-OC-008` 소유. 나는 아래 3행의 기대 매핑만 제공.
|
||||
|
||||
| Trigger (본 브랜치가 발생시키는 지점) | 기대 kind |
|
||||
|---|---|
|
||||
| HTTP `401` | `AUTH_REQUIRED` |
|
||||
| HTTP `403` | `FORBIDDEN` |
|
||||
| attach/recovery adapter throw·reject·invalid state | `AUTH_INTEGRATION_FAILURE` |
|
||||
|
||||
각 kind 의 retry·action·telemetry 규칙은 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §8.2 소유이며 여기에 옮겨 적지 않는다.
|
||||
|
||||
### 5. Browser-security 불변식 (auth) — `FE-OC-019` 기여
|
||||
|
||||
> **Trace**: D6 · `FE-OC-019`(browser-security 브랜치에 기여) · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.5 (`AUTH_TOKEN` forbidden) · §6.1 (secret config) · §5.8 (telemetry forbidden) · §8.1 (failure exclusions).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — 금지 목록이 hub registry/config에 고정.
|
||||
> - **OUT_OF_BRANCH_SCOPE**: CSP/header/secret-scan lint 메커니즘은 [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] 의 `FE-OC-019`, storage 스키마는 [[raw/branch-notes/feature-frontend-storage-registry-contract]] 의 `FE-OC-013` 소유.
|
||||
|
||||
- token/secret을 `localStorage`/`sessionStorage`/bundle/`import.meta.env`/`/config.json`에 저장 금지 (§6.1).
|
||||
- `AUTH_TOKEN` storage key = `forbidden` + `sensitive-forbidden`, 외부 auth owner만 token 저장 (§5.5).
|
||||
- telemetry `forbiddenAttributes`에 token·email·raw URL 포함, auth 이벤트는 route ID만 (§5.8·§8.2).
|
||||
- normalized failure에 raw body·token·authorization header·stack 미포함 (§8.1).
|
||||
|
||||
### 6. no-token-lifecycle 강제 (측정 항목 "no token lifecycle import tests")
|
||||
|
||||
> **Trace**: D1 + D6 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §4.2·§4.3 (dependency matrix) — §20 Measurable completion의 "no token lifecycle import tests".
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: token-lifecycle 심볼 집합(예: `refresh`/`rotate`/`token-store`)과 restricted-import glob은 내 임의 제안 — trade-off: `adapters/auth/` 밖에서 token 저장·회전 심볼 import를 차단하는 최소 룰로 시작하되 오탐 시 auth owner 계약에 맞춰 조정.
|
||||
> - **OUT_OF_BRANCH_SCOPE**: import-lint의 실행 메커니즘(dependency-cruiser/ESLint restricted imports fixture)은 [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]] 의 `FE-OC-002` 소유. 나는 *검사 대상 불변식*만 정의.
|
||||
|
||||
- 불변식: token 발급·저장·refresh·rotation 심볼은 `src/adapters/auth/` 내부에서만 존재/참조. `domain`/`application`/`presentation`은 이를 import 금지 (§4.3).
|
||||
|
||||
### 7. Route session-integration 접점 — `FE-OC-005` 기여
|
||||
|
||||
> **Trace**: D7 · `FE-OC-005`(routing 브랜치에 기여) · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §9.3 (route behavior) · §7.8 · §5.2 (route access enum).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음.
|
||||
> - **OUT_OF_BRANCH_SCOPE**: route registry 스키마·`access` 필드·redirect-loop·param validation은 [[raw/branch-notes/feature-routing-navigation-guard-contract]] 의 `FE-OC-005` 소유. 나는 아래 2 규칙만 기여.
|
||||
|
||||
- `access: session-required`/`integration-defined` route는 `AuthSessionPort` state를 **UX hint**로만 소비 (§9.3·§5.2).
|
||||
- backend authorization result가 최종 권한 판단이며, guard는 이를 대체하지 않는다 (§9.3, `FE-RISK-005`).
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- attach callback이 throw/reject → `AUTH_INTEGRATION_FAILURE`, unauthenticated-safe shell (hub §8.5).
|
||||
- recovery가 invalid state 반환 → `AUTH_INTEGRATION_FAILURE`.
|
||||
- 같은 logical request의 두 번째 `401` → 추가 recovery 없이 terminal `AUTH_REQUIRED` (loop 금지).
|
||||
- `none`(unkeyed) mutation이 recovery 성공 → replay 금지, 명시적 재시도 요구.
|
||||
- recovery 대기 중 navigation/user abort → `REQUEST_ABORTED`, 대기 취소(error event 금지).
|
||||
- `keyed` mutation인데 active backend replay contract 부재 → replay 금지.
|
||||
- **다른 계약 의존** (§20 Dependency + hub §4.3):
|
||||
- [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] 의 `FE-OC-002`(application-owned port + dependency rule)에 의존 — §20의 hard dependency. 이 계약이 바뀌면 port 소유 위치가 흔들림.
|
||||
- [[raw/branch-notes/feature-api-client-response-envelope-contract]] 의 `FE-OC-006`(shared client)에 의존 — attach·정규화·bounded state machine이 그 client 안에서 실행. timeout/abort/retry는 그 계약이 소유.
|
||||
- [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] 의 `FE-OC-008`(error registry)에 의존 — auth kind 3종이 그 registry에 존재해야 매핑 성립.
|
||||
- [[raw/branch-notes/feature-routing-navigation-guard-contract]] 의 `FE-OC-005`(route registry)에 의존 — session-required `access` 필드가 존재해야 hint 접점 성립.
|
||||
- [[raw/branch-notes/feature-frontend-storage-registry-contract]] 의 `FE-OC-013`, [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] 의 `FE-OC-019`, [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] 의 `FE-OC-014`에 기여 — 각 `AUTH_TOKEN` forbidden·no-token-bundle·telemetry redaction 불변식.
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| `AuthSessionPort`가 token 문자열을 domain/application model로 반환하지 않는다 | 구현 코드 없음(`planned`) | port contract test + no-token-lifecycle import test(§4.6 architecture fixture) | `needs-confirmation` |
|
||||
| bounded 401 machine이 recovery loop를 만들지 않는다(request당 recovery ≤1, 2nd 401 terminal) | machine 미구현 | deterministic state-transition test(`ClockPort` + 주입된 401 시퀀스, §7.8) | `needs-confirmation` |
|
||||
| recovery replay가 unkeyed mutation을 replay하지 않는다 | 미구현 | negative fixture: recovery succeeds for unkeyed mutation → no replay (§8.5) | `needs-confirmation` |
|
||||
| attach/recovery adapter fault가 `AUTH_INTEGRATION_FAILURE`로 정규화된다 | 미구현 | negative fixture: attach throw/reject, recovery invalid state (§8.5) | `needs-confirmation` |
|
||||
| `401→AUTH_REQUIRED`·`403→FORBIDDEN` 정규화 + raw body/token 미노출 | 미구현 | error catalog test + redaction assertion(telemetry에 principal/token 없음, §8.2) | `needs-confirmation` |
|
||||
| session-required route hint가 backend authz를 대체하지 않는다 | 미구현 | e2e: guarded route에서 backend `403` 처리 확인 (`FE-RISK-005`) | `needs-confirmation` |
|
||||
| skeleton 어디에도 token이 browser storage/bundle에 저장되지 않는다 | 미구현 | secret scan + storage registry test(`AUTH_TOKEN` forbidden, §5.5·§6.1) | `needs-confirmation` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
> `/coverage`가 채우는 생성물이며 손으로 유지하지 않는다.
|
||||
|
||||
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| TODO — `/coverage` 실행 전 | missing | (없음) | 미평가 | TODO |
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-OC-002@1` | [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] | `domain <- application <- presentation` 의존 방향과 application-owned output port를 MUST 지킴 | import 참조로 적용 |
|
||||
| `FE-OC-005@1` | [[raw/branch-notes/feature-routing-navigation-guard-contract]] | route ID/path/params/access/loading/error owner는 route registry 하나여야 함 | import 참조로 적용 |
|
||||
| `FE-OC-006@1` | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | 모든 HTTP는 shared client를 MUST 통과하고 timeout·abort·response parsing을 page에서 구현하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-008@1` | [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] | 모든 failure는 stable frontend error kind로 MUST 정규화하고 raw body·stack을 UI에 노출하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-019@1` | [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] | browser bundle에 secret을 넣지 않고 untrusted HTML injection을 기본 금지 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
### Sub-branches (세부 작업)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 오류 기록 (이 branch 작업 중 발생)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 강의 (이 작업을 위해 학습한 강의)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### job-posting tie-ins (이 작업에서 파생된 글감)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크:
|
||||
- 리뷰 메모:
|
||||
- 머지 결과 / 배포 환경:
|
||||
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
|
||||
- `actually-implemented` 항목:
|
||||
- `locally-verified` 항목:
|
||||
- `prod-verified` 항목:
|
||||
- **추출하지 않을 항목** (planned / documented-only / abandoned):
|
||||
+345
@@ -0,0 +1,345 @@
|
||||
---
|
||||
title: branch / feature-frontend-browser-security-boundary-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-021
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-021
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-AUTH-BOUNDARY-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-TELEMETRY-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-008, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-011, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-012]
|
||||
contract_packet: 1
|
||||
branch: feature-frontend-browser-security-boundary-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract]
|
||||
tags: [branch, ca-skeleton, frontend, security, owasp, static-analysis]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
contract_packet_sha256: 67f39972a793315acd8de19be381ec163d5e8843a539a147103b74c6d4324274
|
||||
imports: [FE-GATE-002@1, FE-GATE-006@1, FE-GATE-013@1, FE-GATE-019@2, FE-OC-004@1, FE-OC-008@1, FE-OC-010@1, FE-OC-013@1, FE-OC-014@1, FE-OC-016@1, FE-OC-018@1, FE-OC-020@1]
|
||||
accepts_delegations: [DELEG-FE-001@1]
|
||||
|
||||
---
|
||||
|
||||
# branch: feature-frontend-browser-security-boundary-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — TODO·결정·진행 기록. 구현 결과는 검증 뒤 `/ingest`로만 추출한다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
[[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: CSP·header·secret·storage·telemetry browser-boundary fixture가 통과한다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-AUTH-BOUNDARY-001@1` | auth lifecycle은 외부 owner가 소유하고 skeleton은 AuthSessionPort만 소비한다 | token·secret browser storage 금지 fixture에 적용 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-TELEMETRY-001@1` | telemetry는 best-effort queue와 redaction을 사용하며 sink failure가 UI를 실패시키지 않는다 | telemetry forbidden-attribute leak fixture에 적용 | [[raw/project-notes/ca-skeleton-frontend-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 -->
|
||||
## 목표
|
||||
|
||||
- 이 branch는 project-wide contract `FE-OC-019`(browser bundle에 secret을 넣지 않고 untrusted HTML injection을 기본 금지)의 single owner로서, 이를 *되묻지 않아도 코드를 작성할 수 있는* implementation-ready spec으로 내린다. 근거는 hub [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §13.2(browser security boundary)·§6.1(secret 3-way 구분)·§13.1(secret scan)·§15.1(`FE-GATE-013` security gate)·§15.2(negative fixture)다.
|
||||
- 동시에 `FE-OC-010`(auth session), `FE-OC-013`(storage), `FE-OC-014`(telemetry), `FE-OC-018`(build/bundle supply-chain), `FE-OC-020`(test taxonomy)에 **contribute**한다 — 각 registry의 *schema*는 그 owner branch가 갖고, 본 branch는 그 경계를 넘는 값(secret·token·untrusted HTML·PII)이 브라우저 표면(bundle·env·HTML·storage·telemetry)에 새지 않는지 검증하는 **cross-cutting security fixture와 injection/secret lint+scan**을 소유한다.
|
||||
- 측정 가능한 완료 조건(§20): `CSP/header/secret/storage/telemetry browser-boundary fixtures`.
|
||||
- 이슈: 없음 (스캐폴딩 단계)
|
||||
- PR: 없음
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- browser bundle을 public artifact로 간주하고 **secret(client secret·private key·refresh token material)을 bundle·env·HTML에 넣지 않도록** 강제하는 계약 — env registry의 name-based 거부 + source/built-asset secret scan.
|
||||
- **untrusted HTML injection과 dynamic code 실행 기본 금지** — `dangerouslySetInnerHTML`·`eval`·dynamic code(`new Function`)·untrusted script URL의 prohibited import/API lint rule. 불가피한 HTML rendering의 예외 조건(sanitizer owner·allowlist·malicious fixture·CSP interaction evidence) 명세.
|
||||
- bundle이 `unsafe-inline`/`unsafe-eval` 없는 **strict CSP와 호환**되도록 하는 frontend 측 불변식(inline script·inline handler·eval 미의존) + 선언된 security header 정책의 verification fixture.
|
||||
- production public path에 **source map 미배포** 기본 정책.
|
||||
- 위 경계를 넘는 값을 잡는 **cross-cutting security fixture 집합**(secret / storage token-key / telemetry forbidden-attribute / HTML-injection)과 이를 `FE-GATE-013`으로 집계 + `FE-GATE-002`(lint)에 기여.
|
||||
- **`FE-GATE-006`(component gate)의 `FE-OC-019` 슬라이스 fixture 본문 소유** — hub §15.1 이 component gate 의 Covered FE-OC 에 `FE-OC-019` 를 명시했으므로, render 시점에만 관측 가능한 injection 불변식(예외 sanitizer 경로의 malicious fixture + rendered subtree 의 prohibited-API 산출물 부재)은 본 branch 가 component-level fixture 로 소유한다(D8).
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외한 것. "이건 범위에 없었습니다"라고 답할 근거.
|
||||
|
||||
- **CSP/HSTS/frame/referrer header의 실제 directive 값(production)** — hosting/backend header owner 소유(hub §13.2). 본 branch는 값이 아니라 *호환성*만 본다. "선언 == 실제"의 **검증 위치**는 2026-07-21 에 `FE-GATE-019@2` 로 확정됐다 — D9 참조.
|
||||
- **hosting header(`Cache-Control`·content-type·security header) 정책의 declared-vs-actual 검증** — release·cache 계약 [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] 소유(cache 축 `D1`, security-header 축 `D6`). hub §15.1 `FE-GATE-019@2` 가 Covered FE-OC 에 `FE-OC-019` 를 포함하도록 확장돼 security header 도 그 gate 범위다. 본 branch 는 검증 대상 header 정책을 공급한다.
|
||||
- **storage key/version/classification registry schema** — [[raw/branch-notes/feature-frontend-storage-registry-contract]] `FE-OC-013` 소유. 본 branch는 token/secret 저장 시도가 실패하는 security fixture만 갖는다.
|
||||
- **token lifecycle(발급·저장 위치·refresh·rotation·logout)** — 외부 auth owner + [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] `FE-OC-010` 소유.
|
||||
- **telemetry redaction allowlist와 transport-boundary 강제** — [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] `FE-OC-014` 소유. 본 branch는 forbidden-attribute leak fixture만 기여.
|
||||
- **secret scanner/vulnerability scanner 도구 선택·severity threshold** — supply-chain 계약 [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] `FE-OC-018`. hub §13.1에서 도구는 `deferred`.
|
||||
- **error registry 구조·정규화** — [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] `FE-OC-008`.
|
||||
- **backend authorization·CORS enforcement** — 서버/브라우저 책임. route guard는 authorization control이 아니며(hub §13.2), client validation은 backend validation을 대체하지 않는다.
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/official-docs/owasp-html5-storage-xss-spa]] | D4 — 단일 XSS로 localStorage/sessionStorage 전체 탈취·주입 가능하므로 token/secret을 browser storage에 두지 않는다(`OWASP-HTML5-C1`~`C3`). |
|
||||
| [[raw/official-docs/owasp-content-security-policy-cheat-sheet]] | D2·D3 — CSP는 server가 보내는 HTTP response header이고(`OWASP-CSP-C1`), `'unsafe-inline'`/`'unsafe-eval'`이 없으면 inline script·eval이 차단되므로(`OWASP-CSP-C2`·`C3`) bundle이 이를 미의존해야 strict CSP(second layer, `OWASP-CSP-C4`)를 적용할 수 있다. |
|
||||
| [[raw/official-docs/owasp-hsts-cheat-sheet]] | D3 — HSTS 등 security response header는 response header owner(hosting)의 opt-in 결정이며(`OWASP-HSTS-C1`), frontend는 값이 아닌 호환성만 책임진다는 경계의 근거. |
|
||||
| [[raw/official-docs/owasp-logging-cheat-sheet]] | D5 — 다른 trust zone에서 온 event data는 untrusted이며(`OWASP-LOG-C1`) sanitization으로 민감정보를 제거해야 한다(`OWASP-LOG-C3`)는 telemetry/error redaction fixture의 원칙 근거. |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | D1·D6·D7 — secret 3-way 구분과 name-based 거부(§6.1), bundle=public artifact·source map 미배포(§13.2), secret scan(§13.1), security gate·negative fixture(§15.1·§15.2)의 project decision 근거. |
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] secret-exclusion: env registry의 name-based 거부(`SECRET`/`PASSWORD`/`PRIVATE_KEY`/`TOKEN`) + source·`dist/` built-asset secret scan fixture 정의 — 등급: `planned`
|
||||
- [ ] injection-lint: `dangerouslySetInnerHTML`·`eval`·dynamic code·untrusted script URL 금지 rule + **금지 rule 1개당 1개**의 deliberately-failing negative fixture(총 3개, D10) — 등급: `planned`
|
||||
- [ ] injection-render fixture(`FE-GATE-006`): 예외 sanitizer 경로 malicious fixture + rendered subtree 에 inline `<script>`/inline handler 부재 assertion(D8) — 등급: `planned`
|
||||
- [ ] csp-compat: **no-unsafe 정본 test baseline**(D10) 하에서 bundle의 `unsafe-inline`/`unsafe-eval` 미의존 assertion + CSP violation 0 관측 — 등급: `planned`
|
||||
- [ ] security-header declared-vs-actual: 선언 CSP/HSTS/frame/referrer 정책 == 실제 hosting 응답 verification. gate 귀속은 `FE-GATE-019@2` 로 확정됐고(D9), 본 branch 는 검증 대상 header 정책을 공급 — 등급: `planned`
|
||||
- [ ] storage-boundary fixture: token/secret key 등록 시도가 실패함을 증명(§15.2 "storage: token key registration attempt") — 등급: `planned`
|
||||
- [ ] telemetry-boundary fixture: forbidden attribute(raw URL/query/token 등) 전송 시도가 실패함을 증명(§15.2 "telemetry: event includes raw URL/query") — 등급: `planned`
|
||||
- [ ] source-map policy: production public path에 source map 미배포 확인 fixture — 등급: `planned`
|
||||
- [ ] gate wiring: 위 fixture를 `FE-GATE-013`(security)로 집계 + `FE-GATE-002`(lint) 기여 + `FE-GATE-006`(component)의 `FE-OC-019` 슬라이스 소유(D8) — 등급: `planned`
|
||||
- [x] 상호 개정: test-taxonomy 계약의 `FE-GATE-006` row "Fixture 본문 owner" 에 `browser-security(FE-OC-019 슬라이스)` 추가 — **2026-07-21 완료**(D8)
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
- frontend code가 아직 없다(hub §1.2). 본 branch의 모든 항목은 `planned` blueprint이며, 경로·rule 이름 등 hub가 근거하지 않는 detail은 `UNSUPPORTED_IMPL_DECISION`으로 표시했다.
|
||||
- 핵심 관점: 본 branch는 새 registry를 만들지 않고, 이미 owner가 있는 5개 표면(bundle·storage·telemetry·auth·supply-chain)의 *security 불변식*을 fixture로 집행하는 cross-cutting 계약이다. registry schema를 재정의하면 owner 경계를 침범한다(§15.5 R3).
|
||||
- CSP directive 값은 배포 환경 header owner 소유 → 본 branch는 "bundle이 strict CSP를 깨지 않는가"만 검증한다.
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> 대안과 함께 기록. 각 결정 근거는 위 Sources를 가리킨다. 모든 결정은 `planned`(코드 evidence 없음).
|
||||
|
||||
- 2026-07-19: **D1 secret은 browser 표면에 미포함** — bundle을 public artifact로 간주하고 obfuscation으로 secret을 보호할 수 있다고 가정하지 않는다. env registry가 `SECRET`/`PASSWORD`/`PRIVATE_KEY`/`TOKEN` 이름 key를 build·runtime 모두에서 거부하고, secret scan을 source와 built asset(`dist/`) 모두에 돌린다. / 이유: browser에 도달한 값은 복원 가능(hub §13.2)이므로 예방이 유일한 통제. / 대안: 값이 browser 가시이나 민감한 endpoint류는 `public-sensitive`로 분류(§5.4) — secret 아님. / 근거: hub §6.1·§13.1·§13.2.
|
||||
- 2026-07-19: **D2 untrusted HTML/dynamic code 실행 기본 금지** — `dangerouslySetInnerHTML`·`eval`·dynamic code(`new Function`)·untrusted script URL을 prohibited import/API lint rule로 막는다. 불가피한 HTML rendering은 sanitizer owner·allowlist·malicious fixture·CSP interaction evidence를 요구한다. / 이유: injection이 XSS의 1차 진입점이고, CSP는 second layer일 뿐 primary가 아니다(`OWASP-CSP-C4`). / 대안: 4종 evidence를 갖춘 예외 rendering 경로만 허용. / 근거: hub §13.2 + `OWASP-CSP-C2`·`C3`.
|
||||
- 2026-07-19: **D3 strict-CSP 호환은 frontend, header 값은 header owner** — bundle과 그 의존성이 `'unsafe-inline'`/`'unsafe-eval'`을 요구하지 않도록 유지하고, 선언된 header 정책(CSP/HSTS/frame/referrer)이 실제 hosting 응답과 일치하는지 verification fixture로 확인한다. directive 값 자체는 header owner 소유. / 이유: CSP는 server response header이며(`OWASP-CSP-C1`) HSTS도 opt-in header 결정(`OWASP-HSTS-C1`)이라 값은 배포 계층 소유. / 대안: 불가피한 inline이 필요하면 nonce/hash는 header owner가 관리(본 branch 범위 밖). / 근거: hub §13.2 + `OWASP-CSP-C1`·`C4` + `OWASP-HSTS-C1`.
|
||||
- 2026-07-19: **D4 token/secret은 browser storage 금지(contributes `FE-OC-013`)** — token/secret/PII의 browser storage 저장을 금지하고 token-key 등록 시도가 실패하는 security fixture를 소유한다. classification schema는 storage-registry branch 소유. / 이유: 단일 XSS로 storage 전체 탈취 가능(`OWASP-HTML5-C2`), storage 객체는 trusted가 아님(`OWASP-HTML5-C3`). / 대안: 외부 auth owner가 storage를 반드시 써야 하면 별도 threat model + owner evidence(§6.1) — skeleton default 아님. / 근거: hub §5.5·§13.2 + `OWASP-HTML5-C1`~`C3`.
|
||||
- 2026-07-19: **D5 telemetry/error에 token·PII·raw payload 미포함(contributes `FE-OC-014`·`FE-OC-008`)** — forbidden attribute(token·email·raw URL/query/body·storage value·stack)가 telemetry event나 normalized failure에 새지 않는지 검사하는 security scan fixture를 소유한다. redaction allowlist와 transport-boundary 강제는 observability branch 소유. / 이유: 외부 trust zone data는 untrusted이며 민감정보는 제거해야 한다(`OWASP-LOG-C1`·`C3`). / 대안: N/A(항상 금지, 분기 없음). / 근거: hub §11.1·§5.8·§8.1 + `OWASP-LOG-C1`·`C3`.
|
||||
- 2026-07-19: **D6 source map production 미배포** — production public path에 source map을 기본 배포하지 않는다. / 이유: source map은 최소화된 소스·주석·경로를 재노출해 secret/logic leak 표면을 넓힌다(D1과 연속). / 대안: 디버깅 필요 시 authenticated 경로 또는 error-tracking backend에만 업로드(공개 아님) — 예외 결정. / 근거: hub §13.2("source map은 production public path에 기본 배포하지 않는다").
|
||||
- 2026-07-19: **D7 security gate 집계 + negative fixture 필수(owns `FE-OC-019`, contributes `FE-OC-020`)** — 5개 fixture family(secret·CSP/header·HTML-injection·storage·telemetry)를 `FE-GATE-013`으로 집계하고 각 family는 최소 1개 deliberately-failing negative fixture를 갖는다(rule 존재 확인만으로는 `locally-verified` 불가, §15.2). / 이유: hub 수용 질문 8 "위반 시 어떤 test가 실패하는가"에 답해야 `documented-only`를 넘는다(§2.2). / 대안: N/A. / 근거: hub §15.1·§15.2·§2.2.
|
||||
|
||||
- 2026-07-20: **D8 `FE-GATE-006`의 `FE-OC-019` 슬라이스는 본 branch가 소유(contributes `FE-OC-020`)** — hub §15.1의 component gate row가 Covered FE-OC에 `FE-OC-019`를 명시하므로, static lint(`FE-GATE-002`)로는 관측 불가능한 *render 시점* injection 불변식을 component-level fixture로 본 branch가 소유한다. 2종: (a) 예외 sanitizer rendering 경로의 **malicious fixture**(hub §13.2가 예외 승인 조건으로 요구하는 4종 evidence 중 하나), (b) 렌더된 subtree에 inline `<script>` 노드·inline event-handler attribute·`javascript:` URL이 존재하지 않음을 확인하는 assertion. / 이유: lint는 소스에 없는 sink(런타임 문자열 조립·서드파티 컴포넌트 경유)를 못 잡고, hub §15.2는 "rule 존재 확인"을 evidence로 인정하지 않는다. / 대안: component gate가 async/render/keyboard 전용이라 보고 위임 — 채택하지 않음. 위임하면 hub가 요구한 `FE-OC-019` 커버리지의 owner가 공백이 되고, 당시 test-taxonomy 계약의 `FE-GATE-006` row는 fixture 본문 owner로 `async-ui-state / render-recovery`만 등재해 security를 배제하고 있었고, 그대로 두면 이 슬라이스를 아무도 갖지 않게 된다. (2026-07-21 에 그 row 에 `browser-security(FE-OC-019 슬라이스)` 가 등재돼 해소됐다.) / 근거: hub §15.1(`FE-GATE-006` Covered FE-OC)·§13.2·§15.2.
|
||||
- 2026-07-20: **D9 security header의 declared-vs-actual 검증의 gate 귀속** — 초판은 `FE-GATE-019`에 위임했으나 당시 그 row 는 pass condition 이 Cache-Control/content-type 으로 한정되고 Covered FE-OC 도 `FE-OC-016` 하나뿐이라 실제로는 어느 gate 에도 착지하지 않았다. 그래서 `FE-GATE-013`에 잠정 배치하고 hub 개정을 권고했다. / **2026-07-21 확정**: 권고한 두 안 중 (a)가 채택돼 hub §15.1 `FE-GATE-019`의 Covered FE-OC 에 `FE-OC-019`가 추가되고 pass condition 이 security header 까지 확장됐다(`FE-GATE-019@2`, Owner 는 release-cache). 근거: `FE-GATE-013`은 artifact 를 스캔하는 gate 이고 여기서 필요한 것은 실제 HTTP 응답의 declared-vs-actual 대조로 `FE-GATE-019`와 같은 메커니즘·같은 증거 형식이다. directive *값*은 여전히 header owner 소유. / 근거: hub §15.1(`FE-GATE-019@2` row)·§2.1.1·§13.2 + `OWASP-CSP-C1`·`OWASP-HSTS-C1`.
|
||||
- 2026-07-20: **D10 CSP 호환 fixture는 no-unsafe 정본 test baseline에서 실행, negative fixture는 금지 rule 1개당 1개** — (a) production directive 값이 header owner 미확정이어도 test가 실행 가능하도록, `'unsafe-inline'`·`'unsafe-eval'`이 없는 **최소 test baseline CSP**를 본 branch가 정본으로 고정하고 compatibility fixture는 이 baseline 하에서 CSP violation 0을 관측한다(production 값과 별개의 test 전용 상수). (b) §2의 금지 API 3종은 서로 다른 rule이 잡으므로 family당 1개가 아니라 **rule당 1개**의 고의 실패 fixture를 둔다. / 이유: (a) 값이 위임되었다는 이유로 "CSP violation 0"을 측정 불가로 남기면 claim이 영구 `needs-confirmation`이 된다. (b) hub §15.2의 "gate당 최소 1개"는 하한이며, 1개만 두면 나머지 2개 rule은 존재만 확인된 상태 = §15.2가 evidence로 불인정하는 상태다. / 대안: (b) family당 1개로 축소 — fixture 3개 유지비는 줄지만 미검증 rule 2개가 남아 채택하지 않음. / 근거: hub §15.2·§13.2 + `OWASP-CSP-C2`·`OWASP-CSP-C3`.
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
> `Decision ID`는 이 branch-note 안에서 안정적으로 유지한다. `Supporting Claims`는 official-doc의 Claim ID 또는 hub의 §/`FE-OC`/`FE-D` reference.
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | secret은 bundle·env·HTML에 미포함; env registry name-based 거부 + source/`dist/` secret scan (`FE-OC-019`, contributes `FE-OC-018`) | client-only public bundle인 한 항상 예방 통제 / 값이 browser 가시이나 민감한 endpoint류면 secret이 아니라 `public-sensitive` 분류(§5.4)로 다룸 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §6.1(name reject)·§13.2(public artifact)·§13.1(secret scan) · FE-D024 | `project-decision` | scanner 도구·threshold가 `deferred`(§13.1)라 false-negative 가능성 미검증; 값 분류(`public-sensitive` vs secret) 경계 판정 |
|
||||
| D2 | untrusted HTML/dynamic code 실행 기본 금지 (`dangerouslySetInnerHTML`·`eval`·`new Function`·untrusted script URL) | default는 항상 금지 / 불가피한 HTML rendering은 sanitizer owner+allowlist+malicious fixture+CSP interaction evidence 4종을 갖춘 예외 경로만 허용(hub §13.2) | [[raw/official-docs/owasp-content-security-policy-cheat-sheet]] OWASP-CSP-C2·OWASP-CSP-C3 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §13.2 | `official-doc + project-decision` | lint rule/plugin이 dynamic sink(문자열 template→DOM)를 실제로 포착하는지 미검증; 예외 rendering 경로 발생 시 4종 evidence 강제 누락 위험 |
|
||||
| D3 | bundle의 strict-CSP 호환(`unsafe-inline`/`unsafe-eval` 미의존) + header 정책 verification; directive 값은 header owner 소유 | frontend는 항상 no-unsafe 유지 / 불가피한 inline 필요 시 nonce/hash는 hosting header owner가 관리(본 branch 범위 밖) | [[raw/official-docs/owasp-content-security-policy-cheat-sheet]] OWASP-CSP-C1·OWASP-CSP-C4 · [[raw/official-docs/owasp-hsts-cheat-sheet]] OWASP-HSTS-C1 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §13.2 | `official-doc + project-decision` | production directive 값은 header owner 의존(테스트는 D10의 no-unsafe baseline으로 분리); 선언-vs-실제 검증 gate 귀속은 `FE-GATE-019@2` 로 확정(D9); 의존성 중 eval 사용 lib이 CSP를 깰 위험 |
|
||||
| D4 | token/secret/PII의 browser storage 저장 금지 + token-key 등록 실패 fixture (contributes `FE-OC-013`·`FE-OC-010`) | skeleton default는 항상 금지 / 외부 auth owner가 storage를 반드시 써야 하면 별도 threat model + owner evidence(§6.1)일 때만 예외 | [[raw/official-docs/owasp-html5-storage-xss-spa]] OWASP-HTML5-C1·OWASP-HTML5-C2·OWASP-HTML5-C3 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.5·§13.2 | `official-doc + project-decision` | classification schema는 storage-registry 소유 → fixture 중복/누락 조율 필요(공동 집행 경계) |
|
||||
| D5 | telemetry/normalized failure에 token·PII·raw URL/query/body·storage value·stack 미포함 fixture (contributes `FE-OC-014`·`FE-OC-008`) | 분기 없음 — 항상 forbidden. 신규 attribute는 low-cardinality+non-PII 검토 통과 시에만 registry 추가(observability 소유) | [[raw/official-docs/owasp-logging-cheat-sheet]] OWASP-LOG-C1·OWASP-LOG-C3 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §11.1·§5.8·§8.1 | `official-doc + project-decision` | redaction 강제는 transport boundary(observability adapter)에서 일어남 → 본 branch fixture는 leak 관측만, 강제 위치는 위임 |
|
||||
| D6 | production public path에 source map 미배포 | 기본 미배포 / 디버깅 필요 시 authenticated 경로·error-tracking backend 업로드(공개 아님)만 예외 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §13.2(source map 미배포) | `project-decision` | build 도구 flag로 강제하는 구체 메커니즘 미명세(§구현 가이드 5 UNSUPPORTED_IMPL) |
|
||||
| D7 | 5개 security fixture family를 `FE-GATE-013`으로 집계 + 각 family 최소 1 negative fixture (owns `FE-OC-019`, contributes `FE-OC-020`) | 분기 없음 — negative fixture 없는 rule은 evidence로 불인정(§15.2) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.1(`FE-GATE-013`)·§15.2·§2.2(질문 8) | `project-decision` | test taxonomy/artifact retention은 `FE-OC-020` 소유 → gate 배선은 test branch와 조율 |
|
||||
| D8 | `FE-GATE-006`(component)의 `FE-OC-019` 슬라이스 fixture 본문 소유 — 예외 sanitizer 경로 malicious fixture + rendered subtree의 inline `<script>`/inline handler/`javascript:` URL 부재 assertion (owns `FE-OC-019`, contributes `FE-OC-020`) | 분기 없음 — hub §15.1이 component gate의 Covered FE-OC에 `FE-OC-019`를 명시하는 한 본 branch 소유 / 위임하려면 hub §15.1에서 `FE-GATE-006`의 `FE-OC-019` 커버리지를 제거하는 개정이 선행돼야 함 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.1(`FE-GATE-006` Covered FE-OC = `FE-OC-019` 포함)·§13.2(prohibited API)·§15.2(negative fixture 요건) | `project-decision` | test-taxonomy 계약의 `FE-GATE-006` row 에 `browser-security(FE-OC-019 슬라이스)` 가 2026-07-21 에 등재돼 두 노트의 owner 표기가 일치한다. 이후 그 row 가 다시 바뀌면 여기도 함께 갱신해야 한다 |
|
||||
| D9 | security header(CSP/HSTS/frame/referrer)의 declared-vs-actual 검증은 `FE-GATE-019@2` 소유이고, 본 branch 는 검증 대상 header 정책을 공급 (`FE-OC-019`) | hub §15.1 `FE-GATE-019@2` 가 Covered FE-OC 에 `FE-OC-019` 를 포함하는 한 이 배치 유지 / hub 가 그 범위를 되돌리면 gate 귀속 재확정 필요 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.1(`FE-GATE-019@2` — Covered FE-OC 에 `FE-OC-019` 포함, pass condition 이 security header 포함)·§2.1.1(revision 2)·§13.2 · [[raw/official-docs/owasp-content-security-policy-cheat-sheet]] OWASP-CSP-C1 · [[raw/official-docs/owasp-hsts-cheat-sheet]] OWASP-HSTS-C1 | `project-decision + official-doc` | gate 는 release-cache 소유이므로 fixture 실행 시점·artifact 형식은 그 branch 와 맞춰야 함 |
|
||||
| D10 | CSP compatibility fixture는 본 branch가 고정한 **no-unsafe test baseline CSP** 하에서 실행; 금지 API는 family당이 아니라 **rule당 1개**의 고의 실패 fixture (`FE-OC-019`, contributes `FE-OC-020`) | 분기 없음 — production 값 확정 여부와 무관하게 test는 baseline으로 실행 / production 값이 확정되면 baseline은 유지하고 실제 값 대조는 D9 fixture가 별도 담당 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.2("rule 존재만 확인한 결과는 `locally-verified` 증거로 부족")·§13.2 · [[raw/official-docs/owasp-content-security-policy-cheat-sheet]] OWASP-CSP-C2·OWASP-CSP-C3 | `project-decision + official-doc` | baseline directive 집합 자체는 hub 미명명(§구현 가이드 3 `UNSUPPORTED_IMPL_DECISION`); baseline이 production 값보다 느슨하면 통과해도 실제 환경에서 깨질 수 있음 |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> `planned` blueprint. 경로는 hub §4.6(Planned directory blueprint)·§5.1(registry owner map)에서 인용했으나 repository가 아직 없어 전체가 `planned`다. 3-rule(R1 Trace / R2 UNSUPPORTED_IMPL / R3 no OUT_OF_BRANCH_SCOPE) 준수.
|
||||
|
||||
### 1. Secret 배제 강제 (bundle·env·HTML)
|
||||
|
||||
> **Trace**: D1 — `FE-OC-019` + hub §6.1·§13.1·§13.2. env name-based 거부 규칙과 secret scan을 결합한다.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: secret scanner 도구명·정규식 패턴·severity threshold는 hub §13.1에서 `deferred` → 미명세. trade-off: 도구를 지금 고정하면 supply-chain branch(`FE-OC-018`)의 도구 선택과 충돌 → 도구 중립적으로 "source+built asset 스캔이 credential 패턴에 실패"라는 *계약*만 고정.
|
||||
|
||||
| 강제 지점 | 규칙 | 근거 |
|
||||
|---|---|---|
|
||||
| env registry 등록 시 | key 이름에 `SECRET`/`PASSWORD`/`PRIVATE_KEY`/`TOKEN` 포함 → build·runtime 모두 거부 | hub §6.1 |
|
||||
| build-time public | `BUILD_ID`·`COMMIT_SHA`·`ROUTER_BASE_PATH` 등 compiler/asset identity 값만 `VITE_*` 허용 | hub §6.1·§5.4 |
|
||||
| secret scan 대상 | source tree + `dist/`(built asset) 모두 | hub §13.1(secret scan: source + built asset) |
|
||||
| 값 분류 | browser 가시이나 민감한 endpoint류(`API_BASE_URL`·`TELEMETRY_ENDPOINT`)는 `public-sensitive` — 로그·telemetry에 원문 미기록, secret 아님 | hub §5.4 |
|
||||
|
||||
### 2. dynamic code 금지 lint
|
||||
|
||||
> **Trace**: D2·D10 — `FE-OC-019` + hub §13.2 + §15.2 + `OWASP-CSP-C2`·`OWASP-CSP-C3`. prohibited import/API 카탈로그 + 예외 경로 조건 + fixture 단위.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 구체 lint rule id/plugin(예: ESLint `react/no-danger`, `no-eval`, custom no-restricted-syntax)·`FE-GATE-002` 배선 형식은 hub가 명명하지 않음. trade-off: rule id를 지금 못박으면 test-taxonomy(`FE-OC-020`)의 lint 도구 선택을 침범 → "이 API/import가 금지되고 negative fixture가 실패한다"는 계약만 고정.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: fixture 단위를 "family당 1개"가 아니라 **"금지 rule당 1개"**로 강화(D10 (b))한 것은 hub 미명시 — hub §15.2는 *gate당* 최소 1개만 요구한다. trade-off: fixture 3개는 유지비가 늘지만, 1개만 두면 나머지 2개 rule은 "존재만 확인"된 상태로 남아 §15.2가 evidence로 불인정하는 구간에 들어간다 → 유지비를 택함.
|
||||
|
||||
**fixture 단위 답 (D10 (b))**: family당 1개로는 부족하다. 아래 3행은 각각 *다른 lint rule*이 잡으므로 **행당 1개씩, 총 3개의 고의 실패 negative fixture**를 둔다.
|
||||
|
||||
| 금지 대상 | 성격 | 전용 negative fixture(고의 실패) | 예외 조건 |
|
||||
|---|---|---|---|
|
||||
| `dangerouslySetInnerHTML` | prohibited API (default) | `presentation`이 untrusted 문자열을 `dangerouslySetInnerHTML`로 렌더 → lint 실패 | sanitizer owner + allowlist + malicious fixture + CSP interaction evidence 4종 |
|
||||
| `eval` / `new Function` / dynamic code | prohibited (default) | 모듈이 문자열을 `eval`/`new Function`으로 실행 → lint 실패 | 예외 없음(skeleton) |
|
||||
| untrusted script URL 주입 | prohibited (default) | 런타임 값으로 `<script src>`/`javascript:` URL 조립 → lint 실패 | 예외 없음(skeleton) |
|
||||
|
||||
- 3개 fixture 모두 `FE-GATE-002`(lint) 기여 → `FE-GATE-013` 집계. static lint로 관측 불가능한 *render 시점* 위반은 §6(`FE-GATE-006`)이 담당한다.
|
||||
|
||||
### 3. Strict-CSP 호환 + security header verification (production 값만 위임)
|
||||
|
||||
> **Trace**: D3·D9·D10 — `FE-OC-019` + hub §13.2 + §15.1(`FE-GATE-013`·`FE-GATE-019` row) + `OWASP-CSP-C1`·`OWASP-CSP-C4`·`OWASP-HSTS-C1`.
|
||||
>
|
||||
> - **R3 OUT_OF_BRANCH_SCOPE**: CSP/HSTS/frame/referrer의 **production directive 값**·max-age·preload는 hosting/backend header owner 소유 → 여기 명세하지 않는다.
|
||||
> - **범위 정정(D9) — 2026-07-21 확정**: 이전 판은 이 검증을 `FE-GATE-013`(security)에 *잠정* 배치했다. 그런데 `FE-GATE-013` 은 artifact 를 스캔하는 gate(secret·vulnerability·license·dependency review)이고, 여기서 필요한 것은 **실제 HTTP 응답의 declared-vs-actual 대조**로 `FE-GATE-019`(hosting header)와 같은 메커니즘·같은 증거 형식이다. 그래서 hub §15.1 `FE-GATE-019` 의 Covered FE-OC 에 `FE-OC-019` 를 추가하고 pass condition 을 security header 까지 넓혔다(`FE-GATE-019@2`, Owner 는 그대로 [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]]). 본 branch 는 gate 를 소유하지 않고 **검증 대상 header 정책을 공급**한다.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 아래 no-unsafe test baseline의 구체 directive 집합은 hub가 명명하지 않음(D10 (a)). trade-off: baseline을 느슨하게 잡으면 통과해도 실제 production 정책에서 깨지고, 과도하게 조이면 존재하지 않는 위반으로 개발을 막는다 → `'unsafe-inline'`/`'unsafe-eval'` 부재라는 *불변식*을 만족하는 최소 집합으로 잡고, production 값 확정 시 대조는 D9 fixture가 별도 담당.
|
||||
|
||||
**no-unsafe 정본 test baseline (D10 (a))** — production 값과 무관하게 compatibility fixture가 실행되는 test 전용 상수. `'unsafe-inline'`·`'unsafe-eval'` 미포함이 이 baseline의 불변식이다.
|
||||
|
||||
```text
|
||||
default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:;
|
||||
connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
|
||||
```
|
||||
|
||||
| frontend가 소유(assert) | header owner가 소유(값만 위임) |
|
||||
|---|---|
|
||||
| bundle·의존성이 `unsafe-inline`/`unsafe-eval` 미의존(inline `<script>`·inline handler·`eval` 없음) | `Content-Security-Policy` production directive 집합 값 |
|
||||
| 위 baseline 하 sample route에서 CSP violation 0 관측(compatibility fixture) — hosting owner 확정 전에도 실행 가능 | HSTS `max-age`·`includeSubDomains`·`preload` 채택 여부 |
|
||||
| 선언된 security header 정책 == 실제 hosting 응답인지 verification(`pnpm verify:hosting-headers`(security-header 축)) — `FE-GATE-019@2` 에 배치 확정(2026-07-21) | frame policy·referrer policy 값 |
|
||||
|
||||
**hub 개정 (D9) — 반영 완료(2026-07-21)**: 권고했던 두 안 중 (a)가 채택됐다. hub §15.1 `FE-GATE-019` 의 Covered FE-OC 에 `FE-OC-019` 가 추가되고 pass condition 이 security header 까지 확장됐으며, hub §2.1.1 의 `FE-GATE-019` revision 이 2 로 올라갔다. 이 gate 를 pin 한 문서는 revision 이 낡아 자동으로 잡힌다.
|
||||
|
||||
### 4. Cross-cutting security fixture + gate 집계 (storage·telemetry)
|
||||
|
||||
> **Trace**: D4·D5·D7·D8·D9·D10 — `FE-OC-019`(owns) + contributes `FE-OC-013`·`FE-OC-014`·`FE-OC-020` + hub §5.5·§11.1·§5.8·§15.1·§15.2.
|
||||
>
|
||||
> - **R3 OUT_OF_BRANCH_SCOPE**: storage classification schema는 [[raw/branch-notes/feature-frontend-storage-registry-contract]] `FE-OC-013`, redaction allowlist는 [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] `FE-OC-014` 소유. 본 표는 *security 위반 관측 fixture*만 소유한다.
|
||||
|
||||
| Fixture family | Negative fixture(고의 실패) | 기대 결과 | 집계 gate | schema owner(위임) |
|
||||
|---|---|---|---|---|
|
||||
| secret | env에 `*_TOKEN` key 등록 / `dist/`에 credential 패턴 | 거부·scan 실패 | `FE-GATE-013` | env(`FE-OC-004`)·supply-chain(`FE-OC-018`) |
|
||||
| HTML-injection (static, rule당 1개 = 3개) | ①`dangerouslySetInnerHTML` 렌더 ②`eval`/`new Function` ③런타임 script URL 조립 | 각 lint rule 실패 | `FE-GATE-002`→`FE-GATE-013` | 본 branch (D10 (b)) |
|
||||
| HTML-injection (render 시점) | 예외 sanitizer 경로에 malicious payload 주입 / subtree에 inline `<script>`·inline handler 존재 | component test 실패 | `FE-GATE-006` | 본 branch (D8) |
|
||||
| CSP 호환 | no-unsafe baseline(§3) 하 sample route 렌더 시 CSP violation 발생 | compatibility fixture 실패 | `FE-GATE-013` | 본 branch (D10 (a)) |
|
||||
| security header 선언-vs-실제 | 선언 CSP/HSTS/frame/referrer 정책 != 실제 hosting 응답 | `pnpm verify:hosting-headers`(security-header 축) 실패 | `FE-GATE-019@2` (Owner = release-cache) | 값만 header owner, 정책 공급은 본 branch(D9) |
|
||||
| storage | token/secret key 등록 시도 | 등록 거부 | `FE-GATE-013` | storage(`FE-OC-013`) |
|
||||
| telemetry | event에 raw URL/query/token 포함 | 전송 거부·scan 실패 | `FE-GATE-013` | observability(`FE-OC-014`) |
|
||||
|
||||
### 5. Source map production 정책
|
||||
|
||||
> **Trace**: D6 — `FE-OC-019` + hub §13.2.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 강제 메커니즘(예: Vite `build.sourcemap=false` vs post-build strip vs authenticated 경로 업로드)을 hub가 명명하지 않음. trade-off: `build.sourcemap=false`가 가장 단순하나 error-tracking symbolication을 포기 → 미결. "production public path에 `.map`이 존재하지 않는다"는 fixture 계약만 고정.
|
||||
|
||||
- fixture: production build 산출물의 public path에 `*.map`이 노출되지 않음.
|
||||
|
||||
### 6. `FE-GATE-006`(component gate)의 `FE-OC-019` 슬라이스
|
||||
|
||||
> **Trace**: D8 — `FE-OC-019`(owns) + contributes `FE-OC-020` + hub §15.1(`FE-GATE-006` Covered FE-OC에 `FE-OC-019` 포함)·§13.2(prohibited API)·§15.2(negative fixture 요건).
|
||||
>
|
||||
> - **소유 근거**: hub §15.1이 component gate의 Covered FE-OC로 `FE-OC-019`를 명시하므로 이 커버리지에는 owner가 있어야 한다. static lint(§2)는 *소스에 나타난* prohibited API만 잡고, 런타임 문자열 조립·서드파티 컴포넌트 경유로 생기는 sink는 렌더 결과에서만 관측된다 → component-level fixture가 필요하다.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: component test runner·assertion helper 형태(예: RTL `container.querySelector` 기반 subtree 검사)는 hub 미명명이며 test stack은 test-taxonomy(`FE-OC-020`) 소유. trade-off: helper를 지금 고정하면 그 branch의 도구 선택을 침범 → "렌더된 subtree에 금지 산출물이 없어야 하고, malicious payload는 fixture를 실패시킨다"는 계약만 고정.
|
||||
|
||||
| Component fixture | 대상 | 기대 결과 |
|
||||
|---|---|---|
|
||||
| malicious payload (positive-guard) | hub §13.2 예외 조건으로 승인된 sanitizer rendering 경로 | 알려진 XSS payload가 실행 가능한 노드로 남지 않음. sanitizer 우회 시 fixture 실패 |
|
||||
| prohibited 산출물 부재 assertion | sample route/컴포넌트의 렌더된 subtree | inline `<script>` 노드·inline event-handler attribute·`javascript:` URL 0건 |
|
||||
|
||||
- 예외 rendering 경로가 하나도 없는 skeleton 초기 상태에서는 첫 fixture가 "예외 경로 부재"를 확인하는 형태로 축약될 수 있으나, 예외가 승인되는 즉시 malicious fixture는 hub §13.2의 4종 evidence 요건상 필수다.
|
||||
- **상호 개정 완료(2026-07-21)**: [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] (`FE-OC-020`)의 `FE-GATE-006` row 는 fixture 본문 owner 를 `async-ui-state / render-recovery`로만 등재하고 있었다. 그 열(gate → fixture 본문 owner)의 owner 는 test-taxonomy 이므로 그쪽 표에 `browser-security(FE-OC-019 슬라이스)` 를 추가했다.
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- **예외 HTML rendering**: 불가피한 HTML 렌더가 필요할 때 sanitizer/allowlist/malicious fixture/CSP evidence 4종 중 하나라도 빠지면 → 예외 승인 거부(default 금지 유지). sanitizer 자체가 우회되면 malicious fixture가 실패로 잡아야 함.
|
||||
- **secret scan false-negative**: 도구·패턴이 `deferred`(§13.1)라 새 credential 형태를 못 잡을 수 있음 → 기대 동작: 도구 확정 시 known-secret 양성 fixture로 탐지율 검증.
|
||||
- **CSP runtime 위반**: 의존성 lib이 `eval`을 쓰면 strict CSP에서 런타임 깨짐 → 기대 동작: compatibility fixture가 CSP violation을 관측해 실패.
|
||||
- **storage fallback 노출**: quota 초과·private mode에서 값이 memory-only로 fallback될 때도 sensitive 값은 애초에 storage 대상이 아니어야 함(D4) → fallback이 sensitive 값을 노출하지 않음.
|
||||
- **telemetry 신규 attribute leak**: registry에 새 attribute 추가 시 PII/token이 섞이면 → forbidden-attribute scan fixture가 실패로 잡음.
|
||||
- **다른 계약 의존** (sibling의 local Decision ID + contract ID로 링크):
|
||||
- [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] `D6`(`FE-OC-010`) — token 저장 금지·`AUTH_TOKEN` forbidden을 이미 결정. 본 branch는 그 위반을 security fixture로 관측(공동 집행). 그 계약이 바뀌면 storage/telemetry fixture 조정.
|
||||
- [[raw/branch-notes/feature-frontend-storage-registry-contract]] `D6`(`FE-OC-013`) — `sensitive-forbidden` classification schema 소유. 본 branch는 token-key 등록 실패 fixture만 제공.
|
||||
- [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] `D2`(`FE-OC-014`) — redaction allowlist·transport-boundary 강제 소유. 본 branch는 forbidden-attribute leak fixture 기여.
|
||||
- [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] `D5`·`D6`(`FE-OC-018`) — `D5`가 security gate 묶음(secret+vuln+license+dependency review)이되 scanner·severity threshold는 `deferred`(hub §13.1)로 고정, `D6`이 secret scan을 source + built asset 양쪽으로 확정. 도구 자체는 **아직 pinned decision 없음(hub §13.1 `deferred`)** → 본 branch의 secret scan은 도구 중립 계약만.
|
||||
- [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] `D1`·`D6`(`FE-OC-016`) — `D1` 이 surface별 cache policy, `D6` 이 `FE-GATE-019@2` 의 security-header 축 검증 메커니즘 소유. 그 gate 는 2026-07-21 에 security header 까지 범위가 넓어졌으므로(Covered FE-OC 에 `FE-OC-019` 포함) security header 검증도 그쪽 소유이고, 본 branch 는 검증 대상 정책을 공급한다(D9). production directive **값**에 대해서는 hosting provider 미확정으로 **pinned decision 없음**.
|
||||
- [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] `D6`·`D3`(`FE-OC-020`) — `D6`이 gate → test level / fixture KIND taxonomy 소유(gate→FE-OC coverage 매핑 자체는 hub §15.1 소유), `D3`이 gate당 ≥1 고의 실패 negative fixture 원칙 소유. 본 branch의 security fixture는 그 taxonomy에 plug-in하고 어느 표도 복제·재정의하지 않는다. `FE-GATE-006` fixture 본문 owner 목록은 그쪽 `D6` 소관이며 2026-07-21 에 `browser-security` 가 추가됐다.
|
||||
- [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] `D2`(`FE-OC-008`) — normalized failure가 §8.1 safe 필드만 담고 raw body·token·authorization header·full URL/query·stack·storage value를 drop하도록 소유. 본 branch의 telemetry leak fixture와 경계 공유.
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| env·bundle·HTML 어디에도 secret이 새지 않는다 | 코드·CI 없음; scanner 도구 `deferred` | env name-reject unit fixture + source/`dist/` secret scan에 known-secret 양성 fixture 삽입 후 실패 확인 | `needs-confirmation` |
|
||||
| `dangerouslySetInnerHTML`·`eval`·dynamic code가 CI에서 차단된다 | lint rule/plugin 미확정 | 금지 rule **3종 각각**의 negative fixture(고의 위반)가 대응 lint rule 실패로 잡히는지 실행(D10 (b)) | `needs-confirmation` |
|
||||
| bundle이 `unsafe-inline`/`unsafe-eval` 없는 strict CSP에서 동작한다 | 의존성 중 eval 사용 lib 여부 미확인 | §3의 **no-unsafe 정본 test baseline** 하 sample route e2e에서 CSP violation 0 관측 — hosting owner의 production 값 확정을 기다리지 않고 실행 가능(D10 (a)) | `needs-confirmation` |
|
||||
| 렌더된 subtree에 inline `<script>`/inline handler/`javascript:` URL이 없고 sanitizer 예외 경로가 malicious payload를 실행하지 않는다 | component test stack 미확정, 예외 경로 미존재 | `FE-GATE-006` component fixture 2종(§6) 실행 → malicious payload 실패·prohibited 산출물 0건 확인(D8) | `needs-confirmation` |
|
||||
| 선언된 security header 정책이 실제 hosting 응답과 일치한다 | header 값은 외부 owner이고 실제 응답이 아직 미측정 (gate 귀속은 `FE-GATE-019@2` 로 확정 — D9) | `pnpm verify:hosting-headers`(security-header 축)로 HTML/config/manifest 응답의 CSP/HSTS/frame/referrer 대조 → `FE-GATE-019@2` | `planned` |
|
||||
| token/secret key 등록 시도가 실패한다 | storage registry 구현 없음 | storage token-key 등록 negative fixture(§15.2) 실행 → 거부 확인 | `needs-confirmation` |
|
||||
| telemetry event/normalized failure에 forbidden attribute가 없다 | redaction 강제 위치는 observability adapter | forbidden-attribute(raw URL/query/token) 포함 event negative fixture(§15.2) → 전송/scan 실패 확인 | `needs-confirmation` |
|
||||
| production public path에 source map이 없다 | build 미실행 | production build 후 public path에 `*.map` 부재 fixture | `needs-confirmation` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
- 스캐폴딩 단계: `/coverage` 실행 전 수동 행을 만들지 않는다.
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
- **위임처 gate 범위 과대 가정(해소 완료)** — 초판은 security header의 declared-vs-actual 검증을 `FE-GATE-019`에 위임했으나, 당시 hub §15.1의 해당 row는 Cache-Control/content-type 전용이고 Covered FE-OC도 `FE-OC-016` 하나뿐이었다. 위임처 노트 본문에도 CSP/HSTS 언급이 0건이라 실제로는 어느 gate에도 착지하지 않는 상태였다. D9로 `FE-GATE-013`에 잠정 배치한 뒤 hub 개정을 권고했고, **2026-07-21 에 그 권고가 채택돼 `FE-GATE-019@2` 로 확정됐다**.
|
||||
- **`FE-GATE-006`의 `FE-OC-019` 커버리지 무주공산(해소 완료)** — hub §15.1은 component gate가 `FE-OC-019`를 덮도록 요구하지만, 초판은 이를 TODO의 "기여" 한 줄로만 언급하고 Decision·fixture를 두지 않았다. 위임 후보인 test-taxonomy 계약의 `FE-GATE-006` row도 fixture 본문 owner에 security를 넣지 않아 owner가 공백이었다. D8 + §구현 가이드 6으로 본 branch가 소유를 확정했고, **2026-07-21 에 test-taxonomy 의 해당 row 도 갱신됐다**.
|
||||
- 교훈: 위임 문장을 쓸 때 위임처 *노트*의 존재만이 아니라 hub gate row의 **pass condition과 Covered FE-OC 문자열**까지 확인해야 한다. gate 이름이 그럴듯하다고 범위가 넓은 것은 아니다.
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: received-delegations:start -->
|
||||
### 수신한 위임
|
||||
|
||||
| Delegation Ref | From | Concern | Status |
|
||||
|---|---|---|---|
|
||||
| `DELEG-FE-001@1` | [[raw/branch-notes/feature-tailwind-design-token-styling-contract]] | `fe.deleg.dynamic-class-lint` | accepted |
|
||||
<!-- GENERATED: received-delegations:end -->
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-GATE-002@1` | [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]] | 금지된 API·import 가 남아 있으면 merge 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-006@1` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | component 레벨이 실패하면 merge 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-013@1` | [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] | secret·vulnerability·license·dependency review 정책 위반이면 merge·release 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-019@2` | [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] | 선언한 Cache-Control·content-type·security header 와 실제 응답이 다르면 release 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-OC-004@1` | [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] | build-time, runtime-public, secret config를 MUST 분리하고 boot 전에 runtime config를 검증 | import 참조로 적용 |
|
||||
| `FE-OC-008@1` | [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] | 모든 failure는 stable frontend error kind로 MUST 정규화하고 raw body·stack을 UI에 노출하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-010@1` | [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] | skeleton은 session state를 소비하되 token lifecycle을 MUST 소유하지 않음 | import 참조로 적용 |
|
||||
| `FE-OC-013@1` | [[raw/branch-notes/feature-frontend-storage-registry-contract]] | storage key는 namespace·version·classification을 MUST 가지며 token/secret 저장을 금지 | import 참조로 적용 |
|
||||
| `FE-OC-014@1` | [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] | telemetry는 best-effort이며 render·API success를 차단하면 안 되고 PII·token을 전송하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-016@1` | [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] | HTML, asset, runtime config, release manifest cache policy를 MUST 구분 | import 참조로 적용 |
|
||||
| `FE-OC-018@1` | [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] | frozen lockfile, dependency review, secret scan, SBOM 또는 dependency inventory를 release gate에 MUST 포함 | import 참조로 적용 |
|
||||
| `FE-OC-020@1` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | gate 종류별 책임·fixture·artifact를 분리하고 실패를 warning으로 낮추면 안 됨 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
<!-- GENERATED: sources:start -->
|
||||
- [[raw/official-docs/owasp-content-security-policy-cheat-sheet]]
|
||||
<!-- GENERATED: sources:end -->
|
||||
|
||||
- 없음 — 자식 자료는 생성 후 controller가 parent Cluster와 함께 등록한다.
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
- 없음 — daily note는 이 작업에서 수정하지 않는다.
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크: 없음
|
||||
- 리뷰 메모: 없음
|
||||
- 머지 결과 / 배포 환경: `planned`
|
||||
- **wiki 추출 대상** (verified만): 없음
|
||||
- **추출하지 않을 항목** (planned / documented-only / abandoned): 현재 전체
|
||||
+345
@@ -0,0 +1,345 @@
|
||||
---
|
||||
title: branch / feature-frontend-build-bundle-supply-chain-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-020
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-020
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-BUILD-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-SUPPLY-CHAIN-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-001, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-017]
|
||||
contract_packet: 1
|
||||
branch: feature-frontend-build-bundle-supply-chain-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract]
|
||||
tags: [branch, ca-skeleton, frontend, ci-cd, build-tooling, supply-chain]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
contract_packet_sha256: 3dccf22904aa14c909b778cb3242f70be7a11dce4256b8fdaed40f5a1ad36035
|
||||
imports: [ART-FE-001@1, FE-GATE-001@1, FE-OC-003@1, FE-OC-016@1, FE-OC-019@1, FE-OC-020@1, FE-OC-021@1]
|
||||
---
|
||||
|
||||
# branch: feature-frontend-build-bundle-supply-chain-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — TODO·결정·진행 기록. 구현 결과는 검증 뒤 `/ingest`로만 추출한다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
[[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: frozen build·inventory·scan·bundle report가 CI artifact로 생성된다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-BUILD-001@1` | Vite client-only SPA를 build baseline으로 사용한다 | clean production build와 bundle report gate에 적용 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-SUPPLY-CHAIN-001@1` | dependency lock, secret scan, vulnerability scan, license inventory, dependency review를 merge/release gate로 분리한다 | install·security·inventory·dependency review gate에 적용 | [[raw/project-notes/ca-skeleton-frontend-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 -->
|
||||
## 목표
|
||||
|
||||
이 브랜치는 project-wide 계약 `FE-OC-018` (frozen lockfile · dependency review · secret scan · SBOM/dependency inventory 를 release gate 에 MUST 포함) 를 *되묻지 않고 구현 착수 가능한* 명세로 내린다. 근거 결정은 hub 의 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D024 (supply-chain control 을 **분리된** merge/release gate 로 운영 — control 열거는 hub 소유) 이며, hub §13.1 (supply-chain minimums), §12.1 (release artifact set), §14.3 (planned commands), §15.1 의 `FE-GATE-001`/`FE-GATE-011`/`FE-GATE-012`/`FE-GATE-013` 를 구현 blueprint 로 삼는다. 부수적으로 `FE-OC-003` (frozen install), `FE-OC-016` (release artifact), `FE-OC-019` (secret-in-bundle 경계), `FE-OC-020` (gate 분리), `FE-OC-021` (bundle NFR) 에 기여한다. **현 시점 frontend 코드/CI 는 존재하지 않으므로 아래 모든 항목은 `planned` 등급이다** — "구현했다" 가 아니라 "이렇게 구현될 것이다" 의 사전 명세다.
|
||||
|
||||
- 이슈: 없음 (스캐폴딩 단계)
|
||||
- PR: 없음
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- **frozen-lockfile install gate** — lockfile drift 없이 재현 가능한 install 을 merge+release 차단 gate 로 강제 (`FE-GATE-001`, `FE-OC-018`).
|
||||
- **clean production build gate** — hashed immutable static asset + build-manifest 산출을 merge+release 차단 gate 로 강제 (`FE-GATE-011`, `FE-OC-018`).
|
||||
- **bundle report gate** — release 시 app + lazy chunk 크기를 machine-readable report 로 산출 (`FE-GATE-012`, `FE-OC-018`). (수치 threshold 자체는 아래 Out of scope.)
|
||||
- **security gate** — secret scan · vulnerability scan · license inventory · **dependency review** 를 하나의 차단 gate 로 묶어 SARIF/inventory/dependency-diff 산출 (`FE-GATE-013`, `FE-OC-018`).
|
||||
- **dependency review (dependency diff)** — base↔head lockfile 을 direct + transitive 까지 diff 해 변경 집합을 산출하고, review 기록 없는 high-risk change 를 차단 gate 로 처리 (hub §13.1 `dependency review` row, `FE-OC-018`). 본 브랜치가 `FE-OC-018` 소유자이며 sibling [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] 가 이 관심사를 본 브랜치로 명시 위임했다.
|
||||
- **release supply-chain artifact set** — dependency inventory · build-manifest(provenance metadata) · checksums 를 release artifact 로 명세 (hub §12.1, §13.1).
|
||||
- **vulnerability suppression policy** — reason·owner·expiry·affected package·compensating control 을 강제하고 expiry 경과 suppression 을 gate failure 로 처리 (hub §13.3).
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외 — 다른 owner 브랜치의 결정 영역. "이건 범위에 없었습니다" 근거.
|
||||
|
||||
- **package manager 선택 및 lockfile 형식 확정** — [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] (`FE-OC-003`) 소유. 본 브랜치는 그 frozen-install 스크립트를 *소비*만 한다.
|
||||
- **bundle 크기 threshold 수치 (`FE-NFR-001` ≤200 KiB, `FE-NFR-002` ≤120 KiB) 와 측정 context** — [[raw/branch-notes/feature-web-vitals-performance-budget-contract]] (`FE-OC-021`) 소유. 본 브랜치는 report 를 *생성*하고 pass/fail 판정은 위임.
|
||||
- **browser security boundary 규칙 (CSP·`dangerouslySetInnerHTML` 금지·frame/referrer policy)** — [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] (`FE-OC-019`) 소유. 본 브랜치는 secret scan *실행 gate* 만 담당.
|
||||
- **release manifest schema · cache policy · rollback drill** — [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] (`FE-OC-016`) 소유. 본 브랜치 artifact 는 그 release set 에 *공급*될 뿐이다.
|
||||
- **CI gate orchestration · 실행 순서 · artifact retention 정책** — 배선은 [[raw/branch-notes/feature-frontend-ci-quality-gates-contract]](어느 `FE-OC-*` 의 owner 도 아닌 기여 브랜치), taxonomy 는 [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] (`FE-OC-020` owner) 소유. 본 브랜치는 gate 를 *제공*, 배선은 위임.
|
||||
- **scanner 도구·severity threshold·SBOM 형식 확정** — hub §13.1 이 `deferred` 로 명시 (organization security policy 부재). 임의 확정 금지.
|
||||
- **runtime config artifact (`dist/config.json`, `dist/config/runtime-config.schema.json`)** — hub §12.1 release artifact set 에 함께 나열되지만 소유는 [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] (`FE-OC-004`). 본 브랜치의 release artifact 책임은 supply-chain 3종(inventory·build-manifest·checksums)뿐이며 config 산출/검증은 위임한다.
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/official-docs/supply-chain-slsa-provenance-framework]] | `SLSA-FW-C1`/`C4`/`C5` — provenance = build platform/process/top-level input 을 기술하는 verifiable 정보. release build-manifest(buildId/commit) 를 provenance 최소선으로 두는 D7 의 공식 근거. signed attestation(L2+) 은 미채택 표지. |
|
||||
| [[raw/official-docs/vite-build-tool-official]] | `VITE-C2` — production build 가 optimized 정적 자산을 산출 (D3 clean build gate 근거). `VITE-C3`/`C4`/`C5` — `import.meta.env` build-time 정적 치환 + `VITE_` prefix 만 클라이언트 노출 + 비밀값 금지 (D6 built-asset secret scan 경계 근거). |
|
||||
|
||||
> 나머지 세부 (gate 분리·§13.1 control·§12.1 artifact·suppression policy) 의 근거는 외부 문서가 아니라 **hub 자체의 project decision** 이므로 Evidence Map 에서 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D024 로 인용한다 (hub §3.2 가 `accepted-documented-only` 로 명시).
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] frozen-lockfile install gate (`FE-GATE-001`) 명세 — command·drift 검출·install log artifact — 등급: `planned`
|
||||
- [ ] clean production build gate (`FE-GATE-011`) 명세 — hashed asset + build-manifest 산출 — 등급: `planned`
|
||||
- [ ] bundle report gate (`FE-GATE-012`) 명세 — machine-readable bundle report (threshold 판정은 `FE-OC-021` 위임) — 등급: `planned`
|
||||
- [ ] security gate (`FE-GATE-013`) 명세 — secret/vuln/license/dependency-review fixture + SARIF/inventory/dependency-diff — 등급: `planned`
|
||||
- [ ] dependency review 명세 — base↔head lockfile direct+transitive diff · high-risk 분류축 · review 기록 · dependency diff report — 등급: `planned`
|
||||
- [ ] release supply-chain artifact set (dependency inventory·checksums·build-manifest) + provenance metadata 정의 — 등급: `planned`
|
||||
- [ ] vulnerability suppression policy (reason·owner·expiry·affected package·compensating control) 정의 — 등급: `planned`
|
||||
- [ ] scanner/SBOM/threshold `deferred` 항목의 revisit trigger (organization security policy) 문서화 — 등급: `planned`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
- scanner 도구명·severity threshold·SBOM 형식(CycloneDX/SPDX)·suppression expiry SLA 는 hub §13.1/§13.3 이 `deferred` 로 명시 — 특정 도구를 썼다고 주장하지 않는다.
|
||||
- 모든 command(`pnpm install --frozen-lockfile`·`pnpm build`·`pnpm check:bundle`·`pnpm scan:security`)와 artifact 경로는 hub §14.3/§12.1 의 planned contract 이며 실행/검증되지 않았다 (`PLANNED_NOT_EXECUTED`).
|
||||
- **hub 내부 불일치 발견 → 해소 완료**: hub §2.1 의 `FE-OC-018` 과 §13.1 은 `dependency review` 를 요구하는데 §3.2 `FE-D024` 본문과 §15.1 `FE-GATE-013` required fixtures 는 그것을 누락하고 있었다. 본 브랜치가 상위 계약을 따라 D9 로 편입했고, **hub 도 정정됐다** — 현재 `FE-D024` 는 dependency review 를 포함해 열거하고(lock 은 `FE-GATE-001`, 나머지 4개는 `FE-GATE-013`), §15.1 `FE-GATE-013` required fixtures 도 `secret/vulnerability/license/dependency-review` 다.
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> 근거는 Sources 또는 hub project decision 을 가리킨다. FE-D### 인용은 hub 경로에 붙인다 (Evidence Map 과 mirror).
|
||||
|
||||
- 2026-07-18: supply-chain control 을 **하나의 monolithic gate 가 아니라** dependency lock/secret/vuln/license 로 분리된 merge/release gate 로 운영 / 이유: 실패 지점을 구분해 blocking scope 를 정확히 하기 위함 / 검토한 대안: 단일 "security gate" 통합 / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D024 (D1).
|
||||
- 2026-07-18: frozen-lockfile install 을 merge+release 차단 gate 로 강제, drift = FAIL / 이유: 재현 가능한 install / 검토한 대안: 비-frozen install 후 사후 검증 / 근거: hub §13.1 install row + `FE-GATE-001` (D2).
|
||||
- 2026-07-18: production build gate 는 hashed immutable asset + build-manifest 산출 / 이유: 정적 호스팅 배포 + release 식별 / 검토한 대안: unhashed asset / 근거: `raw/official-docs/vite-build-tool-official.md#VITE-C2` + hub §12.1 (D3).
|
||||
- 2026-07-18: bundle report 는 생성하되 수치 threshold 판정은 `FE-OC-021` 에 위임 / 이유: NFR context/threshold 소유권 분리 / 근거: hub §14.2 (`FE-NFR-001`/`FE-NFR-002`) + `FE-GATE-012` (D4).
|
||||
- 2026-07-18: security gate 는 secret+vuln+license 를 묶고 scanner/threshold 는 `deferred` / 이유: org policy 부재로 도구 확정이 불가 / 검토한 대안: 지금 특정 scanner 확정 / 근거: hub §13.1 (D5).
|
||||
- 2026-07-18: secret scan 은 source 뿐 아니라 **built asset** 까지 검사 / 이유: browser bundle 은 public artifact 이고 `VITE_` 값은 build-time 에 정적 inline 되므로 / 근거: `raw/official-docs/vite-build-tool-official.md#VITE-C4`, `#VITE-C5` + hub §13.2 (D6).
|
||||
- 2026-07-18: release 는 dependency inventory + build-manifest(buildId/commit provenance) + checksums 를 포함, mismatch = 차단; signed SLSA attestation 은 미채택 / 이유: provenance 최소선 확보 / 근거: `raw/official-docs/supply-chain-slsa-provenance-framework.md#SLSA-FW-C1`, `#SLSA-FW-C4` + hub §12.1 (D7).
|
||||
- 2026-07-18: vulnerability suppression 은 reason·owner·expiry·affected package·compensating control 을 요구, expiry 경과 = gate failure / 이유: 무기한 예외 방지 / 근거: hub §13.3 (D8).
|
||||
- 2026-07-20: dependency review 를 `FE-GATE-013` 의 **네 번째 control** 로 편입 (별도 gate ID 신설 대신 기존 gate 범위 확장). base↔head lockfile 을 direct+transitive 까지 diff 하고, review 기록 없는 high-risk change = 차단, 산출물은 dependency diff report / 이유: hub §2.1 `FE-OC-018` 과 §13.1 이 dependency review 를 요구하는데 소유 gate 가 없었다. 새 `FE-GATE-027` 을 만들면 hub §15.1 의 "26개 row" registry 와 §15.3 promotion formula 를 동시에 고쳐야 하는데 그건 hub 소유 변경이라 본 브랜치 권한 밖이다. `FE-GATE-013` 은 이미 `FE-OC-018` 을 covered 하고 blocking scope 도 merge+release 로 dependency review 요구와 일치한다 / 검토한 대안: (a) 신규 gate ID 신설 — hub registry 변경 필요로 기각, (b) `FE-GATE-001`(lockfile) 에 합류 — 그쪽은 drift 유무만 보는 결정론 검사라 "변경 내용의 위험도 심사"라는 성격이 다르고 실패 의미가 섞임 / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D024 + hub §2.1 `FE-OC-018` + hub §13.1 dependency review row (D9).
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | supply-chain control 을 **분리된** merge·release gate 로 운영 (`FE-OC-018`) — control 집합은 hub `FE-D024` 소유이며 dependency review 는 D9 로 편입됐다 | 이 분리가 project 최소선; organization security policy 가 더 강한 gate 를 지정하면 강화·재분할 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D024 | `project-decision` | repo/CI 부재 → gate 배선 UNVERIFIED |
|
||||
| D2 | frozen-lockfile install 을 merge+release 차단 gate, drift = FAIL (`FE-GATE-001`, `FE-OC-018`) | frozen install 은 항상 필수; package manager/lockfile *형식*은 `FE-OC-003` (bootstrap) 소유 → 그쪽 변경 시 command 만 교체 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D024 (hub §13.1 install row) | `project-decision` | pnpm default 는 bootstrap 결정; org 가 npm/yarn 강제 시 install command 재확정 |
|
||||
| D3 | production build gate = hashed immutable asset + build-manifest 산출 (`FE-GATE-011`, `FE-OC-016`/`FE-OC-018`) | Vite client-only SPA build baseline 이 유지되는 한; SSR/edge rendering 이 requirement 가 되면 build 출력 형태 재검토 | `raw/official-docs/vite-build-tool-official.md#VITE-C2` | `official-doc` | build 미존재 → artifact 이름/경로는 planned |
|
||||
| D4 | bundle report 는 release gate 로 *생성*, 수치 threshold 판정은 위임 (`FE-GATE-012`, `FE-OC-018`/`FE-OC-021`) | report 는 항상 release 에 산출; `FE-NFR-001`(≤200 KiB)/`FE-NFR-002`(≤120 KiB) 값과 `FE-NFR-C04` context 는 web-vitals 브랜치가 소유·재검토 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D024 (hub §14.2) | `project-decision` | report↔threshold 소유 경계; threshold 변경은 `FE-OC-021` 에서 |
|
||||
| D5 | security gate = secret+vuln+license 묶음(2026-07-20 D9 로 dependency review 가 4번째 control 로 편입), scanner/severity threshold 는 `deferred` (`FE-GATE-013`, `FE-OC-018`) | 이 구성이 최소선; repository/organization policy 가 생기면 특정 scanner·threshold 확정 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D024 (hub §13.1) | `conditional-default` | 지금 scanner 명시 = 날조 → deferred 유지 |
|
||||
| D6 | secret scan 은 source + **built asset** 모두 검사 (`FE-OC-018`/`FE-OC-019`) | browser bundle 을 public artifact 로 간주하는 한 항상; boundary 규칙(CSP·HTML injection) 자체는 `FE-OC-019` 소유 | `raw/official-docs/vite-build-tool-official.md#VITE-C4`, `#VITE-C5` (hub §13.2) | `official-doc` | scan 이 로그·debug 등 *모든* 유출 경로를 증명하진 못함 (VITE-C4 does-not-prove) |
|
||||
| D7 | release 는 dependency inventory + build-manifest(buildId/commit provenance) + checksums 포함, mismatch 차단; signed attestation 미채택 (`FE-OC-018`, hub §12.1) | 최소선 = inventory + build metadata 를 provenance 로; org 가 더 강한 provenance 요구 시 signed SLSA(L2+) 채택 | `raw/official-docs/supply-chain-slsa-provenance-framework.md#SLSA-FW-C1`, `#SLSA-FW-C4`, `#SLSA-FW-C5` | `official-doc` | L1 provenance 는 "trivial to forge" (SLSA-FW-C1) — signing/SBOM 형식 deferred |
|
||||
| D8 | vulnerability suppression 은 reason·owner·expiry·affected package·compensating control 필수, expiry 경과 = gate failure (`FE-OC-018`, hub §13.3) | fix 즉시 불가한 accepted vuln 에 적용; org 가 더 엄격한 SLA 정의 시 재검토 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D024 (hub §13.3) | `project-decision` | expiry SLA 수치 미정 → deferred |
|
||||
| D9 | dependency review 를 `FE-GATE-013` 의 4번째 control 로 편입: base↔head lockfile direct+transitive diff, review 기록 없는 high-risk change = 차단, dependency diff report 산출 (`FE-OC-018`) | hub §15.1 gate registry 가 26 row 로 고정된 동안은 기존 gate 확장; hub 가 registry+promotion formula 를 개정해 전용 gate 를 신설하면 그쪽으로 이관 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D024 (hub §2.1 `FE-OC-018` 문구 + hub §13.1 dependency review row) | `project-decision` | hub `FE-D024` 가 dependency review 를 누락하던 불일치는 hub 정정으로 해소됨(현재 5개 control 열거, `FE-GATE-013` fixtures 도 dependency-review 포함). "high-risk" 판정축·diff 도구는 hub 미지정 → `UNSUPPORTED_IMPL_DECISION` |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> `planned` blueprint — frontend 코드/CI 는 아직 없다. 경로·command 는 hub §4.6/§12.1/§14.3 의 planned contract 에서 도출한 anchor 이며 repository 생성 시 확정된다. CLAUDE.md §15.5 R1(Trace)/R2(UNSUPPORTED_IMPL_DECISION)/R3(OUT_OF_BRANCH_SCOPE) 준수.
|
||||
|
||||
### 1. Gate topology — merge vs release 분리
|
||||
|
||||
> **Trace**: D1 ([[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D024), D2/D3/D4/D5/D9 — hub §15.1 gate registry + `FE-OC-018`.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — blocking scope·covered contract·artifact 는 hub §15.1 이 직접 명시. `FE-GATE-013` 의 control 4번째(dependency review) 편입은 D9 근거이며 gate ID 신설이 아니므로 hub registry row 수(26)를 바꾸지 않는다.
|
||||
|
||||
> gate 의 **blocking scope · Covered FE-OC · evidence artifact 는 hub §15.1 이 소유**한다. 아래 표는 그 열을 옮겨 적지 않고, 본 브랜치가 각 gate 안에서 *무엇을 명세하는지*(control) 만 담는다. 값이 필요하면 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.1 을 본다.
|
||||
|
||||
| Gate ID | Control (본 브랜치 명세) |
|
||||
|---|---|
|
||||
| `FE-GATE-001` | frozen install drift |
|
||||
| `FE-GATE-011` | clean production build |
|
||||
| `FE-GATE-012` | bundle report 생성 (판정은 `FE-OC-021` owner 위임) |
|
||||
| `FE-GATE-013` | ① secret scan ② vulnerability scan ③ license inventory ④ **dependency review** (D9) |
|
||||
|
||||
- 실패는 warning 으로 낮추지 않는다 (`FE-OC-020`). 각 gate 는 최소 1개의 deliberately-failing negative fixture 로 "실제 동작"을 증명해야 한다 (hub §15.2). `FE-GATE-013` 은 4개 control 각각이 독립 negative fixture 를 갖는다 (§5).
|
||||
- gate 는 4개지만 control 은 7개(install·build·bundle·secret·vuln·license·dependency review)다. D1 의 "분리" 원칙은 gate ID 개수가 아니라 **실패 지점이 artifact 단위로 구분 가능한가**로 만족시킨다 — `FE-GATE-013` 내부 4 control 은 서로 다른 artifact(SARIF · license inventory · dependency diff report)로 실패 원인을 구분한다.
|
||||
|
||||
### 2. Frozen-lockfile install gate
|
||||
|
||||
> **Trace**: D2 — hub §13.1 install row + §14.3.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — command·artifact 는 hub §14.3 이 명시. package manager 명(pnpm)은 본 브랜치 결정이 아님 → §OUT_OF_BRANCH_SCOPE 참조.
|
||||
|
||||
- command: `pnpm install --frozen-lockfile` (hub §14.3, `PLANNED_NOT_EXECUTED`).
|
||||
- pass 조건: manifest ↔ lockfile drift 없음, exit 0.
|
||||
- artifact: `artifacts/quality/install.txt` (hub §14.3) / `artifacts/quality/lockfile-check.txt` (hub §13.1).
|
||||
- negative fixture: lockfile drift(수동 편집) → frozen install 이 exit≠0 로 실패해야 함.
|
||||
- **OUT_OF_BRANCH_SCOPE**: package manager 선택·`packageManager` field·`pnpm-lock.yaml` commit 은 [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] (`FE-OC-003`) 소유. 본 gate 는 그 lockfile 을 frozen 으로 *검증*만 한다.
|
||||
|
||||
### 3. Clean production build gate
|
||||
|
||||
> **Trace**: D3 — `raw/official-docs/vite-build-tool-official.md#VITE-C2` + hub §12.1 artifact set.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — build command·artifact·hash 규칙은 hub §12.1/§14.3 + VITE-C2 에서 도출.
|
||||
|
||||
- command: `pnpm build` (hub §14.3).
|
||||
- pass 조건: exit 0 + 기대 artifact 존재.
|
||||
- 산출 artifact (hub §12.1): `dist/index.html`, `dist/assets/<content-hash>.*` (immutable hashed), `dist/release-manifest.json`, `artifacts/release/build-manifest.json`, `artifacts/release/checksums.txt`.
|
||||
- hashed asset 의 immutable cache 정책 자체는 [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] (`FE-OC-016`) 소유 — 여기서는 hash 산출까지만.
|
||||
|
||||
### 4. Bundle report gate
|
||||
|
||||
> **Trace**: D4 — hub §14.3 (`pnpm check:bundle`) + §14.2 (`FE-NFR-001`/`FE-NFR-002`).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: (a) bundle 분석 도구(rollup-plugin-visualizer / 자체 스크립트 등)는 hub 가 지정하지 않음 → 도구 선택은 repo 생성 시 결정. trade-off: 지금 도구명을 박으면 날조가 되므로 report *형식*(machine-readable JSON)만 고정하고 도구는 미정. (b) `bundle.json` 의 필드명·구조는 **2026-07-21 해소됨** — hub §2.1.3 이 `ART-FE-002@1` 로 등록하고 `bundle-report.schema.json` 이 정본이다. 아래 §schema 참조.
|
||||
|
||||
- command: `pnpm check:bundle` (hub §14.3).
|
||||
- artifact: `artifacts/performance/bundle.json` (machine-readable, hub §14.3).
|
||||
- 측정 대상: initial JS(app) + 각 lazy route chunk 의 gzip 크기.
|
||||
- pass/fail 판정: `FE-NFR-001` (initial JS gzip ≤ 200 KiB), `FE-NFR-002` (lazy chunk gzip ≤ 120 KiB), context `FE-NFR-C04`.
|
||||
- **schema = `ART-FE-002@1`** (hub §2.1.3 · `harness/source/artifact-schemas/ca-skeleton-frontend/bundle-report.schema.json`). 이전 판은 이 스키마를 "공동 소유라 단독 결정 불가" 로 두고 필드 초안을 여기에 적었는데, 그래서 producer(`runner.node`)와 consumer(snake_case) 가 서로 다른 키 이름을 계약이라 부르는 상태가 됐다. 이제 **Schema Owner 는 본 브랜치 단독**이고 필드 추가·rename 은 스키마 파일을 고쳐 revision 을 올리는 것으로 하며, 소비 branch 는 `imports` pin 이 낡아 자동으로 잡힌다.
|
||||
- `context`/`runner` 는 hub §14.1 의 "context 없는 숫자는 evidence 로 인정하지 않는다" 요구 때문에 required 다 (`FE-NFR-C04`).
|
||||
- `buildId`/`commit` 은 §6 build-manifest(`ART-FE-001@1`)와 동일 값이어야 하며, 이 대조로 report 가 어느 build 의 것인지 식별된다.
|
||||
- **OPEN QUESTION — budget 이 JS-only 인가 CSS 포함인가**: hub §14.2 는 `FE-NFR-001` 을 "initial JS gzip", `FE-NFR-002` 를 "any lazy route chunk gzip" 으로만 정의하고 **CSS 전용 NFR ID 가 없다**. 따라서 현재 계약은 *JS-only 판정*으로 읽는 것이 문언에 충실하다. 본 gate 는 CSS asset 의 gzip 크기도 report 에 **기록은 하되 판정 대상으로 삼지 않는다**. CSS 를 budget 에 포함할지, 별도 NFR ID 를 신설할지는 `FE-OC-021` 소유자와 hub §14.2 개정 사항이다.
|
||||
- **OUT_OF_BRANCH_SCOPE**: 위 threshold 수치·측정 context 정의는 [[raw/branch-notes/feature-web-vitals-performance-budget-contract]] (`FE-OC-021`) 소유. 본 gate 는 report 를 *생성*하고 threshold 를 *소비*한다.
|
||||
|
||||
### 5. Security gate — secret · vulnerability · license · dependency review
|
||||
|
||||
> **Trace**: D5/D6/D8/D9 — hub §13.1 (secret/vuln/license/**dependency review** row) + §13.3 (suppression) + §2.1 `FE-OC-018` + `VITE-C4`/`C5`.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: (a) scanner 도구(secret: gitleaks/trufflehog?, vuln: npm audit/osv-scanner/trivy?, license: 자체?) 미정, (b) severity threshold(어느 CVSS 등급부터 차단) 미정, (c) suppression expiry SLA(며칠) 미정. **모두 hub §13.1/§13.3 이 `deferred` 로 명시** — 임의 확정 시 날조. trade-off: 지금은 gate *구조·fixture 계약*만 고정하고 도구·수치는 organization security policy 확정 후 채운다.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: (d) dependency diff **도구**(GitHub Dependency Review Action / `pnpm why` 기반 자체 스크립트 / osv-scanner diff 등) 미정 — hub §13.1 은 "direct/transitive diff" 라는 *대상*만 규정하고 도구를 지정하지 않는다. trade-off: 도구명을 지금 박으면 날조이므로 **입력(base↔head lockfile)·출력(dependency diff report)·차단 조건**만 고정한다. (e) "high-risk change" 의 **분류축**(아래 R1~R5) 도 hub 미지정 — hub 는 `unreviewed high-risk change` 라는 차단 조건만 준다. trade-off: 분류축이 없으면 gate 가 판정 불가능해 구현 착수가 막히므로, **fail-closed 기본값**(분류 불가/미기록 = high-risk 취급)을 두고 축 목록은 org policy 확정 시 교체 가능한 것으로 표시한다. 축을 좁게 잡으면 위험 변경이 통과하고, 넓게 잡으면 모든 renovate PR 이 수동 리뷰를 요구해 마찰이 커지는 trade-off 를 인지하고 fail-closed 를 택했다.
|
||||
|
||||
- command: `pnpm scan:security` (hub §14.3). **dependency review 도 이 command 안에서 수행한다** — hub §14.3 planned command 표에 dependency-review 전용 script 가 없으므로 새 script 명을 만들면 hub 계약과 어긋난다. script 를 분리하려면 hub §14.3 + §15.1 artifact mapping 을 함께 갱신해야 한다 (hub §14.3 말미 규칙).
|
||||
- artifact: `artifacts/security/scan.sarif` (hub §14.3) + license inventory + `artifacts/release/dependency-inventory.*` (hub §12.1) + dependency diff report (아래).
|
||||
- secret scan (D6): **source + built asset(`dist/`) 모두** 검사. 이유: `VITE_` prefix 값은 build-time 에 정적 inline 되므로(VITE-C3) 유출은 built bundle 에서만 관측될 수 있음(VITE-C4/C5). browser bundle = public artifact (hub §13.2).
|
||||
- vulnerability scan (D5): severity policy 위반이 approved expiry 없이 존재하면 차단 (hub §13.1).
|
||||
- license inventory: denied/unknown license 미해결 시 차단 (hub §13.1).
|
||||
- suppression (D8): 각 suppression 은 reason·owner·expiry·affected package·compensating control 보유; expiry 경과 suppression = gate failure (hub §13.3).
|
||||
|
||||
#### 5.1 Dependency review (control ④)
|
||||
|
||||
> **Trace**: D9 — hub §2.1 `FE-OC-018` (frozen lockfile · **dependency review** · secret scan · SBOM/inventory 를 release gate 에 MUST 포함) + hub §13.1 `dependency review` row (`direct/transitive diff` / `unreviewed high-risk change` / `dependency diff report`).
|
||||
|
||||
- **무엇을 diff 하는가 (입력)**: PR 의 **base commit lockfile ↔ head commit lockfile**. 두 lockfile 을 각각 resolve 해 얻은 *완전한 패키지 집합*(direct + transitive, 즉 lockfile 에 기록된 모든 resolved entry)을 비교한다. manifest(`package.json`) diff 만 보지 않는다 — hub §13.1 이 명시적으로 `direct/transitive` 를 요구하고, transitive 변경은 manifest 에 나타나지 않기 때문이다.
|
||||
- release 시점에는 base = **직전 release 의 lockfile**(release token 기준)로 잡아 release 단위 누적 변경도 같은 방식으로 산출한다.
|
||||
- **변경 분류 (출력 행)**: 각 diff row 는 `{package, from, to, changeKind, depth, riskFlags[], reviewRef}` 를 갖는다.
|
||||
- `changeKind` ∈ `added | removed | version-changed | resolution-changed`(같은 버전인데 resolved URL/integrity 가 바뀐 경우).
|
||||
- `depth` ∈ `direct | transitive`.
|
||||
- **무엇이 "unreviewed high-risk change" 인가 (차단 조건)**: 아래 두 조건을 **동시에** 만족하는 row 가 하나라도 있으면 `FE-GATE-013` FAIL.
|
||||
1. **high-risk 로 분류됨** — 아래 riskFlag 축 중 하나 이상에 해당. (축 목록 자체는 위 `UNSUPPORTED_IMPL_DECISION` (e).)
|
||||
- `R1 new-package` — 이전 lockfile 에 없던 패키지 추가 (direct/transitive 무관; 새 코드가 신뢰 경계에 들어옴).
|
||||
- `R2 install-script` — install/postinstall 등 lifecycle script 를 실행하는 패키지의 추가·변경.
|
||||
- `R3 major-bump` — semver major 상승 (hub §13.3 이 major update 에 `FE-D*` impact check + registry compatibility check 를 별도로 요구하므로 위험 등급이 다르다).
|
||||
- `R4 license-change` — 해당 패키지의 license 식별자가 변경됨 (license inventory control 과 교차).
|
||||
- `R5 known-vuln` — vulnerability scan 이 해당 패키지에 severity policy 위반을 보고함 (vulnerability control 과 교차).
|
||||
- **fail-closed 기본값**: riskFlag 산출에 필요한 metadata(license/lifecycle script/이전 버전)를 확보하지 못해 **분류 자체가 불가능한 row 는 high-risk 로 간주**한다. "정보 부족 = 통과" 는 gate 를 무력화하므로 채택하지 않는다.
|
||||
2. **review 기록이 없음** — 해당 row 에 대응하는 review record(reviewer, 날짜, 대상 package@version, 승인 사유)가 없거나, 기록의 `package@to` 가 실제 diff 와 불일치. review record 는 vulnerability suppression(D8, hub §13.3)과 **별개 트랙**이다: suppression 은 "알려진 취약점을 기한부로 감수", review 는 "이 의존성 변경을 사람이 보았다" 이며 후자는 expiry 를 갖지 않는 대신 **해당 package@version 에만** 유효하다(버전이 다시 바뀌면 재검토 대상).
|
||||
- low-risk row(위 축 어디에도 해당 없음)는 review 없이 통과한다 — 그렇지 않으면 patch 단위 갱신마다 gate 가 막혀 정책이 실질적으로 우회된다.
|
||||
- **evidence artifact (dependency diff report)**: hub §13.1 은 artifact 를 `dependency diff report` 라고만 명명하고 경로를 주지 않는다. 본 브랜치는 `artifacts/security/dependency-diff.json` 을 anchor 로 둔다 — hub §14.3 이 security 계열 artifact 를 `artifacts/security/` 아래 두므로(`scan.sarif`) 그 규약을 따른 것이다.
|
||||
- **UNSUPPORTED_IMPL_DECISION**: 위 파일명·경로는 hub 가 지정하지 않은 명명 결정. trade-off: 경로를 비워두면 CI 배선(`FE-OC-020` 소유자)이 artifact 를 수집할 수 없어 gate 가 성립하지 않으므로, hub 의 기존 디렉터리 규약에서 가장 마찰이 적은 이름을 anchor 로 고정하고 repository 생성 시 확정한다.
|
||||
- report 최소 내용: `{baseRef, headRef, rows[], blocking[]}` — `rows[]` 는 위 diff row 전체, `blocking[]` 은 차단 사유가 된 row 의 부분집합. 통과한 build 도 report 를 남긴다(변경 0건이면 빈 `rows[]`) — 산출 자체가 hub §13.1 의 evidence 요구다.
|
||||
- **negative fixture**: review record 없이 `R1 new-package` 에 해당하는 transitive 의존성을 추가한 fixture 가 `FE-GATE-013` 을 FAIL 시켜야 한다. 대칭으로, 동일 변경에 유효한 review record 를 붙이면 PASS 해야 한다(가짜 PASS 방지).
|
||||
- **OUT_OF_BRANCH_SCOPE**: review record 를 *어디에* 보관할지(PR label / repo 내 파일 / 외부 시스템)와 reviewer 권한 모델은 CI orchestration 영역으로 [[raw/branch-notes/feature-frontend-ci-quality-gates-contract]] 소유. 본 gate 는 "review record 가 조회 가능해야 한다"는 인터페이스 요구만 둔다. lockfile 형식·package manager 는 [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] (`FE-OC-003`) 소유이며 본 control 은 그 lockfile 을 *읽기*만 한다.
|
||||
|
||||
- negative fixture 후보: (i) `VITE_`-var 에 심은 가짜 secret 이 `dist/` 번들에서 탐지되어 실패, (ii) known-vuln 의존성이 approved expiry 없이 차단, (iii) 만료된 suppression 이 실패, (iv) denied license 가 실패, (v) review record 없는 신규 transitive 의존성 추가가 실패 (§5.1).
|
||||
|
||||
### 6. Release supply-chain artifact set & provenance
|
||||
|
||||
> **Trace**: D7 — hub §12.1 artifact set + §13.1 provenance/SBOM row + `SLSA-FW-C1`/`C4`/`C5`.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: SBOM 형식(CycloneDX vs SPDX)과 signed attestation(in-toto/DSSE, SLSA L2+) 채택 여부 미정 → hub §13.1 이 "tool selected by owner" 로 `deferred`. trade-off: 최소선(dependency inventory + build metadata)만 고정하고 signing 은 org 요구 시.
|
||||
|
||||
- release artifact (hub §12.1): `artifacts/release/dependency-inventory.*`, `artifacts/release/build-manifest.json`, `artifacts/release/checksums.txt`.
|
||||
- provenance 최소선: build-manifest 에 buildId/commit 을 기록해 build platform/process/top-level input 을 기술(SLSA-FW-C1, C4). buildId/commit mismatch = 차단 (hub §13.1 provenance row).
|
||||
- dependency inventory 는 SLSA `resolvedDependencies` 개념(build time 필요 artifact 의 collection, SLSA-FW-C5)에 대응하되 "완전성"을 주장하지 않는다("if known", SLSA-FW-C5 does-not-prove).
|
||||
- **미채택 표지**: SLSA L1 provenance 는 "trivial to forge"(SLSA-FW-C1) — signed/authenticated attestation 은 별도 결정이며 현재 채택하지 않는다.
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- lockfile drift → frozen install exit≠0 → `FE-GATE-001` FAIL.
|
||||
- production build 실패 또는 기대 artifact 누락 → `FE-GATE-011` FAIL.
|
||||
- bundle threshold 초과 → `FE-GATE-012` FAIL (판정 값은 `FE-OC-021` 소유).
|
||||
- built asset 에서 secret 패턴 hit → `FE-GATE-013` FAIL.
|
||||
- severity threshold 위반이 approved expiry 없이 존재 / 만료된 suppression → `FE-GATE-013` FAIL.
|
||||
- denied/unknown license 미해결 → `FE-GATE-013` FAIL.
|
||||
- review record 없는 high-risk dependency 변경(신규 패키지·install script·major bump·license 변경·known-vuln) → `FE-GATE-013` FAIL (§5.1).
|
||||
- dependency diff row 의 riskFlag 를 분류할 metadata 부재 → fail-closed 로 high-risk 취급 → review 없으면 `FE-GATE-013` FAIL (§5.1).
|
||||
- base lockfile 을 확정할 수 없음(base ref 소실·shallow clone) → dependency review 를 "통과" 로 처리하지 않고 gate ERROR 로 처리해 차단 (fail-closed).
|
||||
- release inventory 누락 또는 buildId/commit mismatch → release 차단 (hub §12.1/§13.1).
|
||||
- **다른 계약 의존** (sibling 링크는 `FE-OC-###` 로만 표기):
|
||||
- [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] (`FE-OC-003`) 에 의존 — package manager·lockfile·frozen-install 스크립트를 consume. 그 계약이 바뀌면 §2 install command 영향.
|
||||
- [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] (`FE-OC-020`) 에 의존 — 본 브랜치 gate 가 CI gate taxonomy/artifact 분리 규칙에 편입.
|
||||
- [[raw/branch-notes/feature-web-vitals-performance-budget-contract]] (`FE-OC-021`) 에 기여/의존 — bundle threshold 값·NFR context 를 그쪽에서 consume. **`artifacts/performance/bundle.json` 스키마의 Schema Owner 는 본 브랜치**(hub §2.1.3 `ART-FE-002@1`) — 그쪽은 소비자로서 `imports` 로 pin 한다.
|
||||
- [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] (`FE-OC-019`) 에 기여 — secret-in-bundle·untrusted-HTML 경계 규칙은 그쪽 소유, 본 브랜치는 scan gate 실행.
|
||||
- [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] (`FE-OC-016`) 에 기여 — 본 브랜치 artifact(inventory·manifest·checksums)가 release set 에 공급.
|
||||
- [[raw/branch-notes/feature-frontend-ci-quality-gates-contract]] 에 의존 — gate orchestration·artifact retention 은 그쪽 소유(그 브랜치는 `FE-OC-*` owner 가 아니다).
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| frozen install 이 lockfile drift 를 실제로 차단한다 | CI/repo 부재 | drift fixture 로 `pnpm install --frozen-lockfile` 이 exit≠0 → `artifacts/quality/install.txt` | `needs-confirmation` |
|
||||
| production build 가 기대 artifact set + hashed asset 을 산출한다 | build 미실행 | build gate fixture 로 `pnpm build` exit 0 + `artifacts/release/build-manifest.json` 존재 확인 | `needs-confirmation` |
|
||||
| bundle report 가 initial JS + lazy chunk gzip 을 machine-readable 로 기록한다 | 도구 미정 | `pnpm check:bundle` → `artifacts/performance/bundle.json` 스키마 검증 (threshold 판정은 `FE-OC-021`) | `needs-confirmation` |
|
||||
| secret scan 이 source 뿐 아니라 built asset 의 secret 을 탐지한다 | 코드/scanner 미정 | negative fixture: `VITE_`-var 의 가짜 secret 이 `dist/` 번들에서 탐지되어 `FE-GATE-013` FAIL | `needs-confirmation` |
|
||||
| vulnerability gate 가 known-vuln(무-expiry)과 만료된 suppression 을 차단한다 | scanner/threshold `deferred` | negative fixture 로 `pnpm scan:security` 가 두 경우 FAIL → `artifacts/security/scan.sarif` | `needs-confirmation` |
|
||||
| license inventory 가 denied/unknown license 를 flag 한다 | 도구 미정 | fixture: denied license 의존성이 security gate FAIL | `needs-confirmation` |
|
||||
| dependency review 가 base↔head lockfile 의 **transitive** 변경까지 잡아낸다 | diff 도구 미정, lockfile 미존재 | fixture: manifest 는 그대로 두고 transitive 만 바뀐 lockfile 로 `pnpm scan:security` → `artifacts/security/dependency-diff.json` 의 `rows[]` 에 해당 row 존재 | `needs-confirmation` |
|
||||
| review record 없는 high-risk 변경이 실제로 차단되고, record 를 붙이면 통과한다 | review record 저장 위치가 `FE-OC-020` 소유로 미확정 | negative/positive 쌍 fixture: 신규 transitive 패키지 추가 → record 없으면 FAIL, 있으면 PASS | `needs-confirmation` |
|
||||
| `bundle.json` 이 소비자(`FE-OC-021`)가 `FE-NFR-001`/`FE-NFR-002` 를 판정하기에 충분한 필드를 담는다 | 스키마(`ART-FE-002@1`)는 확정됐으나 실제 report 생성이 미실행 | 스키마대로 report 생성 후 web-vitals 판정 로직이 추가 필드 요구 없이 동작하는지 대조 | `needs-confirmation` |
|
||||
| release 가 dependency inventory + build-manifest(buildId/commit) + checksums 를 포함하고 mismatch 를 차단한다 | pipeline 부재 | release verification fixture 로 buildId/commit mismatch 차단 확인 | `needs-confirmation` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
- 스캐폴딩 단계: `/coverage` 실행 전 수동 행을 만들지 않는다.
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
- 없음 — 스캐폴딩 단계.
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: artifact-imports:start -->
|
||||
### 가져온 artifact 계약
|
||||
|
||||
| Artifact Ref | Owner | Producer | Schema Ref |
|
||||
|---|---|---|---|
|
||||
| `ART-FE-001@1` | [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] | [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] | `harness/source/artifact-schemas/ca-skeleton-frontend/build-manifest.schema.json` |
|
||||
<!-- GENERATED: artifact-imports:end -->
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-GATE-001@1` | [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] | lockfile 이 manifest 와 어긋나면 merge·release 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-OC-003@1` | [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] | package manager, engine, lockfile, source language, checkJs command를 한 곳에서 MUST 고정 | import 참조로 적용 |
|
||||
| `FE-OC-016@1` | [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] | HTML, asset, runtime config, release manifest cache policy를 MUST 구분 | import 참조로 적용 |
|
||||
| `FE-OC-019@1` | [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] | browser bundle에 secret을 넣지 않고 untrusted HTML injection을 기본 금지 | import 참조로 적용 |
|
||||
| `FE-OC-020@1` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | gate 종류별 책임·fixture·artifact를 분리하고 실패를 warning으로 낮추면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-021@1` | [[raw/branch-notes/feature-web-vitals-performance-budget-contract]] | NFR은 device/network/cache/build context와 함께 MUST 측정 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
- 없음 — 자식 자료는 생성 후 controller가 parent Cluster와 함께 등록한다.
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
- 없음 — daily note는 이 작업에서 수정하지 않는다.
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크: 없음
|
||||
- 리뷰 메모: 없음
|
||||
- 머지 결과 / 배포 환경: `planned`
|
||||
- **wiki 추출 대상** (verified만): 없음
|
||||
- **추출하지 않을 항목** (planned / documented-only / abandoned): 현재 전체
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
---
|
||||
title: branch / feature-frontend-ci-quality-gates-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-027
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-027
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-TEST-STACK-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-SUPPLY-CHAIN-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-DEPLOYMENT-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-017, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-020, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-024, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-025, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-026]
|
||||
contract_packet: 1
|
||||
branch: feature-frontend-ci-quality-gates-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract]
|
||||
tags: [branch, ca-skeleton, frontend, ci-cd, build-tooling, static-analysis, supply-chain]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
contract_packet_sha256: 642f71eb6bed0e706b19f3c814c85a626371ec65a0fc08eef602c9185b13e6dd
|
||||
imports: [FE-GATE-001@1, FE-GATE-002@1, FE-GATE-004@1, FE-GATE-012@1, FE-GATE-014@1, FE-GATE-016@1, FE-GATE-018@1, FE-GATE-021@1, FE-OC-016@1, FE-OC-017@1, FE-OC-018@1, FE-OC-019@1, FE-OC-020@1, FE-OC-021@1, FE-OC-022@1, FE-OC-023@1, FE-OC-025@1]
|
||||
---
|
||||
|
||||
# branch: feature-frontend-ci-quality-gates-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — TODO·결정·진행 기록. 현재는 `/branch-spec` 로 spec 을 채운 `planned` 단계다(frontend 코드 저장소 미생성). 구현 결과는 검증 뒤 `/ingest`로만 추출한다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
[[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: blocking gate가 분리되고 dependency graph와 artifact retention이 검증된다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-TEST-STACK-001@1` | test stack default는 Vitest, RTL, MSW, Playwright, axe다 | test gate 결과의 CI stage orchestration에 적용 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-SUPPLY-CHAIN-001@1` | dependency lock, secret scan, vulnerability scan, license inventory, dependency review를 merge/release gate로 분리한다 | supply-chain gate의 blocking·artifact retention 배선에 적용 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-DEPLOYMENT-001@1` | static release는 immutable release directory와 atomic active pointer로 배포한다 | release·production-promotion stage와 rollback artifact retention에 적용 | [[raw/project-notes/ca-skeleton-frontend-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 -->
|
||||
## 목표
|
||||
|
||||
이 branch 는 **어느 `FE-OC-*` 의 single owner 도 아니다.** 대신 `FE-OC-020`·`FE-OC-021`·`FE-OC-022`·`FE-OC-023`·`FE-OC-024`·`FE-OC-025` 의 acceptance gate 들을 **하나의 실행 가능한 CI orchestration** 으로 묶는 contribution branch 다([[raw/project-notes/ca-skeleton-frontend-operational-contract]] §20 Branch Decomposition — Primary contract IDs `—`, Measurable completion = "separate blocking gates, dependency graph, artifact retention"). 구체적으로 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.1 의 26-gate acceptance matrix 와 §15.3 promotion formula(MERGE_READY → RELEASE_READY → PROD_PROMOTION_READY → FIELD_SLO_READY)를 CI pipeline 의 **stage dependency graph + blocking-check 배선 + evidence artifact retention 정책** 으로 내린다. gate 의 *정의*(blocking scope·Covered FE-OC·pass condition·evidence artifact)와 promotion formula 는 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.1·§15.3 이 소유하고, gate 별 Owner 는 §2.1.1 이 확정한다. gate → **test level / fixture KIND** taxonomy 와 `artifacts/` 트리 taxonomy 는 [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] (`FE-OC-020`) 가, gate 의 *fixture 본문* 은 각 contract owner 가 소유한다. 이 branch 는 그 gate 들이 **어떤 순서로 / 어떤 blocking 의미로 / 어떤 의존 관계로 실행되고, 그 증거가 어떻게 보관되는지** 만 명세한다. 모든 진술 등급은 `planned` — frontend repository 와 CI 설정이 아직 없다.
|
||||
|
||||
- 이슈: 없음 (repository·CI 미생성)
|
||||
- PR: 없음
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
이 branch 가 소유하는 CI orchestration 레이어(gate 정의가 아니라 gate 의 *실행/배선/보관*):
|
||||
|
||||
- **Gate stage dependency graph** — [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.3 promotion formula 를 CI pipeline 의 4 stage(merge / release / prod-promotion / field-SLO)로 매핑하고, downstream stage 가 upstream stage 의 gate 집합 전부 PASS 없이는 실행/승격되지 않는 AND 의존을 배선.
|
||||
- **Blocking-check 배선 + no-downgrade 집행** — §15.1 Blocking scope 열의 각 gate 를 독립 required check 로 wiring 하고, gate 실패를 warning / soft-fail / `continue-on-error` 로 낮추지 못하게 강제(`FE-OC-020` normative summary).
|
||||
- **Evidence artifact retention 정책** — 각 gate 가 §14.3 / §15.1 이 정한 evidence artifact 를 공유 `artifacts/` 트리에 산출하도록 upload/retention 을 배선하고, rollback target(§12.5)·drill record(`FE-GATE-016`/`FE-GATE-021`~`025`)가 승격 감사에 필요한 기간 동안 남도록 retention class 를 정의.
|
||||
- **Gate → CI trigger 매핑** — 각 gate 가 어느 event(merge PR / release / production promotion / field-window)에서 실행되는지의 배선.
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외 — 다른 owner 소유. 여기서는 이름만 가리키고 detail 을 재명세하지 않는다(CLAUDE.md §15.5 R3).
|
||||
|
||||
- **Gate 정의와 promotion formula**(blocking scope·Covered FE-OC·pass condition·evidence artifact·tier→gate 집합) → [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.1·§15.3, gate 별 Owner 는 §2.1.1.
|
||||
- **Gate taxonomy**(gate → test level / fixture KIND 열거·negative-fixture-per-gate 규칙·`artifacts/` 트리 taxonomy) → [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] (`FE-OC-020`). 이 branch 는 둘 다 *consume* 만 하고 재정의하지 않는다.
|
||||
- **각 gate 의 fixture 본문·pass-condition** → contract owner 위임: build/bundle/security → [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] (`FE-OC-018`); release-coherence/config-compat/rollback/hosting-header → [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] (`FE-OC-016`, `FE-OC-017`, `FE-OC-019` — hosting-header gate 의 security 축); bundle/lab/field performance → [[raw/branch-notes/feature-web-vitals-performance-budget-contract]] (`FE-OC-021`); runbook drill(`FE-RB-001`~`005`) → [[raw/branch-notes/feature-frontend-operational-runbook-contract]] (`FE-OC-025`); registry diff → [[raw/branch-notes/feature-frontend-contract-registry-governance]] (`FE-OC-022`); compatibility fixture → [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] (`FE-OC-023`); sample-removal → [[raw/branch-notes/feature-sample-feature-slice-contract-fixture]] (`FE-OC-024`); merge-tier test gate 본문 → 각 test/arch owner.
|
||||
- **구체 CI provider workflow syntax + 실제 merge protection / required-check 설정** — provider 미확정([[raw/project-notes/ca-skeleton-frontend-operational-contract]] §14.1 CI runner 미확정, `FE-Q-002`/`FE-Q-003`/`FE-Q-007`/`FE-Q-010`). 이 branch 는 provider-agnostic orchestration contract 만 정의(D6).
|
||||
- **NFR 임계값·gate pass-condition 수치**(timeout 10s / retry ≤2 / bundle KiB / axe 0 / p75 등) → 각 NFR owner. orchestration 은 gate 결과만 소비.
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.3 promotion formula | D1 stage dependency graph(4 tier AND 의존)의 1차 근거. |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.1 gate matrix (Blocking scope · Evidence artifact 열) | D2 blocking-check 배선 + D3 artifact→gate 매핑의 근거(26-row acceptance gate registry). |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-020` normative summary | D2 no-downgrade 불변식("실패를 warning 으로 낮추면 안 됨")의 근거. |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §14.3 planned commands (artifact 열) + §4.6 `artifacts/` blueprint | D3 evidence artifact retention 트리(script→artifact 매핑)의 근거. |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §12.5 rollback invariant + §15.1 `FE-GATE-016`(prior release pair) | D3/D4 retention 하한(rollback target·drill record 가 다음 release 승격까지 생존)의 근거. |
|
||||
| [[raw/official-docs/supply-chain-slsa-provenance-framework]] `SLSA-FW-C6`, `SLSA-FW-C4` | D3 rationale — release/security evidence 는 machine-readable provenance(in-toto attestation = "authenticated, machine-readable statement about a software artifact")이므로 CI 가 retain/traceable 하게 보관해야 함. **범위 한정**: SLSA 는 build provenance *artifact* 의 machine-readability/traceability 만 근거하고, gate ordering·blocking 정책은 근거하지 않음(그건 hub §15.3 project decision). SLSA gate/fixture 본문은 [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] (`FE-OC-018`) 소유. |
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] §15.3 promotion formula 를 CI 4-stage dependency graph(merge → release → prod-promotion → field-SLO)로 배선 + stage 간 AND gating 명세 — 등급: `planned`
|
||||
- [ ] §15.1 각 gate 를 독립 required check 로 wiring + no-downgrade(`continue-on-error` 금지) 집행 규칙 정의 — 등급: `planned`
|
||||
- [ ] evidence artifact upload + retention class(merge/release/prod-drill) 정의; rollback target·drill record 가 다음 release 승격까지 생존하도록 하한 고정 — 등급: `planned`
|
||||
- [ ] artifact retention **기간 수치**(day/count) 확정 — 등급: `needs-confirmation` (`UNSUPPORTED_DECISION` — hub 미규정, D4)
|
||||
- [ ] provider 선택 후 required-check 이름 + branch-protection 을 이 orchestration contract 에 바인딩 — 등급: `planned` (provider 미정, out of scope)
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
- `/branch-spec` self-map 완료(2026-07-19): 이 branch 는 no-primary-owner contribution branch. SSOT = hub §15.1 gate matrix + §15.3 promotion formula + §14.3 artifact 열 + §12.5 rollback invariant. gate 정의(blocking scope·Covered FE-OC·pass condition·evidence artifact)는 hub §15.1 소유이고 gate → test level / fixture KIND taxonomy 는 [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] 소유이므로 26-row 표를 복제하지 않고 **stage 레벨**로만 orchestration 을 명세(RESTATED_FOREIGN_DECISION 회피).
|
||||
- 4개 dependency sibling(build-supply-chain / release-cache / web-vitals / operational-runbook)이 모두 자기 Out of scope 에서 "CI gate orchestration · 실행 순서 · artifact retention" 을 이 branch 로 위임 확인 — 방향 일관.
|
||||
- 외부 web research 불필요(모든 orchestration 결정 hub-grounded). SLSA 는 seeded source 를 artifact-provenance-retention rationale 로만 범위 한정 인용. frontend 코드·CI 부재 → 전부 `planned`.
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> 각 결정의 근거는 아래 Sources 및 Decision Evidence Map 참조.
|
||||
|
||||
- 2026-07-19: **CI pipeline = §15.3 promotion formula 를 그대로 반영한 4-stage dependency graph** (merge → release → prod-promotion → field-SLO); downstream stage 는 upstream stage gate 전부 PASS 전에는 실행/승격 불가(AND) / 검토한 대안: 단일 flat gate 집합(stage 없음) / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.3.
|
||||
- 2026-07-19: **각 gate 는 독립 blocking required check**; 선언된 Blocking scope 내에서 실패는 warning/soft-fail/`continue-on-error` 로 낮출 수 없음 / 검토한 대안: 비핵심 gate 를 non-blocking advisory 로 강등 / 근거: `FE-OC-020` normative summary + §15.1 Blocking scope 열 + §15.3.
|
||||
- 2026-07-19: **각 gate 는 evidence artifact 를 공유 `artifacts/` 트리에 산출하고 CI 가 이를 retain**(승격 감사 trail); rollback target·drill record 는 최소한 다음 release 가 승격될 때까지 생존 / 근거: §14.3 artifact 열 + §12.5 rollback invariant + `SLSA-FW-C6`.
|
||||
- 2026-07-19: **artifact retention 기간(day/count)·storage backend 는 미결정** → `UNSUPPORTED_DECISION`; hub 는 *어떤* artifact 를 남기는지만 규정하고 *얼마나* 보관하는지는 규정 안 함. 하한만 rollback invariant 로 grounding, 수치는 provider/조직 정책 확정 후 채움.
|
||||
- 2026-07-19: **fixture 본문·gate pass-condition 은 CI 가 정의하지 않고 owner branch 에 위임**(R3); orchestration 은 gate 결과·artifact·blocking 만 배선 / 근거: §20 dependency 열 + §15.1 Covered-FE-OC.
|
||||
- 2026-07-19: **provider-agnostic orchestration contract**; 구체 workflow syntax·required-check 이름·branch-protection 은 provider 선택 시 바인딩(deferred) / 근거: §14.1 CI runner 미확정 + `FE-Q-002`/`FE-Q-003`/`FE-Q-010`.
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | CI pipeline 을 §15.3 promotion formula 와 동형인 4-stage dependency graph(merge → release → prod-promotion → field-SLO)로 배선; downstream stage 는 upstream gate 전부 PASS 전 실행/승격 불가(AND) | 이 조건: gate 들이 §15.3 의 4 promotion tier 로 분류될 때. 대안(flat 배선): 새 blocking scope 가 추가되면 §15.1 gate 수와 promotion formula 를 함께 갱신하고 stage graph 도 재도출(§15.1 "이 수와 promotion formula 를 함께 갱신") | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.3 promotion formula; §15.1 Blocking scope 열 | `project-decision` (hub formula 도출) | stage 내 fail-fast vs full-fan-out, stage 간 부분 재실행 정책을 hub 가 규정하지 않음 |
|
||||
| D2 | 각 gate = 독립 blocking required check; 선언된 Blocking scope 내 실패를 warning/soft-fail/`continue-on-error` 로 낮출 수 없음 | 불변식(분기 N/A) — `FE-OC-020` 이 downgrade 를 금지하고 각 promotion tier 가 지정 gate 집합의 AND 로 고정돼 우회 여지가 없으므로 항상 blocking | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-020` normative summary; §15.1 Blocking scope 열; §15.3 formula | `accepted-documented-only` (invariant) | downgrade 를 실제로 막는 지점은 provider 의 branch-protection/required-check 설정 — provider 미확정(D6) |
|
||||
| D3 | 각 gate 는 §14.3/§15.1 이 정한 evidence artifact 를 공유 `artifacts/` 트리에 산출하고 CI 가 retain; rollback target(§12.5 coherent set)·drill record 는 다음 release 승격까지 생존 | 이 조건: gate 가 machine-readable evidence 를 남길 때(전 gate). 대안: script rename 시 gate+artifact 매핑을 동시 갱신해야 함(§14.3 말미) — 매핑 갱신 없는 rename 금지 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §14.3 artifact 열, §4.6 `artifacts/` blueprint, §12.5 rollback invariant, §15.1 `FE-GATE-016`(prior release pair); `raw/official-docs/supply-chain-slsa-provenance-framework.md#SLSA-FW-C6`, `#SLSA-FW-C4` (machine-readable provenance retention rationale, 범위 한정) | `project-decision` (경로/트리) + `official-standard` (provenance-artifact retention rationale) | artifact 포맷(JUnit XML/SARIF/JSON)이 실제 CI reporter/artifact store 와 호환되는지 미검증 |
|
||||
| D4 | **UNSUPPORTED_DECISION** — artifact retention 기간(day/count)·storage backend·tier 별 차등 보관은 hub 미규정. 하한(rollback target·drill record 는 다음 release 승격까지 보관)만 §12.5 로 grounding, 구체 수치는 미결정 | 이 조건: rollback/drill evidence 는 다음 release pair 검증 전 삭제 금지(§12.5, `FE-GATE-016` "prior release pair"). 대안: merge-tier lint/test artifact 는 1 build cycle 후 만료 허용 — **수치 자체는 근거 없음**(trade-off: 짧으면 rollback/audit 증거 유실, 길면 storage 팽창) | 없음 — hub §14/§15 는 *어떤* artifact 인지만 규정, retention 기간 미규정. `FE-Q-010`(security), `FE-Q-003`(provider)도 retention 수치 미포함 | `UNSUPPORTED` | 잘못된 retention → `FE-GATE-016` rollback drill 이 prior release pair 를 잃어 실행 불가; 값은 provider/조직 정책 확정 후 결정 필요 |
|
||||
| D5 | fixture 본문·gate pass-condition 은 CI orchestration 이 정의하지 않고 각 FE-OC owner branch 에 위임; orchestration 은 gate 결과·artifact·blocking 배선만 소유(R3) | 이 조건: gate 가 단일 FE-OC owner 로 매핑될 때. 대안: 한 gate 가 다수 owner fixture 를 요구하면(예 `FE-GATE-004`/`005`/`007`) 모든 owner fixture 를 실행하도록 wiring 하되 test-level taxonomy owner([[raw/branch-notes/feature-frontend-test-taxonomy-contract]] `FE-OC-020`)가 조정 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §20 dependency 열; §15.1 Covered-FE-OC 열 | `project-decision` (R3 경계) | 없음 material — 위임 대상은 §엣지·실패·의존 참조 |
|
||||
| D6 | provider-agnostic orchestration contract; 구체 workflow syntax·required-check 이름·branch-protection 은 provider 선택 시 바인딩(deferred) | 이 조건: provider 미확정 동안은 stage graph + blocking 불변식 + retention 정책만 정의. 대안: provider 확정 시 required-check 이름을 이 contract 의 gate 에 1:1 바인딩하고 branch-protection 을 stage graph 에 맞춤 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §14.1 (CI runner 미확정); `FE-Q-002`/`FE-Q-003`/`FE-Q-010` (open questions) | `deferred` / `conditional-default` | provider primitive 가 4-tier 를 독립 required check 로 표현 못 할 수 있음(예: 단일 job 강제) |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> 전 항목 `planned` — frontend repository·CI 미생성. stage/artifact/경로는 hub §15.1(gate matrix)·§15.3(promotion formula)·§14.3(planned commands)·§4.6(directory blueprint)에서 도출한 blueprint 이며 repo·provider 확정 시 변경 가능. gate *정의* 는 재명세하지 않고 [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] taxonomy 를 consume(R3).
|
||||
|
||||
### 1. Stage dependency graph (promotion formula → CI stage)
|
||||
|
||||
> **Trace**: D1 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.3 / §15.1 Blocking scope 열
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: stage 내 gate 병렬 실행 시 **fail-fast(첫 실패에서 stage 중단) vs full-fan-out(전 gate 실행 후 집계)** 은 hub 미규정 → default 로 full-fan-out 제안(trade-off: full-fan-out 은 CI 시간↑ 이나 한 push 에서 여러 gate 실패를 한 번에 보고해 되돌이 횟수↓).
|
||||
|
||||
CI pipeline 은 §15.3 promotion formula 와 동형의 stage graph 다. downstream stage 는 upstream stage 의 gate 집합이 **전부 PASS** 이기 전에는 실행/승격되지 않는다(formula 의 `AND` 배선).
|
||||
|
||||
각 stage 의 **gate 집합은 hub §15.3 promotion formula 소유**이며 여기에 열거하지 않는다 — hub 가 gate 를 추가·supersede 하면 복제한 ID 목록만 조용히 낡는다. 본 표는 stage ↔ trigger ↔ 통과 의미의 배선만 정의한다.
|
||||
|
||||
| Stage | Trigger event | Gate 집합 | 의존(upstream stage) | 통과 의미 |
|
||||
|---|---|---|---|---|
|
||||
| S1 merge | PR → protected branch merge | hub §15.3 `MERGE_READY` 집합 | — | `MERGE_READY` |
|
||||
| S2 release | release cut | S1 + hub §15.3 `RELEASE_READY` 추가분 | S1 (`MERGE_READY`) | `RELEASE_READY` |
|
||||
| S3 prod-promotion | production promotion | S2 + hub §15.3 `PROD_PROMOTION_READY` 추가분 | S2 (`RELEASE_READY`) | `PROD_PROMOTION_READY` |
|
||||
| S4 field-SLO | 28-day field window 후 | S3 + hub §15.3 `FIELD_SLO_READY` 추가분 | S3 (`PROD_PROMOTION_READY`) | `FIELD_SLO_READY` |
|
||||
|
||||
**Off-chain gate**(선형 승격 chain 밖 — §15.1 Blocking scope 열 그대로):
|
||||
|
||||
- `FE-GATE-017`(scoped diagram review, Blocking scope = documentation readiness, 현재 `PASS_SCOPED`) — 선형 merge→release chain 에 넣지 않고 문서 준비 gate 로 독립 배선.
|
||||
- `FE-GATE-018` 은 위 S4 로, 다른 gate 와 달리 field window 종속이라 별 stage.
|
||||
|
||||
> 참고: `FE-GATE-008`(e2e)·`FE-GATE-009`(a11y)·`FE-GATE-011`(build)·`FE-GATE-013`(security) 등은 Blocking scope 가 "merge + release" 이므로 S1·S2 양쪽 required. 이 branch 는 gate 를 stage 에 배정만 하고, 각 gate 의 fixture/pass-condition 은 owner 소유(D5).
|
||||
|
||||
### 2. Blocking-check 배선 + no-downgrade 집행
|
||||
|
||||
> **Trace**: D2 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-020` / §15.1 Blocking scope 열 / §15.3
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — blocking 규칙은 `FE-OC-020`("실패를 warning 으로 낮추면 안 됨") + §15.3 formula verbatim.
|
||||
|
||||
- 각 gate 는 §15.1 Blocking scope 열이 지정한 stage 에서 **독립 required check** 로 실행된다(통합 test job 으로 합치지 않음 — gate KIND 분리는 taxonomy owner 소유이나, CI 는 그 KIND 를 별 check 로 배선).
|
||||
- gate 실패 → 해당 Blocking scope 의 promotion tier 를 `NOT_READY` 로 고정. **warning / soft-fail / `continue-on-error: true` / manual override 로 승격을 통과시키는 배선 금지**(`FE-OC-020` 위반).
|
||||
- promotion 판정은 §15.3 formula 를 그대로 계산:
|
||||
- `MERGE_READY` = S1 gate 전부 PASS
|
||||
- `RELEASE_READY` = `MERGE_READY` AND S2 추가 gate 전부 PASS
|
||||
- `PROD_PROMOTION_READY` = `RELEASE_READY` AND S3 추가 gate 전부 PASS
|
||||
- `FIELD_SLO_READY` = `PROD_PROMOTION_READY` AND `FE-GATE-018` PASS
|
||||
- exception/override 가 조직 정책상 필요하면 그 승인 owner·audit 기록을 **별도 결정 row 로** 등재해야 하며(§2.2 Q4 "허용되는 예외와 승인 owner"), 무기록 override 는 금지.
|
||||
|
||||
### 3. Evidence artifact retention
|
||||
|
||||
> **Trace**: D3 + D4 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §14.3 artifact 열 / §4.6 / §12.5 rollback invariant / §15.1 `FE-GATE-016` · `raw/official-docs/supply-chain-slsa-provenance-framework.md#SLSA-FW-C6`
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: (a) retention **기간 수치**(아래 표 "보관 하한" 의 day/count) 전부 — hub 미규정(D4). rollback/drill 은 §12.5 로 "다음 release 승격까지" 라는 *상대적* 하한만 grounding, 절대 수치는 provider/조직 정책 확정 후. (b) storage backend(CI artifact store vs 별도 object store) 미규정 — default 로 CI 기본 artifact store 제안(trade-off: 기본 store 는 무료·간단하나 보관기간 상한/용량 제약이 provider 종속).
|
||||
|
||||
각 gate 는 §14.3/§15.1 이 정한 artifact 를 공유 `artifacts/` 트리(§4.6)에 산출하고 CI 가 upload/retain 한다. gate 는 자체 트리를 만들지 않는다(taxonomy owner 의 `artifacts/` SSOT 를 consume).
|
||||
|
||||
```text
|
||||
artifacts/
|
||||
quality/ install.txt · lint.txt · check-types.txt # S1
|
||||
tests/ runtime-schema.xml · unit.xml · component.xml · integration.xml · a11y.json · sample-removal.xml · e2e/ # S1(+e2e S1/S2)
|
||||
performance/ bundle.json · lab.json · field-web-vitals.json # bundle/lab S2, field S4
|
||||
security/ scan.sarif # S1/S2
|
||||
release/ build-manifest.json · verification.json · hosting-headers.json · dependency-inventory.* · checksums.txt # S2 (§12.1)
|
||||
runbooks/ FE-RB-00N/<release-id>/record.json # S3 drill (FE-GATE-021~025)
|
||||
```
|
||||
|
||||
| Retention class | 대상 artifact | 보관 하한(상대) | 근거 |
|
||||
|---|---|---|---|
|
||||
| merge-cycle | `quality/*`, `tests/{unit,component,integration,runtime-schema,a11y,sample-removal}` | `UNSUPPORTED` (수치 미정; 최소 해당 PR 승격 판정까지) | §14.3 artifact 열 |
|
||||
| release-coherence | `release/*`, `performance/{bundle,lab}`, `security/scan.sarif` | **다음 release 가 승격될 때까지**(rollback target coherent set 생존) | §12.5 rollback invariant + `FE-GATE-016` prior release pair |
|
||||
| prod-drill | `runbooks/FE-RB-00N/<release-id>/record.json` | **다음 production promotion 승격 판정까지**(drill evidence 는 승격 gate 입력) | §15.1 `FE-GATE-016`/`021`~`025` |
|
||||
| field | `performance/field-web-vitals.json` | **28-day field window + 집계 완료까지** | §14.2 `FE-NFR-013`~`015`, `FE-GATE-018` |
|
||||
|
||||
- artifact 는 machine-readable(§14.3 확장자 `.xml`/`.sarif`/`.json`) 이어야 하고, release/security artifact 는 provenance 성격이므로 traceable 하게 보관(`SLSA-FW-C6`: in-toto attestation = machine-readable statement about artifact digests). **단** SLSA gate/fixture(build provenance attestation 생성 자체)는 [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] (`FE-OC-018`) 소유 — 이 branch 는 산출된 artifact 의 upload/retention 만 배선.
|
||||
- script rename 은 gate+artifact 매핑 동시 갱신 조건으로만 허용(§14.3 말미) — retention 배선도 함께 갱신.
|
||||
|
||||
### 4. Fixture-content 위임 경계 (R3)
|
||||
|
||||
> **Trace**: D5 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §20 dependency 열 / §15.1 Covered-FE-OC
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — 순수 위임 표. 이 branch 는 아래 gate 의 실행/blocking/retention 만 배선하고 fixture 본문은 owner 소유.
|
||||
> - **owner 열 회수(2026-07-21)**: 이전 판은 gate 별 fixture-content owner 를 이 표에 복제했는데, 그 사본이 실제로 낡아 있었다 — `FE-GATE-001` 을 build-bundle 로 적었으나 hub §2.1.1 owner 는 `feature-frontend-project-bootstrap-toolchain-contract` 이고, `FE-GATE-014` 를 release-cache-rollback 으로 적었으나 hub owner 는 `feature-frontend-contract-compatibility-governance` 이며 지목된 branch 는 그 gate 를 한 번도 언급하지 않는다. 같은 문서의 §가져온 프로젝트 계약 표(아래)는 두 gate 모두 hub 와 같게 적고 있어 문서가 자기모순 상태였다. [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] 가 이미 같은 함정에서 회수한 선례를 따라 **owner 열을 삭제하고 hub §2.1.1 포인터만 남긴다.**
|
||||
|
||||
> gate 별 **Owner 는 hub §2.1.1 이 SSOT** 다. 이 표는 owner 를 재진술하지 않고, *이 branch 가 CI 에서 무엇을 배선하는가* 만 소유한다.
|
||||
|
||||
| Gate 군 | 이 branch 가 배선하는 것 |
|
||||
|---|---|
|
||||
| `FE-GATE-001,011,012,013` (install/build/bundle/security) | stage 배정 + required check + artifact retention |
|
||||
| `FE-GATE-014,015,016,019` (config-compat/release-coherence/rollback/hosting-header) | stage 배정 + blocking + drill artifact 보관 |
|
||||
| `FE-GATE-018,026` (field/lab performance) | stage 배정 + field window retention |
|
||||
| `FE-GATE-021,022,023,024,025` (`FE-RB-001`~`005` drill) | prod-promotion stage 배정 + drill record retention |
|
||||
| `FE-GATE-002,003,004,005,006,007,008,009,010,020` (test/arch/sample) | S1 배선 + required check |
|
||||
| registry diff / compatibility gate | gate 결과 소비 |
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- gate 실패가 `continue-on-error`/warning 으로 downgrade → `FE-OC-020` 위반(기대: promotion formula 가 해당 tier 를 `NOT_READY` 로 유지, 승격 차단).
|
||||
- upstream stage 미완인데 downstream stage 실행 → dependency graph 위반(기대: S2/S3/S4 는 upstream gate 전부 PASS 전 skip).
|
||||
- retention 만료로 rollback target/drill record 소실 → `FE-GATE-016` 이 prior release pair 를 잃어 실행 불가(기대: release-coherence/prod-drill retention class 가 다음 승격까지 보관, §12.5).
|
||||
- script rename 후 gate+artifact 매핑 미갱신 → §14.3 위반(기대: drift check 가 매핑 불일치 검출, retention 배선도 함께 갱신).
|
||||
- provider 가 4-tier 를 독립 required check 로 표현 못 함 → D6 open risk(기대: equivalent primitive + 그 rollback/blocking semantics 를 결정 row 로 기록, §12.4 유사 절차).
|
||||
- flaky gate(e2e/perf) → deterministic fixture(fake clock §15.1 `FE-GATE-005`, recorded context metadata §14.1) 요구는 taxonomy/owner 소유; orchestration 은 flaky 결과를 PASS 로 취급하지 않도록 retry-suppression(무한 retry 로 통과 금지) 배선.
|
||||
- **다른 계약 의존** (§20 dependency 열 + §4.3):
|
||||
- [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] (`FE-OC-020`) — gate → test level / fixture KIND taxonomy 와 `artifacts/` 트리 taxonomy 를 이 branch 가 consume. 그 taxonomy 가 바뀌면 stage graph·retention 배선 재도출. (gate→FE-OC mapping 과 promotion formula 는 hub §15.1·§15.3 소유.)
|
||||
- [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] (`FE-OC-018`) — install/build/bundle/security gate fixture·SLSA provenance artifact 제공. 산출 artifact 경로가 바뀌면 retention 배선 갱신.
|
||||
- [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] (`FE-OC-016`, `FE-OC-017`, `FE-OC-019`) — release-coherence/rollback drill + hosting-header(cache·security) fixture 제공. rollback target coherent set(§12.5)이 retention 하한을 규정.
|
||||
- [[raw/branch-notes/feature-web-vitals-performance-budget-contract]] (`FE-OC-021`) — bundle/lab/field gate pass-condition 제공. field window 가 S4 retention 을 규정.
|
||||
- [[raw/branch-notes/feature-frontend-operational-runbook-contract]] (`FE-OC-025`) — `FE-RB-001`~`005` drill 본문 제공. drill record 가 prod-promotion 승격 gate 입력.
|
||||
- [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] (`FE-OC-003`) — `pnpm test:*` script host / engine 없이는 어떤 gate 도 실행 불가(간접 의존; taxonomy 경유).
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| required gate 실패가 merge/release/promotion 을 실제로 막는다 | CI·branch-protection 설정 없음 | provider 확정 후 negative fixture(§15.2)로 gate 를 고의 실패시켜 해당 tier 가 `NOT_READY` 로 승격 차단되는지 확인 | `needs-confirmation` |
|
||||
| 4-stage dependency graph 가 §15.3 formula 와 정합(downstream 이 upstream AND 없이 승격 안 됨) | CI 미배선 | stage 별 gate 집합을 §15.3 verbatim 과 대조하고, upstream gate 1개 실패 시 downstream stage skip 을 e2e 로 확인 | `needs-confirmation` |
|
||||
| gate 실패가 warning/`continue-on-error` 로 downgrade 되지 않음 | CI wiring·override 정책 미구현 | workflow 에 `continue-on-error` 부재 grep + override 감사 로그 확인 | `planned` |
|
||||
| rollback target·drill record 가 다음 release/promotion 승격까지 생존 | retention 배선·수치 미정(D4) | release pair 를 만들어 `FE-GATE-016` 이 prior release artifact 를 실제로 사용할 수 있는지 drill(§12.5) | `needs-confirmation` |
|
||||
| artifact 포맷(XML/SARIF/JSON)이 CI reporter/artifact store 와 호환 | reporter 미선택 | 각 gate reporter 산출물을 CI artifact upload + 재파싱으로 검증 | `planned` |
|
||||
| retention 기간 수치가 조직/provider 정책에 부합 | hub 미규정(`UNSUPPORTED_DECISION`) | `FE-Q-010`/`FE-Q-003` resolution 으로 retention day/count 확정 후 배선 | `needs-confirmation` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
- 스캐폴딩 단계: `/coverage` 실행 전 수동 행을 만들지 않는다.
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
- 없음 — 구현 착수 전(`planned`).
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-GATE-001@1` | [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] | lockfile 이 manifest 와 어긋나면 merge·release 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-002@1` | [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]] | 금지된 API·import 가 남아 있으면 merge 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-004@1` | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | invalid fixture 가 예상 kind 로 거부되지 않으면 merge 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-012@1` | [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] | 번들 NFR threshold 초과면 release 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-014@1` | [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] | 지원 대상 config 버전이 boot 에 실패하면 release 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-016@1` | [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] | rollback 과 smoke 증거가 없으면 production promotion 을 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-018@1` | [[raw/branch-notes/feature-web-vitals-performance-budget-contract]] | p75 목표 미달이거나 표본 임계가 미해결이면 field readiness 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-021@1` | [[raw/branch-notes/feature-frontend-operational-runbook-contract]] | `FE-RB-001` 의 containment·escalation·recovery 단언이 실패하면 production promotion 을 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-OC-016@1` | [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] | HTML, asset, runtime config, release manifest cache policy를 MUST 구분 | import 참조로 적용 |
|
||||
| `FE-OC-017@1` | [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] | rollback은 immutable prior release로 수행하고 build/config/API compatibility를 MUST 검증 | import 참조로 적용 |
|
||||
| `FE-OC-018@1` | [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] | frozen lockfile, dependency review, secret scan, SBOM 또는 dependency inventory를 release gate에 MUST 포함 | import 참조로 적용 |
|
||||
| `FE-OC-019@1` | [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] | browser bundle에 secret을 넣지 않고 untrusted HTML injection을 기본 금지 | import 참조로 적용 |
|
||||
| `FE-OC-020@1` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | gate 종류별 책임·fixture·artifact를 분리하고 실패를 warning으로 낮추면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-021@1` | [[raw/branch-notes/feature-web-vitals-performance-budget-contract]] | NFR은 device/network/cache/build context와 함께 MUST 측정 | import 참조로 적용 |
|
||||
| `FE-OC-022@1` | [[raw/branch-notes/feature-frontend-contract-registry-governance]] | 8개 registry는 single primary owner와 compatibility impact를 MUST 기록 | import 참조로 적용 |
|
||||
| `FE-OC-023@1` | [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] | API/config/storage/release schema의 breaking change는 migration 또는 version bump 없이 배포하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-025@1` | [[raw/branch-notes/feature-frontend-operational-runbook-contract]] | boot, chunk mismatch, API degradation, telemetry failure, rollback runbook을 MUST 유지 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
- 없음 — 자식 자료는 생성 후 controller가 parent Cluster와 함께 등록한다.
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
- 없음 — daily note는 이 작업에서 수정하지 않는다.
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크: 없음
|
||||
- 리뷰 메모: 없음
|
||||
- 머지 결과 / 배포 환경: `planned`
|
||||
- **wiki 추출 대상** (verified만): 없음 — 구현 착수 전(전부 `planned`).
|
||||
- **추출하지 않을 항목** (planned / documented-only / abandoned): 현재 전체 — verified evidence 확보 전까지 추출 금지.
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
---
|
||||
title: branch / feature-frontend-clean-architecture-layering-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-002
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-002
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-ARCHITECTURE-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-PORT-OWNERSHIP-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-COMPOSITION-ROOT-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-001]
|
||||
contract_packet: 1
|
||||
branch: feature-frontend-clean-architecture-layering-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract.md]
|
||||
tags: [branch, ca-skeleton, frontend, architecture, application, javascript, clean-architecture]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
contract_packet_sha256: 1a7d904e9fe76e1aeb6ebd25fea852de7cc888232e96110e832f764d97e518ee
|
||||
imports: [FE-OC-004@1, FE-OC-024@1]
|
||||
accepts_delegations: [DELEG-FE-003@1, DELEG-FE-005@1]
|
||||
|
||||
---
|
||||
|
||||
# branch: feature-frontend-clean-architecture-layering-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — 단일 브랜치의 TODO·결정·진행 기록. 머지/종료 후 verified 결과는 `/ingest`로 `wiki/projects/`에 추출한다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: directory 책임·port owner·allowed import matrix가 문서와 fixture로 고정된다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-ARCHITECTURE-001@1` | domain, application, presentation, adapters, bootstrap 책임을 분리한다 | 5-layer directory 책임과 allowed-import matrix에 적용 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-PORT-OWNERSHIP-001@1` | output port interface는 application이 소유하고 adapter가 구현한다 | port ownership matrix에 적용 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-COMPOSITION-ROOT-001@1` | bootstrap을 단일 composition root로 두고 concrete adapter를 application에 주입한다 | bootstrap boot order와 adapter injection 경계에 적용 | [[raw/project-notes/ca-skeleton-frontend-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 -->
|
||||
## 목표
|
||||
|
||||
이 브랜치는 project-wide 계약 `FE-OC-002`(`domain <- application <- presentation` 의존 방향 + application-owned output port를 MUST 지킴)를 *구현 착수 가능한 상세 명세*로 내린다. 구체적으로 세 가지 불변식을 고정한다: `FE-D009`(domain/application/presentation/adapters/bootstrap 5-layer 책임 분리), `FE-D010`(output port interface는 application 소유, adapter가 구현), `FE-D011`(단일 composition root `bootstrap`이 concrete adapter를 주입). 산출물은 §20 Measurable completion이 요구하는 **directory responsibility + port owner + allowed import matrix** 세 표다. frontend repository가 아직 없으므로 이 브랜치의 모든 항목은 `planned` 등급이며, 착수 시점의 blueprint 근거는 hub §4(§4.2 component responsibility / §4.3 dependency matrix / §4.4 port ownership / §4.5 composition root / §4.6 directory blueprint)다.
|
||||
|
||||
- 이슈:
|
||||
- PR:
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- domain / application / presentation / adapters / bootstrap **5-layer 책임 경계** 정의 (hub §4.2) — `FE-D009`
|
||||
- **planned directory blueprint** 확정 (hub §4.6) — `FE-D009`
|
||||
- **application-owned output port ownership matrix** — port 정의 owner·consumer·I/O·failure vocabulary·"port는 application이 소유한다" 규칙 (hub §4.4) — `FE-D010`
|
||||
- **allowed / forbidden import matrix** *규칙 정의* (hub §4.3) — `FE-D009` + `FE-D010`
|
||||
- **단일 composition root(bootstrap) injection 원칙 + boot order** (hub §4.5) — `FE-D011`
|
||||
- `FE-OC-002`의 minimum evidence인 **dependency rule report** 산출물 정의
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외. 인접 계약은 다른 owner 브랜치가 소유하며, 여기서 detail을 쓰지 않고 그 브랜치를 가리킨다 (CLAUDE.md §15.5 R3 `OUT_OF_BRANCH_SCOPE` 방지).
|
||||
|
||||
- import 규칙의 **실제 lint 강제** (dependency-cruiser / ESLint restricted-import config, allowed/forbidden fixture) → [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]]
|
||||
- 각 port의 **concrete method 시그니처 / 구현** → 해당 adapter 브랜치: [[raw/branch-notes/feature-api-client-response-envelope-contract]] (`ResourceQueryPort`/`ResourceCommandPort`), [[raw/branch-notes/feature-server-state-caching-contract]] (`QueryCachePort`), [[raw/branch-notes/feature-frontend-storage-registry-contract]] (`StoragePort`), [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] (`TelemetryPort`), [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] (`ReleaseInfoPort`)
|
||||
- **AuthSessionPort 내부 shape / token lifecycle** → [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] + 외부 Keycloak
|
||||
- **runtime config schema / 검증 내용** → [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] (`FE-OC-004`)
|
||||
- **boot error shell 렌더링 / reload-loop 방지** → [[raw/branch-notes/feature-frontend-render-recovery-boundary-contract]] (`FE-OC-015`)
|
||||
- **toolchain / manifest / checkJs / dev dependency 설치** → [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] (`FE-OC-003`)
|
||||
- **React 사용 결정 자체** (hub `FE-D004`) → [[raw/branch-notes/feature-async-ui-state-contract]]. 본 브랜치는 "선택된 UI framework를 presentation에 가둔다"는 *경계 규칙*만 소유
|
||||
- **test gate 종류·fixture·artifact 구조** → [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] (`FE-OC-020`)
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `FE-OC-002` owner 계약 + `FE-D009`/`FE-D010`/`FE-D011` 결정 register + §4 architecture blueprint의 SSOT (본 브랜치의 모든 planned 경로·규칙 근거) |
|
||||
| [[raw/project-notes/ca-skeleton-operational-contract]] | `FE-D009`/`FE-D010`/`FE-D011`의 Clean Architecture **선례** — backend 운영계약의 layer 분리·"application use case는 output port에만 의존"·composition root 단일화(app-bootstrap) 철학을 frontend에 적용 |
|
||||
| [[raw/official-docs/react-ui-library-official]] | import matrix의 **React 경계 규칙** — `REACT-UI-C1`(React 앱은 컴포넌트 단위 UI 구성) → React는 presentation 전용, domain/application의 React import 금지 |
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] hub §4.2 component responsibility + §4.6 directory blueprint를 실제 폴더/모듈 책임표로 확정 — 등급: `planned`
|
||||
- [ ] hub §4.4 port ownership matrix를 `application/ports` 인터페이스 스텁 목록으로 표현 (정의 owner=application) — 등급: `planned`
|
||||
- [ ] hub §4.3 allowed/forbidden import matrix를 machine-readable 규칙 사양으로 문서화 (강제는 enforcement 브랜치) — 등급: `planned`
|
||||
- [ ] hub §4.5 composition root boot order(10단계) + adapter injection 지점 명세 — 등급: `planned`
|
||||
- [ ] `FE-OC-002` minimum evidence인 dependency rule report 산출물 형식 정의 — 등급: `planned`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
`/branch-spec`로 채움 (2026-07-19). frontend 코드 부재 → 전 항목 `planned`. 근거 SSOT = frontend hub §4 + backend CA 선례 + `REACT-UI-C1`. 웹 리서치 불필요 (hub가 이미 충분).
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> 각 결정의 근거는 아래 Decision Evidence Map과 1:1. 대안과 함께 기록.
|
||||
|
||||
- 2026-07-19: **5-layer 책임 분리** (domain/application/presentation/adapters/bootstrap) 채택 (`FE-D009`) / 이유: framework-neutral domain 보호 + 의존 방향을 `domain <- application <- presentation` 단방향으로 강제 / 검토한 대안: flat structure, Feature-Sliced Design(FSD) / 근거: backend ca-skeleton 운영계약 CA 철학 [[raw/project-notes/ca-skeleton-operational-contract]]
|
||||
- 2026-07-19: **output port interface는 application 소유, adapter가 구현** (`FE-D010`) / 이유: dependency inversion — application이 concrete adapter 이름을 모르게 함 / 검토한 대안: adapter가 인터페이스 소유(전통적 layered) / 근거: project decision + backend port 소유 선례
|
||||
- 2026-07-19: **단일 composition root(bootstrap)가 concrete adapter 주입** (`FE-D011`) / 이유: owner ambiguity 제거, 조립 지점 1개로 고정 / 검토한 대안: framework DI container / 근거: project decision + backend app-bootstrap 선례
|
||||
- 2026-07-19: **선택된 UI framework(React, hub `FE-D004`)를 presentation에 가둠** (import matrix 규칙) / 이유: React는 UI 구성 관심사이므로 domain/application에 유입 금지 / 검토한 대안: domain/application에 rendering 혼입 / 근거: `REACT-UI-C1`
|
||||
- 2026-07-19: **import 규칙 정의=본 브랜치, 강제=enforcement 브랜치 위임** (범위 경계) / 이유: 규칙 정의와 lint 강제 관심사 분리 / 근거: §20 분해표 + §4.3 `Planned enforcement` 컬럼
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
> 각 결정과 raw source claim의 연결. `Decision ID`는 이 노트 안에서 안정적으로 유지. `Supporting Claims`는 backtick 포인터(`raw/<cat>/<slug>.md#<CLAIM>`) 또는 hub `FE-D###` / live wikilink.
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | 5-layer 책임 분리 domain/application/presentation/adapters/bootstrap (`FE-D009` / `FE-OC-002`) | sample slice가 경계의 값을 증명하는 한 default 유지; sample이 불필요한 ceremony임을 증명하거나 FSD fork 승인 시 flat/FSD로 전환 | 프로젝트 결정 `FE-D009`; backend CA 선례 [[raw/project-notes/ca-skeleton-operational-contract]] (domain이 CA 심장, 모든 의존 화살표가 domain으로 수렴) | `project-decision` | 코드 없음 — 5-layer 경계가 실제로 값을 하는지 sample slice(`FE-OC-024`) 전까지 미검증 (over-engineering 위험) |
|
||||
| D2 | output port interface는 application 소유, adapter가 구현 (`FE-D010` / `FE-OC-002`) | default 유지; port가 domain invariant 자체를 표현해야 하는 concrete case 발생 시 그 port를 domain으로 이동 | 프로젝트 결정 `FE-D010` (dependency inversion); backend "application use case는 output port에만 의존" 선례 [[raw/project-notes/ca-skeleton-operational-contract]] | `project-decision` | port granularity/개수 미검증 — 잘못된 분할 시 adapter 표면 폭증 |
|
||||
| D3 | 단일 composition root(bootstrap)가 concrete adapter 주입 (`FE-D011` / `FE-OC-002`·`FE-OC-004`) | hand-wired DI default 유지; framework DI container 도입 시 재검토 | 프로젝트 결정 `FE-D011`; backend app-bootstrap composition-root 선례 [[raw/project-notes/ca-skeleton-operational-contract]] | `project-decision` | boot order(§4.5 10단계) 결합 — 단계 순서 변경이 여러 adapter 조립에 영향 |
|
||||
| D4 | 선택된 UI framework(React)를 presentation에 가둠 — import matrix의 React 금지 row (`FE-OC-002`; framework 선택은 hub `FE-D004`, async-ui 소유) | React가 UI framework인 동안 유지; native/custom-element 또는 다른 framework로 fork(hub `FE-D004` revisit) 시 matrix의 React 금지 심볼만 갱신 | `raw/official-docs/react-ui-library-official.md#REACT-UI-C1`; hub §4.3 dependency matrix | `official-doc` | framework 교체 시 domain/application 격리 규칙 자체는 불변이나 구체 금지 심볼 목록이 바뀜 |
|
||||
| D5 | import 규칙 *정의*=본 브랜치, *강제*=enforcement 브랜치 위임 (범위 경계) (`FE-OC-002` contributes) | 규칙 정의(여기)와 lint 강제(enforcement 브랜치) 분리 유지; 두 관심사 병합 승인 시 재검토 | §20 분해표 (`feature-frontend-architecture-enforcement-lint-contract` Primary=—, contributes `FE-OC-002`); hub §4.3 `Planned enforcement` 컬럼 | `project-decision` | 규칙/강제 drift — matrix 변경이 enforcement fixture 미갱신 시 규칙이 무력화 |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> `planned` blueprint. 경로/책임은 hub §4.2/§4.3/§4.4/§4.5/§4.6에서 도출(근거 있음). frontend 코드는 존재하지 않으므로 전 항목 `planned`. 3-rule (R1 Trace / R2 UNSUPPORTED_IMPL_DECISION / R3 OUT_OF_BRANCH_SCOPE 정제) 준수.
|
||||
|
||||
### 1. Layer 책임 · directory 책임 map
|
||||
|
||||
> **Trace**: D1 (`FE-D009`) + `FE-OC-002`; hub §4.2 component responsibility + §4.6 directory blueprint.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: §4.6 blueprint보다 깊은 하위 파일/모듈 명명(예: `domain/models/*` 개별 파일명, `application/use-cases/*` 클래스명)은 hub가 권고하지 않음 → 구현 repository 생성 시 확정. trade-off: blueprint 수준(폴더 책임)까지만 grounded, 그 이하 명명은 첫 sample slice에서 정한다.
|
||||
|
||||
아래 표에서 본 브랜치가 더하는 것은 **planned path 열** 뿐이다. `Owns`·`Consumes`·`MUST NOT own` 의 owner 는 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §4.2 이므로 여기서 값을 고치지 않는다 — 고쳐야 하면 §4.2 를 고치고 이 표를 따라 갱신한다.
|
||||
|
||||
| Layer (planned path — 본 브랜치 소유) | Owns (§4.2) | Consumes (§4.2) | MUST NOT own (§4.2) |
|
||||
|---|---|---|---|
|
||||
| `src/domain/` | framework-neutral model, value semantics, pure policy | standard JavaScript only | React, router, Query, fetch, storage, telemetry |
|
||||
| `src/application/` | use case, input/output port, `QueryCachePort` policy, orchestration, view-model contract | domain | concrete adapter, browser global, React component |
|
||||
| `src/presentation/` | page/component, user event, view state rendering | application public API | raw API DTO, fetch, storage key, telemetry transport |
|
||||
| `src/adapters/http · storage · telemetry · query-cache · auth · release` | application output port 구현, envelope/schema/error·serialization·redaction·key mapping | application port + 해당 browser API | use-case policy, component rendering |
|
||||
| `src/bootstrap/` (`main.jsx`, `composition-root.js`, `load-runtime-config.js`) | config load, adapter 생성, DI, React mount | 모든 runtime module | business rule, page-specific orchestration |
|
||||
|
||||
`src/contracts/` 8개 registry 파일(`routes.js`…`release-tokens.js`)은 각 registry owner 브랜치가 채운다 — 본 브랜치는 *디렉토리 위치*만 blueprint로 고정 (§4.6). registry schema 내용은 governance/owner 브랜치 소유 (R3).
|
||||
|
||||
### 2. forbidden import matrix (규칙 정의)
|
||||
|
||||
> **Trace**: D1 (`FE-D009`) + D2 (`FE-D010`) + D4 (`REACT-UI-C1`) + `FE-OC-002`; hub §4.3 dependency matrix.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: `Planned enforcement` 컬럼의 도구(dependency-cruiser + ESLint restricted imports)는 §4.3에 명시되어 grounded이나, *구체 rule config/glob*은 본 브랜치가 정하지 않음 → enforcement 브랜치 소유 (D5, R3). trade-off: 본 표는 "무엇이 금지인가"(machine-readable 규칙)까지만, "어떤 lint 설정으로 잡는가"는 위임.
|
||||
|
||||
**matrix 본문은 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §4.3 소유다** — 여기에 옮겨 적지 않는다. 이전 판은 §4.3 의 6행 중 5행만 복제해 `test fixtures` 행과 "**May import 열은 예시(illustrative)이고 MUST NOT 열이 규범(normative)**" 이라는 §4.3 의 경고 문단을 통째로 빠뜨렸고, 그 사본만 읽는 구현자는 §4.3 이 명시적으로 경고한 allow-only 오독(= `FE-D022` 가 의무화한 test stack 이 전부 금지되는 해석)에 그대로 빠진다. `test fixtures` 행의 enforcement 는 [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]] 가 소유한다.
|
||||
|
||||
Normative 요약: `application -> adapters` concrete import는 MUST NOT; output port는 application이 MUST 소유; adapter는 application을 모름; presentation은 raw envelope를 직접 다루지 않음; bootstrap만 concrete adapter 조립. 이 규칙의 **강제**(fixture pass/fail)는 D5(범위 경계)에 따라 위임한다 → [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]].
|
||||
|
||||
### 3. Port ownership + composition-root wiring
|
||||
|
||||
> **Trace**: D2 (`FE-D010`) + D3 (`FE-D011`) + `FE-OC-002`·`FE-OC-004`; hub §4.4 port ownership matrix + §4.5 composition root.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: §4.4의 I/O·failure vocabulary 컬럼 이상의 *concrete method 시그니처*는 각 adapter/port owner 브랜치 소유 (R3) — 본 브랜치는 "port 정의는 application, 구현은 adapter, 조립은 bootstrap"이라는 *ownership 규칙*만 명세. `AuthSessionPort`의 opaque-credential shape는 auth owner가 정함(§4.4 주석).
|
||||
|
||||
Port 정의 owner = `application` (전부). 구현 위치 = `adapters/*`. 정의된 port(§4.4): `ResourceQueryPort`·`ResourceCommandPort`(→http), `QueryCachePort`(→query-cache), `AuthSessionPort`(→외부 auth), `StoragePort`(→storage), `TelemetryPort`(→telemetry), `ClockPort`(→system), `ReleaseInfoPort`(→release). 각 port의 concrete impl은 해당 owner 브랜치 (Out of scope 참조).
|
||||
|
||||
Composition root boot order (§4.5, `MUST`): (1) build identity → (2) runtime config fetch → (3) config envelope·schema·compatibility 검증 → (4) release manifest 정합성 → (5) registry snapshot load → (6) auth adapter 주입 → (7) http/storage/telemetry/query-cache adapter 생성 → (8) application facade 생성 → (9) router 생성 → (10) React root mount. **2~4단계 실패 시 product route를 mount하지 않고 boot error shell만 렌더**; telemetry adapter(7) 생성 실패는 console-safe fallback으로 진행. (config 검증 내용=env-config 브랜치, boot error shell 렌더=render-recovery 브랜치 — R3.)
|
||||
|
||||
### 4. Dependency rule report 산출물
|
||||
|
||||
> **Trace**: D5 + `FE-OC-002` minimum evidence("dependency rule report", hub §2.1).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: report의 정확한 파일 형식(JSON/HTML)·CI 배치는 미결 → enforcement + test-taxonomy 브랜치와 조율. trade-off: 본 브랜치는 report가 *검증해야 할 명제*(allowed pass / forbidden fail / domain framework-free)만 정의, 형식은 산출 브랜치 소유.
|
||||
|
||||
report가 assert해야 할 명제: (a) allowed import fixture green, (b) forbidden import fixture red, (c) `domain`의 프레임워크/브라우저 전역 import 0건, (d) concrete adapter 생성이 `bootstrap` 밖에 없음, (e) output port 정의가 `application`에만 존재. 생성 주체·artifact 경로는 enforcement/test-taxonomy 브랜치 (R3, `FE-OC-020`).
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- *import-rule 위반* (한 layer가 금지 방향 import): dependency rule report / architecture fixture가 **red**로 실패 → merge gate 차단. 탐지 mechanism은 enforcement 브랜치 소유(§4.3 Planned enforcement).
|
||||
- *composition-root boot 실패* (§4.5 2~4단계: runtime config fetch/검증/release manifest 부정합): product route를 mount하지 않고 **boot error shell만 렌더** (fail-fast). config 검증 내용은 `FE-OC-004`, error shell 렌더는 `FE-OC-015`.
|
||||
- *adapter 누락/오주입* (bootstrap이 특정 port impl 미주입): application facade 생성(8단계)이 boot 시점에 throw → fail-fast, boot error shell.
|
||||
- *telemetry adapter 생성 실패* (7단계): UI를 실패시키지 않고 console-safe fallback으로 진행(§4.5, `FE-OC-014` best-effort 원칙).
|
||||
- *presentation이 raw DTO/fetch/storage 직접 접근*: import matrix 위반 → forbidden fixture가 잡음(enforcement 브랜치).
|
||||
- **다른 계약 의존**:
|
||||
- [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] `FE-OC-003` 에 의존 — manifest·checkJs·dev dependency가 있어야 import graph가 분석·강제 가능 (§20 Dependency).
|
||||
- **위임(D5 범위 경계)**: 본 브랜치 import matrix(§2)의 fixture 강제는 [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]] 소유 — 그 계약이 바뀌면 규칙 강제력에 직접 영향.
|
||||
- [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] `FE-OC-004` — composition root boot 2~4단계가 소비하는 runtime config 검증·fallback 정책 owner.
|
||||
- [[raw/branch-notes/feature-frontend-render-recovery-boundary-contract]] `FE-OC-015` — boot 실패 시 boot error shell 렌더 owner.
|
||||
- [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] `FE-OC-010` — `AuthSessionPort` shape/token lifecycle owner (boot 6단계 주입 대상).
|
||||
- Port 구현 소비: [[raw/branch-notes/feature-api-client-response-envelope-contract]] `FE-OC-006`, [[raw/branch-notes/feature-server-state-caching-contract]] `FE-OC-012`, [[raw/branch-notes/feature-frontend-storage-registry-contract]] `FE-OC-013`, [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] `FE-OC-014`, [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] `FE-OC-016`/`FE-OC-017`.
|
||||
- Contributes to: [[raw/branch-notes/feature-async-ui-state-contract]] `FE-OC-011` (application-owned view-model 경계 제공), [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] `FE-OC-020` (architecture fixture를 test 분류의 한 category로 제공).
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| `domain` 모듈이 프레임워크/브라우저 전역을 0건 import한다 | 코드 없음 — 규칙만 존재 | dependency graph snapshot + forbidden-import fixture (enforcement 브랜치 산출) | `needs-confirmation` |
|
||||
| `application`이 concrete adapter를 직접 import하지 않는다 | 위와 동일 | allowed/forbidden import fixture (allowed pass / forbidden fail) | `needs-confirmation` |
|
||||
| composition root(`bootstrap`)만 concrete adapter를 생성한다 | 위와 동일 | grep + composition-root review — adapter 생성이 bootstrap 밖에 없음 | `needs-confirmation` |
|
||||
| output port 정의는 `application`에, 구현은 `adapters/*`에 위치한다 | 위와 동일 | directory 검사 + import graph snapshot | `needs-confirmation` |
|
||||
| 5-layer 분리가 sample slice에서 실제로 경계 값을 한다 (over-engineering 아님) | hub `FE-D009` revisit trigger — 미검증 | sample-feature-slice fixture(`FE-OC-024`)로 경계가 값을 증명 / 아니면 재검토 | `needs-confirmation` |
|
||||
| 이 import matrix가 dependency-cruiser + ESLint로 실제 강제 가능하다 | 도구 미도입 | enforcement 브랜치의 allowed/forbidden fixture pass/fail (`dependency rule report`) | `needs-confirmation` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
> `/coverage`가 채우는 생성물이며 손으로 유지하지 않는다.
|
||||
|
||||
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| TODO — `/coverage` 실행 전 | missing | (없음) | 미평가 | TODO |
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
없음 — scaffolding/spec 단계 (frontend 코드 부재).
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: received-delegations:start -->
|
||||
### 수신한 위임
|
||||
|
||||
| Delegation Ref | From | Concern | Status |
|
||||
|---|---|---|---|
|
||||
| `DELEG-FE-003@1` | [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]] | `fe.deleg.composition-root-review` | accepted |
|
||||
| `DELEG-FE-005@1` | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | `fe.deleg.injectable-random` | accepted |
|
||||
<!-- GENERATED: received-delegations:end -->
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-OC-004@1` | [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] | build-time, runtime-public, secret config를 MUST 분리하고 boot 전에 runtime config를 검증 | import 참조로 적용 |
|
||||
| `FE-OC-024@1` | [[raw/branch-notes/feature-sample-feature-slice-contract-fixture]] | sample은 contract fixture이며 production feature가 의존하면 안 됨 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
### Sub-branches (세부 작업)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 오류 기록 (이 branch 작업 중 발생)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 강의 (이 작업을 위해 학습한 강의)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### job-posting tie-ins (이 작업에서 파생된 글감)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크:
|
||||
- 리뷰 메모:
|
||||
- 머지 결과 / 배포 환경:
|
||||
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
|
||||
- `actually-implemented` 항목:
|
||||
- `locally-verified` 항목:
|
||||
- `prod-verified` 항목:
|
||||
- **추출하지 않을 항목** (planned / documented-only / abandoned):
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
---
|
||||
title: branch / feature-frontend-contract-compatibility-governance
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-023
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-023
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-REGISTRY-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-CONFIG-FALLBACK-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-022, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-006, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-011]
|
||||
contract_packet: 1
|
||||
branch: feature-frontend-contract-compatibility-governance
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract]
|
||||
tags: [branch, ca-skeleton, frontend, api-design, semver, api-contract]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
contract_packet_sha256: eb4a422e6da50f16e5b6d59964943ebca79d655fc09066815953aa3ba22e4311
|
||||
imports: [ART-FE-003@1, FE-GATE-004@1, FE-GATE-015@1, FE-GATE-016@1, FE-OC-004@1, FE-OC-007@1, FE-OC-012@1, FE-OC-013@1, FE-OC-016@1, FE-OC-017@1]
|
||||
---
|
||||
|
||||
# branch: feature-frontend-contract-compatibility-governance
|
||||
|
||||
> Layer: `raw/branch-notes/` — TODO·결정·진행 기록. 구현 결과는 검증 뒤 `/ingest`로만 추출한다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
[[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: version tuple·additive/breaking fixture·migration/rollback rule가 검증된다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-REGISTRY-001@1` | route, API operation, env, storage, error, query, telemetry, release token을 8개 registry로 관리한다 | registry 변경의 compatibility impact와 version tuple 입력을 집행한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-CONFIG-FALLBACK-001@1` | runtime config fallback 시 environment별 rebuild는 허용하되 artifact의 multi-environment 재사용은 금지한다 | config 변경의 migration·fallback·rollback 호환성 규칙에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
|
||||
<!-- section-id: branch-local-decisions -->
|
||||
### 브랜치 지역 결정
|
||||
|
||||
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
||||
|---|---|---|---|---|
|
||||
| D1 | compatibility impact 공통 어휘를 고정한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D2 | boot compatibility를 version tuple로 판정한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D3 | schema 계열별 독립 version field를 사용한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D4 | breaking 변경은 migration·version bump·discard·fallback과 test evidence를 요구한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D5 | rollback은 coherent tuple 집합을 복원한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D6 | 호환 불가 cache data는 기본 discard한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
|
||||
<!-- section-id: declared-overrides -->
|
||||
### 선언한 예외
|
||||
|
||||
| Override ID | Overrides | Reason | Approval | Status |
|
||||
|---|---|---|---|---|
|
||||
|
||||
없음.
|
||||
<!-- GENERATED: branch-contract:end -->
|
||||
|
||||
<!-- section-id: branch-goal -->
|
||||
## 목표
|
||||
|
||||
이 브랜치는 프로젝트 계약 `FE-OC-023`("API/config/storage/release schema의 breaking change는 migration 또는 version bump 없이 배포하면 안 됨", minimum evidence = compatibility report)을 **구현 착수 가능한 명세**로 낮춘다. hub는 호환성 규칙을 여러 곳에 흩어 정의해 두었다 — 분류 어휘([[raw/project-notes/ca-skeleton-frontend-operational-contract]] §3.3), registry 변경 프로토콜(§5.10), boot compatibility tuple(§12.3), rollback invariant(§12.5). 본 브랜치는 이들을 **하나의 governance 계약**(버전 tuple 행렬 + additive/breaking 분류 fixture + migration/rollback 규칙)으로 통합해 owner로서 mechanism과 test를 제공한다. 결정 자체(`FE-D012/013/016/019`)는 다른 owner 브랜치가 소유하고, 본 브랜치는 그 결정들이 공유하는 `FE-OC-023` 계약의 **집행 규칙**만 소유한다.
|
||||
|
||||
- 이슈: 없음 (스캐폴딩 단계)
|
||||
- PR: 없음
|
||||
|
||||
산출물 등급: 프론트엔드 코드가 없으므로 이 브랜치의 모든 구현 주장은 `planned`이다.
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- **버전 tuple 행렬**: `(buildId, configSchemaVersion, apiContractVersion, assetManifestHash, releaseId)` + storage schemaVersion + query namespace version 을 필드별 source·compatibility 역할·mismatch 결과로 정리한 표(hub §12.3 / §5.9 통합).
|
||||
- **additive vs breaking 분류 fixture**: `compatibility_impact ∈ {none, additive, behavior-change, breaking}` 어휘(hub §3.3)를 API/config/storage/release 4개 schema 계열에 적용하는 synthetic fixture 집합과 각 등급의 required action.
|
||||
- **migration/rollback 규칙**: breaking 변경이 version bump·migration·discard·fallback 없이 merge/배포되지 않게 하는 규칙 + rollback이 coherent tuple 집합을 복원하도록 하는 규칙(hub §5.10 / §9.2 / §12.5).
|
||||
- **compatibility gate 소유**: `FE-GATE-014@1`(config compatibility) 의 fixture·report artifact 정의. `FE-GATE-015`(release coherence) 는 **소유가 아니라 소비/기여** 다 — Owner 는 hub §2.1.1 이 [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] 로 확정했고, 본 branch 는 그 gate 가 쓰는 version tuple 호환 판정을 공급한다.
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외. 인접 계약은 각 owner 브랜치가 소유하며 본 브랜치는 그 계약을 *소비*하고 호환성 영향만 집행한다.
|
||||
|
||||
- runtime config schema 정의·boot 검증 mechanism → [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] (owns `FE-OC-004`).
|
||||
- boundary runtime(Zod) schema 검증 → [[raw/branch-notes/feature-runtime-schema-validation-contract]] (owns `FE-OC-007`).
|
||||
- storage key namespace·schemaVersion·migration mechanism → [[raw/branch-notes/feature-frontend-storage-registry-contract]] (owns `FE-OC-013`).
|
||||
- 8개 registry single-owner·diff-check tooling → [[raw/branch-notes/feature-frontend-contract-registry-governance]] (owns `FE-OC-022`).
|
||||
- release directory·atomic pointer·실제 rollback drill 실행 → [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] (owns `FE-OC-016`, `FE-OC-017`).
|
||||
- API client retry/idempotency 동작 → [[raw/branch-notes/feature-api-client-response-envelope-contract]] (owns `FE-OC-006`, `FE-OC-009`).
|
||||
- CI gate blocking 분리 wiring → [[raw/branch-notes/feature-frontend-ci-quality-gates-contract]].
|
||||
- backend API versioning 정책과 실제 migration 실행 → backend / 외부 owner (frontend 계약 밖).
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/official-docs/semver-2-0-0-spec-semver-official]] | D1 분류 어휘의 공식 근거(`SEMVER-C1`: MAJOR=incompatible / MINOR=backward-compatible additive / PATCH=backward-compatible fix). 단 SEMVER-C1은 "무엇이 breaking인지" 자동 분류는 증명하지 않으므로 경계 정의는 project decision(D1). |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | §3.3 compatibility_impact 어휘·§5.10 registry change protocol·§12.3 compatibility tuple·§12.5 rollback invariant·§9.2 cache discard·§6.4 config 검증·§15 `FE-GATE-014/015` — D1~D6 전부의 project-decision 근거. |
|
||||
|
||||
> 공식 표준(semver)이 *어휘*를 주고, hub가 *프로젝트 적용 규칙과 tuple 필드*를 준다. 두 계층이 함께 D1~D6을 닫는다.
|
||||
|
||||
## TODO
|
||||
|
||||
각 항목 등급: `planned`(코드 없음).
|
||||
|
||||
- [ ] 버전 tuple 행렬을 §구현 가이드 1에 확정 — 필드·source·compatibility 역할·mismatch 결과 — 등급: `planned`
|
||||
- [ ] additive/breaking 분류 fixture 표를 §구현 가이드 2에 확정(4개 schema 계열 × 각 등급 예시) — 등급: `planned`
|
||||
- [ ] migration/rollback 규칙 R1~R5를 §구현 가이드 3에 확정 — 등급: `planned`
|
||||
- [ ] `FE-GATE-014` config compatibility fixture(old/new config) + report artifact 스펙 — 등급: `planned`
|
||||
- [ ] `FE-GATE-015` release coherence fixture(mixed HTML/asset/config) + verification artifact 스펙 — 등급: `planned`
|
||||
- [ ] cache 호환성 default(discard) vs migration 선택 규칙 명세(§9.2 소유 조건) — 등급: `planned`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
- hub는 `FE-OC-023`의 owner를 이 브랜치로 지정하지만 `FE-D*` 결정 표에는 이 브랜치를 owner로 둔 행이 없다. 즉 이 브랜치는 *결정*이 아니라 *집행 규칙(governance)*을 소유한다 — 다른 브랜치의 `FE-D012/013/016/019`가 만든 schema 변경을 `FE-OC-023` 규칙으로 검사한다.
|
||||
- 미해결 위험(seed에서 승계): additive 변경이 cache+config+release **조합**에서 breaking이 될 수 있다(§구현 가이드 3의 Open Risk / R4에서 추적).
|
||||
- 버전 encoding(정수 MAJOR vs semver 문자열)은 hub가 "major incompatibility"만 말하고 literal 표기는 정하지 않았다 → §구현 가이드에서 `UNSUPPORTED_IMPL_DECISION`으로 표시.
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> 아래는 Decision Evidence Map의 prose 요약. 근거는 Sources 및 hub 섹션 참조.
|
||||
|
||||
- 2026-07-18: **D1** compatibility_impact 분류 어휘를 `{none, additive, behavior-change, breaking}` 단일 enum으로 채택하고 API/config/storage/release 4개 schema 계열 모두에 적용 / 이유: hub §3.3이 이 4값을 이미 정의; semver `SEMVER-C1`이 breaking/additive/fix 의미론을 공식 뒷받침 / 대안: 계열별 별도 어휘 / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §3.3 + `raw/official-docs/semver-2-0-0-spec-semver-official.md#SEMVER-C1`.
|
||||
- 2026-07-18: **D2** boot 호환성 identity를 `(buildId, configSchemaVersion, apiContractVersion, assetManifestHash, releaseId)` tuple로 판정하고 string lexical compare를 금지 / 이유: hub §12.3이 tuple과 비교 규칙을 명시 / 대안: 단일 monolithic release 버전 문자열 비교 / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §12.3 + §5.9.
|
||||
- 2026-07-18: **D3** 각 schema 계열은 독립 버전 필드를 가지며 breaking = MAJOR 상향(config/API), storage는 `schemaVersion` increment, query는 namespace version bump / 이유: hub §5.4/§5.5/§5.7이 필드를 정의; semver `SEMVER-C1` MAJOR 의미론 / 대안: 전 계약 공통 단일 버전 / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.4 §5.5 §5.7.
|
||||
- 2026-07-18: **D4** breaking/behavior-change 변경은 migration·version bump·discard·fallback 중 하나 + test evidence 없이 merge 금지 / 이유: hub §3.3(4)·§5.10(4) 규칙 / 대안: 없음(invariant) / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §3.3 §5.10.
|
||||
- 2026-07-18: **D5** rollback은 coherent tuple 집합(HTML+asset manifest+assets+compatible config+compatible API+release manifest)을 복원하고 HTML-only rollback을 금지 / 이유: hub §12.5 rollback invariant / 대안: 없음(invariant) / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §12.5.
|
||||
- 2026-07-18: **D6** 호환 불가 cache data는 default로 discard(재사용 금지)하며 migration을 선택할 때만 본 브랜치가 fixture·rollback을 소유 / 이유: hub §9.2 / 대안: 항상 migration / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §9.2.
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | compatibility_impact 어휘 = `{none, additive, behavior-change, breaking}` 단일 enum, 4개 schema 계열 공통 (`FE-OC-023`) | 4개 계열이 하나의 governance register를 공유하는 한 유지; 어떤 계열이 5번째 impact class가 필요하면 계열별 어휘로 분기 | `raw/official-docs/semver-2-0-0-spec-semver-official.md#SEMVER-C1`; [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §3.3 | `official-standard`(어휘) + `project-decision`(경계) | semver는 "무엇이 breaking인지"를 자동 분류하지 않음(`SEMVER-C1` does-not-prove) — 경계 정의가 사람 판단에 남음 |
|
||||
| D2 | boot 호환성 identity = 5-field tuple `(buildId, configSchemaVersion, apiContractVersion, assetManifestHash, releaseId)`, lexical compare 금지 (`FE-OC-023`, `FE-OC-016`) | static SPA release 인 동안 유지; SSR/edge 도입 시 별도 project fork(§6.2) 또는 tuple 차원 추가 시 재검토 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §12.3, §5.9 | `project-decision` | tuple 필드 중 하나라도 source가 비어 있으면(예: provider가 releaseId 미노출) 판정 불가 → §9 검증 대상 |
|
||||
| D3 | 계열별 독립 버전 필드; breaking→config/API MAJOR 상향, storage `schemaVersion` increment, query namespace version bump (`FE-OC-004`, `FE-OC-007`, `FE-OC-012`, `FE-OC-013`) | 외부 codegen SSOT가 없는 동안 유지; code generation SSOT 채택 시 버전 표기를 codegen 산출로 이관(hub `FE-D018` revisit trigger와 정렬) | `raw/official-docs/semver-2-0-0-spec-semver-official.md#SEMVER-C1`; [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.4, §5.5, §5.7 | `official-standard` + `project-decision` | literal encoding(정수 MAJOR vs semver 문자열) 미확정 → §구현 가이드 `UNSUPPORTED_IMPL_DECISION` |
|
||||
| D4 | breaking/behavior-change는 migration·version bump·discard·fallback 중 하나 + test evidence 없이 merge 금지 (`FE-OC-023`) | invariant — 항상 성립. 단 "additive"로 분류된 변경은 이 게이트를 우회하므로 분류 정확성이 전제 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §3.3, §5.10 | `project-decision`(invariant) | 오분류(breaking을 additive로) 시 게이트가 조용히 통과 → D1 분류 fixture로 방어 |
|
||||
| D5 | rollback은 coherent tuple 집합 복원, HTML-only rollback 금지 (`FE-OC-017`, `FE-OC-023`) | invariant — 항상 성립. 실제 drill 실행·pointer switch mechanism은 release-cache-rollback owner에 위임 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §12.5 | `project-decision`(invariant) | provider가 atomic pointer/cache purge를 지원하지 않으면 coherence 보장 불가 → reachability probe 필요(§12.5) |
|
||||
| D6 | 호환 불가 cache data는 default discard; migration 선택 시에만 본 브랜치가 fixture·rollback 소유 (`FE-OC-012`, `FE-OC-013`) | offline/persistence 요구가 없어 data 손실이 허용되는 동안 discard 유지; offline 요구가 생기면 migration으로 전환(hub `FE-D019` service worker off 조건과 연동) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §9.2 | `project-decision` + `conditional-default` | discard가 UX상 허용되는지 미검증(현재 persistence default off이라 위험 낮음) |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> `planned` blueprint. 프론트엔드 코드가 없으므로 경로는 hub §4.6 Planned directory blueprint / §5 registry owner map에서 인용한 *예정 경로*이다. 실제 path는 repository 생성 후 확정한다.
|
||||
|
||||
### 1. 버전 tuple 행렬 (Version tuple matrix)
|
||||
|
||||
> **Trace**: D2 (5-field boot tuple) + D3 (계열별 버전 필드). 근거 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §12.3 (tuple + 비교 규칙), §5.9 (release token registry), §5.4 (`CONFIG_SCHEMA_VERSION`/`API_CONTRACT_VERSION`).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: literal 버전 encoding(정수 MAJOR `"3"` vs semver 문자열 `"3.1.0"`). hub는 "major incompatibility"만 말하고 표기를 정하지 않음. trade-off: 정수 MAJOR는 boot 호환 판정이 가장 단순하나 additive/minor 가시성을 잃음 → **boot 판정용 정수 MAJOR + 진단용 optional MINOR** 병기를 제안(planned).
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 필드 저장 위치 파일명(예: `src/contracts/compatibility-tuple.js`). hub §4.6은 `src/bootstrap/`, `src/contracts/` 계층만 주고 파일명은 미지정. trade-off: contract 계층에 두어 boot·application 양쪽이 참조 가능하게 함.
|
||||
|
||||
| 버전 필드 | Source (§5 registry) | Compatibility 역할 | Mismatch 시 동작 (§12.3) | 정규화 error kind (§5.6) |
|
||||
|---|---|---|---|---|
|
||||
| `buildId` | CI build (`VITE_BUILD_ID`, §5.4) | asset/HTML coherence | assetManifestHash와 함께 coherence 판정 | `DEPLOY_MISMATCH` |
|
||||
| `configSchemaVersion` | runtime config schema (`CONFIG_SCHEMA_VERSION`, §5.4/§5.9) | boot compatibility | major incompatible → boot fail, product route 미mount | `BOOT_CONFIG_FAILURE` |
|
||||
| `apiContractVersion` | frontend/backend agreement (`API_CONTRACT_VERSION`, §5.4/§5.9) | schema compatibility | incompatible → route mount fail 또는 explicitly supported compatibility adapter | `DEPLOY_MISMATCH` |
|
||||
| `assetManifestHash` | build output (§5.9) | chunk integrity/mismatch | mismatch → controlled reload **once**(§10.2 guard) | `CHUNK_LOAD_FAILURE` |
|
||||
| `releaseId` | deploy system (§5.9) | rollback target | 나머지 버전 호환 시 mismatch → warning telemetry 후 continue 가능 | (telemetry only) |
|
||||
| storage `schemaVersion` | storage registry physicalKey `v<schema>` (§5.5) | 영속 data 호환 | previous version 읽으면 migration 또는 discard | `STORAGE_*` / discard |
|
||||
| query namespace version | query key registry (§5.7) | cache identity partition | API/schema breaking → namespace version bump; 호환 불가 cache → discard(D6) | `QUERY_CACHE_FAILURE` |
|
||||
|
||||
핵심 규칙(§12.3 그대로): **string lexical compare로 버전 호환을 판정하지 않는다.** 각 필드는 선언된 버전 값으로만 비교한다.
|
||||
|
||||
### 2. Additive vs breaking 분류 fixture
|
||||
|
||||
> **Trace**: D1 (분류 어휘) + D4 (분류→required action). 근거 `raw/official-docs/semver-2-0-0-spec-semver-official.md#SEMVER-C1`, [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §3.3, §6.4 (unknown key policy), §5.5, §5.7.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 분류를 사람이 PR checklist로 판정할지 diff 도구로 자동화할지. hub §3.3은 *수동 프로토콜*만 정의. trade-off: 초기엔 수동 checklist + fixture로 회귀 방지, 자동 diff 도구는 registry-governance 브랜치 tooling으로 위임(planned).
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: fixture 디렉터리·파일명(예: `test/compatibility/fixtures/*.json`). hub는 fixture *존재*(§15 "old/new config versions", "mixed HTML/assets/config")만 요구, 경로 미지정. trade-off: gate별 하위 폴더로 분리해 `FE-GATE-014`/`015`가 독립 소비.
|
||||
|
||||
| 변경 예시 | compatibility_impact | 근거 규칙 | Required action |
|
||||
|---|---|---|---|
|
||||
| config에 optional key 추가(schema passthrough/default 존재) | `additive` | §6.4 unknown key: additive keys allowed only if schema explicitly passthroughs | 버전 bump optional, migration 불필요 |
|
||||
| config에 required key 추가 / 기존 key 의미 변경 | `breaking` | §5.4 `CONFIG_SCHEMA_VERSION` compatibility fail | configSchemaVersion MAJOR 상향 + migration/fallback + `FE-GATE-014` fixture |
|
||||
| API 응답에 optional field 추가(schema가 unknown 안전 처리) | `additive` | §6.4 default strict; passthrough 시 additive | none/additive, apiContractVersion 유지 |
|
||||
| API 응답 field 제거·rename(mapper가 소비) | `breaking` | §5.7 "API/schema breaking change" | apiContractVersion 상향 + compatibility adapter 또는 coordinated release |
|
||||
| storage 값 shape 변경 | `breaking` | §5.5 "incompatible change 시 increment", migration/discard | storage `schemaVersion` increment + migration 또는 discard(D6) |
|
||||
| release asset set 변경(chunk hash 변경) | 호환상 `none` | §12.3 assetManifestHash coherence | atomic deploy 순서(§12.4), coherence는 `FE-GATE-015`가 검증 |
|
||||
| error kind enum 제거 | `breaking`(behavior-change) | §5.6 stable enum | consumer migration + version note, D4 게이트 |
|
||||
|
||||
분류 경계의 근거 한계: `SEMVER-C1`은 MAJOR=incompatible / MINOR=additive / PATCH=fix *의미론*을 주지만 "내부 구현 변경이 API에 미치는 영향을 자동 분류하지 않는다"(does-not-prove). 따라서 위 표의 각 행 경계는 **project decision(D1)**이며 fixture로 회귀 고정한다.
|
||||
|
||||
### 3. rollback 규칙
|
||||
|
||||
> **Trace**: D4 (merge 게이트) + D5 (rollback coherence) + D6 (cache discard). 근거 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §3.3, §5.10, §9.2, §12.5.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: migration 함수 배치·명명(예: storage per-version migrator API 모양). hub §5.5는 "migration 또는 discard" 원칙만, mechanism 미지정. trade-off: storage adapter 소유이므로 storage-registry 브랜치와 공동 정의 — 본 브랜치는 *규칙*만, migrator *구현*은 위임(R3).
|
||||
|
||||
- **R1 (no silent breaking)**: `compatibility_impact ∈ {behavior-change, breaking}` 인 변경은 migration OR version bump + test evidence 없이 merge 금지(§3.3.4). additive/none은 게이트 우회 가능하나 §2 분류 fixture로 오분류 방어.
|
||||
- **R2 (breaking → 처리 택1)**: registry/storage/cache breaking은 version bump와 함께 **migration · discard · fallback** 중 하나를 명시(§5.10.4, §9.2). "택1"을 비우면 orphan token scan(§5.10.8)과 D4 게이트가 fail.
|
||||
- **R3 (rollback coherence)**: rollback target은 prior HTML + prior asset manifest·assets + compatible runtime config + compatible API contract(또는 backend compatibility window) + release manifest 의 **coherent set**을 복원한다(§12.5). HTML만 과거로 되돌리고 config를 최신에 남기는 rollback 금지.
|
||||
- **R4 (조합 breaking 방어)**: 개별 additive라도 cache+config+release **조합**에서 incompatible하면 D6에 따라 cache discard로 강등한다(§9.2). 이 조합 판정은 §1 tuple 행렬 전체를 함께 평가한다. — *잔여 위험: 조합 폭발을 전수 fixture로 덮지 못할 수 있음(§9 검증 대상).*
|
||||
- **R5 (비교 방식)**: 모든 버전 비교는 선언 필드 기준(§12.3), lexical string compare 금지.
|
||||
|
||||
gate 소유 매핑:
|
||||
|
||||
| Gate | 이 브랜치 산출물 |
|
||||
|---|---|
|
||||
| `FE-GATE-014@1` config compatibility (Owner = 본 branch) | old/new config version fixture 제공 |
|
||||
| `FE-GATE-015@1` release coherence (Owner = release-cache) | version tuple 호환 판정 공급 |
|
||||
| `FE-GATE-004@1` runtime schema (Owner = runtime-schema-validation) | config invalid matrix 에 compatibility 필드 기여 |
|
||||
|
||||
각 gate 의 blocking scope·pass condition·evidence artifact 는 hub §15.1 소유이며 여기에 옮겨 적지 않는다.
|
||||
|
||||
> **UNSUPPORTED_IMPL_DECISION**: artifact 파일 경로(예: `artifacts/release/compatibility-report.json`). hub §15는 artifact *이름*("compatibility report"/"release verification")만 주고 경로 미지정. trade-off: §14.3 `pnpm verify:release`(`artifacts/release/verification.json`) 관례를 따라 `artifacts/release/` 하위로 통일(planned).
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- config schema major incompatible → `BOOT_CONFIG_FAILURE`, product route 미mount, boot error shell만 렌더(§6.3).
|
||||
- API contract incompatible → route mount fail 또는 supported compatibility adapter, `DEPLOY_MISMATCH`(§12.3).
|
||||
- asset manifest mismatch → controlled reload **once**; 같은 release pair 두 번째 실패 시 auto reload 중단·rollback/support surface(§10.2 `CHUNK_RELOAD_GUARD`).
|
||||
- releaseId만 mismatch·나머지 호환 → warning telemetry(`release.mismatch.detected`) 후 continue(§12.3).
|
||||
- 호환 불가 cache → discard, 재사용 금지(§9.2, D6).
|
||||
- partial rollout / cached config / mixed release: tuple 조합이 incompatible일 수 있음 → R4로 강등, 잔여는 `needs-confirmation`.
|
||||
- **다른 계약 의존** (§20 Dependency + §4.3 matrix; 각 sibling은 FE-OC 계약으로만 참조 — 로컬 D 번호 미확인):
|
||||
- [[raw/branch-notes/feature-frontend-contract-registry-governance]] 의 `FE-OC-022` — 8개 registry가 single owner·compatibility impact를 기록해야 본 브랜치 분류가 대상 필드를 가짐. 그 계약이 바뀌면 §1 tuple 행렬 필드 source가 흔들린다.
|
||||
- [[raw/branch-notes/feature-runtime-schema-validation-contract]] 의 `FE-OC-007` — boundary schema 검증이 additive/breaking을 실제로 감지(unknown key strict/passthrough)한다. §2 분류의 런타임 근거.
|
||||
- [[raw/branch-notes/feature-frontend-storage-registry-contract]] 의 `FE-OC-013` — storage `schemaVersion`·migration/discard mechanism 소유. 본 브랜치의 cache-discard 결정과 §3 R2가 이 계약 위에서 동작.
|
||||
- [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] 의 `FE-OC-004` — `CONFIG_SCHEMA_VERSION` 을 runtime config로 공급(그 브랜치 결정 근거는 hub [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D012, FE-D013).
|
||||
- [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] 의 `FE-OC-016`, `FE-OC-017` — 실제 rollback drill·release tuple 산출. 본 브랜치 rollback-coherence 규칙의 집행 주체(그 브랜치 근거는 hub [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D019~FE-D023).
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| breaking change가 migration/version bump 없이 배포되지 않는다 | CI·release flow 없음, 코드 없음 | additive/breaking synthetic fixture(§2) → `FE-GATE-014` config compatibility test(old/new config: supported pass / incompatible boot fail) | `needs-confirmation` |
|
||||
| release coherence가 mixed HTML/asset/config를 탐지한다 | 실제 release verification 미실행 | `FE-GATE-015` mixed fixture → mismatch detected / coherent set passes → release verification artifact | `needs-confirmation` |
|
||||
| 버전 비교가 lexical string compare에 의존하지 않는다 | 구현 없음 | tuple 비교 함수 unit test에 `"9" vs "10"` 류 lexical trap fixture 포함 | `planned` |
|
||||
| 호환 불가 cache data가 discard되고 재사용되지 않는다 | query cache 구현 없음 | query namespace version bump 시 stale cache discard integration test(§9.2) | `planned` |
|
||||
| rollback이 coherent tuple 집합을 복원한다(HTML-only rollback 차단) | 실제 rollback drill 없음 | `FE-GATE-016` rollback drill: HTML-only rollback fixture가 fail, coherent tuple rollback이 pass(§12.5) | `needs-confirmation` |
|
||||
| additive 변경이 cache+config+release 조합에서 breaking이 되지 않는다(또는 R4로 강등된다) | 조합 폭발, 전수 fixture 어려움 | 대표 조합 fixture matrix로 R4 강등 경로 검증; 미커버 조합은 명시적 잔여 위험 기록 | `needs-confirmation` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
- 스캐폴딩 단계: `/coverage` 실행 전 수동 행을 만들지 않는다.
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
- 없음 — 스캐폴딩 단계.
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: artifact-imports:start -->
|
||||
### 가져온 artifact 계약
|
||||
|
||||
| Artifact Ref | Owner | Producer | Schema Ref |
|
||||
|---|---|---|---|
|
||||
| `ART-FE-003@1` | [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] | [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] | `harness/source/artifact-schemas/ca-skeleton-frontend/release-verification.schema.json` |
|
||||
<!-- GENERATED: artifact-imports:end -->
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-GATE-004@1` | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | invalid fixture 가 예상 kind 로 거부되지 않으면 merge 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-015@1` | [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] | 혼재된 release 조합이 감지되지 않으면 release 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-016@1` | [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] | rollback 과 smoke 증거가 없으면 production promotion 을 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-OC-004@1` | [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] | build-time, runtime-public, secret config를 MUST 분리하고 boot 전에 runtime config를 검증 | import 참조로 적용 |
|
||||
| `FE-OC-007@1` | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | JSON envelope와 payload는 boundary에서 runtime schema를 MUST 통과 | import 참조로 적용 |
|
||||
| `FE-OC-012@1` | [[raw/branch-notes/feature-server-state-caching-contract]] | query key와 invalidation은 registry factory만 MUST 사용 | import 참조로 적용 |
|
||||
| `FE-OC-013@1` | [[raw/branch-notes/feature-frontend-storage-registry-contract]] | storage key는 namespace·version·classification을 MUST 가지며 token/secret 저장을 금지 | import 참조로 적용 |
|
||||
| `FE-OC-016@1` | [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] | HTML, asset, runtime config, release manifest cache policy를 MUST 구분 | import 참조로 적용 |
|
||||
| `FE-OC-017@1` | [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] | rollback은 immutable prior release로 수행하고 build/config/API compatibility를 MUST 검증 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
- 없음 — 자식 자료는 생성 후 controller가 parent Cluster와 함께 등록한다.
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
- 없음 — daily note는 이 작업에서 수정하지 않는다.
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크: 없음
|
||||
- 리뷰 메모: 없음
|
||||
- 머지 결과 / 배포 환경: `planned`
|
||||
- **wiki 추출 대상** (verified만): 없음
|
||||
- **추출하지 않을 항목** (planned / documented-only / abandoned): 현재 전체
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
---
|
||||
title: branch / feature-frontend-contract-registry-governance
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-022
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-022
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-REGISTRY-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-001, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-002]
|
||||
contract_packet: 1
|
||||
branch: feature-frontend-contract-registry-governance
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract]
|
||||
tags: [branch, ca-skeleton, frontend, api-design, javascript, api-contract]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
contract_packet_sha256: 42419c3541919063c7668d0cbd210002b61996c27b573d86b36ba2b19337597b
|
||||
imports: [FE-OC-004@1, FE-OC-005@1, FE-OC-006@1, FE-OC-008@1, FE-OC-012@1, FE-OC-013@1, FE-OC-014@1, FE-OC-016@1, FE-OC-020@1, FE-OC-023@1]
|
||||
|
||||
---
|
||||
|
||||
# branch: feature-frontend-contract-registry-governance
|
||||
|
||||
> Layer: `raw/branch-notes/` — TODO·결정·진행 기록. 구현 결과는 검증 뒤 `/ingest`로만 추출한다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
[[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: 8개 registry snapshot·schema validation·single-owner check가 통과한다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-REGISTRY-001@1` | route, API operation, env, storage, error, query, telemetry, release token을 8개 registry로 관리한다 | 8개 registry의 owner·schema·impact·snapshot governance를 집행한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
|
||||
<!-- section-id: branch-local-decisions -->
|
||||
### 브랜치 지역 결정
|
||||
|
||||
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
||||
|---|---|---|---|---|
|
||||
| D1 | 8개 registry를 single-owner model로 관리한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D2 | project owner map을 registry 소유 SSOT로 사용한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D3 | 모든 registry change에 compatibility impact를 기록한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D4 | uniform schema validation과 orphan scan을 실행한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D5 | per-registry snapshot과 diff를 evidence로 남긴다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D6 | producer와 consumer test의 동기 갱신을 gate한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
|
||||
<!-- section-id: declared-overrides -->
|
||||
### 선언한 예외
|
||||
|
||||
| Override ID | Overrides | Reason | Approval | Status |
|
||||
|---|---|---|---|---|
|
||||
|
||||
없음.
|
||||
<!-- GENERATED: branch-contract:end -->
|
||||
|
||||
<!-- section-id: branch-goal -->
|
||||
## 목표
|
||||
|
||||
- 이 branch는 `FE-OC-022`(8개 registry는 single primary owner와 compatibility impact를 MUST 기록)를 *구현 착수 가능한 governance 명세*로 내린다. 근거는 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D018 (route/API-operation/env/storage/error/query/telemetry/release token을 8개 registry로 관리)이며, 관리 대상 registry 목록과 owner는 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.1 owner map, 변경 절차는 §5.10, compatibility 분류는 §3.3에서 온다.
|
||||
- 본 branch는 **registry의 *내용*(각 registry의 schema field·row)을 재정의하지 않는다.** 각 registry의 schema는 그 registry의 owner branch가 소유한다(§5.2~§5.9). 본 branch는 그 registry들을 *가로질러* 강제하는 **governance 규칙**만 소유한다: owner map single-owner check, uniform schema-validation harness, compatibility-impact 기록 gate, per-registry snapshot/diff. 모든 항목은 repo가 없으므로 `planned`.
|
||||
- 이슈: 없음 (스캐폴딩 단계)
|
||||
- PR: 없음
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- **owner map governance** — [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.1의 8-registry owner map을 registry 소유의 SSOT로 고정하고, single-owner check(registry당 owner가 0개/2개 이상이면 fail)를 정의 (`FE-OC-022`).
|
||||
- **uniform schema-validation harness** — 8개 registry 각 row가 *owner가 선언한* minimum schema(§5.2~§5.9)를 만족하는지 대조 + orphan/ad hoc token scan = 0 (`FE-SC-005`, §5.10 step 8).
|
||||
- **compatibility-impact 기록 gate** — 모든 registry change가 `compatibility_impact ∈ {none, additive, behavior-change, breaking}`를 MUST 기록 (§3.3, §5.10).
|
||||
- **per-registry snapshot + diff artifact** — `FE-OC-022`의 minimum evidence(registry diff check) 산출물.
|
||||
- **producer/consumer test 동기 갱신 gate** — registry change 시 producer test와 consumer test가 *함께* 갱신되었음을 검사 가능한 증거로 강제 (§5.10 step 5). 개별 test 자체의 계층·러너·fixture 책임은 [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] (`FE-OC-020`) 소유이며, 본 branch는 *registry change 시점의 동기 갱신 여부*만 gate 한다.
|
||||
- contributes to (owner 아님, fixture/gate 협업): [[raw/branch-notes/feature-routing-navigation-guard-contract]] (`FE-OC-005`), [[raw/branch-notes/feature-api-client-response-envelope-contract]] (`FE-OC-006`), [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] (`FE-OC-004`), [[raw/branch-notes/feature-frontend-storage-registry-contract]] (`FE-OC-013`), [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] (`FE-OC-008`), [[raw/branch-notes/feature-server-state-caching-contract]] (`FE-OC-012`), [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] (`FE-OC-014`), [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] (`FE-OC-016`), [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] (`FE-OC-023`), [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] (`FE-OC-020`).
|
||||
- ⚠️ hub §20 branch decomposition의 "Contributes to" cell은 이 branch에 대해 `FE-OC-004`(env)·`FE-OC-012`(query)를 누락하고 있다. 그러나 §5.1 owner map은 `FE-REG-ENV`·`FE-REG-QUERY`를 8개 governed registry에 포함하므로, 본 note의 owner map(§1)과 위 목록은 §5.1을 따른다. hub 수정은 hub owner 소관 — 본 branch는 hub를 편집하지 않는다.
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외 — 다른 owner branch 소유. 여기서 detail을 정의하면 `OUT_OF_BRANCH_SCOPE` bleed (CLAUDE.md §15.5 R3).
|
||||
|
||||
- **각 registry의 실제 내용·schema field·초기 row** — 그 registry의 owner branch 소유: route [[raw/branch-notes/feature-routing-navigation-guard-contract]] (`FE-OC-005`), API operation [[raw/branch-notes/feature-api-client-response-envelope-contract]] (`FE-OC-006`), env [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] (`FE-OC-004`), storage [[raw/branch-notes/feature-frontend-storage-registry-contract]] (`FE-OC-013`), error [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] (`FE-OC-008`), query key [[raw/branch-notes/feature-server-state-caching-contract]] (`FE-OC-012`), telemetry [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] (`FE-OC-014`), release token [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] (`FE-OC-016`). 본 branch는 그 schema를 *검증*할 뿐 *정의*하지 않는다.
|
||||
- **test 계층·러너·fixture 분류 자체** — [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] (`FE-OC-020`) 소유. 본 branch는 "어떤 test를 어떻게 짜는가"를 정의하지 않고, registry change PR에서 producer/consumer test가 *함께 움직였는지*만 검사한다.
|
||||
- **version-tuple matrix, additive/breaking fixture, migration/rollback 규칙** — [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] (`FE-OC-023`) 소유. 본 branch는 impact label을 *기록*하고, breaking 판정 후의 version bump·migration 메커니즘은 그 branch로 위임한다.
|
||||
- **registry code generation SSOT** — [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D018`의 revisit trigger(미도래). governance는 hand-maintained registry 파일을 전제로 한다.
|
||||
- **payload runtime boundary schema 검증** — [[raw/branch-notes/feature-runtime-schema-validation-contract]] (`FE-OC-007`) 소유. registry schema 검증(build/test-time)과 다른 관심사.
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/official-docs/semver-2-0-0-spec-semver-official]] | compatibility impact label 중 `breaking`/`additive`/`patch` 구분의 외부 표준 기준 — `SEMVER-C1` (MAJOR/MINOR/PATCH 증가 의미론). (D3) |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | 8-registry 관리 결정 FE-D018, owner map §5.1, registry change protocol §5.10, decision change protocol §3.3 — governance 규칙 전체의 project-decision SSOT. (D1/D2/D3/D4/D5) |
|
||||
|
||||
## TODO
|
||||
|
||||
각 항목 옆에 증거 등급 표기. 현재 frontend repo 부재 → 전부 `planned`.
|
||||
|
||||
- [ ] §5.1 owner map을 governance manifest로 고정 + single-owner check(zero/duplicate owner fail) 정의 — 등급: `planned`
|
||||
- [ ] 8개 registry를 owner minimum schema(§5.2~§5.9)로 검증하는 uniform validation harness 명세 — 등급: `planned`
|
||||
- [ ] orphan/ad hoc token scan = 0 (`FE-SC-005`) 규칙 + 실패 fixture 정의 — 등급: `planned`
|
||||
- [ ] registry change 시 `compatibility_impact` 4-label 기록 gate + behavior-change/breaking merge block 규칙 — 등급: `planned`
|
||||
- [ ] per-registry snapshot + diff artifact(owner·affected FE-OC·impact 표면화) 명세 — 등급: `planned`
|
||||
- [ ] registry change 시 producer/consumer test 동기 갱신 gate(§5.10 step 5) 명세 — 검사 가능한 증거(PR touch-set + consumer-side token 참조 검증) 정의 — 등급: `planned`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
- registry row와 branch ownership의 분리 방식 확정: **ownership은 owner map manifest가 소유, registry의 실제 row/schema는 각 owner branch가 소유.** governance harness는 registry 파일을 *읽어 검증*할 뿐 *편집*하지 않는다 — 이로써 single-owner invariant를 유지한다.
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> Decision Evidence Map의 prose mirror. 근거는 Sources 또는 hub decision register.
|
||||
|
||||
- 2026-07-19: 8개 contract registry를 **single-owner governance model**로 관리 (FE-D018) / 이유: rename·compatibility 영향 추적 / 검토한 대안: registry code generation SSOT (FE-D018 revisit trigger) / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D018 (`accepted-documented-only`).
|
||||
- 2026-07-19: `§5.1` owner map을 registry 소유 SSOT로 삼고 single-owner check로 zero/duplicate owner를 차단 / 이유: registry당 정확히 1 owner invariant / 검토한 대안: 명시적 co-owner protocol(현재 미채택) / 근거: `FE-OC-022`, §5.1.
|
||||
- 2026-07-19: registry change마다 `compatibility_impact` 4-label 기록, `behavior-change`/`breaking`은 migration/rollback/test evidence 없이 merge 금지 / 이유: 무증거 breaking 배포 차단 / 검토한 대안: 자유 서술 changelog / 근거: §3.3, §5.10, `SEMVER-C1` (version-tuple 메커니즘 자체는 `FE-OC-023` owner).
|
||||
- 2026-07-19: uniform schema-validation harness가 각 registry를 *owner가 선언한* minimum schema로 검증 + orphan token scan 0 / 이유: ad hoc token 0 (`FE-SC-005`) 강제 / 근거: `FE-OC-022`, §5.10 step 8.
|
||||
- 2026-07-19: per-registry snapshot + diff = `FE-OC-022`의 registry diff check evidence / 근거: §5.10 step 6-7.
|
||||
- 2026-07-20: registry change는 **producer test와 consumer test의 동기 갱신을 검사 가능한 증거로 증명**해야 merge 가능 (§5.10 step 5) / 이유: registry row만 바뀌고 test는 이전 token을 계속 검증하면 gate가 green인 채로 계약이 깨짐(silent contract drift) / 검토한 대안: (a) 사람 리뷰 체크리스트만 두기 — 검사 불가라 기각, (b) 전부 [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] (`FE-OC-020`)에 위임 — test *계층*은 그 branch 소유가 맞으나 "registry change 시점의 동기성"은 §5.10 registry change protocol의 step이므로 `FE-OC-022`가 소유 / 근거: §5.10 step 5 + step 8 orphan scan(`FE-SC-005`).
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
> 각 결정의 raw source claim. `Decision ID`는 이 branch-note 안에서 안정. FE-D### 참조는 hook 회피를 위해 hub project 경로에만 부착.
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | 8개 registry를 single-owner governance model로 관리 ([[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D018 / `FE-OC-022`) | hand-maintained registry 파일 + governance gate가 default; code generation SSOT가 채택되면 generated registry로 전환 (FE-D018 revisit trigger) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D018, §5.1 | `project-decision` (accepted-documented-only) | FE-D018은 code evidence 없는 accepted-documented-only — repo 생성 전까지 governance gate 미검증 |
|
||||
| D2 | owner map §5.1이 registry 소유 SSOT; single-owner check가 zero/duplicate owner를 차단 (`FE-OC-022`) | registry당 정확히 1 owner가 invariant; 공동 소유가 필요하면 명시적 co-owner protocol을 신규 제안(planned)해야 하며 그 전엔 single-owner 강제 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.1 (`FE-OC-022`) | `project-decision` | owner map이 owner branch보다 늦게 갱신되면 `STALE_OWNER` 위험 |
|
||||
| D3 | registry change마다 `compatibility_impact`(none/additive/behavior-change/breaking) 기록; behavior-change/breaking은 migration/rollback/test 없이 merge 금지 (§3.3) | `none`·`additive`는 gate 통과; `behavior-change`·`breaking`은 version bump + migration/rollback/test evidence 필요(version-tuple 메커니즘은 `FE-OC-023` owner branch) | `raw/official-docs/semver-2-0-0-spec-semver-official.md#SEMVER-C1`; [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §3.3 · §5.10 | `official-doc + project-decision` | `SEMVER-C1`은 *무엇이* breaking인지 자동 분류하지 않음 — label 판정은 사람 판단, 오분류 위험 |
|
||||
| D4 | uniform schema-validation harness가 각 registry를 owner-declared minimum schema(§5.2~§5.9)로 검증 + orphan/ad hoc token scan 0 | 각 registry schema는 owner branch가 §5.2~§5.9에서 선언; governance는 그 schema 대조 + `FE-SC-005` orphan scan만 수행, schema 내용은 재정의 안 함 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.10 step 8 (`FE-OC-022`, `FE-SC-005`) | `project-decision` | validation 라이브러리/방식 미지정(UNSUPPORTED_IMPL_DECISION); owner schema 변경 시 harness 동기화 필요 |
|
||||
| D5 | per-registry snapshot + diff artifact = `FE-OC-022` registry diff check evidence | 모든 registry change에서 snapshot 재생성 + 이전 snapshot과 diff; diff는 owner·affected FE-OC·compatibility impact를 표면화 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.10 step 6-7 (`FE-OC-022`) | `project-decision` | snapshot 포맷/저장 경로 미지정(UNSUPPORTED_IMPL_DECISION) |
|
||||
| D6 | registry change는 producer/consumer test 동기 갱신을 검사 가능한 증거로 증명해야 merge 가능 (§5.10 step 5) | registry token이 add/rename/remove 되면 gate 발동; 순수 주석·문서 변경이면 미발동. test *계층/러너/fixture 분류*는 [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] (`FE-OC-020`) 소유, *동기성 검사*만 본 branch | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.10 step 5 · step 8 (`FE-OC-022`, `FE-SC-005`) | `project-decision` | §5.10 step 5는 "함께 갱신한다"는 원칙만 말하고 *무엇이 producer/consumer test인지*·*어떤 증거로 증명하는지*를 지정하지 않음 — 판정 메커니즘은 UNSUPPORTED_IMPL_DECISION |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> `planned` blueprint. 모든 경로는 hub §4.6 Planned directory blueprint + §5.1 owner map에서 도출(grounded)되나, frontend repo가 없으므로 전체 `planned`. 3-rule(R1 Trace / R2 UNSUPPORTED_IMPL_DECISION / R3 no OUT_OF_BRANCH_SCOPE) 준수.
|
||||
|
||||
### 1. Registry owner map + single-owner check
|
||||
|
||||
> **Trace**: D1 + D2 — [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D018, §5.1 (`FE-OC-022`).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: governance manifest 파일 경로 — hub §4.6 blueprint의 `src/contracts/`에는 8개 registry 파일만 있고 governance manifest 파일이 없다. 제안: `src/contracts/registry-manifest.js` (planned). trade-off: registry 8파일 옆에 두면 응집도↑이나 registry 파일과 manifest를 혼동할 위험 → 파일명에 `-manifest` 접미로 구분.
|
||||
|
||||
owner map(§5.1에서 그대로 도출 — registry의 *내용*이 아니라 *소유*만 governance가 소유):
|
||||
|
||||
| Registry ID | Owner branch (single) | Planned registry path (§5.1) | Governed contract |
|
||||
|---|---|---|---|
|
||||
| `FE-REG-ROUTE` | `feature-routing-navigation-guard-contract` | `src/contracts/routes.js` | `FE-OC-005` |
|
||||
| `FE-REG-API` | `feature-api-client-response-envelope-contract` | `src/contracts/api-operations.js` | `FE-OC-006` |
|
||||
| `FE-REG-ENV` | `feature-frontend-env-runtime-config-contract` | `src/contracts/env.js` | `FE-OC-004` |
|
||||
| `FE-REG-STORAGE` | `feature-frontend-storage-registry-contract` | `src/contracts/storage-keys.js` | `FE-OC-013` |
|
||||
| `FE-REG-ERROR` | `feature-frontend-error-classification-boundary-contract` | `src/contracts/errors.js` | `FE-OC-008` |
|
||||
| `FE-REG-QUERY` | `feature-server-state-caching-contract` | `src/contracts/query-keys.js` | `FE-OC-012` |
|
||||
| `FE-REG-TELEMETRY` | `feature-frontend-observability-logging-trace-contract` | `src/contracts/telemetry.js` | `FE-OC-014` |
|
||||
| `FE-REG-RELEASE` | `feature-frontend-release-cache-rollback-contract` | `src/contracts/release-tokens.js` | `FE-OC-016` |
|
||||
|
||||
single-owner check 규칙:
|
||||
- registry가 manifest에 owner 0개 → `zero-owner` fail.
|
||||
- registry가 owner ≥2개 → `duplicate-owner` fail.
|
||||
- owner branch가 아닌 change가 registry 파일을 편집 → `non-owner-mutation` fail. **이는 repo-level ownership(누가 그 파일을 *편집*할 수 있는가) 검사이며, runtime module mutation 검사가 아니다** — 아래 §2 schema harness는 registry의 *내용*만 읽어 검증하므로 이 규칙을 집행하지 않는다.
|
||||
- **UNSUPPORTED_IMPL_DECISION**: `non-owner-mutation`의 강제 메커니즘 — hub는 owner map(§5.1)에 owner branch 이름만 적고 강제 수단을 지정하지 않는다. 두 후보는 서로 다른 것을 본다: (a) **CODEOWNERS / path-glob repo ownership** — `src/contracts/<registry>.js` 경로별 owner를 선언하고 non-owner PR을 review-block. owner map과 1:1로 대응해 *편집 권한*을 정확히 표현하나, git host 기능에 의존하고 CI에서 재현하려면 별도 glob 검사 스크립트가 필요. (b) **import-graph 정적 검사** ([[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] `FE-OC-002`의 dependency-cruiser 재사용) — 도구는 이미 있으나 import graph는 *누가 파일을 수정했는가*를 볼 수 없고 *어느 모듈이 registry를 import 하는가*만 본다. registry는 설계상 모든 layer가 read 목적으로 import 하므로 이 신호로는 owner 위반을 구분할 수 없다. **선택: (a) path-glob repo ownership.** trade-off: git host 종속을 받아들이는 대신 owner map invariant를 있는 그대로 검사한다. (b)는 관심사 불일치로 기각.
|
||||
|
||||
### 2. Uniform schema-validation harness + orphan token scan
|
||||
|
||||
> **Trace**: D4 — §5.10 step 8, `FE-SC-005` (`FE-OC-022`). 각 registry의 minimum schema는 owner branch가 §5.2~§5.9에서 선언 — 본 §은 그 schema를 *검증*하는 harness만 명세하며 schema field를 재정의하지 않는다 (R3).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: validation 구현 방식 — Zod(`FE-OC-007` owner의 stack) 재사용 vs 독립 plain-JS assertion. hub 미지정. trade-off: Zod 재사용은 신규 의존 없이 통일성↑이나, registry validation은 build/test-time이라 runtime boundary(`FE-OC-007`)와 결합하면 concern 혼입 → 독립 test-time validator를 default로 두고 스키마 표현만 공유 검토.
|
||||
|
||||
harness 규칙(각 registry 공통, 내용 불변):
|
||||
|
||||
| 검사 | 규칙 | 근거 |
|
||||
|---|---|---|
|
||||
| required-field | registry의 각 row가 owner schema의 `Required: yes` field를 전부 보유 | §5.2~§5.9 각 owner schema |
|
||||
| id-format | stable ID(routeId·operationId·storage logicalName·error kind·query namespace·telemetry eventName·release token·env key)가 owner schema가 지정한 casing 규칙 준수 | 각 owner schema |
|
||||
| id-uniqueness | registry 내 stable ID 중복 0 | single-owner invariant 파생 |
|
||||
| orphan-token (bidirectional) | 코드가 참조하는 모든 token이 registry에 존재 **and** registry의 모든 token이 코드에서 ≥1회 참조 → orphan 0 | §5.10 step 8, `FE-SC-005` |
|
||||
| ad-hoc-token | registry를 우회한 literal(§5.1의 "Ad hoc use failure" 열 case) 검출 시 fail — 정적 강제 세부는 각 owner branch, governance는 **aggregate scan** | §5.1 |
|
||||
|
||||
### 3. Compatibility-impact 기록 gate
|
||||
|
||||
> **Trace**: D3 — §3.3 decision change protocol, §5.10 registry change protocol, `SEMVER-C1`. version-tuple/migration/rollback 메커니즘은 `FE-OC-023` owner branch로 위임 (R3 pointer).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: impact label 기록 매체 — PR template field vs snapshot metadata vs changelog row. hub 미지정. trade-off: snapshot metadata에 넣으면 diff와 원자적이나 PR review 가시성↓ → snapshot metadata를 SSOT로, PR template은 mirror로 검토.
|
||||
|
||||
기록 절차(§3.3 step 3-4 + §5.10 step 3-4 도출):
|
||||
1. registry change 제안 시 `compatibility_impact ∈ {none, additive, behavior-change, breaking}` 중 하나를 MUST 기록.
|
||||
2. `none`·`additive` → gate 통과 (예: schema에 optional field 추가).
|
||||
3. `behavior-change`·`breaking` → migration/rollback/test evidence 없이 merge block. rename은 stable ID 규칙상 breaking(§5.2 `routeId` rename=breaking 등).
|
||||
4. version bump 규칙(어느 tuple을 몇으로 올릴지)·migration 실행은 `FE-OC-023` owner branch 정의를 소비 — 본 gate는 *label 존재와 evidence 유무*만 강제.
|
||||
|
||||
### 4. Per-registry snapshot + diff artifact
|
||||
|
||||
> **Trace**: D5 — §5.10 step 6-7, `FE-OC-022` minimum evidence(registry diff check).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: snapshot 포맷(JSON vs serialized JS) + 저장 경로 — hub §4.6 `artifacts/`에 registry 전용 subdir 없음. 제안: `artifacts/quality/registry-snapshots/<registry-id>.json` (planned). trade-off: JSON은 도구 독립 diff가 쉬우나 registry가 JS 함수(query-key factory 등)를 포함하면 직렬화 손실 → 함수형 registry는 shape/서명만 snapshot.
|
||||
|
||||
- 각 registry change마다 snapshot 재생성 후 직전 snapshot과 diff.
|
||||
- diff는 최소 다음을 표면화: added/removed/renamed token, owner, affected `FE-OC-*`, `compatibility_impact`.
|
||||
- orphan token ≠ 0 이면 merge 불가 (§5.10 step 8).
|
||||
|
||||
### 5. Producer/consumer test 동기 갱신 gate
|
||||
|
||||
> **Trace**: D6 — §5.10 step 5("producer와 consumer test를 함께 갱신한다") + step 8 orphan scan (`FE-OC-022`, `FE-SC-005`). test 계층·러너·fixture 분류는 [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] (`FE-OC-020`) 소유 — 본 §은 *registry change 시점의 동기성*만 명세한다 (R3).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: producer/consumer test의 식별 방식 — hub §5.10 step 5는 원칙만 말하고 "무엇이 producer test이고 무엇이 consumer test인지", "동기 갱신을 어떤 증거로 증명하는지"를 지정하지 않는다. 후보: (a) **PR touch-set 규칙** — registry 파일이 바뀐 PR은 대응 test 경로도 함께 touch 해야 통과. 구현이 단순하나 *빈 수정*으로 우회 가능. (b) **token-reference 검사** — §2의 bidirectional orphan scan을 test 소스까지 확장해, consumer test가 registry에 더 이상 없는 token을 참조하면 fail. 우회 불가하나 remove/rename만 잡고 *추가된 token에 test가 없는 경우*는 못 잡는다. **선택: (a)+(b) 동시 적용** — (b)가 정확성을, (a)가 커버리지(신규 token)를 담당. trade-off: 검사 2개를 유지해야 하고 (a)는 우회 가능성이 남지만, 하나만 쓰면 rename(=breaking, §5.2)이나 신규 token 중 한쪽이 무검사로 통과한다.
|
||||
|
||||
gate 규칙:
|
||||
|
||||
| 검사 | 규칙 | 실패 라벨 | 근거 |
|
||||
|---|---|---|---|
|
||||
| touch-set | registry 파일의 token 집합이 변한 PR은 해당 registry의 producer test와 consumer test 경로를 함께 수정해야 함 (주석·포맷만 바뀐 change는 미발동) | `unsynced-registry-test` | §5.10 step 5 |
|
||||
| token-reference (test 확장) | test 소스가 참조하는 registry token이 registry에 존재해야 함 — registry에서 제거·rename된 token을 test가 계속 참조하면 fail | `stale-test-token` | §5.10 step 5 + step 8 (`FE-SC-005`) |
|
||||
| new-token coverage | registry에 새로 추가된 token은 producer/consumer 양쪽에서 ≥1회 test 참조되어야 함 | `untested-new-token` | §5.10 step 5 + step 8 bidirectional orphan 규칙의 test-side 확장 |
|
||||
|
||||
- 본 gate의 producer/consumer 정의는 registry별로 owner branch가 §5.2~§5.9 schema와 함께 선언한 stable ID를 기준으로 한다 — governance는 그 ID 집합의 *변화*와 test 참조를 대조할 뿐, test 내용을 규정하지 않는다.
|
||||
- 실행 지점: registry change PR의 merge gate. CI stage 배선(어느 workflow job에서 도는지)은 [[raw/branch-notes/feature-frontend-ci-quality-gates-contract]] 소유이고, `FE-GATE-005@1`(unit gate — all registries fixture 포함) 자체의 owner 는 [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] 다(hub §2.1.1).
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- `duplicate-owner`: 두 owner branch가 같은 registry 소유 주장 → single-owner check fail (D2).
|
||||
- `zero-owner`: registry가 owner map에 owner 없음(orphan registry) → fail (D2).
|
||||
- `renamed-token-without-label`: stable ID rename인데 `compatibility_impact` 미기록/`breaking` 미표기 → gate block (D3, §5.2 rename=breaking).
|
||||
- `orphan-token`: 코드가 참조하나 registry 부재, 또는 registry row가 코드에서 미참조 → `FE-SC-005` 위반 (D4).
|
||||
- `missing-impact-label`: registry change에 `compatibility_impact` 누락 → gate block (D3).
|
||||
- `unevidenced-breaking`: `behavior-change`/`breaking`인데 migration/rollback/test evidence 없음 → merge block (D3).
|
||||
- `ad-hoc-token`: literal route path / raw `localStorage` key / 자유 문자열 event 등 registry 우회 → §5.1 "Ad hoc use failure" (정적 강제는 각 owner, governance는 aggregate scan).
|
||||
- `unsynced-registry-test`: registry token 집합이 바뀐 PR이 producer/consumer test를 함께 수정하지 않음 → §5.10 step 5 위반, merge block (D6).
|
||||
- `stale-test-token`: test가 registry에서 제거·rename된 token을 계속 참조 → gate fail. registry만 바뀌고 test는 green으로 남는 silent contract drift의 주 경로 (D6).
|
||||
- `untested-new-token`: registry에 추가된 token이 producer/consumer test 어느 쪽에서도 참조되지 않음 → gate fail (D6).
|
||||
- **다른 계약 의존** (sibling 링크는 `FE-OC-###`로만 참조):
|
||||
- upstream: [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] (`FE-OC-003`) — checkJs/test toolchain 위에서 harness 실행. [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] (`FE-OC-002`) — `contracts/` 레이어 소유 + layer 간 import 규칙(§4.3 dependency matrix는 `domain`/`application`/`presentation`/`adapters`/`bootstrap` **layer** 단위 import 허용/금지를 정의하며, registry 파일별 branch ownership을 정의하지 않는다). 따라서 `non-owner-mutation` 강제는 §4.3에서 도출되지 않고 본 note §1의 path-glob repo ownership 선택(UNSUPPORTED_IMPL_DECISION)이 소유한다.
|
||||
- downstream(본 branch를 consume): [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] (`FE-OC-023`) — `compatibility_impact` 기록을 소비해 version-tuple/migration 판정. [[raw/branch-notes/feature-frontend-ci-quality-gates-contract]] (`FE-OC-022`) — governance gate를 CI blocking gate로 실행.
|
||||
- registry supplier(8 owner가 registry+schema 제공): routing(`FE-OC-005`), api-client(`FE-OC-006`), env(`FE-OC-004`), storage(`FE-OC-013`), error(`FE-OC-008`), server-state(`FE-OC-012`), observability(`FE-OC-014`), release-cache(`FE-OC-016`). 이 중 하나라도 schema를 바꾸면 §2 harness가 동기화돼야 함.
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| 각 registry가 정확히 1 primary owner를 가진다 | branch·code 없음, owner map manifest 미구현 | single-owner check fixture: duplicate/zero-owner manifest fixture가 fail (§20 measurable: single-owner checks) | `needs-confirmation` |
|
||||
| 각 registry row가 owner minimum schema를 만족한다 | schema harness 미구현 | schema validation fixture: required-field 누락 row가 fail (§20 measurable: schema validation) | `needs-confirmation` |
|
||||
| orphan/ad hoc token scan이 0 (`FE-SC-005`) | frontend code 없음 | bidirectional orphan token scan fixture (registry↔code) | `needs-confirmation` |
|
||||
| 모든 registry change가 `compatibility_impact`를 기록한다 | gate 미구현 | change-protocol gate fixture: label 없는 change가 fail | `needs-confirmation` |
|
||||
| snapshot diff가 affected FE-OC + compatibility impact를 표면화한다 | snapshot 미구현 | snapshot diff test: additive vs breaking fixture의 diff 비교 (§20 measurable: 8 registry snapshots) | `needs-confirmation` |
|
||||
| registry change 시 producer/consumer test가 함께 갱신됨을 gate가 검출한다 (§5.10 step 5) | gate 미구현, hub는 원칙만 진술하고 판정 메커니즘 미지정 | 3개 negative fixture: (1) registry token rename + test 미수정 PR → `unsynced-registry-test` fail, (2) registry에서 제거된 token을 참조하는 test → `stale-test-token` fail, (3) test 참조 없는 신규 token → `untested-new-token` fail | `needs-confirmation` |
|
||||
| `non-owner-mutation`을 path-glob repo ownership으로 검사할 수 있다 | CODEOWNERS/glob 검사 미구현, git host 기능 종속 | owner map의 8 registry path glob과 ownership 선언이 1:1 대응하는지 대조 + non-owner 경로 수정 fixture가 block 되는지 확인 | `needs-confirmation` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
- 스캐폴딩 단계: `/coverage` 실행 전 수동 행을 만들지 않는다.
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
- 없음 — 스캐폴딩 단계.
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-OC-004@1` | [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] | build-time, runtime-public, secret config를 MUST 분리하고 boot 전에 runtime config를 검증 | import 참조로 적용 |
|
||||
| `FE-OC-005@1` | [[raw/branch-notes/feature-routing-navigation-guard-contract]] | route ID/path/params/access/loading/error owner는 route registry 하나여야 함 | import 참조로 적용 |
|
||||
| `FE-OC-006@1` | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | 모든 HTTP는 shared client를 MUST 통과하고 timeout·abort·response parsing을 page에서 구현하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-008@1` | [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] | 모든 failure는 stable frontend error kind로 MUST 정규화하고 raw body·stack을 UI에 노출하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-012@1` | [[raw/branch-notes/feature-server-state-caching-contract]] | query key와 invalidation은 registry factory만 MUST 사용 | import 참조로 적용 |
|
||||
| `FE-OC-013@1` | [[raw/branch-notes/feature-frontend-storage-registry-contract]] | storage key는 namespace·version·classification을 MUST 가지며 token/secret 저장을 금지 | import 참조로 적용 |
|
||||
| `FE-OC-014@1` | [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] | telemetry는 best-effort이며 render·API success를 차단하면 안 되고 PII·token을 전송하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-016@1` | [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] | HTML, asset, runtime config, release manifest cache policy를 MUST 구분 | import 참조로 적용 |
|
||||
| `FE-OC-020@1` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | gate 종류별 책임·fixture·artifact를 분리하고 실패를 warning으로 낮추면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-023@1` | [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] | API/config/storage/release schema의 breaking change는 migration 또는 version bump 없이 배포하면 안 됨 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
- 없음 — 자식 자료는 생성 후 controller가 parent Cluster와 함께 등록한다.
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
- 없음 — daily note는 이 작업에서 수정하지 않는다.
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크: 없음
|
||||
- 리뷰 메모: 없음
|
||||
- 머지 결과 / 배포 환경: `planned`
|
||||
- **wiki 추출 대상** (verified만): 없음
|
||||
- **추출하지 않을 항목** (planned / documented-only / abandoned): 현재 전체
|
||||
+396
@@ -0,0 +1,396 @@
|
||||
---
|
||||
title: branch / feature-frontend-env-runtime-config-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-004
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-004
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-RUNTIME-CONFIG-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-CONFIG-FALLBACK-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-001]
|
||||
contract_packet: 1
|
||||
branch: feature-frontend-env-runtime-config-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract.md]
|
||||
tags: [branch, ca-skeleton, frontend, runtime, security, javascript, externalized-config]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
contract_packet_sha256: 781ef2d614370c6a8603dfd4c254c8584737803fa6f88b19159cb46a349e841c
|
||||
imports: [FE-GATE-004@1, FE-OC-002@1, FE-OC-003@1, FE-OC-007@1, FE-OC-008@1, FE-OC-009@1, FE-OC-014@1, FE-OC-016@1, FE-OC-019@1, FE-OC-022@1, FE-OC-023@1, FE-OC-025@1]
|
||||
---
|
||||
|
||||
# branch: feature-frontend-env-runtime-config-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — 단일 브랜치의 TODO·결정·진행 기록. 머지/종료 후 verified 결과는 `/ingest`로 `wiki/projects/`에 추출한다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: build/runtime/secret registry와 boot-invalid matrix가 검증된다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-RUNTIME-CONFIG-001@1` | deploy별 public value는 runtime config, compiler value는 build-time config로 분리한다 | config registry와 pre-mount runtime config validation에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-CONFIG-FALLBACK-001@1` | runtime config fallback 시 environment별 rebuild는 허용하되 artifact의 multi-environment 재사용은 금지한다 | static-only hosting fallback과 artifact 재사용 금지에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
|
||||
<!-- section-id: branch-local-decisions -->
|
||||
### 브랜치 지역 결정
|
||||
|
||||
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
||||
|---|---|---|---|---|
|
||||
| D1 | build-time public·runtime-public·secret config를 분리한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D2 | runtime config fallback은 environment별 rebuild만 허용한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D3 | secret-name key를 build·runtime registry에서 거부한다 | `local` | `raw/official-docs/vite-build-tool-official.md#VITE-C4`, `raw/official-docs/vite-build-tool-official.md#VITE-C5` | `proposed` |
|
||||
| D4 | React mount 전에 runtime config를 fetch하고 검증한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D5 | runtime config validation matrix를 고정한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D6 | boot failure 화면은 safe field만 노출한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D7 | 모든 public config는 FE-REG-ENV를 경유한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D8 | boot config validation 시간 예산의 측정 구간을 고정한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
|
||||
<!-- section-id: declared-overrides -->
|
||||
### 선언한 예외
|
||||
|
||||
| Override ID | Overrides | Reason | Approval | Status |
|
||||
|---|---|---|---|---|
|
||||
|
||||
없음.
|
||||
<!-- GENERATED: branch-contract:end -->
|
||||
|
||||
<!-- section-id: branch-goal -->
|
||||
## 목표
|
||||
|
||||
이 브랜치는 project-wide 계약 `FE-OC-004`("build-time / runtime-public / secret config를 MUST 분리하고 boot 전에 runtime config를 검증")를 *되묻지 않고 코드를 작성할 수 있는* 구현 명세로 내린다. 구체적으로 (1) 환경 config registry `FE-REG-ENV`(`src/contracts/env.js`)를 single owner로 소유하고, (2) React mount 이전에 실행되는 runtime config fetch·검증 게이트(boot sequence 2~4단계, hub §4.5/§6.3)를 정의하며, (3) 세 종류 config(build-time public / runtime public / secret)의 분리 규칙과 secret 유출 차단 규칙(hub §6.1)을 확정한다. 근거는 hub decision `FE-D012`(deploy별 public value = pre-render runtime config, compiler value = build-time config)·`FE-D013`(runtime config fallback 규칙)과 Vite 공식 문서의 `import.meta.env` build-time 정적 치환·`VITE_` prefix 노출 경계·secret 금지 경고(`VITE-C3`/`VITE-C4`/`VITE-C5`)다. 이 계약은 `FE-OC-016`(release/cache — runtime config cache policy)과 `FE-OC-023`(compatibility — config/API schema version)에 기여한다. **현재 frontend repository는 존재하지 않으므로 본 노트의 모든 구현 항목 등급은 `planned`다.**
|
||||
|
||||
- 이슈: (없음 — repository 미생성)
|
||||
- PR: (없음 — repository 미생성)
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- `FE-REG-ENV` 환경 config registry(`src/contracts/env.js`)의 schema·초기 row·single-owner 규칙 (`FE-OC-004`, hub §5.4)
|
||||
- build-time public / runtime public / secret 3분류 규칙과 `VITE_` prefix 사용 경계 (`FE-D012`, hub §6.1)
|
||||
- secret-name(`SECRET`/`PASSWORD`/`PRIVATE_KEY`/`TOKEN`) key를 build·runtime registry 양쪽에서 거부하는 정적 가드 (hub §6.1, `VITE-C4`/`VITE-C5`)
|
||||
- React mount 이전 runtime config fetch(`no-store`) + 검증 게이트와 boot 실패/버전 불일치 분기 (`FE-D012`, hub §4.5/§6.3)
|
||||
- runtime config 검증 규칙 카탈로그(required key·URL protocol allowlist·int range·boolean parse·schema/contract version compat·unknown-key strict) (`FE-OC-004`, hub §6.4)
|
||||
- boot 실패 시 화면 노출 safe-field allowlist + redaction (hub §6.4)
|
||||
- environment별 rebuild fallback 규칙: 한 artifact를 여러 env에 재사용하지 않음 (`FE-D013`)
|
||||
- **boot config 검증 시간 예산 `FE-NFR-006`(≤ 500ms, network delay 제외)의 측정 경계와 valid-config timing fixture** — `FE-GATE-004` pass condition의 timing 절반 (hub §14.2, §15.1)
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외 — 다른 owner 브랜치/계약 소유. 각 브랜치는 자신이 소유한 `FE-OC-*` 계약으로 표기(하위 `FE-D*`는 hub decision register 참조).
|
||||
|
||||
- **normalized error kind 어휘**(`BOOT_CONFIG_FAILURE`, `DEPLOY_MISMATCH`)와 raw body/stack UI 유출 catalog → [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] (`FE-OC-008`)
|
||||
- **runtime schema(Zod) 구성·parse 메커니즘** 자체 → [[raw/branch-notes/feature-runtime-schema-validation-contract]] (`FE-OC-007`)
|
||||
- **release manifest 정합성 tuple·cache header·rollback·`DEPLOY_MISMATCH` recovery UI** → [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] (`FE-OC-016`, `FE-OC-017`)
|
||||
- **config/API schema version breaking-change migration 정책** → [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] (`FE-OC-023`)
|
||||
- **8-registry governance(single-owner diff·compatibility 추적)** → [[raw/branch-notes/feature-frontend-contract-registry-governance]] (`FE-OC-022`)
|
||||
- **telemetry endpoint redaction/전송** → [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] (`FE-OC-014`) — 본 registry는 `TELEMETRY_ENABLED`/`TELEMETRY_ENDPOINT` key와 분류만 선언, 전송·redaction 메커니즘은 관측 브랜치 소유
|
||||
- **token/session lifecycle** → [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] (`FE-OC-010`) — 본 registry는 `AUTH_MODE` key만 선언
|
||||
- **Vite/toolchain·`import.meta.env` 노출 메커니즘 자체** → 의존 브랜치 [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] (`FE-OC-003`)
|
||||
- **composition root 조립 순서 enforcement** → [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] (`FE-OC-002`)
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/official-docs/vite-build-tool-official]] | `VITE-C3`(`import.meta.env` build-time 정적 치환) → build-time config는 재빌드로만 바뀐다는 D1/D2 전제; `VITE-C4`(오직 `VITE_` prefix만 client 노출) → D3 노출 경계; `VITE-C5`(`VITE_*`에 secret 금지, 프로덕션 secret은 backend/serverless) → D3 secret 차단 규칙; `VITE-C2`(정적 자산 output) → static-only hosting fallback(D2) 전제 |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | 이 branch가 owner인 `FE-D012`/`FE-D013` decision, `FE-OC-004` 계약, `FE-REG-ENV`(§5.4), boot order(§4.5)·boot sequence(§6.3)·runtime config 검증 규칙(§6.4)·boot 실패 safe-output(§6.4)·`FE-RB-001` runbook(§16.1)의 project-decision 근거. 추가로 §14.2 `FE-NFR-006`(boot config validation ≤ 500ms, deterministic mocked fetch)·§15.1 `FE-GATE-004`(valid boot config validation timing 을 pass condition 에 포함)가 D8 시간 예산의 근거 |
|
||||
|
||||
## TODO
|
||||
|
||||
각 항목 옆에 증거 등급 표기.
|
||||
|
||||
- [ ] `FE-REG-ENV` registry schema + 초기 14 key row 구현 (`src/contracts/env.js`) — 등급: `planned`
|
||||
- [ ] build/runtime/secret 3분류 + secret-name 거부 정적 가드 구현 — 등급: `planned`
|
||||
- [ ] `src/bootstrap/load-runtime-config.js` — mount 이전 `no-store` fetch + boot 분기 구현 — 등급: `planned`
|
||||
- [ ] runtime config 검증 규칙(§6.4 8항) 구현 (schema 메커니즘은 `FE-OC-007` 브랜치 consume) — 등급: `planned`
|
||||
- [ ] boot 실패 safe-field allowlist + redaction 구현 — 등급: `planned`
|
||||
- [ ] **boot invalid-config matrix** 테스트(§20 Measurable completion) 작성 — 등급: `planned`
|
||||
- [ ] config schema test(`FE-OC-004` minimum evidence) 작성 — 등급: `planned`
|
||||
- [ ] **valid-config timing fixture** 작성 — `FE-NFR-006`(≤ 500ms, mocked network delay 제외) 측정 + `FE-GATE-004` timing report 산출 — 등급: `planned`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
없음 — scaffolding 단계. repository 미생성이므로 모든 항목 `planned`.
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> 각 결정의 근거는 아래 Decision Evidence Map과 1:1 대응. 대안·선택 조건 포함.
|
||||
|
||||
- 2026-07-18: **build-time public / runtime-public / secret 3분류 분리**(`FE-D012`) / 이유: deploy마다 달라지는 public value(API endpoint 등)를 재빌드 없이 바꾸려면 build-time 정적 치환(`import.meta.env`)이 아닌 pre-render runtime config가 필요 / 검토한 대안: 모든 값을 build-time으로 고정(env별 재빌드) / 근거: `VITE-C3`, hub §6.1·`FE-D012`
|
||||
- 2026-07-18: **runtime config fallback = env별 rebuild 허용하되 artifact 재사용 금지**(`FE-D013`) / 이유: hosting이 atomic config publish를 못 할 때 deploy ambiguity를 제한 / 검토한 대안: 단일 artifact를 여러 env에 재사용 + build-time fallback / 근거: hub `FE-D013`
|
||||
- 2026-07-18: **secret-name key 양쪽 registry 거부 + `VITE_`는 build metadata·non-secret 상수만** / 이유: `VITE_*`는 번들에 정적 치환되어 client에 노출되므로 secret 금지 / 검토한 대안: 관례 문서화만(정적 강제 없음) / 근거: `VITE-C4`, `VITE-C5`, hub §6.1
|
||||
- 2026-07-18: **React mount 이전 runtime config fetch+검증 게이트(boot 2~4단계)** / 이유: 잘못된 config로 product route를 mount하지 않기 위해 / 검토한 대안: mount 이후 lazy config load / 근거: hub §4.5 boot order, §6.3 sequence
|
||||
- 2026-07-18: **runtime config 검증 8항 커버리지 + unknown-key strict default** / 이유: config는 신뢰 경계 밖 입력이므로 boot 전 전량 검증 / 검토한 대안: 필수 key 존재만 확인 / 근거: hub §6.4 (schema 메커니즘은 `FE-OC-007` 위임)
|
||||
- 2026-07-18: **boot 실패 화면 safe-field allowlist + endpoint/stack redaction** / 이유: 실패 화면으로 endpoint·raw config·stack 유출 금지 / 검토한 대안: raw error 그대로 표시 / 근거: hub §6.4 (error kind 어휘는 `FE-OC-008` 위임)
|
||||
- 2026-07-18: **모든 public config는 `FE-REG-ENV` 경유(ad hoc `import.meta.env` 금지)** / 이유: rename·compatibility 영향 추적 single owner / 검토한 대안: 파일마다 `import.meta.env` 직접 접근 / 근거: hub §5.1·§5.4, `FE-D018`
|
||||
- 2026-07-20: **`MAX_RETRY_ATTEMPTS` 허용 범위를 retry cap 소유 결정에 정렬(0–2)** / 이유: config가 owner 결정보다 넓은 값을 통과시키면 하류 client가 조용히 clamp 하게 되어 "설정한 값 ≠ 동작하는 값" 이 되므로, 경계 검증을 owner cap 과 동일하게 둔다 / 검토한 대안: config는 0–5를 통과시키고 API client가 clamp(설정-동작 괴리 허용) / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D015`(initial call 이후 최대 2회) — cap 소유는 `FE-OC-009`, 본 branch는 그 cap 을 config 경계에서 재선언만 하고 값 자체를 정하지 않음
|
||||
- 2026-07-20: **boot config 검증 시간 예산 `FE-NFR-006` 은 "검증 구간만" 측정하며 mocked network delay 를 제외한다** / 이유: `FE-GATE-004` pass condition 이 timing 을 포함하는데(hub §15.1) 측정 구간을 고정하지 않으면 fetch 대기 시간이 예산을 잠식해 gate 가 무의미해짐 / 검토한 대안: fetch 시작~mount 직전 end-to-end 측정(hosting/network 변동에 좌우) / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-NFR-006`(§14.2, deterministic mocked fetch, ≤ 500ms excluding network delay), §15.1 `FE-GATE-004`
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
> `Supporting Claims`: 공식 문서는 `raw/official-docs/<slug>.md#<CLAIM>` (백틱), project decision은 hub wikilink + FE-D/§ 참조. 위임 대상 sibling 브랜치는 소유 `FE-OC-*`로 표기(하위 `FE-D*`는 hub register).
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | `FE-D012` — deploy별 public value는 pre-render runtime config(`/config.json`), compiler value·asset identity는 build-time config로 분리 | hosting이 HTML보다 먼저 runtime config를 atomic publish 가능 → runtime config 경로; 정적 파일만 제공 → D2 env별 rebuild fallback; SSR/edge 도입 → 본 계약 그대로 적용 않고 별도 project fork(hub §6.2) | `raw/official-docs/vite-build-tool-official.md#VITE-C3`, `raw/official-docs/vite-build-tool-official.md#VITE-C2`; [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D012` §6.1 | `conditional-default + official-doc` | hosting이 runtime config atomic publish를 미지원하면 재검토(hub revisit trigger) |
|
||||
| D2 | `FE-D013` — runtime config fallback은 env별 rebuild를 허용하되 한 artifact를 여러 env에 재사용하지 않음 | atomic config publish 불가한 static-only hosting일 때만 env별 rebuild; runtime config endpoint 도입되면 단일 artifact + runtime fetch로 복귀. 어떤 경우에도 동일 artifact를 여러 env로 재배포 금지 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D013`; `raw/official-docs/vite-build-tool-official.md#VITE-C3` | `conditional-default + project-decision` | runtime config endpoint 도입 시 재검토; artifact 재사용 시 deploy ambiguity 재발 |
|
||||
| D3 | secret-name(`SECRET`/`PASSWORD`/`PRIVATE_KEY`/`TOKEN`) key를 build·runtime registry 모두 거부; `VITE_` prefix는 build metadata·non-secret compile-time 상수만 | 항상 적용되는 invariant; auth owner가 browser storage를 꼭 써야 하는 경우에만 별도 threat model + owner evidence로 예외(skeleton default 아님, hub §6.1) | `raw/official-docs/vite-build-tool-official.md#VITE-C4`, `raw/official-docs/vite-build-tool-official.md#VITE-C5`; [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §6.1 | `official-doc` | 로그·디버그 등 다른 경로의 우발적 유출은 `VITE-C4`가 커버 안 함 → `FE-OC-019` browser-security와 교차 필요 |
|
||||
| D4 | runtime config + release manifest를 React mount 이전에 `no-store` fetch → 검증 → (valid) 조립·mount / (invalid) boot error shell / (mismatch) recovery UI. boot 2~4단계 실패 시 product route mount 안 함 | config invalid → `BOOT_CONFIG_FAILURE`(product route mount 중단); version mismatch → `DEPLOY_MISMATCH`(controlled recovery, reload loop 금지); valid → mount. telemetry adapter 생성 실패는 non-blocking(console-safe fallback) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §4.5 boot order, §6.3 sequence, `FE-OC-004` | `project-decision` | bounded refetch(최대 1회, §16.1)와 recovery UI 경계가 `FE-OC-016`/`FE-OC-025` 소유와 겹침 |
|
||||
| D5 | runtime config 검증은 required-key·URL protocol allowlist(prod https)·int range(timeout/retry)·boolean strict parse·config schema version·API contract version·release/build ID coherence·unknown-key strict를 모두 커버 | unknown key는 strict reject default; schema가 명시적으로 passthrough할 때만 additive key 허용. protocol allowlist는 prod https 강제, local 예외는 문서화된 경우만 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §6.4, `FE-OC-004`; schema 메커니즘은 [[raw/branch-notes/feature-runtime-schema-validation-contract]] (`FE-OC-007`) 위임 | `project-decision` | `REQUEST_TIMEOUT_MS` 경계 값은 hub 미규정(아래 impl §4 `UNSUPPORTED_IMPL_DECISION`); `MAX_RETRY_ATTEMPTS` 범위는 retry cap owner(`FE-OC-009`)에 정렬해 해소(0–2); version compat 정책은 `FE-OC-023` 위임 |
|
||||
| D6 | boot 실패 화면은 safe-field(`error.kind`,`error.code`,`buildId`,`configSchemaVersion`,`releaseId`,`supportReference`)만 노출; endpoint·query·header·raw config·stack은 화면 금지 | 항상 적용되는 redaction invariant — 어떤 실패 종류에서도 forbidden field는 user-facing screen에 표시 안 함 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §6.4, `FE-OC-004`; error kind 어휘는 [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] (`FE-OC-008`) 위임 | `project-decision` | `supportReference` 생성 방식 미규정(impl §5 `UNSUPPORTED_IMPL_DECISION`); telemetry로의 상관 전송은 `FE-OC-014` 소유 |
|
||||
| D7 | 모든 build/runtime public config key는 `FE-REG-ENV`(`src/contracts/env.js`) 등록 후 사용; registry 밖 `import.meta.env`·config key 직접 사용은 violation. `public-sensitive`=browser 가시이나 로그·telemetry 원문 금지 | 항상 적용(hub `FE-D018` 8-registry single-owner invariant); code generation SSOT 채택 시 registry 형태 재검토 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.1 §5.4 `FE-D018` | `project-decision` | registry diff·single-owner 강제와 compatibility 추적은 `FE-OC-022`/`FE-OC-023` 위임 |
|
||||
| D8 | boot config 검증 시간 예산 `FE-NFR-006`(≤ 500ms) 은 *검증 구간만* 측정한다 — config 본문이 메모리에 있는 시점부터 normalized config 반환 또는 `BOOT_CONFIG_FAILURE` 확정까지. fetch·mocked network delay·mount 이후는 제외. `FE-GATE-004` 는 이 branch 가 config invalid matrix + valid-config timing fixture 를, schema 브랜치가 content-type/JSON/envelope/payload invalid matrix 를 각각 제공해 함께 PASS 시킨다 | deterministic mocked fetch 환경에서 항상 측정(hub §14.2 context); 실제 network 를 타는 환경에서는 이 예산을 주장하지 않음(lab 값을 production 수치로 표현 금지) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §14.2 `FE-NFR-006`, §15.1 `FE-GATE-004`, `FE-OC-004`; invalid fixture 절반은 [[raw/branch-notes/feature-runtime-schema-validation-contract]] (`FE-OC-007`) 소유 — 해당 노트도 timing fixture 소유를 본 branch 로 귀속 | `project-decision` | 측정 시작점·통계(단일 실행 vs 중앙값)는 hub 미규정(impl §6 `UNSUPPORTED_IMPL_DECISION`); 검증 대상 fixture 규모가 커지면 500ms 예산 재검토 필요 |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> `planned` blueprint — frontend repository 미생성. 경로는 hub §4.6 Planned directory blueprint + §5.1 registry owner map에서 도출되어 grounded지만, 코드가 없으므로 전체가 `planned`다.
|
||||
|
||||
### 1. 환경 config registry `FE-REG-ENV`
|
||||
|
||||
> **Trace**: D7 (hub §5.1·§5.4 `FE-REG-ENV`, `FE-D018`) + D3. Planned path `src/contracts/env.js` (§5.1 owner map).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: registry의 JS 표현(row 배열 `export const ENV_REGISTRY = [...]` vs key→meta object map)은 hub가 schema 컬럼만 규정하고 JS 구조는 미규정 → row 배열 선택. trade-off: 순서 보존 + snapshot diff(`FE-OC-022`)가 단순.
|
||||
|
||||
초기 14 key(hub §5.4 그대로 — 신규 발명 아님):
|
||||
|
||||
| Key | Phase | Classification | Required | Default | Failure |
|
||||
|---|---|---|---|---|---|
|
||||
| `VITE_BUILD_ID` | build | public metadata | yes | none | build fail |
|
||||
| `VITE_COMMIT_SHA` | build | public metadata | yes in CI | local sentinel allowed | release evidence fail |
|
||||
| `VITE_ROUTER_BASE_PATH` | build | non-secret compile-time constant | yes | `/` | route mount fail |
|
||||
| `VITE_RUNTIME_CONFIG_URL` | build | non-secret compile-time constant | yes | `/config.json` | boot fail |
|
||||
| `APP_ENV` | runtime | public | yes | none | boot fail |
|
||||
| `API_BASE_URL` | runtime | public-sensitive | yes | none | boot fail |
|
||||
| `REQUEST_TIMEOUT_MS` | runtime | public | no | `10000` | invalid value boot fail |
|
||||
| `MAX_RETRY_ATTEMPTS` | runtime | public | no | `2` after initial (허용 범위 0–2, cap owner `FE-OC-009`) | invalid value boot fail |
|
||||
| `TELEMETRY_ENABLED` | runtime | public | yes | `false` | invalid value boot fail |
|
||||
| `TELEMETRY_ENDPOINT` | runtime | public-sensitive | conditional | none | telemetry degrade |
|
||||
| `AUTH_MODE` | runtime | public | yes | `external` | unsupported mode boot fail |
|
||||
| `CONFIG_SCHEMA_VERSION` | runtime | public | yes | none | compatibility fail |
|
||||
| `API_CONTRACT_VERSION` | runtime | public | yes | none | compatibility fail |
|
||||
| `RELEASE_MANIFEST_URL` | runtime | public | yes | `/release-manifest.json` | mismatch detection degrade/fail per policy |
|
||||
|
||||
- `public-sensitive`(예: `API_BASE_URL`, `TELEMETRY_ENDPOINT`) = browser 가시이나 로그·telemetry에 원문 금지 (hub §5.4). secret 분류 아님.
|
||||
- ad hoc 사용 위반(hub §5.1): registry 없는 `import.meta.env` 또는 config key 사용.
|
||||
|
||||
### 2. runtime / secret 3분류 + secret-name 거부 가드
|
||||
|
||||
> **Trace**: D1 (`FE-D012`, hub §6.1, `VITE-C3`) + D3 (`VITE-C4`, `VITE-C5`, hub §6.1).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: secret-name 거부 매칭 알고리즘(case-insensitive substring `/(SECRET|PASSWORD|PRIVATE_KEY|TOKEN)/i` vs 정확 word 매칭) 미규정 — hub는 4개 토큰만 나열(§6.1) → case-insensitive substring(fail-safe) 선택. trade-off: `TOKENIZER` 같은 정당한 이름 false-positive 위험 → 문서화된 명시적 예외 목록으로 완화.
|
||||
|
||||
| Class | 예시 | Browser 가시 | 변경 메커니즘 | Cache | 규칙 |
|
||||
|---|---|---|---|---|---|
|
||||
| build-time public | `VITE_BUILD_ID`, `VITE_COMMIT_SHA`, `VITE_ROUTER_BASE_PATH` | yes | rebuild(정적 치환) | bundled | compiler behavior·asset identity만 |
|
||||
| runtime public | `API_BASE_URL`, public feature flag, `TELEMETRY_ENDPOINT` | yes | runtime config publish | `no-store` | React mount 이전 검증 |
|
||||
| secret | client secret, private key, DB credential, refresh token material | 번들 금지 | server/auth owner | N/A | frontend env·bundle·HTML 어디에도 금지 |
|
||||
|
||||
- `VITE_` prefix는 build metadata + non-secret compile-time 상수(base path, `/config.json` 위치)에만 (hub §6.1, `VITE-C4`).
|
||||
- 이름에 `SECRET`/`PASSWORD`/`PRIVATE_KEY`/`TOKEN` 포함 key는 build·runtime registry 양쪽에서 거부 (hub §6.1). 프로덕션 secret은 backend/serverless 소관 (`VITE-C5`).
|
||||
|
||||
### 3. mount 이전 runtime config 로더 + boot 분기
|
||||
|
||||
> **Trace**: D4 (hub §4.5 boot order, §6.3 sequence). Planned paths `src/bootstrap/load-runtime-config.js`, `src/bootstrap/composition-root.js`, `src/bootstrap/main.jsx` (§4.6). 의존: build/`import.meta.env` 노출은 [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] (`FE-OC-003`); composition root 조립은 [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] (`FE-OC-002`) 소유.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: (a) boot error shell 컴포넌트 명/경로(예: `presentation/boundaries/BootErrorShell.jsx`) — hub는 "boot error shell" 개념만, 명명/경로 미규정 → presentation/boundaries 하위에 두어 layering(`FE-OC-002`) 위반 회피. (b) pre-mount config fetch 클라이언트(raw `fetch` vs shared client) — boot 2단계 시점엔 shared client(`FE-OC-006`)가 아직 조립 전 → raw `fetch` 선택. trade-off: shared client의 timeout/retry 정책은 config 로드에 적용 안 됨(부트 전용 최소 fetch).
|
||||
|
||||
**본 branch 소유 구간은 단계 번호가 아니라 *의미*로 정의한다: "runtime config 취득 + 검증 완료까지, React mount 이전".** 그 앞(build identity 읽기)과 뒤(adapter 조립·mount)는 타 owner 구간이다. hub 의 두 절이 나열 순서를 서로 다르게 쓰므로(§4.5 는 config 검증 → release manifest 정합성, §6.3 sequence 는 두 fetch → 검증) 번호 기반 참조는 깨지기 쉽다. 아래 목록은 §6.3 실행 순서를 따르고, 각 행에 hub §4.5 번호를 명시 매핑한다.
|
||||
|
||||
| 실행 순서(hub §6.3 기준) | hub §4.5 번호 | Owner |
|
||||
|---|---|---|
|
||||
| build identity 읽기 *(build-time config, §1)* | 1 | build/toolchain (`FE-OC-003`) — 본 branch 는 key 분류만 |
|
||||
| runtime config fetch — `GET {VITE_RUNTIME_CONFIG_URL}` `no-store` | 2 | **본 branch** |
|
||||
| release manifest fetch — `GET {RELEASE_MANIFEST_URL}` `no-store` | 4의 입력 취득 | **본 branch** (정합성 판정 자체는 `FE-OC-016`) |
|
||||
| config envelope·schema·compatibility 검증 *(§4)* | 3 | **본 branch** (schema 메커니즘은 `FE-OC-007` consume) |
|
||||
| registry snapshot → auth adapter → HTTP/storage/telemetry/query-cache adapter → application facade → router → React root mount | 5–10 | composition root (`FE-OC-002`) |
|
||||
|
||||
분기(hub §6.3):
|
||||
|
||||
- **valid & compatible** → normalized public config로 dependency 조립 + mount.
|
||||
- **invalid config** → `BOOT_CONFIG_FAILURE` → boot error shell, product route mount 안 함. automatic refetch 최대 1회(hub §16.1).
|
||||
- **version mismatch** → `DEPLOY_MISMATCH` → controlled recovery UI, reload loop 금지. *(recovery UI 상세는 [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] 소유 — 본 branch는 트리거/분기까지만.)*
|
||||
- telemetry adapter 생성 실패 → console-safe fallback로 계속(boot 실패 아님, hub §4.5).
|
||||
|
||||
### 4. runtime config 검증 규칙
|
||||
|
||||
> **Trace**: D5 (hub §6.4). schema 구성·parse 메커니즘은 [[raw/branch-notes/feature-runtime-schema-validation-contract]] (`FE-OC-007`) 위임 — 본 §는 *무엇을* 검증하고 *어떤 boot 결과*로 이어지는지만.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: `REQUEST_TIMEOUT_MS` 정수 범위(제안 1000–60000)는 hub가 default(10000)만 주고 경계 미규정 → 0/음수 timeout 방지용으로 제안. trade-off: 상한 60000 은 임의값이며 api-client owner(`FE-OC-009`)가 total timeout 정책을 lock 할 때 재확인 필요.
|
||||
> - **해소됨(구 `UNSUPPORTED_IMPL_DECISION`)**: `MAX_RETRY_ATTEMPTS` 허용 범위는 **0–2** — [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D015`(initial call 이후 최대 2회)의 cap 을 config 경계에서 그대로 재선언한다. 이전 초안의 0–5 는 owner cap 보다 넓어 "config 는 통과, client 는 clamp" 하는 설정-동작 괴리를 만들었으므로 폐기. cap 값의 owner 는 `FE-OC-009` 이고 본 branch 는 값을 정하지 않으므로, cap 이 개정되면 이 범위도 따라 개정한다(본 노트 단독 변경 금지).
|
||||
|
||||
검증 MUST 커버(hub §6.4):
|
||||
|
||||
- required key 존재 (§1 Required=yes 전부)
|
||||
- URL protocol allowlist — prod policy는 `https`, local 예외는 문서화된 경우만
|
||||
- timeout/retry 정수 범위 — `REQUEST_TIMEOUT_MS` 1000–60000(제안), `MAX_RETRY_ATTEMPTS` 0–2(cap owner `FE-OC-009` 에 정렬)
|
||||
- boolean parse — truthy-string 모호성 없이(`"false"`가 true 되지 않게)
|
||||
- config schema version 호환 (`CONFIG_SCHEMA_VERSION`)
|
||||
- API contract version 호환 (`API_CONTRACT_VERSION`)
|
||||
- provider가 둘 다 노출하면 release/build ID coherence
|
||||
- unknown-key 정책: default strict, schema가 명시적 passthrough일 때만 additive 허용
|
||||
|
||||
*compat 실패 시 migration/version bump 정책은 [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] (`FE-OC-023`) 위임.*
|
||||
|
||||
### 5. boot 실패 safe-output (redaction)
|
||||
|
||||
> **Trace**: D6 (hub §6.4). normalized error kind 어휘와 raw body/stack UI 유출 catalog는 [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] (`FE-OC-008`) 위임.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: `supportReference` 생성 방식(무작위 correlation id vs `releaseId`+timestamp) 미규정 — hub는 field만 나열 → 무작위 opaque id 선택. trade-off: deploy timing 유출 방지하나, 트리아지용으로 telemetry event와 매핑되어야 함(`FE-OC-014` 소유).
|
||||
|
||||
boot error shell 노출 허용 field(allowlist, hub §6.4):
|
||||
|
||||
```text
|
||||
error.kind
|
||||
error.code
|
||||
buildId
|
||||
configSchemaVersion
|
||||
releaseId (if present)
|
||||
supportReference
|
||||
```
|
||||
|
||||
화면 금지: endpoint, query, header, raw config object, stack (hub §6.4).
|
||||
|
||||
### 6. boot config 검증 시간 예산 (`FE-NFR-006`) + `FE-GATE-004` 소유 분할
|
||||
|
||||
> **Trace**: D8 (hub §14.2 `FE-NFR-006` — deterministic mocked fetch, ≤ 500ms excluding network delay; hub §15.1 `FE-GATE-004` pass condition). invalid fixture 의 나머지 절반(content-type/JSON/envelope/payload)은 [[raw/branch-notes/feature-runtime-schema-validation-contract]] (`FE-OC-007`) 소유 — 해당 노트도 timing fixture 소유를 본 branch 로 귀속시키므로 양방향 일치.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 측정 시작·종료 지점을 hub 가 규정하지 않음("excluding network delay" 만 명시) → **시작 = config 원문(text/object)이 validator 에 전달되는 시점, 종료 = normalized config 반환 또는 `BOOT_CONFIG_FAILURE` 확정 시점**으로 고정. trade-off: JSON parse 비용이 예산 안에 포함되어 보수적으로 측정되지만, transport 구현(fetch·캐시·mock)에 무관한 재현 가능 구간이 된다.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 회차·통계(단일 실행 vs 다회 중앙값)를 hub 가 미규정 → **동일 fixture 5회 실행의 중앙값을 판정값으로 쓰고 최댓값도 report 에 함께 기록**. trade-off: CI 노이즈로 인한 flake 를 줄이지만 tail latency 를 판정에서 제외하므로, 최댓값이 예산의 2배를 넘으면 report 를 근거로 재검토한다.
|
||||
|
||||
**측정 대상(무엇을 재는가).** `FE-NFR-006` 은 *검증 구간만* 잰다. 포함: required-key 검사, URL protocol allowlist, int range, boolean strict parse, config/API version 호환 비교, release/build ID coherence, unknown-key strict 판정(§4 8항 전부). 제외: `GET {VITE_RUNTIME_CONFIG_URL}`·`GET {RELEASE_MANIFEST_URL}` 의 network 대기, mock 이 주입한 인위적 지연, 검증 이후의 adapter 조립·mount(그 구간은 `FE-OC-002` 소유이며 본 예산의 대상이 아님).
|
||||
|
||||
**fixture 가 network delay 를 배제하는 방법.** transport 를 deterministic mock 으로 대체하고(hub §14.2 context), config 본문을 *이미 메모리에 있는 값*으로 validator 에 직접 전달한다. 즉 fixture 는 fetch 를 거치지 않거나, 지연을 주입한 mock 을 쓰더라도 타이머를 fetch resolve *이후*에 시작한다. 따라서 mock 지연을 늘려도 측정값이 변하지 않아야 하며, 이 불변식 자체를 fixture 의 self-check 로 둔다(지연 0ms 와 지연 200ms 두 실행의 측정값 차이가 노이즈 범위 내).
|
||||
|
||||
**valid-config fixture 형태.** §1 registry 의 runtime key 10개를 모두 채운 valid config 1건(= 실제 boot 가 받는 최대 폭). 판정: 중앙값 ≤ 500ms.
|
||||
|
||||
**`FE-GATE-004@1` 소유 분할** (hub §15.1 의 Covered FE-OC 가 다수라 fixture 소유를 명시해야 중복·누락이 없다 — 어느 계약이 묶여 있는지는 hub §15.1 소유):
|
||||
|
||||
| `FE-GATE-004` 구성요소 | 소유 |
|
||||
|---|---|
|
||||
| config invalid matrix (required key 부재·protocol 위반·range 위반·boolean 모호·unknown key·version 비호환) | **본 branch** (`FE-OC-004`) |
|
||||
| valid-config timing fixture + timing report (`FE-NFR-006`) | **본 branch** (`FE-OC-004`) |
|
||||
| content-type / JSON / envelope / payload invalid matrix | `FE-OC-007` |
|
||||
| 각 invalid 입력의 기대 error kind 어휘 | `FE-OC-008` |
|
||||
| version 비호환 시 migration 판정 | `FE-OC-023` |
|
||||
|
||||
gate 는 두 소유자의 fixture 가 모두 있어야 PASS 하므로, 어느 한쪽만 준비된 상태에서 `FE-GATE-004` 를 PASS 로 올리지 않는다.
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- config fetch non-2xx / JSON parse 실패 / schema 비호환 → `BOOT_CONFIG_FAILURE`, product route mount 중단, auto refetch 최대 1회 (hub §6.3·§16.1).
|
||||
- config/API/release version mismatch → `DEPLOY_MISMATCH`, controlled recovery UI, reload loop 금지 (hub §6.3).
|
||||
- invalid value(timeout/retry 범위 밖, boolean truthy-string, required key 부재) → boot fail (hub §5.4).
|
||||
- URL protocol 위반(prod에서 non-https) → boot fail (hub §6.4).
|
||||
- `TELEMETRY_ENABLED=true`인데 `TELEMETRY_ENDPOINT` 부재 → telemetry degrade(boot fail 아님, hub §5.4).
|
||||
- telemetry adapter 생성 실패 → console-safe fallback, boot 계속 (hub §4.5).
|
||||
- secret-name key가 env에 존재 → registry 거부(build/runtime), boot·build fail (hub §6.1).
|
||||
- **다른 계약 의존**:
|
||||
- [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] (`FE-OC-003`) — `import.meta.env`·`VITE_` prefix 노출과 build identity 주입. 이 계약(checkJs·Vite build)이 바뀌면 build-time config 접근 방식 영향.
|
||||
- [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] (`FE-OC-002`) — composition root(`bootstrap`) 소유. boot 2~4단계는 이 composition root 안의 단계로 slot in.
|
||||
- [[raw/branch-notes/feature-runtime-schema-validation-contract]] (`FE-OC-007`) — config 검증에 쓰는 Zod schema 메커니즘 consume. **`FE-GATE-004` 협업**: 본 branch 가 config invalid matrix + valid-config timing fixture(`FE-NFR-006`)를, 그쪽이 content-type/JSON/envelope/payload invalid matrix 를 제공(impl §6 분할표).
|
||||
- [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] (`FE-OC-019`) — D3 의 secret 차단은 *key 이름 기반 정적 거부*까지만 담당하고, 번들 scan·로그/telemetry 유출 등 *실제 노출 경로* 차단은 그쪽 소유. 두 계약이 함께 있어야 "secret 이 브라우저에 안 간다"가 성립한다.
|
||||
- [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] (`FE-OC-008`) — `BOOT_CONFIG_FAILURE`/`DEPLOY_MISMATCH` normalized kind consume.
|
||||
- [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] (`FE-OC-016`/`FE-OC-017`) — release manifest 정합성·`DEPLOY_MISMATCH` recovery·cache header. 본 branch는 검증된 config를 provide, recovery는 그쪽 소유.
|
||||
- [[raw/branch-notes/feature-api-client-response-envelope-contract]] (`FE-OC-006`) — 검증된 `API_BASE_URL`/`REQUEST_TIMEOUT_MS`/`MAX_RETRY_ATTEMPTS`를 consume(하류 소비자).
|
||||
- [[raw/branch-notes/feature-frontend-operational-runbook-contract]] (`FE-OC-025`) — boot config failure containment/escalation runbook(`FE-RB-001`, hub §16.1)의 technical escalation.
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| build/runtime/secret 분류가 실제로 강제됨(secret-name key가 양쪽 registry에서 거부) | 코드·정적 가드 미존재, 규칙 문서만 있음 | config schema test + secret-name 거부 negative fixture (`FE-OC-004` minimum evidence) | `needs-confirmation` |
|
||||
| boot invalid-config matrix의 각 invalid 입력이 기대 boot 결과(`BOOT_CONFIG_FAILURE`/`DEPLOY_MISMATCH`/boot fail)로 매핑 | 다양한 실패 조합의 실제 boot 분기 미검증 | **boot invalid-config matrix** 테스트(§20 Measurable completion) | `needs-confirmation` |
|
||||
| runtime config가 `no-store`로 fetch되고 React mount 이전에 검증됨(실패 시 product route mount 안 됨) | 조립 순서·no-store가 코드로 보장되는지 미검증 | boot ordering 통합 테스트(mount 이전 fetch·실패 시 route 미mount 확인) | `needs-confirmation` |
|
||||
| boot 실패 화면이 safe-field만 노출(endpoint·raw config·stack 미유출) | redaction 강제 여부 미검증 | boot error shell redaction negative fixture | `needs-confirmation` |
|
||||
| 한 artifact를 여러 env에 재사용하지 않음(`FE-D013`) | 배포 프로세스 속성 — unit test로 완전 증명 불가 | 배포 파이프라인 assertion + env별 artifact hash 대조(문서화된 deploy check) | `needs-confirmation` |
|
||||
| non-`VITE_` build 변수가 client 번들로 유출되지 않음 | 번들 정적 치환 경계는 실제 빌드로만 확인 | build 후 bundle scan (`FE-OC-019` browser-security와 교차) | `needs-confirmation` |
|
||||
| valid config 검증이 `FE-NFR-006` 예산(≤ 500ms, mocked network delay 제외) 안에 들어옴 | 코드·검증 로직 미존재. 8항 검증 + schema 라이브러리(`FE-OC-007`)의 deep clone/parse 비용이 미측정이라 500ms 가 여유인지 빠듯한지 알 수 없음 | runtime key 10개를 채운 valid-config timing fixture 5회 실행의 중앙값 측정(impl §6) → `FE-GATE-004` timing report | `needs-confirmation` |
|
||||
| timing fixture 의 측정값이 mocked network delay 에 영향받지 않음(예산이 검증 구간만 잰다) | 측정 시작점이 fetch resolve 이후인지 코드로 강제되는지 미검증 | 동일 fixture 를 mock 지연 0ms / 200ms 로 각각 실행해 측정값 차이가 노이즈 범위 내인지 확인(impl §6 self-check) | `needs-confirmation` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
> `/coverage`가 채우는 생성물이며 손으로 유지하지 않는다.
|
||||
|
||||
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| TODO — `/coverage` 실행 전 | missing | (없음) | 미평가 | TODO |
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-GATE-004@1` | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | invalid fixture 가 예상 kind 로 거부되지 않으면 merge 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-OC-002@1` | [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] | `domain <- application <- presentation` 의존 방향과 application-owned output port를 MUST 지킴 | import 참조로 적용 |
|
||||
| `FE-OC-003@1` | [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] | package manager, engine, lockfile, source language, checkJs command를 한 곳에서 MUST 고정 | import 참조로 적용 |
|
||||
| `FE-OC-007@1` | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | JSON envelope와 payload는 boundary에서 runtime schema를 MUST 통과 | import 참조로 적용 |
|
||||
| `FE-OC-008@1` | [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] | 모든 failure는 stable frontend error kind로 MUST 정규화하고 raw body·stack을 UI에 노출하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-009@1` | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | retry는 safe/idempotent request에 한정하고 cap·jitter·`Retry-After`를 MUST 적용 | import 참조로 적용 |
|
||||
| `FE-OC-014@1` | [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] | telemetry는 best-effort이며 render·API success를 차단하면 안 되고 PII·token을 전송하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-016@1` | [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] | HTML, asset, runtime config, release manifest cache policy를 MUST 구분 | import 참조로 적용 |
|
||||
| `FE-OC-019@1` | [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] | browser bundle에 secret을 넣지 않고 untrusted HTML injection을 기본 금지 | import 참조로 적용 |
|
||||
| `FE-OC-022@1` | [[raw/branch-notes/feature-frontend-contract-registry-governance]] | 8개 registry는 single primary owner와 compatibility impact를 MUST 기록 | import 참조로 적용 |
|
||||
| `FE-OC-023@1` | [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] | API/config/storage/release schema의 breaking change는 migration 또는 version bump 없이 배포하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-025@1` | [[raw/branch-notes/feature-frontend-operational-runbook-contract]] | boot, chunk mismatch, API degradation, telemetry failure, rollback runbook을 MUST 유지 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
### Sub-branches (세부 작업)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 오류 기록 (이 branch 작업 중 발생)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 강의 (이 작업을 위해 학습한 강의)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### job-posting tie-ins (이 작업에서 파생된 글감)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크:
|
||||
- 리뷰 메모:
|
||||
- 머지 결과 / 배포 환경:
|
||||
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
|
||||
- `actually-implemented` 항목:
|
||||
- `locally-verified` 항목:
|
||||
- `prod-verified` 항목:
|
||||
- **추출하지 않을 항목** (planned / documented-only / abandoned):
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
---
|
||||
title: branch / feature-frontend-error-classification-boundary-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-007
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-007
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-VALIDATION-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-005, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-006]
|
||||
contract_packet: 1
|
||||
branch: feature-frontend-error-classification-boundary-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract.md]
|
||||
tags: [branch, ca-skeleton, frontend, error-handling, integration, javascript, api-contract]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
contract_packet_sha256: 24279f92059756981b403bc43da12c4b65488a08cc137f39a2277e0ae83dfdc7
|
||||
imports: [FE-OC-006@1, FE-OC-007@1, FE-OC-009@1, FE-OC-010@1, FE-OC-011@1, FE-OC-013@1, FE-OC-014@1, FE-OC-015@1, FE-OC-016@1, FE-OC-022@1, FLOW-FE-RESP-001@1, FLOW-FE-RESP-002@1, FLOW-FE-RESP-003@1, FLOW-FE-RESP-004@1, FLOW-FE-RESP-005@1, FLOW-FE-RESP-006@1, FLOW-FE-RESP-007@1]
|
||||
---
|
||||
|
||||
# branch: feature-frontend-error-classification-boundary-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — 단일 브랜치의 TODO·결정·진행 기록. 머지/종료 후 verified 결과는 `/ingest`로 `wiki/projects/`에 추출한다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: normalization matrix와 raw body·stack leakage negative test가 통과한다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-VALIDATION-001@1` | boundary runtime validation은 Zod schema로 수행한다 | validation failure signal을 stable frontend error kind로 정규화한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
|
||||
<!-- section-id: branch-local-decisions -->
|
||||
### 브랜치 지역 결정
|
||||
|
||||
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
||||
|---|---|---|---|---|
|
||||
| D1 | 모든 failure를 total function으로 단일 kind에 정규화한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D2 | normalized failure는 safe field만 보존한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D3 | FE-REG-ERROR를 error UX의 single-owner registry로 사용한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D4 | project trigger-to-kind matrix를 구현 계약으로 사용한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D5 | user copy는 userMessageKey로 간접화한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D6 | recovery action을 closed vocabulary로 제한한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D7 | defaultRetryable은 분류 힌트로만 사용한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D8 | Zod validation failure를 stage별 kind로 매핑한다 | `local` | `raw/official-docs/zod-runtime-schema-validation-official.md#ZOD-VALID-C4`, `raw/official-docs/zod-runtime-schema-validation-official.md#ZOD-VALID-C5` | `proposed` |
|
||||
|
||||
<!-- section-id: declared-overrides -->
|
||||
### 선언한 예외
|
||||
|
||||
| Override ID | Overrides | Reason | Approval | Status |
|
||||
|---|---|---|---|---|
|
||||
|
||||
없음.
|
||||
<!-- GENERATED: branch-contract:end -->
|
||||
|
||||
<!-- section-id: branch-goal -->
|
||||
## 목표
|
||||
|
||||
이 브랜치는 project-wide 계약 `FE-OC-008`("모든 failure 는 stable frontend error kind 로 MUST 정규화하고 raw body·stack 을 UI 에 노출하면 안 됨")을 *구현 착수 가능한 상세 명세*로 내린다. 핵심은 두 불변식이다. (1) **Total normalization** — HTTP response·adapter exception·browser exception 어느 경로든 정확히 하나의 안정 error kind(hub §5.6 의 26-kind enum)로 정규화되고, 어떤 named branch 와도 일치하지 않으면 catch-all `UNKNOWN_FAILURE` 로 폐기되며, 정규화되지 않은 throw 가 presentation 으로 통과하는 경로는 없다(hub §8.2 total-function 문단). (2) **Redaction boundary** — normalized failure 는 §8.1 의 안전 필드 집합만 담고 raw body·token·authorization header·full URL/query·stack·storage value 는 절대 포함하지 않는다. 이 브랜치는 error 계약 registry `FE-REG-ERROR`(`src/contracts/errors.js`)의 single owner 이며(hub §5.1), 그 산출물을 세 계약에 기여한다 — `FE-OC-011`(async terminal-error state 가 registry `action` 을 소비), `FE-OC-015`(operational failure 를 normal state 로 반환해 render boundary 로 throw 하지 않는 분리 신호 제공), `FE-OC-020`(negative fixture 카탈로그). 모든 진술은 코드가 없으므로 `planned` 등급이다.
|
||||
|
||||
- 이슈:
|
||||
- PR:
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- **Total normalization function** — response/adapter/browser exception → 26-kind 중 정확히 하나, 최종 catch-all `UNKNOWN_FAILURE`, presentation 으로의 un-normalized throw 금지 (hub §8.2, §5.6). 등급 `planned`.
|
||||
- 이 총함수가 곧 응답 처리 순서의 마지막 단계이며 **그 stage 의 owner 는 본 branch** 다 — hub §2.1.4 Flow Stage Registry `FLOW-FE-RESP-008@1`(application model 또는 실패 신호 → 정규화된 결과 반환; 총함수이므로 미매핑 예외는 `UNKNOWN_FAILURE` 로 귀결하고 throw 를 presentation 으로 통과시키지 않는다). 이 단계의 Invariants 를 바꾸려면 본 branch 가 revision 을 올려야 한다. stage 1~7 은 남의 소유라 `imports` 로만 pin 한다.
|
||||
- **`FE-REG-ERROR` registry** (`src/contracts/errors.js`) — kind/defaultRetryable/severity/userMessageKey/action/telemetryEvent/redaction 7-field row 를 26 kind 에 대해 소유 (hub §5.6, §5.1 owner map). 등급 `planned`.
|
||||
- **Trigger → kind 매핑 매트릭스** — hub §8.2 의 트리거(network/timeout/abort/content-type/JSON/envelope/schema/HTTP status class/chunk/boot/release/storage/render/telemetry/query-cache/unknown) → kind 총함수 매핑 구현 명세 (hub §8.2, §8.5). 등급 `planned`.
|
||||
- **Normalized failure safe-shape + redaction projection** — §8.1 필드 allowlist 만 통과, 나머지 drop (hub §8.1, §7.1 "raw response body 를 log 금지"). 이게 "raw body/stack 미노출" 절반. 등급 `planned`.
|
||||
- **`action` closed vocabulary 매핑** — 각 kind → `retry`/`reauth`/`navigate`/`reload-once`/`contact-support`/`none` 중 하나 + allowed-when/MUST-NOT 제약 (hub §8.4). 등급 `planned`.
|
||||
- **Negative fixtures + total-normalization matrix test + raw-body/stack leakage negative test** — §20 Measurable completion 의 두 산출물 (hub §8.5). 등급 `planned`.
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적 제외. 각 항목은 소유 브랜치를 명시(§15.5 R3 OUT_OF_BRANCH_SCOPE 방지). 아래 sibling 링크는 hook quirk 회피를 위해 `FE-OC-###` 계약 ID 로만 짝지음.
|
||||
|
||||
- **Retry algorithm/loop**(backoff·jitter·`Retry-After`·cap) — [[raw/branch-notes/feature-api-client-response-envelope-contract]] 의 `FE-OC-009` 소유. 본 브랜치는 kind 별 `defaultRetryable` *분류 힌트*만 선언하고 실제 재시도 루프는 실행하지 않는다.
|
||||
- **Schema/envelope validation 실패 신호 생성**(ZodError) — [[raw/branch-notes/feature-runtime-schema-validation-contract]] 의 `FE-OC-007` 소유. 본 브랜치는 그 실패를 *소비*해 kind 로 매핑만 한다.
|
||||
- **Telemetry transport/queue/redaction sink** — [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] 의 `FE-OC-014` 소유. registry 는 `telemetryEvent` 참조와 redaction *규칙*만 선언한다.
|
||||
- **Error boundary component ownership + reload-loop guard 메커니즘** — [[raw/branch-notes/feature-frontend-render-recovery-boundary-contract]] 의 `FE-OC-015` 소유. 본 브랜치는 operational-vs-defect 분류 *입력*만 공급한다.
|
||||
- **Async surface state 렌더링** — [[raw/branch-notes/feature-async-ui-state-contract]] 의 `FE-OC-011` 소유. 본 브랜치는 kind + action 만 공급한다.
|
||||
- **Token lifecycle / 401 recovery callback state machine** — auth·api-client 소유(`FE-OC-010`/`FE-OC-006`). 본 브랜치는 401→`AUTH_REQUIRED`, 403→`FORBIDDEN`, adapter throw→`AUTH_INTEGRATION_FAILURE` *매핑*만.
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | §8 Frontend Failure Taxonomy(§8.1 shape·§8.2 matrix·§8.3 retry order·§8.4 action·§8.5 fixture) + §5.6 error registry + §5.1 `FE-REG-ERROR` owner map — `FE-OC-008` 의 project-decision SSOT. D1·D2·D3·D4·D5·D6·D7 근거. |
|
||||
| [[raw/official-docs/zod-runtime-schema-validation-official]] | `FE-D007`(boundary runtime validation = Zod). `.parse()` 실패 시 granular `ZodError` throw(`ZOD-VALID-C4`)·`.safeParse()` discriminated union(`ZOD-VALID-C5`) 가 `SCHEMA_MISMATCH`/`ENVELOPE_MISMATCH` 매핑의 *소비 대상* 신호. D8 근거. 단 validator 소유는 sibling(`FE-OC-007`). |
|
||||
|
||||
## TODO
|
||||
|
||||
각 항목 옆에 증거 등급 표기. 현재 frontend 코드가 없으므로 전부 `planned`.
|
||||
|
||||
- [ ] `FE-REG-ERROR` registry `src/contracts/errors.js` — 26-kind × 7-field row 정의 (D3/D4/D5/D6) — 등급: `planned`
|
||||
- [ ] `adapters/http` total normalization function — dispatch 순서 + catch-all + safe-shape projection (D1/D2/D4/D7/D8) — 등급: `planned`
|
||||
- [ ] Trigger → kind 매핑 매트릭스 구현 (D4) — 등급: `planned`
|
||||
- [ ] Redaction / safe-shape projection — 필드 allowlist + drop rule (D2/D5) — 등급: `planned`
|
||||
- [ ] Total-normalization matrix test + raw-body/stack leakage negative test + §8.5 8종 negative fixture (D1/D2/D4) — 등급: `planned`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
없음 — `/branch-spec` 자동 채움 단계. 코드 미착수.
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> 아래 8개 결정은 Decision Evidence Map 의 prose mirror. 근거는 대부분 hub 의 project decision(§8/§5.6)이며, D8 만 외부 official-doc(Zod)이 병행 근거.
|
||||
|
||||
- 2026-07-19: **모든 failure 를 total function 으로 단일 kind 정규화** / 이유: presentation 이 raw exception·status 로 분기하면 계약이 깨지고 leakage 발생 / 검토한 대안: page 별 ad hoc try/catch(hub §5.1 `FE-REG-ERROR` "raw status/message 로 UI 분기" = ad hoc failure) — 배포 0회 throwaway 에서만 / 근거: hub §8.2 total-function 문단, §5.6.
|
||||
- 2026-07-19: **normalized failure 는 §8.1 safe 필드 집합만; raw body·token·header·URL·stack·storage value drop** / 이유: FE-OC-008 의 "raw body/stack 미노출" 강제 / 검토한 대안: 전체 error object 전달 후 UI 에서 마스킹 — 유출 위험으로 기각 / 근거: hub §8.1, §7.1.
|
||||
- 2026-07-19: **`FE-REG-ERROR` 를 error kind → 기본 UX 의 단일 owner registry 로 고정** / 이유: kind/action/userMessageKey/redaction 을 code 전역에서 재정의하면 single-owner 계약 위반 / 검토한 대안: 각 adapter 가 로컬 enum 소유 — governance 붕괴로 기각 / 근거: hub §5.6, §5.1.
|
||||
- 2026-07-19: **hub §8.2 trigger→kind 매핑(31-row / 고유 kind 26종)을 구현 매트릭스로 채택** / 이유: 트리거별 정규화 결과를 명세로 고정해야 total 성 검증 가능 / 검토한 대안: 상위 status class 만 매핑하고 나머지는 generic — negative fixture 통과 불가로 기각 / 근거: hub §8.2, §8.5.
|
||||
- 2026-07-19: **user copy 는 `userMessageKey` 간접화, `severity`(telemetry routing)와 분리, raw backend message 금지** / 이유: 다국어·문안 변경·PII 유출 방지 / 검토한 대안: backend `error.message` 직접 표시 — §5.6 금지 / 근거: hub §5.6.
|
||||
- 2026-07-19: **`action` 은 6개 closed vocabulary 로 제한** / 이유: 무한 spinner·history loop·반복 reload 같은 UX anti-pattern 을 계약으로 차단 / 검토한 대안: 자유 문자열 action — §8.4 제약 강제 불가로 기각 / 근거: hub §8.4, §5.6.
|
||||
- 2026-07-19: **`defaultRetryable` 은 분류 힌트일 뿐 재시도 결정이 아님** / 이유: 재시도 루프는 api-client 소유(method/idempotency/cap 조합), 분류는 요청을 발행하지 않음 / 검토한 대안: 분류 계층이 retryable=true 를 보고 직접 재시도 — safe/idempotency 조건 무시로 storm 위험, 기각 / 근거: hub §5.6(`defaultRetryable` override 가능), §8.3, §8.2 note.
|
||||
- 2026-07-19: **schema/envelope invalid 는 runtime-schema-validation 의 ZodError 를 소비해 `SCHEMA_MISMATCH`/`ENVELOPE_MISMATCH` 로 매핑, safe issue-path count + schema ID 만 보존** / 이유: validator 소유는 sibling, 분류는 결과 계약만 소비 / 검토한 대안: 분류 계층에서 zod schema 직접 실행 — 소유 경계 위반, 기각 / 근거: Zod `ZOD-VALID-C4`/`ZOD-VALID-C5`, hub §8.2·§5.6.
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
> `Supporting Claims` 는 hook quirk 회피를 위해 `FE-D###` 를 hub 경로에만 붙인다(sibling branch 링크 근처에 두지 않는다).
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | 모든 failure(response·adapter·browser exception)를 total function 으로 정확히 하나의 26-kind 로 정규화; 불일치·mapper 실패 시 catch-all `UNKNOWN_FAILURE`; un-normalized throw 의 presentation 통과 금지 (`FE-OC-008`) | client SPA 가 공유 backend 계약을 소비하고 배포·라우트가 존재하는 한 이 default 유지 / ad hoc page-local try/catch 는 route 1개·외부 API 0개·배포 0회 throwaway prototype 일 때만(hub §0.4 escape) | `raw/project-notes/ca-skeleton-frontend-operational-contract.md` §8.2 total-function 문단·§5.6 enum | `project-decision` | 총함수성은 exhaustive matrix test 로만 증명 가능(§8.5) — 미구현 시 mapper 누락 경로가 leak |
|
||||
| D2 | normalized failure 는 §8.1 safe 필드(kind/code/httpStatus?/retryable/operationId/attemptCount/requestId?/traceId?/userMessageKey/action/causeClass)만 담고 raw body·token·authorization header·full URL/query·stack·storage value 는 drop (`FE-OC-008`) | 모든 kind·모든 경로에서 불변(FE-OC-008 이 무조건 강제) / 예외 없음 — 예외 필요 시 FE-OC-008 자체 변경 절차(hub §3.3) | `...frontend-operational-contract.md` §8.1 shape·§7.1 "raw response body 를 log 금지"·§8.2 telemetry 열 | `project-decision` | leakage 는 negative test(직렬화 후 금지 필드 부재 assert)로만 확인 — 이게 FE-OC-008 minimum evidence |
|
||||
| D3 | `FE-REG-ERROR`(`src/contracts/errors.js`)를 error kind→기본 UX 의 single-owner registry 로 고정; kind/defaultRetryable/severity/userMessageKey/action/telemetryEvent/redaction 7-field | 8-registry governance(hub `FE-D018`)가 유효한 한 registry-first / code generation SSOT 채택 시 재검토 | `...frontend-operational-contract.md` §5.6·§5.1(`FE-REG-ERROR` owner=this branch, ad hoc=raw status/message 분기); `...frontend-operational-contract.md` `FE-D018` | `project-decision` | registry snapshot·single-owner scan 강제는 `FE-OC-022` sibling 소유 — 본 브랜치는 스키마·row 만 |
|
||||
| D4 | hub §8.2 trigger→kind 매핑(31-row / 고유 kind 26종)을 구현 매트릭스로 채택; 각 트리거는 정확히 하나의 kind, §8.5 8종 negative fixture 로 검증 | backend 가 structured JSON envelope + 표준 HTTP status 를 제공하는 한 유지 / backend protocol 이 근본적으로 다르면(hub 가정 C 무효) 매트릭스 재도출 | `...frontend-operational-contract.md` §8.2 matrix·§8.5 fixtures | `project-decision` | 일부 row 는 sibling 이 실패 신호를 *생성*해야 성립(schema→FE-OC-007, status/retry→FE-OC-009) — 그 계약 shape 미확정 시 매핑 재조정 |
|
||||
| D5 | user copy 는 `userMessageKey` 간접화, `severity`(telemetry routing hint)와 분리, raw backend `error.message` 표시 금지 | 다국어/문안 거버넌스가 존재하는 한 항상 keyed / 대안 없음 — raw message 표시는 §5.6 이 금지 | `...frontend-operational-contract.md` §5.6(userMessageKey·severity rule) | `project-decision` | message key → 실제 copy 카탈로그 소유(i18n)는 본 브랜치 밖 — 미정 시 key 계약만 고정 |
|
||||
| D6 | 각 kind 는 6개 closed action(`retry`/`reauth`/`navigate`/`reload-once`/`contact-support`/`none`) 중 하나로 매핑, §8.4 allowed-when/MUST-NOT 제약 준수 | UX 계약이 유지되는 한 closed set / product 가 새 recovery 모드를 요구하면 §8.4 확장 후 registry 갱신 | `...frontend-operational-contract.md` §8.4 vocabulary·§5.6 action field | `project-decision` | action 의 실제 UI 실행은 async-ui(`FE-OC-011`)·render-recovery(`FE-OC-015`) 소유 — 본 브랜치는 kind→action 계약만 |
|
||||
| D7 | `defaultRetryable` 은 분류 힌트일 뿐 재시도 결정·루프가 아님; 분류 계층은 어떤 요청도 발행하지 않음, request context override 가능 | 재시도 정책이 api-client(`FE-OC-009`) 소유인 한 힌트-only / 대안(분류가 직접 재시도)은 method+idempotency+cap 조건을 통합 소유하도록 scope 병합 시에만 | `...frontend-operational-contract.md` §5.6(`defaultRetryable` override 가능)·§8.3 retry decision order·§8.2 note("retryable=true 는 필요조건이지 충분조건 아님") | `project-decision (delegated boundary)` | 힌트와 실제 정책이 어긋나면(backend retryable=true 지만 unsafe mutation) storm — 통합 테스트로 경계 검증 필요 |
|
||||
| D8 | content-type/JSON/envelope/payload invalid 는 runtime-schema-validation 이 낸 ZodError 를 소비해 `CONTENT_TYPE_MISMATCH`/`MALFORMED_JSON`/`ENVELOPE_MISMATCH`/`SCHEMA_MISMATCH` 로 매핑, schema ID + safe issue-path count 만 보존 | `FE-D007`(Zod boundary validation)이 유효한 한 소비-매핑 / bundle budget·generated schema pipeline 이 대체안을 요구하면 재검토(hub `FE-D007` revisit) | `raw/official-docs/zod-runtime-schema-validation-official.md#ZOD-VALID-C4`, `#ZOD-VALID-C5`; `...frontend-operational-contract.md` `FE-D007`·§8.2 해당 row·§5.6 | `official-vendor-doc + project-decision` | ZodError → 어느 kind(envelope vs payload)인지는 sibling 이 어느 단계에서 던졌는지에 의존 — 처리 순서(§7.3 4~6단계) 계약 미확정 시 매핑 모호 |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> `planned` blueprint. 경로는 hub §4.6 Planned directory blueprint + §5.1 owner map 에서 유도되므로 grounded 하지만 repository 생성 시 바뀔 수 있어 전체 `planned`. sibling 링크가 필요한 detail 은 §범위 Out of scope 로 위임했고 여기 남기지 않는다(R3).
|
||||
|
||||
### 1. `FE-REG-ERROR` 계약 registry (`src/contracts/errors.js`)
|
||||
|
||||
> **Trace**: D3 + D4 + D5 + D6 / `FE-OC-008`·`FE-REG-ERROR`·hub §5.6·§8.2·§8.4. 26-kind enum(hub §5.6) 각각에 대해 7-field row.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: `code` 필드 포맷(§8.1 은 `code` 존재만 명시, 포맷 미규정) → `<KIND>` 접미 없는 안정 문자열 상수 채택. trade-off: kind 와 1:1 이면 code 잉여지만, backend `error.code`(§7.3)와 대응시키려면 별 축이 필요 — 초기엔 kind 파생 상수로 두고 backend code 매핑표는 추후.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: `userMessageKey` 명명 스킴(§5.6 은 "key" 만 요구, 규칙 미규정) → `error.<kind_snake>.message` 제안(planned). trade-off: i18n 카탈로그 소유 밖이므로 key 계약만 고정, 실제 문안은 미정.
|
||||
|
||||
| Field | 규칙(hub §5.6) | 이 브랜치 명세 |
|
||||
|---|---|---|
|
||||
| `kind` | frontend stable enum | §5.6 26-kind enum 그대로, rename 금지 |
|
||||
| `defaultRetryable` | request context override 가능 | boolean 기본값; 실제 재시도는 D7 대로 미실행 |
|
||||
| `severity` | telemetry routing hint, user copy 분리 | enum(예: `low`/`warn`/`error`) — telemetry 소비, D5 대로 copy 와 분리 |
|
||||
| `userMessageKey` | raw backend message 금지 | key 상수(UNSUPPORTED_IMPL_DECISION 스킴) |
|
||||
| `action` | 6-value closed set | D6 vocabulary 중 하나 |
|
||||
| `telemetryEvent` | registry event 매핑 | `FE-REG-TELEMETRY` event 참조(소유는 FE-OC-014, 여기선 참조만) |
|
||||
| `redaction` | cause/body/header drop rule | D2 safe-shape 와 일치하는 drop rule id |
|
||||
|
||||
### 2. Total normalization 함수 (`adapters/http` error mapper)
|
||||
|
||||
> **Trace**: D1 + D2 + D4 + D7 + D8 / `FE-OC-008`·hub §8.2·§8.1·§7.3(처리 순서 4~8단계). `adapters/http` 가 "envelope/schema/error mapping" 을 소유(hub §4.2).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 정규화 함수 파일/심볼명(hub 는 `adapters/http/` 폴더와 `src/contracts/errors.js` registry 만 grounding, 함수명 미규정) → `adapters/http/normalize-failure.js` 단일 export 제안. trade-off: 이름은 임의지만 "단일 진입 + adapters/http 내부" 두 제약만 지키면 계약 동등.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: dispatch 메커니즘(§8.2 는 총함수·catch-all 만 요구, switch vs lookup table 미규정) → 트리거 판별 → kind lookup 순서 dispatch 제안. trade-off: lookup table 은 registry 대조가 쉽고 switch 는 분기 명시적 — 총함수성만 test 로 보장하면 무관.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: `causeClass` internal allowlist 실제 값(§8.1 "internal allowlist only" 만, 목록 미열거) → 초기 allowlist(예: `network`/`parse`/`schema`/`auth`/`http-status`/`browser-storage`/`render`/`unknown`) 제안(planned). trade-off: allowlist 밖 값은 `unknown` 으로 접어 leak 방지, 세분화는 telemetry 요구에 따라 확장.
|
||||
|
||||
처리 순서(§7.3 4~8단계 하류에서 호출됨, 요청 발행 없음):
|
||||
|
||||
```text
|
||||
input = { transportOutcome | thrownValue, requestContext }
|
||||
1. aborted(navigation/user/superseded) 이면 REQUEST_ABORTED
|
||||
2. network-level opaque 실패면 NETWORK_UNREACHABLE / timeout 이면 REQUEST_TIMEOUT
|
||||
3. content-type/JSON/envelope/payload 실패 신호(sibling 생성)면 D8 매핑
|
||||
4. HTTP status class 면 §8.2 status row 매핑(auth/authz/not-found/conflict/validation/rate/server/generic)
|
||||
5. chunk/boot/release/deploy/storage/render/telemetry/query-cache 트리거면 해당 kind
|
||||
6. 위 어디에도 안 맞거나 mapper 자체 throw 면 UNKNOWN_FAILURE(catch-all)
|
||||
7. 매핑 결과를 §3 safe-shape 로 projection 후 반환 (raw value 폐기)
|
||||
```
|
||||
|
||||
### 3. Trigger → kind 매핑 매트릭스
|
||||
|
||||
> **Trace**: D4 + D8 / hub §8.2 (31-row / 고유 kind 26종 — row 기준으로 세면 같은 kind 로 매핑되는 status row 5개가 누락된다)·§8.5. 아래는 hub §8.2 를 이 브랜치의 in-scope(=여기서 정규화 산출) 관점으로 재기술한 것이며, "생성 소유"가 sibling 인 트리거는 *소비*만 표시(값 재정의 아님).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — 모든 row 는 hub §8.2 가 trigger·kind·retry·fallback·UX·telemetry 를 직접 grounding.
|
||||
|
||||
| 트리거 그룹(§8.2) | 산출 kind | 생성 소유 | 본 브랜치 역할 |
|
||||
|---|---|---|---|
|
||||
| network opaque / total timeout / abort | `NETWORK_UNREACHABLE`·`REQUEST_TIMEOUT`·`REQUEST_ABORTED` | api-client transport(`FE-OC-006`) | 소비→정규화 |
|
||||
| content-type/JSON/envelope/payload invalid | `CONTENT_TYPE_MISMATCH`·`MALFORMED_JSON`·`ENVELOPE_MISMATCH`·`SCHEMA_MISMATCH` | schema-validation(`FE-OC-007`) | 소비→정규화(D8) |
|
||||
| 401/403/404/409/422/other-4xx/429/5xx | `AUTH_REQUIRED`·`FORBIDDEN`·`NOT_FOUND`·`CONFLICT`·`VALIDATION_REJECTED`·`UNKNOWN_CLIENT_FAILURE`·`RATE_LIMITED`·`SERVER_FAILURE` | api-client status(`FE-OC-006`) | 소비→정규화, `defaultRetryable` 힌트만(D7) |
|
||||
| auth attach/recovery adapter 실패 | `AUTH_INTEGRATION_FAILURE` | auth/api-client(`FE-OC-010`) | 소비→정규화 |
|
||||
| chunk/boot/release/deploy | `CHUNK_LOAD_FAILURE`·`BOOT_CONFIG_FAILURE`·`RELEASE_MANIFEST_FAILURE`·`DEPLOY_MISMATCH` | bootstrap/release(`FE-OC-015`/`FE-OC-016`) | 소비→정규화 |
|
||||
| storage unavailable/quota | `STORAGE_UNAVAILABLE`·`STORAGE_QUOTA_EXCEEDED` | storage(`FE-OC-013`) | 소비→정규화 |
|
||||
| render throw / telemetry fail / query-cache fail / unknown | `RENDER_FAILURE`·`TELEMETRY_FAILURE`·`QUERY_CACHE_FAILURE`·`UNKNOWN_FAILURE` | 각 owner / catch-all | 소비→정규화, 최종 catch-all 소유 |
|
||||
|
||||
### 4. Redaction & safe-shape projection
|
||||
|
||||
> **Trace**: D2 + D5 / hub §8.1·§7.1·§8.2 telemetry 열. 정규화 함수 마지막 단계(§2 step 7).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: projection 구현 방식(§8.1 은 필드 집합만, allowlist-copy vs blocklist-delete 미규정) → **allowlist-copy**(안전 필드만 새 객체로 복사) 제안. trade-off: blocklist-delete 는 신규 raw 필드 추가 시 leak 위험 — allowlist 가 fail-closed 이므로 채택.
|
||||
|
||||
- **통과 허용(allowlist)**: §8.1 필드 집합 그대로.
|
||||
- **항상 drop**: raw response body, token, authorization header, full URL/query, stack, storage value(§8.1) + backend raw `error.message`(D5, §5.6).
|
||||
- **telemetry projection**: §8.2 telemetry 열의 kind별 safe 항목만(예: status group·attempts·elapsed bucket·schema ID·safe issue-path count) — raw URL·body·principal·token 금지. 실제 전송은 `FE-OC-014` 소유(여기선 payload 계약만).
|
||||
|
||||
### 5. test 카탈로그 (§20 Measurable completion)
|
||||
|
||||
> **Trace**: D1 + D2 + D4 / hub §8.5·§20("total normalization matrix + raw body/stack leakage negative tests"). `FE-OC-020` 기여.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: test 파일 경로·러너별 배치(hub §4.6 은 `tests/unit|component|...` 폴더만) → `tests/unit/error-classification/*` 배치 제안. trade-off: 경로 임의, "unit 레벨 + registry/mapper 대상" 계약만 유지.
|
||||
|
||||
| Fixture(§8.5) | 기대 정규화 결과 |
|
||||
|---|---|
|
||||
| JSON operation + `text/html` response | `CONTENT_TYPE_MISMATCH` |
|
||||
| auth attach callback throw/reject | `AUTH_INTEGRATION_FAILURE` |
|
||||
| bounded recovery invalid state | `AUTH_INTEGRATION_FAILURE` |
|
||||
| release manifest network/parse/schema 실패 | `RELEASE_MANIFEST_FAILURE` |
|
||||
| QueryCachePort adapter throw / invalid result | `QUERY_CACHE_FAILURE` |
|
||||
| unregistered `418`/기타 unmapped 4xx | `UNKNOWN_CLIENT_FAILURE` |
|
||||
| thrown non-`Error` / symbol / mapper exception | `UNKNOWN_FAILURE` |
|
||||
| **총함수 matrix test**(추가) | 26-kind 전체 트리거 exhaustive → 정확히 1 kind |
|
||||
| **leakage negative test**(추가) | 정규화 결과 직렬화 후 body/token/header/URL/stack/storage value 부재 assert |
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- *Mapper 자체 throw* → 최종 catch-all 이 raw value 폐기 후 `UNKNOWN_FAILURE` 반환(§8.2 total-function 문단). 정규화 실패로 인한 un-normalized throw 는 계약상 존재 불가.
|
||||
- *Unmapped 4xx*(예: `418`) → `UNKNOWN_CLIENT_FAILURE`; *unmapped thrown value*(non-Error/symbol) → `UNKNOWN_FAILURE`(§8.5).
|
||||
- *이미 정규화된 failure 재진입* → 재정규화는 idempotent 여야 함(같은 kind 유지) — Claims To Verify 로 승격.
|
||||
- *registry 미등록 kind 사용* → registry 가 closed enum 이므로 컴파일/lint 단계 차단이 이상적(강제는 `FE-OC-022` governance sibling).
|
||||
- *`defaultRetryable=true` 이지만 unsafe mutation* → 분류는 힌트만 노출, 재시도 미실행(D7). 실제 안전성은 api-client 가 method/idempotency/cap 으로 최종 판단.
|
||||
- **다른 계약 의존**(hook quirk 회피: sibling 링크는 `FE-OC-###` 로만 짝지음):
|
||||
- [[raw/branch-notes/feature-api-client-response-envelope-contract]] (`FE-OC-006`/`FE-OC-009`) — transport outcome·HTTP status·retry 정책을 *생성/소유*. 그 계약(§7.3 처리 순서, §7.4 timeout/abort) 이 바뀌면 본 브랜치 트리거→kind 매핑 재조정 필요.
|
||||
- [[raw/branch-notes/feature-runtime-schema-validation-contract]] (`FE-OC-007`) — content-type/JSON/envelope/payload 검증 실패(ZodError)를 *생성*. 어느 단계에서 던지는지가 envelope vs payload kind 를 결정(D8) — 계약 변경 시 매핑 영향.
|
||||
- [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] (`FE-OC-014`) — `telemetryEvent`·redaction sink 를 *소비*. registry 의 telemetry payload 계약이 그 소유와 정합해야 함.
|
||||
- [[raw/branch-notes/feature-async-ui-state-contract]] (`FE-OC-011`) 와 [[raw/branch-notes/feature-frontend-render-recovery-boundary-contract]] (`FE-OC-015`) — normalized kind + action 을 *소비*(terminal-error state·operational-vs-defect 분리). 본 브랜치 산출이 이들 입력.
|
||||
- [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] (`FE-OC-020`) — negative fixture 를 gate 로 *소비*.
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| 정규화가 진짜 total 이다 — 어떤 경로도 un-normalized 로 presentation 도달 안 함 | 코드 미존재, mapper 누락 분기 가능 | exhaustive trigger matrix test + mapper-throws fixture → `UNKNOWN_FAILURE` (§8.5) | `needs-confirmation` |
|
||||
| normalized failure 에 raw body/stack/token/header/URL/storage value 가 유출되지 않는다 | allowlist projection 미구현 | leakage negative test — 결과 직렬화 후 금지 필드 부재 assert (FE-OC-008 minimum evidence) | `needs-confirmation` |
|
||||
| 26-kind 각각 정확히 1 registry row + closed action 1개를 갖는다 | registry 미작성 | registry snapshot test + action ∈ 6-set 검증 | `needs-confirmation` |
|
||||
| 분류 계층은 어떤 요청도 발행하지 않는다(재시도는 api-client 소유) | 힌트/정책 경계가 코드로 미분리 | 분류 함수 단위 test 에서 fetch/network mock 호출 0회 assert | `needs-confirmation` |
|
||||
| ZodError → envelope vs payload kind 매핑이 처리 순서와 정합 | sibling 처리 단계 계약 미확정 | schema-invalid fixture(envelope-level, payload-level 각각) → `ENVELOPE_MISMATCH`/`SCHEMA_MISMATCH` | `needs-confirmation` |
|
||||
| 재정규화가 idempotent 하다(이미 정규화된 failure 재진입 시 동일 kind) | 재진입 경로 미설계 | 정규화 결과를 재입력 → 동일 kind assert | `planned` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
> `/coverage`가 채우는 생성물이며 손으로 유지하지 않는다.
|
||||
|
||||
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| TODO — `/coverage` 실행 전 | missing | (없음) | 미평가 | TODO |
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: flow:start -->
|
||||
### 가져온 흐름 단계
|
||||
|
||||
| Stage Ref | Order | Owner | Input | Action | Output |
|
||||
|---|---:|---|---|---|---|
|
||||
| `FLOW-FE-RESP-001@1` | 1 | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | HTTP 요청 | transport 완료 대기 | raw Response |
|
||||
| `FLOW-FE-RESP-002@1` | 2 | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | raw Response | content-type 기대값 검사 | 본문 판독 가능 Response |
|
||||
| `FLOW-FE-RESP-003@1` | 3 | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | 본문 판독 가능 Response | JSON parse | unvalidated JSON |
|
||||
| `FLOW-FE-RESP-004@1` | 4 | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | unvalidated JSON | envelope 공유 스키마 검증 | discriminated envelope |
|
||||
| `FLOW-FE-RESP-005@1` | 5 | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | discriminated envelope | success/failure 분기 검증 | 분기 확정 envelope |
|
||||
| `FLOW-FE-RESP-006@1` | 6 | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | 분기 확정 envelope | payload per-operation 스키마 검증 | 검증된 payload(deep clone) |
|
||||
| `FLOW-FE-RESP-007@1` | 7 | [[raw/branch-notes/feature-boundary-mapper-viewmodel-contract]] | 검증된 payload | DTO → application model 매핑 | application model |
|
||||
<!-- GENERATED: flow:end -->
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-OC-006@1` | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | 모든 HTTP는 shared client를 MUST 통과하고 timeout·abort·response parsing을 page에서 구현하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-007@1` | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | JSON envelope와 payload는 boundary에서 runtime schema를 MUST 통과 | import 참조로 적용 |
|
||||
| `FE-OC-009@1` | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | retry는 safe/idempotent request에 한정하고 cap·jitter·`Retry-After`를 MUST 적용 | import 참조로 적용 |
|
||||
| `FE-OC-010@1` | [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] | skeleton은 session state를 소비하되 token lifecycle을 MUST 소유하지 않음 | import 참조로 적용 |
|
||||
| `FE-OC-011@1` | [[raw/branch-notes/feature-async-ui-state-contract]] | async surface는 initial-loading, success, empty, terminal-error를 MUST 표현 | import 참조로 적용 |
|
||||
| `FE-OC-013@1` | [[raw/branch-notes/feature-frontend-storage-registry-contract]] | storage key는 namespace·version·classification을 MUST 가지며 token/secret 저장을 금지 | import 참조로 적용 |
|
||||
| `FE-OC-014@1` | [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] | telemetry는 best-effort이며 render·API success를 차단하면 안 되고 PII·token을 전송하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-015@1` | [[raw/branch-notes/feature-frontend-render-recovery-boundary-contract]] | expected operational error와 render defect를 MUST 분리하고 reload loop를 금지 | import 참조로 적용 |
|
||||
| `FE-OC-016@1` | [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] | HTML, asset, runtime config, release manifest cache policy를 MUST 구분 | import 참조로 적용 |
|
||||
| `FE-OC-022@1` | [[raw/branch-notes/feature-frontend-contract-registry-governance]] | 8개 registry는 single primary owner와 compatibility impact를 MUST 기록 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
### Sub-branches (세부 작업)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 오류 기록 (이 branch 작업 중 발생)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 강의 (이 작업을 위해 학습한 강의)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### job-posting tie-ins (이 작업에서 파생된 글감)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크:
|
||||
- 리뷰 메모:
|
||||
- 머지 결과 / 배포 환경:
|
||||
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
|
||||
- `actually-implemented` 항목:
|
||||
- `locally-verified` 항목:
|
||||
- `prod-verified` 항목:
|
||||
- **추출하지 않을 항목** (planned / documented-only / abandoned):
|
||||
+378
@@ -0,0 +1,378 @@
|
||||
---
|
||||
title: branch / feature-frontend-observability-logging-trace-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-012
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-012
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-TELEMETRY-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-REGISTRY-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-004]
|
||||
contract_packet: 1
|
||||
branch: feature-frontend-observability-logging-trace-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract.md]
|
||||
tags: [branch, ca-skeleton, frontend, observability, error-handling, javascript]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
contract_packet_sha256: e79f4a8ea9b3ea9f54126cdb47a1228194322b9fa883ca499d546f9ffa607e63
|
||||
imports: [FE-OC-008@1, FE-OC-015@1, FE-OC-021@1, FE-OC-025@1]
|
||||
|
||||
---
|
||||
|
||||
# branch: feature-frontend-observability-logging-trace-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — 단일 브랜치의 TODO·결정·진행 기록. `/branch-spec` 로 hub 계약을 구현-준비 spec 으로 내렸다. 프론트엔드 코드가 아직 없으므로 **모든 구현 주장은 `planned`** 이며 코드 evidence 는 repository 생성 후 채운다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: telemetry registry·redaction·bounded queue·sink failure test가 통과한다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-TELEMETRY-001@1` | telemetry는 best-effort queue와 redaction을 사용하며 sink failure가 UI를 실패시키지 않는다 | TelemetryPort·queue·redaction·degradation 계약에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-REGISTRY-001@1` | route, API operation, env, storage, error, query, telemetry, release token을 8개 registry로 관리한다 | hub §5.8이 정의한 FE-REG-TELEMETRY 스키마·초기 event를 코드 registry로 구현한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
|
||||
<!-- section-id: branch-local-decisions -->
|
||||
### 브랜치 지역 결정
|
||||
|
||||
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
||||
|---|---|---|---|---|
|
||||
| D1 | telemetry를 best-effort non-blocking 경로로 격리한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D2 | low-cardinality allowlist와 forbidden attribute redaction을 강제한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D3 | bounded queue와 비재귀 drop reporting을 사용한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D4 | FE-REG-TELEMETRY를 event schema의 single SSOT로 사용한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D5 | backend 지원 여부에 따라 trace correlation을 전파하거나 local ID로 강등한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D6 | delivery transport를 adapter-owned degradation 경로로 둔다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D7 | terminal failure telemetry를 bounded safe event로 제한한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D8 | TELEMETRY_ENABLED를 composition-root kill-switch로 소비한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
|
||||
<!-- section-id: declared-overrides -->
|
||||
### 선언한 예외
|
||||
|
||||
| Override ID | Overrides | Reason | Approval | Status |
|
||||
|---|---|---|---|---|
|
||||
|
||||
없음.
|
||||
<!-- GENERATED: branch-contract:end -->
|
||||
|
||||
<!-- section-id: branch-goal -->
|
||||
## 목표
|
||||
|
||||
이 브랜치는 project-wide contract `FE-OC-014` (telemetry 는 best-effort 이며 render·API success 를 차단하면 안 되고 PII·token 을 전송하면 안 됨) 와 그 owner decision [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 (telemetry = best-effort queue + redaction, sink failure 는 UI 를 실패시키지 않음) 를 **구현자가 되묻지 않아도 코드를 쓸 수 있는 수준의 spec** 으로 내린다. 핵심 불변식은 **운영 격리 (operational isolation)** — telemetry 실패가 사용자 경험(render/API critical path)과 완전히 분리된다는 것이다. 동시에 이 브랜치는 `FE-REG-TELEMETRY` registry (§5.8 event/attribute/redaction) 의 single owner 로서 hub §5.8 이 정의한 최소 스키마와 초기 event 집합을 코드 registry 로 구현하고 emit 지점을 확정하며, `FE-OC-008` (실패→telemetry rule), `FE-OC-021` (low-cardinality 성능 attribute), `FE-OC-025` (`FE-RB-004` telemetry sink failure runbook) 에 telemetry 기여 edge 를 제공한다. 등급: 전 항목 `planned` (repository 부재).
|
||||
|
||||
- 이슈: 없음 (repository 미생성)
|
||||
- PR: 없음
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- **Best-effort 전달 불변식** — telemetry send 가 render·API critical path 를 절대 block 하지 않음, sink/queue/adapter-init 실패가 UI 를 실패시키지 않음 (`FE-OC-014`, [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §11.2).
|
||||
- **`FE-REG-TELEMETRY` registry 파일 소유** — registry 스키마와 초기 5 event 의 *정의* 는 hub §5.8 소유이고, 본 브랜치는 그 registry 를 코드로 구현하고 emit 지점을 확정한다(§5.8, §5.1). 자유 문자열 event 금지.
|
||||
- **Redaction** — low-cardinality allowlist context 만 전송, forbidden attribute(token·email·raw URL/query/body·storage value·stack) 전송 금지 (§11.1, §5.8).
|
||||
- **Bounded queue + overflow 정책 + 비재귀** — bounded queue, overflow drop 정책 registry 명시, telemetry 실패를 동일 sink 로 재귀 보고하지 않음 (§11.2, §16.4).
|
||||
- **Delivery degradation** — local/dev console-safe sink, prod endpoint 부재/invalid 시 telemetry 만 degrade 하고 app 계속 실행 (§11.2, §16.4).
|
||||
- **`TELEMETRY_ENABLED` kill-switch 소비** — runtime flag(default `false`)가 off 일 때 telemetry 전체를 무력화하는 **소비 측 의미**와 그 단일 적용 지점 확정, `FE-RB-004` mitigation "telemetry runtime flag disable" 의 실행 가능성 보장 (§5.4, §16.4). key 선언·schema 검증 자체는 `FE-OC-004` 소유.
|
||||
- **Trace correlation (telemetry 관점)** — W3C `traceparent` 가 backend contract 상 허용될 때만 전파, 미지원 시 local operation ID 로 degrade, retry 는 같은 logical operation correlation 유지하되 attempt 구분 (§11.3, §8.1).
|
||||
- **기여 edge** — `FE-OC-008` 실패→telemetry rule column, `FE-OC-021` duration/attempt bucket 제공, `FE-OC-025` `FE-RB-004` recovery assertion.
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외 — 다른 owner 브랜치/외부 계약이 소유. 본 브랜치는 telemetry 관점의 consume/기여만 한다.
|
||||
|
||||
- **Error kind 정규화 taxonomy 자체** — `FE-OC-008` owner (frontend-error-classification-boundary branch). 본 브랜치는 `error_kind` 를 소비만 하고 정의하지 않음.
|
||||
- **Render error boundary 소유·복구** — `FE-OC-015` owner (frontend-render-recovery-boundary branch). 본 브랜치는 boundary-catch 신호를 consume 해 `ui.render.failed` 를 emit 만 함.
|
||||
- **Web Vitals 측정·NFR 리포트** — `FE-OC-021` owner ([[raw/branch-notes/feature-web-vitals-performance-budget-contract]]). 본 브랜치는 low-cardinality attribute bucket 만 공급.
|
||||
- **`FE-RB-004` runbook 1차 소유** — `FE-OC-025` owner (frontend-operational-runbook branch). 본 브랜치는 technical escalation 이며 diagnosis evidence field 만 공급.
|
||||
- **Telemetry endpoint 의 CSP `connect-src`·bundle 내 secret 금지 강제** — `FE-OC-019` owner (frontend-browser-security-boundary branch).
|
||||
- **Runtime config 로딩·검증** — `FE-OC-004` owner (frontend-env-runtime-config branch). 본 브랜치는 endpoint 값을 consume 만 함(의존, §엣지·실패·의존).
|
||||
- **Token lifecycle** — 외부 Keycloak / [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] (`FE-OC-010`). telemetry 는 token 을 절대 전송하지 않음.
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | 본 브랜치의 거의 모든 결정의 SSOT — FE-D021(§3.2), FE-OC-014(§2.1), telemetry 계약(§11), `FE-REG-TELEMETRY`(§5.8/§5.1), 실패 matrix telemetry column(§8.2), `FE-RB-004`(§16.4), `TELEMETRY_ENABLED` runtime key(§5.4) + boot sequence(§6.3)/config validation(§6.4)/composition root(§4.5). D1~D8 전부 이 hub 의 project decision 을 인용한다. |
|
||||
| [[raw/official-docs/react-ui-library-official]] | `REACT-UI-C1` — presentation 이 React component 로 구성됨(버튼~페이지). `ui.render.failed` event 의 emit point 가 React component boundary catch 라는 D4 event catalog 항목을 간접 뒷받침. boundary 자체의 소유는 render-recovery branch(`FE-OC-015`)에 위임. |
|
||||
|
||||
> 참고: §11.3 trace correlation 이 언급하는 W3C `traceparent` (Trace Context) 는 실제 표준이나 `raw/official-docs/` 에 아직 아카이브되지 않았다. 따라서 사실로 인용하지 않고 hub §11.3 의 project decision(허용될 때만 전파)만 근거로 쓴다. 표준 자체를 근거로 삼아야 할 결정이 생기면 `wiki-source-summarizer` 로 먼저 아카이브한다.
|
||||
|
||||
## TODO
|
||||
|
||||
각 항목 옆 증거 등급. repository 부재 → 전부 `planned` / `needs-confirmation`.
|
||||
|
||||
- [ ] `FE-REG-TELEMETRY` registry (`src/contracts/telemetry.js`): hub §5.8 의 7-field 스키마와 초기 5 event 를 코드로 구현 + emit 지점 배선 — 등급: `planned`
|
||||
- [ ] Redaction 강제 (allowlist projection) + forbidden-attribute scan test — 등급: `planned`
|
||||
- [ ] Bounded queue + overflow drop 정책 + queue drain/memory test — 등급: `planned`
|
||||
- [ ] Sink failure / degradation test (endpoint invalid → telemetry 만 degrade, app 계속) — 등급: `planned`
|
||||
- [ ] Trace correlation 전파 + retry attempt 구분 test — 등급: `planned`
|
||||
- [ ] `TelemetryPort` (application) + telemetry adapter + composition-root wiring — 등급: `planned`
|
||||
- [ ] `TELEMETRY_ENABLED=false` → no-op port 주입 + zero-network/zero-queue 회귀 test (`FE-RB-004` mitigation 재현) — 등급: `planned`
|
||||
- [ ] 구현 repository 및 검증 evidence 식별 — 등급: `needs-confirmation`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
- `/branch-spec` 로 hub §2/§3/§5/§8/§11/§16 을 내려 D1~D7 을 확정. 모든 grounding 은 hub project decision(FE-D021 중심) — 외부 official-doc 은 react-ui(간접)만 관여. web research 0건(hub 가 충분).
|
||||
- 2026-07-20 loop-back fill: coverage 감사에서 `TELEMETRY_ENABLED` kill-switch **소비 측** 메커니즘이 미결정(MISSING_CONCERN)으로 드러나 D8 + 구현 가이드 7 을 추가했다. hub §5.4 는 key 를 선언하고 §16.4 는 그 disable 을 mitigation lever 로 *요구* 하지만 소비 형태는 미명시 — 사용자 소유 브랜치가 없어 본 브랜치가 소비 owner 다(`FE-OC-004` 는 key 선언·schema 검증만 소유). 같은 pass 에서 `telemetry.delivery.dropped` 의 전달 채널(비재귀 구체화)과 `route_id`/`operation_id` producer 의존을 명시했다.
|
||||
- 운영 격리(operational isolation)가 이 브랜치의 축: telemetry 는 관찰 목적이며 절대 UX 를 볼모로 잡지 않는다. 그래서 delivery guarantee 를 주장하지 않고 best-effort 로 못 박는다.
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> 아래 Decision Evidence Map 의 prose mirror. 각 결정의 근거는 hub project decision.
|
||||
|
||||
- 2026-07-18: **Telemetry = best-effort, non-blocking** — render/API critical path 를 차단하지 않고 sink failure 가 UI 를 실패시키지 않는다. 대안(delivery-guaranteed audit channel)은 regulated audit event 가 필요할 때만 별도 계약으로 분리. 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §11.2.
|
||||
- 2026-07-18: **Redaction 우선** — low-cardinality allowlist context 만 전송, token/PII/raw payload 는 forbidden. 근거: hub §11.1, §5.8.
|
||||
- 2026-07-18: **Bounded queue + 비재귀** — overflow drop 정책을 registry 에 명시, telemetry 실패를 동일 sink 로 재귀 전송하지 않음. 근거: hub §11.2, §16.4.
|
||||
- 2026-07-18: **`FE-REG-TELEMETRY` single SSOT** — 자유 문자열 event 금지, 초기 5 event 고정. 근거: hub §5.8, §5.1, FE-D018.
|
||||
- 2026-07-18: **Trace correlation 은 조건부 전파** — backend contract 가 허용할 때만 traceparent 전파, 아니면 local operation ID 로 degrade. 근거: hub §11.3, §8.1.
|
||||
- 2026-07-18: **Delivery transport 는 adapter-owned·degradable** — dev console sink, prod endpoint invalid 시 telemetry 만 degrade. 근거: hub §11.2, §4.2, §16.4.
|
||||
- 2026-07-18: **실패→telemetry 매핑은 bounded·safe** (`FE-OC-008` 기여) — terminal 실패당 safe field 만으로 최대 1 event, abort 는 error event 미발생, `TELEMETRY_FAILURE` 재귀 금지. 근거: hub §8.2, §8.1.
|
||||
- 2026-07-20: **`TELEMETRY_ENABLED` 는 composition-root 단일 지점의 kill-switch** — flag 가 `false`(hub 기본값)면 real adapter 를 **아예 구성하지 않고** no-op `TelemetryPort` 를 주입한다. queue·redaction·sink·counter 가 전혀 생성되지 않으므로 disable 은 "전송 억제"가 아니라 "경로 부재"다. flag 는 boot-time runtime config 이므로 in-session flip 은 없고, 다음 boot 에 반영된다. 근거: hub §5.4(`TELEMETRY_ENABLED` runtime·required·default `false`), §16.4 Mitigation("telemetry runtime flag disable"), §6.3 boot sequence, §4.5 composition root.
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
> 모든 Supporting Claim 은 hub project decision. `[[...operational-contract]]` (project link) 옆의 `FE-D###`·`§n` 은 consistency hook 상 project 링크로 안전하게 검증된다.
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | Telemetry 는 best-effort — render·API critical path 를 block 하지 않고 sink failure 가 UI 를 실패시키지 않는다 (`FE-OC-014`) | product telemetry 는 best-effort default 유지. regulated audit event 처럼 delivery guarantee 가 필요하면 best-effort 와 분리된 **별도 audit channel 계약** 신설 (FE-D021 revisit trigger) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §11.2 | `project-decision` | repository 부재 — telemetry throw 가 render/API success 를 깨지 않음을 증명하는 non-blocking test 필요 |
|
||||
| D2 | Redaction — low-cardinality allowlist context 만 전송, forbidden attribute(token·email·raw URL/query/body·storage value·stack) 전송 금지 | allowlist 가 invariant(accepted-documented-only). 신규 attribute 는 registry 추가 전 low-cardinality + non-PII 검토 통과 시에만 허용; 실패하면 forbidden 분류 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §11.1 §5.8 | `project-decision` | redaction 은 caller 가 아니라 transport boundary(adapter)에서 강제해야 함 — forbidden-attribute scan test 로 leakage 0 증명 필요 |
|
||||
| D3 | Bounded queue + overflow drop 정책 registry 명시 + telemetry 실패 비재귀 보고 | queue 는 항상 bounded. drop 방향(oldest vs newest)은 event class 별 registry 선언값 — 미선언 시 기본 oldest-drop (§구현 가이드 4 의 `UNSUPPORTED_IMPL_DECISION`) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §11.2 §16.4 | `project-decision` | queue 상한 크기 미확정 — memory/queue drain test 로 bound 내 drain 증명 필요 |
|
||||
| D4 | `FE-REG-TELEMETRY` 가 event/attribute/redaction 의 single SSOT; 자유 문자열 event 금지; 초기 5 event(`app.boot.failed`·`api.request.failed`·`ui.render.failed`·`release.mismatch.detected`·`telemetry.delivery.dropped`) 고정 | registry-owned 유지. code generation SSOT 채택이 FE-D018 revisit trigger. `ui.render.failed` trigger 는 React boundary catch (`REACT-UI-C1` 이 presentation=React 구성을 뒷받침) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D018 §5.8 §5.1; [[raw/official-docs/react-ui-library-official]] REACT-UI-C1 | `project-decision` | registry schema validation test(자유 문자열 event reject) 필요; event 별 required attribute 가 실제 발생 지점에서 수집 가능한지 미검증 |
|
||||
| D5 | Trace correlation — W3C `traceparent` 는 backend contract 허용 시에만 전파, requestId/traceId 는 safe internal reference 로 보관, raw trace header user 미노출, 미지원 backend 는 local operation ID 로 degrade, retry 는 같은 logical operation correlation 유지하되 attempt 구분 | backend contract 가 traceparent 지원 → 전파; 미지원 → local operation ID 로 degrade | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §11.3 §8.1 | `conditional-default` | 전파는 backend contract 의존(외부); W3C Trace Context 표준 미아카이브 → 표준 세부는 사실 인용 불가; retry 간 correlation(같은 op, distinct attempt) test 필요 |
|
||||
| D6 | Delivery transport 는 adapter-owned·degradable — local/dev console-safe sink, prod endpoint 부재/invalid 면 telemetry 만 degrade 하고 app 계속, page-hide `sendBeacon` 은 adapter decision 이며 delivery guarantee 아님 | local/dev → console sink; prod → endpoint sink; page-hide `sendBeacon` 은 optional(no guarantee) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §11.2 §4.2 §16.4 | `project-decision` | endpoint invalid boot path 가 telemetry 만 degrade(app 계속)함을 증명하는 sink-failure test 필요 |
|
||||
| D7 | 실패→telemetry 매핑은 bounded·safe (`FE-OC-008` 기여) — §8.2 각 terminal normalized failure 는 safe field(status group·attempt bucket·route ID)만으로 최대 1 event, abort 는 error event 미발생, `TELEMETRY_FAILURE` 는 재귀 금지 | normalized error taxonomy 는 error-classification branch(`FE-OC-008`) 소유 — 본 브랜치는 그 kind 를 consume 해 telemetry rule column 만 구현. taxonomy 가 바뀌면 매핑 갱신 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §8.2 §8.1 | `project-decision` | `error_kind` registry 소유는 error-classification boundary branch(`FE-OC-008`) — 그 registry 미확정 시 매핑 draft 상태 |
|
||||
| D8 | `TELEMETRY_ENABLED` kill-switch 는 composition root 단일 지점에서 소비 — `false`(hub default)면 real adapter 미구성 + no-op `TelemetryPort` 주입(queue·redaction·sink·counter 모두 미생성), `true` 면 D6 delivery ladder 진입. flag 는 boot-time 값이므로 in-session flip 없음(다음 boot 반영), 따라서 flip 시 stranded queue 문제가 정의상 발생하지 않음. `FE-RB-004` mitigation "telemetry runtime flag disable" 은 이 경로로 실행된다 | flag `false` → no-op(관측 0, 부작용 0); `true` → 정상 경로. call-site 조건 분기(`if (telemetry)`)나 port null 주입은 채택하지 않음 — hub §4.2 상 presentation/use-case 는 `TelemetryPort` 만 참조하므로 disable 이 call site 로 새면 안 됨 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §5.4 §16.4 §6.3 §4.5 §11.2 | `project-decision` | no-op vs 미구성의 *구현 형태* 는 hub 미명시(구현 가이드 7 의 `UNSUPPORTED_IMPL_DECISION`); flag off 상태에서도 product e2e 가 동일해야 함을 증명하는 both-state test 필요 |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> `planned` blueprint (프론트엔드 코드 부재). 경로는 hub §4.6 Planned directory blueprint + §5.1 registry owner map 에서 도출된 `planned` anchor 이며 repository 생성 시 변경될 수 있다. 3-rule (R1 Trace 필수 / R2 UNSUPPORTED_IMPL_DECISION / R3 no OUT_OF_BRANCH_SCOPE) 준수.
|
||||
|
||||
### 1. TelemetryPort + adapter + composition-root wiring
|
||||
|
||||
> **Trace**: D1 + D6 · `FE-OC-014` · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §4.2 §4.6
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: port method 표면(`record(event)` 단일 vs `record`+`flush`+`shutdown`) 과 파일명은 hub 미명시 → 최소 표면(`record` only)을 초기 default 로 제안. trade-off: 최소 표면은 오용 여지가 적으나 page-hide flush 를 adapter 내부로 숨겨야 함.
|
||||
|
||||
| 요소 | Planned 경로 | 책임 | MUST NOT |
|
||||
|---|---|---|---|
|
||||
| `TelemetryPort` (application-owned interface) | `src/application/ports/` | use-case/presentation 이 부르는 telemetry 계약 정의 | 구현·browser transport·UX 결정 |
|
||||
| telemetry adapter | `src/adapters/telemetry/` | queue·redaction·sink 구현, port 구현 | navigation/UX 결정 (hub §4.2) |
|
||||
| composition root | `src/bootstrap/composition-root.js` | runtime config(`TELEMETRY_ENABLED` + endpoint)로 **real adapter 또는 no-op port** 를 생성·주입 (kill-switch 단일 지점 — 7 참조) | business rule, call-site 조건 분기 |
|
||||
|
||||
- presentation/use-case 는 `TelemetryPort` 만 참조하고 transport 를 직접 부르지 않는다 (hub §4.2 presentation MUST NOT own telemetry transport).
|
||||
- adapter 는 endpoint 값을 runtime config 에서 주입받는다 (config 로딩은 env-runtime-config branch 소유 — §엣지·실패·의존).
|
||||
|
||||
### 2. `FE-REG-TELEMETRY` registry
|
||||
|
||||
> **Trace**: D4 · `FE-REG-TELEMETRY` · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D018 §5.8 §5.1
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — 스키마 필드와 초기 event 집합은 hub §5.8 을 그대로 채택(신규 제안 아님).
|
||||
|
||||
Planned 경로: `src/contracts/telemetry.js` (single owner: 본 브랜치, hub §5.1).
|
||||
|
||||
**registry 최소 스키마(7-field)의 owner 는 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.8 이다** — "§5.8 그대로" 라고 스스로 밝혔듯 복제였으므로 걷어낸다. 요약 한 줄: event 는 `eventName`·`trigger`·`requiredAttributes`·`optionalAttributes`·`forbiddenAttributes`·`sampling`·`delivery` 를 모두 갖고, required attribute 는 low-cardinality 만 허용한다.
|
||||
|
||||
초기 5 event 의 **정의(trigger + required attributes)는 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.8 소유**다. 본 절은 그 event 를 *어디서 emit 하는가* 만 정한다 — 정의를 옮겨 적으면 hub 가 attribute 를 바꿀 때 이 표가 조용히 낡는다(실제로 `attempt_count` → `attempt_count_bucket` rename 을 놓쳤었다).
|
||||
|
||||
| Event | 본 브랜치의 emit 지점 |
|
||||
|---|---|
|
||||
| `app.boot.failed` | boot config/release validation 실패 경로 |
|
||||
| `api.request.failed` | API client 의 terminal normalized failure 반환 직전 |
|
||||
| `ui.render.failed` | render recovery boundary 의 catch 핸들러 |
|
||||
| `release.mismatch.detected` | release check 가 mismatch 를 확정한 지점 |
|
||||
| `telemetry.delivery.dropped` | 본 브랜치 sink adapter 의 queue drop 경로 |
|
||||
|
||||
- 자유 문자열 event 전송 금지 (hub §5.1 ad hoc use failure). registry 미등록 event 는 build/test 에서 reject.
|
||||
|
||||
### 3. Redaction 강제
|
||||
|
||||
> **Trace**: D2 · `FE-OC-014` · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §11.1 §5.8
|
||||
>
|
||||
> - **해소됨(2026-07-21) — 근거 있는 결정**: redaction 메커니즘은 hub 가 정한다. [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §11.1 이 "이 목록은 **exhaustive default-deny allowlist**다 … 목록 밖 attribute 는 transport boundary 에서 제거된다" 로 메커니즘(default-deny allowlist projection)과 강제 지점(transport boundary)을 모두 명시했다. 본 브랜치가 고른 trade-off 가 아니므로 `UNSUPPORTED_IMPL_DECISION` 라벨을 뗀다.
|
||||
|
||||
- **허용/금지 attribute 어휘의 owner 는 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §11.1** 이며 exhaustive default-deny allowlist 다. 목록을 여기에 옮겨 적지 않는다 — 옮겨 적은 사본이 hub 보다 짧으면 본 절 §2 가 선언한 event(`release.mismatch.detected` 의 `active_release_id`·`mismatch_kind`, `telemetry.delivery.dropped` 의 `reason`·`queue_size_bucket`)가 transport boundary 에서 전부 제거되어 계약이 자기모순에 빠진다.
|
||||
- 본 브랜치가 소유하는 것은 *강제 방법* 이다: redaction 은 adapter 의 transport boundary 에서 수행하고 caller 를 신뢰하지 않는다. forbidden-attribute scan test 가 emit payload 를 검사해 위반 시 실패(§검증).
|
||||
|
||||
### 4. Bounded queue + overflow + degradation ladder + 비재귀
|
||||
|
||||
> **Trace**: D3 + D6 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §11.2 §16.4
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: (a) queue 상한 크기, (b) 기본 drop 방향(oldest vs newest), (c) page-hide `sendBeacon` 사용 여부는 hub 미명시 → 초기 default 로 **oldest-drop + 유한 상한(초기 제안값, memory test 로 확정)** 제안, `sendBeacon` 은 adapter 내부 optional. trade-off: oldest-drop 은 최신 event 를 보존하나 boot 초기 event 를 잃을 수 있음.
|
||||
|
||||
| 단계 | 동작 | 근거 |
|
||||
|---|---|---|
|
||||
| 정상 | bounded queue 적재 → sink flush | §11.2 |
|
||||
| overflow | drop 정책(registry 선언; 기본 oldest-drop) 적용 + `telemetry.delivery.dropped` self-metric 1건 | §11.2, §5.8 |
|
||||
| sink non-2xx/network 실패 | product flow 계속, console-safe fallback(safe field 한정), 동일 sink 재귀 보고 금지 | §16.4 Containment |
|
||||
| adapter init 실패 | telemetry 만 degrade, app 계속 | §11.2 |
|
||||
| prod endpoint 부재/invalid | telemetry 만 degrade, app 계속 | §11.2 |
|
||||
|
||||
- telemetry failure 를 telemetry 로 재귀 전송하지 않는다 (hub §11.2). `telemetry.delivery.dropped` 는 self-metric 이며 sink 실패의 원인 event 를 다시 sink 로 보내지 않는다.
|
||||
|
||||
**`telemetry.delivery.dropped` 자체의 전달 채널** (비재귀 불변식의 구체화):
|
||||
|
||||
> **UNSUPPORTED_IMPL_DECISION**: hub §5.8 은 `telemetry.delivery.dropped` 의 event *shape* 만 정의하고 그 event 자신이 *어느 채널로* 나가는지는 명시하지 않는다. hub §16.4 Diagnosis evidence 가 요구하는 산출물이 event stream 이 아니라 **"dropped event count"·"queue size bucket"** 이라는 점에 근거해, 아래 counter-우선 채널을 초기 default 로 제안한다. trade-off: counter 는 drop 폭주 시에도 자기 증폭이 없고 §16.4 evidence 형태와 1:1 이지만, 개별 drop 의 시점 분포(timeline)를 잃는다.
|
||||
|
||||
- self-metric 은 **동일 bounded queue 에 재적재(re-enqueue)하지 않는다** — full/dead queue 로 되돌리는 것은 정의상 순환이며 overflow 를 가속한다.
|
||||
- 대신 adapter 내부의 **in-process 단조 counter**(key = `reason` × `queue_size_bucket`, hub §5.8 required attribute 와 동형)로 집계하고, hub §16.4 Containment 의 console-safe fallback(safe field 한정)으로 즉시 관측 가능하게 한다.
|
||||
- 이 counter 는 `FE-RB-004` diagnosis evidence 의 `dropped event count` 로 그대로 공급된다(§6 기여 edge).
|
||||
- sink 가 회복되어 **정상 flush 가 성공한 이후**에 한해, 누적 counter 를 aggregated event 1건으로 승격 전송하는 것은 adapter 의 optional 결정이다 — 실패 중인 sink 로는 시도하지 않으며 delivery guarantee 로 표현하지 않는다 (hub §11.2).
|
||||
- counter 자체는 sink 실패로 소실되지 않아야 하므로 queue 와 독립된 lifetime 을 가진다(document lifetime 한정, 영속화 없음 — 영속화는 storage registry owner 영역).
|
||||
|
||||
### 5. Trace correlation
|
||||
|
||||
> **Trace**: D5 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §11.3 §8.1
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: local operation ID 포맷(예: `operationId#attempt`)은 hub 미명시 → 사용자 선택. trade-off: 짧은 포맷은 로그 가독성↑ 이나 충돌 회피를 위해 request-scoped uniqueness 보장 로직 필요.
|
||||
|
||||
- W3C `traceparent` 는 외부 auth/backend contract 가 허용할 때만 전파 (hub §11.3).
|
||||
- backend 응답의 `requestId`/`traceId` (envelope `meta`, §7.3)는 safe support reference 로 내부 state 보관 가능, user 에 raw 노출 금지.
|
||||
- normalized failure shape(§8.1)의 `requestId`/`traceId` 는 optional — 존재 시 telemetry attribute 로 승격하지 않고 내부 correlation 에만 사용.
|
||||
- retry(new request)는 같은 logical operation correlation 유지하되 `attempt` 로 구분 (hub §11.3, §7.2 `attempt`).
|
||||
- trace propagation 미지원 backend 는 local operation ID 로 degrade.
|
||||
|
||||
### 6. 기여 edge (contribution, ownership 은 위임)
|
||||
|
||||
> **Trace**: D7 · `FE-OC-008` / `FE-OC-021` / `FE-OC-025` 기여 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §8.2 §16.4
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — 아래는 hub 계약 인용이며 각 owner 브랜치에 위임(R3). 본 절은 telemetry 기여 edge 만 명세.
|
||||
|
||||
| 기여 대상 | 본 브랜치가 제공하는 telemetry edge | Owner (위임) |
|
||||
|---|---|---|
|
||||
| `FE-OC-008` 실패 taxonomy | §8.2 Telemetry rule column 구현 — terminal 실패당 safe field 만으로 최대 1 event, abort 는 error event 미발생, raw URL/body 금지 | error-classification boundary branch |
|
||||
| `FE-OC-021` NFR | `duration_bucket`·`attempt_count_bucket` 등 low-cardinality attribute 공급(측정·리포트는 미소유) | web-vitals-performance-budget branch |
|
||||
| `FE-OC-025` runbook | `FE-RB-004` diagnosis evidence field(endpoint classification·queue size bucket·dropped count·build/release ID·redaction test result) + recovery assertion 공급, **및 Mitigation "telemetry runtime flag disable" 의 실행 경로(D8, 구현 가이드 7) 보장** | frontend-operational-runbook branch |
|
||||
|
||||
### 7. `TELEMETRY_ENABLED` kill-switch 소비
|
||||
|
||||
> **Trace**: D8 (+ D1 non-blocking / D6 degradation ladder) · `FE-OC-014` · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §5.4 §16.4 §6.3 §4.5 §11.2
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: hub §5.4 는 `TELEMETRY_ENABLED` 를 required runtime key(default `false`)로 *선언*하고 §16.4 는 그 disable 을 *mitigation lever* 로 *요구*하지만, 소비 형태(real adapter 미구성 + **no-op port 주입** vs port 자체를 optional/null 로 두고 call site 에서 분기)는 명시하지 않는다 → **no-op port 주입** 을 초기 default 로 제안. trade-off: no-op 은 disable 경로를 composition root 한 곳에 가두고 call site 를 flag-무지 상태로 유지하나(hub §4.2 의 "presentation 은 `TelemetryPort` 만 참조" 와 정합), no-op 객체가 항상 존재하므로 "telemetry 가 꺼져 있다"는 사실이 호출자에게 보이지 않는다(관측은 boot-time config snapshot 으로만 확인 가능).
|
||||
|
||||
**flag 상태별 구성 (composition root 분기 지점 1곳)**
|
||||
|
||||
| `TELEMETRY_ENABLED` | composition root 동작 | 생성되는 것 | 생성되지 않는 것 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| `false` (hub default, §5.4) | no-op `TelemetryPort` 주입 | port 표면(`record`)만 | queue · redaction projection · sink/transport · dropped counter · `TELEMETRY_ENDPOINT` 해석 | §5.4, §16.4 Mitigation |
|
||||
| `true` | real telemetry adapter 구성 후 주입 | 구현 가이드 2~5 전체 경로 | — | §11.2, §4.5 |
|
||||
|
||||
- **no-op 의 계약**: `record(event)` 는 인자를 읽지 않고 즉시 반환하며 throw 하지 않는다(D1 non-blocking 불변식을 flag 양쪽 상태에서 동일하게 유지). 어떤 event 도 buffer 하지 않으므로 나중에 flag 가 켜져도 소급 전송되는 event 는 없다.
|
||||
- **disable 은 "전송 억제"가 아니라 "경로 부재"**: queue 도 counter 도 생성되지 않으므로 §11.1 redaction 위반 표면과 §11.2 overflow 표면이 동시에 0 이 된다. `TELEMETRY_ENDPOINT` 는 hub §5.4 상 `Required: conditional` — 그 조건이 곧 `TELEMETRY_ENABLED=true` 라는 것이 본 브랜치의 소비 측 해석이며, schema 상 conditional 강제는 `FE-OC-004` 소유(§엣지·실패·의존).
|
||||
- **runtime flip 가능성**: runtime config 는 hub §6.3 boot sequence 에서 `GET /config.json` (no-store) 로 **boot 시 1회** 로드된 뒤 §4.5 composition root 가 의존성을 구성한다. hub 에 config hot-reload 계약이 없으므로 **in-session flip 은 존재하지 않는다** — flag 변경은 provider 측에서 반영한 뒤 **다음 document load(boot)** 부터 적용된다.
|
||||
- **flip 시 이미 queue 에 쌓인 event**: 위 결과로 정의상 문제가 발생하지 않는다. `true`→`false` 는 이전 session 의 queue 를 flush 하지 않고 document 와 함께 폐기하며(§11.2 best-effort — delivery guarantee 없음이므로 손실이 계약 위반이 아님), `false`→`true` 는 시작 시점부터의 event 만 다룬다(no-op 이 아무것도 보관하지 않았으므로 backfill 대상 없음).
|
||||
- **`FE-RB-004` mitigation 충족 경로**: §16.4 Mitigation 의 "telemetry runtime flag disable" 은 ① provider 의 runtime config 에서 `TELEMETRY_ENABLED=false` 설정 → ② 이후 boot 부터 no-op 주입 → ③ sink 호출·queue 적재·drop counter 증가가 **발생 원천에서** 중단 → ④ §16.4 Containment("product flow 계속")와 Recovery assertion("product e2e unaffected")이 flag 양쪽 상태에서 동일하게 성립, 의 순서로 실행된다. 이 lever 는 sink restore 없이도 즉시 사용 가능한 격리 수단이다.
|
||||
- **invalid value**: hub §5.4 failure column 은 `TELEMETRY_ENABLED` invalid 를 **boot fail** 로 규정하고 §6.4 는 "boolean parsing without truthy string ambiguity" 를 요구한다. 따라서 composition root 는 **검증된 boolean** 만 받으며 `"false"` 같은 문자열을 스스로 해석하지 않는다(파싱·거부는 `FE-OC-004`). endpoint 부재/invalid 의 **telemetry degrade**(§5.4)와 달리 flag invalid 는 degrade 가 아니라 boot fail 이라는 비대칭에 유의.
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
> R4 캡처. sibling 브랜치 링크는 소유 계약 `FE-OC-###` 로만 참조(consistency hook 안전).
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- sink non-2xx/network 실패 → `TELEMETRY_FAILURE`(hub §8.2), product error 없음, console-safe/drop, 재귀 금지.
|
||||
- queue overflow → 정책(기본 oldest-drop) 적용 + `telemetry.delivery.dropped` self-metric, unbounded 적재 금지 (§16.4).
|
||||
- telemetry adapter init 실패 / prod endpoint 부재·invalid → telemetry 만 degrade, app 계속 (§11.2).
|
||||
- redaction miss(forbidden attribute 유출) → forbidden-attribute scan test 가 build 를 실패시켜야 함 (§검증).
|
||||
- page hide → `sendBeacon` best-effort, delivery guarantee 로 표현 금지 (§11.2).
|
||||
- backend 가 `traceparent` 미지원 → local operation ID 로 degrade (§11.3).
|
||||
- `TELEMETRY_ENABLED=false` (hub §5.4 기본값) → real adapter 미구성, no-op port 주입, network·queue·counter 전부 부재. app 은 정상 동작하며 `FE-RB-004` mitigation lever 로 사용 (구현 가이드 7).
|
||||
- `TELEMETRY_ENABLED` invalid → **boot fail** (§5.4, degrade 아님). 파싱·거부는 `FE-OC-004` 소유이며 telemetry adapter 는 검증된 boolean 만 수신.
|
||||
- flag `true`→`false` 전환 → 이전 session queue 는 flush 되지 않고 폐기 (§11.2 best-effort, delivery guarantee 없음). in-session flip 은 §6.3 boot-time config 로딩상 존재하지 않으며 다음 boot 부터 반영.
|
||||
- navigation/user abort(`REQUEST_ABORTED`, §8.2) → error telemetry event 미발생(interaction-only).
|
||||
- **다른 계약 의존**:
|
||||
- [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] (`FE-OC-004`) — `TELEMETRY_ENABLED`(boolean, required, default `false`) 와 `TELEMETRY_ENDPOINT`(conditional) 를 runtime config 로 consume. 그 registry 는 key 선언·분류·schema 검증만 소유하고 **전송·redaction·kill-switch 소비 메커니즘은 본 브랜치 소유**(D8, 구현 가이드 7). config 계약이 바뀌면 flag 해석·endpoint 해석·boot degrade 경로 영향. (hub §20 Dependency 가 본 브랜치의 유일 명시 dependency 로 이 브랜치를 지목.)
|
||||
- [[raw/branch-notes/feature-routing-navigation-guard-contract]] (`FE-OC-005`) — `route_id` 의 **producer**. `FE-REG-ROUTE` 가 low-cardinality route ID 를 발급하며, telemetry 는 `ui.render.failed`·`api.request.failed` 의 required attribute 로 그 값을 그대로 소비한다(직접 생성·정규화 금지). route ID 어휘가 바뀌면 event attribute cardinality 가 영향받음.
|
||||
- [[raw/branch-notes/feature-api-client-response-envelope-contract]] (`FE-OC-006`) — `operation_id`(+`route_id`·`attempt`)의 **producer**. hub §7.2 request context 가 logical request 마다 `operationId`/`routeId`/`attempt` 를 보유하므로, telemetry emit point 는 이 request context 에서 값을 읽고 `attempt` → `attempt_count_bucket` 만 파생한다. request context 필드가 바뀌면 emit point 수집 경로 영향.
|
||||
- [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] (`FE-OC-008`) — `error_kind` 를 consume 해 `api.request.failed` 등 event 의 required attribute 채움. taxonomy 변경 시 매핑 갱신.
|
||||
- [[raw/branch-notes/feature-frontend-render-recovery-boundary-contract]] (`FE-OC-015`) — React boundary-catch 신호를 consume 해 `ui.render.failed` emit. boundary 소유 계약 변경 시 emit point 영향.
|
||||
- [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] (`FE-OC-016`) — `build_id`·`active_release_id`·release token 을 consume 해 `app.boot.failed`·`release.mismatch.detected` attribute 채움.
|
||||
- [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] (`FE-OC-019`) — telemetry endpoint 의 CSP `connect-src`·bundle 내 secret 금지를 강제(그 브랜치가 본 브랜치를 contributor 로 지목).
|
||||
- [[raw/branch-notes/feature-frontend-operational-runbook-contract]] (`FE-OC-025`) — `FE-RB-004` recovery assertion(product e2e unaffected·delivery self-check·queue drains·forbidden-attribute scan pass) 소비.
|
||||
- [[raw/branch-notes/feature-frontend-contract-registry-governance]] (`FE-OC-022`) — `FE-REG-TELEMETRY` 를 8-registry governance 의 single-owner/compatibility check 로 감사.
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
> hub 계약은 근거지만 내 프로젝트 코드의 동작을 자동 보장하지 않는다. repository 생성 후 검증. 모두 `needs-confirmation`.
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| telemetry throw/sink 실패가 render·API critical path 를 깨지 않는다 (D1) | 코드 부재; non-blocking 은 wiring 방식에 의존 | throwing sink 주입 후 render + API success 유지 assert 하는 **sink failure test** (§20 Measurable completion) | `needs-confirmation` |
|
||||
| forbidden attribute 가 client 를 절대 떠나지 않는다 (D2) | redaction 이 transport boundary 에서 강제되는지 코드로 확인 필요 | emit payload 를 검사하는 **forbidden-attribute scan / redaction test**; 위반 시 build 실패 | `needs-confirmation` |
|
||||
| bounded queue 가 planned bound 내 drain 하고 정책대로 drop 한다 (D3) | queue 상한·drop 방향이 UNSUPPORTED_IMPL_DECISION | **memory/queue drain test** (`FE-RB-004` recovery assertion) | `needs-confirmation` |
|
||||
| 자유 문자열/미등록 event 가 reject 된다 (D4) | registry enforcement 미구현 | `FE-REG-TELEMETRY` **schema validation test** | `needs-confirmation` |
|
||||
| retry 간 같은 logical operation correlation 유지 + attempt 구분 (D5) | traceparent 전파는 backend contract 의존 | local operation ID + attempt 구분 **correlation test** (MSW 로 backend 유/무 traceparent 시나리오) | `needs-confirmation` |
|
||||
| prod endpoint 부재/invalid 시 telemetry 만 degrade 하고 app 계속 (D6) | boot 경로에서 degrade 격리 미검증 | invalid endpoint **boot/sink-failure matrix test** | `needs-confirmation` |
|
||||
| `TELEMETRY_ENABLED=false` 에서 network 요청·queue·counter 가 전혀 생성되지 않고 product e2e 가 flag `true` 와 동일하다 (D8) | no-op 주입이 composition root 한 곳에만 있는지, call site 로 새지 않는지 코드로 확인 필요 | flag off/on **both-state test** — off 상태에서 telemetry 관련 network 호출 0건 assert + `FE-RB-004` recovery assertion("product e2e unaffected") 양쪽 상태 재실행 | `needs-confirmation` |
|
||||
| `telemetry.delivery.dropped` self-metric 이 실패한 queue/sink 로 재진입하지 않는다 (D3 + 구현 가이드 4) | counter 채널이 queue 와 독립 lifetime 인지 미검증 | overflow 유발 후 **비재귀 test** — queue 재적재 0건 assert + dropped counter 가 `FE-RB-004` diagnosis evidence 로 노출되는지 확인 | `needs-confirmation` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
- 없음 — scaffolding/spec 단계 (구현 전).
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-OC-008@1` | [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] | 모든 failure는 stable frontend error kind로 MUST 정규화하고 raw body·stack을 UI에 노출하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-015@1` | [[raw/branch-notes/feature-frontend-render-recovery-boundary-contract]] | expected operational error와 render defect를 MUST 분리하고 reload loop를 금지 | import 참조로 적용 |
|
||||
| `FE-OC-021@1` | [[raw/branch-notes/feature-web-vitals-performance-budget-contract]] | NFR은 device/network/cache/build context와 함께 MUST 측정 | import 참조로 적용 |
|
||||
| `FE-OC-025@1` | [[raw/branch-notes/feature-frontend-operational-runbook-contract]] | boot, chunk mismatch, API degradation, telemetry failure, rollback runbook을 MUST 유지 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
### Sub-branches (세부 작업)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
### 오류 기록 (이 branch 작업 중 발생)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
### 강의 (이 작업을 위해 학습한 강의)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
### job-posting tie-ins (이 작업에서 파생된 글감)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크: TODO
|
||||
- 리뷰 메모: TODO
|
||||
- 머지 결과 / 배포 환경: TODO
|
||||
- **wiki 추출 대상**: 없음 — 전 항목 `planned` (repository 부재, 추출 조건 미충족)
|
||||
- **추출하지 않을 항목**: D1~D8 전부 — `planned` 등급이므로 verified 승급 및 wiki/projects 추출 전까지 제외
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
---
|
||||
title: branch / feature-frontend-operational-runbook-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-026
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-026
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-DEPLOYMENT-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-TELEMETRY-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-024, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-005, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-012, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-004]
|
||||
contract_packet: 1
|
||||
branch: feature-frontend-operational-runbook-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract]
|
||||
tags: [branch, ca-skeleton, frontend, runtime, observability, externalized-config]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
contract_packet_sha256: f1bc0e83f35bfe8d37d36486dffefddcbdd308cb9b5820c5945b54a3ba163e8e
|
||||
imports: [FE-GATE-014@1, FE-GATE-015@1, FE-OC-001@1, FE-OC-004@1, FE-OC-006@1, FE-OC-009@1, FE-OC-014@1, FE-OC-016@1, FE-OC-017@1, FE-OC-023@1, FE-OC-026@1]
|
||||
---
|
||||
|
||||
# branch: feature-frontend-operational-runbook-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — TODO·결정·진행 기록. 구현 결과는 검증 뒤 `/ingest`로만 추출한다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
[[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: 5개 drill의 trigger·window·escalation·evidence assertion이 검증된다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-DEPLOYMENT-001@1` | static release는 immutable release directory와 atomic active pointer로 배포한다 | release mismatch와 rollback runbook의 trigger·recovery assertion에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-TELEMETRY-001@1` | telemetry는 best-effort queue와 redaction을 사용하며 sink failure가 UI를 실패시키지 않는다 | telemetry sink failure runbook의 containment와 evidence에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
|
||||
<!-- section-id: branch-local-decisions -->
|
||||
### 브랜치 지역 결정
|
||||
|
||||
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
||||
|---|---|---|---|---|
|
||||
| D1 | 다섯 operational runbook을 4-assertion 계약으로 관리한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D2 | window와 rate를 planned conditional-default로 라벨한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D3 | deterministic drill과 record evidence로 runbook을 검증한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D4 | escalation을 technical owner에서 platform·approver로 이어지는 고정 chain으로 둔다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D5 | recovery는 action 수행이 아니라 assertion evidence로 판정한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
|
||||
<!-- section-id: declared-overrides -->
|
||||
### 선언한 예외
|
||||
|
||||
| Override ID | Overrides | Reason | Approval | Status |
|
||||
|---|---|---|---|---|
|
||||
|
||||
없음.
|
||||
<!-- GENERATED: branch-contract:end -->
|
||||
|
||||
<!-- section-id: branch-goal -->
|
||||
## 목표
|
||||
|
||||
이 브랜치는 project-wide 계약 `FE-OC-025`("boot, chunk mismatch, API degradation, telemetry failure, rollback runbook을 MUST 유지, evidence = drill records")를 *구현 착수 가능한 runbook 계약*으로 내린다. hub [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §16이 정의한 다섯 runbook(`FE-RB-001`~`FE-RB-005`)을 각각 **trigger 집합 / immediate containment + window / escalation chain / recovery-evidence assertion**의 4-계약으로 고정하고, 이를 `FE-GATE-021`~`FE-GATE-025`(production-promotion drill gate)로 매핑한다. 동시에 boot config(`FE-OC-004`), API degradation(`FE-OC-006`), telemetry sink(`FE-OC-014`), release cache/rollback(`FE-OC-016`·`FE-OC-017`)의 acceptance drill을 *기여*한다. 이 브랜치는 runbook 계약과 drill 증거 스키마만 소유하며, 각 runbook이 소비하는 하부 메커니즘(config load, retry, telemetry queue, release pointer)은 owner 브랜치에 위임한다. 원천 상태가 전부 `planned`(코드 없음, hub §16이 유일 SSOT)이므로 모든 항목 등급은 `planned`.
|
||||
|
||||
- 이슈: 없음 (스캐폴딩 단계)
|
||||
- PR: 없음
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- `FE-OC-025` 소유 항목: 다섯 runbook 계약(`FE-RB-001` boot config / `FE-RB-002` chunk·release-manifest·deploy mismatch / `FE-RB-003` backend API degradation / `FE-RB-004` telemetry sink failure / `FE-RB-005` release rollback)의 trigger·containment+window·escalation·recovery-evidence assertion 고정.
|
||||
- drill harness 계약(`pnpm drill:runbook -- FE-RB-00X` → `artifacts/runbooks/FE-RB-00X/<release-id>/record.json`)과 `FE-GATE-021`~`FE-GATE-025` 매핑 + 각 drill의 negative fixture 요구.
|
||||
- window/rate 값을 `planned conditional-default`(measured SLO 아님)로 라벨하고 재검토 트리거를 명세.
|
||||
- escalation 2-hop chain(technical owner 브랜치 → platform/approver)의 routing 계약.
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외. 다른 owner 브랜치 소유이거나 hosting 확정 이후 항목.
|
||||
|
||||
- boot config load + runtime config schema/validation 메커니즘 → [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] 소유 (`FE-OC-004`).
|
||||
- retry/timeout/idempotency·degradation triage 메커니즘 → [[raw/branch-notes/feature-api-client-response-envelope-contract]] 소유 (`FE-OC-006` · `FE-OC-009`).
|
||||
- telemetry queue/redaction/sink adapter 메커니즘 → [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] 소유 (`FE-OC-014`).
|
||||
- release tuple/cache header/atomic pointer/rollback 메커니즘 → [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] 소유 (`FE-OC-016` · `FE-OC-017`).
|
||||
- drill gate를 CI 파이프라인 blocking stage로 wiring → [[raw/branch-notes/feature-frontend-ci-quality-gates-contract]] 소유 (orchestration 담당이며 `FE-OC-*` owner 는 아니다).
|
||||
- provider-specific console command과 실제 incident response 수행 → hosting 확정(`FE-Q-003`) 이후 release 브랜치가 채움.
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | primary SSOT. §16 다섯 runbook 정의, §14.3 drill 명령/artifact, §15 gate matrix(`FE-GATE-021`~`025`)+negative fixture, §12.5 rollback invariant, §8.2 failure taxonomy — D1~D5 전부의 project-decision 근거. |
|
||||
| [[raw/official-docs/vite-build-tool-official]] | `VITE-C2`(production build가 content-hash 정적 자산을 산출) — chunk/deploy mismatch(`FE-RB-002`)가 *실재 operational failure class*라는 근거(D1). |
|
||||
| [[raw/official-docs/prometheus-alertmanager-silences]] | operational recovery를 "action 수행"이 아니라 시간제한 window + suppression/evidence 규율로 판정하는 cross-domain 공식 precedent — drill window + recovery-evidence 계약(D3)의 참고 근거. frontend 계약 값 자체는 아님. |
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] 다섯 runbook의 4-assertion 계약(trigger / containment+window / escalation / recovery-evidence)을 표로 고정 — 등급: `planned`
|
||||
- [ ] drill harness 계약(`pnpm drill:runbook -- FE-RB-00X` → `record.json`) + record 스키마 초안 정의 — 등급: `planned`
|
||||
- [ ] `FE-GATE-021`~`FE-GATE-025` 매핑 + 각 runbook의 negative fixture(고의 실패 drill) 정의 — 등급: `planned`
|
||||
- [ ] window/rate 값 `planned conditional-default` 라벨 + 재검토 트리거(첫 drill + baseline) 명세 — 등급: `planned`
|
||||
- [ ] escalation 2-hop chain을 owner 브랜치 위임 링크로 고정 — 등급: `planned`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
- Ground truth: frontend 코드/repo 없음. 다섯 runbook의 trigger·window·assertion은 전부 hub §16의 `planned conditional-default`이며 measured SLO가 아니다. 이 브랜치는 hub §16을 재진술이 아니라 *drill-backed 계약 + gate 매핑*으로 내린다.
|
||||
- window 값(5분 triage, rolling 5분 rate window, 10/15분 등)은 첫 drill 결과 + hosting/backend baseline이 생길 때까지 owner가 유지·변경. 외부 답변에서 이 값을 달성 SLO처럼 말하면 §22 answer-boundary 위반.
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> 각 결정의 근거는 Sources 또는 hub §-ref. 대안과 함께 기록.
|
||||
|
||||
- 2026-07-19: hub §16이 정의한 다섯 runbook을 `FE-OC-025` 소유 집합으로 채택하고 각각 4-assertion(trigger/containment+window/escalation/recovery-evidence)으로 고정 / 이유: `FE-OC-025`의 minimum evidence가 drill records이므로 runbook을 검증 가능한 계약으로 내려야 함 / 검토한 대안: HTTP status별 개별 runbook 세분화 / 근거: hub §16 · §8.2 · `VITE-C2`.
|
||||
- 2026-07-19: 모든 window/rate 값을 `planned conditional-default`(measured SLO 아님)로 라벨 / 이유: implementation/telemetry evidence 없음(hub §16 서두 명시) / 검토한 대안: 초기 값을 target SLO로 선언 / 근거: hub §16 서두 · `FE-OC-001`·`FE-OC-026`.
|
||||
- 2026-07-19: runbook 검증은 결정론적 drill harness(`pnpm drill:runbook`) + evidence record + `FE-GATE-021`~`025` + runbook별 negative fixture로 수행 / 이유: rule 존재만으론 `locally-verified` 부족(§15.2) / 검토한 대안: 수동 체크리스트 review / 근거: hub §14.3 · §15.
|
||||
- 2026-07-19: escalation은 runbook별 고정 2-hop chain이며, 하부 메커니즘은 owner 브랜치에 위임(R3) / 이유: runbook 브랜치는 routing+evidence 계약만 소유 / 검토한 대안: 메커니즘까지 runbook에 재명세 / 근거: hub §16 escalation rows · §20 dependency · §4.3.
|
||||
- 2026-07-19: recovery는 assertion evidence(reachability probe/e2e/self-check)로만 판정하며 "mitigation action 수행"으로 판정하지 않음 / 이유: cache purge 완료≠recovery(hub §12.5) / 검토한 대안: provider action 완료를 recovery로 간주 / 근거: hub §12.5 · §16 recovery assertions.
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | 다섯 runbook(`FE-RB-001`~`005`)을 `FE-OC-025` 소유 집합으로 채택, 각각 trigger/containment+window/escalation/recovery-evidence 4-assertion으로 고정 | §8.2 failure taxonomy의 *operational(비-request) failure class*가 이 다섯에 매핑되는 한 이 집합 유지 / §8.2에 어느 runbook에도 안 담기는 owner-blocking operational class가 새로 생기면 runbook 추가·분할. HTTP status별 개별 runbook은 만들지 않음(request-level은 §8.2 failure matrix가 처리) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-OC-025 · FE-D019 · FE-D020 · FE-D023 · §16 · §8; [[raw/official-docs/vite-build-tool-official]] VITE-C2 | `project-decision` + `official-doc` (VITE-C2) | §8.2에 다섯이 못 덮는 operational class가 나타날 수 있음 — 집합 완전성은 현재 taxonomy 기준으로만 주장됨 |
|
||||
| D2 | 모든 window/rate 값을 `planned conditional-default`로 라벨(measured SLO 아님), 첫 drill 결과 + hosting/backend baseline 전까지 유지 | baseline·첫 drill 이전엔 documented window(default) 유지 / (a) 해당 runbook 첫 drill의 timing evidence 와 (b) hosting/backend baseline SLO 가 둘 다 생기면 owner가 measured target으로 교체. 그 전까지 이 값을 달성 SLO로 인용하면 answer-boundary 위반 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §16(서두: window=planned conditional default, measured SLO 아님) · FE-OC-001 · FE-OC-026 | `conditional-default` | window가 첫 drill에서 달성 불가로 판명될 수 있고, downstream 문서가 이를 SLO로 오인 인용할 위험 |
|
||||
| D3 | 검증은 결정론적 drill harness(`pnpm drill:runbook -- FE-RB-00X`) + `record.json` evidence + `FE-GATE-021`~`025` + runbook별 negative fixture | drill record + negative fixture(깨진 경로에서 실제 실패 증명)가 둘 다 있을 때만 runbook을 operational로 주장 / repo/harness 없으면 runbook은 `documented-only`(drill=`PLANNED_NOT_EXECUTED`, §14.3) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §14 · §15(FE-GATE-021~025 · negative fixture) · FE-OC-025; [[raw/official-docs/prometheus-alertmanager-silences]] (recovery-evidence 규율 precedent) | `project-decision` + `official-doc` precedent | `record.json` 필드 스키마를 hub가 정의하지 않음(§구현 가이드 2의 UNSUPPORTED_IMPL_DECISION) |
|
||||
| D4 | escalation은 runbook별 고정 2-hop chain(technical owner 브랜치 → platform/approver), 하부 메커니즘은 owner 브랜치 위임(R3) | 이 브랜치는 escalation routing + evidence assertion만 명세 / 메커니즘 detail(retry cap·config schema·cache header·atomic pointer)은 owner 브랜치 FE-OC 계약으로 위임하고 여기서 재명세 금지. 기존 owner 브랜치가 제공 못하는 escalation hop이 필요할 때만 재검토 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §16(escalation rows) · §20(dependency) · §4 · FE-OC-025 | `project-decision` | rollback 결정 주체(config owner↔release owner) hand-off가 모호하면 runbook이 단절될 수 있음(FE-RISK-002) — §16이 hand-off를 고정하나 drill 전까지 미검증 |
|
||||
| D5 | recovery는 assertion evidence(clean boot·asset 2xx·reachability probe·critical e2e·telemetry self-check·forbidden-attribute scan)로만 판정, "action 수행"으로 판정 금지; provider console command은 hosting 확정까지 유보 | 항상 evidence 기반 / cache purge 필요한 provider는 purge 완료가 아니라 실제 old/new reachability probe 결과로 recovery 판정(§12.5). provider console command은 hosting 확정(FE-Q-003) 후 release 브랜치가 채움 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §12 · §16(recovery assertions · "provider console command 발명 안 함") · FE-OC-017 | `project-decision` | `FE-RB-005`의 provider-dependent recovery target은 hosting 확정 전 `TBD`(FE-Q-003) |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> `planned` blueprint. 코드 없음 — 경로/명령은 hub §14.3 blueprint(`pnpm drill:runbook`, `artifacts/runbooks/...`)에서 유래하므로 근거가 있으나 전체 섹션은 `planned`.
|
||||
|
||||
### 1. 다섯 runbook의 4-assertion 계약
|
||||
|
||||
> **Trace**: D1 · D2 · D5 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-OC-025 · §16 · §8
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음. 아래 trigger·window·assertion·escalation 값은 전부 hub §16에서 그대로 내려받았고, 임의 발명 값이 없다. window는 §16이 명시한 `planned conditional-default`이므로 measured SLO로 표기하지 않는다(D2).
|
||||
|
||||
| Runbook | Trigger(정규화 kind) | Immediate containment + window(planned-default) | Escalation 1-hop | Recovery-evidence assertion | Drill gate |
|
||||
|---|---|---|---|---|---|
|
||||
| `FE-RB-001` boot config | `BOOT_CONFIG_FAILURE`(config non-2xx/parse/schema, refetch 1회도 실패) | product route mount 중단 + safe support shell, auto refetch ≤1회; owner triage 목표 5분 | env-config owner → release owner | clean session boot 성공 · product root mount · config validation artifact pass · 반복 boot error telemetry 없음 | `FE-GATE-021` |
|
||||
| `FE-RB-002` chunk/manifest/deploy mismatch | `CHUNK_LOAD_FAILURE` · `RELEASE_MANIFEST_FAILURE` · `DEPLOY_MISMATCH`(asset 404/integrity, manifest active≠loaded) | dirty-state 경고 후 manifest `no-store` 1회 조회; mismatch면 reload guard 기록 후 reload 1회만; release owner triage 5분 | release-cache owner → hosting/CDN owner | entry+lazy asset 2xx · manifest fetch·parse·schema+tuple coherence pass · 2차 auto reload 없음 · release coherence gate pass · route e2e pass | `FE-GATE-022` |
|
||||
| `FE-RB-003` API degradation | terminal network/timeout/5xx rate > threshold(rolling 5분) 또는 `SCHEMA_MISMATCH` 1건 | retry cap runtime 확대 금지 · safe cache는 stale-degraded 제공 · mutation은 idempotency 없이 retry 금지 · schema mismatch는 retry 금지; 최초 분류 10분 | api-client owner → backend operation owner → release compatibility owner | terminal failure rate가 baseline window로 복귀 · retry amplification 없음 · critical read/write e2e pass · schema fixtures pass | `FE-GATE-023` |
|
||||
| `FE-RB-004` telemetry sink | `TELEMETRY_FAILURE`(sink non-2xx/network, queue overflow, adapter init 실패) | product flow 유지 · bounded queue 초과 적재 금지 · 동일 sink 재귀 보고 금지 · console fallback은 safe field 한정; platform triage 15분 | observability owner → telemetry platform owner | product e2e 영향 없음 · delivery self-check 성공 · queue가 planned bound 내 drain · forbidden-attribute scan pass | `FE-GATE-024` |
|
||||
| `FE-RB-005` release rollback | release-blocking boot/chunk/render/API/security defect이고 forward fix가 incident window 내 안전 미증명 | prior immutable release로 target tuple 선택 → asset·config·API compat 확인 → active pointer atomic switch → smoke; provider recovery target은 hosting 전 `TBD` | release-cache owner → release approver/hosting owner | `FE-GATE-014`·`FE-GATE-015` pass · critical e2e pass · 반복 `DEPLOY_MISMATCH` 없음 · incident timeline에 release ID 기록 | `FE-GATE-025` |
|
||||
|
||||
### 2. Drill harness + evidence record
|
||||
|
||||
> **Trace**: D3 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §14 · §15 · FE-OC-025
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: `record.json` 필드 스키마 — hub §14.3은 artifact *경로*(`artifacts/runbooks/FE-RB-00X/<release-id>/record.json`)만 고정하고 JSON 필드는 정의하지 않음. 아래 필드 집합은 이 브랜치의 임의 제안(trade-off: assertion 결과를 기계 판정 가능하게 최소 필드만 고정 — 확장은 owner drill 구현 시). 실제 필드명은 harness 구현 시 확정.
|
||||
|
||||
- 명령: `pnpm drill:runbook -- FE-RB-00X` (hub §14.3, 상태 `PLANNED_NOT_EXECUTED`).
|
||||
- 산출물: `artifacts/runbooks/FE-RB-00X/<release-id>/record.json` (hub §14.3).
|
||||
- 제안 record 필드(planned, UNSUPPORTED_IMPL): `runbookId`, `releaseId`, `drillTimestamp`, `triggerInjected`(주입한 정규화 kind), `containmentAsserted`(bool), `escalationPathAsserted`(2-hop 도달 여부), `recoveryAssertions`(assertion→pass/fail 목록), `negativeFixtureFailedAsExpected`(bool), `windowObservedBucket`(planned-default 비교용 bucket, SLO 아님).
|
||||
- Negative fixture(runbook별 고의 실패 drill, §15.2 규율):
|
||||
|
||||
| Runbook | Negative fixture(반드시 실패해야 함) | 근거 |
|
||||
|---|---|---|
|
||||
| `FE-RB-001` | 유효 config인데 boot을 mount 실패로 처리 → recovery assertion이 fail 나야 정상 | §15.2 runtime schema/reload 계열 |
|
||||
| `FE-RB-002` | 동일 release pair에서 2차 chunk 실패 → reload guard가 반복 reload를 막아야(§15.2 reload guard) | §15.2 reload guard |
|
||||
| `FE-RB-003` | idempotency key 없는 POST가 503 수신 → 자동 retry 하면 fail | §15.2 retry |
|
||||
| `FE-RB-004` | telemetry event에 raw URL/query 포함 → forbidden-attribute scan이 fail 나야 | §15.2 telemetry |
|
||||
| `FE-RB-005` | HTML build A + asset manifest B(mixed) → release coherence가 mismatch 검출해야 | §15.2 release |
|
||||
|
||||
### 3. Escalation & delegation map (R3 경계)
|
||||
|
||||
> **Trace**: D4 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §16 · §20 · §4
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음. 각 hop의 owner 브랜치와 계약 ID는 hub §16 escalation row + §20 dependency에서 그대로 내려받음. 하부 메커니즘은 아래 owner 브랜치로 위임하며 여기서 재명세하지 않음.
|
||||
|
||||
| Runbook | Technical owner (mechanism 위임) | Platform / approver hop |
|
||||
|---|---|---|
|
||||
| `FE-RB-001` | [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] (`FE-OC-004`) | [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] (`FE-OC-016` · `FE-OC-017`) |
|
||||
| `FE-RB-002` | [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] (`FE-OC-016` · `FE-OC-017`) | hosting/CDN owner (외부, hosting 확정 후) |
|
||||
| `FE-RB-003` | [[raw/branch-notes/feature-api-client-response-envelope-contract]] (`FE-OC-006` · `FE-OC-009`) | backend operation owner → release compatibility (외부/`FE-OC-023`) |
|
||||
| `FE-RB-004` | [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] (`FE-OC-014`) | telemetry platform owner (외부) |
|
||||
| `FE-RB-005` | [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] (`FE-OC-016` · `FE-OC-017`) | release approver / hosting owner (외부) |
|
||||
|
||||
### 4. Window/rate governance
|
||||
|
||||
> **Trace**: D2 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §16 · FE-OC-001 · FE-OC-026
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음. window 값은 §16이 명시한 conditional-default를 그대로 인용. 새 값을 발명하지 않음.
|
||||
|
||||
- 라벨 규칙: 모든 window/rate(`FE-RB-001` 5분, `FE-RB-002` 5분, `FE-RB-003` rolling 5분 rate + 10분 분류, `FE-RB-004` 15분, `FE-RB-005` provider-dependent `TBD`)는 `planned conditional-default`로만 표기.
|
||||
- 재검토 트리거: 해당 runbook 첫 drill의 `windowObservedBucket` + hosting/backend baseline SLO 존재 → owner가 measured target으로 승격.
|
||||
- 금지: 이 값을 measured SLO/달성 지표로 외부 답변에 사용(§22 answer boundary). 위반 시 `/lint` answer-boundary 검사 대상.
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로** (runbook 계약 자체의 meta-failure):
|
||||
- drill이 negative fixture 없이 "pass" → 거짓 보증. 기대 동작: 각 gate는 고의 실패 drill을 포함해야 통과 인정(§15.2).
|
||||
- window 값을 measured SLO로 외부 인용 → answer-boundary 위반. 기대 동작: `planned conditional-default` 라벨 강제(D2).
|
||||
- recovery를 "action 수행"(purge 발행/pointer switch)으로 판정 → 거짓 recovery. 기대 동작: reachability probe/e2e evidence로만 판정(§12.5, D5).
|
||||
- rollback 결정 hand-off 모호(config owner ↔ release owner) → runbook 단절(FE-RISK-002). 기대 동작: config owner가 원인 분류 실패 시 release owner에게 rollback 결정 이관(§16.1).
|
||||
- `FE-RB-004` drill 중 telemetry 실패를 동일 sink로 재귀 보고 → amplification. 기대 동작: 재귀 금지 + console-safe fallback(§11.2).
|
||||
- `FE-RB-002` reload가 user input 손실(FE-RISK-009). 기대 동작: dirty-state guard + one-reload cap.
|
||||
- **다른 계약 의존** (owner 브랜치 위임, `FE-OC` 계약 consume):
|
||||
- [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] (`FE-OC-016` · `FE-OC-017`) — release tuple/cache header/atomic pointer/rollback; `FE-RB-002`·`FE-RB-005`가 consume. 이 계약 변경 시 chunk/rollback runbook assertion 재검토.
|
||||
- [[raw/branch-notes/feature-api-client-response-envelope-contract]] (`FE-OC-006` · `FE-OC-009`) — degradation triage/retry cap; `FE-RB-003`이 consume.
|
||||
- [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] (`FE-OC-014`) — telemetry queue/redaction/sink; `FE-RB-004`가 consume.
|
||||
- [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] (`FE-OC-004`) — boot config validation; `FE-RB-001`이 consume.
|
||||
- [[raw/branch-notes/feature-frontend-ci-quality-gates-contract]] — `FE-GATE-021`~`025`를 파이프라인 blocking stage로 wiring; 이 브랜치의 drill 계약에 의존. (`FE-OC-020` owner 는 [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] 이고 ci-quality-gates 는 `FE-OC-*` owner 가 아니다.)
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| 다섯 runbook 각각이 trigger→containment→escalation→recovery를 drill evidence로 닫는다 | repo/harness 없음 | `pnpm drill:runbook -- FE-RB-00X` → `record.json` 생성 + `FE-GATE-021`~`025` pass(negative fixture 동반) | `needs-confirmation` |
|
||||
| window/rate default가 달성 가능하고 SLO 아님으로 정직히 라벨된다 | baseline/첫 drill 없음 | 첫 drill `windowObservedBucket` vs hosting/backend baseline 비교 + answer-boundary scan | `needs-confirmation` |
|
||||
| recovery가 action이 아니라 evidence로 판정된다 | 설계 assertion | drill이 reachability/e2e/self-check를 assert하고 "action 발행"을 assert하지 않음 확인 | `planned` |
|
||||
| escalation hand-off(config→release rollback 결정)가 단절되지 않는다 | hand-off 미검증 | `FE-RB-001`→`FE-RB-005` chained drill이 hand-off 경로를 exercise | `needs-confirmation` |
|
||||
| chunk-mismatch runbook이 reload 시 user input을 잃지 않는다 | reload semantics | `FE-RB-002` e2e에 dirty-state + one-reload guard fixture | `needs-confirmation` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
- 스캐폴딩 단계: `/coverage` 실행 전 수동 행을 만들지 않는다.
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
- 없음 — 스캐폴딩 단계.
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-GATE-014@1` | [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] | 지원 대상 config 버전이 boot 에 실패하면 release 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-015@1` | [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] | 혼재된 release 조합이 감지되지 않으면 release 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-OC-001@1` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | 모든 구현 주장은 evidence grade를 MUST 표시하고 repo evidence가 없는 상태에서 구현 완료를 MUST NOT 주장 | import 참조로 적용 |
|
||||
| `FE-OC-004@1` | [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] | build-time, runtime-public, secret config를 MUST 분리하고 boot 전에 runtime config를 검증 | import 참조로 적용 |
|
||||
| `FE-OC-006@1` | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | 모든 HTTP는 shared client를 MUST 통과하고 timeout·abort·response parsing을 page에서 구현하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-009@1` | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | retry는 safe/idempotent request에 한정하고 cap·jitter·`Retry-After`를 MUST 적용 | import 참조로 적용 |
|
||||
| `FE-OC-014@1` | [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] | telemetry는 best-effort이며 render·API success를 차단하면 안 되고 PII·token을 전송하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-016@1` | [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] | HTML, asset, runtime config, release manifest cache policy를 MUST 구분 | import 참조로 적용 |
|
||||
| `FE-OC-017@1` | [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] | rollback은 immutable prior release로 수행하고 build/config/API compatibility를 MUST 검증 | import 참조로 적용 |
|
||||
| `FE-OC-023@1` | [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] | API/config/storage/release schema의 breaking change는 migration 또는 version bump 없이 배포하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-026@1` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | 외부 답변은 evidence grade를 MUST 보존하고 목표 수치를 측정 결과처럼 말하면 안 됨 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
- 없음 — 자식 자료는 생성 후 controller가 parent Cluster와 함께 등록한다.
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
- 없음 — daily note는 이 작업에서 수정하지 않는다.
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크: 없음
|
||||
- 리뷰 메모: 없음
|
||||
- 머지 결과 / 배포 환경: `planned`
|
||||
- **wiki 추출 대상** (verified만): 없음
|
||||
- **추출하지 않을 항목** (planned / documented-only / abandoned): 현재 전체
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
---
|
||||
title: branch / feature-frontend-project-bootstrap-toolchain-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-001
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-001
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-TOOLCHAIN-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-LANGUAGE-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-BUILD-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: []
|
||||
contract_packet: 1
|
||||
branch: feature-frontend-project-bootstrap-toolchain-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract.md]
|
||||
tags: [branch, ca-skeleton, frontend, architecture, testing, javascript, build-tooling]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
contract_packet_sha256: 96291fb32a210358e477a7242241d20382c2d978ad8d6c137fcb4735b9dff6d8
|
||||
imports: [FE-GATE-011@1, FE-OC-007@1, FE-OC-016@1, FE-OC-018@1, FE-OC-020@1, FE-OC-021@1]
|
||||
accepts_delegations: [DELEG-FE-002@1]
|
||||
|
||||
---
|
||||
|
||||
# branch: feature-frontend-project-bootstrap-toolchain-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — 단일 브랜치의 TODO·결정·진행 기록. 머지/종료 후 verified 결과는 `/ingest`로 `wiki/projects/`에 추출한다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: manifest·engines·pnpm lock·checkJs scripts와 frozen install evidence가 존재한다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-TOOLCHAIN-001@1` | package manager default는 pnpm이며 packageManager field와 pnpm-lock.yaml을 commit한다 | manifest·lockfile·frozen install 계약에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-LANGUAGE-001@1` | JavaScript ESM과 tsc allowJs/checkJs/noEmit을 typecheck-equivalent baseline으로 사용한다 | source language와 check:types script에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-BUILD-001@1` | Vite client-only SPA를 build baseline으로 사용한다 | Vite build scaffold와 build artifact gate에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
|
||||
<!-- section-id: branch-local-decisions -->
|
||||
### 브랜치 지역 결정
|
||||
|
||||
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
||||
|---|---|---|---|---|
|
||||
| D1 | pnpm과 committed lockfile을 toolchain baseline으로 사용한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D2 | JavaScript ESM과 checkJs를 source/typecheck baseline으로 사용한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D3 | Vite client-only SPA를 build baseline으로 사용한다 | `local` | `raw/official-docs/vite-build-tool-official.md#VITE-C2` | `proposed` |
|
||||
|
||||
<!-- section-id: declared-overrides -->
|
||||
### 선언한 예외
|
||||
|
||||
| Override ID | Overrides | Reason | Approval | Status |
|
||||
|---|---|---|---|---|
|
||||
|
||||
없음.
|
||||
<!-- GENERATED: branch-contract:end -->
|
||||
|
||||
<!-- section-id: branch-goal -->
|
||||
## 목표
|
||||
|
||||
`ca-skeleton-frontend`의 project-wide bootstrap 계약 `FE-OC-003`("package manager, engine, lockfile, source language, checkJs command를 한 곳에서 MUST 고정")을 되묻지 않아도 코드를 작성할 수 있는 implementation-ready 명세로 내린다. 이 branch는 §20 Branch Decomposition에서 **Dependency `—`** 인 branch DAG의 root이며, 다른 27개 branch가 의존하는 toolchain 그릇(manifest·lockfile·source 언어·typecheck·build baseline)을 확정한다. 근거 결정은 [[raw/project-notes/ca-skeleton-frontend-operational-contract]]의 `FE-D001`(pnpm)·`FE-D002`(JavaScript ESM + `tsc --allowJs --checkJs --noEmit`)·`FE-D003`(Vite client-only SPA)이다. Measurable completion(§20)은 "manifest/engines/pnpm lock/checkJs scripts + frozen install evidence"이며, 이는 `FE-GATE-001`(manifest/lockfile)·`FE-GATE-003`(typecheck-equivalent)·`FE-GATE-011`(build) 로 판정된다. 또한 `FE-OC-018`(supply-chain: frozen lockfile)·`FE-OC-020`(test taxonomy: gate script 배선)에 **contributes-to** 로 참여한다. 현재 frontend repository는 존재하지 않으므로 본 노트의 모든 구현 주장은 `planned` 등급이다.
|
||||
|
||||
- 이슈:
|
||||
- PR:
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- `package.json` 매니페스트 확정 — `type: module`(ESM), `packageManager: pnpm@<pin>`, `engines`(Node/pnpm), script 슬롯 — 등급: `planned` (`FE-OC-003`, `FE-D001`/`FE-D002`)
|
||||
- `pnpm-lock.yaml` commit + `pnpm install --frozen-lockfile` 재현성 계약 — 등급: `planned` (`FE-OC-003` → `FE-OC-018` 기여, `FE-D001`, `FE-GATE-001`)
|
||||
- source 언어 = JavaScript ESM 고정 + `tsconfig.json`(`allowJs`/`checkJs`/`noEmit`) + `check:types` script — 등급: `planned` (`FE-OC-003`, `FE-D002`, `FE-GATE-003`)
|
||||
- Vite client-only SPA build baseline + 최소 `vite.config.js` + `dev`/`build` script — 등급: `planned` (`FE-OC-003`, `FE-D003`, `FE-GATE-011`)
|
||||
- Node/pnpm engine pin + engine 강제 정책 — 등급: `planned` (`FE-OC-003`, FE-NFR-C04 build context)
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외한 것. 다른 owner branch 소유 관심사이며 본 §구현 가이드에 detail을 남기지 않는다 (CLAUDE.md §15.5 R3).
|
||||
|
||||
- **build/runtime/secret env config 분리·runtime config 검증** — `FE-OC-004`, owner [[raw/branch-notes/feature-frontend-env-runtime-config-contract]]. 본 branch는 Vite가 `import.meta.env` 정적 치환 메커니즘을 제공한다는 사실만 확정하고 registry·검증은 위임.
|
||||
- **dependency lint rule / restricted-import 규칙 내용** — `FE-OC-002`, owner [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] + [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]]. 본 branch는 `lint` script 슬롯만 예약, 규칙 정의 위임.
|
||||
- **test suite 내용·gate 오케스트레이션·artifact 보존** — `FE-OC-020`, owner [[raw/branch-notes/feature-frontend-test-taxonomy-contract]]. 본 branch는 `check:types`만 소유, level별 test·CI 배선 위임.
|
||||
- **bundle budget·secret scan·SBOM·dependency review** — `FE-OC-018`/`FE-OC-021`, owner [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] + [[raw/branch-notes/feature-web-vitals-performance-budget-contract]]. 본 branch는 frozen lockfile evidence만 기여.
|
||||
- **8-registry 스키마·single-owner governance** — `FE-OC-022`, owner [[raw/branch-notes/feature-frontend-contract-registry-governance]]. 본 branch는 registry를 소유하지 않는다.
|
||||
- **runtime schema(Zod) 검증** — `FE-OC-007`, owner [[raw/branch-notes/feature-runtime-schema-validation-contract]]. checkJs는 JSDoc 타입 검사만 제공하고 boundary runtime 검증은 위임.
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/official-docs/vite-build-tool-official]] | D3 — Vite production build가 Rolldown으로 최적화된 정적 자산을 산출(`VITE-C2`)하고 dev server가 native ESM 위에서 동작(`VITE-C1`)하므로 client-only SPA를 build baseline으로 채택 |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | D1·D2·D3 — Decision Register(`FE-D001`/`FE-D002`/`FE-D003`)와 contract index(`FE-OC-003`), supply-chain 최소값(§13.1), planned command 계약(§14.3), gate matrix(§15.1)의 governing SSOT |
|
||||
| [[raw/project-notes/ca-skeleton-operational-contract]] | D1~D3의 상위 철학 precedent — backend skeleton의 운영 계약(port는 application 소유·sample은 제거 가능 fixture) 원칙을 frontend toolchain이 담을 그릇으로 확정 (사실 인용이 아닌 rationale precedent) |
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] `package.json` 작성 — `type: module`, `packageManager: pnpm@<pin>`, `engines`, script 슬롯 배치 — 등급: `planned`
|
||||
- [ ] `pnpm-lock.yaml` commit + clean checkout에서 `pnpm install --frozen-lockfile` exit 0 / drift 시 non-zero 재현 — 등급: `planned`
|
||||
- [ ] `tsconfig.json`(`allowJs`/`checkJs`/`noEmit`) + `check:types` script + checkJs negative fixture 배치 — 등급: `planned`
|
||||
- [ ] 최소 `vite.config.js` + `dev`/`build` script (Vite client-only SPA baseline) — 등급: `planned`
|
||||
- [ ] Node/pnpm 버전 pin(`.nvmrc` + engine 강제) + FE-NFR-C04 build context(Node/pnpm 버전) 기록 배선 — 등급: `planned`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
`/branch-spec` 채움 완료 (2026-07-19). frontend repository 미생성 — 모든 항목 `planned`. 실제 코드 착수 전까지 evidence 등급 상향 금지.
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> 각 결정은 아래 Decision Evidence Map의 prose 미러이다. 근거는 Sources 또는 hub Decision Register를 가리킨다.
|
||||
|
||||
- 2026-07-18: package manager를 **pnpm**으로 고정하고 `pnpm-lock.yaml` + `packageManager` 필드를 commit / 이유: project-local 재현성 default(lockfile drift·PM 혼용 방지) / 검토한 대안: npm·yarn·Bun / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D001`, §13.1 supply-chain 최소값(pnpm + committed lockfile).
|
||||
- 2026-07-18: source 언어를 **JavaScript ESM**으로 고정하고 typecheck는 `tsc --allowJs --checkJs --noEmit`로 대체 / 이유: 사용자 제약 + boundary runtime schema(Zod) 필요성 하에서 타입 안전성 확보 / 검토한 대안: TypeScript strict 소스 / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D002`.
|
||||
- 2026-07-18: build baseline을 **Vite client-only SPA**로 채택 / 이유: production build가 최적화된 정적 자산을 산출해 정적 호스팅 배포에 적합 / 검토한 대안: SSR/메타 프레임워크(Next 등)·edge rendering / 근거: [[raw/official-docs/vite-build-tool-official]] `VITE-C2`, [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D003`.
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
> 각 결정과 raw source Claim ID의 연결. `Decision ID`(D1·D2·D3)는 본 노트 안에서 안정적으로 유지한다.
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | package manager = pnpm; `pnpm-lock.yaml` + `packageManager` 필드 commit (`FE-D001` / `FE-OC-003`, 기여 `FE-OC-018`·`FE-OC-020`) | target CI가 pnpm을 지원하고 조직이 특정 PM을 강제하지 않는 동안 → pnpm. 조직 표준이 npm/yarn/Bun을 강제하거나 target CI가 pnpm을 미지원 → 해당 PM으로 교체하되 lockfile·`packageManager` 필드·frozen install script를 동시 변경 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D001`, §13.1(pnpm + committed lockfile) | `conditional-default` (project-decision) | pnpm 미지원 CI runner 채택 시 재현성 계약 재작성; lockfile drift가 gate로 실제 차단되는지 미검증 |
|
||||
| D2 | source = JavaScript ESM; typecheck-equivalent = `tsc --allowJs --checkJs --noEmit` (`FE-D002` / `FE-OC-003`·`FE-OC-007`·`FE-OC-020`) | 사용자 제약(JS 유지) + runtime schema 경계 검증이 있는 동안 → JS ESM + checkJs. TypeScript strict 전환이 승인되면 → `.ts` 소스 + strict `tsconfig`로 이행하고 checkJs 경로 폐기 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D002`, §14.3(`pnpm check:types` → checkJs diagnostic 0), §15.1 `FE-GATE-003` | `project-decision` (accepted-documented-only) | checkJs가 strict TS 수준 타입 안전을 보장하지 않음 — JSDoc 커버리지 공백 존재 가능; 실제 diagnostic 0 여부 미검증 |
|
||||
| D3 | build baseline = Vite client-only SPA (`FE-D003` / `FE-OC-003`, 기여 `FE-OC-016`·`FE-OC-021`) | 제품 요구가 client-only SPA(정적 호스팅)로 충분한 동안 → Vite SPA. SSR/SEO/edge rendering이 제품 요구가 되면 → 별도 project fork로 Vite SSR 또는 메타 프레임워크 재평가(`FE-D003` revisit) | [[raw/official-docs/vite-build-tool-official]] `VITE-C2`(Rolldown production build → 최적화된 정적 자산), `VITE-C1`(dev server native ESM); [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D003` | `official-doc` (official-vendor-doc) | `VITE-C2`는 정적 자산 산출만 증명하고 이 프로젝트 bundle/성능 threshold(`FE-OC-021`)는 별도 검증 필요; `pnpm build` exit 0 + manifest 산출 미검증 |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> `planned` blueprint. 경로는 hub §4.5/§4.6 planned directory blueprint에서 도출되므로 grounded이지만, frontend 코드가 없으므로 전 구간 `planned`. 각 sub-section은 CLAUDE.md §15.5 3-rule(R1 Trace·R2 UNSUPPORTED_IMPL_DECISION·R3 no OUT_OF_BRANCH_SCOPE)을 따른다.
|
||||
|
||||
### 1. `package.json` 매니페스트 계약
|
||||
|
||||
> **Trace**: D1(`FE-D001`) + D2(`FE-D002`) + D3(`FE-D003`) → `FE-OC-003`. planned 경로 `package.json`(repo root) + engine 강제 파일(`.npmrc`/`.nvmrc`, 아래 UNSUPPORTED 참조), 소비자는 pnpm·Vite·tsc.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: `packageManager` 의 정확한 pnpm 버전 pin(예: `pnpm@9.x`) — hub는 "pnpm"만 지정하고 버전을 못박지 않음. trade-off: 최신 pnpm major는 lockfile 포맷 변화 위험 → 착수 시점 pnpm LTS major로 pin하고 FE-NFR-C04에 기록.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: `engines` 의 정확한 Node 범위(예: `>=20 <21`) — hub §14.1은 "Node/pnpm versions recorded"만 요구하고 특정 버전을 명시하지 않음. trade-off: Node LTS 경계 선택은 임의 → 착수 시점 active LTS major로 pin.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: engine **강제(enforcement) 메커니즘** — hub `FE-OC-003`은 "engine을 한 곳에서 MUST 고정"만 요구하고, hub §17 `FE-Q-002`의 검증 조건도 "manifest `engines` + fresh clone pass"까지만 명시할 뿐 *무엇이 버전 위반 install을 실제로 거부하는가* 는 지정하지 않는다. `package.json` 의 `engines` 필드 단독은 기본 설정에서 경고에 그칠 수 있어(강제 여부는 package manager 설정 의존) 별도 장치가 없으면 no-op이 될 수 있다. 후보: (a) `.npmrc` 의 `engine-strict=true` + Node 버전 단일 소스 `.nvmrc`, (b) Corepack(`packageManager` 필드로 pnpm 버전 자체를 고정), (c) `preinstall` guard script. trade-off: (a)+(b) 조합을 기본값으로 채택 — `engine-strict` 가 Node/pnpm 범위 위반 install을 non-zero로 떨어뜨리고 `packageManager` 필드가 pnpm 버전 축을 덮어 런타임/PM 두 축이 모두 강제되며, `.nvmrc` 는 로컬 버전 전환용 단일 소스로만 쓰고 gate 판정 근거로는 쓰지 않는다. (c)는 커스텀 스크립트 유지비 때문에 보류. 세 후보의 실제 거부 동작은 미검증이므로 착수 시 §Claims To Verify의 engine 강제 항목으로 확정한다.
|
||||
|
||||
| 필드 | planned 값 | 근거 | 소유 경계 |
|
||||
|---|---|---|---|
|
||||
| `type` | `"module"` (ESM) | D2 (`FE-D002` JavaScript ESM) | this branch |
|
||||
| `packageManager` | `"pnpm@<LTS-major>"` | D1 (`FE-D001`) | this branch (버전 pin은 UNSUPPORTED_IMPL) |
|
||||
| `engines.node` / `engines.pnpm` | `<active-LTS>` 범위 | `FE-OC-003`("engine을 한 곳에서 고정") | this branch (버전 UNSUPPORTED_IMPL) |
|
||||
| engine 강제 메커니즘 (`.npmrc` `engine-strict=true` + `.nvmrc`, `packageManager` 필드 병행) | 범위 위반 install을 non-zero로 거부 | `FE-OC-003`(engine 고정) + hub §17 `FE-Q-002` 검증 조건("manifest `engines` + fresh clone pass") | this branch (메커니즘 선택은 UNSUPPORTED_IMPL — 위 3번째 라벨) |
|
||||
| `scripts.dev` / `scripts.build` | `vite` / `vite build` | D3 (`FE-D003`), §14.3 `pnpm build` | this branch |
|
||||
| `scripts.check:types` | `tsc --allowJs --checkJs --noEmit` | D2 (`FE-D002`), §14.3 `pnpm check:types` | this branch |
|
||||
| `scripts.lint`·`test:*`·`check:bundle`·`scan:security` 등 | 이름 슬롯만 예약 | §14.3 script 계약 | **delegated** — 각 owner branch가 구현 정의(§5 아래 슬롯 표) |
|
||||
|
||||
### 2. Lockfile + frozen install 재현성
|
||||
|
||||
> **Trace**: D1(`FE-D001`) → `FE-OC-003` 소유 + `FE-OC-018` 기여. planned 경로 `pnpm-lock.yaml`(commit) + `artifacts/quality/install.txt`. gate `FE-GATE-001@1`(manifest/lockfile — blocking scope·pass condition 은 hub §15.1 소유), §14.3 `pnpm install --frozen-lockfile`.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — frozen install 메커니즘(`--frozen-lockfile`)·evidence 경로(`artifacts/quality/install.txt`)·gate(`FE-GATE-001`)·supply-chain 최소값(§13.1 lockfile-check)이 모두 hub에 grounded.
|
||||
|
||||
- `pnpm-lock.yaml`을 repo에 commit; manifest range와 lockfile이 drift하면 `pnpm install --frozen-lockfile`이 non-zero exit → `FE-GATE-001` FAIL로 merge 차단.
|
||||
- evidence artifact: install 로그(`artifacts/quality/install.txt`, §14.3) + lockfile 검증(`artifacts/quality/lockfile-check.txt`, §13.1).
|
||||
- SBOM·secret scan·dependency review는 본 branch 산출물(lockfile)을 소비하지만 owner는 [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] — 본 §에 detail 미기재(R3).
|
||||
|
||||
### 3. Source 언어 + typecheck-equivalent 툴체인
|
||||
|
||||
> **Trace**: D2(`FE-D002`) → `FE-OC-003`·`FE-OC-007`·`FE-OC-020`. planned 경로 `tsconfig.json`(repo root, checkJs 전용) + checkJs negative fixture. gate `FE-GATE-003@1`(typecheck-equivalent — blocking scope 는 hub §15.1 소유), §14.3 `pnpm check:types` → checkJs diagnostic 0.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: `tsconfig.json`의 `allowJs`/`checkJs`/`noEmit` 외 부수 옵션(`target`/`moduleResolution`/`lib`) — `FE-D002`는 세 flag만 명시. trade-off: Vite ESM·최신 브라우저 전제 하에 임의 선택 → 착수 시 Vite 권장 preset에 맞춰 확정하고 fixture로 검증.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: checkJs negative fixture의 파일 경로·형태 — hub는 "JSDoc/checkJs negative fixture"(§15.1 `FE-GATE-003`)만 요구. trade-off: fixture 위치는 임의 → `tests/` 하위 typecheck fixture 컨벤션으로 확정.
|
||||
|
||||
- `tsconfig.json`은 emit 없이(`noEmit`) `.js`를 검사(`allowJs`+`checkJs`)한다. 별도 `.ts` 소스는 생성하지 않는다(D2).
|
||||
- `pnpm check:types`는 production 소스에서 diagnostic 0이어야 하고, negative fixture는 의도적으로 fail해야 `FE-GATE-003@1`이 PASS(pass condition 원문은 hub §15.1 소유).
|
||||
- boundary runtime 검증(Zod)은 `FE-OC-007` owner [[raw/branch-notes/feature-runtime-schema-validation-contract]] — checkJs는 compile-time JSDoc 타입만 담당(R3).
|
||||
|
||||
### 4. Vite build baseline 스캐폴딩
|
||||
|
||||
> **Trace**: D3(`FE-D003`) → `FE-OC-003` 소유 + `FE-OC-016`·`FE-OC-021` 기여. planned 경로 `vite.config.js`(repo root) + `src/bootstrap/main.jsx`(hub §4.5 composition root). gate `FE-GATE-011@1`(build — owner 는 [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]], blocking scope·pass condition 은 hub §15.1 소유), §14.3 `pnpm build` → `artifacts/release/build-manifest.json`.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: `vite.config.js`의 정확한 plugin 목록(예: React JSX plugin 패키지명) — hub는 plugin을 명시하지 않음. JSX 컴파일은 React 채택(`FE-D004`, owner [[raw/branch-notes/feature-async-ui-state-contract]]) 때문에 필요하나 plugin 패키지 선택은 미근거. trade-off: 착수 시 Vite 공식 React plugin 채택하고 build fixture로 검증.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: build output/asset hashing 세부 설정 — release cache 정책(`FE-OC-016` hashed asset immutable)은 owner [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] 소유. 본 §은 build가 hashed 정적 자산을 산출한다는 baseline만 확정하고 cache header 정책은 위임(R3).
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: `artifacts/release/build-manifest.json` **산출(emission) 메커니즘** — hub §12.1은 이 파일을 expected artifact로 열거하고 §14.3은 `pnpm build` 의 assertion을 "exit 0 + manifest present"로 두지만, *어떤 경로로 그 파일이 계약 경로에 생기는가* 는 지정하지 않는다(§12.1: "실제 path는 repository가 생기면 owner branch에서 확정한다"). 근거 source 의 `VITE-C2` 는 "최적화된 정적 자산 산출"만 증명할 뿐 manifest 파일의 이름·위치·스키마를 증명하지 않으므로, 번들러 기본 manifest 경로/형식은 본 노트에서 확정된 사실이 아니다. 후보: (a) 번들러 manifest 옵션을 켜고 산출물을 계약 경로로 옮기는 post-build wrapper script, (b) 번들러 출력 설정만으로 계약 경로에 직접 쓰기. trade-off: (a)를 기본값으로 채택 — 번들러 기본 출력 규약과 계약 artifact 경로를 분리해 두면 번들러/옵션이 바뀌어도 downstream gate(`FE-GATE-011`) 계약 경로가 깨지지 않는다. 착수 시 실제 산출 경로를 확인해 확정.
|
||||
> - **해소됨(2026-07-21) — 근거 있는 결정**: `FE-NFR-C04` build context 의 기록 위치·필드명은 이제 스키마가 정한다. hub §2.1.3 `ART-FE-001@1`(Schema Owner = 본 branch)의 `build-manifest.schema.json` 이 `buildContext.nodeVersion` · `buildContext.packageManagerVersion` · `buildContext.runnerImage` 를 required 로 고정한다. 이전 판이 제안하던 top-level `pnpmVersion` 은 그 스키마의 `buildContext.packageManagerVersion` 으로 확정됐다(패키지 매니저를 pnpm 으로 못박지 않기 위함). 필드 추가·rename 은 Schema Owner 단독 결정이고 소비 branch 는 `imports` pin 으로 따라온다.
|
||||
|
||||
- 최소 `vite.config.js` + `pnpm dev`/`pnpm build` script로 client-only SPA build baseline을 확정.
|
||||
- `pnpm build`는 exit 0 + build manifest(`artifacts/release/build-manifest.json`)를 산출해야 `FE-GATE-011` PASS.
|
||||
- **manifest 산출 책임 경계**: `artifacts/release/build-manifest.json` 의 *생성* 은 본 branch 가 소유한다 — 근거는 gate owner 가 아니라 hub §2.1.3 `ART-FE-001@1` 의 Producer·Schema Owner 등록이다(`FE-GATE-011` 자체의 owner 는 [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]]). release tuple 파일(`dist/release-manifest.json`)과 cache header 정책은 `FE-OC-016`/`FE-OC-017` owner [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] 소유이며 본 §에 detail 미기재(R3).
|
||||
- **build context 기록 vs 소비 경계**: `FE-NFR-C04`(Node/pnpm 버전 등) 값을 build manifest에 *기록* 하는 것은 본 branch, 그 값을 bundle threshold 판정 맥락으로 *소비* 하는 것은 `FE-OC-021` owner [[raw/branch-notes/feature-web-vitals-performance-budget-contract]] — threshold·판정 로직은 본 §에 미기재(R3).
|
||||
- bundle size threshold(`FE-NFR-001`/`002`)와 성능 예산은 `FE-OC-021` owner [[raw/branch-notes/feature-web-vitals-performance-budget-contract]] — 본 §에 threshold 미기재(R3).
|
||||
|
||||
### 5. §14.3 script 슬롯 vs owner 위임 (FE-OC-020 기여)
|
||||
|
||||
> **Trace**: `FE-OC-003`(command 한 곳 고정) + `FE-OC-020` 기여(gate script 배선). §14.3 planned command 계약의 script 이름은 project-wide SSOT이며, 본 branch는 매니페스트에 슬롯을 예약하되 non-owned script의 구현은 정의하지 않는다.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — 어떤 script를 본 branch가 소유하고 어떤 것을 위임하는지는 §15.1 gate ownership + §5.1 registry owner map으로 결정론적으로 도출됨.
|
||||
|
||||
| §14.3 script | 소유 | 본 branch 역할 |
|
||||
|---|---|---|
|
||||
| `pnpm install --frozen-lockfile` | this branch | 정의 + evidence (`FE-GATE-001`) |
|
||||
| `pnpm check:types` | this branch | 정의 (`FE-GATE-003`) |
|
||||
| `pnpm build` | this branch | baseline 정의 (`FE-GATE-011`) |
|
||||
| `pnpm lint` | [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]] | 슬롯만 예약 |
|
||||
| `pnpm test:unit`/`test:component`/`test:integration`/`test:e2e`/`test:a11y` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | 슬롯만 예약 |
|
||||
| `pnpm check:bundle`/`test:performance` | [[raw/branch-notes/feature-web-vitals-performance-budget-contract]] | 슬롯만 예약 |
|
||||
| `pnpm scan:security` | [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] | 슬롯만 예약 |
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- **lockfile drift**: manifest range와 `pnpm-lock.yaml` 불일치 → `pnpm install --frozen-lockfile` non-zero exit → `FE-GATE-001` FAIL. 기대 동작: CI가 merge 차단, 부분 install 없음.
|
||||
- **engine mismatch**: 로컬/CI Node·pnpm이 `engines` 범위 밖 → engine 강제로 install 거부. 기대 동작: 명확한 에러 + silent 진행 금지. (강제 메커니즘 = §구현 가이드 1의 "engine 강제 메커니즘" 행 + 같은 § 3번째 `UNSUPPORTED_IMPL_DECISION` 라벨 — 후보 (a)/(b)/(c) 중 미확정)
|
||||
- **checkJs diagnostic > 0**: production 소스 타입 오류 → `pnpm check:types` non-zero → `FE-GATE-003` FAIL. 기대 동작: merge 차단. negative fixture는 반대로 fail해야 정상.
|
||||
- **Vite build 실패/manifest 부재**: `pnpm build` non-zero 또는 `build-manifest.json` 미산출 → `FE-GATE-011` FAIL.
|
||||
- **script 이름 drift**: §14.3 script rename을 gate/artifact mapping 갱신 없이 수행 → downstream gate가 없는 script 참조. 기대 동작: §14.3 규칙("script 이름을 바꾸면 acceptance gate와 artifact mapping을 동시에 갱신")으로 방지.
|
||||
- **다른 계약 의존**:
|
||||
- **상류 의존 해당 없음** — 본 branch는 §20 Dependency `—` 인 branch DAG root. sibling 계약에서 consume하는 것 없음.
|
||||
- **하류 소비자(역의존)**: 본 산출물(pnpm/lockfile·`type: module`·`check:types`·Vite baseline)을 [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]], [[raw/branch-notes/feature-frontend-env-runtime-config-contract]], [[raw/branch-notes/feature-frontend-test-taxonomy-contract]], [[raw/branch-notes/feature-tailwind-design-token-styling-contract]], [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] 가 §20 Dependency로 consume. 이 계약(script 이름·lockfile 정책)이 바뀌면 해당 branch 영향.
|
||||
- **기여(contributes-to)**: `FE-OC-018` owner [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] 의 supply-chain gate가 본 frozen lockfile evidence를 consume; `FE-OC-020` owner [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] 가 `check:types`를 `FE-GATE-003`으로 배선.
|
||||
- **CA 철학 precedent**: [[raw/project-notes/ca-skeleton-operational-contract]] 의 운영 계약(port ownership·sample fixture 원칙) — 본 toolchain이 그 구조를 담을 그릇을 만든다(사실 의존이 아닌 설계 precedent).
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| clean checkout에서 `pnpm install --frozen-lockfile`이 exit 0, lockfile drift 시 non-zero | repo·lockfile 미생성 | `FE-GATE-001` frozen install; evidence `artifacts/quality/install.txt` + `lockfile-check.txt` (§14.3 / §13.1) | `needs-confirmation` |
|
||||
| `pnpm check:types`가 production 소스에서 checkJs diagnostic 0, negative fixture에서 fail | `tsconfig` checkJs 설정 실효성 미검증 | `FE-GATE-003` typecheck; JSDoc/checkJs negative fixture; `artifacts/quality/check-types.txt` (§15.1) | `needs-confirmation` |
|
||||
| `pnpm build`(Vite)가 exit 0 + `build-manifest.json` 산출 | `vite.config.js` 미작성 | `FE-GATE-011` build; `artifacts/release/build-manifest.json` (§14.3) | `needs-confirmation` |
|
||||
| engine 강제(Node/pnpm 범위)가 버전 불일치 install을 실제 차단 | 강제 메커니즘 후보 (a) `.npmrc engine-strict` (b) Corepack (c) `preinstall` guard 중 미확정·미검증 (§구현 가이드 1 UNSUPPORTED) | 로컬 Node 버전을 `engines` 범위 밖으로 변조 후 install → non-zero exit 재현; fresh clone pass(hub §17 `FE-Q-002`) | `needs-confirmation` |
|
||||
| `artifacts/release/build-manifest.json` 이 계약 경로에 실제 산출되고 `FE-NFR-C04` build context(Node/패키지 매니저 버전 + runner image)를 포함 | 산출 메커니즘(wrapper vs 번들러 직접 출력) 미확정 — 필드명은 `ART-FE-001@1` 스키마로 확정됨 | `pnpm build` 후 경로 존재 + context 필드 존재 확인; `FE-GATE-011` assertion + hub §14.1 context 요구 대조 | `needs-confirmation` |
|
||||
| §14.3 script 이름이 downstream gate(`FE-GATE-001`/`003`/`011`)와 일치 유지 | script rename drift 위험 | gate matrix ↔ 매니페스트 script cross-ref (ci-quality-gates 협업) | `needs-confirmation` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
> `/coverage`가 채우는 생성물이며 손으로 유지하지 않는다.
|
||||
|
||||
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| TODO — `/coverage` 실행 전 | missing | (없음) | 미평가 | TODO |
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: received-delegations:start -->
|
||||
### 수신한 위임
|
||||
|
||||
| Delegation Ref | From | Concern | Status |
|
||||
|---|---|---|---|
|
||||
| `DELEG-FE-002@1` | [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]] | `fe.deleg.lint-toolchain-substrate` | accepted |
|
||||
<!-- GENERATED: received-delegations:end -->
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-GATE-011@1` | [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] | clean production build 가 실패하거나 기대 artifact 가 없으면 merge·release 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-OC-007@1` | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | JSON envelope와 payload는 boundary에서 runtime schema를 MUST 통과 | import 참조로 적용 |
|
||||
| `FE-OC-016@1` | [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] | HTML, asset, runtime config, release manifest cache policy를 MUST 구분 | import 참조로 적용 |
|
||||
| `FE-OC-018@1` | [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] | frozen lockfile, dependency review, secret scan, SBOM 또는 dependency inventory를 release gate에 MUST 포함 | import 참조로 적용 |
|
||||
| `FE-OC-020@1` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | gate 종류별 책임·fixture·artifact를 분리하고 실패를 warning으로 낮추면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-021@1` | [[raw/branch-notes/feature-web-vitals-performance-budget-contract]] | NFR은 device/network/cache/build context와 함께 MUST 측정 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
### Sub-branches (세부 작업)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 오류 기록 (이 branch 작업 중 발생)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 강의 (이 작업을 위해 학습한 강의)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### job-posting tie-ins (이 작업에서 파생된 글감)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크:
|
||||
- 리뷰 메모:
|
||||
- 머지 결과 / 배포 환경:
|
||||
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
|
||||
- `actually-implemented` 항목:
|
||||
- `locally-verified` 항목:
|
||||
- `prod-verified` 항목:
|
||||
- **추출하지 않을 항목** (planned / documented-only / abandoned):
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
---
|
||||
title: branch / feature-frontend-release-cache-rollback-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-024
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-024
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-DEPLOYMENT-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-CACHE-POLICY-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-OFFLINE-CACHE-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-020, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-004, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-023]
|
||||
contract_packet: 1
|
||||
branch: feature-frontend-release-cache-rollback-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract]
|
||||
tags: [branch, ca-skeleton, frontend, ci-cd, build-tooling, externalized-config]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
contract_packet_sha256: 63a7ea47dea99d7a8dfe7275a2636dd5f529c280884fe593d2f083dfb15ed1fc
|
||||
imports: [ART-FE-001@1, FE-OC-019@1]
|
||||
---
|
||||
|
||||
# branch: feature-frontend-release-cache-rollback-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — TODO·결정·진행 기록. 구현 결과는 검증 뒤 `/ingest`로만 추출한다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
[[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: release tuple·cache header·mixed fixture failure·rollback drill이 검증된다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-DEPLOYMENT-001@1` | static release는 immutable release directory와 atomic active pointer로 배포한다 | immutable release layout·atomic switch·rollback에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-CACHE-POLICY-001@1` | hashed asset은 immutable, HTML·runtime config·release manifest는 revalidate/no-store로 분리한다 | surface별 cache header와 coherence gate에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-OFFLINE-CACHE-001@1` | service worker와 offline asset cache는 default off다 | service worker registration과 offline cache 기본 정책에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
|
||||
<!-- section-id: branch-local-decisions -->
|
||||
### 브랜치 지역 결정
|
||||
|
||||
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
||||
|---|---|---|---|---|
|
||||
| D1 | surface별 cache policy를 분리한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D2 | immutable release directory와 atomic active pointer를 사용한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D3 | service worker와 offline asset cache를 기본 off로 둔다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D4 | rollback은 coherent prior-release set을 복원한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D5 | FE-REG-RELEASE와 typed compatibility comparison을 사용한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D6 | `FE-GATE-019@2`의 security-header 축 검증 메커니즘을 소유하고 정책 내용은 browser-security가 공급한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
|
||||
<!-- section-id: declared-overrides -->
|
||||
### 선언한 예외
|
||||
|
||||
| Override ID | Overrides | Reason | Approval | Status |
|
||||
|---|---|---|---|---|
|
||||
|
||||
없음.
|
||||
<!-- GENERATED: branch-contract:end -->
|
||||
|
||||
<!-- section-id: branch-goal -->
|
||||
## 목표
|
||||
|
||||
이 브랜치는 프로젝트 계약 `FE-OC-016`(HTML/asset/runtime-config/release-manifest cache policy를 MUST 구분)과 `FE-OC-017`(rollback은 immutable prior release로 수행하고 build/config/API compatibility를 MUST 검증)을 *구현 착수 가능한 명세*로 내린다. hub의 결정 `FE-D019`(service worker/offline cache default off), `FE-D020`(hashed asset immutable + HTML/config/manifest revalidate·no-store 분리), `FE-D023`(immutable release directory + atomic active pointer)와 registry `FE-REG-RELEASE`(release token registry, §5.9)를 owner로서 상세화하고, 여기에 §12.3 compatibility tuple / §12.4 atomic deploy expectation / §12.5 rollback invariant를 착수 수준으로 고정한다. 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D019 · FE-D020 · FE-D023 · §12. 아직 frontend repository·hosting provider가 없으므로 본 노트의 모든 구현 항목 등급은 `planned`이며, 코드/헤더/드릴 evidence가 생기기 전에는 `actually-implemented`로 승급하지 않는다.
|
||||
|
||||
- 이슈: 없음 (스캐폴딩 단계)
|
||||
- PR: 없음
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- **Cache policy 소유** (`FE-OC-016`): hashed JS/CSS/font/image, `index.html`, `/config.json`(runtime config), `release-manifest.json`, source map, service worker 6개 surface의 default cache policy 명세 (§12.2). 실제 `Cache-Control` header syntax는 policy로만 소유하고 provider 확정 후 adapter runbook에 기록.
|
||||
- **Immutable release + atomic pointer** (`FE-D023`, §12.4): immutable release directory layout + atomic active-pointer deploy order.
|
||||
- **Rollback contract** (`FE-OC-017`, §12.5): coherent prior-release set 정의 + rollback invariant + FE-RB-005 drill evidence 요건(`FE-GATE-016`).
|
||||
- **Release token registry** (`FE-REG-RELEASE`, §5.9): release/compatibility tuple 토큰 + typed(비-lexical) compatibility comparison.
|
||||
- **Release coherence gate + mixed-version negative fixture** (`FE-GATE-015@1`): HTML/asset/config mismatch 탐지 fixture.
|
||||
- **Hosting header gate** (`FE-GATE-019@2`, 2026-07-21 에 security 축 편입): 응답 header 의 declared-vs-actual 대조를 **cache 축과 security 축 둘 다** 담당한다. 본 branch 는 gate owner 로서 **검증 메커니즘**(응답 probe · 대조 · `hosting-headers.json` artifact)을 소유하고, 검증 대상 **security header 정책의 내용**은 [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] (`FE-OC-019`) 가 공급한다. directive 값 자체는 여전히 hosting/backend header owner 소유다(D6).
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외한 것. 면접 등에서 "이건 범위에 없었습니다"라고 답할 근거.
|
||||
|
||||
- **Hosting/CDN provider의 실제 콘솔 command와 deploy execution** — provider 확정 후 adapter/runbook에서 채움.
|
||||
- **Runtime config 자체의 3-way 분리·boot 검증 로직** (`FE-OC-004`) — owner는 [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] (`FE-OC-004`). 본 브랜치는 그 config의 *cache/coherence*만 소유.
|
||||
- **Build output의 asset hashing·build manifest·dependency inventory 생성** (`FE-OC-018`) — owner는 [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] (`FE-OC-018`). 본 브랜치는 그 산출물을 *release coherence 입력*으로 consume만.
|
||||
- **Version tuple compatibility 규칙(additive/breaking/migration)** (`FE-OC-023`) — owner는 [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] (`FE-OC-023`). 본 브랜치는 그 규칙을 rollback 판정에 *적용*만.
|
||||
- **Runbook 서술 문서(FE-RB-002/FE-RB-005 narrative) 유지와 5개 drill orchestration** (`FE-OC-025`) — owner는 [[raw/branch-notes/feature-frontend-operational-runbook-contract]] (`FE-OC-025`). 본 브랜치는 rollback *기술 escalation 대상*이자 drill evidence 요건 제공자.
|
||||
- **DEPLOY_MISMATCH 사용자 recovery UI·reload-loop 방지** (`FE-OC-015`) — owner는 [[raw/branch-notes/feature-frontend-render-recovery-boundary-contract]] (`FE-OC-015`).
|
||||
- **8-registry single-owner governance orchestration** (`FE-OC-022`) — owner는 [[raw/branch-notes/feature-frontend-contract-registry-governance]] (`FE-OC-022`). 본 브랜치는 `FE-REG-RELEASE` 한 registry의 *content owner*.
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
> 이 branch의 구현·설계 결정의 **근거가 되는 외부 자료**.
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/official-docs/vite-build-tool-official]] `VITE-C2` | production build가 content-hash 붙은 optimized static asset을 산출한다는 공식 근거 — D1(hashed asset = long-lived immutable) cache 분리와 D2(static-hosting immutable release directory) 전제의 build-tool 근거. cache header 자체는 hosting provider 확정 후 보강. |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.1 `FE-GATE-019@2` · §2.1.1(revision 2, Owner = 본 branch) · §14.3(`pnpm verify:hosting-headers`) | D6 — 이 gate 가 cache header 뿐 아니라 **security header 집합**의 declared-vs-actual 대조까지 담당한다는 근거. 정책 내용 공급자는 [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] (`FE-OC-019`). |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D019 · FE-D020 · FE-D023 (§12 Release/Cache/Version/Rollback, §5.9 FE-REG-RELEASE) | 본 브랜치 owner 결정 3건 + release token registry + compatibility tuple/atomic deploy/rollback invariant의 project decision SSOT. release coherence·rollback invariant는 외부 vendor 표준이 아니라 project inference이므로 hub row를 근거로 인용. |
|
||||
|
||||
## TODO
|
||||
|
||||
각 항목 옆에 증거 등급 표기.
|
||||
|
||||
- [ ] `FE-REG-RELEASE` release token registry(`src/contracts/release-tokens.js`) + typed compatibility comparator 명세 — 등급: `planned`
|
||||
- [ ] surface별 cache policy 표 + `pnpm verify:hosting-headers`(`FE-GATE-019@2`) assertion 명세 — 등급: `planned`
|
||||
- [ ] `FE-GATE-019@2` security-header 축: browser-security 가 공급한 정책 집합(CSP/HSTS/frame/referrer)의 declared-vs-actual 대조를 같은 probe·artifact 로 편입 — 등급: `planned`
|
||||
- [ ] immutable release directory layout + atomic active-pointer deploy order(§12.4) 명세 — 등급: `planned`
|
||||
- [ ] rollback coherent-set invariant + FE-RB-005 drill evidence(`FE-GATE-016`) 요건 명세 — 등급: `planned`
|
||||
- [ ] mixed-version negative fixture + release coherence gate(`FE-GATE-015`) 명세 — 등급: `planned`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
- hosting/CDN provider 미확정 → cache header 문자열·atomic switch primitive·purge semantics는 provider 확정 시 adapter runbook에서 확정. 현재는 policy와 invariant만 소유한다.
|
||||
- 모든 항목 `planned` — frontend repository가 없어 코드/헤더/드릴 evidence 부재.
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> 각 결정의 근거는 hub decision register(§3.2)와 §12/§5.9.
|
||||
|
||||
- 2026-07-18: **surface별 cache policy 분리 채택** / 이유: hashed asset은 content-hash로 identity가 고정돼 immutable 가능하지만 HTML/runtime-config/release-manifest는 release마다 교체·mismatch 탐지가 필요 / 검토한 대안: 전 surface 단일 cache 규칙(운영 단순) / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D020 · §12.2.
|
||||
- 2026-07-18: **immutable release directory + atomic active pointer 채택** / 이유: rollback 가능한 artifact와 partial-deploy 없는 전환을 위해 / 검토한 대안: in-place overwrite deploy(rollback 불가·mixed window 발생) / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D023 · §12.4.
|
||||
- 2026-07-18: **service worker/offline asset cache default off** / 이유: stale asset·config mismatch surface 축소 / 검토한 대안: SW precache(오프라인 UX 확보하나 stale 복잡도 증가) / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D019.
|
||||
- 2026-07-18: **rollback = coherent prior-release set + compatibility 검증** / 이유: HTML만 되돌리고 runtime config를 최신에 남기면 mismatch로 boot/route 실패 / 검토한 대안: HTML pointer만 교체하는 fast rollback(§12.5가 금지) / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-OC-017 · §12.5 · §16.5.
|
||||
- 2026-07-21: **`FE-GATE-019` 에 security-header 축 편입(D6)** / 이유: hub §15.1 이 이 gate 의 Covered FE-OC 에 `FE-OC-019` 를 추가해 pass condition 이 security header 까지 넓어졌다. 응답 header 의 declared-vs-actual 대조라는 메커니즘이 cache header 와 동일하므로 같은 probe·같은 artifact 를 쓴다 / 검토한 대안: `FE-GATE-013`(security) 에 두기 — 그쪽은 artifact 를 스캔하는 gate 라 실행 시점·증거 형식이 달라 기각 / 근거: hub §15.1 `FE-GATE-019@2` · §2.1.1 revision 2. 검증 대상 정책 집합은 [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] 가 공급.
|
||||
- 2026-07-18: **release token registry + typed(비-lexical) compatibility comparison** / 이유: `releaseId`/schema/API version을 string lexical로 비교하면 오판정(§12.3 금지) / 검토한 대안: page 안에서 직접 version string 비교(§5.1 ad hoc failure) / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.9 · §12.3.
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | `FE-OC-016` surface별 cache policy 분리: hashed asset = long-lived immutable, `index.html` = no-cache/revalidate, `/config.json` = no-store(또는 URL explicit version), `release-manifest.json` = no-store/immediate revalidate, source map = public off, service worker = off | 기본값으로 이 분리를 적용. hosting cache primitive가 surface별 `Cache-Control`을 표현하지 못하면(단일 global 규칙만 제공) provider-specific 등가 정책을 adapter runbook + decision row에 기록해 대체 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D020, FE-OC-016 · §12.2 · §6.1(runtime public=no-store); [[raw/official-docs/vite-build-tool-official]] `VITE-C2`(content-hash static asset) | `project-decision + official-doc` | 실제 hosting header가 선언 policy와 일치하는지 미검증(`FE-GATE-019@2` 필요); 정확한 `max-age`/`immutable` directive 문자열 미확정 |
|
||||
| D2 | `FE-D023` immutable release directory + atomic active pointer 배포. deploy order: immutable asset → release manifest → runtime config → asset reachability smoke → active HTML pointer switch → post-switch smoke (§12.4) | provider가 atomic pointer switch를 지원하면 이 primitive 사용. provider가 *다른* atomic primitive만 제공하면 그 등가 primitive + rollback semantics를 decision row에 기록(§12.4 fallback) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D023, FE-OC-016, FE-OC-017 · §12.4 · §12.1(artifact set) | `conditional-default` | provider primitive 미확정 — atomic switch·purge semantics는 hosting owner 확정 전 `TBD`; partial-deploy window 무발생 검증 필요 |
|
||||
| D3 | `FE-D019` service worker·offline asset cache default off | stale asset/config mismatch surface 축소를 위해 기본 off. offline product requirement + update UX가 *설계된 뒤에만* SW precache 재검토(FE-D019 revisit trigger) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D019, FE-OC-016 · §12.2(service worker=default off row) | `conditional-default` | SW가 실제로 등록되지 않는지 build/e2e로 미검증; offline 요구가 생기면 update UX 설계 없이는 재도입 금지 |
|
||||
| D4 | `FE-OC-017` rollback = coherent prior-release set 복구 + build/config/API compatibility 검증. 금지: rebuild-as-rollback, HTML-only 교체, compatibility 미확인 pointer 변경, smoke 없는 close (§16.5) | release-blocking defect가 확인되고 forward fix가 incident window 안에서 안전하다고 증명되지 않을 때 rollback(§16.5 activation). prior immutable release·config·API compatibility가 알려져 있어야 실행 가능(preconditions) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-OC-017, FE-D023 · §12.5(rollback invariant) · §16.5(FE-RB-005 procedure invariant) | `project-decision` | recovery를 cache purge 완료가 아니라 old/new reachability probe로 판정해야 함(§12.5) — provider probe 미구현; rollback drill(`FE-GATE-016`) evidence 부재 |
|
||||
| D5 | `FE-REG-RELEASE` release token registry(토큰 목록은 hub §5.9 소유 — 8-token tuple) + typed compatibility comparison — string lexical version 비교 금지(§12.3) | tuple 토큰과 comparator를 registry factory로 소유. page/component가 raw string version을 비교하거나 cache key를 직접 작성하면 ad hoc use failure(§5.1) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.9(release token schema) · §12.3(compatibility tuple + no-lexical-compare rule) · §5.1(FE-REG-RELEASE owner) | `project-decision` | comparator API 모양·semver 파싱 규칙 미확정(UNSUPPORTED_IMPL_DECISION); `builtAt`이 cache identity로 오용되지 않는지 검증 필요 |
|
||||
| D6 | `FE-GATE-019@2` 의 **security-header 축**: 본 branch 는 gate owner 로서 declared-vs-actual **검증 메커니즘**(응답 probe · 대조 · `hosting-headers.json` artifact)을 소유하고, 검증 대상 security header **정책의 내용**은 [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] (`FE-OC-019`) 가 공급한다 | hub §15.1 이 이 gate 의 Covered FE-OC 에 `FE-OC-019` 를 포함하는 한 유지. cache header 와 같은 probe·같은 artifact 를 쓰므로 별도 command 를 만들지 않는다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.1(`FE-GATE-019@2` — pass condition 에 security-header 포함) · §2.1.1(revision 2, Owner = 본 branch) · §14.3(`pnpm verify:hosting-headers` 행) | `project-decision` | directive 값은 hosting/backend header owner 소유라 실제 응답 대조는 provider 확정 후에만 가능; 정책 공급자(browser-security)의 정책 집합이 바뀌면 본 gate fixture 재도출 필요 |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> 모든 경로는 `planned` — frontend repository 미생성. 경로는 hub §4.6 Planned directory blueprint + §5.1 registry owner map에서 도출(grounded)하되 코드가 없으므로 전체 `planned`.
|
||||
|
||||
### 1. Release token registry + typed compatibility comparator
|
||||
|
||||
> **Trace**: D5 — [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.9(FE-REG-RELEASE schema) · §12.3(compatibility tuple, no-lexical-compare) · §5.1(owner map: `src/contracts/release-tokens.js`). Registry content owner = 본 브랜치.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: comparator 함수 이름/시그니처와 version 파싱 규칙(semver vs 명시적 정수 필드)은 hub가 원칙(“lexical 금지”)만 주고 detail은 미권고 → 임의 선택. trade-off: 명시적 정수 필드 비교는 구현이 단순하나 organization version 규약이 semver를 강제하면 재작성 필요.
|
||||
|
||||
- **Planned path**: `src/contracts/release-tokens.js` (§5.1).
|
||||
- **Tokens**: 8-token release tuple 의 **정의(토큰명 · Source · Compatibility role)는 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.9 소유**이며 여기에 옮겨 적지 않는다. 본 § 이 쓰는 불변식만: `builtAt` 은 진단용이고 **cache identity 가 아니다**.
|
||||
- **Comparison contract (§12.3)**: `config schema major incompatible → boot fail`; `API contract incompatible → route mount fail 또는 supported compatibility adapter`; `asset manifest mismatch → controlled reload once`; `releaseId mismatch but all versions compatible → warning telemetry 후 continue`. 판정은 구조적 비교로만 — **string lexical compare 금지**.
|
||||
|
||||
### 2. Per-surface hosting header contract (cache + security)
|
||||
|
||||
> **Trace**: D1 — [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D020 · §12.2(cache policy 표) · §6.1(runtime public=no-store). 정책만 소유, header 문자열은 provider adapter로 위임.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 정확한 `Cache-Control` directive 문자열(예: `max-age` 초, `immutable`, `no-store`)과 hosting 설정 문법은 미권고 → provider 확정 후 확정. trade-off: 지금 숫자를 고정하면 provider 제약과 충돌 위험.
|
||||
|
||||
| Surface | Default cache policy | Reason (§12.2) |
|
||||
|---|---|---|
|
||||
| hashed JS/CSS/font/image | long-lived immutable | content hash identity |
|
||||
| `index.html` | `no-cache` / revalidate | active entry point 교체 |
|
||||
| `/config.json` (runtime config) | `no-store` 또는 URL explicit version | deploy-specific public config |
|
||||
| `release-manifest.json` | `no-store` 또는 immediate revalidate | mismatch detection |
|
||||
| source map | public hosting off; secured artifact store | stack/source exposure boundary |
|
||||
| service worker | off (D3/FE-D019) | stale release 복잡도 |
|
||||
|
||||
- **Verification (§14.3)**: `pnpm verify:hosting-headers` → `artifacts/release/hosting-headers.json`; assertion = HTML/config/manifest/hashed-asset 응답의 header 가 선언 policy 와 일치(`FE-GATE-019@2`). **cache header 뿐 아니라 security header(CSP/HSTS/frame/referrer)도 같은 probe 로 대조**한다 — 정책 내용은 [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] 가 공급.
|
||||
|
||||
### 3. Immutable release directory + atomic active-pointer deploy
|
||||
|
||||
> **Trace**: D2 — [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D023 · §12.1(artifact set) · §12.4(atomic deploy order).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: release directory naming 규약(예: `releases/<releaseId>/`)은 hub가 명시하지 않음 → 임의. trade-off: `releaseId` 기반 디렉토리는 rollback target 매핑이 단순하나 provider 경로 제약과 충돌 가능. provider-specific atomic switch/purge command는 **OUT_OF_BRANCH_SCOPE** → §범위 참조([[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] `FE-OC-018` build output, hosting owner).
|
||||
|
||||
- **Artifact set (§12.1)**: `dist/index.html`, `dist/assets/<content-hash>.*`, `dist/config.json`, `dist/release-manifest.json`, `dist/config/runtime-config.schema.json`, `artifacts/release/build-manifest.json`, `artifacts/release/dependency-inventory.*`, `artifacts/release/checksums.txt`.
|
||||
- **Atomic deploy order (§12.4)**: (1) immutable asset upload → (2) release manifest upload → (3) runtime config upload → (4) asset reachability smoke → (5) active HTML pointer switch → (6) post-switch boot/e2e smoke. provider가 이 순서를 지원하지 않으면 등가 atomic primitive + rollback semantics를 decision row에 기록.
|
||||
|
||||
### 4. Rollback coherent-set invariant + drill evidence
|
||||
|
||||
> **Trace**: D4 — [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-OC-017 · §12.5(rollback invariant) · §16.5(FE-RB-005 procedure invariant). Runbook *서술 문서*와 drill orchestration은 [[raw/branch-notes/feature-frontend-operational-runbook-contract]] `FE-OC-025` 소유 — 본 브랜치는 invariant + evidence 요건 제공 + 기술 escalation 대상(§16.5).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: reachability probe의 구체 구현(요청 방식·판정 임계)은 provider 미확정으로 임의 → trade-off: probe를 origin에만 하면 edge 불일치를 놓칠 수 있어 old/new 양쪽 URL 실측 필요.
|
||||
|
||||
- **Coherent rollback set (§12.5)**: prior HTML + prior asset manifest·assets + compatible runtime config + compatible API contract(또는 backend compatibility window) + release manifest를 **함께** 되돌린다. HTML만 과거로, runtime config는 최신 유지하는 rollback은 **금지**.
|
||||
- **Procedure invariant (§16.5)**: target release tuple 선택 → prior assets reachability 확인 → prior runtime config compatibility 확인 → active pointer atomic switch → provider cache action → boot+route+API critical smoke → telemetry/reload-loop 확인 → rollback record 저장.
|
||||
- **Recovery 판정**: cache purge *완료*가 아니라 old/new reachability probe 결과로 판정(§12.5).
|
||||
- **Evidence**: `artifacts/runbooks/FE-RB-005/<release-id>/record.json`; drill = `pnpm drill:runbook -- FE-RB-005`(`FE-GATE-016` rollback drill / `FE-GATE-025` FE-RB-005 drill).
|
||||
|
||||
### 5. Release coherence gate + mixed-version negative fixture
|
||||
|
||||
> **Trace**: D1·D4 — [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-OC-016 · FE-OC-017 · §8.2(DEPLOY_MISMATCH / RELEASE_MANIFEST_FAILURE) · §15.2(negative fixture “HTML build A + asset manifest B”). config-schema *검증 로직*은 [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] `FE-OC-004`(boot sequence §6.3) 소유 — 본 브랜치는 release/asset coherence 판정만.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: fixture를 구성하는 구체 mock 파일 세트·verify 스크립트 내부 알고리즘은 repository 확정 전 미권고 → 임의. trade-off: 최소 fixture(HTML A + manifest B)만으로 시작하면 config mismatch 조합은 별도 fixture 필요.
|
||||
|
||||
| Fixture | 기대 정규화 결과 (§8.2) |
|
||||
|---|---|
|
||||
| HTML(build A) + asset manifest(build B) | `DEPLOY_MISMATCH` — request retry 없이 controlled reload once 또는 rollback |
|
||||
| release manifest fetch/parse/schema 실패 | `RELEASE_MANIFEST_FAILURE` — boot 시 bounded refetch 1회, update/support shell |
|
||||
| chunk fetch 실패(release check 후) | `CHUNK_LOAD_FAILURE` — release check 후 controlled reload 1회만 |
|
||||
|
||||
- **Verification (§14.3)**: `pnpm verify:release` → `artifacts/release/verification.json`(compatibility tuple coherent); `FE-GATE-015` release coherence = mixed set은 mismatch detected, coherent set은 pass.
|
||||
- **schema = `ART-FE-003@1`** (hub §2.1.3 · `harness/source/artifact-schemas/ca-skeleton-frontend/release-verification.schema.json`). 위 `verification.json` 의 **Schema Owner 는 본 branch 단독**이며, 필드 추가·rename 은 스키마 파일을 고쳐 revision 을 올리는 것으로 한다. 소비 branch([[raw/branch-notes/feature-frontend-contract-compatibility-governance]])는 본문에 스키마를 옮겨 적지 않고 `imports` 에 `ART-FE-003@1` 로 pin 하므로, revision 이 오르면 낡은 pin 이 자동으로 잡힌다. 필드 명명은 hub §2.1.3 의 camelCase 규약을 따른다.
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로** (§8.2 / §16.2):
|
||||
- `DEPLOY_MISMATCH` (HTML/asset/config release mismatch): request retry 금지, controlled reload once 또는 rollback, telemetry = mismatch kind + IDs(raw 금지).
|
||||
- `RELEASE_MANIFEST_FAILURE` (manifest fetch/parse/schema 실패): boot 시 bounded refetch 1회만; 실패 시 reload하지 말고 update/support shell로 격리(§16.2 immediate containment 3).
|
||||
- `CHUNK_LOAD_FAILURE`: release manifest를 `no-store`로 1회 조회해 active release mismatch가 *확인된 경우에만* reload guard 기록 후 1회 reload; asset set incomplete면 prior coherent release로 rollback(§16.2 mitigation).
|
||||
- CDN propagation 불일치(origin 정상, edge stale): active switch를 되돌리고 reachability probe 재실행 후 hosting/CDN owner로 escalation(§16.2).
|
||||
- **다른 계약 의존**:
|
||||
- [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] `FE-OC-004` — runtime config publish + boot config validation(§6.3/§6.4)을 consume. config schema 계약이 바뀌면 compatibility tuple 판정과 coherent-set 정의에 영향.
|
||||
- [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] `FE-OC-018` — asset content-hash·`build-manifest.json`·`assetManifestHash`를 생성; 이것이 release coherence 입력. hashing 규칙이 바뀌면 asset immutability·mismatch 탐지 영향.
|
||||
- [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] `FE-OC-023` — additive/breaking/migration 규칙을 정의; rollback의 “compatible config/API” 판정이 이 규칙에 의존.
|
||||
- [[raw/branch-notes/feature-frontend-operational-runbook-contract]] `FE-OC-025` — FE-RB-002/FE-RB-005 runbook 서술과 drill orchestration 소유; 본 브랜치는 기술 escalation 대상 + drill evidence 요건 제공(`FE-GATE-016`/`FE-GATE-022`/`FE-GATE-025`).
|
||||
- [[raw/branch-notes/feature-frontend-render-recovery-boundary-contract]] `FE-OC-015` — `DEPLOY_MISMATCH` 사용자 recovery UI와 reload-loop 방지 소유; 본 브랜치는 normalized kind와 “reload once” 계약만 제공.
|
||||
- [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] `FE-OC-019` — `FE-GATE-019@2` 의 security-header 축에서 **검증 대상 정책 집합(CSP/HSTS/frame/referrer)을 공급**(D6). 그 정책이 바뀌면 본 gate 의 fixture·probe 기대값 재도출.
|
||||
- [[raw/branch-notes/feature-frontend-contract-registry-governance]] `FE-OC-022` — 8-registry single-owner/snapshot governance; `FE-REG-RELEASE`는 그 governance 하에 관리되는 registry.
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| rollback이 coherent prior release(HTML+assets+config+API+manifest)를 복구한다 | deploy artifact·drill evidence 없음 | mixed-version fixture + `pnpm drill:runbook -- FE-RB-005`(`FE-GATE-016`), recovery = old/new reachability probe pass | `needs-confirmation` |
|
||||
| 실제 hosting header가 선언 cache policy와 일치한다 | header 문자열·provider 미확정 | `pnpm verify:hosting-headers`(`FE-GATE-019@2`) → `hosting-headers.json` 대조 | `needs-confirmation` |
|
||||
| 실제 hosting 응답의 security header(CSP/HSTS/frame/referrer)가 선언 정책과 일치한다 | 정책 내용은 browser-security 공급분이고 provider 미확정 | 같은 `pnpm verify:hosting-headers` probe 에 security header 축 편입(`FE-GATE-019@2`) | `needs-confirmation` |
|
||||
| mixed HTML/asset/config가 `DEPLOY_MISMATCH`로 탐지되고 coherent set은 pass한다 | verify 스크립트·fixture 미구현 | `pnpm verify:release`(`FE-GATE-015`) mixed vs coherent fixture | `needs-confirmation` |
|
||||
| compatibility comparison이 string lexical compare를 쓰지 않는다 | comparator 미구현 | comparator unit test에 lexical-trap fixture(예: `"10"` vs `"9"`) 투입 → 정확 판정 확인 | `planned` |
|
||||
| atomic active-pointer 전환 중 HTML과 asset이 서로 다른 release인 window가 없다 | atomic primitive 미확정 | 배포 시뮬레이션 중 boot e2e + reachability probe | `needs-confirmation` |
|
||||
| service worker가 실제로 등록되지 않는다(D3) | 코드 없음 | production build 산출물 scan + e2e에서 SW registration 부재 확인 | `planned` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
- 스캐폴딩 단계: `/coverage` 실행 전 수동 행을 만들지 않는다.
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
- 없음 — 스캐폴딩 단계.
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-OC-019@1` | [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] | browser bundle에 secret을 넣지 않고 untrusted HTML injection을 기본 금지 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
<!-- GENERATED: artifact-imports:start -->
|
||||
### 가져온 artifact 계약
|
||||
|
||||
| Artifact Ref | Owner | Producer | Schema Ref |
|
||||
|---|---|---|---|
|
||||
| `ART-FE-001@1` | [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] | [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] | `harness/source/artifact-schemas/ca-skeleton-frontend/build-manifest.schema.json` |
|
||||
<!-- GENERATED: artifact-imports:end -->
|
||||
|
||||
- 없음 — 자식 자료는 생성 후 controller가 parent Cluster와 함께 등록한다.
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
- 없음 — daily note는 이 작업에서 수정하지 않는다.
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크: 없음
|
||||
- 리뷰 메모: 없음
|
||||
- 머지 결과 / 배포 환경: `planned`
|
||||
- **wiki 추출 대상** (verified만): 없음
|
||||
- **추출하지 않을 항목** (planned / documented-only / abandoned): 현재 전체
|
||||
+336
@@ -0,0 +1,336 @@
|
||||
---
|
||||
title: branch / feature-frontend-render-recovery-boundary-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-015
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-015
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-UI-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-ARCHITECTURE-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-007, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-013]
|
||||
contract_packet: 1
|
||||
branch: feature-frontend-render-recovery-boundary-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract.md]
|
||||
tags: [branch, ca-skeleton, frontend, error-handling, react]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
contract_packet_sha256: 2c336d6be17e196dcbbbf75409f97f8ff916672d7b634db5e6cf3e568000b57b
|
||||
imports: [FE-OC-008@1, FE-OC-011@1, FE-OC-014@1]
|
||||
accepts_delegations: [DELEG-FE-006@1]
|
||||
|
||||
---
|
||||
|
||||
# branch: feature-frontend-render-recovery-boundary-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — 단일 브랜치의 TODO·결정·진행 기록. 머지/종료 후 verified 결과는 `/ingest`로 `wiki/projects/`에 추출한다. 현재는 `/branch-spec` 자동 채움 단계이며 frontend 코드가 없으므로 모든 진술은 `planned`다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: boot·route·feature·async boundary ownership과 recovery fixture가 검증된다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-UI-001@1` | UI composition은 React를 사용한다 | React render boundary와 recovery surface에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-ARCHITECTURE-001@1` | domain, application, presentation, adapters, bootstrap 책임을 분리한다 | boot·route·feature·async boundary ownership과 adapter seam에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
|
||||
<!-- section-id: branch-local-decisions -->
|
||||
### 브랜치 지역 결정
|
||||
|
||||
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
||||
|---|---|---|---|---|
|
||||
| D1 | operational failure와 render defect를 분리한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D2 | boot·route·feature·async boundary ownership을 고정한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D3 | route별 error surface owner를 하나로 제한한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D4 | release pair별 controlled reload를 한 번으로 제한한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D5 | boot validation failure 시 product route 대신 boot shell을 렌더한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D6 | render failure telemetry를 best-effort로 emit한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
|
||||
<!-- section-id: declared-overrides -->
|
||||
### 선언한 예외
|
||||
|
||||
| Override ID | Overrides | Reason | Approval | Status |
|
||||
|---|---|---|---|---|
|
||||
|
||||
없음.
|
||||
<!-- GENERATED: branch-contract:end -->
|
||||
|
||||
<!-- section-id: branch-goal -->
|
||||
## 목표
|
||||
|
||||
이 브랜치는 project-wide 계약 `FE-OC-015`("expected operational error 와 render defect 를 MUST 분리하고 reload loop 를 금지")를 *구현 착수 가능한 상세 명세*로 내린다. 핵심은 두 불변식이다. (1) **분리(separation)** — 정규화된 *운영 실패*(error-classification 이 낸 26-kind operational error)는 컴포넌트의 *정상 state* 로 반환되며 render error boundary 로 throw 하지 않는다. render boundary 가 잡는 것은 *programmer defect 또는 invariant breach*(렌더 도중 던져진 예외)뿐이다([[raw/project-notes/ca-skeleton-frontend-operational-contract]] §10.1 마지막 문단, §8.2 `RENDER_FAILURE` 행). (2) **reload loop 금지** — chunk/deploy mismatch 복구용 자동 reload 는 §10.2 의 5개 controlled 조건과 `CHUNK_RELOAD_GUARD` 를 만족할 때 정확히 한 번만 허용되고, 같은 release pair 에서 두 번째 실패가 나면 auto reload 를 멈추고 rollback/support surface 로 넘어간다. 이 브랜치는 boot/route/feature/async 4계층 error boundary 의 *ownership*(무엇을 잡고·무엇을 안 잡고·어떻게 복구하는가)을 §10.1 매트릭스로 고정하고, 그 산출물을 세 계약에 기여한다 — `FE-OC-005`(route-level error/loading surface owner 와의 이중 소유 금지), `FE-OC-011`(async surface 의 terminal-error state 를 boundary 가 아닌 정상 state 로 소비), `FE-OC-025`(boot·chunk mismatch runbook 이 호출할 boundary/reload 메커니즘 제공). UI 기술은 React(`FE-D004`), 라우팅은 React Router Declarative Mode(`FE-D008`)를 전제한다.
|
||||
|
||||
- 이슈:
|
||||
- PR:
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- **operational-error vs render-defect 분리 계약** — 정규화된 운영 실패는 정상 state 로 반환, render boundary 는 defect/invariant breach 만 catch (hub §10.1·§8.2). 등급 `planned`.
|
||||
- **4계층 error boundary ownership 매트릭스** — boot shell / route boundary / feature boundary / async boundary 각각의 catches / does-not-catch / recovery 명세 (hub §10.1). 등급 `planned`.
|
||||
- **controlled reload + `CHUNK_RELOAD_GUARD` state machine** — §10.2 의 5개 조건, release pair 당 1회, 2번째 실패 시 rollback/support (hub §10.2·§5.5·§8.4 `reload-once`). 등급 `planned`.
|
||||
- **boot error shell** — §4.5 boot order 2~4단계 실패 시 product route 미마운트, boot error shell 만 렌더 (hub §4.5·§8.2 `BOOT_CONFIG_FAILURE`). 등급 `planned`.
|
||||
- **render-failure telemetry hook** — `ui.render.failed`(route_id·build_id·component_boundary) best-effort emit, sink 실패가 복구를 막지 않음 (hub §5.8·§10.1). 등급 `planned`.
|
||||
- **recovery fixtures / boundary 테스트** — §20 Measurable completion("boot/route/feature/async boundary ownership + recovery fixtures") + §8.5 관련 negative fixture. 등급 `planned`.
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적 제외. 각 항목은 소유 브랜치를 명시(CLAUDE.md §15.5 R3 OUT_OF_BRANCH_SCOPE 방지). 아래 sibling 링크는 hook quirk 회피를 위해 `FE-OC-###` 계약 ID 로만 짝지음(§4b).
|
||||
|
||||
- **실패의 정규화(어떤 exception → 어떤 kind)와 26-kind enum·`action` vocabulary** — [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] 의 `FE-OC-008` 소유. 본 브랜치는 정규화된 kind + `action`(특히 `reload-once`/`retry`/`navigate`)을 *소비*해 boundary 배치·복구만 결정한다.
|
||||
- **async surface state 렌더링**(initial-loading/success/empty/terminal-error 스켈레톤·문안) — [[raw/branch-notes/feature-async-ui-state-contract]] 의 `FE-OC-011` 소유. 본 브랜치는 "operational 실패는 boundary 가 아닌 정상 state 로 간다"는 *seam* 만 정의한다.
|
||||
- **route registry schema(`errorSurface`/`loadingSurface`/`chunkId` 필드)와 navigation guard** — [[raw/branch-notes/feature-routing-navigation-guard-contract]] 의 `FE-OC-005` 소유. 본 브랜치는 route boundary 가 그 owner 필드를 *채우되* 스키마·guard 로직은 정의하지 않는다.
|
||||
- **`CHUNK_RELOAD_GUARD` storage row 등록**(namespace/version/classification/quota fallback) — [[raw/branch-notes/feature-frontend-storage-registry-contract]] 의 `FE-OC-013` 소유. 본 브랜치는 guard 의 *의미*(reload loop 차단)만, 키 등록은 위임.
|
||||
- **release manifest·`DEPLOY_MISMATCH` 신호 생성 + rollback 실행** — [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] 의 `FE-OC-016` 소유. 본 브랜치는 그 신호를 *소비*해 controlled reload/rollback surface 로 분기만 한다.
|
||||
- **runbook 의 trigger/window/escalation/evidence** — [[raw/branch-notes/feature-frontend-operational-runbook-contract]] 의 `FE-OC-025` 소유. 본 브랜치는 그 runbook 이 호출할 boundary/reload 메커니즘만 제공한다.
|
||||
- **telemetry transport/queue/redaction sink** — [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] 의 `FE-OC-014` 소유. 본 브랜치는 `ui.render.failed` payload 계약만.
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | §10.1 error boundary ownership 매트릭스·§10.2 reload loop prevention·§8.2 `RENDER_FAILURE`/operational-vs-defect note·§4.5 boot order·§5.5 `CHUNK_RELOAD_GUARD`·§5.8 `ui.render.failed`·§8.4 `reload-once`·§9.3 route error owner 중복 금지 — `FE-OC-015` 의 project-decision SSOT. D1·D2·D3·D4·D5·D6 근거. |
|
||||
| [[raw/official-docs/react-ui-library-official]] | `FE-D004`(UI composition = React). **error boundary claim 은 이 자료에 없음** — `REACT-UI-C1` 은 "React 는 컴포넌트로 구성된다"만 증명하므로 render boundary *기술 전제*(React 사용)만 근거하고, boundary API 는 아래 web-research 로 보강. D2·D5 부분 근거. |
|
||||
| [[raw/official-docs/react-router-official]] | `FE-D008`(routing = React Router Declarative Mode). `REACT-ROUTER-C1`/`C4` 가 client-side route 선언을 근거. **route error element API 는 이 발췌 범위 밖**(archived doc 이 명시) → route boundary 의 정확한 error element 형태는 `UNSUPPORTED_IMPL_DECISION`. D3 부분 근거. |
|
||||
| react.dev 웹 조사(2026-07-19, `react.dev/reference/react/Component`) — 미아카이브 | React error boundary 메커니즘: `static getDerivedStateFromError`(+옵션 `componentDidCatch`)를 가진 컴포넌트가 자식이 *렌더 중* 던진 에러를 잡되 **event handler·async 코드·boundary 자신이 던진 에러는 잡지 않는다**. 이 사실이 "operational 실패는 boundary 로 throw 하지 않는다"(D1)를 강화. **후속: raw/official-docs 로 정식 아카이브 필요**(Claims To Verify). |
|
||||
|
||||
## TODO
|
||||
|
||||
각 항목 옆에 증거 등급 표기. 현재 frontend 코드가 없으므로 전부 `planned`.
|
||||
|
||||
- [ ] 4계층 boundary 컴포넌트 배치(`src/presentation/boundaries/*` + boot shell) — boot/route/feature/async catches·does-not-catch·recovery 구현 (D1/D2/D5) — 등급: `planned`
|
||||
- [ ] operational-vs-defect seam — 정규화된 kind 를 정상 state 로, defect 만 boundary 로 라우팅하는 경계 wiring (D1) — 등급: `planned`
|
||||
- [ ] `CHUNK_RELOAD_GUARD` controlled reload state machine — 5조건 순서 + release pair 2회차 중단 (D4) — 등급: `planned`
|
||||
- [ ] boot error shell — composition root 가 §4.5 2~4단계 실패 시 product route 미마운트 (D5) — 등급: `planned`
|
||||
- [ ] `ui.render.failed` best-effort telemetry hook — boundary catch 시 emit, 재귀·차단 없음 (D6) — 등급: `planned`
|
||||
- [ ] recovery fixtures — reload-loop deterministic test + operational-vs-defect fixture + boot invalid-config → boot shell (D1~D5) — 등급: `planned`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
없음 — `/branch-spec` 자동 채움 단계. 코드 미착수. React error boundary 는 class-component 전용 API(`getDerivedStateFromError`)라는 점을 web 조사로 확인했고, 정식 아카이브는 후속 dispatch 로 남긴다.
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> 아래 6개 결정은 Decision Evidence Map 의 prose mirror. 근거는 대부분 hub 의 project decision(§10·§8·§4.5·§5)이며, D2/D3 는 외부 자료(React·React Router·react.dev web) 가 기술 전제로 병행 근거.
|
||||
|
||||
- 2026-07-19: **operational error 와 render defect 를 분리 — 정규화된 운영 실패는 정상 state 로 반환, render boundary 는 defect/invariant breach 만 catch** / 이유: 운영 실패를 boundary 로 throw 하면 async·event-handler 경로에서 애초에 안 잡히고(React error boundary 는 그 경로를 catch 하지 않음) 정상 복구 UX(재시도·stale)를 blank crash 로 격하 / 검토한 대안: 모든 실패를 throw 해 단일 boundary 로 처리 — React 가 event/async 를 안 잡으므로 불완전, hub §10.1 default 위반으로 기각 / 근거: hub §10.1·§8.2, react.dev error boundary 조사.
|
||||
- 2026-07-19: **boot / route / feature / async 4계층 boundary ownership 을 §10.1 매트릭스로 고정** / 이유: 계층마다 catch 대상·복구가 달라(config vs lazy chunk vs subtree defect vs 정규화 state) 단일 boundary 는 복구 granularity 를 잃음 / 검토한 대안: 전역 단일 boundary — route 1개·lazy chunk 0·외부 API 0 throwaway(hub §0.4)에서만 / 근거: hub §10.1·§4.5·§9.3.
|
||||
- 2026-07-19: **route error surface 의 이중 소유 금지 — route element 와 React error boundary 중 route 당 정확히 하나가 owner** / 이유: 둘 다 소유하면 같은 render 실패를 두 번 처리하거나 복구가 충돌 / 검토한 대안: 둘 다 두고 우선순위 규칙 — 복잡·모호로 기각 / 근거: hub §9.3 의 *비중복 owner* 원칙(이 결정의 실제 grounding), React Router `REACT-ROUTER-C1`/`C4`(Declarative Mode 의 client-side route 선언). **전제의 한계 명시**: Declarative Mode 가 *route 레벨 error API 자체*(존재 여부·형태)를 제공하는지는 아카이브된 발췌 범위 밖이므로 미확정이다 — 즉 이 결정이 강제하는 것은 "route element 계층에 error API 가 있으면 boundary 와 이중 소유하지 말 것"이라는 비중복 규칙이지, 그 API 의 존재를 주장하는 것이 아니다. 정식 아카이브는 source 후속(Claims To Verify 마지막 행).
|
||||
- 2026-07-19: **controlled reload 는 `CHUNK_RELOAD_GUARD` 로 release pair 당 1회, 2회차 실패 시 중단** / 이유: `ChunkLoadError`/deploy mismatch 를 무한 reload 로 대응하면 boot loop / 검토한 대안: guard 없는 즉시 reload — §8.4 가 금지(`reload-once` MUST NOT: session guard 없이 반복 reload) / 근거: hub §10.2 5조건·§5.5·§8.4·§8.2.
|
||||
- 2026-07-19: **boot config/release 검증 실패 시 product route 미마운트, boot error shell 만 렌더** / 이유: 잘못된 config 로 앱을 띄우면 endpoint mismatch·secret 노출·부분 렌더 위험 / 검토한 대안: 실패해도 기본값으로 진행 — §4.5 가 2~4단계 실패를 hard stop 으로 규정, 기각 / 근거: hub §4.5·§8.2 `BOOT_CONFIG_FAILURE`.
|
||||
- 2026-07-19: **render-failure telemetry(`ui.render.failed`)는 best-effort, sink 실패가 복구·재렌더를 막지 않음** / 이유: 관측이 복구를 blocking 하면 안 됨(operational isolation) / 검토한 대안: 전송 보장 채널 — audit 채널은 별도 owner(FE-OC-014), 기각 / 근거: hub §5.8·§10.1, `FE-D021`.
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
> `Supporting Claims` 는 hook quirk 회피를 위해 hub 는 plain-text 경로(`...frontend-operational-contract.md §X`)로, official-doc claim 은 plain-text `raw/official-docs/<slug>.md#<CLAIM>` 로, `FE-D###` 는 hub 경로에만 붙여 sibling branch 링크 근처에 두지 않는다(§4b).
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | operational error(정규화된 26-kind 운영 실패)는 정상 컴포넌트 state 로 반환하고 render error boundary 로 throw 하지 않음; boundary 는 programmer defect / invariant breach(렌더 중 throw)만 catch (`FE-OC-015`) | error-classification 이 실패를 총함수로 정규화하는 한(hub `FE-OC-008`) 이 default 유지 / "throw 후 boundary 처리" 대안은 정규화 계층이 없을 때만인데 hub 가 그것을 강제하므로 분기 없음(불변식) | `...frontend-operational-contract.md` §10.1 마지막 문단·§8.2 `RENDER_FAILURE` 행 및 total-function 문단; `raw/official-docs/react-ui-library-official.md#REACT-UI-C1`(React 사용); react.dev `Component`(error boundary 는 event handler·async·boundary 자체 throw 를 catch 안 함 → operational 을 throw 로 흘리면 애초에 미포착) | `project-decision + official-vendor-doc(web, 미아카이브)` | 총함수적 분리는 fixture 로만 증명 — operational 실패가 실수로 throw 되거나 boundary 가 실제 defect 를 operational 로 오분류하면 crash/은닉. exhaustive boundary fixture 필요 |
|
||||
| D2 | boot shell / route boundary / feature boundary / async boundary 4계층 ownership 을 §10.1 매트릭스(각 계층의 catches·does-not-catch·recovery)로 고정 | client SPA + lazy route chunk + async 데이터(React·React Router·TanStack Query) 구성인 한 4계층 default / 전역 단일 boundary 는 route 1개·lazy chunk 0·외부 API 0 throwaway prototype(hub §0.4 escape)에서만 | `...frontend-operational-contract.md` §10.1 boundary 매트릭스·§4.5 boot order(2~4단계 실패→boot error shell)·§9.3 route 동작; `raw/official-docs/react-ui-library-official.md#REACT-UI-C1` | `project-decision` | async boundary 는 실제로 "throw 를 잡는 boundary"가 아니라 정규화 state 소비 surface(§10.1 행) — `FE-OC-011` 과의 소유 seam 이 모호하면 이중 처리. seam 명세 필요 |
|
||||
| D3 | route 당 error surface 는 React Router route element 와 React error boundary 중 *정확히 하나*가 owner; 이중 소유 금지 (규칙의 grounding 은 hub §9.3 비중복 owner 원칙이며, Declarative Mode 의 route-error API 존재 자체를 주장하지 않음) | Declarative Mode(`FE-D008`) 의 route element 계층이 error surface 를 소유할 수 있는 한 route 별 owner 를 하나 지정 / 그 계층에 error API 가 없으면 owner 는 전부 React error boundary 로 고정(규칙 자체는 유지, 위반 여지 소멸) / Data/Framework Mode 로 전환되면(그 mode 의 `errorElement`/loader 계약) 재도출 | `...frontend-operational-contract.md` §9.3("route error element 와 React error boundary 의 owner 를 중복하지 않는다"); `raw/official-docs/react-router-official.md#REACT-ROUTER-C1`, `#REACT-ROUTER-C4`; `...frontend-operational-contract.md` `FE-D008` | `project-decision + conditional-default(React Router)` | archived router doc 은 error element API 를 다루지 않음 → 정확한 error element 형태는 `UNSUPPORTED_IMPL_DECISION`; owner 선정 규칙이 route 별로 일관되지 않으면 §9.3 위반 |
|
||||
| D4 | chunk/deploy mismatch 복구 자동 reload 는 §10.2 5조건(kind∈{`CHUNK_LOAD_FAILURE`,`DEPLOY_MISMATCH`}·release manifest fetch 성공·active release≠current build·`CHUNK_RELOAD_GUARD` unset·guard 선기록 후 reload)을 모두 만족할 때 release pair 당 1회; 같은 pair 2회차 실패 시 auto reload 중단→rollback/support | mismatch 가 감지되고 manifest 가 *더 새로운* release 를 확인할 때만 reload / manifest fetch 실패·같은 pair 이미 guard·storage 불가면 no auto reload(update/support surface) | `...frontend-operational-contract.md` §10.2 5조건·§5.5 `CHUNK_RELOAD_GUARD`(sessionStorage / session / no second auto reload)·§8.4 `reload-once`(MUST NOT: session guard 없이 반복 reload)·§8.2 `CHUNK_LOAD_FAILURE`/`DEPLOY_MISMATCH` 행 | `project-decision` | guard 가 sessionStorage → StoragePort unavailable(private mode)·cross-tab 시 guard 미지속 가능 → fail-safe 로 no-auto-reload 강등 필요. deterministic reload test 로 2회차 중단 증명 |
|
||||
| D5 | boot config/release 검증(§4.5 2~4단계) 실패 시 product route 를 mount 하지 않고 boot error shell 만 렌더; telemetry adapter 생성 실패(7단계)는 console-safe fallback 으로 계속 | boot order 2~4단계(runtime config fetch·schema·compatibility·release manifest) 실패 → boot error shell / telemetry 등 비필수 adapter 실패 → 계속 진행 | `...frontend-operational-contract.md` §4.5 boot order + 실패 규칙·§8.2 `BOOT_CONFIG_FAILURE`/`RELEASE_MANIFEST_FAILURE` 행 | `project-decision` | boot shell 자체가 실패한 config 에 의존하면 안 됨(zero-config 로 렌더 가능해야) — 미검증 시 boot shell 이 같은 실패로 재크래시. boot invalid-config fixture 필요 |
|
||||
| D6 | render-failure telemetry(`ui.render.failed`: route_id·build_id·component_boundary)는 best-effort emit, sink/queue 실패가 복구·재렌더를 막지 않음 | telemetry 가 best-effort isolation(`FE-D021`)인 한 항상 non-blocking / 전송 보장이 필요한 audit event 는 별도 owner(`FE-OC-014`) 채널이므로 본 결정 밖 | `...frontend-operational-contract.md` §5.8 `ui.render.failed` event·§10.1 feature boundary recovery; `...frontend-operational-contract.md` `FE-D021` | `project-decision (transport delegated to FE-OC-014)` | boundary 의 `componentDidCatch` 안 telemetry 호출이 throw 하면 boundary 자신이 throw(react.dev: boundary 자체 throw 는 미포착) → 상위 boundary 로 전파. emit 은 try/catch 로 감싸야 함 |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> `planned` blueprint. 경로는 hub §4.6 Planned directory blueprint(`src/presentation/boundaries/`, `src/bootstrap/`) + §5 registry 에서 유도되므로 grounded 하지만 repository 생성 시 바뀔 수 있어 전체 `planned`. sibling 소유 detail 은 §범위 Out of scope 로 위임하고 여기 남기지 않는다(R3).
|
||||
|
||||
### 1. 4계층 error boundary 배치 (`src/presentation/boundaries/` + boot shell)
|
||||
|
||||
> **Trace**: D1 + D2 + D5 / `FE-OC-015`·hub §10.1 매트릭스·§4.5. React error boundary 는 `static getDerivedStateFromError`(+옵션 `componentDidCatch`)를 가진 컴포넌트가 자식의 *렌더 중* throw 를 catch(react.dev 조사).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: boundary 구현 방식(hand-rolled class vs `react-error-boundary` 라이브러리) — hub·archived doc 미규정. hand-rolled class 컴포넌트(외부 의존 0) 제안. trade-off: 라이브러리는 reset/fallback API 가 편하지만 supply-chain(`FE-OC-018`) 표면 추가; class 는 boilerplate 지만 의존 0.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: boundary 컴포넌트·파일명(hub 는 `presentation/boundaries/` 폴더만 grounding) — `RouteErrorBoundary.jsx`/`FeatureErrorBoundary.jsx`/`BootErrorShell.jsx` 제안. trade-off: 이름 임의, "presentation/boundaries 내부 + 계층당 1 컴포넌트" 제약만 유지하면 계약 동등.
|
||||
|
||||
| 계층 | catches (hub §10.1) | does NOT catch | recovery | planned 배치 |
|
||||
|---|---|---|---|---|
|
||||
| boot shell | config/release/bootstrap 실패 | product route error | config refetch·support·rollback signal | `src/bootstrap/` composition root (D5) |
|
||||
| route boundary | route 의 lazy chunk / render 실패 | expected API result(정규화 state) | route retry 또는 controlled reload(D4) | `presentation/boundaries/` route 래핑 |
|
||||
| feature boundary | 컴포넌트 subtree render defect | 정규화된 operational failure | component reset | `presentation/boundaries/` subtree 래핑 |
|
||||
| async boundary | 정규화된 query/mutation state | throw 된 render defect | registry `action` | `FE-OC-011` async surface 와 공유 seam(D2) |
|
||||
|
||||
### 2. operational-vs-defect seam (정규화 kind 라우팅)
|
||||
|
||||
> **Trace**: D1 / `FE-OC-015`·hub §10.1·§8.2. error-classification(`FE-OC-008`)이 낸 정규화 kind 를 *소비*만 하며 정규화 자체는 하지 않는다(R3 위임).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — 분기 원칙은 hub §10.1(operational→정상 state, defect→boundary)이 직접 grounding. 실제 kind→state/action 매핑 값은 `FE-OC-008`/`FE-OC-011` 소유.
|
||||
|
||||
```text
|
||||
정규화된 failure(kind, action) 수신 → async/feature 계층의 정상 state 로 렌더 (terminal-error/stale-degraded 등, action 은 FE-OC-011 소유)
|
||||
렌더 중 throw(non-normalized) 발생 → 가장 가까운 feature/route boundary 가 catch → RENDER_FAILURE recovery
|
||||
boundary 가 catch 한 값이 정규화 실패로 판명 → 재-throw 금지, RENDER_FAILURE 로 처리(operational 은닉 방지는 fixture 로 검증)
|
||||
```
|
||||
|
||||
### 3. controlled reload + `CHUNK_RELOAD_GUARD` state machine
|
||||
|
||||
> **Trace**: D4 / `FE-OC-015`·hub §10.2·§5.5·§8.4. guard 저장은 StoragePort 경유(`FE-OC-013` 소유 registry 의 `CHUNK_RELOAD_GUARD` row 를 *소비*).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: reload 결정 로직 위치(boundary 내부 vs release adapter) — hub 미규정. release adapter(`ReleaseInfoPort` 구현, §4.4)가 mismatch 판정, boundary 는 그 결과로 reload/rollback surface 분기 제안. trade-off: adapter 집중이 test 용이하나 boundary→adapter 호출 경계 추가.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: guard 값 shape(§5.5 는 "session / no second auto reload"만) — `<activeReleaseId>:<currentBuildId>` pair 키 + boolean 제안. trade-off: pair 키여야 "같은 pair 2회차"를 판별; 단일 flag 면 서로 다른 release 간 오차단.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: dirty-state 선경고 훅의 호출 위치 — [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §10.2 의 5조건은 warn-first 를 포함하지 않고, §16.2 immediate containment 1단계가 "current user input 이 있으면 destructive reload 전에 경고"를 *별도로* 규정한다(두 절의 결합 지점은 hub 미규정). 조건 4 통과 후·조건 5(guard 기록 → reload) 직전 호출 제안. trade-off: 이 위치면 경고가 실제 reload 직전 1회만 뜨고 사용자가 취소해도 guard 미기록이라 이후 재시도가 가능하다; 앞으로 당기면(조건 1 직후) mismatch 도 아닌 경우까지 경고해 소음이 된다.
|
||||
|
||||
```text
|
||||
1. failure kind ∈ {CHUNK_LOAD_FAILURE, DEPLOY_MISMATCH} ? 아니면 → reload 안 함
|
||||
2. release manifest fetch 성공 ? 실패 → no reload, update/support(RELEASE_MANIFEST_FAILURE 는 FE-OC-016 소유)
|
||||
3. active release ≠ current build ? 같으면 → no reload(mismatch 아님)
|
||||
4. CHUNK_RELOAD_GUARD[pair] unset ? set 이면 → auto reload 중단, rollback/support surface
|
||||
4b. dirty-state 선경고 훅(runbook 소유) 호출 → 사용자가 취소하면 reload 안 함(guard 미기록)
|
||||
5. guard[pair] 기록 후 → 1회 reload
|
||||
```
|
||||
|
||||
> **각주 — dirty-state seam 교차 참조**: 위 4b 는 본 브랜치가 새로 만드는 정책이 아니라 *이미 존재하는 두 계약을 명시적으로 잇는 자리*다. [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §19 의 `FE-RISK-009`("chunk auto reload 가 user input 손실", mitigation = dirty-state guard + one reload cap)는 두 짝으로만 닫힌다 — *one reload cap* 은 본 절의 guard 가 제공하고, *dirty-state guard(warn-first)* 는 §16.2 immediate containment 1단계가 규정한다.
|
||||
>
|
||||
> 그 warn-first step 과 위 risk row 의 owner 는 [[raw/branch-notes/feature-frontend-operational-runbook-contract]] (`FE-OC-025`) 이다. 본 브랜치는 훅을 *호출*만 하며 "무엇이 dirty 인가"의 판정 기준·경고 문안·취소 UX 는 그 소유다(R3 위임). 이 seam 을 적지 않으면 본 절의 자동 reload 가 runbook 의 warn-first 가정을 조용히 우회하고, 두 문서가 *암묵적으로만* 일관된 상태로 남는다.
|
||||
|
||||
### 4. boot error shell
|
||||
|
||||
> **Trace**: D5 / `FE-OC-015`·hub §4.5·§8.2. composition root(`src/bootstrap/composition-root.js`, §4.5)가 boot order 를 소유.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: boot shell 컴포넌트명·위치(hub 는 `bootstrap/` 만) — `src/bootstrap/BootErrorShell.jsx` + composition-root 가 2~4단계 실패 시 이것만 mount 제안. trade-off: 이름 임의; "zero runtime config 로 렌더 가능 + product route 미마운트" 제약만 유지.
|
||||
|
||||
- boot order §4.5 의 2단계(runtime config fetch)~4단계(release manifest 정합) 실패 → `BOOT_CONFIG_FAILURE`/`RELEASE_MANIFEST_FAILURE` → boot error shell 만 렌더(product route 미마운트).
|
||||
- 7단계(telemetry adapter) 생성 실패 → console-safe fallback, boot 계속(§4.5).
|
||||
- boot shell 은 실패한 config 에 의존 불가 — build-time 상수(§6.1 build-time public)만 참조.
|
||||
|
||||
### 5. render-failure telemetry hook
|
||||
|
||||
> **Trace**: D6 / `FE-OC-015`·hub §5.8·§10.1. transport/redaction sink 는 `FE-OC-014` 소유(R3) — 본 절은 emit 시점·payload 계약만.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음(payload 는 §5.8 이 required attribute 를 grounding). emit 실패 격리 방식만: `componentDidCatch` 내 emit 을 try/catch 로 감싸 재귀·전파 차단 제안(react.dev: boundary 자체 throw 는 상위로 전파).
|
||||
|
||||
- `componentDidCatch`(또는 등가 hook)에서 `ui.render.failed`{route_id, build_id, component_boundary} best-effort emit.
|
||||
- emit 은 try/catch — 실패해도 fallback UI 렌더·recovery 를 막지 않음(§10.1·§5.8).
|
||||
|
||||
### 6. boundary 테스트 (§20 Measurable completion)
|
||||
|
||||
> **Trace**: D1 + D2 + D3 + D4 + D5 / hub §20("boot/route/feature/async boundary ownership + recovery fixtures")·§8.5·§10.2.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: test 파일 경로·러너 배치(hub §4.6 은 `tests/component|integration` 폴더만) — `tests/component/boundaries/*` + `tests/integration/reload-guard/*` 제안. trade-off: 경로 임의, "component 레벨 boundary + integration 레벨 reload state machine" 계약만 유지.
|
||||
|
||||
| Fixture | 기대 결과 |
|
||||
|---|---|
|
||||
| async operation 실패(정규화 kind) | boundary 미발동, async surface 의 terminal-error/stale state 로 렌더(operational 은 정상 state) |
|
||||
| 컴포넌트 render 중 throw | 가장 가까운 feature/route boundary 가 catch → `RENDER_FAILURE` recovery |
|
||||
| boundary 자체 throw | 상위 boundary/boot shell 로 전파(react.dev), 무한 루프 없음 |
|
||||
| `CHUNK_LOAD_FAILURE` 1회차 + manifest 새 release | guard 기록 후 1회 reload |
|
||||
| 같은 release pair 2회차 실패 | auto reload 중단 → rollback/support surface(§10.2) |
|
||||
| StoragePort unavailable | guard 미지속 → fail-safe no-auto-reload |
|
||||
| boot invalid runtime config | product route 미마운트, boot error shell 렌더(D5) |
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- *boundary 자체가 render 중 throw* → React error boundary 는 자신이 던진 에러를 catch 하지 않음(react.dev) → 상위 boundary 또는 boot shell 이 처리. 최상위(boot shell)까지 throw 되면 최소 static crash surface.
|
||||
- *event handler / async(setTimeout 등)에서 발생한 에러* → React error boundary 미포착(react.dev) → 반드시 error-classification 이 정규화한 operational failure 로 다뤄 정상 state 로 표현(D1). boundary 에 의존하면 blank crash.
|
||||
- *StoragePort unavailable/quota*(private mode 등) → `CHUNK_RELOAD_GUARD` 미지속 → fail-safe 로 auto reload 강등(no reload, update/support). guard 부재를 "unset"으로 오해해 무한 reload 하면 안 됨.
|
||||
- *release manifest fetch 실패* → controlled reload 2단계 불충족 → no reload; `RELEASE_MANIFEST_FAILURE` 자체 생성은 `FE-OC-016` 소유.
|
||||
- *route element 와 boundary 이중 소유* → 같은 실패 두 번 처리/복구 충돌 → route 당 owner 1개(D3)로 정적 방지.
|
||||
- **다른 계약 의존**(hook quirk 회피: sibling 링크는 `FE-OC-###` 로만 짝지음):
|
||||
- [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] (`FE-OC-008`) — 정규화된 kind + `action` 을 *생성*. 그 계약(어떤 exception→어떤 kind, operational vs `RENDER_FAILURE` 구분)이 바뀌면 본 브랜치 seam(D1) 재조정. 해당 sibling 은 error boundary·reload-guard 소유를 이미 본 브랜치로 위임함.
|
||||
- [[raw/branch-notes/feature-async-ui-state-contract]] (`FE-OC-011`) — async surface state(initial-loading/success/empty/terminal-error) 렌더를 *소유*. 본 브랜치는 "operational 은 boundary 아닌 정상 state" seam(D2)만; state 문안·스켈레톤은 그 소유.
|
||||
- [[raw/branch-notes/feature-routing-navigation-guard-contract]] (`FE-OC-005`) — route registry(`errorSurface`/`chunkId`)를 *소유*. route boundary 가 그 owner 필드를 채우되 스키마는 그 소유(D3).
|
||||
- [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] (`FE-OC-016`) — release manifest·`DEPLOY_MISMATCH` 신호·rollback 실행을 *생성/소유*. 본 브랜치는 소비해 reload/rollback 분기(D4).
|
||||
- [[raw/branch-notes/feature-frontend-storage-registry-contract]] (`FE-OC-013`) — `CHUNK_RELOAD_GUARD` storage row 를 *소유*. 본 브랜치는 guard 의미만(D4).
|
||||
- [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] (`FE-OC-004`) — build/runtime/secret 분리와 **boot 전 runtime config 검증을 *소유*하며, 그 검증 실패가 본 브랜치 boot error shell 을 발동시키는 `BOOT_CONFIG_FAILURE` 신호를 *생성*** 한다(D5). hub §20 이 그 브랜치의 measurable completion 을 "build/runtime/secret registry + boot invalid matrix" 로 규정하므로 *어떤 config 가 invalid 인가*의 판정은 그쪽 소유이고, 본 브랜치는 그 신호를 소비해 "product route 미마운트 + shell 렌더" 분기만 한다. invalid matrix 의 kind 매핑(§8.2 행)이 바뀌면 D5·구현 가이드 §4 재조정.
|
||||
- [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] (`FE-OC-002`) — **boot error shell 을 호스팅하는 composition root(`bootstrap` 단일 root)를 *소유*** 한다(hub §4.2 `bootstrap` 행: config load·adapter construction·DI·React mount). 본 브랜치는 그 host 를 *소비*하는 쪽이며, root 가 adapter 를 주입하는 wiring 컨벤션(주입 순서·DI 형태·단일 root 불변식)은 그쪽 소유다(R3 위임). 본 브랜치가 명세하는 것은 hub §4.5 boot order 2~4단계 실패 시의 *분기 규칙*(shell 만 mount)뿐이다(D5).
|
||||
- [[raw/branch-notes/feature-frontend-operational-runbook-contract]] (`FE-OC-025`) — boot·chunk mismatch runbook 을 *소유*. 본 브랜치가 제공하는 boundary/reload 를 *소비*. 역방향으로, runbook 이 소유한 destructive-reload 선경고 step 을 본 브랜치 reload state machine 이 *호출*한다(구현 가이드 §3 각주).
|
||||
- [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] (`FE-OC-014`) — telemetry transport/sink 를 *소유*. 본 브랜치는 `ui.render.failed` payload 계약만(D6).
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| reload loop 가 실제로 차단된다 — 같은 release pair 2회차 실패 시 auto reload 안 함 | state machine·guard 미구현 | deterministic reload-guard test — 1회차 reload 기록 후 2회차 → reload 미호출 assert(§10.2) | `needs-confirmation` |
|
||||
| operational 실패가 render boundary 에 절대 도달하지 않고, defect 만 도달한다 | seam 미구현, 오분류 가능 | fixture: async operational 실패 → terminal-error state(boundary 미발동) / 렌더 throw → boundary catch → `RENDER_FAILURE` | `needs-confirmation` |
|
||||
| boot error shell 이 실패한 runtime config 에 의존하지 않고 렌더된다 | boot shell 미작성 | boot invalid-config matrix → product route 미마운트 + shell 렌더, shell 이 runtime config 미참조 assert | `needs-confirmation` |
|
||||
| boundary 의 `componentDidCatch` telemetry emit 이 재귀·전파를 일으키지 않는다 | emit try/catch 미구현 | telemetry sink throw mock → boundary 가 재-throw 안 하고 fallback 렌더 assert | `needs-confirmation` |
|
||||
| StoragePort unavailable 시 guard 가 fail-safe(no-auto-reload)로 강등된다 | fallback 경로 미설계 | storage unavailable mock → reload 미호출 + update/support surface assert | `needs-confirmation` |
|
||||
| React error boundary 가 event-handler·async·자체 throw 를 catch 하지 않는다는 전제 | react.dev web 조사만, vault 미아카이브 | `react.dev/reference/react/Component` 를 `wiki-source-summarizer` 로 `raw/official-docs/` 정식 아카이브(verbatim quote + self-grep) 후 D1 근거 승격 | `planned` |
|
||||
| route 당 error surface owner 가 정확히 하나다(이중 소유 없음) | route element API 미확정(archived doc 미포함) | route element vs boundary owner 지정 규칙 test + React Router error element 공식 문서 보강 | `planned` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
> `/coverage` 가 채우는 생성물이며 손으로 유지하지 않는다.
|
||||
|
||||
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| TODO — `/coverage` 실행 전 | missing | (없음) | 미평가 | TODO |
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: received-delegations:start -->
|
||||
### 수신한 위임
|
||||
|
||||
| Delegation Ref | From | Concern | Status |
|
||||
|---|---|---|---|
|
||||
| `DELEG-FE-006@1` | [[raw/branch-notes/feature-async-ui-state-contract]] | `fe.deleg.reload-once-action` | accepted |
|
||||
<!-- GENERATED: received-delegations:end -->
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-OC-008@1` | [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] | 모든 failure는 stable frontend error kind로 MUST 정규화하고 raw body·stack을 UI에 노출하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-011@1` | [[raw/branch-notes/feature-async-ui-state-contract]] | async surface는 initial-loading, success, empty, terminal-error를 MUST 표현 | import 참조로 적용 |
|
||||
| `FE-OC-014@1` | [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] | telemetry는 best-effort이며 render·API success를 차단하면 안 되고 PII·token을 전송하면 안 됨 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
### Sub-branches (세부 작업)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 오류 기록 (이 branch 작업 중 발생)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 강의 (이 작업을 위해 학습한 강의)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### job-posting tie-ins (이 작업에서 파생된 글감)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크:
|
||||
- 리뷰 메모:
|
||||
- 머지 결과 / 배포 환경:
|
||||
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
|
||||
- `actually-implemented` 항목:
|
||||
- `locally-verified` 항목:
|
||||
- `prod-verified` 항목:
|
||||
- **추출하지 않을 항목** (planned / documented-only / abandoned):
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
---
|
||||
title: branch / feature-frontend-storage-registry-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-011
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-011
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-REGISTRY-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-002]
|
||||
contract_packet: 1
|
||||
branch: feature-frontend-storage-registry-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract.md]
|
||||
tags: [branch, ca-skeleton, frontend, persistence, security, javascript]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
contract_packet_sha256: aa55f276dab66670f66f2424f059e394925f5f1c84ec23381fe506064f4823a1
|
||||
imports: [FE-GATE-005@1, FE-OC-002@1, FE-OC-010@1, FE-OC-019@1, FE-OC-023@1]
|
||||
---
|
||||
|
||||
# branch: feature-frontend-storage-registry-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — 단일 브랜치의 TODO·결정·진행 기록. 현재는 `/branch-spec` 로 채운 `planned` 명세 단계다 (frontend repository 미생성).
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: namespace·version·classification·quota fallback test가 통과한다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-REGISTRY-001@1` | route, API operation, env, storage, error, query, telemetry, release token을 8개 registry로 관리한다 | FE-REG-STORAGE namespace·version·classification schema에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
|
||||
<!-- section-id: branch-local-decisions -->
|
||||
### 브랜치 지역 결정
|
||||
|
||||
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
||||
|---|---|---|---|---|
|
||||
| D1 | storage key를 FE-REG-STORAGE와 versioned namespace로 관리한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D2 | storage item classification을 필수로 둔다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D3 | schemaVersion mismatch를 migration 또는 discard로 처리한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D4 | application-owned StoragePort와 storage adapter를 사용한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D5 | quotaFallback을 registry field로 관리한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D6 | token·secret·PII·raw payload 저장을 거부한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
|
||||
<!-- section-id: declared-overrides -->
|
||||
### 선언한 예외
|
||||
|
||||
| Override ID | Overrides | Reason | Approval | Status |
|
||||
|---|---|---|---|---|
|
||||
|
||||
없음.
|
||||
<!-- GENERATED: branch-contract:end -->
|
||||
|
||||
<!-- section-id: branch-goal -->
|
||||
## 목표
|
||||
|
||||
이 브랜치는 project-wide 계약 `FE-OC-013` (browser storage key 는 namespace·version·classification 을 MUST 보유하고 token/secret 저장을 금지) 를, 다음 구현자가 되묻지 않고 `src/contracts/storage-keys.js` 와 `adapters/storage` 를 작성할 수 있는 implementation-ready 명세로 내린다. `FE-REG-STORAGE` 는 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D018` 이 규정한 8개 registry 중 하나이며 본 브랜치가 single owner 다. 최소 스키마는 §5.5, 런타임 동작은 §9.4, 브라우저 보안 불변식(번들·storage = 공개물, secret 저장 금지)은 §13.2, quota/unavailable 실패 정규화는 §8.2 에 근거한다. 아직 frontend repository 가 없으므로 본 브랜치의 모든 항목은 `planned` 등급이다.
|
||||
|
||||
- 이슈: TODO (아직 없음)
|
||||
- PR: TODO (아직 없음)
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- `FE-REG-STORAGE` registry 스키마 정의 및 single-owner 소유 (§5.5): `logicalName` / `physicalKey` (`<app>:<scope>:v<schema>:<name>`) / `backend` / `classification` / `schemaVersion` / `ttl` / `migration` / `quotaFallback` 필드 계약.
|
||||
- 구조화된 physical key 규약(namespace + schema version 내장) + raw literal key 금지 강제.
|
||||
- classification 3분류(`public-preference` / `opaque-cache` / `sensitive-forbidden`) + sensitive 저장 금지 불변식.
|
||||
- `schemaVersion` + previous-version migration-or-discard 규약.
|
||||
- 단일 application 소유 `StoragePort` + `adapters/storage` 어댑터 boundary, try/catch 로 unavailable / security / quota 구분.
|
||||
- quota fallback 정책(`memory` / `no-persist` / `feature-disable`) + correctness-critical 값의 fallback 금지.
|
||||
- storage 관련 negative fixture: token key 등록 시도 실패(§15.2), quota-exceeded → memory fallback, 미등록 raw key 사용 금지.
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외 — 다른 owner 브랜치가 소유. CLAUDE.md §15.5 R3(OUT_OF_BRANCH_SCOPE) 준수.
|
||||
|
||||
- Token / refresh token / auth session material 의 lifecycle·저장 위치 — 외부 auth owner + [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] (`FE-OC-010`) 소유. 본 registry 는 이를 `sensitive-forbidden` 으로 *거부* 만 한다.
|
||||
- CSP / header / secret-scan 등 브라우저 보안 경계 전반 — [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] (`FE-OC-019`) 소유. 본 브랜치는 storage 관련 fixture 만 기여.
|
||||
- Query cache 의 in-memory 정책·persistence 활성화 — [[raw/branch-notes/feature-server-state-caching-contract]] (`FE-OC-012`) 소유. 본 registry 는 opt-in persistence 가 요구하는 storage key 계약만 제공.
|
||||
- 8-registry governance 전반의 single-owner / compatibility 추적 메커니즘 — [[raw/branch-notes/feature-frontend-contract-registry-governance]] (`FE-OC-022`) 소유. 본 브랜치는 storage registry 스냅샷 1개를 기여.
|
||||
- storage schema 의 breaking-change migration / version-bump 판정 규약 — [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] (`FE-OC-023`) 소유. 본 브랜치는 `schemaVersion` 필드와 discard 기본값만 정의.
|
||||
- `STORAGE_UNAVAILABLE` / `STORAGE_QUOTA_EXCEEDED` error kind enum 정의 — [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] (`FE-OC-008`) 소유. 본 브랜치는 adapter 실패 → 해당 kind 매핑만.
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
> 본 브랜치는 project-decision-heavy — 외부 storage best-practice 인용 없이 hub 계약(SSOT)에 근거한다. 아카이브된 6개 frontend official-doc(vite/react-ui/tailwind/tanstack-query/zod/react-router) 중 browser storage 를 다루는 것은 없음(확인 완료).
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | 본 브랜치 SSOT. `FE-OC-013` + `FE-D018` + §5.5 / §9.4 / §13.2 / §8.2 / §5.1 이 D1–D6 전부의 근거 (project decision). |
|
||||
| [[raw/official-docs/react-ui-library-official]] | 시드된 일반 frontend UI-composition source (hub §21.3, `FE-D004` React 선택 근거 `REACT-UI-C1`). **storage 전용 결정을 직접 정당화하지 않음** — 본 브랜치 grounding 은 위 hub 계약이다. |
|
||||
|
||||
## TODO
|
||||
|
||||
각 항목 옆 증거 등급 표기. frontend repository 미생성이므로 전부 `planned` / `needs-confirmation`.
|
||||
|
||||
- [ ] `src/contracts/storage-keys.js` 에 `FE-REG-STORAGE` 스키마 + 초기 행(COLOR_SCHEME / CHUNK_RELOAD_GUARD / QUERY_PERSISTENCE / AUTH_TOKEN) 정의 — 등급: `planned`
|
||||
- [ ] physical key 빌더 `<app>:<scope>:v<schema>:<name>` + raw literal 금지 lint/test — 등급: `planned`
|
||||
- [ ] classification enforcement + `sensitive-forbidden` 등록 거부 negative fixture(token key 등록 시도) — 등급: `planned`
|
||||
- [ ] `schemaVersion` + migration-or-discard 경로 및 previous-version fixture — 등급: `planned`
|
||||
- [ ] `StoragePort` + `adapters/storage` try/catch 어댑터, unavailable / security / quota 분기 매핑 — 등급: `planned`
|
||||
- [ ] quota fallback 정책 test(`memory` / `no-persist` / `feature-disable`) + correctness-critical no-fallback assertion — 등급: `planned`
|
||||
- [ ] 구현 repository·검증 evidence 식별 — 등급: `needs-confirmation`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
- `/branch-spec` fill 완료 (2026-07-19). 모든 근거는 hub 계약(FE-OC-013 / FE-D018 / §5.5 / §9.4 / §13.2 / §8.2). 외부 storage best-practice 인용 없음 — project-decision 중심 브랜치.
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> Decision Evidence Map 의 prose mirror. 각 근거는 hub 계약을 가리킨다(외부 source 없음).
|
||||
|
||||
- **D1**: 모든 storage 항목은 `FE-REG-STORAGE` registry 에만 등록하고 physical key 는 `<app>:<scope>:v<schema>:<name>` 구조를 MUST 가진다(raw `localStorage` literal 금지). 검토한 대안: code-generation SSOT 로 key 생성. 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-013` · §5.5 · §5.1.
|
||||
- **D2**: 각 항목은 classification(`public-preference` / `opaque-cache` / `sensitive-forbidden`)을 MUST 명시하며 분류 불명 항목은 등록 거부한다. 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-013` · §5.5.
|
||||
- **D3**: 각 항목은 `schemaVersion` 을 MUST 가지며 incompatible change 시 증가, previous version 을 읽으면 migration 또는 discard(기본 discard). 검토한 대안: 무버전 + 항상 discard. 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.5 · §9.2.
|
||||
- **D4**: 모든 Web Storage 접근은 application 소유 `StoragePort` + `adapters/storage` 어댑터를 통해서만 하고 try/catch 로 unavailable / security / quota 를 구분한다. 검토한 대안: 컴포넌트 직접 `localStorage` 접근. 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §9.4 · §4.4 · `FE-D010`(port ownership).
|
||||
- **D5**: `quotaFallback` 은 필수 필드(`memory` / `no-persist` / `feature-disable`)이며 quota 초과 시 허용된 cache 를 registry 명시 순서로 evict 후 memory fallback, 단 correctness-critical(mutation / idempotency record) 값은 fallback 금지·terminal 처리한다. 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.5 · §9.4 · §8.2.
|
||||
- **D6**: token / secret / PII / raw API response / error body 는 default registry 에 등록 불가(`sensitive-forbidden`)이며 브라우저 번들·storage 를 공개물로 간주한다. 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-013` · §9.4 · §13.2. 공동 집행: [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] (`FE-OC-019`); token lifecycle 은 [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] (`FE-OC-010`) 외부 소유.
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
> `선택 조건` = hub 결정이 `accepted-documented-only`(`FE-D018`) 이므로 대부분 불변식을 고정. 분기 있는 것만 대안 조건 명시. Supporting Claims 는 hub 계약을 가리킨다(project-decision-heavy 브랜치 — 외부 doc 없음).
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | storage 항목은 `FE-REG-STORAGE` 에만 등록, physical key `<app>:<scope>:v<schema>:<name>` 구조 필수, raw literal 금지 (`FE-OC-013`) | skeleton storage 는 항상 registry 경유; 대안(code-generation SSOT 로 key 생성)은 `FE-D018` revisit trigger(code generation SSOT 채택) 발생 시에만 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-013` · §5.5 · §5.1 | `project-decision` | `<app>` literal 값과 `<scope>` 분류 체계(feature별 vs flat) 미확정 — 구현 시 결정 |
|
||||
| D2 | 각 항목 classification 3분류 MUST 명시; 분류 불명 → 등록 거부 | 모든 항목 분류 강제(안전 기본); 분기 없음 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-013` · §5.5 | `project-decision` | `opaque-cache` vs `public-preference` 경계 판정 기준 문서화 필요 |
|
||||
| D3 | `schemaVersion` 필수 + incompatible 시 증가, previous version 은 migration 또는 discard | 기본 discard; migration 선택 시 fixture·rollback 은 compatibility-governance(`FE-OC-023`)로 위임 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.5 · §9.2 | `project-decision` | migration 필요 항목 발생 시 `FE-OC-023` 과 계약 조율 필요 |
|
||||
| D4 | 단일 application 소유 `StoragePort` + `adapters/storage` try/catch, unavailable / security / quota 분기 구분 | Clean Arch layering(`FE-OC-002`) 하에 port-owned 항상; 직접 `localStorage` 접근 금지 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §9.4 · §4.4 · `FE-D010` | `project-decision` | private-mode / 정책 차단의 SecurityError 세부 분기 미검증 |
|
||||
| D5 | `quotaFallback` 필수(`memory` / `no-persist` / `feature-disable`); quota 초과 시 evict→memory, correctness-critical 값 fallback 금지 | preference write 실패 → memory fallback 무중단; mutation / idempotency 등 correctness-critical → fallback 없이 terminal | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.5 · §9.4 · §8.2 | `project-decision` | eviction 순서(어떤 cache 먼저)의 registry 표기 형식 미정 |
|
||||
| D6 | token / secret / PII / raw response / error body = `sensitive-forbidden`, default registry 등록 불가, storage = 공개물 | skeleton default 는 항상 금지; auth owner 가 storage 사용 필요 시 별도 threat model + owner evidence(§6.1) — 본 브랜치 범위 밖 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-013` · §9.4 · §13.2 · §5.4 | `project-decision` | 공동 집행 경계(browser-security `FE-OC-019` / auth `FE-OC-010`) fixture 중복·누락 조율 |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> `planned` blueprint — frontend repository 미생성. 경로는 hub §4.6 Planned directory blueprint + §5.1 registry owner map 에서 도출(grounded)되나, 코드는 아직 없으므로 전체가 `planned`. CLAUDE.md §15.5 R1(Trace)·R2(UNSUPPORTED_IMPL_DECISION)·R3(OUT_OF_BRANCH_SCOPE) 준수.
|
||||
|
||||
### 1. `FE-REG-STORAGE` registry schema (`src/contracts/storage-keys.js`)
|
||||
|
||||
> **Trace**: D1, D2, D3, D5 → [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-013` · `FE-D018` · §5.5 · §5.1.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**:
|
||||
> - physical key 의 `<app>` literal 값(예: `ca`)과 `<scope>` 분류 체계(feature-prefix vs flat namespace) — §5.5 는 *형식*만 규정하고 구체 값을 권고하지 않음. trade-off: 짧은 prefix = 충돌 위험, 긴 prefix = key 길이 증가.
|
||||
> - registry 를 JS object literal vs factory 함수로 표현 — hub 미권고. trade-off: object = 단순, factory = 등록 시 검증 강제 용이.
|
||||
> - `schemaVersion` 표기(정수 vs semver) — §5.5 는 increment 만 규정. trade-off: 정수 = 단순 비교, semver = additive/breaking 구분.
|
||||
|
||||
**필드 계약(8-field 스키마)과 초기 4행의 owner 는 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.5 다** — 이전 판은 두 표를 1:1 로 옮겨 적고 있었고("전부 §5.5 grounded" · "§5.5 planned rows 그대로" 라고 스스로 밝힌 그대로), 그러면 §5.5 가 필드를 추가할 때 이 사본이 조용히 낡는다. 요약 한 줄: storage key 는 `logicalName`·`physicalKey`·`backend`·`classification`·`schemaVersion`·`ttl`·`migration`·`quotaFallback` 8필드를 가지고, 초기 행은 색상 테마·chunk reload guard·query persistence(비활성)·auth token(금지) 4개다.
|
||||
|
||||
본 브랜치가 소유하는 것은 그 위의 **강제 방법**이다 — 아래 enforcement point, key-name deny 패턴, quota fallback 사다리.
|
||||
|
||||
### 2. `StoragePort` boundary + adapter failure mapping (`application/ports` + `adapters/storage`)
|
||||
|
||||
> **Trace**: D4, D5 → [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §9.4 · §4.4 · §8.2 · `FE-D010`.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**:
|
||||
> - `StoragePort` method 시그니처(예: `get(logicalName)` / `set(logicalName, value)` / `remove(logicalName)`)의 정확한 이름·인자 — §9.4 는 boundary 원칙만 규정. trade-off: 좁은 API = 안전, 넓은 API = 유연.
|
||||
> - **OUT_OF_BRANCH_SCOPE**: `STORAGE_UNAVAILABLE` / `STORAGE_QUOTA_EXCEEDED` **kind enum 정의**는 [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] (`FE-OC-008`) 소유(§5.6). 본 § 는 adapter 실패 → 해당 kind *매핑*만 명세한다.
|
||||
|
||||
어댑터 실패 매핑 (§8.2 · §9.4 grounded):
|
||||
|
||||
| adapter 조건 | normalized kind | fallback |
|
||||
|---|---|---|
|
||||
| Storage API 부재 / `SecurityError`(private mode·정책 차단) | `STORAGE_UNAVAILABLE` | memory-only (§8.2) |
|
||||
| `setItem` quota 초과 | `STORAGE_QUOTA_EXCEEDED` | 허용 cache evict → memory-only (§8.2) |
|
||||
|
||||
### 3. Classification enforcement + `sensitive-forbidden` invariant
|
||||
|
||||
> **Trace**: D2, D6 → [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-013` · §5.5 · §9.4 · §13.2 · §15.2 · §6.1.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**:
|
||||
> - 거부 강제 지점(build-time lint vs runtime registry assert vs 둘 다) — hub 미권고. trade-off: lint = 조기 차단, runtime = 동적 등록도 방어.
|
||||
> - 금지 key 이름 패턴(정규식/glob) 구체 — §6.1 은 이름 목록(`SECRET`/`PASSWORD`/`PRIVATE_KEY`/`TOKEN`)만 제시. trade-off: 넓은 패턴 = 오탐, 좁은 패턴 = 누락.
|
||||
> - **OUT_OF_BRANCH_SCOPE**: CSP / secret-scan / `dangerouslySetInnerHTML` 등 브라우저 보안 경계 전반은 [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] (`FE-OC-019`) 소유(§13.2). 본 § 는 storage 등록 거부만.
|
||||
|
||||
강제 규약:
|
||||
- `classification: sensitive-forbidden` 항목은 등록 자체를 거부(§5.5 · §9.4).
|
||||
- key 이름에 `SECRET` / `PASSWORD` / `PRIVATE_KEY` / `TOKEN` 포함 시 거부(§6.1 정책을 storage 에 적용).
|
||||
- negative fixture: `token key registration attempt` → 반드시 실패(§15.2).
|
||||
|
||||
### 4. Quota fallback + correctness-critical policy
|
||||
|
||||
> **Trace**: D5 → [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.5 · §9.4 · §8.2.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**:
|
||||
> - eviction 순서 표기 형식(registry 필드 vs 별도 목록)과 `feature-disable` 시 UX notice 형식 — §5.5·§9.4 는 "허용 순서를 registry 에 기록"만 요구, 형식 미권고. trade-off.
|
||||
|
||||
값 등급별 fallback (§9.4 · §5.5 · §8.2 grounded):
|
||||
|
||||
| value class | quota / unavailable 시 동작 |
|
||||
|---|---|
|
||||
| `public-preference` (예: `COLOR_SCHEME`) | memory fallback, silent — product flow 중단 없음 |
|
||||
| `opaque-cache` (예: `CHUNK_RELOAD_GUARD`) | 허용 cache evict 후 memory; guard 손실 허용 |
|
||||
| correctness-critical (mutation / idempotency record) | fallback 없음 → terminal; 임의 storage fallback 금지 |
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- Storage API 부재 / `SecurityError`(private mode·정책 차단) → `STORAGE_UNAVAILABLE`, memory-only, 대개 silent (§8.2).
|
||||
- `setItem` quota 초과 → `STORAGE_QUOTA_EXCEEDED`, 허용 cache evict 후 memory, feature 영향 시 non-blocking notice (§8.2).
|
||||
- previous `schemaVersion` 데이터 read → migration 또는 discard; discard 시 기본값 재생성 (D3 · §9.2).
|
||||
- `sensitive-forbidden` 값 등록 시도 → 등록 거부(negative fixture, §15.2).
|
||||
- correctness-critical 값의 storage 실패 → fallback 금지, terminal 처리 (D5 · §9.4).
|
||||
- **다른 계약 의존**:
|
||||
- [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] (`FE-OC-002`) — `StoragePort` 를 application 이 소유하고 adapter 가 구현하는 layering·port 규약에 의존(§20 Dependency). 이 계약이 바뀌면 port 위치·주입 방식 영향.
|
||||
- [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] (`FE-OC-008`) — `STORAGE_UNAVAILABLE` / `STORAGE_QUOTA_EXCEEDED` kind 정의를 consume; 본 브랜치는 매핑만.
|
||||
- [[raw/branch-notes/feature-frontend-contract-registry-governance]] (`FE-OC-022`) — 8-registry single-owner·compatibility governance 에 storage snapshot 기여.
|
||||
- [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] (`FE-OC-023`) — schema breaking-change migration·version-bump 판정 위임.
|
||||
- [[raw/branch-notes/feature-sample-feature-slice-contract-fixture]] (`FE-OC-024`) — sample slice 가 storage key 계약을 fixture 로 사용.
|
||||
- [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] (`FE-OC-010`) — token lifecycle 외부 소유; 본 registry 는 token 저장 거부만.
|
||||
- [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] (`FE-OC-019`) — secret·storage 브라우저 경계 fixture 공동.
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
> hub 계약은 근거지만 내 프로젝트에서의 동작을 자동 보장하지 않는다. frontend repository 미생성이므로 전부 `needs-confirmation`. 검증 아티팩트는 §20 Measurable completion(namespace/version/classification/quota fallback tests) + §15.2 negative fixture 에서 도출.
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| physical key 규약이 실제 코드에서 raw literal 사용을 0건으로 만든다 | repo·lint 규칙 미구현 | namespace/registry lint + "raw localStorage key literal" negative fixture (§5.1·§15.2) | `needs-confirmation` |
|
||||
| token key 등록 시도가 반드시 실패한다 | 강제 지점(build vs runtime) 미구현 | "token key registration attempt" negative fixture (§15.2) | `needs-confirmation` |
|
||||
| `schemaVersion` mismatch 시 migration-or-discard 가 결정적으로 동작 | migration 경로 미작성 | previous-version fixture + discard/default 재생성 test | `needs-confirmation` |
|
||||
| quota 초과 시 preference = memory fallback, correctness-critical = no fallback | 브라우저 quota 동작 환경차 | quota fallback 결정적 test(mock quota) + correctness-critical no-fallback assertion | `needs-confirmation` |
|
||||
| classification 3분류가 모든 항목에 강제된다 | registry validation 미구현 | 미분류 항목 등록 거부 unit test (FE-GATE-005 registries) | `needs-confirmation` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
- 없음 — `/branch-spec` fill 단계 (구현 전).
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-GATE-005@1` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | unit 레벨이 실패하면 merge 를 MUST 차단하고 warning 으로 낮추면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-002@1` | [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] | `domain <- application <- presentation` 의존 방향과 application-owned output port를 MUST 지킴 | import 참조로 적용 |
|
||||
| `FE-OC-010@1` | [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] | skeleton은 session state를 소비하되 token lifecycle을 MUST 소유하지 않음 | import 참조로 적용 |
|
||||
| `FE-OC-019@1` | [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] | browser bundle에 secret을 넣지 않고 untrusted HTML injection을 기본 금지 | import 참조로 적용 |
|
||||
| `FE-OC-023@1` | [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] | API/config/storage/release schema의 breaking change는 migration 또는 version bump 없이 배포하면 안 됨 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
### Sub-branches (세부 작업)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
### 오류 기록 (이 branch 작업 중 발생)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
### 강의 (이 작업을 위해 학습한 강의)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
### job-posting tie-ins (이 작업에서 파생된 글감)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크: TODO
|
||||
- 리뷰 메모: TODO
|
||||
- 머지 결과 / 배포 환경: TODO
|
||||
- **wiki 추출 대상**: 없음 — 전부 `planned` (frontend repository 미생성)
|
||||
- **추출하지 않을 항목**: D1–D6 전체 — 구현·검증 evidence 확보 전까지 추출 금지
|
||||
+352
@@ -0,0 +1,352 @@
|
||||
---
|
||||
title: branch / feature-frontend-test-taxonomy-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-017
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-017
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-TEST-STACK-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-001]
|
||||
contract_packet: 1
|
||||
branch: feature-frontend-test-taxonomy-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract.md]
|
||||
tags: [branch, ca-skeleton, frontend, testing, react, javascript]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
contract_packet_sha256: 160ba678c61d567516938e08a0a4413a55424796822f145f22d575376746274d
|
||||
imports: [ART-FE-001@1, ART-FE-002@1, ART-FE-004@1, FE-GATE-001@1, FE-GATE-002@1, FE-GATE-003@1, FE-GATE-004@1, FE-GATE-009@1, FE-GATE-010@1, FE-GATE-011@1, FE-GATE-012@1, FE-GATE-013@1, FE-GATE-020@1, FE-OC-019@1, FE-OC-021@1, FE-OC-025@1]
|
||||
---
|
||||
|
||||
# branch: feature-frontend-test-taxonomy-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — 단일 브랜치의 TODO·결정·진행 기록. 현재는 `/branch-spec` 로 spec 을 채운 `planned` 단계다(frontend 코드 저장소 미생성).
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: gate·fixture·artifact mapping과 test level별 최소 1개 test가 존재한다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-TEST-STACK-001@1` | test stack default는 Vitest, RTL, MSW, Playwright, axe다 | test level·gate·fixture·artifact taxonomy에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
|
||||
<!-- section-id: branch-local-decisions -->
|
||||
### 브랜치 지역 결정
|
||||
|
||||
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
||||
|---|---|---|---|---|
|
||||
| D1 | frontend test stack default를 고정한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D2 | gate를 KIND별 단일 책임으로 분리한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D3 | test level별 대표 test와 gate별 negative fixture를 요구한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D4 | gate failure를 warning으로 낮추지 않는다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D5 | 공유 artifacts evidence tree를 사용한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
| D6 | gate별 test level과 fixture KIND taxonomy를 이 branch가 소유한다 (gate-to-contract coverage mapping은 hub §15.1 소유) | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
|
||||
|
||||
<!-- section-id: declared-overrides -->
|
||||
### 선언한 예외
|
||||
|
||||
| Override ID | Overrides | Reason | Approval | Status |
|
||||
|---|---|---|---|---|
|
||||
|
||||
없음.
|
||||
<!-- GENERATED: branch-contract:end -->
|
||||
|
||||
<!-- section-id: branch-goal -->
|
||||
## 목표
|
||||
|
||||
이 branch 는 `FE-OC-020`(owner) — "gate 종류별 책임·fixture·artifact 를 분리하고 실패를 warning 으로 낮추면 안 됨" — 을 구현 착수 가능한 spec 으로 내린다. 구체적으로 frontend **quality-gate taxonomy** 를 정의한다: 각 gate 가 어느 test level 에 속하고 어떤 fixture *종류* 를 요구하는지(각 gate 의 blocking scope·Covered FE-OC·pass condition·증거 artifact 는 hub §15.1 소유), "test level 당 대표 test 최소 1개(one-test-per-level)" 수락 규칙, "gate 당 최소 1개의 의도적 실패 negative fixture" 규칙([[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.2), 그리고 gate 실패를 warning 으로 downgrade 하지 않는다는 불변식(§15.3 promotion formula). 아울러 test stack default(Vitest + RTL + MSW + Playwright + axe — [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D022)를 고정한다. **gate→FE-OC coverage-mapping 의 owner 는 hub §15.1 이고 본 branch 가 아니다** — hub §15.1 이 "이 표가 gate 의 정의다 … branch 는 이 표를 옮겨 적지 않는다" 라고 명시하며, hub §2.1.1 이 gate 26개의 Owner 를 각각 확정한다. 본 branch 가 소유하는 것은 gate → **test level / fixture KIND** taxonomy 다(약 8개 sibling branch 가 자신의 gate artifact 를 이 taxonomy 에 예치). 모든 진술 등급은 `planned` — frontend repository 가 아직 없다.
|
||||
|
||||
- 이슈: 없음 (repository 미생성)
|
||||
- PR: 없음
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
`FE-OC-020` 이 소유하는 것만:
|
||||
|
||||
- **Gate KIND 열거 + 스키마**: §15.1 의 26-gate 를 gate → test level → 필요한 fixture *종류* 로 매핑. 각 gate 의 blocking scope · Covered FE-OC · pass condition · 증거 artifact 는 hub §15.1 소유이므로 여기서 재진술하지 않고 gate ID 로 참조한다.
|
||||
- (제외) gate→FE-OC coverage-mapping 의 owner 는 hub §15.1 이다. 이전 판에서 본 branch 를 SSOT 로 적었던 것은 Single-Owner 위반이었고 2026-07-21 에 hub 로 확정했다.
|
||||
- **one-test-per-level 수락 규칙** — 각 test level(runtime-schema / unit / component / integration / e2e / a11y)마다 최소 1개 대표 test 로 taxonomy 가 선택한 stack 으로 realizable 함을 증명.
|
||||
- **negative-fixture-per-gate 규칙**(§15.2) — 각 gate 는 ≥1 의도적 실패 fixture 를 실제 실행; rule 존재만으로는 `locally-verified` 증거 불충분.
|
||||
- **no-downgrade 불변식 + blocking-scope promotion formula**(§15.3: MERGE_READY / RELEASE_READY / PROD_PROMOTION_READY / FIELD_SLO_READY / DOCUMENTATION_READY).
|
||||
- **test stack default 도구 배정**([[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D022) — 각 level 을 어떤 도구가 실행하는지.
|
||||
- **공유 `artifacts/` evidence-tree taxonomy**(§14.3 artifact column, §4.6 blueprint) — sibling gate 들이 예치하는 정본 트리.
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외 — 다른 owner branch 소유. 여기서는 이름만 가리키고 detail 을 재명세하지 않는다(CLAUDE.md §15.5 R3).
|
||||
|
||||
- **CI workflow orchestration**(gate job dependency graph, artifact retention wiring, blocking-gate 배선) → [[raw/branch-notes/feature-frontend-ci-quality-gates-contract]] (`FE-OC-020` 공동 기여). 이 branch 는 taxonomy 를 정의하고, CI 가 그것을 어떻게 실행/보관하는지는 저 branch.
|
||||
- **각 gate 의 fixture 본문(content)** 은 contract owner 에 위임: runtime-schema fixture → [[raw/branch-notes/feature-runtime-schema-validation-contract]] (`FE-OC-007`); error taxonomy fixture → [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] (`FE-OC-008`); architecture forbidden-import fixture → [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]] (`FE-OC-002`); build/bundle/security fixture → [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] (`FE-OC-018`); component gate 의 browser-security 슬라이스 fixture → [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] (`FE-OC-019`); performance threshold → [[raw/branch-notes/feature-web-vitals-performance-budget-contract]] (`FE-OC-021`); sample-removal fixture → [[raw/branch-notes/feature-sample-feature-slice-contract-fixture]] (`FE-OC-024`); runbook drill → [[raw/branch-notes/feature-frontend-operational-runbook-contract]] (`FE-OC-025`).
|
||||
- **Toolchain / package-script host**(pnpm script, engine, lockfile) → [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] (`FE-OC-003`). 이 branch 는 script slot 을 consume 만.
|
||||
- **NFR 임계값 자체**(timeout 10s, retry ≤2, bundle KiB, axe 0) → 각 NFR contract owner. taxonomy 는 assertion slot 만 hosting.
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/official-docs/vite-build-tool-official]] (`VITE-C1`, `VITE-C2`) | D1 배경 근거 — build/test 파이프라인이 Vite 위에 올라감(dev = native ESM 위 기능, prod = Rolldown 정적 자산 산출). 단 특정 test runner(Vitest 등) 선택은 이 문서가 말하지 않음 — 도구 선택 자체는 hub FE-D022 project decision. build gate artifact(§14.3 `pnpm build`)의 정적 자산 산출 근거로만 직접 인용 가능. |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D022 (§15, §14) | D1 test stack default(Vitest+RTL+MSW+Playwright+axe)의 1차 근거. Vite/browser/component/e2e 책임 분리라는 conditional-default. |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.1 gate matrix | D2 gate-kind 분리의 근거(26-row acceptance gate registry). gate→FE-OC mapping 은 이 §15.1 이 소유하며 D6 는 그 위에 test level / fixture KIND 층만 얹는다. |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.2 negative fixture requirement | D3 one-test-per-level + gate 당 ≥1 negative fixture 근거. |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.3 promotion formula | D4 no-downgrade / blocking-in-scope 불변식 근거. |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §14 planned commands + §4 directory blueprint | D5 공유 `artifacts/` evidence-tree taxonomy 근거(script→artifact 매핑, `artifacts/` 트리). |
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] repository(`src/`, `tests/`) 생성 후 §15.1 26-gate 를 gate→test level→fixture-kind taxonomy 로 고정(artifact·blocking scope 는 hub §15.1 소유) — 등급: `planned`
|
||||
- [ ] 각 test level(runtime-schema / unit / component / integration / e2e / a11y)마다 대표 test 최소 1개 작성(one-test-per-level) — 등급: `planned`
|
||||
- [ ] 각 gate 에 ≥1 의도적 실패 negative fixture 연결(§15.2 카탈로그) 후 "예상대로 실패" 확인 — 등급: `planned`
|
||||
- [ ] `artifacts/{quality,tests,performance,security,release,runbooks}` evidence-tree + `pnpm test:*` script→artifact 매핑 확정(§14.3) — 등급: `planned`
|
||||
- [ ] no-downgrade 불변식 + promotion formula(§15.3)를 반영한 gate 상태 판정 규칙 정의 — 등급: `planned`
|
||||
- [ ] 구현 repository 와 검증 evidence 식별 — 등급: `needs-confirmation`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
- `/branch-spec` self-map 완료(2026-07-19): hub §15 gate matrix + §14.3 planned commands + §8.5 negative fixtures + FE-D022 가 이 branch 의 SSOT. 6개 official-doc source 중 testing-tool 을 직접 말하는 claim 은 없음 → 도구 선택 근거는 hub project decision, Vite 문서는 파이프라인 배경으로만 인용. 외부 web research 불필요(모든 결정 hub-grounded). frontend 코드 부재 → 전부 `planned`.
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> 각 결정의 근거는 아래 Sources 및 Decision Evidence Map 참조.
|
||||
|
||||
- 2026-07-19: **test stack default = Vitest + RTL + MSW + Playwright + axe** / 이유: Vite 위 build/test 파이프라인 통합 + unit/component/integration/e2e/a11y 책임 분리 / 검토한 대안: Jest + Cypress, 조직 test platform / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D022 (conditional-default).
|
||||
- 2026-07-19: **gate 는 KIND별 단일 책임으로 분리**하고 blocking scope(merge / release / prod-promotion / field / documentation)를 명시; 통합 test job 으로 합치지 않음 / 검토한 대안: 단일 test 스텝 / 근거: §15.1 26-gate matrix.
|
||||
- 2026-07-19: **one-test-per-level + gate 당 ≥1 negative fixture** 불변식; rule 존재만으로는 evidence 불충분 / 근거: §15.2 + §20 measurable completion.
|
||||
- 2026-07-19: **gate 실패를 warning 으로 낮추지 않음**(scope 내 전부 blocking), promotion 은 §15.3 formula 준수 / 근거: `FE-OC-020` normative summary + §15.3.
|
||||
- 2026-07-19: **공유 `artifacts/` evidence-tree taxonomy**; sibling gate 는 자체 트리를 만들지 않고 여기에 machine-readable artifact 예치 / 근거: §14.3 + §4.6.
|
||||
- 2026-07-19: ~~§15.1 gate→FE-OC coverage-mapping 표의 single owner(SSOT)~~ → **2026-07-21 철회**: 그 매핑의 owner 는 hub §15.1 이다(§15.1 서두 "이 표가 gate 의 정의다 … branch 는 이 표를 옮겨 적지 않는다"). 본 branch 가 SSOT 를 자처한 것은 Single-Owner 위반이었다. 남는 결정: 본 branch 는 gate → **test level / fixture KIND** taxonomy 를 소유하고 sibling 은 그 taxonomy 를 복제·재정의하지 않는다.
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | test stack default = Vitest + RTL + MSW + Playwright + axe (`FE-OC-020` / hub FE-D022) | 이 조건: Vite 기반 client-only SPA + React + 자체 CI. 대안 전환: 조직 표준 test platform 이 다른 runner(Jest/Cypress 등)를 강제하거나 CI 가 이 스택 미지원 시 runner 교체 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D022; `raw/official-docs/vite-build-tool-official.md#VITE-C1`, `#VITE-C2` (파이프라인 배경) | `conditional-default` (도구 선택은 project decision; 외부 doc 는 Vite 배경만 제공, Vitest 를 직접 말하지 않음) | 5개 도구가 6개 test level 을 gap 없이 커버하는지 미검증; DOM 환경(jsdom vs happy-dom) 미확정 |
|
||||
| D2 | gate 를 KIND별 단일 책임으로 분리하고 blocking scope(merge/release/prod-promotion/field/documentation) 명시; 통합 job 금지 (`FE-OC-020`) | 이 조건: gate 들이 서로 다른 fixture/artifact/blocking scope 를 가질 때(§15.1 26-row 전부). 대안: 새 gate 가 기존 KIND 책임과 1:1 이면 별도 gate 가 아니라 그 row 의 superseding clarification 으로 병합 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.1 (26-gate matrix); `FE-OC-020` normative summary | `project-decision` | 26개 gate 가 실제 CI 에서 독립 실행 가능한지, 중복 없이 FE-OC 를 완전 분해하는지 미검증 |
|
||||
| D3 | one-test-per-level + gate 당 ≥1 의도적 실패 negative fixture; rule 존재만으로는 evidence 불충분 (`FE-OC-020`) | 불변식(분기 N/A) — gate 가 실제로 위반을 잡는다고 말하려면 negative fixture 가 실행돼야 하고(§15.2), level 이 realizable 하려면 대표 test 1개가 필요하므로 항상 요구 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.2 (negative fixture requirement); §20 measurable completion | `project-decision` (invariant) | negative fixture 가 "예상대로 실패" 하는지는 repository 생성 후에만 검증 가능 |
|
||||
| D4 | gate 실패를 warning 으로 낮추지 않음; 선언된 scope 내 모든 gate 는 blocking, promotion 은 §15.3 formula 준수 (`FE-OC-020`) | 불변식(분기 N/A) — MERGE_READY / RELEASE_READY / PROD_PROMOTION_READY / FIELD_SLO_READY / DOCUMENTATION_READY 각 단계는 지정 gate PASS 없이 통과 불가로 고정되어 downgrade 여지가 없음 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-OC-020 normative summary; §15.3 promotion formula | `accepted-documented-only` (invariant) | CI wiring 이 실제로 downgrade 를 막는지는 CI 계약 구현 후에 검증(위임 대상은 §Edge·Dependency 참조) |
|
||||
| D5 | 공유 `artifacts/` evidence-tree taxonomy(quality/tests/performance/security/release/runbooks); sibling gate 는 자체 트리 없이 여기에 machine-readable artifact 예치 (`FE-OC-020`, contributes `FE-OC-021`/`FE-OC-025`) | 이 조건: gate 가 CI 에서 재사용 가능한 evidence 를 남겨야 할 때. 대안: script rename 은 허용하되 gate+artifact 매핑을 동시 갱신해야 함(§14.3 말미) — 매핑 갱신 없는 rename 금지 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §14 (planned artifact column); §4 (`artifacts/` blueprint) | `project-decision` | artifact 포맷(JUnit XML / SARIF / JSON)이 실제 CI reporter 와 호환되는지 미검증 |
|
||||
| D6 | 이 branch 는 gate→**test level / fixture KIND** taxonomy 의 owner 다. gate→FE-OC coverage-mapping 과 gate 정의(blocking scope·pass condition·artifact)의 owner 는 hub §15.1 이고, gate 별 Owner 는 hub §2.1.1 이 확정한다 (`FE-OC-020`) | 이 조건: 다수 sibling 이 test artifact 를 이 taxonomy 에 위임할 때(§20 contributes-to 8개 FE-OC). gate 추가/supersede 는 hub §15.1·§2.1.1 소관이며 본 branch 는 test-level 슬롯만 따라 갱신 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.1 서두("branch 는 이 표를 옮겨 적지 않는다"); §2.1.1 gate registry; §20 (contributes-to 매핑) | `project-decision` | 매핑이 모든 FE-OC 의 test evidence 를 빠짐없이 덮는지는 coverage-auditor 가 별도 판정. 2026-07-21 정정 — 이전 판이 본 branch 를 coverage-mapping SSOT 로 적어 hub 와 Single-Owner 충돌이었다 |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> 전 항목 `planned` — frontend repository 미생성. 경로/스크립트는 hub §14.3(planned commands)·§15.1(gate matrix)·§4.6(directory blueprint)에서 도출된 blueprint 이며 repo 생성 시 변경 가능.
|
||||
|
||||
### 1. Gate → test level / fixture-kind 매핑 (taxonomy core)
|
||||
|
||||
> **Trace**: D2 + D3 + D5 + D6 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.1 / §14.3 / `FE-OC-020`
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — 아래 표에서 본 branch 가 정하는 것은 `Test level / KIND` 열뿐이다. 나머지 열(blocking scope · Covered FE-OC · 필요 fixture 본문 · 증거 artifact · pass condition)은 hub §15.1 소유이며 옮겨 적지 않는다(§15.1: "branch 는 이 표를 옮겨 적지 않는다").
|
||||
|
||||
아래는 **gate → test level** taxonomy 다. 이전 판은 hub §15.1 의 blocking scope·fixture·artifact 열까지 복제했는데, 그 사본이 실제로 낡아 있었다(`FE-GATE-013` 에 `dependency-review` fixture 누락, `FE-GATE-008` 의 `repeated guarded-route` 한정어 소실). 그래서 정의 열은 전부 걷어내고 gate ID 참조만 남긴다.
|
||||
|
||||
| Gate ([[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.1) | Test level / KIND (본 branch 소유) | Fixture 본문 owner |
|
||||
|---|---|---|
|
||||
| `FE-GATE-001` | manifest/lockfile | bootstrap-toolchain |
|
||||
| `FE-GATE-002` | lint | architecture-enforcement |
|
||||
| `FE-GATE-003` | typecheck-equivalent | bootstrap-toolchain + runtime-schema |
|
||||
| `FE-GATE-004` | runtime-schema | runtime-schema-validation |
|
||||
| `FE-GATE-005` | unit | api-client / boundary-mapper / contract-registry |
|
||||
| `FE-GATE-006` | component | async-ui-state / render-recovery / browser-security(`FE-OC-019` 슬라이스) |
|
||||
| `FE-GATE-007` | integration | api-client / error-classification / auth-session |
|
||||
| `FE-GATE-008` | e2e | routing / auth-session / release-cache |
|
||||
| `FE-GATE-009` | accessibility | accessibility-baseline |
|
||||
| `FE-GATE-010` | architecture | architecture-enforcement |
|
||||
| `FE-GATE-011` | build | build-bundle |
|
||||
| `FE-GATE-012` | bundle | build-bundle / web-vitals |
|
||||
| `FE-GATE-013` | security | build-bundle / browser-security |
|
||||
| `FE-GATE-020` | sample-removal | sample-feature-slice |
|
||||
|
||||
나머지 gate — `FE-GATE-014..019`, `FE-GATE-021..026` — 도 hub §15.1 에 같은 형태로 등재돼 있고 fixture 본문·artifact 는 각 contract owner(release-cache / contract-compatibility / operational-runbook / web-vitals; `FE-GATE-019` 의 security-header 축 정책은 browser-security 공급) 소유다. 이 branch 는 그 row 들의 test-level 슬롯만 관리한다(D6).
|
||||
|
||||
### 2. Test-stack 도구 배정 per level
|
||||
|
||||
> **Trace**: D1 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D022 / §14.3
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: (a) DOM 환경 = jsdom 을 default 로 제안 — hub 는 Vitest+RTL 만 규정하고 환경을 명시하지 않음(trade-off: happy-dom 이 더 빠르나 Web API 커버리지 낮아 boundary/error 테스트 신뢰도 저하 위험). (b) integration 을 Vitest+jsdom+MSW 로 실행 — hub §14.3 는 `test:integration`=MSW matrix 만 말하고 runner 를 명시하지 않음(추론; 대안은 Playwright request-mocking).
|
||||
|
||||
| Test level | 도구 | 실행 환경 | 비고 |
|
||||
|---|---|---|---|
|
||||
| runtime-schema | Vitest | node/jsdom | zod fixture 가 invalid 입력을 기대 kind 로 reject |
|
||||
| unit | Vitest | node | retry fake-clock, mapper, registry 순수 로직(§15.1 `FE-GATE-005`) |
|
||||
| component | Vitest + RTL | jsdom | async/success/empty/terminal-error state, render boundary, keyboard |
|
||||
| integration | Vitest + MSW | jsdom | API status/failure/auth-recovery taxonomy (UNSUPPORTED: runner 추론) |
|
||||
| e2e | Playwright | Chromium/Firefox/WebKit | boot/route/mutation/chunk-mismatch/redirect-pair(§14.1 `FE-NFR-C02`) |
|
||||
| a11y | axe | Playwright 또는 component | critical/serious 0(`FE-NFR-009`) + manual checklist |
|
||||
|
||||
### 3. Blocking scope + no-downgrade promotion 집행
|
||||
|
||||
> **Trace**: D4 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.3 / `FE-OC-020`
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — promotion formula 가 §15.3 verbatim 이라는 것은 곧 **owner 가 §15.3** 이라는 뜻이므로 tier→gate 집합을 복제하지 않는다.
|
||||
|
||||
taxonomy 가 강제하는 불변식:
|
||||
|
||||
- 각 gate 는 정확히 하나의 blocking scope 를 가지며(§15.1 Blocking scope 열), 실패 시 그 scope 를 blocking 한다. **warning/soft-fail/`continue-on-error` 로 낮출 수 없다**(`FE-OC-020`).
|
||||
- promotion 은 tier→gate 집합으로 고정된다. 그 **집합의 owner 는 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.3** 이며 여기에 옮겨 적지 않는다 — hub 가 gate 를 추가·supersede 하면 복제본만 낡는다. tier 는 `MERGE_READY` → `RELEASE_READY` → `PROD_PROMOTION_READY` → `FIELD_SLO_READY` 의 누적 순서이고 `DOCUMENTATION_READY` 는 그와 직교한다.
|
||||
- CI 에서 이 tier 배선을 실제로 실행/강제하는 것은 **out of scope** → [[raw/branch-notes/feature-frontend-ci-quality-gates-contract]] (`FE-OC-020` 공동 기여). 이 branch 는 no-downgrade 불변식만 소유하고, tier→gate 집합 자체는 hub §15.3 소유다(R3).
|
||||
|
||||
### 4. Negative-fixture 요구(taxonomy 레벨)
|
||||
|
||||
> **Trace**: D3 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.2 / `FE-OC-020`
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — §15.2 카탈로그 참조. 각 fixture "본문" 은 owner branch 소유(R3).
|
||||
|
||||
규칙: 모든 gate 는 최소 1개의 **의도적으로 실패하는** fixture 를 실행해야 한다. rule 존재만 확인한 결과는 `locally-verified` 증거로 불충분(§15.2 말미). 대표 카탈로그(본문은 owner):
|
||||
|
||||
| Gate | Negative fixture 예 (§15.2) |
|
||||
|---|---|
|
||||
| architecture | `presentation` 이 `adapters/http` 를 import |
|
||||
| checkJs | application port 를 잘못된 shape 로 호출 |
|
||||
| runtime schema | `data` 없는 success envelope |
|
||||
| retry | idempotency key 없는 POST 가 503 수신 |
|
||||
| storage | token key 등록 시도 |
|
||||
| telemetry | event 에 raw URL/query 포함 |
|
||||
| release | HTML build A + asset manifest B |
|
||||
| reload guard | 같은 release pair 에서 2번째 chunk 실패 |
|
||||
| lab performance | context metadata 누락 또는 named threshold 초과 |
|
||||
|
||||
failure 로 정규화되는 경계 fixture(§8.5)도 integration/runtime-schema gate 의 negative fixture 로 재사용: `CONTENT_TYPE_MISMATCH`, `AUTH_INTEGRATION_FAILURE`, `RELEASE_MANIFEST_FAILURE`, `QUERY_CACHE_FAILURE`, `UNKNOWN_CLIENT_FAILURE`, `UNKNOWN_FAILURE` — 단 기대 kind 정의는 error-classification owner 소유.
|
||||
|
||||
### 5. 공유 `artifacts/` evidence-tree + one-test-per-level bootstrap
|
||||
|
||||
> **Trace**: D5 + D3 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §14.3 / §4.6
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: artifact 포맷(test:* → JUnit XML, security → SARIF, performance/release → JSON)은 §14.3 가 확장자(.xml/.sarif/.json)만 규정 → 구체 스키마는 reporter 선택 시 결정(trade-off: JUnit XML 은 CI 호환 넓으나 표현력 낮음).
|
||||
|
||||
evidence-tree(§4.6 `artifacts/` + §14.3 artifact 열):
|
||||
|
||||
```text
|
||||
artifacts/
|
||||
quality/ install.txt · lint.txt · check-types.txt
|
||||
tests/ runtime-schema.xml · unit.xml · component.xml · integration.xml · a11y.json · sample-removal.xml · e2e/
|
||||
performance/ bundle.json · lab.json · field-web-vitals.json
|
||||
security/ scan.sarif
|
||||
release/ build-manifest.json · verification.json · hosting-headers.json
|
||||
runbooks/ FE-RB-00N/<release-id>/record.json
|
||||
```
|
||||
|
||||
**one-test-per-level bootstrap**(이 branch 가 직접 인도, sibling 의 full suite 와 구분): 각 level 에서 taxonomy 가 realizable 함을 증명하는 최소 대표 test 1개 —
|
||||
|
||||
- runtime-schema: 1개 valid + 1개 invalid envelope → 기대 결과 확인
|
||||
- unit: fake-clock retry 1개(≤2 backoff)
|
||||
- component: async state 4종(initial/success/empty/terminal-error) 1개 컴포넌트
|
||||
- integration: MSW 로 1개 실패 status → normalized kind 1개
|
||||
- e2e: boot → 1개 route 진입 smoke 1개
|
||||
- a11y: 1개 sample route axe critical/serious 0
|
||||
|
||||
각 script 는 §14.3 `pnpm test:*` slot 에 매핑되고 위 artifact 경로로 결과를 남긴다. script rename 은 gate+artifact 매핑 동시 갱신 조건으로만 허용(§14.3, D5).
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- gate 에 negative fixture 없이 rule 존재만 확인 → §15.2 위반, `locally-verified` 불충분(기대: taxonomy 가 그 gate 를 "unverified" 로 표시, promotion 미충족).
|
||||
- gate 실패가 warning 으로 downgrade → `FE-OC-020` 위반(기대: promotion formula 가 해당 tier 를 NOT_READY 로 유지).
|
||||
- 어떤 test level 에 대표 test 0개 → one-test-per-level 미충족(기대: taxonomy 불완전으로 merge 차단).
|
||||
- script rename 시 gate+artifact 매핑 미갱신 → §14.3 위반(기대: drift check 가 매핑 불일치 검출).
|
||||
- flaky e2e/perf gate → deterministic fixture(fake clock, recorded context metadata §14.1)로 강제; 비결정성은 gate 신뢰도 훼손이므로 taxonomy 는 결정적 fixture 를 요구.
|
||||
- **다른 계약 의존**:
|
||||
- [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] (`FE-OC-003`) — pnpm script host / engine 없이는 `pnpm test:*` 를 실행할 수 없음(§20 dependency). 그 계약의 script 명이 바뀌면 이 taxonomy 의 script→artifact 매핑도 갱신 필요.
|
||||
- [[raw/branch-notes/feature-frontend-ci-quality-gates-contract]] — CI orchestration/retention 이 이 taxonomy 를 consume; 그쪽 wiring 이 blocking 집행에 영향(`FE-OC-020` 공동).
|
||||
- fixture-content 의존(test level 슬롯은 본 branch, 본문은 owner): runtime-schema `FE-OC-007` · error `FE-OC-008` · architecture `FE-OC-002` · build/bundle/security `FE-OC-018` · browser-security 슬라이스 `FE-OC-019` · performance `FE-OC-021` · sample-removal `FE-OC-024` · runbook `FE-OC-025`. 각 owner 의 fixture kind 가 바뀌면 본 branch 의 §1 taxonomy 표(D6)를 갱신한다 — hub §15.1 표는 hub 소유이므로 건드리지 않는다.
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| Vitest+RTL+MSW+Playwright+axe 가 6개 test level 을 gap 없이 커버 | repository 부재, 도구 조합 미실행 | repo 생성 후 level 별 대표 test(`pnpm test:runtime-schema/unit/component/integration/e2e/a11y`) 실행(§14.3) | `needs-confirmation` |
|
||||
| 각 gate 의 negative fixture 가 "예상대로 실패" | rule 존재만으로 불충분(§15.2) | §15.2 카탈로그 fixture 를 실행해 기대 kind 로 실패하는지 확인 | `needs-confirmation` |
|
||||
| gate 실패가 CI 에서 warning 으로 downgrade 되지 않음 | CI wiring 미구현(위임 대상) | CI 계약 구현 후 promotion formula(§15.3) 위반 시 tier NOT_READY 확인 | `planned` |
|
||||
| 26-gate 매핑이 모든 FE-OC 의 test evidence 를 완전 분해 | 매핑 완전성 미검증 | coverage-auditor + §15.1 Covered-FE-OC 대조 | `needs-confirmation` |
|
||||
| boot config ≤500ms / retry ≤2 / axe 0 등 NFR 임계 slot | 값 owner 는 sibling, taxonomy 는 slot 만 hosting | 각 gate 가 해당 NFR assertion 을 실행(§14.2 target + §14.3 command) | `planned` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
- 없음 — 구현 착수 전(`planned`).
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: artifact-imports:start -->
|
||||
### 가져온 artifact 계약
|
||||
|
||||
| Artifact Ref | Owner | Producer | Schema Ref |
|
||||
|---|---|---|---|
|
||||
| `ART-FE-001@1` | [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] | [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] | `harness/source/artifact-schemas/ca-skeleton-frontend/build-manifest.schema.json` |
|
||||
| `ART-FE-002@1` | [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] | [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] | `harness/source/artifact-schemas/ca-skeleton-frontend/bundle-report.schema.json` |
|
||||
| `ART-FE-004@1` | [[raw/branch-notes/feature-accessibility-baseline-contract]] | [[raw/branch-notes/feature-accessibility-baseline-contract]] | `harness/source/artifact-schemas/ca-skeleton-frontend/a11y-report.schema.json` |
|
||||
<!-- GENERATED: artifact-imports:end -->
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-GATE-001@1` | [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] | lockfile 이 manifest 와 어긋나면 merge·release 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-002@1` | [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]] | 금지된 API·import 가 남아 있으면 merge 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-003@1` | [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] | production diagnostic 이 남아 있으면 merge 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-004@1` | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | invalid fixture 가 예상 kind 로 거부되지 않으면 merge 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-009@1` | [[raw/branch-notes/feature-accessibility-baseline-contract]] | automated threshold 미달이거나 manual checklist 서명이 없으면 merge·release 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-010@1` | [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]] | 금지된 layer import 가 통과하면 merge 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-011@1` | [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] | clean production build 가 실패하거나 기대 artifact 가 없으면 merge·release 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-012@1` | [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] | 번들 NFR threshold 초과면 release 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-013@1` | [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] | secret·vulnerability·license·dependency review 정책 위반이면 merge·release 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-020@1` | [[raw/branch-notes/feature-sample-feature-slice-contract-fixture]] | sample 제거 후 build·smoke 가 실패하면 merge·release 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-OC-019@1` | [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] | browser bundle에 secret을 넣지 않고 untrusted HTML injection을 기본 금지 | import 참조로 적용 |
|
||||
| `FE-OC-021@1` | [[raw/branch-notes/feature-web-vitals-performance-budget-contract]] | NFR은 device/network/cache/build context와 함께 MUST 측정 | import 참조로 적용 |
|
||||
| `FE-OC-025@1` | [[raw/branch-notes/feature-frontend-operational-runbook-contract]] | boot, chunk mismatch, API degradation, telemetry failure, rollback runbook을 MUST 유지 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
### Sub-branches (세부 작업)
|
||||
|
||||
- 없음 — 구현 착수 전.
|
||||
|
||||
### 오류 기록 (이 branch 작업 중 발생)
|
||||
|
||||
- 없음 — 구현 착수 전.
|
||||
|
||||
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
||||
|
||||
- 없음 — 구현 착수 전.
|
||||
|
||||
### 강의 (이 작업을 위해 학습한 강의)
|
||||
|
||||
- 없음 — 구현 착수 전.
|
||||
|
||||
### job-posting tie-ins (이 작업에서 파생된 글감)
|
||||
|
||||
- 없음 — 구현 착수 전.
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
- 없음 — 구현 착수 전.
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크: TODO
|
||||
- 리뷰 메모: TODO
|
||||
- 머지 결과 / 배포 환경: TODO
|
||||
- **wiki 추출 대상**: 없음 — 구현 착수 전(전부 `planned`).
|
||||
- **추출하지 않을 항목**: 현재 전 항목 `planned` — verified evidence 확보 전까지 추출 금지.
|
||||
+313
@@ -0,0 +1,313 @@
|
||||
---
|
||||
title: branch / feature-routing-navigation-guard-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
branch: feature-routing-navigation-guard-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract.md]
|
||||
tags: [branch, ca-skeleton, frontend, application, auth, react, integration]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-009
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-009
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-ROUTING-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-AUTH-BOUNDARY-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-008, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-007]
|
||||
contract_packet: 1
|
||||
contract_packet_sha256: fc5b09275d9cfe6bccc3c7f28c67ca370f6921a9afe79f114398b7761ef660f9
|
||||
imports: [FE-GATE-008@1, FE-OC-008@1, FE-OC-015@1]
|
||||
---
|
||||
|
||||
# branch: feature-routing-navigation-guard-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — 단일 브랜치의 TODO·결정·진행 기록. 머지/종료 후 verified 결과는 `/ingest`로 `wiki/projects/`에 추출한다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: registry route·param validation·404·redirect-loop·session UX test가 통과한다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-ROUTING-001@1` | routing default는 React Router Declarative Mode다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-AUTH-BOUNDARY-001@1` | auth lifecycle은 외부 owner가 소유하고 skeleton은 AuthSessionPort만 소비한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-frontend-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 -->
|
||||
## 목표
|
||||
|
||||
이 브랜치는 project-wide 계약 `FE-OC-005`("route ID/path/params/access/loading/error owner는 route registry 하나여야 함")를 *구현 착수 가능한 명세*로 내린다. 즉 route 메타데이터의 단일 소유 registry(`FE-REG-ROUTE`, `src/contracts/routes.js`)를 정의하고, [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D008`(React Router Declarative Mode를 라우팅 default로 채택, `conditional-default`)을 이 registry 위에서 구현한다. 부수적으로 `FE-OC-010`(session state를 소비하되 token lifecycle을 소유하지 않음), `FE-OC-015`(route error/loading surface 소유를 render boundary와 중복하지 않고 reload loop 금지), `FE-OC-024`(sample route는 제거 가능한 fixture)에 기여한다. 아직 frontend repository가 없으므로 본 노트의 모든 구현 주장은 `planned` 등급이다.
|
||||
|
||||
- 이슈: (없음 — repository 미생성)
|
||||
- PR: (없음)
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- `FE-REG-ROUTE` route registry를 단일 SSOT로 정의: `routeId`/`path`/`paramsSchema`/`searchSchema`/`access`/`loadingSurface`/`errorSurface`/`chunkId` 필드(§5.2 minimum schema) — 등급 `planned`
|
||||
- React Router Declarative Mode 라우팅(`FE-D008`): `<Routes>`/`<Route>` 컴포넌트 트리 + nested `<Outlet/>` 합성 — 등급 `planned`
|
||||
- route `access` 분류 enum `{public, session-required, integration-defined}`(§5.2) — 등급 `planned`
|
||||
- navigation guard(=UX hint): `session-required` route가 `AuthSessionPort` state를 소비, redirect loop 차단(bounded hop) — 등급 `planned`
|
||||
- unknown route → `NOT_FOUND`(`*`, public) surface, API 요청 없이 처리(§9.3) — 등급 `planned`
|
||||
- route param/search runtime validation *진입점*: registry가 schema 참조를 선언(검증 엔진은 위임) — 등급 `planned`
|
||||
- route별 `loadingSurface`/`errorSurface` owner 선언(render boundary와 owner 중복 금지) — 등급 `planned`
|
||||
- sample route fixtures(`APP_HOME`, `SAMPLE_RESOURCE_LIST`, `NOT_FOUND`)(§5.2 initial rows) — 등급 `planned`
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외 — 다른 owner 브랜치 또는 외부 소유.
|
||||
|
||||
- token lifecycle(code exchange·refresh·rotation·logout·revocation): 외부 auth owner + [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] `FE-OC-010` 소유. 본 브랜치는 `AuthSessionPort` state를 *소비*만.
|
||||
- backend authorization 결정(최종 권한 판단): backend 소유. guard는 이를 대체하지 않음.
|
||||
- failure 정규화 taxonomy(`401`→`AUTH_REQUIRED`, `404`→`NOT_FOUND`, `RENDER_FAILURE` 등): [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] `FE-OC-008` 소유.
|
||||
- runtime schema 검증 *엔진*(Zod): [[raw/branch-notes/feature-runtime-schema-validation-contract]] `FE-OC-007` 소유. 본 registry는 schema *참조*만 선언.
|
||||
- React error boundary taxonomy + reload-loop guard 구현: [[raw/branch-notes/feature-frontend-render-recovery-boundary-contract]] `FE-OC-015` 소유. 본 registry는 route별 surface owner *선언*만.
|
||||
- lazy chunk ID ↔ release manifest 매핑 생성: [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] `FE-OC-016` 소유. route registry는 생성된 `chunkId` 값만 보유.
|
||||
- 8-registry cross-cutting governance(single-owner·compatibility): [[raw/branch-notes/feature-frontend-contract-registry-governance]] `FE-OC-022` 소유.
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/official-docs/react-router-official]] `REACT-ROUTER-C1`·`REACT-ROUTER-C4` | D1 — `<Routes>`/`<Route>`로 URL segment를 UI에 결합하는 선언적 route 구성 + "Declarative Mode"가 파일 기반 Framework Mode와 별개로 존재(client-only Vite SPA 적합) |
|
||||
| [[raw/official-docs/react-router-official]] `REACT-ROUTER-C2` | D2·D8 — nested route + `<Outlet/>` 합성(레이아웃 아래 보호된 자식 route 중첩) |
|
||||
| [[raw/official-docs/react-router-official]] `REACT-ROUTER-C3` | D4 — `Link`/`NavLink` 활성 스타일링. **navigation guard(라우트 접근 제어)는 증명하지 않음**(§Usage Boundaries) → guard 결정은 hub project decision + D4 web 조사로 근거화 |
|
||||
| reactrouter.com/start/declarative/navigating (2026-07-19 web 조사, 미아카이브) | D4 mechanism — Declarative Mode의 `useNavigate` programmatic navigation(로그인/로그아웃 등 비상호작용 redirect) 근거. `RR-NAV-WEB-C1`(§구현 가이드 3 인용) |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-005` (§5.1 owner map, §5.2 route registry schema) | D2·D3·D6·D8 — route registry 단일 소유, access enum, NOT_FOUND, surface owner |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D008` (§9.3 route behavior, §7.8 auth boundary, §13.2 route guard≠authorization) | D1·D4·D5·D7 — routing default, guard=UX hint, param validation, redirect loop 차단 |
|
||||
|
||||
> 미아카이브 web 근거(`RR-NAV-WEB-C1`)는 wiki 승격 전 `wiki-source-summarizer`로 `raw/official-docs/`에 정식 아카이브 필요(React Router 공식 doc의 §메모가 "loader/redirect 패턴 별도 자료 추가 필요"로 이미 flag).
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] `FE-REG-ROUTE` route registry 모듈(`routeId`/`path`/`paramsSchema`/`searchSchema`/`access`/`loadingSurface`/`errorSurface`/`chunkId`) — 등급: `planned`
|
||||
- [ ] registry로부터 Declarative Mode router 구성(`<Routes>`/`<Route>`/`<Outlet>`) — 등급: `planned`
|
||||
- [ ] access 분류 + navigation guard(UX hint, `AuthSessionPort` 소비) — 등급: `planned`
|
||||
- [ ] param/search validation 진입점(schema 참조 선언; Zod 검증은 위임) — 등급: `planned`
|
||||
- [ ] `NOT_FOUND` route + redirect-loop guard(automatic redirect ≤ 1, 동일 pair 반복 금지) — 등급: `planned`
|
||||
- [ ] route `loadingSurface`/`errorSurface` owner 선언(boundary 중복 금지) — 등급: `planned`
|
||||
- [ ] 테스트: registry snapshot · param validation · 404 no-API · redirect-loop · session UX — 등급: `planned`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
- `REACT-ROUTER-C3`이 guard를 증명하지 않는다는 점이 이 브랜치의 핵심 함정이다. guard *결정*은 hub project decision(§7.8/§9.3/§13.2)으로, guard *메커니즘*은 `useNavigate` web 조사(`RR-NAV-WEB-C1`)로 근거화하고, 구체 컴포넌트 설계는 `UNSUPPORTED_IMPL_DECISION`으로 남긴다.
|
||||
- Declarative Mode에는 built-in loader/redirect가 없으므로 param validation과 guard가 모두 component 계층 구현이 된다(§구현 가이드 3·4).
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> 대안과 근거를 함께 기록. 상세 근거 매핑은 아래 Decision Evidence Map.
|
||||
|
||||
- 2026-07-19: 라우팅은 React Router Declarative Mode를 default로 채택 / 이유: client-only Vite SPA는 SSR·file-based convention·route-level loader가 없어 선언적 `<Routes>`/`<Route>` 트리로 충분 / 검토한 대안: data router mode(route 객체 + loader/action), framework mode(파일 기반 컨벤션+SSR) / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D008`, `raw/official-docs/react-router-official.md#REACT-ROUTER-C1`·`#REACT-ROUTER-C4`
|
||||
- 2026-07-19: route ID/path/params/access/loading/error를 단일 route registry(`FE-REG-ROUTE`)가 소유 / 이유: rename·rollback 영향 범위를 한 곳에서 계산, component literal route path로 인한 분산 방지 / 검토한 대안: 파일 기반/컴포넌트 인라인 route 정의 / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-005` (§5.1·§5.2)
|
||||
- 2026-07-19: route `access`는 `{public, session-required, integration-defined}` 3-값 enum / 이유: 접근 정책을 registry 필드로 고정해 component 분기 제거 / 검토한 대안: boolean `requiresAuth`, role 배열 / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-005` (§5.2 access field)
|
||||
- 2026-07-19: navigation guard는 UX hint일 뿐 authorization이 아니고 backend authorization이 최종 판단 / 이유: client guard는 우회 가능하므로 보안 경계로 삼지 않음(§13.2) / 검토한 대안: client-side 강제(백엔드 authz 없이 route로 접근 통제) / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D008` (§7.8·§9.3·§13.2); guard mechanism은 `RR-NAV-WEB-C1`(`useNavigate`)
|
||||
- 2026-07-19: route param/search는 application 호출 전 runtime validation, registry가 schema 참조 선언·검증 엔진은 위임 / 이유: 잘못된 URL 입력을 경계에서 차단하되 Zod 채택은 별도 owner 결정 / 검토한 대안: validation 생략(신뢰) / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D008` (§9.3), `FE-D007`(Zod)
|
||||
- 2026-07-19: unknown route → API 없이 not-found surface, `NOT_FOUND`(`*`, public)를 registry에 포함 / 이유: 존재하지 않는 route에 불필요한 네트워크 요청 금지 / 검토한 대안: 서버 라우팅 위임 / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D008` (§9.3·§8.2·§5.2)
|
||||
- 2026-07-19: redirect loop 차단 — navigation attempt당 automatic auth redirect ≤ 1, 동일 source→target pair 반복 금지 / 이유: guard redirect가 무한 루프가 되지 않도록 hop 제한 / 검토한 대안: 무제한 redirect / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D008` (§9.3·§7.8·§10.2; `FE-GATE-008` e2e invariant)
|
||||
- 2026-07-19: route별 `loadingSurface`/`errorSurface` owner를 registry가 선언, route error element와 React error boundary owner 중복 금지 / 이유: 같은 실패를 두 소유자가 처리하는 모호성 제거(§9.3·§10.1) / 검토한 대안: boundary만으로 처리 / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-005` (§5.2·§9.3·§10.1)
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
> `Decision ID`는 본 branch-note 안에서 안정적. `Supporting Claims`의 `FE-D###`·`§n`은 hub project 문서 기준.
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | React Router Declarative Mode를 라우팅 default로 채택 (`FE-OC-005` 구현 기반) | Declarative Mode 유지: client-only Vite SPA에 loader·SSR·file-based convention 요구가 없을 때. 대안(data router/framework mode)은 route-level data loading·SSR이 product requirement가 될 때 전환 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D008`; `raw/official-docs/react-router-official.md#REACT-ROUTER-C1`, `#REACT-ROUTER-C4` | official-doc + conditional-default | Declarative Mode에는 built-in loader/redirect가 없어 guard·validation을 component 계층에서 구현해야 함(D4·D5 impl 위험) |
|
||||
| D2 | route ID/path/params/access/loading/error를 단일 route registry(`FE-REG-ROUTE`)가 소유 — component 내 literal route path 금지 | 항상 registry 경유: route 메타데이터가 rename·compatibility 추적 대상일 때(=본 skeleton). literal 경로는 §0.4 throwaway single-route prototype에서만 허용 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-005` (§5.1 owner map, §5.2 schema) | project-decision | registry field 확장(신규 access class 등)은 `FE-REG-ROUTE` 변경 프로토콜(§5.10) 필요 |
|
||||
| D3 | route `access` = `{public, session-required, integration-defined}` 3-값 enum | 이 3-값으로 고정. 새 access class는 `FE-REG-ROUTE` schema 변경 절차를 거칠 때만 추가 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-005` (§5.2 access field) | project-decision | `integration-defined` semantics는 auth owner 결정에 의존(§7.8) |
|
||||
| D4 | navigation guard는 UX hint일 뿐 authorization 아님; backend authorization이 최종 판단; `session-required` route는 `AuthSessionPort` state를 소비 | guard=advisory 유지: backend가 authz를 강제하는 한. client-only 강제(백엔드 authz 부재)가 필요하면 별도 결정 필요(현재 근거 없음 → UNSUPPORTED) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D008` (§7.8·§9.3·§13.2); `RR-NAV-WEB-C1`(useNavigate); `raw/official-docs/react-router-official.md#REACT-ROUTER-C3`(guard 미증명 — UX 링크 스타일만) | project-decision | guard 우회 시 backend authz가 유일 방어선 — client guard를 보안 경계로 오인 금지 |
|
||||
| D5 | route param·search를 application 호출 전 runtime validation; registry가 `paramsSchema`/`searchSchema` 참조 선언, 검증 엔진(Zod)은 위임 | dynamic param/search 존재 시 validation(conditional field). static route는 schema 없음 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D008` (§9.3), `FE-D007`(Zod, §5.2 conditional field) | project-decision (delegated) | Zod 통합 형태(route wrapper vs effect)는 Declarative Mode에 loader가 없어 impl 미정 |
|
||||
| D6 | unknown route → API 없이 not-found surface; `NOT_FOUND`(`*`, public) route를 registry에 포함 | catch-all `*` route 상시 존재. API 응답 404는 별도 정규화(`NOT_FOUND` kind)로 error-classification branch 소유 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D008` (§9.3·§8.2, §5.2 NOT_FOUND row) | project-decision | route-level 404 UX와 API 404 UX 일관성은 `FE-OC-008`와 조율 필요 |
|
||||
| D7 | redirect loop 차단: navigation attempt당 automatic auth redirect ≤ 1, 동일 source→target pair 반복 금지 | 첫 guard redirect 1회 허용; 두 번째 동일 redirect → terminal auth-required/error surface(§7.8 second-`401` terminal, §10.2 guard-record-then-act와 동형) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D008` (§9.3·§7.8·§10.2; `FE-GATE-008` e2e invariant) | project-decision | hop-count 상수·guard 자료구조는 문서 미명세 → `UNSUPPORTED_IMPL_DECISION`(§구현 가이드 5) |
|
||||
| D8 | route registry가 route별 `loadingSurface`·`errorSurface` owner 선언; route error element와 React error boundary owner 중복 금지 | route-level `errorSurface`는 lazy-chunk/route render 실패 소유; expected operational 실패는 normal async state로 반환(throw 금지) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-005` (§5.2·§9.3·§10.1) | project-decision | boundary taxonomy는 `FE-OC-015`(render-recovery) 소유 — surface owner token 어휘 정합 필요 |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> 모든 세부는 `planned`(frontend repository 미생성). 경로는 hub §4.6 planned directory blueprint + §5.1 registry owner map에서 도출된 anchor.
|
||||
|
||||
### 1. FE-REG-ROUTE route registry 모듈
|
||||
|
||||
> **Trace**: D2 (`FE-OC-005`, `FE-REG-ROUTE`) — hub §5.1(`src/contracts/routes.js` 소유) + §5.2(minimum schema)에서 도출. D6·D8의 필드(`NOT_FOUND` row, `loadingSurface`/`errorSurface`)도 이 모듈이 담는다.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 모듈의 JS 형태(frozen descriptor 배열 vs factory 함수) — 문서 미명세. trade-off: snapshot 테스트 용이성을 위해 `Object.freeze`된 route descriptor 배열 + `routeId` 조회 헬퍼로 채택(임의 선택).
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 3개 seed row 외 실제 route naming — 문서는 `APP_HOME`/`SAMPLE_RESOURCE_LIST`/`NOT_FOUND`만 제시. trade-off: 신규 route는 `UPPER_SNAKE_CASE` 규칙만 따르고 product route는 sample 제거 후 추가.
|
||||
|
||||
필드(§5.2 그대로, `planned`):
|
||||
|
||||
| Field | Required | Rule (hub §5.2) |
|
||||
|---|---|---|
|
||||
| `routeId` | yes | stable `UPPER_SNAKE_CASE`; rename은 breaking |
|
||||
| `path` | yes | 중앙 literal; component 내부 literal 금지 |
|
||||
| `paramsSchema` | conditional | dynamic param 있으면 runtime validation(D5) |
|
||||
| `searchSchema` | conditional | query string을 application input으로 넘기기 전 validation(D5) |
|
||||
| `access` | yes | `public` \| `session-required` \| `integration-defined`(D3) |
|
||||
| `loadingSurface` | yes | route-level fallback owner(D8) |
|
||||
| `errorSurface` | yes | route-level error owner(D8) |
|
||||
| `chunkId` | generated | release manifest와 매핑(생성값만 보유; 매핑은 out-of-scope) |
|
||||
|
||||
Initial planned rows(§5.2): `APP_HOME`(`/`, public), `SAMPLE_RESOURCE_LIST`(`/sample/resources`, integration-defined), `NOT_FOUND`(`*`, public, no API retry).
|
||||
|
||||
### 2. Declarative Mode router 구성
|
||||
|
||||
> **Trace**: D1 (`FE-D008`, `REACT-ROUTER-C1`·`C2`·`C4`) — registry rows를 `<Routes>`/`<Route>` 트리로 렌더, nested route는 `<Outlet/>`로 합성. router는 boot order 9단계(§4.5)에서 생성. anchor: `src/presentation/app/`, `src/presentation/routes/`(§4.6).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: `<BrowserRouter>` 컴포넌트 vs 다른 history 구성 — 문서 미명세. trade-off: Declarative Mode 표준인 `<BrowserRouter>` + registry 기반 `<Route>` 생성 함수 채택. base path는 `VITE_ROUTER_BASE_PATH`(§5.4, default `/`) 소비.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: registry→route-element 생성 함수 이름/시그니처 — 임의. trace 가능한 단일 함수로 두어 registry가 유일 SSOT임을 보장.
|
||||
|
||||
절차(`planned`): (1) registry 로드(§4.5 step 5) → (2) 각 row를 `<Route path element access>`로 매핑 → (3) 레이아웃 route는 `<Outlet/>`로 자식 중첩(`REACT-ROUTER-C2`) → (4) `NOT_FOUND` catch-all `*`는 마지막 → (5) `<BrowserRouter basename=VITE_ROUTER_BASE_PATH>`로 mount(§4.5 step 10).
|
||||
|
||||
### 3. Access 분류 + navigation guard (UX hint)
|
||||
|
||||
> **Trace**: D3·D4 — `session-required` route는 [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] `FE-OC-010`의 `AuthSessionPort` state를 application facade 경유로 소비(§7.8). guard가 미인증 시 auth-required surface 렌더 또는 programmatic redirect. guard≠authorization(§13.2). redirect 메커니즘 근거는 `RR-NAV-WEB-C1`.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: guard를 컴포넌트 wrapper vs route element로 구현, 그리고 `<Navigate>` element vs `useNavigate` effect 중 무엇 — 문서상 `useNavigate`만 근거 확보(`RR-NAV-WEB-C1`), `<Navigate>`는 미검증. trade-off: 우선 route wrapper + `useNavigate`(doc-grounded)로 구현하고 `<Navigate>` 채택은 별도 검증 전 보류.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: guard 컴포넌트/훅 명명 및 `integration-defined` access의 정확한 소비 형태 — auth owner 결정에 의존. trade-off: `integration-defined`는 auth adapter가 접근 가부를 반환할 때까지 loading surface 유지.
|
||||
|
||||
`RR-NAV-WEB-C1` (web 인용, reactrouter.com/start/declarative/navigating, 2026-07-19):
|
||||
> "This hook allows the programmer to navigate the user to a new page without the user interacting."
|
||||
> 문서 예시 용례: "Logging them out after inactivity" — 즉 비상호작용 상황의 programmatic redirect가 `useNavigate`의 정당한 용도이며, guard redirect가 이에 해당.
|
||||
|
||||
access별 동작(`planned`): `public`=무조건 렌더 / `session-required`=session 있으면 렌더, 없으면 auth-required surface + (선택) 1회 redirect(D7) / `integration-defined`=auth adapter 판정까지 loading, 판정 후 렌더 or auth-required.
|
||||
|
||||
### 4. Param/Search validation 진입점
|
||||
|
||||
> **Trace**: D5 (`FE-D008` §9.3, §5.2 conditional field) — registry의 `paramsSchema`/`searchSchema`는 *참조*만 담고, 실제 Zod 검증 엔진은 [[raw/branch-notes/feature-runtime-schema-validation-contract]] `FE-OC-007`가 소유. 검증은 application use case 호출 *전*에 수행.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: Declarative Mode에는 loader가 없어 검증을 어디서 실행할지(route-entry 훅 vs 컴포넌트 mount effect) 문서 미명세. trade-off: route-entry 훅에서 schema 참조를 조회→검증→실패 시 not-found/route error surface로 분기(임의 선택, loader 부재 대응).
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 검증 실패를 `NOT_FOUND`로 볼지 `VALIDATION_REJECTED`로 볼지 — 정규화는 `FE-OC-008` 소유. trace: 잘못된 route param은 존재하지 않는 리소스로 보아 not-found surface가 default(§9.3 "unknown route" 연장), 최종 kind 매핑은 error-classification과 조율.
|
||||
|
||||
### 5. NOT_FOUND + redirect-loop 방지
|
||||
|
||||
> **Trace**: D6·D7 (`FE-D008` §9.3·§8.2·§7.8·§10.2, `FE-GATE-008` e2e invariant) — `NOT_FOUND` catch-all은 API 요청 없이 not-found surface. guard redirect는 navigation attempt당 ≤ 1이고 동일 source→target pair 반복 금지.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: source→target pair를 기록하는 guard 자료구조/키 형태와 max hop 상수 — 문서 미명세. trade-off: §10.2 `CHUNK_RELOAD_GUARD` 패턴을 차용해 `(fromRouteId,toRouteId)` 키의 per-navigation guard를 두고 두 번째 동일 pair에서 redirect 중단(임의 설계, 문서 패턴 동형).
|
||||
|
||||
절차(`planned`): 첫 미인증 진입 → guard 기록 후 auth-required target으로 1회 redirect → 복귀 후 여전히 미인증이고 동일 pair면 redirect 대신 terminal auth-required surface(§7.8 second-`401` terminal과 동형). unknown path → 즉시 `NOT_FOUND` surface, network 0건.
|
||||
|
||||
### 6. Loading/Error surface owner 선언
|
||||
|
||||
> **Trace**: D8 (`FE-OC-005` §5.2·§9.3·§10.1) — registry가 route별 `loadingSurface`/`errorSurface` owner token을 선언. route error element와 React error boundary는 owner 중복 금지(§9.3). boundary taxonomy 자체는 [[raw/branch-notes/feature-frontend-render-recovery-boundary-contract]] `FE-OC-015` 소유.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: surface owner token 어휘 — 문서 미명세. trade-off: §10.1 boundary 명칭(`boot shell`/`route boundary`/`feature boundary`/`async boundary`)을 owner token으로 재사용해 render-recovery branch와 어휘 정합(임의 선택, 문서 표 차용).
|
||||
|
||||
원칙(`planned`): expected operational 실패(API 실패 등)는 normal async state로 반환하고 render boundary에 throw하지 않음(§10.1). route render/lazy-chunk 실패만 `errorSurface`가 처리. `loadingSurface`는 route-level fallback owner.
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- unknown route → `NOT_FOUND` surface, API 요청 0건(§9.3)
|
||||
- `session-required` route + 미인증 → automatic redirect ≤ 1; 동일 source→target 재발 → terminal auth-required surface(loop 없음)(§7.8·§10.2·`FE-GATE-008`)
|
||||
- invalid route param/search → application 호출 전 validation 실패 → not-found/route error surface(§9.3·§5.2)
|
||||
- lazy route chunk fetch 실패 → `CHUNK_LOAD_FAILURE`, controlled reload once(§8.2·§10.2) — reload guard는 render-recovery 소유; 본 registry는 `chunkId`만 매핑
|
||||
- in-flight 요청 중 navigation abort → `REQUEST_ABORTED`, error toast 금지(§8.2) — API client 소유; route는 `routeId`+`abortReason=navigation`만 공급(§7.2)
|
||||
- route render throw → `RENDER_FAILURE`(route boundary, §8.2·§10.1) — boundary는 render-recovery 소유; 본 registry는 `errorSurface` owner 선언만
|
||||
- **다른 계약 의존**:
|
||||
- [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] `FE-OC-010` — `AuthSessionPort` session state를 UX hint로 소비. 이 계약이 바뀌면 guard의 session 판정 방식 영향.
|
||||
- [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] `FE-OC-008` — `401`→`AUTH_REQUIRED`, `404`→`NOT_FOUND`, `RENDER_FAILURE` 정규화. route-level 404/auth UX의 kind 매핑을 여기서 consume.
|
||||
- [[raw/branch-notes/feature-frontend-render-recovery-boundary-contract]] `FE-OC-015` — route/React error boundary taxonomy + reload-loop guard. surface owner token 어휘 정합 대상.
|
||||
- [[raw/branch-notes/feature-runtime-schema-validation-contract]] `FE-OC-007` — param/search schema의 Zod 검증 엔진.
|
||||
- [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] `FE-OC-016` — `chunkId` ↔ release manifest 매핑.
|
||||
- [[raw/branch-notes/feature-frontend-contract-registry-governance]] `FE-OC-022` — `FE-REG-ROUTE` single-owner + compatibility governance.
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| route registry가 유일 SSOT — component에 literal route path 0건 | frontend 코드 미존재, registry 우회 가능성 | registry snapshot 테스트(`FE-OC-005` min evidence) + literal-path 정적 검사(component에 route literal 금지) | `needs-confirmation` |
|
||||
| dynamic route param/search가 application 호출 전 검증됨 | Declarative Mode에 loader가 없어 검증 위치가 impl 의존 | invalid param fixture로 param validation deterministic 테스트(§20 measurable) | `needs-confirmation` |
|
||||
| unknown route가 API 요청 0건으로 not-found surface 렌더 | 라우팅 setup에 따라 우발적 fetch 가능 | 404 테스트에서 network 호출 0건 assert(§9.3) | `needs-confirmation` |
|
||||
| navigation guard가 navigation attempt당 automatic redirect ≤ 1, 동일 source→target 반복 없음 | guard 자료구조 미설계(`UNSUPPORTED_IMPL_DECISION`) | redirect-loop e2e 테스트(`FE-GATE-008` invariant: automatic auth redirect ≤ 1, pair 무반복) | `needs-confirmation` |
|
||||
| `session-required` route가 `AuthSessionPort` state를 UX hint로만 사용, token lifecycle 미소유 | 위임 경계가 코드로 강제되는지 미확인 | session UX 테스트 + token-lifecycle import 금지 assert(§4.3 dependency rule) | `needs-confirmation` |
|
||||
| route error element와 React error boundary owner가 중복되지 않음 | boundary가 render-recovery 소유라 경계 조율 필요 | route surface owner vs boundary ownership 테스트(render-recovery와 공동)(§9.3·§10.1) | `needs-confirmation` |
|
||||
| Declarative Mode `<Routes>`/`<Route>`/`<Outlet>`가 registry 트리를 렌더(framework/file-based convention 없이) | 라이브러리 API 정합성 미검증 | registry 기반 route 트리 component 렌더 테스트(`REACT-ROUTER-C1`·`C2`) | `needs-confirmation` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
> `/coverage`가 채우는 생성물이며 손으로 유지하지 않는다.
|
||||
|
||||
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| TODO — `/coverage` 실행 전 | missing | (없음) | 미평가 | TODO |
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-GATE-008@1` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | critical e2e 시나리오가 실패하면 merge·release 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-OC-008@1` | [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] | 모든 failure는 stable frontend error kind로 MUST 정규화하고 raw body·stack을 UI에 노출하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-015@1` | [[raw/branch-notes/feature-frontend-render-recovery-boundary-contract]] | expected operational error와 render defect를 MUST 분리하고 reload loop를 금지 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
### Sub-branches (세부 작업)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 오류 기록 (이 branch 작업 중 발생)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 강의 (이 작업을 위해 학습한 강의)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### job-posting tie-ins (이 작업에서 파생된 글감)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크:
|
||||
- 리뷰 메모:
|
||||
- 머지 결과 / 배포 환경:
|
||||
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
|
||||
- `actually-implemented` 항목:
|
||||
- `locally-verified` 항목:
|
||||
- `prod-verified` 항목:
|
||||
- **추출하지 않을 항목** (planned / documented-only / abandoned):
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
---
|
||||
title: branch / feature-runtime-schema-validation-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
branch: feature-runtime-schema-validation-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract.md]
|
||||
tags: [branch, ca-skeleton, frontend, validation, integration, javascript, json]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-006
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-006
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-VALIDATION-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-005]
|
||||
contract_packet: 1
|
||||
contract_packet_sha256: 489734960b06bb60f76ac96b8ad7f49731c8bb8d11e7b9de7e53af63d746b6a2
|
||||
imports: [FE-OC-006@1, FE-OC-008@1, FE-OC-023@1, FE-OC-024@1, FLOW-FE-RESP-001@1, FLOW-FE-RESP-002@1, FLOW-FE-RESP-003@1, FLOW-FE-RESP-007@1, FLOW-FE-RESP-008@1]
|
||||
---
|
||||
|
||||
# branch: feature-runtime-schema-validation-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — 단일 브랜치의 TODO·결정·진행 기록. 머지/종료 후 verified 결과는 `/ingest`로 `wiki/projects/`에 추출한다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: content-type·JSON·envelope·payload invalid fixture가 기대 failure kind로 정규화된다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-VALIDATION-001@1` | boundary runtime validation은 Zod schema로 수행한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-frontend-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 -->
|
||||
## 목표
|
||||
|
||||
이 브랜치는 project-wide 계약 `FE-OC-007`(JSON envelope와 payload는 boundary에서 runtime schema를 MUST 통과)을 구현 착수 가능한 상세 명세로 내린다. 스켈레톤은 컴파일 타임 타입 보장이 없는 plain JavaScript ESM 이므로([[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D002), 컴파일러가 API 응답 같은 경계 데이터의 형태를 보장할 수 없다. 그 빈자리를 HTTP 경계의 런타임 스키마 검증 계층으로 채우며, 검증 라이브러리는 Zod 로 고정한다([[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D007). 이 검증 계층이 방출하는 실패 신호는 `FE-OC-008`(failure normalization)과 `FE-OC-023`(schema compatibility) 계약이 소비하는 입력이 된다. 프런트엔드 코드는 아직 존재하지 않으므로 본 노트의 모든 구현 주장 등급은 `planned` 이다.
|
||||
|
||||
- 이슈:
|
||||
- PR:
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- 응답 경계 검증의 4-stage gate 정의 — `FE-OC-007`, hub §7.3 processing order stage 2~6: (2) content-type 검사, (3) JSON parse, (4) envelope schema, (5) success/failure 분기, (6) payload schema.
|
||||
- 이 중 **stage 4~6 의 owner 는 본 branch** 다 — hub §2.1.4 Flow Stage Registry `FLOW-FE-RESP-004@1`(envelope 공유 스키마 검증; 경계 검증은 `.safeParse()` non-throwing 이며 throw 를 상위로 누출하지 않는다) · `FLOW-FE-RESP-005@1`(success/failure 분기 검증; 200 이어도 envelope 이 invalid 하면 success 로 반환하지 않는다) · `FLOW-FE-RESP-006@1`(payload per-operation 스키마 검증; payload invalid 는 `SCHEMA_MISMATCH` 이고 mapper 는 검증 통과분만 받는다). 이 세 단계의 Invariants 를 바꾸려면 본 branch 가 revision 을 올려야 한다. stage 2~3 은 [[raw/branch-notes/feature-api-client-response-envelope-contract]] 소유라 `imports` 로만 pin 한다.
|
||||
- 각 stage 실패를 4종의 구분된 raw failure 신호로 방출 — CONTENT_TYPE_MISMATCH / MALFORMED_JSON / ENVELOPE_MISMATCH / SCHEMA_MISMATCH (hub §8.2, `FE-OC-008` 기여).
|
||||
- Zod 스키마 작성 규약 — envelope 공유 스키마 1개 + operation별 payload 스키마, `FE-REG-API` responseSchema 참조 (hub §5.3).
|
||||
- 경계에서 `.safeParse()`(non-throwing) 사용 — 검증 실패가 throw 로 presentation 까지 누출되지 않고 normalized 실패로 매핑되도록.
|
||||
- Outbound requestSchema 검증 — params/search/body 를 전송 전 operation requestSchema 로 검증 (hub §5.3).
|
||||
- 4종 invalid fixture(content-type/JSON/envelope/payload) → 기대 kind 매핑 (§20 Measurable completion).
|
||||
- payload additive-tolerance posture — `FE-OC-023` 기여 (정책 자체는 위임, 아래 Out of scope).
|
||||
|
||||
### 제외 범위
|
||||
|
||||
- normalized failure 의 최종 shape·userMessageKey·severity·action·UX·telemetry 매핑 → `FE-OC-008` owner [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] 소유(`FE-REG-ERROR`). 본 branch 는 stage 신호와 safe issue subset 까지만.
|
||||
- shared HTTP client 자체(transport, timeout, abort, retry, request context) → `FE-OC-006` owner [[raw/branch-notes/feature-api-client-response-envelope-contract]].
|
||||
- runtime config 검증(hub §6.4)은 별개 경계 → `FE-OC-004` owner [[raw/branch-notes/feature-frontend-env-runtime-config-contract]].
|
||||
- DTO → application model mapper(processing order stage 7, `FLOW-FE-RESP-007@1`) → [[raw/branch-notes/feature-boundary-mapper-viewmodel-contract]] (`FE-OC-024`, `FE-OC-007` 기여). 본 branch 는 검증된 DTO 를 mapper 에 넘기는 데까지만.
|
||||
- schema breaking/additive 분류·migration·version bump 정책 → `FE-OC-023` owner [[raw/branch-notes/feature-frontend-contract-compatibility-governance]].
|
||||
- form input 런타임 검증 — 현재 hub 에 대응 `FE-OC` 계약 없음. 필요 시 신규 제안(planned)으로만 다룬다(임의 확대 금지).
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/official-docs/zod-runtime-schema-validation-official]] | D1 Zod 채택(`ZOD-VALID-C2` plain JS 동작), D2·D5 `.parse()` 검증 관문(`ZOD-VALID-C3`), D3 `.safeParse()` non-throwing 경계(`ZOD-VALID-C4`·`ZOD-VALID-C5`) |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | 계약 `FE-OC-007` + 결정 FE-D007(Zod) + processing order §7.3 + failure matrix §8.2 + error enum §5.6 — D2·D4·D5·D6 의 project-decision 근거 |
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] envelope 공유 스키마(success/failure discriminated union) + operation payload 스키마 작성 규약 확정 — 등급: `planned`
|
||||
- [ ] adapters/http 4-stage boundary validation pipeline 명세 — 등급: `planned`
|
||||
- [ ] 4종 invalid fixture(content-type/JSON/envelope/payload) → 기대 kind 매핑 테스트 — 등급: `planned`
|
||||
- [ ] ZodError → safe issue path/count 매핑(redaction) — 등급: `planned`
|
||||
- [ ] outbound requestSchema 검증 wiring — 등급: `planned`
|
||||
- [ ] payload additive-tolerance 정책 확인(`FE-OC-023` 위임 경계 확정) — 등급: `planned`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
`/branch-spec` fill 완료(2026-07-19). 프런트엔드 repo 부재 — 전 항목 `planned`. hub + zod official-doc 만을 SSOT 로 사용, 근거 없는 사실 미기재.
|
||||
|
||||
## 결정 사항
|
||||
|
||||
- 2026-07-18: boundary runtime validation 을 Zod 로 수행 / 이유: plain JS 는 컴파일 타임 타입 보장이 없어 경계의 외부 데이터 형태를 런타임에 강제해야 함 / 검토한 대안: Yup·ajv·io-ts·generated schema / 언제 대안: bundle budget 초과 또는 generated schema pipeline 필요 시 / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D007 + zod 공식 문서 `ZOD-VALID-C2`.
|
||||
- 2026-07-18: 검증 지점은 shared HTTP adapter 경계 하나(adapters/http) — call-site 개별 검증 금지 / 근거: hub §7.3 processing order + §4.2 component responsibility.
|
||||
- 2026-07-18: 경계에서 `.safeParse()`(non-throwing) default, `.parse()`(throw)는 이미 정규화 catch 안에서만 / 근거: hub §8 total-function 규칙 + `ZOD-VALID-C4`·`ZOD-VALID-C5`.
|
||||
- 2026-07-18: 4 stage 를 4종 구분 kind 로 매핑(content-type/JSON/envelope/payload) / 근거: hub §8.2 failure matrix + §5.6 error enum(enum 소유는 `FE-REG-ERROR`).
|
||||
- 2026-07-18: envelope 스키마 1개 공유(먼저) → payload 스키마 per-operation(다음) / 근거: hub §7.3 + §5.3 responseSchema.
|
||||
- 2026-07-18: payload 는 additive 미지 필드 tolerate, envelope 필수 필드 strict / 정책 owner 는 `FE-OC-023` / 근거: hub §6.4 strict 선례 + `FE-OC-023`.
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
> 각 결정과 raw source Claim ID 의 연결. `Decision ID` 는 이 branch-note 안에서 안정적으로 유지한다.
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | 경계 런타임 검증 라이브러리 = Zod (본 branch 가 소유하는 결정 FE-D007) | bundle budget 이 허용하고 generated schema pipeline 이 불필요한 동안 Zod default; bundle budget 초과 또는 generated schema pipeline 필요 시 lighter/generated validator 로 재검토 | `zod-runtime-schema-validation-official.md#ZOD-VALID-C2`, `#ZOD-VALID-C3`, `#ZOD-VALID-C4`; [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D007 | official-doc + project-decision (accepted-documented-only) | accepted-documented-only — bundle 크기·코드 evidence 없음 |
|
||||
| D2 | 검증은 shared HTTP adapter 경계(adapters/http)에서만 실행, §7.3 processing order stage 2~6 으로 | 고정 invariant — shared client 경계(`FE-OC-006`)에서만; per-call-site 검증은 registry violation 이라 대안 분기 없음 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §7.3 · §4.2 · §4.4; `zod-runtime-schema-validation-official.md#ZOD-VALID-C3` | project-decision + official-doc | shared client(`FE-OC-006`) wiring 존재에 의존; client 파이프라인 변경 시 삽입 지점 이동 |
|
||||
| D3 | 경계에서 `.safeParse()`(non-throwing) default, `.parse()`(throw)는 정규화 catch 내부 한정 | 실패를 normalized kind 로 변환해야 하는 경계 지점 = safeParse; 이미 정규화 catch 가 감싸는 내부 지점에 한해 parse+catch 허용 | `zod-runtime-schema-validation-official.md#ZOD-VALID-C4`, `#ZOD-VALID-C5`; [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §8.2 total-function | official-doc + project-decision | ZodError → safe issue path 매핑이 raw value 를 누출하면 안 됨(`FE-OC-008` 과 공동 소유) |
|
||||
| D4 | 4 stage 를 4종 구분 kind 로 방출: content-type→CONTENT_TYPE_MISMATCH, JSON→MALFORMED_JSON, envelope→ENVELOPE_MISMATCH, payload→SCHEMA_MISMATCH | §8.2·§7.3 로 고정; 단일 generic parse kind 로 병합은 fixture 별 기대 kind 매핑(Measurable completion) 위반이라 거부 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §8.2 · §7.3 · §5.6 | project-decision | kind enum 은 `FE-REG-ERROR`(`FE-OC-008` owner) 소유; 이름 변경 시 fixture 갱신 필요 |
|
||||
| D5 | envelope 스키마 1개(공유 discriminated union) 먼저(stage 4/5) → payload 스키마 per-operation(stage 6), `FE-REG-API` responseSchema 참조 | 200 이어도 envelope·payload invalid 면 success 반환 금지(SCHEMA_MISMATCH); backend envelope 형태 변경은 `FE-OC-023` compatibility 사건으로 재검토 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §7.3 envelope shapes · §5.3 responseSchema; `zod-runtime-schema-validation-official.md#ZOD-VALID-C5` | project-decision + official-doc | 공유 envelope surface — backend 계약 변경이 전 operation 에 파급 |
|
||||
| D6 | payload 는 additive 미지 필드 tolerate(forward-compatible), envelope 필수 필드는 strict | additive 필드가 검증을 깨지 않게 하되 additive vs breaking 분류가 바뀌면 `FE-OC-023` 정책을 따름 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-023` · §6.4 strict 선례 | project-decision (정책 위임); 메커니즘은 UNSUPPORTED_IMPL | Zod object 의 strip/passthrough/strict default 는 archived claim 에 없음 → 로컬 검증 필요 |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> 결정에서 도출된 `planned` blueprint. 프런트엔드 코드 부재이므로 경로·이름은 hub §4.6 planned blueprint / §5.1 registry owner map 에서 grounded 하되 전체는 `planned`.
|
||||
|
||||
### 1. 4-stage boundary validation pipeline (adapters/http)
|
||||
|
||||
> **Trace**: D2 + D4 + D5 · `FE-OC-007` (hub §7.3 processing order stage 2~6, §8.2 failure matrix)
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: content-type 매칭 규칙(`application/json` prefix match vs exact)과 JSON parse 메커니즘(`response.text()` + `JSON.parse` vs `response.json()`)은 archived claim 없음. prefix match + try/catch 를 제안 — trade-off: 단계 분리를 명시화해 fixture 별 kind 매핑이 쉬워지나 표준 근거가 아닌 임의 선택.
|
||||
|
||||
| Stage | Check | 메커니즘 (planned) | 실패 kind | Negative fixture |
|
||||
|---|---|---|---|---|
|
||||
| 1 transport | HTTP 완료 — 본 branch 범위 밖 | (owned by `FE-OC-006`) | network kinds (위임) | — |
|
||||
| 2 content-type | operation 기대 media type 과 응답 Content-Type 비교 | `application/json` prefix match (UNSUPPORTED_IMPL) | CONTENT_TYPE_MISMATCH | JSON operation + `text/html` 응답 (§8.5) |
|
||||
| 3 JSON parse | body 를 JSON 으로 파싱 | try/catch around JSON.parse (UNSUPPORTED_IMPL) | MALFORMED_JSON | not-valid-JSON body |
|
||||
| 4 envelope | envelope discriminated union `.safeParse()` | Zod object {success, data\/error, meta} | ENVELOPE_MISMATCH | top-level envelope 필드 누락 |
|
||||
| 5 success/failure 분기 | `success` 판별자 분기; false 면 error envelope shape 검증 | discriminated union on `success` | ENVELOPE_MISMATCH (분기 형태 불일치); 정상 failure 는 status 기반 kind (§8.2, 위임) | success:false + malformed error envelope |
|
||||
| 6 payload | operation responseSchema `.safeParse()` | Zod payload schema (`FE-REG-API` responseSchema) | SCHEMA_MISMATCH | 200 + payload 필드 타입 불일치 |
|
||||
| 7 mapper | DTO → application model — 본 branch 범위 밖 | (delegated) | UNKNOWN_FAILURE catch-all | [[raw/branch-notes/feature-boundary-mapper-viewmodel-contract]] `FE-OC-024` |
|
||||
|
||||
stage 1·7 은 다른 branch 소유이므로 detail 을 여기서 명세하지 않고 owner 를 가리킨다(R3).
|
||||
|
||||
### 2. 스키마 작성·배치 규약
|
||||
|
||||
> **Trace**: D1 + D5 · `FE-OC-007` + `FE-REG-API` responseSchema/requestSchema (hub §5.3)
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 스키마 파일 위치 — hub §4.6 blueprint 에 schemas 디렉터리가 없음. envelope 공유 스키마 `src/adapters/http/response-envelope-schema.js`, operation payload/request 스키마 `src/adapters/http/schemas/<operation>.js` 를 제안 — trade-off: envelope/schema mapping 을 소유한 adapters/http(§4.2)에 배치해 layering 은 유지되나 정확한 경로는 repo 생성 시 확정.
|
||||
|
||||
- **envelope 스키마**(공유, 1개) — success branch {success: literal true, data, meta{requestId, traceId, correlationId?}}, failure branch {success: literal false, error{code, category, message, retryable, details?}, meta{requestId, traceId}} (hub §7.3 shapes).
|
||||
- **payload/request 스키마**(operation별) — 이름은 §5.3 initial planned rows 에서 grounded: responseSchema `SampleResourceListPayload`·`SampleResourcePayload`, requestSchema `SampleResourceListQuery`·`CreateSampleResourceCommand`. body 없으면 requestSchema explicit `none`.
|
||||
- operation → 스키마 참조의 registry(`FE-REG-API`)는 `FE-OC-006` owner 가 소유 — 본 branch 는 참조 대상 스키마의 shape/규약만 소유(R3).
|
||||
|
||||
### 3. 검증 실패 → safe 신호 매핑 (redaction)
|
||||
|
||||
> **Trace**: D3 + D4 · `FE-OC-007` → `FE-OC-008` 기여 (hub §8.1 normalized shape, §8.2 telemetry rule)
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: `.safeParse()` result.error(ZodError)에서 추출할 정확한 필드 shape — archived claim 은 "granular information"(`ZOD-VALID-C4`)까지만. {schemaId, issuePathCount, safeIssuePaths[]} 만 추출하고 raw value 제외를 제안 — trade-off: §8.2 SCHEMA_MISMATCH telemetry rule("schema ID + safe issue path count")과 일치하나 issue path 직렬화 세부는 로컬 검증 필요.
|
||||
|
||||
- ENVELOPE_MISMATCH telemetry: schema version, no body (§8.2).
|
||||
- SCHEMA_MISMATCH telemetry: schema ID + safe issue path count (§8.2).
|
||||
- normalized failure 에 raw body/value/token/authorization header/full URL/stack 포함 금지 (§8.1).
|
||||
- 최종 normalized failure shape·userMessageKey·action·severity·UX 는 [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] `FE-OC-008` owner 소유로 위임(R3) — 본 branch 는 stage 신호 + safe issue subset 까지만.
|
||||
|
||||
### 4. outbound requestSchema 검증
|
||||
|
||||
> **Trace**: D5 (requestSchema 필드) · `FE-OC-007` + `FE-REG-API` (hub §5.3 "params/search도 검증")
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: outbound requestSchema 실패의 normalized kind — hub §8.2 에 "로컬 outbound schema 실패" row 없음. 개발자 계약 위반이므로 요청 전송 없이 즉시 실패시키고 kind 는 `FE-OC-008` owner 와 협의(잠정 UNKNOWN_CLIENT_FAILURE 또는 전용 kind)를 제안 — trade-off: 사용자 노출 실패가 아니라 개발 단계 검출용이므로 별도 kind 없이 throw + test 로 처리 가능.
|
||||
|
||||
- params/search/body 를 send 전 operation requestSchema 로 검증. body 없으면 explicit `none`(§5.3).
|
||||
|
||||
### 5. compatibility posture (additive tolerance)
|
||||
|
||||
> **Trace**: D6 · `FE-OC-007` → `FE-OC-023` 기여 (hub §6.4 strict 선례)
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: Zod object 의 unknown-key 처리(strip/passthrough/strict) default — archived claim 없음(zod 문서는 parse/safeParse/ZodError 만 발췌). payload 는 unknown 필드 tolerate(additive-safe), envelope 는 strict 를 제안 — trade-off: additive backend 필드가 검증을 깨지 않으나 정확한 Zod 구성은 로컬 검증 필요.
|
||||
> - **R3(OUT_OF_BRANCH_SCOPE)**: additive vs breaking 분류·migration·version bump 규칙은 [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] `FE-OC-023` owner 소유 — 여기서 정하지 않음.
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- 200 status 인데 JSON/envelope/payload invalid → success 로 반환하지 않고 각 stage kind 로 실패 (§7.3).
|
||||
- 4xx/5xx body 가 invalid → status 기반 safe fallback error 생성, raw body 폐기 (§7.3). status → kind 매핑 자체는 §8.2(`FE-OC-008` 소유).
|
||||
- 정상 실패 envelope(success:false) → SCHEMA_MISMATCH 아님; error envelope shape 검증 후 status 기반 kind 로 매핑.
|
||||
- 빈 body / body 없는 operation(requestSchema `none`) → payload 검증 skip, envelope 검증만.
|
||||
- validator/mapper 자체 throw → 최종 catch-all UNKNOWN_FAILURE (§8.2 total function); throw 를 presentation 으로 통과시키는 경로 금지.
|
||||
- deep clone(대량 payload) 비용 — `ZOD-VALID-C3` 은 deep clone 을 명시하나 성능은 증명 안 함 → Claims To Verify 로 이월.
|
||||
- **다른 계약 의존**:
|
||||
- [[raw/branch-notes/feature-api-client-response-envelope-contract]] 의 shared client 경계 `FE-OC-006` 을 consume — 검증은 이 client 응답 파이프라인 stage 2~6 에 삽입. `FE-REG-API` responseSchema/requestSchema 필드 변경 시 본 branch wiring 영향.
|
||||
- [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] 에 `FE-OC-008` 기여 — 4종 kind + normalized shape + UX/telemetry 소유.
|
||||
- [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] 에 `FE-OC-023` 기여 — schema additive/breaking 정책 소유.
|
||||
- [[raw/branch-notes/feature-boundary-mapper-viewmodel-contract]] 가 검증된 payload(stage 7)를 consume — raw DTO 직접 사용 금지(`FE-OC-007` 기여).
|
||||
- [[raw/branch-notes/feature-sample-feature-slice-contract-fixture]] 가 sample operation 스키마로 이 gate 를 관통(`FE-OC-024`).
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| 4종 invalid fixture(content-type/JSON/envelope/payload)가 각각 기대 kind 로 매핑됨 | 코드 없음; 매핑은 planned 명세뿐 | §20 measurable: content-type/JSON/envelope/payload invalid fixture 테스트(`FE-GATE-004` schema report) | `needs-confirmation` |
|
||||
| `.safeParse()` 경로가 어떤 invalid 응답에서도 throw 를 presentation 으로 누출하지 않음(total function) | zod 는 ZodError 를 throw 가능(`ZOD-VALID-C4`); safeParse 사용이 코드로 강제되는지 미검증 | catch-all UNKNOWN_FAILURE fixture + throw 누출 negative test (§8.2) | `needs-confirmation` |
|
||||
| payload additive 미지 필드가 SCHEMA_MISMATCH 를 유발하지 않음(forward-compatible) | Zod unknown-key default 가 archived claim 에 없음 | additive-field fixture 통과 확인 + `FE-OC-023` compatibility fixture | `needs-confirmation` |
|
||||
| envelope → payload 순서로 200 + invalid payload 가 success 로 반환되지 않음 | 처리 순서는 §7.3 명세뿐, 코드 없음 | 200 + invalid payload fixture → SCHEMA_MISMATCH 기대 | `needs-confirmation` |
|
||||
| ZodError → safe issue subset 매핑이 raw value/PII 를 누출하지 않음 | granular info 추출 시 원본 값 포함 위험(`ZOD-VALID-C4`) | redaction negative test(raw body/stack 누출 검사, §8.2 · `FE-OC-008`) | `needs-confirmation` |
|
||||
| deep clone 검증 성능이 boundary budget 내 | `ZOD-VALID-C3` deep clone 비용 미증명 | 대량 payload 벤치(`FE-GATE-004` timing fixture 는 config 소유 — 협업) | `needs-confirmation` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
> `/coverage`가 채우는 생성물이며 손으로 유지하지 않는다.
|
||||
|
||||
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| TODO — `/coverage` 실행 전 | missing | (없음) | 미평가 | TODO |
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: flow:start -->
|
||||
### 가져온 흐름 단계
|
||||
|
||||
| Stage Ref | Order | Owner | Input | Action | Output |
|
||||
|---|---:|---|---|---|---|
|
||||
| `FLOW-FE-RESP-001@1` | 1 | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | HTTP 요청 | transport 완료 대기 | raw Response |
|
||||
| `FLOW-FE-RESP-002@1` | 2 | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | raw Response | content-type 기대값 검사 | 본문 판독 가능 Response |
|
||||
| `FLOW-FE-RESP-003@1` | 3 | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | 본문 판독 가능 Response | JSON parse | unvalidated JSON |
|
||||
| `FLOW-FE-RESP-007@1` | 7 | [[raw/branch-notes/feature-boundary-mapper-viewmodel-contract]] | 검증된 payload | DTO → application model 매핑 | application model |
|
||||
| `FLOW-FE-RESP-008@1` | 8 | [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] | application model 또는 실패 신호 | 정규화된 결과 반환 | application result 또는 normalized failure |
|
||||
<!-- GENERATED: flow:end -->
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-OC-006@1` | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | 모든 HTTP는 shared client를 MUST 통과하고 timeout·abort·response parsing을 page에서 구현하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-008@1` | [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] | 모든 failure는 stable frontend error kind로 MUST 정규화하고 raw body·stack을 UI에 노출하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-023@1` | [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] | API/config/storage/release schema의 breaking change는 migration 또는 version bump 없이 배포하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-024@1` | [[raw/branch-notes/feature-sample-feature-slice-contract-fixture]] | sample은 contract fixture이며 production feature가 의존하면 안 됨 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
### Sub-branches (세부 작업)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 오류 기록 (이 branch 작업 중 발생)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 강의 (이 작업을 위해 학습한 강의)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### job-posting tie-ins (이 작업에서 파생된 글감)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크:
|
||||
- 리뷰 메모:
|
||||
- 머지 결과 / 배포 환경:
|
||||
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
|
||||
- `actually-implemented` 항목:
|
||||
- `locally-verified` 항목:
|
||||
- `prod-verified` 항목:
|
||||
- **추출하지 않을 항목** (planned / documented-only / abandoned):
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
---
|
||||
title: branch / feature-sample-feature-slice-contract-fixture
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
branch: feature-sample-feature-slice-contract-fixture
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract.md]
|
||||
tags: [branch, ca-skeleton, frontend, testing, react, clean-architecture]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-016
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-016
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-SAMPLE-FIXTURE-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-005, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-006, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-007, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-009, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-010]
|
||||
contract_packet: 1
|
||||
contract_packet_sha256: ba88989473c8fb9032f91d0f50b2c7d1dfc9cb05ad0a6d0354e81d318256dee9
|
||||
imports: [FE-GATE-006@1, FE-GATE-007@1, FE-GATE-008@1, FE-OC-002@1, FE-OC-005@1, FE-OC-007@1, FE-OC-011@1, FE-OC-012@1]
|
||||
---
|
||||
|
||||
# branch: feature-sample-feature-slice-contract-fixture
|
||||
|
||||
> Layer: `raw/branch-notes/` — 단일 브랜치의 TODO·결정·진행 기록. 현재는 scaffolding 단계다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: full contract slice와 sample removal smoke test가 통과한다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-SAMPLE-FIXTURE-001@1` | sample slice는 제거 가능한 contract fixture이며 product import를 금지한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-frontend-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 -->
|
||||
## 목표
|
||||
|
||||
이 브랜치는 project-wide contract `FE-OC-024`(sample은 contract fixture이며 production feature가 의존하면 안 됨)를 *되묻지 않고 구현할 수 있는 명세*로 내린다. hub 결정 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D025(sample slice = 제거 가능한 contract fixture, product import 금지)에 근거해, skeleton이 "새 feature도 같은 architecture·API failure language·runtime validation·async UI·server-state·quality gate를 재사용하는가"를 증명하는 **단일 end-to-end reference vertical**(API → schema → mapper → application → presentation, hub `FE-SC-002`)을 정의한다. 그리고 그 vertical이 언제든 통째로 제거돼도 production build/smoke가 깨지지 않음을 gate `FE-GATE-020`(`pnpm test:sample-removal`)으로 강제한다. 이 vertical은 여러 계약을 end-to-end로 **행사(exercise)** 하지만 각 계약의 메커니즘은 소유하지 않고 owner branch에 위임한다. frontend 구현 repository가 아직 식별되지 않았으므로 본 노트의 모든 구현 항목은 `planned` 등급이다.
|
||||
|
||||
- 이슈: 없음 (repository 미생성)
|
||||
- PR: 없음
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- sample contract fixture slice의 **존재·격리·제거 가능성** — `FE-OC-024`, `FE-GATE-020`.
|
||||
- `FE-REG-API`의 2개 sample operation 행 **소유(등록 정의)** — `LIST_SAMPLE_RESOURCES`, `CREATE_SAMPLE_RESOURCE` (hub §5.3).
|
||||
- API → schema → mapper → application → presentation을 관통하는 **end-to-end reference vertical wiring** — `FE-SC-002` (hub §20 measurable completion "full contract slice").
|
||||
- **sample removal smoke test/gate** — `pnpm test:sample-removal` → `artifacts/tests/sample-removal.xml` (hub §14.3, `FE-GATE-020`).
|
||||
- product/production 코드의 **sample import 금지 invariant** — FE-D025.
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외. 아래는 다른 owner branch가 소유하며 sample vertical은 이들을 *소비/행사* 만 한다 (CLAUDE.md §15.5 R3, OUT_OF_BRANCH_SCOPE 방지).
|
||||
|
||||
- shared HTTP client 내부(timeout/abort/retry/envelope parsing, idempotency key 생성) → [[raw/branch-notes/feature-api-client-response-envelope-contract]] `FE-OC-006`·`FE-OC-009` 소유.
|
||||
- runtime schema 작성·검증 엔진 (Zod schema shape/validation) → [[raw/branch-notes/feature-runtime-schema-validation-contract]] `FE-OC-007` 소유.
|
||||
- boundary mapper 메커니즘 (2-stage 배치 = stage 7 `DTO→application model`(`FLOW-FE-RESP-007@1`) 이후 application 이 view-model 로 투영, raw DTO 직접 사용 금지 규칙, mapper negative fixture, mapper 모듈 명명) → [[raw/branch-notes/feature-boundary-mapper-viewmodel-contract]] 소유 (`FE-OC-007`·`FE-OC-024` 기여 branch). 본 branch는 그 mapper가 산출할 sample view-model *필드 목록* 만 확정한다.
|
||||
- styling 시연(디자인 token·arbitrary value policy·async 시각 primitive)의 내용과 화면 구성 → [[raw/branch-notes/feature-tailwind-design-token-styling-contract]] 소유 (`FE-OC-024` 협업 branch). **본 서브트리의 co-tenant 기여자** — 그 branch가 `src/sample/contract-fixture/` 안에 styling 시연 UI 를 놓는다(mapper branch와 동일 패턴). 본 branch는 그 시연부를 §1 제거 단위 *안에* 수용할 뿐 token 어휘·시각 primitive 를 정의하지 않는다.
|
||||
- error 정규화 taxonomy/matrix → [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] `FE-OC-008` 소유.
|
||||
- route registry schema·guard·param validation·redirect-loop 방지 → [[raw/branch-notes/feature-routing-navigation-guard-contract]] `FE-OC-005` 소유.
|
||||
- `QueryCachePort` 정의·TanStack adapter·invalidation·stale 정책 → [[raw/branch-notes/feature-server-state-caching-contract]] `FE-OC-012` 소유.
|
||||
- async surface state model 정의 → [[raw/branch-notes/feature-async-ui-state-contract]] `FE-OC-011` 소유.
|
||||
- CI gate/fixture/artifact taxonomy → [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] `FE-OC-020` 소유.
|
||||
- web-vitals budget/report (sample list는 측정 fixture일 뿐) → [[raw/branch-notes/feature-web-vitals-performance-budget-contract]] `FE-OC-021` 소유.
|
||||
- 정적 import 금지 규칙 *authoring* (dependency-cruiser/ESLint rule) → [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]] `FE-OC-002` 소유.
|
||||
- token 발급/저장/refresh lifecycle → external auth owner / [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] `FE-OC-010` 소유.
|
||||
- domain/business rule, product analytics taxonomy, branding/copy (hub §0.5 out of scope).
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/official-docs/react-ui-library-official]] REACT-UI-C1 | D5 — sample presentation을 React 컴포넌트로 구성 (project decision FE-D004 consume) |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D025 · FE-OC-024 | D1·D6 — sample = 제거 가능 fixture, product import 금지 |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-SC-002 · §20 | D2 — API→schema→mapper→application→presentation full contract slice reference vertical |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-GATE-020 · §14 | D3 — sample removal smoke gate (`pnpm test:sample-removal`) |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5 | D4 — sample API operation/route registry 행(`FE-REG-API`/`FE-REG-ROUTE`) |
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] `src/sample/contract-fixture/` 서브트리 생성 + 모든 sample 코드를 이 한 디렉터리로 격리 — 등급: `planned`
|
||||
- [ ] `pnpm test:sample-removal` smoke + `artifacts/tests/sample-removal.xml` 산출 — 등급: `planned`
|
||||
- [ ] `FE-REG-API` sample operation 2행(`LIST_SAMPLE_RESOURCES`/`CREATE_SAMPLE_RESOURCE`) + schema 참조 wiring — 등급: `planned`
|
||||
- [ ] API→schema→mapper→application→presentation 관통 vertical 구현 (contributing 계약 owner 완료 후) — 등급: `planned`
|
||||
- [ ] §4.1 sample view-model 필드 목록을 backend payload 계약 확정 시 재검토 (현재 임의 채택) — 등급: `needs-confirmation`
|
||||
- [ ] product code의 sample import 금지 정적 규칙 연동 (architecture-enforcement branch 위임) — 등급: `needs-confirmation`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
- 없음 — repository 미생성, 모든 항목 `planned`.
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> 아래는 Decision Evidence Map의 prose 미러. 각 근거는 hub 결정 register 또는 archived official-doc.
|
||||
|
||||
- 2026-07-19: sample slice는 **제거 가능한 contract fixture**이며 production/product 코드가 import하지 못한다 (D1). 이유: skeleton의 계약 준수를 증명할 reference가 필요하되 제품 코드가 그것에 결합되면 안 됨. 검토한 대안: fixture 없이 각 계약을 unit test로만 검증 → 계약 간 wiring 회귀를 못 잡음. 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D025 · FE-OC-024.
|
||||
- 2026-07-19: sample slice는 **API → schema → mapper → application → presentation을 관통하는 단일 end-to-end reference vertical**이다 (D2). 이유: 계약 상호작용을 통합 fixture 1개로 증명. 대안: 통합 vertical 없이 계약별 unit fixture만. 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-SC-002 · §20.
|
||||
- 2026-07-19: sample **removal은 전용 smoke gate로 강제**한다 — `src/sample/` 제거 후 production build/smoke green + product import 0 (D3). 이유: removability를 회귀 방지 gate로. 대안: 수동 리뷰. 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-GATE-020 · §14.
|
||||
- 2026-07-19: sample은 **등록된 registry 행만 사용**한다 — `FE-REG-API`의 2 operation, `FE-REG-ROUTE`의 sample 행; call site raw fetch/route literal 금지 (D4). 이유: fixture가 "좋은 예시"여야 함. 대안: ad-hoc token. 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.
|
||||
- 2026-07-19: sample presentation은 **React 컴포넌트로 구성**한다 (D5, FE-D004 consume). 이유: React가 project UI 기본. 대안: 다른 framework/native. 근거: [[raw/official-docs/react-ui-library-official]] REACT-UI-C1.
|
||||
- 2026-07-19: sample scope는 **fixture wiring으로 한정** — domain/business rule·product analytics 도입 금지, 어떤 product feature의 의존 대상도 되지 않음 (D6). 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D025 · FE-OC-024.
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | sample slice = 제거 가능한 contract fixture, product/production 코드가 import 금지 (`FE-OC-024`) | skeleton이 "새 feature도 같은 계약을 따르는가"를 증명할 reference vertical이 필요한 동안 유지; 대안(fixture 삭제)은 fixture 없이 동일 gate coverage를 증명할 수 있을 때(hub revisit trigger) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D025, FE-OC-024 | `project-decision` | fixture 제거 시 계약들이 end-to-end로 함께 동작하는지 검증할 통합 표면 상실; revisit trigger 충족 여부 미검증 |
|
||||
| D2 | sample = API→schema→mapper→application→presentation을 관통하는 단일 end-to-end reference vertical; contributing 계약(`FE-OC-005/006/007/008/011/012/020/021`)을 행사하나 메커니즘은 미소유 | 통합 fixture 1개로 계약 상호작용을 증명하는 것이 계약별 unit test만보다 나을 때; 대안은 통합 vertical 없이 unit fixture만(계약 간 wiring 회귀 미포착) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-SC-002, §20 | `project-decision` | contributing 계약 owner branch 미완이면 vertical이 실제로 관통 못 함(dependency). vertical의 **mapper stage 는 본 branch 미소유** — [[raw/branch-notes/feature-boundary-mapper-viewmodel-contract]] 이 소유하고 본 branch 는 sample view-model 필드 목록만 확정(§구현 가이드 §4) |
|
||||
| D3 | sample removal을 전용 smoke gate로 강제 — `src/sample/` 제거 후 production build/smoke green + 잔존 product import 0 (`FE-GATE-020`) | removability를 자동 회귀 gate로 둘 때; 대안은 수동 코드리뷰(회귀 방지 불가) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-GATE-020, §14 | `project-decision` | import 검출 메커니즘(정적 스캔 vs build 실패)이 hub 미명시 → §구현 가이드 UNSUPPORTED_IMPL_DECISION |
|
||||
| D4 | sample은 등록된 registry 행만 사용 — `FE-REG-API` 2 operation 소유 + `FE-REG-ROUTE` sample 행 consume; call site raw fetch/route literal 금지 | fixture가 registry-first "좋은 예시"여야 할 때(항상); 대안은 ad-hoc token(fixture 목적에 반함) → N/A | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5 | `project-decision` | request/response schema shape(`SampleResource*`) 이름만 있고 필드 미정 → schema branch 위임 |
|
||||
| D5 | sample presentation을 React 컴포넌트로 구성 (project decision FE-D004 consume) | React가 project UI 기본인 동안 유지; 대안(native/custom-element/다른 framework)은 FE-D004 revisit trigger 충족 시 | [[raw/official-docs/react-ui-library-official]] REACT-UI-C1, [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D004 | `official-doc` | 컴포넌트가 async surface 4상태(§9.1)를 완전히 표현해야 하나 그 matrix는 async-ui branch 소유 → 위임 |
|
||||
| D6 | sample scope는 fixture wiring으로 한정 — domain/business rule·product analytics 도입 금지, 어떤 product feature도 sample에 의존 금지 | invariant(분기 없음) → N/A; product feature의 sample import = build/gate 실패로 고정 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D025, FE-OC-024 | `project-decision` | contributing 계약 owner가 계약을 바꾸면 sample vertical 갱신 필요(delegated dependency) |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> 모두 `planned` — frontend 구현 repository가 아직 없다. 경로는 hub §4.6 Planned directory blueprint + §5 registry owner map에서 도출한 grounded anchor이나 repository 생성 시 변경 가능.
|
||||
|
||||
### 1. Sample 서브트리 & removability 경계
|
||||
|
||||
> **Trace**: D1 + D3 + `FE-OC-024`; hub §4.6 blueprint(`src/sample/contract-fixture/`).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: sample import 금지의 *검출 메커니즘*(dependency-graph inbound-edge 규칙 vs ESLint no-restricted-imports vs removal smoke의 build 실패) 은 hub가 gate(`FE-GATE-020`)와 command만 주고 미명시. Trade-off: 정적 dependency 규칙(외부→`src/sample/**` inbound import 0) + removal smoke의 이중 방어를 권고하되, 규칙 *authoring* 은 [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]] `FE-OC-002` 로 위임(R3).
|
||||
|
||||
| 항목 | Planned 값 | 근거 |
|
||||
|---|---|---|
|
||||
| sample 코드 위치 | `src/sample/contract-fixture/` (단일 서브트리) | hub §4.6 |
|
||||
| removability 규칙 | `src/sample/` 외부의 어떤 모듈도 `src/sample/`를 import 금지 | D1 (FE-D025) |
|
||||
| 제거 단위 | 서브트리 1개 삭제 = feature 제거 (product 코드 무변경) | D1·D3 |
|
||||
|
||||
### 2. Sample removal smoke test / gate
|
||||
|
||||
> **Trace**: D3 + `FE-GATE-020` + hub §14.3 `pnpm test:sample-removal`.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 제거 방식(CI에서 ephemeral copy 후 `rm -rf` vs build flag로 dir 제외 vs git worktree)과 "smoke" 판정 assert 목록이 hub 미명시. Trade-off: source 비파괴적인 ephemeral copy + `rm` 을 권고하고, smoke는 최소 "`APP_HOME` shell mount 성공 + `SAMPLE_RESOURCE_LIST` route 부재 + build exit 0" 를 assert.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: step d의 *ID-residue 검출 메커니즘* — §1의 label은 모듈 *import* 검출만 다루고, 제거 후 남은 **문자열 ID 잔재**(`LIST_SAMPLE_RESOURCES`/`CREATE_SAMPLE_RESOURCE` operationId, `SAMPLE_RESOURCE_LIST` routeId, sample query-key)의 검출 방식은 hub 미명시. Trade-off: 남은 서브트리 전체에 대한 **registry ID token grep(고정 문자열 exact-match, 검사 대상 ID 목록은 `FE-REG-API`/`FE-REG-ROUTE`의 sample owner 행에서 생성)** 을 채택 — dependency-graph 스캔은 문자열 리터럴을 못 잡고 build 실패는 dead 상수를 못 잡기 때문. 검사 범위는 `src/` **와 `tests/`** 둘 다로 둔다. 비용(false positive): grep 은 주석/문서의 우연한 언급도 잡는다. 더 중요한 것은 **false negative** 쪽인데, `src/` 만 스캔하면 delegate branch 가 서브트리 *밖에* 놓은 sample 참조를 놓친다 — 예: [[raw/branch-notes/feature-boundary-mapper-viewmodel-contract]] 의 mapper negative fixture 는 `tests/unit/` 경로를 planned 로 잡고 있어, sample 제거 후 `pnpm build` 는 green 인데 test suite 가 깨지는 상태를 gate 가 통과시킬 수 있다. `tests/` 포함으로 이 구멍을 막는다.
|
||||
> - 그럼에도 `src/`·`tests/` 밖(설정 파일, CI 워크플로, 문서)의 sample 참조는 본 gate 가 검출하지 않는다. 그런 참조를 만든 **delegate branch 가 자기 몫의 제거 책임을 진다** — 본 branch 는 제거 *단위*(§1 서브트리)와 gate 를 소유하고, 각 co-tenant 기여자([[raw/branch-notes/feature-boundary-mapper-viewmodel-contract]], [[raw/branch-notes/feature-tailwind-design-token-styling-contract]])는 자신이 서브트리 밖에 남긴 참조의 제거를 소유한다.
|
||||
|
||||
| 단계 | Planned 동작 | 기대 결과 |
|
||||
|---|---|---|
|
||||
| a | `src/sample/` 서브트리를 전용 fixture/CI job에서 제거 | — |
|
||||
| b | `pnpm build` | exit 0 + manifest 존재 |
|
||||
| c | production smoke (app boot, home shell 렌더) | pass, sample route/operation 미참조 |
|
||||
| d | 잔존 sample operationId/routeId/query-key 참조 검출 — `src/` **+ `tests/`** 대상 registry ID token grep(위 UNSUPPORTED label) | 0건 |
|
||||
| artifact | `artifacts/tests/sample-removal.xml` | hub §14.3 |
|
||||
|
||||
### 3. Sample API operation & schema wiring
|
||||
|
||||
> **Trace**: D4 + hub §5.3(`FE-REG-API` 소유 행) + `FE-OC-006`. 아래 2행은 hub §5.3에서 owner=본 branch 로 지정된 grounded registry 행이다.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: request/response schema *shape*(`SampleResourceListQuery`/`SampleResourceListPayload`/`CreateSampleResourceCommand`/`SampleResourcePayload`)은 hub가 이름만 준다. Trade-off: fixture 안에 최소 placeholder shape을 정의하되 Zod schema *작성·검증 엔진* 은 [[raw/branch-notes/feature-runtime-schema-validation-contract]] `FE-OC-007` 로 위임. keyed mutation의 idempotency key 생성 계약은 [[raw/branch-notes/feature-api-client-response-envelope-contract]] `FE-OC-009` 소유.
|
||||
|
||||
| operationId | method | path | auth | timeoutMs | idempotency | requestSchema | responseSchema |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| `LIST_SAMPLE_RESOURCES` | `GET` | `/api/sample/resources` | `external-session` | `10000` | `safe` | `SampleResourceListQuery` | `SampleResourceListPayload` |
|
||||
| `CREATE_SAMPLE_RESOURCE` | `POST` | `/api/sample/resources` | `external-session` | `10000` | `keyed` | `CreateSampleResourceCommand` | `SampleResourcePayload` |
|
||||
|
||||
- sample query/command use case는 위 operation을 **shared client + application output port**(`ResourceQueryPort`/`ResourceCommandPort`, hub §4.4)로만 호출한다. shared client 메커니즘은 [[raw/branch-notes/feature-api-client-response-envelope-contract]] `FE-OC-006` 위임.
|
||||
|
||||
### 4. Sample vertical wiring (domain → application → presentation → routes)
|
||||
|
||||
> **Trace**: D2 + D5 + `FE-SC-002`; hub §4.2 component responsibility, §4.6 layer dirs, §9.1 async states.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 컴포넌트/파일 이름(예: `SampleResourceListPage.jsx`)과 domain 모델 유무는 hub 미명시. Trade-off: 이름은 operationId를 미러(`SampleResourceListPage`), domain은 fixture이므로 비워두거나 trivial `SampleResource` value만 — layering 규칙 자체는 [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] `FE-OC-002` 소유.
|
||||
|
||||
| 레이어 | sample이 제공(in-scope) | 위임(다른 owner) |
|
||||
|---|---|---|
|
||||
| domain | (선택) trivial `SampleResource` value 또는 없음 | layering 규칙 → `FE-OC-002` |
|
||||
| mapper (boundary) | sample view-model의 **구체 필드 목록**만 확정 — §4.1 표 (`SampleResourceListPayload`/`SampleResourcePayload` → sample view-model) | mapper 메커니즘 자체(2-stage 배치, raw DTO 직접 사용 금지 규칙, negative fixture, 명명 convention) → [[raw/branch-notes/feature-boundary-mapper-viewmodel-contract]] (`FE-OC-024` 기여분 · `FE-OC-007`) |
|
||||
| application | sample query/command use case + view-model **투영**(mapper 계약 준수), `QueryCachePort` 소비 | view-model 계약 메커니즘 → 위 mapper 행; port 정의 → `FE-OC-002`/`FE-OC-012` |
|
||||
| presentation | React sample page/component, async 4상태 렌더(§9.1) | async state model → `FE-OC-011` |
|
||||
| adapters | 기존 http/query-cache adapter *재사용* (신규 adapter 없음) | adapter 구현 → owner branch |
|
||||
| routes | `APP_HOME`(sample shell)·`SAMPLE_RESOURCE_LIST`(fixture)의 route element/loading/error surface 내용 | route registry/guard → `FE-OC-005` |
|
||||
|
||||
- vertical의 **mapper stage는 본 branch가 소유하지 않는다** — 메커니즘(adapter→validated model→application view-model 2-stage 배치, "raw DTO 직접 사용 금지" 규칙, mapper negative fixture)은 [[raw/branch-notes/feature-boundary-mapper-viewmodel-contract]] 소유이고, 그 branch의 mapper 시연부가 본 sample 서브트리 *안에* 놓인다(양방향 기여). 본 branch는 그 계약을 **소비**하며 그 mapper가 산출할 sample view-model의 *필드 목록* 만 확정한다(해당 branch가 명시적으로 `FE-OC-024` owner 에게 위임한 부분 — §4.1). 따라서 위 표 `application` 행의 "view-model"은 *계약 소유* 가 아니라 *투영 수행* 을 뜻하고, `adapters` 행의 "신규 adapter 없음"은 mapper 모듈이 기존 http adapter 재사용 위에 그 branch 몫으로 추가된다는 뜻이다.
|
||||
- `APP_HOME`(`/`, public) 과 `SAMPLE_RESOURCE_LIST`(`/sample/resources`, integration-defined)는 hub §5.2 등록 행이다. 본 branch는 그 route의 *content* 만 제공하고 registry schema·guard·redirect-loop 방지는 [[raw/branch-notes/feature-routing-navigation-guard-contract]] `FE-OC-005` 위임.
|
||||
|
||||
#### 4.1 Sample view-model 필드 목록 (본 branch 단독 소유)
|
||||
|
||||
> **Trace**: D2 + D5 → hub §5.3(payload schema 이름), §9.1(async 4상태); [[raw/branch-notes/feature-boundary-mapper-viewmodel-contract]] 이 `FE-OC-024` owner 에게 명시 위임한 항목(그 노트 §3 OUT_OF_BRANCH_SCOPE). 그 branch 의 mapper 가 *산출할* 결과물의 shape 을 본 branch 가 확정한다 — mapper 메커니즘(2-stage 배치·total function·negative fixture)은 여전히 그 branch 소유.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 아래 **필드 이름·개수·타입은 hub 미명시**다. hub §5.3 은 payload schema 의 *이름*(`SampleResourceListPayload`/`SampleResourcePayload`)만 주고 필드를 열거하지 않으며, backend API 도 아직 없다. Trade-off: fixture 의 목적은 *도메인 표현*이 아니라 *계약 시연*이므로 **§9.1 4상태를 렌더하는 데 필요한 최소 필드만** 임의 채택했다 — 식별자 1개(list key·mutation 대상), 표시 문자열 1개(도메인 의미 도입 금지 D6), 포맷 완료된 시각 1개(포맷팅이 presentation 이 아닌 view-model 책임임을 시연), 그리고 `success` 와 `empty` 를 presentation 이 재계산 없이 구분할 파생 flag 1개. backend 계약이 확정되면 이 표가 1차 갱신 대상이다.
|
||||
|
||||
| view-model | 필드 | 타입 | 왜 이 필드인가 (시연 목적) |
|
||||
|---|---|---|---|
|
||||
| `SampleResourceListViewModel` (← `SampleResourceListPayload`) | `items` | `SampleResourceItemViewModel[]` | `success` 상태의 render 입력(§9.1) |
|
||||
| | `isEmpty` | `boolean` | `success` vs `empty` 를 presentation 이 재계산 없이 분기(§9.1 "loading boolean 하나로 병합 금지" 정합). `items.length === 0` 의 파생값 |
|
||||
| `SampleResourceItemViewModel` | `id` | `string` | list key + `CREATE_SAMPLE_RESOURCE` 후 invalidation 대상 식별 |
|
||||
| | `label` | `string` | 표시 전용 문자열. 도메인 의미 없음(D6 — fixture 는 business rule 도입 금지) |
|
||||
| | `updatedAtText` | `string` | **포맷 완료된** 표시 문자열. `Date`/epoch 를 넘기지 않아 "포맷팅은 view-model 책임, presentation 은 render 만" 을 시연 |
|
||||
| `SampleResourceViewModel` (← `SampleResourcePayload`) | = `SampleResourceItemViewModel` 과 동일 shape | — | `CREATE_SAMPLE_RESOURCE` 성공 결과를 목록 항목과 같은 shape 으로 투영 → mutation 후 캐시 갱신 시 두 번째 매핑 규칙 불필요 |
|
||||
|
||||
- 위 view-model 은 raw HTTP status·backend error code·DTO 필드명을 **그대로 노출하지 않는다**(mapper branch D3 계약 준수). optional 필드 부재는 throw 가 아니라 안전 default/absent 로 표기한다.
|
||||
- `SampleResourceListQuery`/`CreateSampleResourceCommand` 는 view-model 이 아니라 *request* schema 이므로 본 표 밖이다 — 그 shape 은 §3 의 UNSUPPORTED_IMPL_DECISION 이 다룬다.
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- sample removal이 production build를 깬다 → product 코드가 sample에 의존한다는 신호 → `FE-GATE-020` 실패, merge/release 차단 (D3).
|
||||
- sample list read 실패(네트워크/schema/error) → 정규화된 frontend error kind로 표시되어야 하나, 정규화 자체는 error-classification 소유; sample은 그 결과를 **렌더만** 한다.
|
||||
- empty result → §9.1 `empty` 상태(빈 사유 + primary action) 표현 — 상태 모델은 async-ui 소유.
|
||||
- `CREATE_SAMPLE_RESOURCE`(keyed mutation) 재시도 → stable idempotency key + backend replay contract 없으면 replay 금지(hub §8.5) — 규칙은 api-client 소유.
|
||||
- runtime config/boot 실패 시 sample route는 mount되지 않음(hub §4.5 boot 2~4단계 실패 → boot error shell) — boot는 env/config branch 소유.
|
||||
- **다른 계약 의존** (sibling branch consume; 계약 변경 시 sample vertical 갱신 필요):
|
||||
- [[raw/branch-notes/feature-api-client-response-envelope-contract]] — `FE-OC-006`·`FE-OC-009` shared client + retry/timeout/idempotency consume.
|
||||
- [[raw/branch-notes/feature-runtime-schema-validation-contract]] — `FE-OC-007` boundary schema validation consume.
|
||||
- [[raw/branch-notes/feature-boundary-mapper-viewmodel-contract]] — vertical의 mapper stage owner. "raw DTO 직접 사용 금지 → mapper가 view-model 생산" 계약을 consume 하고, 그 branch의 mapper 시연부·negative fixture 를 본 sample 서브트리 안에 수용한다 (`FE-OC-007`·`FE-OC-024` 교집합). 계약 변경 시 sample view-model 필드 목록 갱신 필요.
|
||||
- [[raw/branch-notes/feature-tailwind-design-token-styling-contract]] — **서브트리 co-tenant 기여자**(mapper branch와 동일 패턴). 그 branch가 `src/sample/contract-fixture/` 안에 token·async 시각 primitive 시연 UI 를 놓는다(그 노트 §5 "sample UI fixture — 협업 `FE-OC-024`"). 본 branch는 그 파일들을 §1 **제거 단위 안에** 수용하며, 따라서 §2 removal smoke 는 그 시연부까지 함께 제거된 상태를 검증한다. token 어휘·화면 구성은 그 branch 소유.
|
||||
- [[raw/branch-notes/feature-frontend-storage-registry-contract]] — `FE-OC-013` **storage key 계약을 fixture 로 사용**(그 노트가 본 branch 를 dependency 로 선언한 단방향 관계의 반대편 기록). sample slice 가 storage 를 쓸지 여부는 본 branch 결정이며 현재 **미확정** — §9.1 4상태 시연에 storage 가 필수는 아니므로 기본 입장은 "sample 은 storage 를 쓰지 않음"이고, 쓰기로 하면 namespace/version/classification 규약은 그 branch 소유다. repository 생성 시 확정.
|
||||
- [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] — `FE-OC-010` **runtime session state consume**. §3의 두 sample operation이 모두 `auth`=`external-session` 이므로 request 전 `AuthSessionPort.attach(request)` 와 unauthenticated transition 통지를 그 branch에서 공급받는다. Out of scope의 *token lifecycle* 위임과는 별개 관심사(그쪽은 발급/저장/refresh, 이쪽은 런타임 세션 소비).
|
||||
- [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] — `FE-OC-008` normalized error kind consume.
|
||||
- [[raw/branch-notes/feature-routing-navigation-guard-contract]] — `FE-OC-005` route registry/guard consume.
|
||||
- [[raw/branch-notes/feature-server-state-caching-contract]] — `FE-OC-012` `QueryCachePort`/invalidation consume.
|
||||
- [[raw/branch-notes/feature-async-ui-state-contract]] — `FE-OC-011` async surface state model consume.
|
||||
- [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] — `FE-OC-020` gate/fixture/artifact taxonomy consume.
|
||||
- [[raw/branch-notes/feature-web-vitals-performance-budget-contract]] — `FE-OC-021` (sample list가 lab/field 측정 fixture).
|
||||
- [[raw/branch-notes/feature-accessibility-baseline-contract]] — (advisory) `FE-GATE-009`(accessibility)가 Covered 계약에 `FE-OC-024` 를 포함하므로 axe/keyboard 검사가 사실상 sample route 를 대상으로 돈다. a11y 기준·증거는 그 branch 소유이고 hub §20이 본 branch 에 배정하지 않았다 — 발견성 목적의 포인터일 뿐 in-scope 아님.
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| sample subtree 제거 후 production build/smoke가 통과한다 | 코드/CI 없음; product import 부재가 미검증 | `pnpm test:sample-removal` (§14.3) → `artifacts/tests/sample-removal.xml` exit 0 (`FE-GATE-020`) | `needs-confirmation` |
|
||||
| sample vertical이 API→schema→mapper→application→presentation을 실제로 관통한다 | contributing 계약 owner branch 미완; wiring 미구현 | integration test(MSW) + e2e sample critical read/write (`FE-GATE-007`/`FE-GATE-008`) | `needs-confirmation` |
|
||||
| 어떤 product feature도 sample을 import하지 않는다 | 정적 검출 메커니즘 미정(UNSUPPORTED_IMPL) | dependency-graph 규칙(architecture-enforcement 위임) + removal smoke | `needs-confirmation` |
|
||||
| sample list가 async surface 4상태(loading/success/empty/terminal-error)를 표현한다 | async 상태 matrix는 async-ui branch 소유, 미구현 | component state matrix test (`FE-GATE-006`, §9.1) | `needs-confirmation` |
|
||||
| §4.1의 sample view-model 필드 목록이 §9.1 4상태 렌더에 충분하다 | 필드가 hub 미명시 상태에서 임의 채택됨(UNSUPPORTED_IMPL_DECISION); backend payload 계약 미존재 | mapper 단위 테스트(payload→view-model 투영) + component state matrix test 로 4상태가 이 필드만으로 렌더되는지 확인; backend 계약 확정 시 표 갱신 | `needs-confirmation` |
|
||||
| React 컴포넌트 구성이 sample presentation에 충분하다 | REACT-UI-C1은 컴포넌트 모델 *존재* 만 증명, 프로젝트 적용 보장 아님 | component test로 sample page 렌더 확인 | `needs-confirmation` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
- 없음 — repository 미생성 단계.
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-GATE-006@1` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | component 레벨이 실패하면 merge 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-007@1` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | MSW 기반 integration 매트릭스가 미충족이면 merge 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-008@1` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | critical e2e 시나리오가 실패하면 merge·release 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-OC-002@1` | [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] | `domain <- application <- presentation` 의존 방향과 application-owned output port를 MUST 지킴 | import 참조로 적용 |
|
||||
| `FE-OC-005@1` | [[raw/branch-notes/feature-routing-navigation-guard-contract]] | route ID/path/params/access/loading/error owner는 route registry 하나여야 함 | import 참조로 적용 |
|
||||
| `FE-OC-007@1` | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | JSON envelope와 payload는 boundary에서 runtime schema를 MUST 통과 | import 참조로 적용 |
|
||||
| `FE-OC-011@1` | [[raw/branch-notes/feature-async-ui-state-contract]] | async surface는 initial-loading, success, empty, terminal-error를 MUST 표현 | import 참조로 적용 |
|
||||
| `FE-OC-012@1` | [[raw/branch-notes/feature-server-state-caching-contract]] | query key와 invalidation은 registry factory만 MUST 사용 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
### Sub-branches (세부 작업)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
### 오류 기록 (이 branch 작업 중 발생)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
### 강의 (이 작업을 위해 학습한 강의)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
### job-posting tie-ins (이 작업에서 파생된 글감)
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
- 없음 — scaffolding 단계
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크: 없음 (repository 미생성)
|
||||
- 리뷰 메모: 없음
|
||||
- 머지 결과 / 배포 환경: 없음 — 모든 항목 `planned`
|
||||
- **wiki 추출 대상**: 없음 — verified 항목 없음
|
||||
- **추출하지 않을 항목**: 전체 (`planned` / `needs-confirmation`)
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
---
|
||||
title: branch / feature-server-state-caching-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
branch: feature-server-state-caching-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract.md]
|
||||
tags: [branch, ca-skeleton, frontend, caching, react]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-010
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-010
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-SERVER-STATE-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-005]
|
||||
contract_packet: 1
|
||||
contract_packet_sha256: 325526901b9b8aa64182bc9aced8ee5ab2600340f803e35d3b012da576da8a4e
|
||||
imports: [FE-GATE-005@1, FE-GATE-007@1, FE-GATE-010@1, FE-OC-002@1, FE-OC-009@1, FE-OC-013@1, FE-OC-020@1, FE-OC-022@1, FE-OC-023@1]
|
||||
---
|
||||
|
||||
# branch: feature-server-state-caching-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — 단일 브랜치의 TODO·결정·진행 기록. `/branch-spec` 로 채운 `planned` 사전 명세 단계다. **frontend 코드는 아직 존재하지 않으므로 모든 구현 주장은 `planned`** 이며, 경로/이름은 hub blueprint 기준 예정치다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: QueryCachePort와 TanStack adapter의 invalidation·stale test가 통과한다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-SERVER-STATE-001@1` | application-owned QueryCachePort와 TanStack Query adapter를 사용하고 client store에 server state를 복제하지 않는다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-frontend-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 -->
|
||||
## 목표
|
||||
|
||||
이 브랜치는 hub 의 **server-state 캐싱 계약**을 구현 착수 가능한 수준으로 낮춘다. 프로젝트 전역 계약 `FE-OC-012`(query key 와 invalidation 은 registry factory 만 MUST 사용)의 single owner 이며, hub 결정 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D006 §4(server state policy = application-owned `QueryCachePort` 정의 + TanStack Query adapter 구현 + client store 비복제)을 실제 port·registry·adapter·failure 매핑으로 전개한다. 부수적으로 `FE-OC-011`(async surface state), `FE-OC-022`(registry governance — 본 브랜치가 `FE-REG-QUERY` owner), `FE-OC-024`(sample fixture)에 기여한다. 핵심 설계 판단은 **port ownership split** — application 이 `QueryCachePort` 를 소유(정의)하고 adapter 가 구현하며, presentation·application 은 TanStack Query client 를 직접 import 하지 않는다는 hub project decision 이다. 등급: `planned`.
|
||||
|
||||
- 이슈: 없음 (repository 미생성)
|
||||
- PR: 없음 (repository 미생성)
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- `QueryCachePort` 계약 정의 (application-owned) 와 TanStack Query adapter 구현 blueprint — `FE-OC-012`, FE-D006.
|
||||
- `FE-REG-QUERY` query key factory + invalidation registry (`src/contracts/query-keys.js`) 최소 스키마 — `FE-OC-012`, `FE-OC-022`, hub §5.7.
|
||||
- server-state 를 client store 에 복제하지 않는 non-duplication 규칙 — FE-D006.
|
||||
- query cache defaults (staleTime / gcTime / refetch-on-focus / persistence) 의 `planned` default 값과 예외 트리거 — hub §9.2.
|
||||
- `QUERY_CACHE_FAILURE` 정규화 + negative fixture 요구 — hub §8.2 / §8.5.
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외. 인접 계약은 다른 owner 브랜치가 소유한다.
|
||||
|
||||
- async surface 의 state 렌더링(initial-loading/success/empty/terminal-error, refreshing/stale-degraded 등 시각 표현) — owner [[raw/branch-notes/feature-async-ui-state-contract]] (`FE-OC-011`). 본 브랜치는 cache state → view-model 로 넘길 뿐 시각 계약은 정하지 않는다.
|
||||
- HTTP retry algorithm·timeout·abort·idempotency 내부 — owner [[raw/branch-notes/feature-api-client-response-envelope-contract]] (`FE-OC-006`, `FE-OC-009`). 본 브랜치는 API policy callback 을 *소비*만 한다.
|
||||
- frontend error kind/code/default UX 사전(`FE-REG-ERROR`) — owner [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] (`FE-OC-008`). 본 브랜치는 `QUERY_CACHE_FAILURE` 를 *어느 kind 로 매핑할지*만 선언한다.
|
||||
- 8-registry governance 의 schema validation·single-owner 검사 기구 — owner [[raw/branch-notes/feature-frontend-contract-registry-governance]] (`FE-OC-022`). 본 브랜치는 `FE-REG-QUERY` 한 registry 의 스키마만 채운다.
|
||||
- sample slice 자체와 removal smoke — owner [[raw/branch-notes/feature-sample-feature-slice-contract-fixture]] (`FE-OC-024`).
|
||||
- layer 의존 방향·composition root 주입 규약 자체 — owner [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] (`FE-OC-002`). 본 브랜치는 `QueryCachePort`/adapter 의 *shape* 과 "presentation·application 이 TanStack Query client 를 직접 import 하지 않는다"는 금지 대상만 공급하고, port 를 composition root 에 어떻게 등록·주입하는지의 convention 과 allowed/forbidden import 매트릭스는 그 owner 가 정한다.
|
||||
- restricted-import fixture 엔진(dependency-cruiser/ESLint rule 구성·실행·리포트) — owner [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]] (`FE-OC-020`, gate `FE-GATE-010`). 본 브랜치는 금지 import 목록을 선언할 뿐 lint 엔진을 소유하지 않는다.
|
||||
- cache persistence 를 opt-in 할 때의 storage key namespace·version·classification 규약 — owner [[raw/branch-notes/feature-frontend-storage-registry-contract]] (`FE-OC-013`). default 가 off 이므로 본 브랜치는 "opt-in 시 version partition 필요"라는 요구만 선언한다.
|
||||
- token/secret lifecycle — 외부 auth owner. cache key 에 token/PII 를 넣지 않는 규칙만 여기서 강제한다.
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/official-docs/tanstack-query-server-state-official]] | D1(server-state 전용 라이브러리 채택, `TSQ-C1`), D2(client store 비복제 — server state 는 구조적 staleness, `TSQ-C3`), D4(background refetch/staleness 위임, `TSQ-C5`·`TSQ-C4`). 초기 source. |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | FE-D006(port ownership split·non-duplication, §4.4) → D1·D2; `FE-OC-012` + §5.7 → D3(registry factory only); §9.2 query cache defaults → D5·D6; §8.2 `QUERY_CACHE_FAILURE` → D6; §5.7 version bump + §9.2 discard → D7. |
|
||||
|
||||
> TanStack Query overview 발췌는 **구체 default 값(staleTime/gcTime/retry) 과 retry semantics 를 증명하지 않는다**(그 문서의 Usage Boundaries 가 명시). 따라서 D5·D6 의 수치·정책은 official-doc 이 아니라 **hub §9.2 project default** 를 근거로 인용한다.
|
||||
|
||||
## TODO
|
||||
|
||||
각 항목 옆 증거 등급 표기. 현재 전부 `planned` (frontend repo 미생성).
|
||||
|
||||
- [ ] `QueryCachePort` interface 정의 (application-owned, read/write/invalidate) — 등급: `planned`
|
||||
- [ ] TanStack Query adapter 구현 + `bootstrap/composition-root.js` 주입 wiring — 등급: `planned`
|
||||
- [ ] `FE-REG-QUERY` query key factory (`src/contracts/query-keys.js`) + §5.7 최소 스키마(namespace/serialization/identity/invalidation/version/persistence) — 등급: `planned`
|
||||
- [ ] query cache defaults wiring (staleTime 30s sample read / gcTime 5m / refetch-on-focus / persistence off) — 등급: `planned`
|
||||
- [ ] `QUERY_CACHE_FAILURE` 정규화 매핑 + negative fixture(adapter throw / invalid cache result) — 등급: `planned`
|
||||
- [ ] deterministic cache tests: key 안정성, mutation→namespace invalidation 좁힘, stale/refetch, non-duplication architecture fixture — 등급: `planned`
|
||||
- [ ] 구현 repository·검증 evidence 식별 — 등급: `needs-confirmation`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
- hub §4.4 port matrix, §5.7 query key registry, §8.2/§8.5 failure, §9.2 cache defaults 를 근거로 자기 매핑 완료. web 조사 불필요(hub + archived TanStack Query 로 충분).
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> 각 결정의 상세 근거·선택 조건·위험은 아래 Decision Evidence Map 참조.
|
||||
|
||||
- 2026-07-18: server state 는 application-owned `QueryCachePort` 가 정의하고 TanStack Query adapter 가 구현 (D1) / 이유: server-state 전용 캐싱을 라이브러리에 위임하되 의존 방향을 뒤집지 않기 위함 / 대안: 수기 `useEffect`+fetch, 다른 server-state 라이브러리(SWR/RTK Query) / 근거: `raw/official-docs/tanstack-query-server-state-official.md#TSQ-C1`, hub FE-D006.
|
||||
- 2026-07-18: server state 를 client store(Redux/Zustand 등)에 복제하지 않음 (D2) / 이유: 두 소스가 갈라지면 staleness 를 스스로 만든다 / 대안: normalized entity store 복제 / 근거: `#TSQ-C3`, FE-D006.
|
||||
- 2026-07-18: query key·invalidation 은 `FE-REG-QUERY` factory 로만 생성, page 내 ad hoc array key 금지 (D3) / 근거: `FE-OC-012`, hub §5.7.
|
||||
- 2026-07-18: staleness·background refetch 는 라이브러리에 위임 (D4) / 근거: `#TSQ-C5`, `#TSQ-C4`, hub §9.2.
|
||||
- 2026-07-18: query cache defaults 는 hub §9.2 project default 를 채택 (D5, conditional-default).
|
||||
- 2026-07-18: retry 는 page-local 숫자 없이 API policy callback 에 위임하고, port 자체 실패는 `QUERY_CACHE_FAILURE` 로 정규화 (D6) / 근거: hub §9.2, §8.2.
|
||||
- 2026-07-18: version-incompatible cache data 는 reuse 하지 않고 discard, breaking 시 namespace version bump (D7) / 근거: hub §5.7, §9.2.
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | server state 는 application-owned `QueryCachePort` 가 정의하고 TanStack Query adapter 가 구현한다 (`FE-OC-012` / FE-D006) | 원격 소유 비동기 데이터를 fetch/cache/sync 할 때 이 결정. presentation·application 이 TanStack Query client 를 직접 import 하지 않는 것이 고정 invariant. offline-first normalized entity cache 가 필요해지면 FE-D006 revisit 로 대안 검토. **대안 선택 기준**: 라이브러리 자체(TanStack Query vs SWR vs RTK Query)는 hub 결정 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D006 에서 상류 고정되며 본 브랜치에서 재결정하지 않는다 — archived TanStack overview 는 대안 대비 우위를 증명하지 않으므로 라이브러리 우열 주장 금지 | `raw/official-docs/tanstack-query-server-state-official.md#TSQ-C1`; [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D006 §4 | `official-doc` + `project-decision` | port 실제 신호(로딩/에러/refetch)를 view-model 로 어떻게 노출할지는 async-ui 브랜치와 계약을 맞춰야 함 |
|
||||
| D2 | server state 를 client store(Redux/Zustand 등)에 복제하지 않는다 (non-duplication, FE-D006) | server-owned 데이터는 `QueryCachePort` 만이 소유. 순수 client-local UI state 는 별도 관리. offline-first normalized cache 요구 시 대안 | `raw/official-docs/tanstack-query-server-state-official.md#TSQ-C3`; [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D006 | `official-doc` + `project-decision` | 개발자가 편의로 server data 를 로컬 store 에 미러링할 수 있음 → architecture fixture 로 강제 필요 |
|
||||
| D3 | query key 와 invalidation 은 `FE-REG-QUERY` factory 로만 생성한다; page 내 ad hoc array key 금지 (`FE-OC-012`) | 모든 key 에 대해 항상 이 결정 (invariant, 분기 없음). 대안 없음 — factory 우회는 계약 위반 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-012` §5 | `project-decision` | factory 를 우회한 inline key 를 정적으로 잡아내는 lint rule 이 아직 미정 |
|
||||
| D4 | staleness·background refetch 를 라이브러리에 위임하고 수기 `useEffect`+fetch 를 쓰지 않는다 (`FE-OC-012` / FE-D006) | stale query 는 focus 시 refetch enabled. high-cost operation 은 owner 가 opt-out(§9.2 exception) | `raw/official-docs/tanstack-query-server-state-official.md#TSQ-C5`, `#TSQ-C4`; [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D006 §9 | `official-doc` + `project-decision` | overview 발췌는 `refetchOnWindowFocus` 등 구체 API·기본값을 증명하지 않음 → 코드에서 확인 필요 |
|
||||
| D5 | query cache defaults 는 hub §9.2 값 채택: staleTime 30s(sample read), gcTime 5m, refetch-on-focus enabled(stale), cache persistence off (`FE-OC-012`) | sample read 기본은 30s; operation owner measurement 가 나오면 조정. persistence 는 offline requirement + storage threat model 확정 시 opt-in | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §9 (query cache defaults) | `conditional-default` | 이 수치는 project-local 초기값 — 측정 근거 없음. TanStack Query overview 는 default 값을 증명하지 않으므로 수치를 official 로 인용 금지 |
|
||||
| D6 | retry 는 page-local 숫자 없이 API policy callback 에 위임하고, `QueryCachePort` 자체 실패는 `QUERY_CACHE_FAILURE` 로 정규화하며 자동 request retry 를 하지 않는다 (`FE-OC-012`) | query 는 API policy callback 사용; mutation retry 는 keyed idempotency contract 있을 때만(§9.2). port 실패는 uncached mode 선언 시만 fallback, 아니면 terminal | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §9 §8 (`QUERY_CACHE_FAILURE`) | `project-decision` | retry semantics 는 archived overview 로 증명 불가 → API client owner 계약(`FE-OC-009`) 확정에 의존 |
|
||||
| D7 | version-incompatible cache data 는 reuse 하지 않고 discard 하며, API/schema breaking change 시 namespace version bump 한다 (`FE-OC-012` → `FE-OC-022`/`FE-OC-023`) | release/config/API schema version 과 호환되면 reuse; 불일치면 discard. cache migration 을 선택하면 compatibility 브랜치가 fixture/rollback 소유(delegated) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5 §9 (version bump / discard) | `project-decision` | migration 을 도입하면 rollback fixture 소유권이 `FE-OC-023` owner [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] 로 이동(hub §9.2) — 도입 시 경계 재확인 필요 |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> `planned` blueprint. 경로는 hub §4.6 Planned directory blueprint + §5.1 registry owner map 기준 예정치이며 repo 생성 시 바뀔 수 있다. CLAUDE.md §15.5 3-rule 준수.
|
||||
|
||||
### 1. `QueryCachePort` 계약 (application-owned)
|
||||
|
||||
> **Trace**: D1 + FE-D006 §4.4 (port ownership matrix) + `FE-OC-012`. Supporting: `#TSQ-C1`.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 정확한 메서드명/시그니처(`readQuery`/`executeMutation`/`invalidateByNamespace` 등)는 hub 가 원칙(registry key + cache command → cache state/invalidation result)만 권고하고 구체 API 모양은 권고하지 않음 → 명명은 임의 trade-off(가독성 우선, 실제 use-case 와 맞춰 조정).
|
||||
|
||||
| 항목 | `planned` 명세 | 근거 |
|
||||
|---|---|---|
|
||||
| 정의 위치 | `src/application/ports/query-cache-port.js` (application 이 소유) | §4.6, §4.2 (application owns `QueryCachePort` policy) |
|
||||
| 입력/출력 | registry query key + cache command → cache state / invalidation result | §4.4 port matrix |
|
||||
| consumer | application query/mutation orchestration (use-case) | §4.4 |
|
||||
| 금지 | presentation·application 이 TanStack Query client 직접 import; application 이 adapter 이름 인지 | §4.3, §9.2 |
|
||||
| failure vocab | `QUERY_CACHE_FAILURE` | §4.4, §8.2 |
|
||||
|
||||
### 2. TanStack Query adapter + composition-root wiring
|
||||
|
||||
> **Trace**: D1 + FE-D006 §4.2 (`adapters/query-cache`) + §4.5 boot order. Supporting: `#TSQ-C1`.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: adapter 파일/클래스명(`query-cache/tanstack-query-cache-adapter.js` 등)은 hub 미권고 → 임의 명명(blueprint 디렉토리 규약에 맞춤).
|
||||
|
||||
| 항목 | `planned` 명세 | 근거 |
|
||||
|---|---|---|
|
||||
| 구현 위치 | `src/adapters/query-cache/` — application-owned port 구현, TanStack Query key/invalidation bridge | §4.2, §4.6 |
|
||||
| 조립 지점 | `bootstrap/composition-root.js` 가 adapter 생성 후 application facade 에 주입 (boot order 7단계: HTTP/storage/telemetry/query-cache adapter 생성) | §4.5, §9.2 |
|
||||
| 의존 방향 | adapter → application port + TanStack Query. adapter 는 use-case policy / page-local key 를 소유하지 않음 | §4.2, §4.3 |
|
||||
| 조립 규약 owner (본 § 밖) | composition root 의 등록·주입 convention 은 `FE-OC-002` owner [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]], 이를 강제하는 restricted-import fixture 는 `FE-OC-020` owner [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]] 소유. 본 §는 *주입 대상 adapter 의 shape* 만 명세한다 | §4.3, §4.5(boot order 7), §15.1 `FE-GATE-010` |
|
||||
|
||||
### 3. `FE-REG-QUERY` query key factory registry
|
||||
|
||||
> **Trace**: D3 + `FE-OC-012` + §5.7 (query key registry minimum schema). Supporting: `FE-OC-012`, hub §5.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: object key ordering canonicalize 알고리즘을 hub 는 "canonicalize" 원칙만 명시하고 구체 알고리즘 미권고 → stable JSON key-sort(재귀 정렬) 채택은 임의 trade-off(결정성 우선, 성능은 key 크기 작다는 가정).
|
||||
|
||||
| Rule | `planned` normative behavior | 근거 |
|
||||
|---|---|---|
|
||||
| 위치 | `src/contracts/query-keys.js`, single owner = 본 브랜치 | §5.1 |
|
||||
| factory 형태 | `queryKeys.<feature>.all()` / `.list(filters)` / `.detail(id)` | §5.7 |
|
||||
| namespace | feature prefix 를 첫 element 로 | §5.7 |
|
||||
| serialization | object key ordering canonicalize (동일 filters → 동일 key) | §5.7 |
|
||||
| identity | PII·token·raw URL 을 key 에 넣지 않음 | §5.7 |
|
||||
| invalidation | mutation outcome 과 mapping 된 factory 만 invalidate; 이유 없는 broad `invalidateQueries()` 금지 | §5.7, §9.2 |
|
||||
| version | API/schema breaking change 시 namespace version bump | §5.7 |
|
||||
| persistence | default disabled; opt-in 시 release/config version partition. storage key 의 namespace·version·classification 규약 자체는 `FE-OC-013` owner [[raw/branch-notes/feature-frontend-storage-registry-contract]] 소유 (조건부 의존, default off 이므로 미발동) | §5.7, §9.2 |
|
||||
|
||||
### 4. Query cache defaults wiring
|
||||
|
||||
> **Trace**: D5 + §9.2 (query cache defaults). Supporting: hub §9.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — 값이 전부 hub §9.2 인용이라는 것은 곧 **owner 가 §9.2** 라는 뜻이므로 표를 복제하지 않는다. (수치의 *적정성* 은 §Claims To Verify 에서 측정 대상.)
|
||||
|
||||
**query cache default 8행의 owner 는 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §9.2 다.** 요약 한 줄: query key 는 registry factory 만 사용하고, stale 30초 / gc 5분 / focus refetch 켬 / cache persistence 끔이 project default 이며, invalidation 은 mutation 결과의 registry namespace 로 한정한다(이유 없는 broad invalidate 금지).
|
||||
|
||||
### 5. `QUERY_CACHE_FAILURE` 정규화 (매핑 선언만)
|
||||
|
||||
> **Trace**: D6 + §8.2 failure matrix row + §8.5 negative fixture. Supporting: hub §8.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — kind/UX/telemetry 규칙은 §8.2 그대로. (kind→code→UX 사전의 *정의* 자체는 `FE-REG-ERROR` owner 소관 — R3 로 아래 §의존에 위임.)
|
||||
|
||||
| 항목 | `planned` 명세 | 근거 |
|
||||
|---|---|---|
|
||||
| trigger | `QueryCachePort` read/write/invalidate 가 throw 하거나 invalid cache result 반환 | §8.2 |
|
||||
| normalized kind | `QUERY_CACHE_FAILURE` | §8.2 |
|
||||
| auto retry | no automatic request retry | §8.2 |
|
||||
| fallback | operation 이 uncached mode 를 선언한 경우만 허용, 아니면 terminal. stale 표시를 위조하지 않음 | §8.2 |
|
||||
| telemetry | phase + query namespace만; raw key/data 금지 | §8.2 |
|
||||
| negative fixture | adapter throw 또는 invalid cache result → `QUERY_CACHE_FAILURE` | §8.5 |
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- `QueryCachePort` adapter throw / invalid cache result → `QUERY_CACHE_FAILURE`, 자동 request retry 없음, uncached-safe fallback 또는 terminal (§8.2). raw key/data 를 telemetry·UI 에 노출 금지.
|
||||
- 동일 filters 로 생성한 두 query key 가 serialization 비결정성으로 갈라지면 캐시 miss·중복 fetch 발생 → canonicalize 알고리즘으로 방지, deterministic key test 로 검증.
|
||||
- version-incompatible cache data 는 discard (§9.2) — reuse 시 stale/incompatible model 렌더 위험.
|
||||
- mutation 후 broad `invalidateQueries()` 남용 → 불필요한 refetch storm. 좁은 namespace invalidation 으로 제한 (§5.7/§9.2).
|
||||
- server state 를 client store 에 복제하면 두 소스가 갈라져 위조된 stale 상태 발생 (D2 위반).
|
||||
- **다른 계약 의존** (owner 브랜치 + 소유 contract 로 링크 — Decision ID 재진술은 hub register 참조):
|
||||
- [[raw/branch-notes/feature-api-client-response-envelope-contract]] — `FE-OC-006`/`FE-OC-009` (shared client + retry/timeout/idempotency policy). 본 브랜치의 D6 retry 위임은 이 계약을 consume; 그 policy 가 바뀌면 cache 의 retry 동작이 바뀐다.
|
||||
- [[raw/branch-notes/feature-async-ui-state-contract]] — `FE-OC-011` (async surface state matrix). cache state(refreshing/stale-degraded/mutation-pending)를 view-model 로 넘길 때 이 계약과 정합.
|
||||
- [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] — `FE-OC-008` (`FE-REG-ERROR` 정의). `QUERY_CACHE_FAILURE` 의 code/UX 사전은 이 owner 가 정의.
|
||||
- [[raw/branch-notes/feature-frontend-contract-registry-governance]] — `FE-OC-022` (registry single-owner/compatibility). `FE-REG-QUERY` 는 이 governance 하에 관리.
|
||||
- [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] — `FE-OC-002` (layer 의존 방향 + application-owned output port + 단일 composition root). **소유권 분할**: 본 브랜치는 `QueryCachePort` 계약과 query-cache adapter 의 shape 을 공급하고, 그 adapter 를 composition root 에서 *어떤 규약으로 생성·등록·주입하는지* 와 layer 별 allowed/forbidden import 매트릭스는 이 owner 가 소유한다. 이 계약이 흔들리면 §구현 가이드 2의 "조립 지점"과 D1 의 port ownership invariant 가 함께 바뀐다.
|
||||
- [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]] — `FE-OC-002`/`FE-OC-020` (restricted-import fixture 엔진, gate `FE-GATE-010`: "forbidden import fixtures including direct TanStack client import"). D1/D2 를 정적으로 강제하는 fixture 는 이 owner 가 구현·집행하며, 본 브랜치는 금지 대상(presentation·application → TanStack Query client 직접 import, server state 의 client store 미러링) 목록만 선언한다.
|
||||
- [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] — `FE-OC-023` (breaking change migration / version bump governance). D7 의 "version-incompatible cache data discard" 는 이 계약에 종속이며, cache migration 을 도입하는 순간 migration fixture 와 rollback 소유권이 이 owner 로 넘어간다 (hub §9.2 명시).
|
||||
- [[raw/branch-notes/feature-frontend-storage-registry-contract]] — `FE-OC-013` (storage key namespace/version/classification). cache persistence 를 opt-in 할 때만 활성화되는 조건부 의존. default off 이므로 현재는 미발동.
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| 동일 filters 에 대해 query key factory 가 항상 동일 key 를 생성 (canonicalization) | serialization/canonicalize 알고리즘이 아직 미구현·미선택 | deterministic cache key unit test (`FE-OC-012` minimum evidence "cache tests"; `FE-GATE-005` unit) | `needs-confirmation` |
|
||||
| mutation outcome 이 mapping 된 registry namespace 만 좁게 invalidate (broad invalidate 없음) | 구현 편의로 broad `invalidateQueries()` 를 쓰기 쉬움 | invalidation unit/integration test (`FE-GATE-007` MSW) | `needs-confirmation` |
|
||||
| staleTime 30s / refetch-on-focus 가 sample read 에 적절 | project-local 초기값, 측정 근거 없음 (overview 문서가 default 미증명) | operation owner measurement + cache/refetch 동작 test (§9.2 exception trigger) | `needs-confirmation` |
|
||||
| `QueryCachePort` adapter throw 가 `QUERY_CACHE_FAILURE` 로 정규화되고 request retry 를 유발하지 않음 | mapping·total-function 보장이 코드로 미검증 | negative fixture(adapter throw / invalid cache result → `QUERY_CACHE_FAILURE`, §8.5) | `needs-confirmation` |
|
||||
| presentation·application 이 TanStack Query client 를 직접 import 하지 않고 client store 에 server state 미복제 (D1/D2) | 의존 방향 위반은 런타임에 드러나지 않음 | dependency-cruiser/ESLint restricted-import architecture fixture (§4.3, gate `FE-GATE-010`). fixture 엔진 owner = `FE-OC-020` ([[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]]), layer 매트릭스 owner = `FE-OC-002` ([[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]]); 본 브랜치는 금지 대상만 제공 | `needs-confirmation` |
|
||||
| application 이 `QueryCachePort` 를 정의·소유하고 adapter 이름을 모름 (port ownership split) | port 정의 위치·주입 방향이 미구현 | dependency graph snapshot + composition-root review (§4.3/§4.5) | `planned` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
- 없음 — `planned` 사전 명세 단계.
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-GATE-005@1` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | unit 레벨이 실패하면 merge 를 MUST 차단하고 warning 으로 낮추면 안 됨 | import 참조로 적용 |
|
||||
| `FE-GATE-007@1` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | MSW 기반 integration 매트릭스가 미충족이면 merge 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-GATE-010@1` | [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]] | 금지된 layer import 가 통과하면 merge 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-OC-002@1` | [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] | `domain <- application <- presentation` 의존 방향과 application-owned output port를 MUST 지킴 | import 참조로 적용 |
|
||||
| `FE-OC-009@1` | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | retry는 safe/idempotent request에 한정하고 cap·jitter·`Retry-After`를 MUST 적용 | import 참조로 적용 |
|
||||
| `FE-OC-013@1` | [[raw/branch-notes/feature-frontend-storage-registry-contract]] | storage key는 namespace·version·classification을 MUST 가지며 token/secret 저장을 금지 | import 참조로 적용 |
|
||||
| `FE-OC-020@1` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | gate 종류별 책임·fixture·artifact를 분리하고 실패를 warning으로 낮추면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-022@1` | [[raw/branch-notes/feature-frontend-contract-registry-governance]] | 8개 registry는 single primary owner와 compatibility impact를 MUST 기록 | import 참조로 적용 |
|
||||
| `FE-OC-023@1` | [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] | API/config/storage/release schema의 breaking change는 migration 또는 version bump 없이 배포하면 안 됨 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
### Sub-branches (세부 작업)
|
||||
|
||||
- 없음 — 사전 명세 단계
|
||||
|
||||
### 오류 기록 (이 branch 작업 중 발생)
|
||||
|
||||
- 없음 — 사전 명세 단계
|
||||
|
||||
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
||||
|
||||
- 없음 — 사전 명세 단계
|
||||
|
||||
### 강의 (이 작업을 위해 학습한 강의)
|
||||
|
||||
- 없음 — 사전 명세 단계
|
||||
|
||||
### job-posting tie-ins (이 작업에서 파생된 글감)
|
||||
|
||||
- 없음 — 사전 명세 단계
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
- 없음 — 사전 명세 단계
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크: 없음 (repository 미생성)
|
||||
- 리뷰 메모: 없음
|
||||
- 머지 결과 / 배포 환경: 없음 — 코드 미착수, 전 항목 `planned`
|
||||
- **wiki 추출 대상**: 없음 — verified 항목 없음
|
||||
- **추출하지 않을 항목**: 전 결정·구현 명세 (`planned` / `needs-confirmation`)
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
---
|
||||
title: branch / feature-tailwind-design-token-styling-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
branch: feature-tailwind-design-token-styling-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract.md]
|
||||
tags: [branch, ca-skeleton, frontend, tailwind, react]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-018
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-018
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-STYLING-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-001]
|
||||
contract_packet: 1
|
||||
contract_packet_sha256: 8425a0ae2048fd82fe493415296631e7d440d81a443d4555e32bb00e37e64f3f
|
||||
imports: [FE-OC-011@1, FE-OC-018@1, FE-OC-019@1, FE-OC-020@1, FE-OC-021@1, FE-OC-024@1]
|
||||
delegates: [DELEG-FE-001@1]
|
||||
accepts_delegations: [DELEG-FE-004@1]
|
||||
|
||||
---
|
||||
|
||||
# branch: feature-tailwind-design-token-styling-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — 단일 브랜치의 TODO·결정·진행 기록. 머지/종료 후 verified 결과는 `/ingest`로 `wiki/projects/`에 추출한다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: theme token·arbitrary value policy·sample UI가 검증된다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-STYLING-001@1` | styling default는 Tailwind theme token과 component primitive다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-frontend-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 -->
|
||||
## 목표
|
||||
|
||||
`ca-skeleton-frontend`의 styling 결정 `FE-D005`("styling default는 Tailwind theme token + component primitive")를 되묻지 않아도 코드를 작성할 수 있는 implementation-ready styling contract로 내린다. 이 branch는 §20 Branch Decomposition에서 **Primary contract IDs `—`** 인 기여형 branch로, 자체 `FE-OC-*` owner는 아니지만 세 project-wide contract에 **contributes-to**로 참여한다: `FE-OC-011`(async surface의 시각 primitive), `FE-OC-019`(browser bundle에 untrusted class 주입 금지), `FE-OC-021`(token 제약이 CSS surface·CLS budget에 미치는 영향). 근거 결정은 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D005`(conditional-default)이며, 공식 근거는 [[raw/official-docs/tailwind-css-utility-first-official]]의 `TAILWIND-UTIL-C1`(제약된 primitive 집합), `TAILWIND-UTIL-C2`(마크업 내 single-purpose utility class), `TAILWIND-UTIL-C4`(predefined design system → magic number 방지·시각 일관성)이다. Measurable completion(§20)은 "theme tokens + arbitrary value policy + sample UI", Priority는 P3, Dependency는 [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]](FE-OC-003 toolchain 그릇이 선행). 현재 frontend repository가 존재하지 않으므로 본 노트의 모든 구현 주장은 `planned` 등급이다.
|
||||
|
||||
- 이슈:
|
||||
- PR:
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- **디자인 토큰 layer** — color/spacing/typography/radius 등 시각 상수를 theme token으로 정의(시각 상수 SSOT). Tailwind theme config 위치와 global stylesheet entry 확정 — 등급: `planned` (`FE-D005`, `TAILWIND-UTIL-C4`; token scale 값은 UNSUPPORTED_IMPL — archived doc가 scale 미정의)
|
||||
- **arbitrary value policy** — 마크업 magic-number 금지·token 강제, `[...]` arbitrary value는 bounded·reviewed escape hatch, 재발 값은 token 승격 — 등급: `planned` (`FE-D005`, `TAILWIND-UTIL-C4`)
|
||||
- **component primitive 어휘** — async surface state(initial-loading skeleton / empty / terminal-error)의 token-driven 시각 primitive 정의 — 등급: `planned` (`FE-OC-011` 기여, 근거 §9.1)
|
||||
- **정적 class 구성 규율** — class name은 compile-time/static, untrusted·runtime-interpolated class 문자열 및 styling 목적 `dangerouslySetInnerHTML` 금지 — 등급: `planned` (`FE-OC-019` 기여, 근거 §13.2)
|
||||
- **sample UI fixture** — token·primitive 사용을 시연하는 제거 가능한 fixture(product import 금지) — 등급: `planned` (`FE-OC-024` 협업, `FE-D025` 원칙)
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외한 것. 다른 owner branch 소유 관심사이며 본 §구현 가이드에 detail을 남기지 않는다 (CLAUDE.md §15.5 R3).
|
||||
|
||||
- **async surface state machine·required-state 정의·상태 전이** — owner [[raw/branch-notes/feature-async-ui-state-contract]] (`FE-OC-011`). 본 branch는 token-driven 시각 primitive 어휘만 소유하고 어떤 state가 required인지·전이는 위임.
|
||||
- **CSP/header/secret scan/prohibited-import 강제** — owner [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] (`FE-OC-019`). 본 branch는 class-construction 규율(정책)만 정의.
|
||||
- **CSS 번들 측정·threshold·web vitals 계측** — owner [[raw/branch-notes/feature-web-vitals-performance-budget-contract]] (`FE-OC-021`) + [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] (`FE-OC-018`). 본 branch는 token 제약으로 기여만.
|
||||
- **전체 sample feature slice 계약·removal smoke** — owner [[raw/branch-notes/feature-sample-feature-slice-contract-fixture]] (`FE-OC-024`). 본 branch는 styling 시연분만.
|
||||
- **axe/keyboard/contrast a11y baseline** — owner [[raw/branch-notes/feature-accessibility-baseline-contract]] (`FE-OC-020` 협업). 단 "color만으로 state 구분 금지"(§10.3)는 token 설계 시 준수.
|
||||
- **arbitrary-value·prohibited-import lint rule 구현(강제)** — owner [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]] + [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] (`FE-OC-020`). 본 branch는 정책만 정의, 강제 tooling은 위임.
|
||||
- **Vite/PostCSS toolchain·build baseline 자체** — owner [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] (`FE-OC-003`). 본 branch는 그 그릇에 Tailwind config를 plug할 뿐 build 파이프라인은 소유하지 않음.
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/official-docs/tailwind-css-utility-first-official]] | D1·D2·D3 — utility-first = 제약된 primitive 집합(`TAILWIND-UTIL-C1`), 마크업 내 single-purpose utility class 조합(`TAILWIND-UTIL-C2`), inline style과 달리 predefined design system에서 값 선택 → magic number 방지·시각 일관성(`TAILWIND-UTIL-C4`). styling default = Tailwind theme token + primitive 및 token 강제 policy의 공식 근거 |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | D1~D7 governing SSOT — Decision Register(`FE-D005`), contract index(`FE-OC-011`/`FE-OC-019`/`FE-OC-021`/`FE-OC-024`), async surface state model(§9.1), a11y baseline(§10.3), browser security boundary(§13.2), NFR matrix(§14.2), directory blueprint(§4.6), sample-fixture 원칙(`FE-D025`) |
|
||||
| [[raw/project-notes/ca-skeleton-operational-contract]] | D7의 상위 철학 precedent — backend skeleton의 "sample = 제거 가능 contract fixture" 원칙을 styling sample UI에 적용 (사실 인용이 아닌 rationale precedent) |
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] 디자인 토큰 layer 정의(color/spacing/typography/radius 등 category) + Tailwind theme config 위치·global stylesheet entry 확정 — 등급: `planned`
|
||||
- [ ] arbitrary value policy 문서화(token 강제 + `[...]` escape allowlist + recurring→promote 규칙) — 등급: `planned`
|
||||
- [ ] component primitive 어휘(skeleton/empty/terminal-error) token-driven 시각 명세 — 등급: `planned`
|
||||
- [ ] sample UI fixture(토큰·primitive 시연, removable, product import 금지) 설계 — 등급: `planned`
|
||||
- [ ] 정적 class 구성 규율 명세 + browser-security/lint owner 위임 링크 배선 — 등급: `planned`
|
||||
- [ ] `/docs/theme` 페이지를 raw-source로 발췌해 token scale 근거 보강 — 등급: `needs-confirmation`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
`/branch-spec` 채움 완료(2026-07-19). frontend repository 미생성 — 전 항목 `planned`. archived Tailwind doc(v4.3)은 utility-first 철학·magic-number 방지만 증명하고 token scale·purge·번들 크기는 미증명(C1/C4 boundary) → 해당 detail은 `UNSUPPORTED_IMPL_DECISION` 라벨 또는 owner 위임으로 분리했다. 실제 코드 착수 전까지 evidence 등급 상향 금지.
|
||||
|
||||
## 결정 사항
|
||||
|
||||
> 각 결정은 아래 Decision Evidence Map의 prose 미러이다. 근거는 Sources 또는 hub Decision Register를 가리킨다.
|
||||
|
||||
- 2026-07-18: styling default를 **Tailwind utility-first + theme-token layer + component primitive**로 채택 / 이유: 제약된 primitive 집합과 predefined design system이 magic number를 막고 시각 일관성을 확보 / 검토한 대안: CSS Modules·CSS-in-JS·plain CSS / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D005`, [[raw/official-docs/tailwind-css-utility-first-official]] `TAILWIND-UTIL-C1`·`TAILWIND-UTIL-C2`·`TAILWIND-UTIL-C4`. (conditional-default)
|
||||
- 2026-07-18: **design token layer를 시각 상수 SSOT**로 두고 raw 값 하드코딩을 대체 / 이유: inline style의 magic number를 predefined design system 값 선택으로 대체(C4) / 검토한 대안: 컴포넌트별 임의 값·글로벌 CSS 변수만 사용 / 근거: [[raw/official-docs/tailwind-css-utility-first-official]] `TAILWIND-UTIL-C4`, [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D005`. (token scale 값은 UNSUPPORTED_IMPL)
|
||||
- 2026-07-18: **arbitrary value policy** — token 강제, `[...]`는 bounded escape hatch, 재발 값은 token 승격 / 이유: escape 상시화 시 magic number가 재유입되어 C4 이점이 무력화 / 검토한 대안: 무제한 arbitrary value 허용 / 근거: [[raw/official-docs/tailwind-css-utility-first-official]] `TAILWIND-UTIL-C4`. (강제 tooling은 `FE-OC-020` 위임)
|
||||
- 2026-07-18: async surface state의 **token-driven 시각 primitive 어휘**를 본 branch가 소유하되 state machine은 위임 / 이유: 시각 표현과 상태 소유의 경계 분리 / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-011`, §9.1. (delegation boundary)
|
||||
- 2026-07-18: **정적 class 구성 규율**(no runtime/untrusted class string, no styling `dangerouslySetInnerHTML`) / 이유: browser bundle은 public artifact이며 untrusted 주입은 default 금지(§13.2) / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-019`, §13.2. (강제는 browser-security owner 위임)
|
||||
- 2026-07-18: **token 제약이 perf budget에 기여**(tokenized sizing→CLS 안정, bounded 어휘→CSS surface 억제) / 이유: 시각 상수 재사용이 layout·번들 예측성을 높임 / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-021`, §14.2. (측정·purge·threshold는 owner 위임)
|
||||
- 2026-07-18: **sample UI를 제거 가능한 fixture**로 제공(product import 금지) / 이유: backend skeleton의 sample-fixture 원칙 적용 / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-024`·`FE-D025`. (removal smoke는 sample-slice owner 위임)
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
> 각 결정과 raw source Claim ID의 연결. `Decision ID`(D1~D7)는 본 노트 안에서 안정적으로 유지한다.
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | styling default = Tailwind utility-first + theme-token layer + component primitive (`FE-D005`; 기여 `FE-OC-011`·`FE-OC-019`·`FE-OC-021`) | 정적 utility 컴파일 + build-time theme token이 디자인 요구를 충족하는 동안 → Tailwind theme token. runtime theming(사용자 런타임 테마 전환) 또는 product design system이 다른 compiler를 요구 → `FE-D005` revisit(다른 styling 엔진 재평가) | [[raw/official-docs/tailwind-css-utility-first-official]] `TAILWIND-UTIL-C1`, `TAILWIND-UTIL-C2`, `TAILWIND-UTIL-C4`; [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D005` | `official-doc` + `conditional-default` (project-decision) | archived doc는 성능/번들 이점을 미증명(C1 boundary); Tailwind 채택이 이 프로젝트 생산성·유지보수를 개선하는지 실측 필요 |
|
||||
| D2 | design token layer(color/spacing/typography/radius…)를 시각 상수 SSOT로 정의, magic number 대체 (`FE-D005` / `TAILWIND-UTIL-C4`) | 값이 팀 공유 시각 상수인 동안 → theme token 등록. 일회성·컴포넌트 로컬 값이면 → 컴포넌트 스코프 유지(token 오염 방지). token은 hub §5 8-registry 밖 신규 domain이므로 governance는 registry-governance와 협의 | [[raw/official-docs/tailwind-css-utility-first-official]] `TAILWIND-UTIL-C4`; [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D005`, §5 | `official-doc`(원칙) + `project-decision`(신규 제안); token scale 값 = UNSUPPORTED_IMPL | archived doc가 spacing/color scale 구조 미정의(C4 boundary) → `/docs/theme` 별도 raw 필요; token registry가 hub §5 8-registry에 부재(신규 제안) |
|
||||
| D3 | arbitrary value policy — 마크업 magic-number 금지·token 강제, `[...]`는 bounded·reviewed escape hatch, 재발 값 token 승격 (`FE-D005` / `TAILWIND-UTIL-C4`) | 디자인 값이 token으로 표현 가능한 동안 → token. token 부재 escape가 필요하면 → allowlist 등록 후 `[...]`; 동일 arbitrary value 2회+ 재발 → token 승격(escape 상시화 금지) | [[raw/official-docs/tailwind-css-utility-first-official]] `TAILWIND-UTIL-C4`; [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D005` | `official-doc`(원칙) + `project-decision`(정책); lint 강제 메커니즘 = UNSUPPORTED_IMPL/위임 | "allowlist 외 arbitrary value 금지" 강제 tooling 미확정(`FE-OC-020` 위임); escape 남용 감지 방법 미검증 |
|
||||
| D4 | async surface state(skeleton/empty/terminal-error)의 token-driven 시각 primitive 어휘를 본 branch가 소유; state machine·required-state는 위임 (기여 `FE-OC-011`, §9.1) | 시각 표현이면 → styling branch primitive. 어떤 state가 required인지·상태 전이는 → async-ui-state owner(`FE-OC-011`). "loading boolean 하나로 empty/error/refreshing 병합 금지"(§9.1)는 state owner 계약 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-011`, §9.1; [[raw/official-docs/tailwind-css-utility-first-official]] `TAILWIND-UTIL-C1` | `project-decision` (boundary/delegation) | primitive 어휘가 §9.1 4-state matrix를 실제로 커버하는지 component test 필요; "color만으로 state 구분 금지"(§10.3) 준수 여부는 a11y 협업 |
|
||||
| D5 | class name은 compile-time/static; untrusted·runtime-interpolated class 문자열 금지; styling 목적 `dangerouslySetInnerHTML` 금지 (기여 `FE-OC-019`, §13.2) | 정적 class로 표현 가능한 동안 → static. 진짜 dynamic이 필요하면 → tokenized variant의 bounded allowlist를 통해 매핑(user 입력 문자열 concat 금지). CSP/scan/prohibited-import 강제는 browser-security owner 위임 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-019`, §13.2 | `project-decision` (boundary) | dynamic class 요구가 실제 발생 시 allowlist 설계 미검증; 강제는 browser-security/lint owner에 의존 |
|
||||
| D6 | token 제약이 perf budget에 기여 — tokenized sizing→layout 안정(CLS `FE-NFR-004` ≤0.10), bounded class 어휘→CSS surface 억제; 측정·purge·threshold는 위임 (기여 `FE-OC-021`, §14.2) | token 재사용으로 CSS surface가 bounded인 동안 → 기여 유지. 번들/CLS threshold 초과가 측정되면 → web-vitals/build owner가 budget 판정·최적화(본 branch는 token 정책 조정으로 협조) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-021`, §14.2 | `project-decision` (boundary); CSS purge/content 메커니즘·번들 수치 = 위임(archived doc 미증명) | "utility 재사용→CSS 축소"·"tokenized sizing→CLS 개선"은 archived doc 미증명·프로젝트 미실측 → lab/bundle report로 검증 필요 |
|
||||
| D7 | sample UI(token·primitive 시연)는 `src/sample/` 하위 제거 가능 fixture이며 product import 금지 (협업 `FE-OC-024`, `FE-D025`) | styling 시연 목적이면 → sample fixture(제거 가능). 실제 제품 화면이 되면 → 제품 feature branch 소유(본 branch out of scope). 전체 slice 계약·removal smoke는 sample-slice owner | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-OC-024`, `FE-D025`, §4.6; [[raw/project-notes/ca-skeleton-operational-contract]] | `project-decision` (boundary) + CA precedent | sample removal 시 product 무영향 검증은 sample-slice owner smoke에 의존 |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> `planned` blueprint. 경로는 hub §4.6 planned directory blueprint에서 도출되므로 grounded이지만, frontend 코드가 없으므로 전 구간 `planned`. 각 sub-section은 CLAUDE.md §15.5 3-rule(R1 Trace·R2 UNSUPPORTED_IMPL_DECISION·R3 no OUT_OF_BRANCH_SCOPE)을 따른다.
|
||||
|
||||
### 1. 디자인 토큰 layer (theme token SSOT)
|
||||
|
||||
> **Trace**: D1(`FE-D005`) + D2(`FE-D005` / `TAILWIND-UTIL-C4`) → 기여 `FE-OC-021`. planned 경로 `src/presentation/styles/`(hub §4.6 presentation dir) 하위 theme config + global stylesheet entry.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: theme config 파일 위치·메커니즘(Tailwind v4 CSS-first `@theme`(예: `src/presentation/styles/theme.css`) vs v3 `tailwind.config.js`) — hub 미명시, archived doc(v4.3)은 config 메커니즘 미서술. trade-off: v4.3 채택이므로 CSS-first `@theme` 우선, 착수 시 `/docs/theme` 발췌로 확정.
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 각 token category의 정확한 scale 값(color palette·spacing step·type scale) — archived `TAILWIND-UTIL-C4`가 scale 구조 미정의. trade-off: 값은 임의 선택 불가 → `/docs/theme` 발췌 + 디자인 요구로 확정, 그 전까지 값 미기재.
|
||||
|
||||
| Token category | planned 소스 | 근거 | 소유 경계 |
|
||||
|---|---|---|---|
|
||||
| color | theme token | D2 (`TAILWIND-UTIL-C4`) | this branch (값 UNSUPPORTED_IMPL) |
|
||||
| spacing | theme token | D2 (`TAILWIND-UTIL-C4`) | this branch (값 UNSUPPORTED_IMPL) |
|
||||
| typography(font family/size/weight/line-height) | theme token | D2 (`TAILWIND-UTIL-C4`) | this branch (값 UNSUPPORTED_IMPL) |
|
||||
| radius/shadow/z-index/breakpoint | theme token | D2 (`FE-D005`) | this branch (값 UNSUPPORTED_IMPL) |
|
||||
| token 값 자체 | `/docs/theme` 발췌 후 | needs raw source | this branch (근거 보강 대기) |
|
||||
|
||||
### 2. arbitrary value policy
|
||||
|
||||
> **Trace**: D3(`FE-D005` / `TAILWIND-UTIL-C4`). 정책은 본 branch 소유, 강제 tooling은 `FE-OC-020` owner 위임(R3).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: allowlist 저장 위치·형식 + lint rule 이름 — hub 미명시. trade-off: 정책 정의는 본 branch, 강제 rule은 [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]] / [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] (`FE-OC-020`)로 위임.
|
||||
|
||||
| 규칙 | planned 내용 | 근거 |
|
||||
|---|---|---|
|
||||
| 기본 | 모든 spacing/color/typography/radius 값은 theme token utility 사용 | D2·D3 (`TAILWIND-UTIL-C4`) |
|
||||
| escape 조건 | `[value]` arbitrary value는 (a) token 부재 + (b) 리뷰 승인 + (c) allowlist 등록 시에만 | D3 |
|
||||
| 승격 | 동일 arbitrary value 2회+ 등장 → theme token 승격 | D3 (magic number 재유입 방지, `TAILWIND-UTIL-C4`) |
|
||||
| 금지 | 무제한 arbitrary value(allowlist 밖 `[...]`) — predefined design system 무력화 | D3 (`TAILWIND-UTIL-C4`) |
|
||||
|
||||
### 3. component primitive 어휘 (async surface 시각) — 기여 FE-OC-011
|
||||
|
||||
> **Trace**: D4 → 기여 `FE-OC-011`, §9.1. planned 경로 `src/presentation/components/`(hub §4.6). R3: state machine·required-state 정의는 async-ui-state owner 위임.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: primitive 컴포넌트 명명(예: `<Skeleton>`/`<EmptyState>`/`<ErrorSurface>`) — hub 미명시. trade-off: 명명은 임의 → 착수 시 확정하되 §9.1 required state와 1:1 매핑을 유지.
|
||||
|
||||
| §9.1 required state | token-driven 시각 primitive | UI 요구(§9.1) |
|
||||
|---|---|---|
|
||||
| `initial-loading` | skeleton primitive | 안정적 skeleton, focus theft 금지; 고정 치수 token으로 layout 안정 |
|
||||
| `empty` | empty-state primitive | empty reason + primary action slot |
|
||||
| `terminal-error` | error-surface primitive | safe message + registry action slot |
|
||||
| `refreshing`/`stale-degraded`/`mutation-*` | non-blocking 시각 hint(subtle indicator/label) | 기존 content 유지; required 여부·의미는 state owner |
|
||||
|
||||
- 어떤 state가 required인지·상태 전이는 owner [[raw/branch-notes/feature-async-ui-state-contract]] (`FE-OC-011`). "loading boolean 하나로 병합 금지"(§9.1)는 state owner 계약이며 본 §에 detail 미기재(R3).
|
||||
- "color만으로 state 구분 금지"(§10.3) 준수 → primitive는 아이콘/텍스트를 색과 병행. a11y 판정은 accessibility owner 협업.
|
||||
|
||||
### 4. 정적 class 구성 규율 — 기여 FE-OC-019
|
||||
|
||||
> **Trace**: D5 → 기여 `FE-OC-019`, §13.2. R3: CSP/scan/prohibited-import 강제는 browser-security owner 위임.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — 규율(정적 class·no runtime string·no styling `dangerouslySetInnerHTML`)은 §13.2에 grounded.
|
||||
|
||||
- class name은 compile-time에 결정한다; user data로 class 문자열을 concat하지 않는다.
|
||||
- dynamic이 불가피하면 tokenized variant map(정적 키 → 정적 class)을 경유한다.
|
||||
- styling 목적의 `dangerouslySetInnerHTML`/untrusted inline style 주입을 금지한다(§13.2).
|
||||
- 위 규율의 정적 강제(prohibited-import lint, secret/injection scan)는 owner [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] (`FE-OC-019`)가 소유하며 본 §에 강제 detail 미기재(R3).
|
||||
|
||||
### 5. sample UI fixture — 협업 FE-OC-024
|
||||
|
||||
> **Trace**: D7 → 협업 `FE-OC-024`, `FE-D025`, §4.6. planned 경로 `src/sample/contract-fixture/`(hub §4.6).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: sample UI 화면 구성·컴포넌트 목록 — hub 미명시(styling 시연 재량). trade-off: 최소 시연(token + 3개 async primitive)만 우선, 전체 slice 구성은 sample-slice owner.
|
||||
|
||||
- sample UI는 theme token·arbitrary value policy·async primitive를 한 화면에서 시연한다.
|
||||
- removable: product 코드가 sample을 import하지 않는다(`FE-D025`).
|
||||
- 전체 contract slice·sample removal smoke는 owner [[raw/branch-notes/feature-sample-feature-slice-contract-fixture]] (`FE-OC-024`)가 소유하며 본 §에 slice detail 미기재(R3).
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- **magic number 재유입**: token 부재 값이 allowlist 없이 하드코딩/arbitrary로 등장 → policy 위반. 기대 동작: lint FAIL(강제는 `FE-OC-020` owner), 리뷰 차단.
|
||||
- **arbitrary value 남용**: 동일 값 반복 escape인데 token 승격 누락 → magic number 상시화. 기대 동작: 승격 규칙(D3)으로 감지·정리.
|
||||
- **dynamic class 문자열**: user input 기반 class 생성 → browser security 위반(§13.2). 기대 동작: 정적 variant map으로 대체, prohibited-import lint FAIL(강제는 `FE-OC-019` owner).
|
||||
- **CLS 회귀**: skeleton/primitive 치수 불안정 → layout shift(`FE-NFR-004` ≤0.10 초과). 기대 동작: 고정 치수 token, web-vitals owner가 lab에서 측정.
|
||||
- **CSS 번들 팽창**: token 미재사용·arbitrary 남발 → CSS surface 증가(`FE-NFR-001` 압박). 기대 동작: bounded 어휘 정책, build/web-vitals owner가 측정.
|
||||
- **color-only state**: state를 색만으로 표현 → a11y 위반(§10.3). 기대 동작: 아이콘/텍스트 병행.
|
||||
- **다른 계약 의존**:
|
||||
- **상류 의존**: [[raw/branch-notes/feature-frontend-project-bootstrap-toolchain-contract]] (`FE-OC-003`) — Vite/toolchain build 그릇에 Tailwind config를 plug. 이 build baseline이 바뀌면 styling 컴파일에 영향.
|
||||
- **기여(contributes-to)**: [[raw/branch-notes/feature-async-ui-state-contract]] (`FE-OC-011`)가 본 primitive 어휘를 consume; [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] (`FE-OC-019`)가 정적 class 규율을 강제; [[raw/branch-notes/feature-web-vitals-performance-budget-contract]] (`FE-OC-021`) + [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] (`FE-OC-018`)가 CSS/CLS budget을 측정; [[raw/branch-notes/feature-sample-feature-slice-contract-fixture]] (`FE-OC-024`)가 sample removal smoke를 소유; [[raw/branch-notes/feature-accessibility-baseline-contract]] (`FE-OC-020`)가 color/contrast a11y를 판정.
|
||||
- **강제 tooling 의존**: arbitrary-value·prohibited-import lint는 [[raw/branch-notes/feature-frontend-architecture-enforcement-lint-contract]] / [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] (`FE-OC-020`)가 배선.
|
||||
- **CA 철학 precedent**: [[raw/project-notes/ca-skeleton-operational-contract]] — "sample = 제거 가능 fixture" 원칙(사실 의존이 아닌 설계 precedent).
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| theme token layer가 시각 상수를 실제로 SSOT화(모든 시각 값 token화, magic number 제거) | 구현·lint 미존재 | arbitrary-value lint fixture(위반 시 FAIL) + token 커버리지 grep (owner `FE-OC-020`) | `needs-confirmation` |
|
||||
| arbitrary value escape가 allowlist로 bounded 유지 | 강제 tooling 미확정 | allowlist 밖 `[...]` 사용 시 lint FAIL negative fixture | `needs-confirmation` |
|
||||
| primitive 어휘가 §9.1 4-state를 커버하고 "loading boolean 병합 금지"를 준수 | component 미존재 | async-ui-state component state matrix test와 cross-ref (owner `FE-OC-011`) | `needs-confirmation` |
|
||||
| 정적 class 규율이 runtime/untrusted class 및 styling `dangerouslySetInnerHTML`를 차단 | 강제 미구현 | prohibited-import/dynamic-class negative fixture (owner `FE-OC-019`) | `needs-confirmation` |
|
||||
| tokenized sizing이 CLS ≤0.10, bounded 어휘로 CSS surface가 번들 budget 내 | archived doc 미증명·프로젝트 미실측 | lab CLS report(`FE-NFR-004`) + CSS bundle report (owner `FE-OC-021`/`FE-OC-018`) | `needs-confirmation` |
|
||||
| Tailwind v4.3 theme token scale이 디자인 요구를 충족 | `/docs/theme` 미발췌 | `/docs/theme` raw-source 발췌 후 token 정의 대조 | `needs-confirmation` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
> `/coverage`가 채우는 생성물이며 손으로 유지하지 않는다 — controller phase에서 생성.
|
||||
|
||||
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| TODO — `/coverage` 실행 전 | missing | (없음) | 미평가 | TODO |
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
없음 — `/branch-spec` 채움 단계
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: received-delegations:start -->
|
||||
### 수신한 위임
|
||||
|
||||
| Delegation Ref | From | Concern | Status |
|
||||
|---|---|---|---|
|
||||
| `DELEG-FE-004@1` | [[raw/branch-notes/feature-accessibility-baseline-contract]] | `fe.deleg.color-contrast` | accepted |
|
||||
<!-- GENERATED: received-delegations:end -->
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-OC-011@1` | [[raw/branch-notes/feature-async-ui-state-contract]] | async surface는 initial-loading, success, empty, terminal-error를 MUST 표현 | import 참조로 적용 |
|
||||
| `FE-OC-018@1` | [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] | frozen lockfile, dependency review, secret scan, SBOM 또는 dependency inventory를 release gate에 MUST 포함 | import 참조로 적용 |
|
||||
| `FE-OC-019@1` | [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] | browser bundle에 secret을 넣지 않고 untrusted HTML injection을 기본 금지 | import 참조로 적용 |
|
||||
| `FE-OC-020@1` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | gate 종류별 책임·fixture·artifact를 분리하고 실패를 warning으로 낮추면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-021@1` | [[raw/branch-notes/feature-web-vitals-performance-budget-contract]] | NFR은 device/network/cache/build context와 함께 MUST 측정 | import 참조로 적용 |
|
||||
| `FE-OC-024@1` | [[raw/branch-notes/feature-sample-feature-slice-contract-fixture]] | sample은 contract fixture이며 production feature가 의존하면 안 됨 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
### Sub-branches (세부 작업)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 오류 기록 (이 branch 작업 중 발생)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### 강의 (이 작업을 위해 학습한 강의)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
### job-posting tie-ins (이 작업에서 파생된 글감)
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
없음 — scaffolding 단계
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크:
|
||||
- 리뷰 메모:
|
||||
- 머지 결과 / 배포 환경:
|
||||
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
|
||||
- `actually-implemented` 항목:
|
||||
- `locally-verified` 항목:
|
||||
- `prod-verified` 항목:
|
||||
- **추출하지 않을 항목** (planned / documented-only / abandoned): 현재 전체 (frontend repository 미생성)
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
---
|
||||
title: branch / feature-web-vitals-performance-budget-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
branch: feature-web-vitals-performance-budget-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
||||
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract]
|
||||
tags: [branch, ca-skeleton, frontend, observability, react, histogram-quantile]
|
||||
created: 2026-07-18
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-025
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-frontend-operational-contract
|
||||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-025
|
||||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-BUILD-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-TEST-STACK-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-020, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-016]
|
||||
contract_packet: 1
|
||||
contract_packet_sha256: 4579fd193d1c3d1a19d54a732084315a1ae27a23bfdecf664e511eef29e83cbe
|
||||
imports: [ART-FE-002@1, FE-GATE-012@1, FE-OC-014@1, FE-OC-020@1, FE-OC-026@1]
|
||||
---
|
||||
|
||||
# branch: feature-web-vitals-performance-budget-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — TODO·결정·진행 기록. 구현 결과는 검증 뒤 `/ingest`로만 추출한다.
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
[[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: context metadata와 lab·bundle·28-day field report가 생성된다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-BUILD-001@1` | Vite client-only SPA를 build baseline으로 사용한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-TEST-STACK-001@1` | test stack default는 Vitest, RTL, MSW, Playwright, axe다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-frontend-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 -->
|
||||
## 목표
|
||||
|
||||
이 branch 는 project-wide 계약 `FE-OC-021` ("NFR 은 device/network/cache/build context 와 함께 MUST 측정, 최소 증거 = machine-readable report") 를 *구현 착수 가능한 명세* 로 내린다. 구체적으로 (1) 측정 context 모델([[raw/project-notes/ca-skeleton-frontend-operational-contract]] §14.1 의 `FE-NFR-C01`~`FE-NFR-C04`), (2) initial target matrix(bundle `FE-NFR-001`/`FE-NFR-002`, lab `FE-NFR-003`~`FE-NFR-005`, field `FE-NFR-013`~`FE-NFR-015`), (3) 세 개의 machine-readable evidence report(`bundle.json` / `lab.json` / `field-web-vitals.json`) 를 정의한다. 이 branch 는 세 performance gate(`FE-GATE-012` bundle, `FE-GATE-026` lab, `FE-GATE-018` field)의 pass-condition 을 정의해 `FE-OC-020`(test taxonomy) 에 기여하고, release-time bundle/lab gate 를 통해 `FE-OC-016`(release readiness) 에 기여한다. **현재 frontend 코드는 존재하지 않으므로 모든 구현 항목은 `planned`** 이다.
|
||||
|
||||
- 이슈: 없음 (스캐폴딩 단계)
|
||||
- PR: 없음
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위
|
||||
|
||||
- `FE-OC-021` measurement-context 계약: 모든 NFR 수치는 4-context(device/runtime · network/cache · route/data · build) metadata 와 함께만 evidence 로 인정 (§14.1).
|
||||
- Initial target matrix 정의 + revisit 절차: `FE-NFR-001`/`FE-NFR-002`(bundle gzip budget), `FE-NFR-003`~`FE-NFR-005`(lab), `FE-NFR-013`~`FE-NFR-015`(field p75).
|
||||
- 세 machine-readable report schema: bundle(`FE-GATE-012`), lab(`FE-GATE-026`), 28-day field Web Vitals(`FE-GATE-018`).
|
||||
- lab ≠ field 불변식 + negative fixture(context metadata 누락 / named threshold 초과).
|
||||
- 세 performance gate 의 pass-condition + required-context 정의.
|
||||
|
||||
### 제외 범위
|
||||
|
||||
> 의도적으로 제외한 것. 각 항목은 owner branch 에 위임한다 (근거 범위 밖 detail 을 여기서 정하지 않음 — CLAUDE.md §15.5 R3).
|
||||
|
||||
- 실제 production RUM 수집·telemetry sink·consent/privacy 정책 — [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] (`FE-OC-014`) + open question `FE-Q-008` 소유.
|
||||
- bundle 을 생성하는 build baseline(Vite production build, code splitting)·supply-chain build gate — [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] (`FE-OC-018`) 소유.
|
||||
- CI gate wiring · blocking scope · artifact retention — [[raw/branch-notes/feature-frontend-ci-quality-gates-contract]] + [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] (`FE-OC-020`) 소유. 본 branch 는 pass-condition 만 제공.
|
||||
- API total timeout(`FE-NFR-007`)·retry count(`FE-NFR-008`) 메커니즘 — [[raw/branch-notes/feature-api-client-response-envelope-contract]] (`FE-OC-006`, `FE-OC-009`) 소유. 본 branch 는 그 NFR *값* 을 target matrix 로 참조만 한다.
|
||||
- browser support matrix(`FE-Q-007`), 실제 CI runner CPU·throttling profile 확정(repo/CI 생성 전 불가), browser vendor-specific tuning.
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| [[raw/official-docs/vite-build-tool-official]] `VITE-C2` | "production build 는 Rolldown 으로 코드를 번들링해 최적화된 정적 자산을 산출" — bundle report 가 측정하는 build artifact 의 공식 근거 (D2 bundle, D6 gate). |
|
||||
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §14(FE-NFR-C01~C04·FE-NFR-001~015) · §14.3(command→artifact) · §15.1(FE-GATE-012/018/026) · §15.2(negative fixture) · FE-OC-021 | measurement context 모델·initial target·three-report split·gate pass-condition 의 project SSOT (D1·D2·D5·D6). |
|
||||
| web.dev Core Web Vitals (researched 2026-07-19, `https://web.dev/articles/vitals`) | LCP/INP/CLS 정의 + good threshold(2.5s / 200ms / 0.1) + 75th-percentile + lab≠field 구분의 공식 표준 근거 (D3·D4). |
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] measurement-context schema(device/runtime · network/cache · route/data · build) 정의 + 각 report 가 embed 할 metadata 필드 명세 — 등급: `planned`
|
||||
- [ ] bundle report schema (`artifacts/performance/bundle.json`: initial JS gzip, lazy chunk gzip vs `FE-NFR-001`/`FE-NFR-002`) — 등급: `planned`
|
||||
- [ ] lab report schema (`artifacts/performance/lab.json`: LCP/CLS/interaction-latency + context metadata vs `FE-NFR-003`~`FE-NFR-005`) — 등급: `planned`
|
||||
- [ ] 28-day field report schema (`artifacts/performance/field-web-vitals.json`: p75 LCP/CLS/INP + consent·route-ID·release-ID·eligible-sample metadata vs `FE-NFR-013`~`FE-NFR-015`) — 등급: `planned`
|
||||
- [ ] 세 performance gate pass-condition + negative fixture(context 누락 / threshold 초과) 명세 — 등급: `planned`
|
||||
- [ ] deferred minimum eligible sample threshold 해소 절차 문서화 (telemetry baseline 확보 이후) — 등급: `planned`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
- vitals threshold(LCP 2.5s / CLS 0.10 / INP 200ms, p75)는 Core Web Vitals "good" 값(web.dev). bundle budget(200/120 KiB)은 project-local initial 값이며 `FE-RISK-010`(threshold 가 실제 device UX 와 무관할 위험)로 첫 측정 후 revisit 대상.
|
||||
- 28-day window 는 hub/CrUX convention 이며 web.dev 문서는 28일을 *명시하지 않음* → 28-day 는 project decision 으로 grounding.
|
||||
- CI runner CPU·throttling profile 미확정(§14.1) → 값을 지금 고정하지 않고 command 실행 시 report metadata 에 기록.
|
||||
|
||||
## 결정 사항
|
||||
|
||||
- 2026-07-19: measurement-context 계약 — 모든 NFR 수치는 4-context 와 함께만 evidence / 이유: context 없는 숫자는 재현·비교 불가 / 대안: 단일 숫자만 기록(reject) / 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §14.1 + FE-OC-021.
|
||||
- 2026-07-19: three machine-readable report split(bundle / lab / 28-day field) / 이유: build-repro · synthetic lab · RUM 은 서로 다른 context / 대안: 단일 통합 report / 근거: hub §14.3 + §20 measurable completion.
|
||||
- 2026-07-19: lab ≠ field 불변식 — lab 결과를 production percentile 로 표현 금지 / 근거: hub §14.2 note + web.dev(field vs lab).
|
||||
- 2026-07-19: initial target = Core Web Vitals good threshold(LCP 2.5s / CLS 0.10 / INP 200ms, p75) + project bundle budget(200/120 KiB) / 대안: device-class 별 커스텀 threshold / 조건: 첫 실측·field data 확보 후 revisit(`FE-RISK-010`) / 근거: web.dev + hub §14.2.
|
||||
- 2026-07-19: 28-day field window + eligibility metadata; minimum eligible sample threshold 는 deferred(telemetry baseline 이후) → `FE-GATE-018` 은 그 전까지 PASS 불가 / 근거: hub §14.2 note + §14.3 + FE-GATE-018.
|
||||
- 2026-07-19: 세 performance gate(FE-GATE-012 bundle / FE-GATE-026 lab / FE-GATE-018 field)에 **NFR threshold 값과 negative fixture 를 공급**; CI wiring 은 위임 / 근거: hub §15.1 + §15.2. (2026-07-21 정정: gate 의 pass condition 자체는 hub §15.1 소유이고 `FE-GATE-012` 의 Owner 는 build-bundle 이다 — hub §2.1.1.)
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
> `Supporting Claims` 의 `[[hub]]` 는 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] 를 가리킨다. 이 branch 는 `FE-OC-021` owner 이며, `FE-D*` decision row 중 이 slug 를 owner 로 갖는 것은 없다 — 아래 결정은 `FE-OC-021` 계약 조항과 §14 메커니즘을 branch-local decision(D1~D6)으로 내린 것.
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | Context-mandatory measurement: 모든 NFR 수치는 4-context(device/runtime · network/cache · route/data · build) metadata 와 함께만 evidence (`FE-OC-021`) | 항상 적용되는 contract invariant. 구체 context 값(CI runner CPU · throttling)은 §14.1 대로 run time 에 report metadata 로 기록 — 지금 고정 불가. 분기 없음 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §14.1 (`FE-NFR-C01`~`FE-NFR-C04`, "context 가 없는 숫자는 evidence 로 인정하지 않는다"), `FE-OC-021` | `project-decision` | CI runner spec · throttling profile 미확정 → repo/CI 생성 전 실제 context 값 확정 불가 (`FE-NFR-C01` note, `FE-Q-002`/`FE-Q-007`) |
|
||||
| D2 | Three machine-readable report split: bundle(`bundle.json`) · lab(`lab.json`) · 28-day field(`field-web-vitals.json`) | three-report split 이 default; lab/field 경계를 보존하는 단일 통합 pipeline 이 등장하면 통합 재검토 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §14.3 (command→artifact 표), §20 measurable completion ("context metadata + lab/bundle/28-day field reports"); [[raw/official-docs/vite-build-tool-official]] `VITE-C2` (bundle 대상 = production build artifact) | `project-decision + official-doc` | 세 report 모두 `PLANNED_NOT_EXECUTED` — schema · collector 미구현 |
|
||||
| D3 | Lab ≠ field 불변식: lab(`FE-NFR-C01` synthetic Playwright)을 production percentile 로 표현 금지, field(`FE-NFR-C03` RUM p75)와 분리 | 불변식 — 대안 없음(분리 위반 = reject). 어떤 조건에서도 lab 값을 field SLO 로 승격하지 않음 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §14.2 note ("lab result 를 production percentile 로 표현하지 않는다"), `FE-OC-026`; web.dev Core Web Vitals (researched: "Only field measurement can accurately capture the complete picture" / "Lab measurement is the best way to test performance ... before they've been released") | `project-decision + official-standard` | collector 가 lab/field 를 혼동해 리포트하면 evidence 신뢰 붕괴 → negative fixture 로 강제 필요 |
|
||||
| D4 | Initial target matrix: LCP lab/field ≤ 2.5s, CLS ≤ 0.10, interaction/INP ≤ 200ms(p75), initial JS gzip ≤ 200 KiB, lazy chunk gzip ≤ 120 KiB | conditional-default: 프로젝트 초기값. device-class 별 커스텀 threshold 는 첫 실측·field data 가 threshold 의 device-UX 무관성을 보일 때 채택(`FE-RISK-010` revisit trigger = "first measurement") | web.dev Core Web Vitals (researched: LCP "2.5 seconds", INP "200 milliseconds", CLS "0.1", "75th percentile of page loads"); [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §14.2 (bundle budget = project-local initial), `FE-RISK-010` | `official-standard (vitals) + conditional-default (bundle budget)` | `FE-RISK-010` — bundle/threshold 가 실제 device UX 와 무관할 수 있음; 첫 측정 후 evidence 로 revisit |
|
||||
| D5 | 28-day field window + eligibility metadata(consent/privacy boundary · route-ID aggregation · production release ID · eligible sample); minimum eligible sample threshold = `deferred` | 28-day window 는 default; min-sample threshold 는 telemetry baseline 확보 후 owner 가 확정 — 그 전엔 `FE-GATE-018` PASS 금지 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §14.2 note, §14.3 (`collect:web-vitals-evidence` = "28-day context + p75 + eligible sample metadata"), §15.1 `FE-GATE-018` (28-day 는 hub/CrUX convention — web.dev 는 28일 미명시) | `project-decision` | min-sample threshold deferred → `FE-GATE-018` blocked; consent/privacy · sink 는 telemetry branch(`FE-OC-014`, `FE-Q-008`)에 의존 |
|
||||
| D6 | 세 performance gate 에 NFR threshold 값 + negative fixture 공급: `FE-GATE-012@1`(bundle — Owner 는 build-bundle), `FE-GATE-026@1`·`FE-GATE-018@1`(Owner 는 본 branch). pass condition 원문은 hub §15.1 소유 | contract 정의(분기 N/A). 단 CI wiring · blocking scope · artifact retention 은 위임(Open Risk 참조) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §15.1 (gate rows), §15.2 (lab negative fixture = "context metadata missing 또는 one named threshold exceeded") | `project-decision` | gate CI wiring/실행은 `feature-frontend-ci-quality-gates-contract` · `feature-frontend-test-taxonomy-contract`(`FE-OC-020`)이 소유 — 본 branch 는 pass-condition 만 정의 |
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> 모든 경로(`artifacts/performance/*`, `src/contracts/*`)는 hub §4.6 Planned directory blueprint 에서 온 `planned` anchor 다. **frontend 코드가 없으므로 전 항목 `planned`.**
|
||||
|
||||
### 1. Measurement context metadata schema
|
||||
|
||||
> **Trace**: D1 (`FE-OC-021`, hub §14.1). 각 report 는 아래 4-context 를 embed 해야 evidence 로 인정된다.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**(`lab.json`·`field-web-vitals.json` 한정): 실제 JSON key 이름(`context.runner`, `context.throttling`, `context.cache` 등)은 hub 가 아직 명명하지 않음. 명명 *스타일* 은 hub §2.1.3 이 정한 **camelCase**(`artifacts/**` report 한정)를 따른다 — 이전 판의 snake_case 제안은 그 규약 이전 것이라 폐기한다. `bundle.json` 은 `ART-FE-002@1` 스키마가 이미 확정했으므로 이 항목 대상이 아니다. trade-off: Lighthouse/Playwright reporter 가 자체 schema 를 고정하면 그 형태로 맞춘다. CI runner CPU/throttling *값* 은 미확정이라 여기서 상수화하지 않고 run time 기록(§14.1) 으로 남긴다.
|
||||
|
||||
| Context ID | 무엇을 기록 | 어느 report 가 embed | 근거 |
|
||||
|---|---|---|---|
|
||||
| `FE-NFR-C01` | Playwright Chromium, CI runner spec, cold cache, throttling profile | `lab.json` | hub §14.1 |
|
||||
| `FE-NFR-C03` | production field data, real network, 28-day window, top route IDs | `field-web-vitals.json` | hub §14.1 |
|
||||
| `FE-NFR-C04` | build runner image + Node/pnpm version | `bundle.json` | hub §14.1 |
|
||||
|
||||
- 규칙(§14.1): context 가 없는 숫자는 evidence 로 인정하지 않는다 → context block 부재 = gate FAIL (negative fixture, §4 참조).
|
||||
|
||||
### 2. Three report artifacts + threshold binding
|
||||
|
||||
> **Trace**: D2 (hub §14.3, §20) + D4 (web.dev vitals threshold + hub §14.2 bundle budget). command·artifact·NFR 매핑은 hub §14.3 표의 도출이다.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**(`lab.json`·`field-web-vitals.json` 한정): 두 report 의 *내부 JSON 구조*(필드 계층·배열 shape)는 아직 미등록 → threshold pass/fail + context block + metric 값을 담는 flat object 로 제안. `bundle.json` 은 hub §2.1.3 `ART-FE-002@1` 스키마가 정본이다. trade-off: downstream gate parser 가 확정되면 그 shape 로 조정.
|
||||
|
||||
| Report | Planned command | Planned artifact | NFR IDs | Threshold (initial) |
|
||||
|---|---|---|---|---|
|
||||
| bundle | `pnpm check:bundle` | `artifacts/performance/bundle.json` | `FE-NFR-001`, `FE-NFR-002` | initial JS gzip ≤ 200 KiB, lazy chunk gzip ≤ 120 KiB |
|
||||
| lab | `pnpm test:performance` | `artifacts/performance/lab.json` | `FE-NFR-003`~`FE-NFR-005` | LCP ≤ 2.5s, CLS ≤ 0.10, named interaction ≤ 200ms + context metadata |
|
||||
| field | `pnpm collect:web-vitals-evidence` | `artifacts/performance/field-web-vitals.json` | `FE-NFR-013`~`FE-NFR-015` | p75 LCP ≤ 2.5s, CLS ≤ 0.10, INP ≤ 200ms + eligible-sample metadata |
|
||||
|
||||
### 3. Field Web Vitals eligibility + deferred threshold
|
||||
|
||||
> **Trace**: D5 (hub §14.2 note, §14.3, §15.1 `FE-GATE-018`). field report 가 반드시 담아야 할 metadata 와 deferred 결정의 처리.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: minimum eligible sample threshold 의 *수치* 는 `deferred`(telemetry baseline 확보 전 확정 불가) → 값을 임의로 지어내지 않고 미정으로 둔다. trade-off: 값이 없으면 `FE-GATE-018` 을 PASS 로 못 올리는 것을 *의도적 안전 기본값* 으로 수용.
|
||||
> - **OUT_OF_BRANCH_SCOPE**: consent/privacy boundary 의 실제 구현·telemetry sink 는 [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] (`FE-OC-014`) 소유 → 여기서 필드 *요구사항* 만 열거하고 수집 pipeline 은 명세하지 않음.
|
||||
|
||||
- field report 필수 metadata: consent/privacy boundary flag · route-ID aggregation · 28-day window · production release ID · eligible-sample count.
|
||||
- deferred 처리: telemetry baseline 획득 → owner 가 min eligible sample threshold 확정 → 그때까지 `FE-GATE-018` 은 `FAIL_UNVERIFIED` 유지(hub §14.2 note).
|
||||
|
||||
### 4. Gate pass-conditions + negative fixtures
|
||||
|
||||
> **Trace**: D6 (hub §15.1 gate rows, §15.2 negative fixture) + D3 (lab≠field invariant). 세 gate 의 pass 조건과 "실제로 동작함" 을 보이는 deliberately-failing fixture.
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — pass 조건·negative fixture 는 hub §15.1/§15.2 에서 직접 도출.
|
||||
|
||||
각 gate 의 blocking scope·pass condition 은 hub §15.1 소유다. 본 브랜치가 공급하는 것은 **NFR threshold 값과 그 negative fixture** 다.
|
||||
|
||||
| Gate ID | 본 브랜치가 공급하는 NFR | Negative fixture |
|
||||
|---|---|---|
|
||||
| `FE-GATE-012@1` | `FE-NFR-001`, `FE-NFR-002` | chunk 가 budget 초과 → FAIL |
|
||||
| `FE-GATE-026@1` | `FE-NFR-003`~`FE-NFR-005` | context metadata 누락 또는 하나의 named threshold 초과 → FAIL (§15.2) |
|
||||
| `FE-GATE-018@1` | `FE-NFR-013`~`FE-NFR-015` | 28-day eligible sample 부족 / min-sample 미해소 → PASS 불가 |
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
- **실패·엣지 경로**:
|
||||
- context metadata 누락 → gate FAIL (lab negative fixture, hub §15.2). context 없는 숫자는 evidence 아님.
|
||||
- named threshold(LCP/CLS/INP/bundle) 하나라도 초과 → 해당 gate FAIL.
|
||||
- field eligible sample 이 (deferred) min threshold 미만 → `FE-GATE-018` PASS 불가(fail-safe, fail-open 아님).
|
||||
- cold vs warm cache · network variance → context 로 구분 기록, 평균으로 뭉개지 않음.
|
||||
- lab 결과를 field percentile 로 오표기(D3 위반) → invariant 위반, negative fixture/answer-boundary 로 차단.
|
||||
- **다른 계약 의존**:
|
||||
- [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] (`FE-OC-018`) — bundle report 는 이 branch 가 만드는 frozen production build artifact 를 측정. build baseline 변경 시 bundle budget 재보정 (§20 Dependency).
|
||||
- [[raw/branch-notes/feature-sample-feature-slice-contract-fixture]] (`FE-OC-024`) — lab/field 측정 대상 route/data 는 sample contract-fixture slice. fixture 제거/변경 시 lab context(route/data) 갱신 (§20 Dependency).
|
||||
- [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] (`FE-OC-014`) — field Web Vitals 수집 pipeline·consent/privacy boundary·telemetry sink 소유. 본 branch 는 field report 의 required metadata 만 정의하고 수집을 소비.
|
||||
- [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] (`FE-OC-020`) + [[raw/branch-notes/feature-frontend-ci-quality-gates-contract]] — gate CI wiring·blocking scope·artifact retention 소유. 본 branch 는 pass-condition 만 제공.
|
||||
- [[raw/branch-notes/feature-api-client-response-envelope-contract]] (`FE-OC-006`, `FE-OC-009`) — `FE-NFR-007`(timeout 10s)·`FE-NFR-008`(retry ≤2) 메커니즘 소유. 본 branch 는 그 NFR 값을 target matrix 로 참조만.
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| bundle initial JS gzip ≤ 200 KiB & lazy chunk gzip ≤ 120 KiB | build/collector 없음 | `pnpm check:bundle` → `bundle.json` threshold 검사 (`FE-GATE-012`); budget 초과 negative fixture | `needs-confirmation` |
|
||||
| lab LCP/CLS/interaction 이 recorded context 와 함께 threshold 이하 | lab runner·throttling profile 미확정 | `pnpm test:performance` → `lab.json` + reproducibility metadata (`FE-GATE-026`); negative fixture: context 누락/threshold 초과 | `needs-confirmation` |
|
||||
| field p75 LCP/CLS/INP 가 28-day eligible sample 에서 threshold 이하 | RUM·consent·min-sample threshold 모두 deferred | `pnpm collect:web-vitals-evidence` → `field-web-vitals.json` (`FE-GATE-018`) — deferred threshold 해소 전 PASS 불가 | `needs-confirmation` |
|
||||
| context 없는 숫자가 gate 에서 reject 된다 | 강제 로직 없음 | lab negative fixture(§15.2): context metadata 제거 시 gate FAIL 확인 | `planned` |
|
||||
| lab 결과가 field percentile 로 표현되지 않는다 (D3) | 관례상 혼동하기 쉬움 | report schema 검사 + answer-boundary 체크(`FE-OC-026`) | `planned` |
|
||||
|
||||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
- 스캐폴딩 단계: `/coverage` 실행 전 수동 행을 만들지 않는다.
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
- 없음 — 스캐폴딩 단계.
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: artifact-imports:start -->
|
||||
### 가져온 artifact 계약
|
||||
|
||||
| Artifact Ref | Owner | Producer | Schema Ref |
|
||||
|---|---|---|---|
|
||||
| `ART-FE-002@1` | [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] | [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] | `harness/source/artifact-schemas/ca-skeleton-frontend/bundle-report.schema.json` |
|
||||
<!-- GENERATED: artifact-imports:end -->
|
||||
|
||||
<!-- GENERATED: project-contract-imports:start -->
|
||||
## 가져온 프로젝트 계약
|
||||
|
||||
| Ref | Owner | 요약 | Branch 적용 |
|
||||
|---|---|---|---|
|
||||
| `FE-GATE-012@1` | [[raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract]] | 번들 NFR threshold 초과면 release 를 MUST 차단 | import 참조로 적용 |
|
||||
| `FE-OC-014@1` | [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] | telemetry는 best-effort이며 render·API success를 차단하면 안 되고 PII·token을 전송하면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-020@1` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | gate 종류별 책임·fixture·artifact를 분리하고 실패를 warning으로 낮추면 안 됨 | import 참조로 적용 |
|
||||
| `FE-OC-026@1` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | 외부 답변은 evidence grade를 MUST 보존하고 목표 수치를 측정 결과처럼 말하면 안 됨 | import 참조로 적용 |
|
||||
<!-- GENERATED: project-contract-imports:end -->
|
||||
|
||||
- 없음 — 자식 자료는 생성 후 controller가 parent Cluster와 함께 등록한다.
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
- 없음 — daily note는 이 작업에서 수정하지 않는다.
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크: 없음
|
||||
- 리뷰 메모: 없음
|
||||
- 머지 결과 / 배포 환경: `planned`
|
||||
- **wiki 추출 대상** (verified만): 없음
|
||||
- **추출하지 않을 항목** (planned / documented-only / abandoned): 현재 전체
|
||||
+2420
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user