7.0.11 인스톨러·코어 업데이트 변경(e60a82d73)에 대해 과거 회귀 22건을 부류별로 대조한
결과, 신규 노출면 1건과 테스트 위생 1건이 나와 인터뷰 결정대로 조치했다.
1. argv 채널의 SAPI 게이트 — CGI/FPM 은 register_argc_argv=On 이면 $_SERVER['argv'] 를
쿼리스트링을 '+' 로 쪼개 채우므로(`GET /?x+core:update` → argv[1]==='core:update', php-cgi
실측) 비인증 웹 요청이 업데이트 트리로 판정되어 bootstrap/app.php 자가 치유가 요청마다
패키지 매니페스트를 지우고 다시 만들었다. CoreUpdateContext 와 bootstrap/app.php 복제본
모두 argv 를 cli·phpdbg 에서만 읽는다. env 플래그 채널은 웹에서 주입할 수 없으므로 그대로
두어 웹 요청 안에서 시작하는 업데이트 흐름(7.1.0)에 영향이 없다. 동형성 테스트에 SAPI 축을
더했다.
2. 매니페스트 삭제 실패 기록 — PackageManifestCacheHelper::clear 가 지우지 못한 파일의
경로를 돌려주고, spawn 직전 호출부가 업그레이드 로그·콘솔에 경고로 남긴다. 권한·소유권
불일치면 자식의 자가 치유도 같은 이유로 실패해 증상은 제보와 같은 「Class not found」 인데,
이 경고가 원인이 권한이라는 유일한 흔적이다.
3. 테스트 격리 — tests/bootstrap.php 가 APP_PACKAGES_CACHE/APP_SERVICES_CACHE 를 테스트
전용 경로로 돌린다. proc_open 으로 자식을 띄우는 기존 테스트 2종의 자식이 개발 클론의
실제 bootstrap/cache 매니페스트를 지우고 다시 쓰던 것(stat 실측)을 부모·자식 함께 막는다.
관리자 [시스템 최적화] 경로(withPreservedContainer 파사드 복원의 미실측 형제 호출처)는
임시 설치본에서 API 로 실측했다 — 200, 설정·라우트 캐시 재생성, 후속 요청 200, 로그 오류 0.
4. 트러블슈팅 사례 ↔ 회귀 테스트 앵커 계약 — 신규 사례는 헤딩에 <!-- case:{영역}-{번호} -->
앵커를 달고 같은 문자열을 그 사례를 잠그는 회귀 테스트에도 남겨야 한다. 사례 번호는
문서마다 1부터 재시작하고 병합으로 중복되므로(이번 리베이스에서도 우리 사례가 develop 과
같은 29 였다가 31 로 밀렸다), 개수만 대조하면 다른 사례를 덮는 테스트도 초록이 된다.
그런데 판정기 check-troubleshooting-test-coverage.cjs 를 부르는 지점이 저장소에 하나도
없었다 — 스크립트 자체 주석에만 실행법이 적혀 있어 아무도 부르지 않으면 영원히 돌지
않았고, 그 사이 위반이 9건 쌓였다(backend 26~31, cache 17~19). 돌지 않는 대조는 아무것도
잠그지 못하므로 위반 해소와 실행 지점 부여를 함께 한다.
9건 전부에 앵커를 부착하고(각 사례가 선언한 회귀 테스트 중 가장 구체적인 파일에 배치,
한 파일이 두 사례에 선언된 경우는 갈라 배치), stop-guard 7.2 에 앵커 계약 + 미커버
baseline ratchet 두 축으로 등록했다. 트러블슈팅 사례 추가 프로토콜에 6단계를
더하고 coverage 에 troubleshooting-case-anchor-contract(manual-only, 전용 판정기 위임)를
등재했다. 판정기 종료코드 1 → 0, 미커버 건수는 전 문서 baseline 그대로다.
Laravel PackageManifest 는 bootstrap/cache/packages.php 가 있으면 stale 여부를 검사하지
않고 그대로 읽어 등재된 provider 를 new 한다. 코어 업데이트는 Step 6/8 에서 vendor 를
--no-dev 로 교체하지만 그 파일은 Step 11 까지 이전 설치본의 것이 남으므로, 옵션 없는
`composer install` 로 깔린 사이트에서는 Step 10 spawn 자식이 새 vendor 에 없는 provider 를
찾다 부팅 단계에서 죽는다. 부팅 전이라 앱 로그에 흔적이 없고 부모에게는 자식의 비정상
종료로만 보여, 운영자에게는 「Class ... not found」 와 수동 재개 안내만 남는다.
(sir.kr 커뮤니티 제보, 7.0.9 → 7.0.10)
3계층으로 막는다.
1. 부모 — spawn 직전 PackageManifestCacheHelper::clear
2. 자식 — bootstrap/app.php 가 G7_UPDATE_IN_PROGRESS 를 보고 스스로 정리한다.
이미 배포된 7.0.9·7.0.10 부모는 고칠 수 없으므로 그 아래에서 도는 신버전
자식의 유일한 방어다. App\ 클래스를 참조하지 않고 실패는 무시한다.
3. 범위 — CoreVersionChecker 의 env APP_VERSION 우선을 CoreUpdateContext 트리 안으로
축소한다. 업데이트 전에 뜬 artisan serve·큐 워커가 옛 값을 물고 확장을
incompatible_core 로 끄던 경로를 닫는다(관리자 템플릿이 대상이면 복구 UI
자체에 도달할 수 없다).
업데이트 트리 판정은 App\Support\CoreUpdateContext 가 단독 소유하고
CoreServiceProvider::isCoreUpdateInProgress 는 위임으로 남는다. bootstrap/app.php 의
복제본은 부팅 전이라 불가피한 예외이며, 두 조건의 동형성을 테스트가 단언한다.
실측 중 드러난 결함 2건을 함께 고쳤다.
- ConfigCacheHelper::withPreservedContainer 가 파사드 애플리케이션을 되돌리지 않아
Step 11 이 `Target class [command.tinker] does not exist` 로 실패·롤백했다.
- updateVersionInEnv 가 프로세스 환경을 갱신하지 않아 config 캐시에 이전 버전이 구워졌다
(Laravel env 저장소가 불변이라 재부팅으로도 덮이지 않는다).
인스톨러는 재사용 vendor 의 개발용 패키지를 installed.json 으로 감지해 설치 환경 확인
카드·설치 로그로 알리되 설치를 차단하지 않고( 결정 D1), 재사용 경로에서도 컴파일 캐시를
정리한다. 실행되는 명령만이 아니라 실패 시 안내하는 수동 명령까지 --no-dev 로 맞췄다.
코어 업데이트 완료·핸드오프·단독 재개 사후 단계에서 queue:restart 신호를 보낸다(D3).
코어 7.0.10 → 7.0.11.
https://github.com/gnuboard/g7/issues/128 (제보자 glitter-gim) 과 그 부수의무
전수조사에서 확정한 결함군을 닫는다.
## 디버그 라우트 게이트 (F1~F5)
게이트가 핸들러 안에 흩어져 있어 8개 라우트 중 3개에만 붙어 있었다. 빠진 쪽에
`File::cleanDirectory(storage/debug-dump)` 를 수행하는 `DELETE clear` 가 있었고,
production·APP_DEBUG=false 에서도 미인증 200 으로 덤프 전체가 지워졌다. GET 4종은
User SPA catch-all 이 등록 순서상 앞서 가려 주고 있었을 뿐이라, 보호를 우연에 맡긴
구조였다.
게이트를 `bootstrap/app.php` 의 devtools 래퍼 한 곳(`debug.gate` 그룹 미들웨어)으로
올린다. 새 라우트는 자동으로 덮이고, 부착 지점이 하나라 훼손이 곧바로 드러난다.
`withRouting(channels:)` 인자가 함께 등록하던 게이트 없는 `/broadcasting/auth` 는
제거하고, 채널 정의 로드만 `BroadcastServiceProvider` 로 옮겨 웹소켓 킬스위치
우회로를 없앤다. catch-all 제외 패턴에는 예약 프리픽스를 전수 추가했다.
## 확장 자산 CSS 상대 참조 (범위 추가 — 결정)
`asset_url_mode=extensionless` 에서 자산 URL 이 쿼리 형태가 되면 브라우저의 기준
디렉토리가 `/api/{타입}/assets/` 로 잡혀, CSS 안의 `url('./woff2/f.woff2')` 가
존재하지 않는 주소가 된다. 사용자 템플릿 웹폰트 1건과 관리자 템플릿 국기 아이콘
약 500건이 이 상태였다. 병합 CSS 번들은 더 나빴다 — 상대 참조를 가진 확장을 번들에서
통째로 제외하고 있었는데, 번들 URL 이 내려오면 프론트는 개별 로딩을 아예 타지 않으므로
제외는 곧 그 확장 스타일이 하나도 적용되지 않음을 뜻했다.
둘 다 서빙 시점에 절대 자산 URL 로 치환한다. 최종 URL 은 런타임 모드와 캐시 버전이
정하므로 빌드 시점에는 알 수 없고, 동봉 자산은 제3자 산출물이라 원본을 손대면 상류
갱신마다 재작업이 된다. 개별 서빙 3경로와 병합 번들이 같은 해석 규칙을 공유한다 —
갈라지면 한쪽만 고쳐진 채 남는다.
이 결함군은 공통적으로 예외도 서버 로그 흔적도 남기지 않는다. 열린 엔드포인트가
정상 응답하는 것, 404 로 글꼴이 기본 서체가 되는 것이 유일한 증상이다.
## 회귀 잠금
되돌림 red 를 모집단 전원에 대해 실측했다 — 그룹 미들웨어 제거, 개별 게이트 재주입,
`channels:` 복원, catch-all 예외 제거, 컨트롤러 3원의 `fileResponse` 회귀, 번들
치환 제거가 각각 어느 테스트를 red 로 만드는지 확인하고 원상 복구했다.
게이트 부착 축은 행위 테스트로 대체할 수 없다 — 가려진 라우트는 행위상 "막힌 것" 과
구분되지 않으므로, `gatherMiddleware` 를 직접 보는 등록 계약 테스트를 둔다. 치환
배선도 같은 이유로 라우트 이름 규약에서 모집단을 도출하는 계약 검사를 둔다(손으로 적은
목록은 네 번째 경로가 생기는 순간 조용히 낡는다).
위임 관리자(부관리자)가 권한·역할·표현식·비밀 콘텐츠 경계를 우회하던
결함군을 계층 대칭성 원칙으로 전건 차단한다. 약한 경로가 정상 응답을
내보내는 것이 유일한 증상이라, 게이트를 생산 지점 한 곳(SSoT)에 두고
같은 데이터를 내보내는 소비 경로 전부가 그 게이트를 경유하도록 맞췄다.
- 등급 상한(rank ceiling): 슈퍼관리자 보호·역할/사용자 역할 배정(추가·제거
대칭)·일괄 상태변경·순서변경을 상세 경로와 동일 강도로 재적용. 가드는
DB 쓰기에 선행하여 거부 시 상태 불변.
- 레이아웃 표현식: new Function/with 실행을 AST 화이트리스트 평가기로 교체.
비-문자열 computed 키 정규화(normalizeKey)·Object facade(리플렉션 static
제거)·legacy 접근자 차단. 저장측 검증·정적 검사와 3계층 동형.
- secret 게이트: 비밀글의 댓글·첨부·문의 독립 경로 재적용, hash 파일서빙
소유권·비밀·발행 상태 검사 통일.
- 신뢰 스크립트 호스트: 확장 선언 기반 + same-origin 브라우저 정규화를
런타임·저장측·정적검사 3층 동형화.
- 회귀 감지: 단위·Feature·E2E·시나리오 매니페스트 전축 + audit 룰 4종 신설.
설치 단계와 코어 부팅의 정책이 엇갈려 있었다. 인스톨러는 root 를 자유롭게
허용하면서, 설치 후 부팅은 같은 값을 보고 확장 로딩 전체를 조용히 스킵했다.
설치는 성공하지만 이후 깨진 화면만 뜨고 원인은 로그 한 줄에만 남았다.
판정을 프레임워크 의존성 0 인 공용 SSoT 로 모아 인스톨러와 코어가 같은 답을
내게 하고, 설치 단계에서 서버 2경로로 차단하며, 설치 후 변경은 전용 에러
페이지로 드러낸다. 가드를 boot 가 아닌 미들웨어에 두어 artisan 이 죽지 않게
해 복구 수단을 남겼다. admin 은 일반 권한 운영 계정 오차단을 막기 위해 차단
목록에서 제외했다.
OPcache 는 상태 조회 경로 자체가 없어 성능 저하를 인지할 방법이 없었다.
설치 단계 경고(비차단)와 관리자 정보 탭 표시를 같은 판정 SSoT 로 추가했다.
확장 로드 여부만으로는 부족해 opcache.enable 지시자까지 확인하며, ini_get 이
차단된 환경은 "확인 불가"로 폴백해 경고도 차단도 하지 않는다.
확장이 SP Kernel 미들웨어 그룹을 직접 조작하거나 라우트 파일에 미들웨어
FQCN 을 직접 부착하던 임시 방식을, 확장이 부착 대상(targets)을 명시 선언하고
코어가 요청 시점에 라우트명·URI 로 매칭해 실행하는 self-gate 로 정규화.
번들 7건(ecommerce/gdpr/pay 3종) 이전, audit 룰 3건·문서·버전 동기화 동반.
부수적으로 generate-skills.cjs 가 재생성 시 신의성실(부수의무) 섹션을 소실
시키던 회귀를 수정 — 스킬별 불릿을 스크립트 SSoT 로 편입해 재생성에도 보존.