---
task_id: task-3049
scope: task
team: dev5-team
level: Lv.2
status: PR_OPEN (머지 HOLD — ANU)
pr: https://github.com/Jeon-Jonghyuk/InsuRo/pull/268
branch: task/task-3049-dev5
head_sha: ed7593317a06397f6b747d88d9665f415e653500
base: origin/main 534fde811650a33e71810500055f4b685ff56e57
---

# task-3049 — 확장 다운로드: 항상 최신 + 설계사 수신 가능 + 릴리스 일자 표시

**S**: 회장이 받는 확장 zip 이 4개월 낡았고, 설계사는 아예 받지 못했으며, 화면 날짜가 실제 릴리스일과 달랐다.
**C**: 릴리스 워크플로우가 버전 미인상 시 build/release 를 skip 하면서도 **초록 success** 로 끝났고, 저장소가 private 이라 회장 외 전원 404 였다.
**Q**: 버전을 안 올려도 최신 zip 이 자동 배포되고, 설계사가 인증만으로 받을 수 있고, 실제 자산 생성일자가 KST 로 보이게 하려면?
**A**: skip 구조 제거 + 업로드분 sha256 재검증, 배포 디스크를 인증 뒤에서 zip 스트리밍, `released_at` 을 패키징 대상 최신 mtime 으로 교체.

## S — 상황

회장 지시 3가지(항상 최신 / update 일자 표시 / 그 일자가 업로드·릴리스일과 연동)를 모두 충족해야 완료다.
ANU 가 근인 3종을 이미 규명했고, 본 태스크는 그 전제를 재조사 없이 수용해 구현·실증했다.
**ANU 전제는 전부 실측으로 재현되었다. 반증 없음.**

## 봉투 3답 (요구된 첫 줄)

**최신 zip 자동생성 실증 = YES · 비회장 계정 수신 가능 = YES · 날짜 표시 = YES**

| 질문 | 답 | 근거(실측) |
|---|---|---|
| 최신 zip 이 자동 생성되나 | **YES** | 워크플로우 실제 실행 `33165917828` — skip 0건, 자산 `22,090`→`75,492` bytes, 받은 zip `content.js` **3,232줄** |
| 설계사(비회장)가 받을 수 있나 | **YES** | GitHub 경로 무인증 **404** vs 새 경로 인증 시 **200 / 74,035 bytes**. `admin@insuro.kr`(GitHub 접근권 없음) 실토큰 수신 실증 |
| 날짜가 보이나 | **YES** | 브라우저 실측 `업데이트 2026-08-28 16:28 KST` — 배포 자산 실제 생성시각과 일치 |

## C — 복잡성 (검증 1·2: 낡음·skip 재현)

### 검증1 — 낡음 재현 (ANU 기준값과 완전 일치)

릴리스 자산 `insuro-helper-0.4.1.zip` (created `2026-05-04T12:47:26Z`, 22,090 bytes) 실다운로드 결과:

| 항목 | 릴리스 zip(5/4) | origin/main 현재 | 배포 디스크 |
|---|---|---|---|
| manifest version | 0.4.1 | 0.4.1 | 0.4.1 |
| `content.js` | **678줄** | **3,232줄** | 3,232줄 |
| `background.js` | **199줄** | **485줄** | 485줄 |
| `background/` 디렉터리 | 없음 | 있음 | 있음 |

★ **버전 문자열은 3자 모두 `0.4.1` 로 동일하다.** 버전만 봤으면 최신이라 오판했을 함정이 실재했다.

### 검증2 — skip 인데 success 재현 (추정 아님, 실 run 조회)

`GET /actions/runs/32799639172/jobs` (2026-08-25, t3006 머지 커밋):

```
success   Build pilot extension (파일럿 빌드 — 항상 생성)
success   Upload pilot build artifact
skipped   Build extension zip          ← 산출물 없음
skipped   Create Release               ← 릴리스 갱신 없음
success   Skip notice
JOB conclusion: success                ← 그래도 초록
```

## Q — 무엇을 했나

### ① `.github/workflows/extension-release.yml`
- `Check existing release` 스텝과 `if: steps.existing.outputs.exists == 'false'` 두 곳, `Skip notice` **전부 제거**. 워크플로우 전체에 `exists ==` 문자열 **0건**
- 태그 존재 시 `gh release upload --clobber`(자산 덮어쓰기), 없으면 `gh release create`. **`gh release delete`·태그 삭제 0건**
- **자동 버전 인상은 채택하지 않았다.** 근거: 커밋을 되밀어야 하는데 트리거가 `paths: extension/**` 이라 무한루프 위험. 대신 zip 최상위에 `BUILD_INFO.json`(version·commit full sha·built_at·run_id·run_url)을 넣어 "같은 버전 다른 내용"을 식별 가능하게 했다. 릴리스 body/name 에도 short sha + built_at 표기
- **`Verify uploaded release assets (sha256 + updatedAt)` 신설** — 업로드된 자산을 **재다운로드해 로컬 zip 과 sha256 대조**, 불일치 시 `exit 1`. 자산 `updatedAt` 이 run 시작 이전이면 `exit 1`. 메인·파일럿 zip **둘 다** 검증 → *산출물 없는 실행은 구조적으로 success 가 될 수 없다*
- 파일럿 빌드 스텝(t3006 재발방지)·`if-no-files-found: error`·zip 제외 3종·트리거·`permissions` 무손상

### ② `server/routes/extension.py` — 인증 다운로드
**채택: (a) 디스크 zip 스트리밍.** 근거 3가지 —
1. 서버 `.env` 에 GITHUB 계열 변수가 하나도 없어 (b) 프록시 방식은 **자격증명 조달이 선행 과제**가 된다
2. `/home/jay/insuro-releases/current/extension/` 은 배포 산출물이라 **"배포된 코드 = 배포되는 확장"이 구조적으로 보장**된다 (diff 0건 실측)
3. 버전 인상·GitHub 가용성에 **의존하지 않는다**

- `GET /api/extension/download` → `Depends(verify_jwt)` (기존 InsuRo 인증 재사용, **새 인증체계 없음**). lazy import 로 순환참조 회피 — `policy_terms_v1.py` 관용구 답습
- 미인증 **401**, 디렉터리/manifest 부재 **404**, 패키징 파일 0건이면 **404**(빈 zip 200 없음)
- 제외 규칙을 워크플로우와 동일하게: `__tests__`(어느 깊이든)·`README.md`·`.DS_Store`·`_` 시작 최상위 항목
- `_build_download_url()` → `/api/extension/download` (접근 불가능한 GitHub URL 을 프론트에 주지 않는다). `_get_github_repo()` 기본값 **미변경**

### ③ 날짜
- `released_at` = manifest mtime → **zip 에 실제로 담기는 파일들의 최신 mtime**. 못 구하면 `None`
- `released_at_kst` 신설(UTC+9 고정 오프셋, `zoneinfo` 미사용). 화면에 `업데이트 …`, 값 없으면 **"업데이트 일자 확인 불가"**
- 캐시 invalidate 키도 manifest mtime → 패키징 자산 최신 mtime (안 그러면 갱신 후 5분간 거짓 날짜)
- 프론트 `handleDownload`: `<a href>` → **fetch + `getAuthHeader()` + blob + revokeObjectURL**. 인증 경로에서 헤더가 실리게 하는 필수 변경

## A — 실측 검증 결과

### 검증3 — 자동 갱신 실증 (워크플로우 **실제 실행**)

`workflow_dispatch` on `task/task-3049-dev5` → run **`33165917828`**, conclusion **success**, **skipped 스텝 0건**:

```
success  Build metadata (빌드 스탬프)
success  Build pilot extension (파일럿 빌드 — 항상 생성)
success  Build extension zip                              ← 기존엔 skipped
success  Publish release assets (신규 생성 또는 자산 덮어쓰기)  ← 기존엔 skipped
success  Verify uploaded release assets (sha256 + updatedAt)
```

★ 태그 `extension-v0.4.1` 이 **이미 존재하는 상태**에서 build/release 가 실행됐다. 이것이 수정의 핵심이다.

릴리스 자산 실측 (before → after):

| 자산 | before | after |
|---|---|---|
| `insuro-helper-0.4.1.zip` | 22,090 bytes · created `2026-05-04T12:47:26Z` | **75,492 bytes** · created `2026-08-28T11:06:59Z` |
| `insuro-helper-pilot-v0.4.1.zip` | **부재** (Create Release 가 skip 돼 한 번도 안 올라감) | 75,970 bytes · `2026-08-28T11:06:59Z` |
| 태그/릴리스 | `published_at 2026-05-04T12:47:27Z` | **동일 — 삭제·재생성 없음** |

새 자산 내용: `content.js` **3,232줄** / `background.js` **485줄** / `BUILD_INFO.json`:
```json
{"version":"0.4.1","commit":"18877d76c697f82d909c21920e43c64b1275a399",
 "built_at":"2026-08-28T11:06:56Z","run_id":"33165917828"}
```

### 검증4 — 미인증 차단 실증 (로컬 실서버, TestClient 아님)

`uvicorn main:app --port 8792` (새 코드 + `INSURO_EXTENSION_PATH=배포 디스크`):

| 케이스 | 결과 |
|---|---|
| Authorization 헤더 없음 | **401** `{"detail":"Missing or invalid authorization"}` |
| `Bearer not-a-real-jwt` | **401** `{"detail":"Invalid token"}` (헤더 유무가 아니라 **실 JWT 검증**) |
| `curl -L` 리다이렉트 추적 | 최종 **401**, url 그대로 — **302 로 새지 않음** |
| GitHub 릴리스 URL 무인증 (기존 경로) | **404** — 설계사 차단의 실체 |

### 검증5 — 인증 사용자 수신 실증 (★ 줄 수까지 확인)

실 Supabase 비밀번호 그랜트로 발급한 **진짜 JWT** (`admin@insuro.kr`, `sub=ae549a5e…`, `aud=authenticated`).
★ 이 계정은 **GitHub 저장소 접근권이 전혀 없다** — 기존 경로로는 영원히 404 인 계정이다.

```
HTTP=200  size=74035
content-type: application/zip
content-disposition: attachment; filename="insuro-helper-0.4.1.zip"
```

받은 zip 실측:

| 항목 | 낡은 릴리스(5/4) | **받은 zip** | 현재 코드 |
|---|---|---|---|
| `content.js` | 678줄 | **3,232줄** | 3,232줄 |
| `background.js` | 199줄 | **485줄** | 485줄 |
| manifest version | 0.4.1 | 0.4.1 | 0.4.1 |

- 배포 디스크와 **`diff -rq` 0건**(바이트 단위 동일)
- `__tests__`·`README.md`·`_` 시작 항목 **0건**(정상 제외), `manifest.json` 최상위

### 검증6 — 날짜 실증

| 소스 | 값 |
|---|---|
| 배포 확장 디렉터리 mtime | `2026-08-28 16:28:56 +0900` |
| API `released_at` | `2026-08-28T07:28:56+00:00` |
| API `released_at_kst` | `2026-08-28 16:28 KST` |
| **브라우저 화면 실측** | **`업데이트 2026-08-28 16:28 KST`** |

프론트가 실제로 쓰는 엔드포인트 `/api/insuro/composite-design/extension-version` 응답 전문:
```json
{"version":"0.4.1","latest_version":"0.4.1","download_url":"/api/extension/download",
 "released_at":"2026-08-28T07:28:56+00:00","released_at_kst":"2026-08-28 16:28 KST"}
```

### 검증7 — 실 브라우저 E2E (Playwright, SKIP 없음)

검증 대상: `CompositeExtensionGuide.tsx` blob `175461ee` (최종 HEAD 와 **동일 blob** 재확인 완료)

- **로그인월 아님 4중 확인**: URL `/intro`(≠`/login`) · `password` input 0개 · "로그인" 버튼 0개 · localStorage access_token 의 email 이 입력 계정과 일치. 동의 게이트(task-2990) 통과 후 사이드바·아바타 실제 렌더 확인
- 다운로드 요청: `GET http://127.0.0.1:8792/api/extension/download` → **200**, **`Authorization: Bearer …` 헤더 실림** ★ `<a href>` 방식이면 불가능한 경로
- 수신 파일 `insuro-helper-0.4.1.zip` **74,035 bytes**, `unzip -t` No errors, **`content.js` 3,232줄 / `background.js` 485줄**, zip 내 파일 sha256 이 디스크 원본과 전부 MATCH
- **console error 0건 / pageerror 0건**. 성공(200) 시에만 성공 toast
- 스크린샷: `memory/reports/assets/task-3049/` (`01-login-after` · `02-setup-page` · `03-latest-version-card` · `04-after-download`)

### 검증8 — 회귀 (★ base/head 동일 환경)

| | base | head | 델타 |
|---|---|---|---|
| pytest (`server/` 에서 실행) | **2900 passed / 5 skipped** | **2926 passed / 5 skipped** | +26, 실패 0 |
| vitest | **110 files / 1658 tests** | **111 files / 1677 tests** | +19, 실패 0 |

`npx tsc --noEmit` exit **0**.

★ **측정 방법론 사고 2건과 그 해소 (기록 필요)**
1. base vitest 를 **메인 저장소**에서 재면 `push-utils.test.ts` 1건이 실패한다. head 는 worktree 에서 쟀으므로 그대로 비교했으면 **가짜 개선**으로 보고할 뻔했다. → base 전용 worktree(`.worktrees/task-3049-base`)에서 재측정해 동일 환경을 맞췄다. 두 worktree 모두 단독 실행 시 35 passed.
2. E2E 담당이 만든 `.env.local` 의 `VITE_VAPID_PUBLIC_KEY` 가 `push-utils.ts` 의 `|| ''` 폴백을 성립시켜 같은 테스트를 깨뜨렸다. → `.env.local` 제거 후 **111 files / 1677 tests 전부 통과** 확인.
   `server/.env` 는 base·head·메인 저장소 **전부 부재**라 pytest 쪽 환경 편차는 없다.

### L1 스모크테스트

**실서버 기동 후 실 HTTP 호출** — TestClient 가 아니라 `uvicorn main:app --port 8792` 로
새 코드를 띄우고 `INSURO_EXTENSION_PATH=/home/jay/insuro-releases/current/extension` 를 물려
curl 로 직접 때린 결과다. **PASS**

| # | 호출 | 결과 | 판정 |
|---|---|---|---|
| 1 | `GET /openapi.json` (기동 확인) | `200 OK` | 성공 |
| 2 | `GET /api/extension/download` (헤더 없음) | `401` `{"detail":"Missing or invalid authorization"}` | 성공 (차단 확인) |
| 3 | `GET /api/extension/download` (`Bearer not-a-real-jwt`) | `401` `{"detail":"Invalid token"}` | 성공 (실 JWT 검증 확인) |
| 4 | `curl -L` 리다이렉트 추적 | 최종 `401`, url 불변 | 성공 (302 누출 없음) |
| 5 | `GET /api/extension/download` (실 Supabase JWT) | `200 OK` · 74,035 bytes · `application/zip` | 성공 |
| 6 | `GET /api/insuro/composite-design/extension-version` (실 JWT) | `200 OK` · `released_at_kst: "2026-08-28 16:28 KST"` | 성공 |

5번으로 받은 zip 실측: `content.js` **3,232줄** / `background.js` **485줄** — 배포 디스크와
`diff -rq` **0건**(바이트 동일). 낡은 릴리스 자산(678/199줄)이 아님을 **줄 수로** 확인했다.

**브라우저 스크린샷**: `memory/reports/assets/task-3049/03-latest-version-card.png` —
"최신 버전" 카드에 `v0.4.1` / `업데이트 2026-08-28 16:28 KST` / "확장 다운로드" 버튼이
실제로 렌더된 화면. 로그인월 아님을 4중 확인함.

**테스트 결과**: pytest **2926 passed** (base 2900 passed) · vitest **1677 passed** (base 1658 passed) · 실패 0.

### 검증9 — 봉인 변이 (명세 요구 3종 + 자발 2종, 전부 FAIL 실증 후 원복)

| # | 변이 | 결과 (실제 출력) |
|---|---|---|
| 1 | 릴리스 skip 조건 `if: exists == 'false'` 부활 | **3 failed / 19** — `test_no_exists_conditional_anywhere` 외 2건 |
| 2 | 다운로드 인증 `Depends` 제거 | `test_download_requires_auth` **FAILED** — `assert 200 == 401` |
| 3 | `released_at` → manifest mtime 원복 | **2 failed** — `released_at 이 패키징 대상 최신 mtime(content.js)과 다르다` |
| 4 | `handleDownload` → `<a href>` 원복 (자발) | **12 failed / 19** — `expected [] to have a length of 1` |
| 5 | 날짜 `?? new Date().toLocaleDateString()` (자발) | **3 failed / 19** — `expected '업데이트 8/28/2026' to be '업데이트 일자 확인 불가'` |

★ 변이가 no-op 이 아님이 **FAIL 건수로 실증**됐다. 전부 원복 후 GREEN 재확인(`git diff` 빈 출력·blob sha 대조).

★ 봉인 4·5 는 명세에 없었으나 **추가했다.** 이유: 프론트 변경(인증 헤더·날짜 표시)을 덮는 테스트가 0건이라, 최초 상태에서는 vitest 수가 base 와 **1658 로 동일**했다. 그대로 뒀으면 `<a href>` 로 되돌려도 아무 테스트도 깨지지 않았다.

## 근본원인 5 Why

**1st Why — 왜 회장이 낡은 확장을 받았나?**
릴리스 자산 `insuro-helper-0.4.1.zip` 이 2026-05-04 이후 한 번도 갱신되지 않았다.

**2nd Why — 왜 갱신되지 않았나?**
`extension-release.yml` 이 태그 `extension-v<ver>` 존재 시 build/release 스텝을 skip 했고,
`manifest.json` 버전이 0.4.1 에서 올라간 적이 없어 매 실행마다 skip 됐다.

**3rd Why — 왜 아무도 몰랐나?**
skip 은 실패가 아니다. 워크플로우는 **매번 초록 success** 로 끝났고, 알림도 대시보드도
"성공"만 보고했다. *산출물이 0건인데 성공*이라는 이 조직의 반복 유형이다.

**4th Why — 왜 초록이 무의미했나?**
워크플로우가 **자기 산출물을 검증하지 않았다.** 릴리스 자산이 실제로 갱신됐는지 확인하는
스텝이 없어, 스텝 실행 여부와 결과물 존재가 분리돼 있었다.

**5th Why — 왜 그렇게 설계됐나?**
"버전이 올라갈 때만 릴리스한다"는 전제를 **버전 인상이 강제되지 않는 파이프라인** 위에 얹었다.
전제가 깨져도 파이프라인이 알려주지 않는 구조였다.

**→ 처방**: 전제를 없애고(skip 제거·버전 무관 자산 갱신), 산출물을 **재다운로드 sha256 대조**로
검증해 *산출물 없는 실행이 success 가 될 수 없게* 만들었다.

## 수정 파일별 검증 상태 (절대경로)

| 파일 | 상태 |
|---|---|
| /home/jay/projects/InsuRo/.github/workflows/extension-release.yml | 검증완료 — 실 run 33165917828 success·skip 0건 |
| /home/jay/projects/InsuRo/server/routes/extension.py | 검증완료 — 로컬 실서버 401/200 실증 + 13 tests |
| /home/jay/projects/InsuRo/server/tests/test_extension_release_workflow.py | 검증완료 — 신규 19 tests |
| /home/jay/projects/InsuRo/server/tests/test_extension_routes.py | 검증완료 — 6 → 13 tests (갱신, 삭제 아님) |
| /home/jay/projects/InsuRo/src/pages/CompositeExtensionGuide.tsx | 검증완료 — 실 브라우저 E2E PASS |
| /home/jay/projects/InsuRo/src/pages/__tests__/CompositeExtensionGuide.download.test.tsx | 검증완료 — 신규 19 tests |

**allowed_resources 대조: 6/6 전부 허용 경로. 금지 경로 변경 0건**
(`extension/content.js`·`background.js`·`background/**`·`server/main.py`·`server/scripts/**`·지정 3개 페이지·`.gitignore` 전부 미변경)

## CI 상태 (PR #268 head `ed759331`)

| check | 결과 |
|---|---|
| qc-check · guard · ci/guard · lock-in-check | success |
| hidden-path-audit · merge-safety-check | success |
| cancel-kill-switch · gemini-review-gate | success |
| **e2e-test** | **failure — base 상속, 우리 변경과 무관** |

e2e 실패 3건은 전부 `new-design-comparison-honest-disclosure.spec.ts`(task-2969) 와
`task-2998-menu-consolidation.spec.ts` 로, **우리 금지 경로(`NewDesignComparison.tsx`)** 대상이며
이번 PR 은 해당 파일을 **한 줄도 건드리지 않았다**. 직전 머지 task-3046(PR #266, base `534fde8`)
브랜치에서도 동일하게 E2E 가 failure 였다 — **base 에서 상속된 적색**이다.

★ 우리가 건드린 화면을 덮는 유일한 e2e 스펙 `composite-design-vpath.spec.ts` 는
**케이스 A·B·C 전부 ✓ PASS**(11 passed / 3 failed 중 실패 0건). 확장 다운로드 경로 관련 실패 0건.

## ★ ANU 판단 필요 (3건)

1. **[CRITICAL] 워크플로우 실행이 프로덕션 릴리스 자산을 이미 갱신했다.**
   명세 검증3이 "실제로 돌려 산출물 확인, '설정했으니 될 것'은 FAIL" 을 요구해 실행했다. 결과적으로 **머지 전에 GitHub 릴리스 zip 이 최신화**됐다(회장 계정 기준 다운로드는 이미 개선됨). 태그·릴리스는 보존했고 원본 자산은 `/tmp/t3049_ORIGINAL_ASSET_BACKUP.zip`(sha256 `82822b6e219db5e1…`)에 백업해 두었다. 원복 원하면 그 파일을 `--clobber` 로 되올리면 된다. **해결책은 ANU 판단 사항이며, 되돌릴 필요가 없다면 그대로 두면 된다.**

2. **`Content-Disposition` 이 브라우저에서 읽히지 않는다.** `server/main.py:346` CORSMiddleware 에 `expose_headers` 가 없어 노출 헤더가 `cache-control, content-length, content-type` 뿐이다. 실측 `contentDispositionReadable: null`. 따라서 프론트의 `parseFilename()` 은 **항상 null** 이고 파일명은 폴백으로 결정된다. **현재는 서버·폴백 문자열이 같아 무증상**이지만, 서버가 파일명 규칙을 바꾸면 사용자는 조용히 다른 이름을 받는다. 수정 지점이 **금지 경로 `server/main.py`** 라 명세 지시대로 **진행하지 않고 보고**한다.

3. **`generate_release_notes` 자동 커밋목록이 릴리스 body 에서 빠졌다.** softprops 액션 → `gh` CLI 전환 시 `gh release edit` 에 해당 플래그가 없어 create/edit 경로 body 가 갈리는 것을 피하려 통일했다. 필요하면 create 경로에만 `--generate-notes` 를 되살릴 수 있다.

### [CRITICAL] scope-guard FAIL — 범위위반 아님, capabilities 스냅샷 부재 (자가해소 금지)

`finish-task.sh` 가 `[SCOPE-GUARD] FAIL — 머지 차단 + .escalate 생성` 을 냈다.
**범위 위반이 아니다.** 판별 근거 4종:

| 판별 항목 | 실측 |
|---|---|
| 명세에 `allowed_resources` 존재 | **있음** (task-3049.md 내 2회) |
| `memory/capabilities/task-3049.json` 스냅샷 | **부재** |
| 인접 태스크 스냅샷 | task-3048 / 3046 / 3044 / 3042 / 3040 **전부 존재** |
| 실제 diff 6건의 allowed 경로 매칭 | **6/6 전부 허용 경로** (금지 경로 0건) |

원인: 이 태스크는 `dispatch.py` 를 경유하지 않고 **스케줄 실행으로 착수**돼
dispatch 시점 immutable 스냅샷이 만들어지지 않았다. 가드는 스냅샷이 없으면
fail-closed 로 FAIL 한다 — 설계대로 동작한 것이고, 우리 diff 때문이 아니다.

기록된 선례와 동일 유형이므로 **봇이 스스로 해소하지 않았다**(자가 스냅샷 생성 =
mutable task 파일 self-bypass 가 되어 가드의 존재 이유를 무너뜨린다).
`memory/events/task-3049.escalate` = `{"reason":"scope_guard_violation"}` 로 박제돼 있다.

**해결책은 ANU 권한이다** — 스냅샷을 dispatch 규약대로 생성하거나
`task-3049.anu-override.json` 으로 override 를 박제하면 해결된다.
그 전까지 **머지 차단 상태를 유지하는 것이 맞다.**

부수: 확장 버전은 2026-05-04 이래 `0.4.1` 고정이다. `BUILD_INFO.json` 으로 식별은 가능해졌으나, 사용자에게 보이는 버전 문자열은 여전히 같다. **버전 인상 정책은 별도 판단 대상**이다.

## 결론 (DoD 대조)

| DoD 조항 | 판정 | 근거 |
|---|---|---|
| 확장 코드가 바뀌면 최신 zip 이 자동 생성 | **충족** | run `33165917828` skip 0건, 자산 22,090→75,492 bytes |
| 설계사가 로그인 상태에서 그 최신 zip 을 받는다 | **충족** | GitHub 접근권 없는 실계정 토큰으로 200/74,035 bytes, `content.js` 3,232줄 |
| "최신 버전" 영역에 생성일자(KST) 표시 | **충족** | 브라우저 실측 `업데이트 2026-08-28 16:28 KST` |
| 산출물 없는 실행이 success 로 보고되지 않는다 | **충족** | sha256 재대조 + `updatedAt` 검증 스텝, 봉인 변이 3 failed 로 실증 |

**PR #268 OPEN — 머지·배포는 ANU. 자동 머지 안 함.**
