Files
llm-wiki/raw/branch-notes/feature-frontend-build-bundle-supply-chain-contract.md
T

346 lines
42 KiB
Markdown

---
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): 현재 전체