Repository navigation
feat: 영향도 분석·추적성·프로젝트 분석 탭 전반 (355 커밋) - #1
Open
kimsungsugit wants to merge 808 commits into
Open
kimsungsugit wants to merge 808 commits into
kimsungsugit wants to merge 808 commits into
Conversation
kimsungsugit
added a commit
that referenced
this pull request
Jul 29, 2026
PR #1 의 CI 실패 2건 모두 **코드 결함이 아니라 CI 설정 결함**이었다. 1. Unit Tests — 러너 OS 불일치 (2026-06 부터 한 달 넘게 방치) ubuntu-latest 에서 33 failed / 4,493 passed. 실패는 전부 Windows 전제 테스트다: SwUT 입력 어댑터의 `C:\...` 경로 리터럴 22개(11건) · Cloudium prefix/UNC 판정(9건) · 경로 confine(4건) · 훅 인터프리터 계약 등. 같은 트리를 로컬 Windows 에서 돌리면 **4,603 passed** 다. 6/20 main 실패 로그를 대조하니 `TestCollectMultiLogFolders` 등 **동일 테스트가 동일 이유로** 깨져 있었다 — 이 브랜치가 만든 게 아니다. → `runs-on: windows-latest`. skipif 로 넘기면 CI 가 그 33건을 영영 안 보게 된다. ⚠ windows 러너 기본 셸은 pwsh 라 `|| true`·줄끝 `\` 가 다르게 해석된다 → `shell: bash`. ⚠ ruff 를 unit-tests 의존성에 추가: `test_hook_env.TestProjectPy` 가 **해석된 인터프리터에 ruff 가 실재하는지**를 검사하는데, CI 엔 `.venv/` 가 없어 러너 Python 으로 폴백한다. 테스트를 약화시키는 게 아니라 환경을 계약에 맞추는 것이다. 2. Lint — `git diff --name-only` 가 낸 **삭제된 파일**을 그대로 검사기에 넘겼다 이 브랜치가 `.agents/skills/*/SKILL.md` 18개를 지웠더니 FileNotFoundError 18건으로 잡이 통째로 실패했다. 없는 파일은 lint 대상이 될 수 없다. ⚠ 같은 가드(`[ -f "$f" ]`)가 **syntax-check 스텝엔 이미 있었다** — 한쪽에만 있어 생긴 결함이라, py/js/SKILL 세 리스트가 갈리지 않게 `CHANGED` 한 곳에서 거른다. 검증: 실 diff 462파일 → 438(삭제 24 제거), SKILL.md 34 → 16. 남은 16개로 `check_skill_frontmatter.py` 실행 시 이상 없음(exit 0). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kimsungsugit
added a commit
that referenced
this pull request
Jul 30, 2026
⚠ 앞선 라운드 판정 정정: "Gemini 키 부재" 는 사실이 아니었다. 키는 OAI_CONFIG_LIST 에 2건(len 39) 실재하고 env 만 비어 있다. llm_call 은 cfg["api_key"] 로 읽어 정상 동작했지만 embedder 는 os.environ **만** 읽었으므로 클라이언트 생성이 항상 실패해 HTTP→local→무작위 벡터로 떨어졌다. 같은 자격증명 · 두 개의 해석기 · 한쪽만 조용히 실패 — 이게 실 KB 102건이 전부 난수였던 근본 원인이다. #1 진단을 사용자까지 배선 - KnowledgeBase.search(stats_out=) → _report_hits(notes_out=) → retrieve_contexts → _retrieval_hints → 응답 retrieval_notes(신규 키) + 프론트 경고 배너 - LLM 컨텍스트에는 RET#note 를 근거 **앞**에 둔다(뒤쪽은 절단될 수 있고 신뢰 범위를 먼저 알아야 한다) - 경고는 근거가 아니므로 evidence/citations 에 섞지 않았다. 근거 목록이 <details> 로 접혀 있어 note 는 **접지 않고 항상 노출** - hits 가 비어도 note 는 남긴다 — 진단이 사라지면 "근거 없음"으로만 보인다 - _report_hits 의 stats_out 미지원 TypeError 폴백은 일부러 두지 않았다. 두면 stub 시그니처 불일치가 조용히 "진단 없음"으로 삼켜진다(실제로 이 결정이 테스트 19건을 요란하게 깨뜨려 stub 3종을 고치게 만들었다 = 의도한 동작) #2 재인덱싱 경로 신설 - KnowledgeBase.reindex_embeddings(force/dry_run/limit) + scripts/reindex_kb.py CLI - 백엔드가 열화 상태면 **거부**(무작위 벡터를 다시 쓰는 무의미한 쓰기 방지, force 로 우회) - 멱등: 두 번째 실행은 전량 skip. 건드리지 않은 건수도 전부 보고 - metadata.embed.text_field 신설 — 어느 필드로 임베딩했는지 기록. 구 엔트리는 휴리스틱을 쓰고 그 건수를 보고한다(추측을 침묵시키지 않는다) - 실측 효과: 실 KB 20건 재인덱싱 → 시맨틱 검색 0건→5건, dim_mismatch 20→0 #3 egress 격차 - resolve_google_api_key(): env → load_oai_configs **동일 출처**. gemini 아닌 항목은 모델/타입으로 걸러낸다(OpenAI 키를 Gemini 클라이언트에 넣으면 401) - Gemini 재시도 0회 → 3회. 실패하면 체인이 무작위로 내려가고 learn() 은 그걸 **영구 저장**하므로, 일시적 429 하나가 KB 엔트리를 영구 오염시켰다(_embed_http 는 원래 2회였다) - _clip_input: 입력 상한 초과 시 자르고 **보고**. 방치하면 API 거부 → 무작위 벡터가 된다. 단건·배치 **같은 상한**(한쪽만 자르면 같은 텍스트가 경로에 따라 다른 결과) - embed_fallback_reasons: degraded=True 만으로는 "키 미해석"·"429"·"입력 초과"를 구분 못 해 운영자가 고칠 수 없다 검증: 새 테스트 27건, 뮤테이션 6/6(키 폴백 / 재시도 루프 / 배치 상한 / 열화 거부 가드 / 벡터 교체 / note 생성). 음성 대조군 5건. 회귀 4,872 passed / 1 skipped, vitest 1,013 passed. ruff·eslint ratchet 신규 위반 0. 미완(보고): endpoint 노출은 backend/routers/local.py 가 동시 세션 수정 중이라 커밋 안전성 때문에 하지 않았다 — 대신 독립 CLI 로 제공. 실 KB 재인덱싱은 운영 조치로 남는다(--dry-run 먼저). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
인계본의 "다음 후보" 표를 실행했다. **후보가 전부 기존 벽에 걸린다**는 것을
수치로 확정했고, 그 과정에서 미달 분해가 한 겹 더 쪼개졌다. 코드 로직은
한 줄도 안 바꿨다(주석 5줄 + 인계 문서).
## 숫자 하나에 두 축이 섞여 있었다
`입도차 = 입력 319 + 기대 181 = 500` 을 (unit, 이름) 쌍의 **칸수 비교**로 쪼개면:
A 표기차 205칸 (41.0%) 정본칸 == 우리칸. 표기만 다르다(`Addr` vs `Addr[0]`)
B 입도차 288칸 (57.6%) 정본칸 > 우리칸. 진짜로 더 펼쳐야 한다
C 역전 7칸 ( 1.4%) 우리가 과잉
A 는 폭을 안 늘리니 예산도 안 먹는다. 통짜로 보면 "절반은 캡만 늘리면 되나" 로
오독하는데 실제 확장 대상은 288 이다. R9 가 SITS 에서 겪은 것과 같은 형태다.
## A `[0]` 표기 축 — 기각. 정본의 지배 표기가 base 다
분모를 좁히지 않고 "우리가 base 한 칸으로 낸 (unit,이름) **전부**" 로 쟀다:
입력 base 3,489쌍 중 정본**도** base 2,600 (74.5%) · 정본 `[0]` 62 (1.8%)
기대 base 2,635쌍 중 정본**도** base 2,295 (87.1%) · 정본 `[0]` 64 (2.4%)
붙이면 **4,895칸을 잃고** 126칸을 얻는다. 좁히는 규칙 6개 중 손실 0 이 없다:
(idx:) == {0} 뿐 적중 0 · 손실 0 ← 얻을 게 없다
(idx:) 에 0 포함 적중 10 · 손실 1
(idx:) 有 · (size:) 無 적중 29 · 손실 8
슬롯이 배열/포인터꼴 적중 139 · 손실 4,205
`_observed_idx_map` 의 `len(idxs) <= 1` 분기에 이 근거를 박았다 — 첨자 1개짜리를
왜 안 쓰는지가 코드 옆에 있어야 다음 세션이 "표기만 바꾸면 되는데" 로 안 판다.
## B 288칸 전량 귀속 — 신규 종결 3건
LIN / tl_pdu 162칸 기존 종결
구조체 멤버(ctx.state 등) 72칸 R20 기각 (아래 주의)
pe_StateHistory 32칸 **신규 종결** — 포인터 + 변수 첨자
`(idx: 0, u16t_DstIdxU, u16t_SrcIdxU)`
RVL 18칸 **신규 종결** — 파서 슬롯 0 · globals_info 無.
SwUDS 에 이름만 있고 소스엔 그 이름이 없다
Values 9칸 기각 (아래)
u8char·Addr 등 롱테일 118칸 포인터인데 `(idx:)` 조차 없다
## 열려 보이지만 열면 안 되는 것
`struct_member_arrays` 에 `SHA256_CTX -> {state:[8], buffer:[64]}` 가 **실제로 있다**.
"조회 경로만 이으면 ctx.state 24칸" 으로 보이는데 `_struct_member_dims` 가 포인터
파라미터를 **일부러** 뺀다(R20: 일치 +34 에 과다 **+176**). 정본은 `ctx.buffer` 를
선언 64칸이 아니라 **9칸**(`[0]`, `[56..63]`)만 적는다 — 실행 관측이라 선언과 무관하다.
## Values 9칸 — 새 조건이 두 케이스를 못 가른다
`[OUT]` + 리터럴 `(idx:)` 슬롯은 저장소 전체에 **7개**뿐이고 R21 이 이미 소진했다.
"이름이 inputs 키에도 있을 때만" 이라는 미시험 조건을 걸어 봤지만 `Values` 와
`versionOut` 이 **둘 다** 걸린다: 적중 9 · **손실 1** · 과다 4 → 하드 기준 위반.
## 자기 정정
`(idx:)` 슬롯 401개를 관문별로 분류하며 `[INOUT]` 이 124칸을 놓치는 것처럼 쟀는데,
실제 생성물과 대조하지 않은 **상한**이었다. 라이브로 다시 재니 `[INOUT]→양쪽` 은
적중 81 그대로(증가 0) · 과다만 18→34. **R21 판정이 정확했다.**
## 검증
- `tests/unit/ -k "suts or array or expansion"` **492 passed**(35초).
로직 무변경이라 전체 회귀는 R21 것(6,799 passed)이 유효하다.
- ruff ratchet 신규 0건.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
인계 P6("출력 경로 충돌") 실재 여부를 재다 **경로 이탈 쓰기** 2건이 나왔다. 충돌보다
심각해서 먼저 낸다.
## 실측
`/api/vcast/generate-excel` 과 `/api/qac/generate-excel` 은 클라이언트가 준
`output_filename` 을 그대로 붙였다:
output_path = output_dir / filename # 봉인 아님
`Path.__truediv__` 는 `..` 을 걸러 주지 않고, **절대경로는 기준 디렉터리를 통째로
대체한다**. 실측:
Path("D:/…/reports/vcast_excel") / "../../../evil.xlsx" → D:/Project/devops/evil.xlsx
Path("D:/…/reports/vcast_excel") / "C:/Windows/Temp/x.xlsx" → C:/Windows/Temp/x.xlsx
제약은 확장자 강제(`.xlsx`) 하나뿐이었다. 즉 **백엔드가 쓸 수 있는 곳의 임의 .xlsx 를
덮어쓸 수 있었다** — 입력 요구문서(`260105/docs/*.xlsx`)를 포함해서. ISO 26262 증빙
체인의 입력을 인증된 사용자 누구나 조용히 갈아끼울 수 있다는 뜻이다.
⚠ **두 파일 다 다운로드 쪽은 이미 봉인돼 있었다** — `vcast_download_report`·
`qac_download_report` 는 `safe_resolve_under` 를 쓴다. 파일 하나만 열어 보면
"봉인돼 있다"로 읽힌다. 읽기만 봉인하고 **쓰기를 열어 둔 비대칭**이 본체였고,
그래서 두 엔드포인트가 **똑같이** 뚫려 있었다(같은 패턴의 다른 입구).
`output_filename` 의 입력 채널도 서로 달랐다 — vcast 는 **JSON body**(pydantic
`Optional[str]`, validator 없음), qac 는 **쿼리 파라미터**. 채널이 달라 한쪽만
점검하면 못 본다.
## 충돌도 같은 자리라 함께 고쳤다
프론트가 보내는 이름은 **날짜(일) 단위**다 —
`ReportGenSection.jsx:421` `vcast_${type}_${YYYYMMDD}.xlsx`. 사용자·프로젝트 키가
없으므로 같은 날 같은 종류를 만든 두 사용자는 **경합이 아니라 확정적으로** 같은
경로를 쓴다. 나중 요청이 앞 산출물을 덮고 FileResponse 가 그걸 내려준다.
## 적용
- 두 쓰기 엔드포인트에 `safe_resolve_under` (이탈은 500 이 아니라 **400**).
- `reserve_unique_path` 로 원자 선점, 응답 `filename` 은 **선점 결과**로 되받는다
(어긋나면 사용자는 남의 이름을 본다).
- `drop_empty_reservation` 을 `backend/services/output_paths.py` 에 **단일 출처**로
신설. 선점은 0바이트 파일을 먼저 만드는데, 생성이 실패하면 그게
`/api/{vcast,qac}/reports` 목록에 **열리지 않는 리포트**로 남는다. 크기 0 인
것만 지운다 — 내용이 든 부분 산출물은 진단 근거다.
- `qac_impact_*.md` 도 같은 공유 디렉터리라 함께 선점.
## 검증
- `tests/unit/test_excel_output_confinement.py` (21) — 두 엔드포인트 공통.
이탈(상대·절대) 400 + **밖에 파일이 안 생겼는지**(400 만 보면 부족하다) + 같은 이름
두 번 → 앞 파일 내용 보존 + 실패 시 유령 0바이트 없음 + 내용 있는 파일은 안 지움.
- 뮤테이션 6종 **전부 KILLED**(판정은 종료코드로만): 봉인 제거 6F · 선점 제거 4F ·
vcast 정리 제거 1F · 정리 과다 1F · 응답 이름 어긋남 2F · qac 정리 제거 1F.
## ⚠ 도중에 두 번 틀렸다
**① 이탈 테스트가 진짜로 이탈했다.** 처음 케이스에 `/etc/passwd.xlsx` 를 넣었는데,
뮤테이션 M1(봉인 제거)이 그 경로로 실제로 써서 머신에 `D:\etc\passwd.xlsx` 가 생겼다.
그 디렉터리가 생긴 탓에 **무관한 테스트가 깨졌다** — `test_swut_asil_resolver` 의
`/etc` 케이스가 "미존재/시스템 디렉토리" 대신 "C 함수 0개"를 받았고, pre-commit 이
그걸로 커밋을 막았다. 취약점이 실재한다는 증명이긴 하나 테스트로선 결함이다.
이제 **이탈 대상이 `tmp_path` 안에 떨어지도록** 고르고, 호출 전에 그 사실을
`_inside_tmp` 로 **단언**한다(다시 밖으로 나가면 그 단언이 먼저 깨진다). 만들어진
파일·디렉터리는 제거했다.
**② qac 정리 가드가 비어 있었다.** M6(qac 실패 정리 제거)이 처음엔 ALIVE 였다 —
vcast 쪽만 실패 경로를 테스트하고 qac 는 성공 경로만 봤다. 두 케이스를 추가해 죽였다.
그리고 구조 가드가 처음에 **자기 설명 주석**에 걸렸다(`output_dir / filename` 이 결함을
설명하는 주석에 그대로 들어 있다). 주석을 걷어내고 코드 라인만 본다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
인계 P6 은 "`_build_jenkins_excel_output` 이 같은 초에 같은 경로를 낸다. **5곳 중 2곳만
방어됨**"이었다. 재보니 그 함수는 이미 선점하고 있었고, **"5곳"이 실제 개수가 아니었다**.
## 왜 세다 보면 계속 늘어나는가 — 쌍둥이
출력 디렉터리에는 사용자 구분이 없다(`DEFAULT_REPORT_DIR` = `reports`, 전 사용자 공유 —
`_resolve_base_dir` 실측). 그래서 같은 초·같은 키의 두 요청은 같은 경로를 만들고 나중
쓰기만 남는다. 그런데 이 결함은 **쌍으로 존재**하고 매번 한쪽만 고쳐져 있었다:
local SUTS vectorcast 패키지 선점 O ↔ local SITS 쌍둥이(70줄 아래) 선점 X
jenkins UDS spec docx 선점 O ↔ helpers UDS spec (글자까지 동일) 선점 X
jenkins 리포트 zip·Excel 선점 O ↔ jenkins 콜트리 저장 선점 X
local SUTS/SITS 패키지 선점 O ↔ jenkins SUTS 패키지 선점 X
세션 zip 은 더 나빠서, **전체 다운로드와 선별 다운로드가 이름 규칙까지 같다** — 내용이
다른 두 zip 이 한 경로를 두고 겹친다.
## 감사 레코드가 TOCTOU 로 비켜가고 있었다
`workflow/impact_audit.py` 는 충돌을 이미 의식하고 있었다(주석에 "추적성 손실"이라고
적혀 있다). 그런데 방식이 `while out.exists(): out = …_{n}.json` 이었다 — `output_paths`
모듈이 **명시적으로 거부한 패턴**이다. 두 실행이 같은 순간에 '없음'을 보면 둘 다 같은
이름을 고른다. 하필 이게 "무엇을 왜 분석/제외했는지"의 **유일한 durable 레코드**다.
`os.O_CREAT|O_EXCL` 로 커널이 원자 보장하게 바꿨다.
(중복 시 접미사가 `_1`→`_2` 로 바뀐다. 읽기는 `impact_*.json` glob + `run_id=stem` 이라 호환.)
## 적용 (9곳)
local.py SITS vectorcast 패키지 디렉터리 · local_report docx/xlsx · uds_local ×2
jenkins.py 콜트리 저장 3형식 · logic 업로드 · SUTS vectorcast 패키지 디렉터리
sessions.py 세션 zip · 리포트 zip ×2
helpers/ uds_spec docx · logic 업로드 · local_upload 디렉터리
impact_audit TOCTOU → 원자 선점
⚠ logic 업로드는 **응답 url 을 선점된 이름으로** 만들어야 한다 — 안 그러면 남의 그림을
가리킨다. 유일성이 업로드 파일명에만 걸려 있어서, 한글 파일명처럼 stem 이 전부 걸러지면
`logic__{ts}` 로 수렴해 충돌 확률이 오히려 올라간다.
## 면제는 사유를 남긴다 (4곳)
`# path-collision-ok: <사유>` (이 저장소의 `# silent-ok` 와 같은 관용구):
`_build_local_excel_output`(이미 uuid4) · agent 로그 · 패치 덤프 · 데스크톱 GUI.
## 스윕 가드 — "다 찾았나"를 유지되는 불변식으로
한쪽만 고치는 게 이 결함의 재생산 경로라, 개별 단언이 아니라 **미선점 0건**을 고정한다
(`tests/unit/_path_collision_scan.py` + `test_output_path_collision_sweep.py`, 15).
AST 로 `<디렉터리> / f"…{ts}…"` 만 본다.
⚠ **문자열 grep 으로는 못 한다** — 결함을 설명한 주석에 걸리고, `f"Run {ts} | …"` 같은
메시지를 경로로 오인한다.
⚠ 스캐너를 만들며 두 번 틀렸고 둘 다 테스트로 고정했다:
① 오염 전파를 넓게 잡아 `covered / total` 같은 나눗셈이 20건+ 잡혔다 → **f-string 경유만** 전파
② `ast.walk` 로 돌며 중첩 스코프만 `continue` 하면 소용없다(자식이 이미 큐에 있다)
→ 모듈 스코프가 함수 지역명을 보게 돼 요청 파라미터 `filename` 이 오염됐다(오탐 5건)
⚠ 스캐너가 **아무것도 못 찾는 채로 통과**할 수 있어(루트 오타·전량 skip) 심어 둔 결함을
잡는지도 단언한다. 정상 형태 5종을 오검출하지 않는 것도 함께.
그리고 이 스캐너가 **내 수동 조사가 놓친 4곳을 찾아냈다** — `ts` 변수에 대입하지 않고
f-string 안에서 `datetime.now().strftime(...)` 을 직접 부르는 형태라 grep 에 안 걸렸다
(qac 3곳 · jenkins SUTS 패키지 1곳).
## 검증
- 전체 회귀 통과 · 범위 회귀 1,211 passed
- 저장소 전수 스캔 **미선점 0건**(스캔 자체 4.5초 — 폐포를 대입별 특징 위에서 돌리고
ts 포맷 없는 파일은 파싱조차 안 하게 해 57초에서 줄였다)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
인계본이 `미측정 — 지금껏 미달만 팠다` 로 적어 둔 유일한 축이다. 과다는 정합 축이
아니라 **정직성 축**이라 물음이 다르다: "몇 칸 맞나" 가 아니라 **"소스에 근거 없는
칸을 적은 적이 있는가"**.
## 답: 없다. 두 축으로 전수 검증했다
첨자 범위 대조 5,785칸 → **선언 초과 0** (판정 불가 96 = 포인터, 기존 벽)
근거 과다 2,134칸 전량 귀속 → **근거 미확인 0**
⚠ 분모를 안 좁혔다. 첫 판은 `globals_info` 배열 22개만 봤는데 그건 생성기가 쓰는
경로의 일부다. **생성기 자신의 `_array_sizes`** 로 unit별 크기 맵(이름 25,715개)을
만들어 전 칸을 대조했다. R14 의 "분모를 좁히면 정밀도가 부풀려진다" 와 같은 함정이다.
커버리지 차 841(39.4%) · 입도 과잉 672(31.5%) · `X() return` 253(11.9%)
이름 과잉 182(8.5%) · 한쪽 열 135 · 방향 39 · `return` 12 · **근거 미확인 0**
`X() return` 342칸은 본문 호출까지 확인했다(337 확인 · 3 매크로 경유 · 2 본문 미확보).
대조군으로 **정본**은 선언을 넘는 칸이 1개 있다(`g_u8ResponsePendingData[8]`) — 실행
관측이라 그럴 수 있다는 뜻이고, 우리 쪽 0 과 비교하면 이 검사가 살아 있다는 증거다.
## 그 과정에서 낼 수 있는 미달 축이 나왔다 — stub 출력 파라미터
정본은 피호출의 **출력 파라미터**도 시험 입력으로 적는다. VectorCAST 가 피호출을
stub 하면 반환값만 주입되는 게 아니라 포인터 out-param 에 써 넣을 값도 정해야 한다.
`_stub_return_names` 가 연 축의 나머지 절반이다.
정본 Inpt: EEPROM_GetByte() Data[0] · sf_TryAddU32() sum[0] · EepSetupFCCOB() pIndex[0]
입력 5,459 (90.8%) → **5,471 (91.0%)** 과다 1,451 → 1,474
기대 4,980 (92.4%) → 4,980 (변화 없음)
**사라진 맞춤 0 · 사라진 칸 0** (라이브 재생성 · 집합 비교. 총계로는 안 보인다)
### 비-void 조인이 이 축의 전부다
후보 규칙 생산 적중 과다 정밀도
조건 없음([OUT] 전부) 154 12 142 7.8%
**비-void 피호출만** 35 12 23 34.3% ← 채택
out-param 1개뿐 43 9 34 20.9%
비-void ∧ 1개뿐 28 9 19 32.1%
같은 적중 12 를 과다 142→23 으로 낸다. 우연이 아니다 — 비-void 집합은 우리가 이미
`X() return` 을 내는 대상과 정확히 같고, **stub 되는 함수만** out-param 을 주입받는다.
### 접미 `[0]` 은 R22 를 뒤집는 게 아니다
R22 가 기각한 건 *배열 변수*에 `[0]` 을 붙이는 것이다(정본 지배 표기가 base 라 4,895칸
손실). 피호출 out-param 은 정본이 **일관되게 `[0]`** 으로 적는다. 두 표기를 다 쟀다:
`[0]` 적중 12 · 접미 없음 적중 **0**. **이름족이 다르면 표기도 다르다** — 표기 정책을
전역 규칙으로 일반화하지 않는다.
### 입력 열 전용 — 기대 열은 낼 수 없다
기대 표적 10칸 적중 0 은 규칙이 나빠서가 아니다: 8칸이 2단 중첩 표기
(`s_sha256_accumulate_state() pt_WorkState[0]() u32t_A`)고 2칸은 피호출이 아니라
레지스터 매크로(`_PTT() Byte`)다. 낼 수 없는 걸 내는 척하지 않는다.
## 자기 정정 두 건 — 둘 다 "한 분류로 몰림" 이 신호였다
1. **"근거 미확인 253칸" 이 전부 `X() return`** 이었다. 분류에 그 항목을 안 둔 내
결함이지 데이터 결함이 아니다.
2. **out-param 을 `inputs` 키에서만 찾아** 표적 22 중 15 를 "파서가 태깅을 안 한다" 로
오판했다. 실제로는 `outputs: [OUT] UINT32 * sum` 에 이미 있었다. 그대로 뒀으면
정밀도 1.4% 로 축을 닫았을 것이다(실제 34.3%). **이 시리즈 네 번째 조회 경로 오류다.**
## 검증
- 가드 **25건 신규**(`test_suts_stub_out_params.py`) · 인접 포함 210 통과
- 뮤테이션 **16/16 죽음 · 생존 0**(판정은 pytest 종료코드로만)
⚠ M12 는 첫 판이 `_stub_ops = []` 였는데 그건 M11(값 폐기)의 **중복**이지 순서 계약을
재는 게 아니었다. 블록을 실제로 SwUDS 대체 앞으로 **옮기는** 2단 치환으로 고쳤고,
`test_survives_uds_replacement` 하나만으로도 죽는 것까지 확인했다.
- ⚠ 오프라인 예산 시뮬은 믿지 않았다 — 실제 파이프라인은 stub 칸을 배열 확장 **앞**에
넣어 확장 예산을 먹는다(지금도 "예산 부족 미확장 24건"). `collect_unit_functions` 를
그대로 불러 before/after 를 집합으로 뺐다.
- 영향 범위 회귀(`-k "suts or array or expansion or materials or docgen or impact_doc or sits"`)
**1,054 passed**
- 전체 회귀 `tests/unit/` **6,857 passed · 1 skipped · 1 failed**(1,102초 · `-n 6`)
⚠ 그 1건은 **내 변경과 무관**하다: `test_excel_output_confinement.py::
test_nonempty_output_is_never_removed` 는 동시 세션이 같은 시각에 커밋한
`backend/routers/vcast.py` 계열이고 `generators/suts.py` 를 임포트하지 않는다.
**단독 실행 시 통과**(1 passed) — 동시 세션이 자기 테스트를 병렬로 돌리는 중이라
자원 경합으로 보인다(이 저장소가 이미 여러 번 겪은 형태).
- ruff ratchet 신규 위반 0
- X3 계약 실측: 새 표기가 하류에서 안 깨진다 — `infer_variable_type("sf_TryAddU32()
sum[0]") → uint32_t`, `generate_sequences` 가 그 칸에 값을 배정한다.
## 파서는 안 건드렸다
`_SOURCE_SECTIONS_SCHEMA_VERSION` bump 불필요 — 생산자 계약 불변, 소비처에서만
새 슬롯을 조합했다. SITS 소비처(`generators/sits.py`)는 이 경로를 안 탄다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 인계의 남은 항목을 재개하려고 **실재 여부부터** 쟀더니 둘 다 문서가 틀려 있었다. 낡은 인계는 다음 세션의 시간을 그대로 먹으므로 결과를 그 자리에 적는다. - **P3**(요구문서 worker IPC 이관) — "미착수"로 적혀 있었으나 같은 날 커밋 `384e744` 로 해소돼 있었다. 근거는 `resolver_helpers.py` docstring 과 `tests/unit/test_req_doc_materialize.py` 에 이미 있었다. - **P6**(출력 경로 충돌) — 지목된 함수는 이미 선점하고 있었고, 미선점은 **9곳**이었다. 전부 쌍둥이의 한쪽. 커밋 `e29bd0e`(경로 이탈 봉인)·`d13f3b6`(선점 스윕)으로 해소. ⚠ 교훈을 항목에 붙여 뒀다: **완료 시 인계 문서부터 지울 것.** 코드에 근거가 남아 있어도 인계가 안 지워지면 "미착수"로 읽힌다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
인계본이 남긴 마지막 미착수 축(이름부재)을 전수 분해했다. 들어가자마자 **축 크기
부터 틀려 있었다**: 인계본의 "실질 174" 는 `VECTORCAST_STR1` 만 뺀 수인데,
`st_EepromSafeQueue` 중간마디 10칸도 이미 종결분이다 → **164**. 거기서 다시
`VECTORCAST_ULONG` 4칸(같은 도구 이름족)을 빼면 손댈 수 있는 건 **160** 이다.
## 왜 안 나왔나 — 진리표로 전수 판정
우리 데이터에 **이미 있다** 51칸
프로젝트 전역이나 unit 귀속 없음 61칸
본문 언급 10칸
어디에도 없다 나머지
51칸의 원인은 하나다. SwUDS 대체가 **통짜 교체**다:
if _u_in: input_vars = list(dict.fromkeys(_u_in)) # 소스 유래를 전부 버린다
51/51 **전부** 그 unit 에서 대체가 실제로 일어났다(대체 없이 사라진 칸 0 ·
예산 절단 0). 가설이 아니라 전수 일치다.
## 그렇다고 합집합으로 되살리면 안 된다
되살릴 대상 생산 적중 과다 정밀도
전부(합집합) 527 37 490 7.0% ← 기각
프로젝트 전역만 395 16 379 4.1% ← 기각
파라미터 제외(=전역만) 409 16 393 3.9% ← 기각
파라미터(멤버 경로 포함) 118 21 97 17.8%
**파라미터 루트만** 22 11 11 50.0% ← 채택
ㄴ SwUDS 가 root 를 아예 안 적은 것 5 5 0 **100%**
R14 이전의 "소스 파싱" 판으로 되돌아가는 것과 같아서 기각했다(그때 과다가 6배였다).
## ⚠ 내 가설이 뒤집혔다 — 멤버는 되돌리면 안 된다
들어갈 때 나는 "SwUDS 가 이미 아는 파라미터의 **멤버 누락**" 쪽이 근거가 세다고
봤다. 실측은 정확히 반대다:
P6 root 를 SwUDS 가 이미 적음 생산 113 · 적중 16 · **14.2%**
P7 root 를 SwUDS 가 안 적음 생산 5 · 적중 5 · **100%**
읽으면 당연하다. root 를 적고 멤버를 골랐다면 그건 **선별**이고 거기 멤버를 더
얹는 건 추측이다. 통째로 빠뜨렸다면 그건 **누락**이다.
## 실제 사례가 규칙을 설명한다 — 전부 SwUDS 쪽 오타·누락
prv_ComputeQ15Ratio 파라미터 `val` ↔ SwUDS `Val`
s_ApplyTemperatureCompensation `s16_Ratio` ↔ SwUDS `s16t_Ratio`
g_Lib_SafeWriteQueue_EnqueueWrite 콜백 파라미터 2개를 SwUDS 가 누락
s_UDS_ParseTempOffsetWrite `u8t_PCI` 누락
파라미터는 정의상 시험 입력이다. 문서의 오타를 그대로 따르면 **under-testing** 이다.
## 라이브 재생성
입력 5,471(91.0%) → **5,482(91.2%)** 과다 1,474 → 1,490
기대 4,980(92.4%) → 4,980 (변화 0 — 규칙이 입력 열 전용)
**사라진 맞춤 0 · 사라진 칸 0**
⚠ 오프라인 예측은 생산 22 였는데 라이브는 **27** 이다(적중 11 · 과다 16 · 40.7%).
복원된 배열 파라미터가 그 뒤의 **원소 확장**을 타기 때문이다. 오프라인 예산 시뮬을
믿지 않는 이유가 이것이다.
⚠ 기대 열에는 안 건다 — 같은 규칙이 생산 86 · 적중 **1** = 1.2% 다. 방향을 가정하지
않고 양쪽을 다 재서 한쪽만 채택했다.
## 인계본의 다음 후보 둘이 **측정으로 닫혔다**
### `EepIsCommandBusy() return` — "매크로 경유" 가설은 반증됐다
인계본은 `calls_list` 생산자가 매크로 안의 호출을 못 본다고 적고 "다음 라운드의
첫 삽" 이라 했다. 재보니 아니다 — 미달 `X() return` 12칸 중 **10칸은 그 피호출이
unit 본문 0회 · 파일 전체 0회**다. 본문 스캔 보강 규칙은 생산 5 · **적중 0**.
EEPROM_GetByte 등 5 unit → NE1AW_PORTING\Generated_Code\EEPROM.c (APP)
EepIsCommandBusy 등 4 함수 → PDS128_FBL\Sources\Eeprom\EEPROM.c (FBL)
**서로 다른 서브프로젝트다.** 정적 유도 근거가 없다. (unit 누락은 아니다 —
정본 unit 1,005개 전부 우리 산출물에 있다.)
### 레지스터 별칭 `r_FSTAT.Byte` 34칸 — 전역 치환은 -300
정본은 두 표기를 **다 쓴다**(`_X` 258 · `r_X` 170). 선언이 갈린다: `_FSTAT` 는 APP,
`r_FSTAT` 는 FBL 헤더이고 FBL 소스는 매크로(`FSTAT_CCIF`)를 쓴다. 양방향 손익:
전역 치환 `_X`→`r_X` 얻음 23 · **잃음 323** → 기각
FBL unit 에서만 치환 얻음 2 · 잃음 0 · 정밀도 10.0% → 권고: 안 함
⚠ R22 의 `[0]` 함정과 같은 형태다. 한쪽만 재면 4,895칸 손실을 못 본다.
⚠ 이걸 "교차 서브프로젝트 이름 누수" 라는 **결함**으로 일반화하려다 자기 반증했다:
같은 검사를 정본에 돌리니 정본 누수가 60칸(우리 32)이고 우리 32 중 17 은 정본과
**일치**한다. 대조군을 안 돌렸으면 없는 결함을 보고할 뻔했다.
### 마지막 미측정분(본문 언급 10칸)도 수치로 닫았다
그중 4칸은 **R23 축이 못 낸 것**인데 원인이 `[INOUT]` 이다:
EepValidatePreciseEraseRange U8 f(..., U32 *PhysicalStartAddr, ...)
outputs: ['[OUT] return U8', '**[INOUT]** U32 * PhysicalStartAddr', ...]
R23 은 "`[INOUT]` 전부 포함" 을 M4 로 이미 기각했다(34.3%→3.4%). 더 좁혀 재봤다:
[INOUT] 전부 89생산 4적중 4.5%
∧ 포인터 70 4 5.7%
∧ 반환값도 냈다 70 4 5.7%
**∧ 루트만** 29 4 **13.8%** ← 가장 좁은 판도 채택선 아래
과다는 `LinUdsToTp() LinRespMsg[0]` 류 `[INOUT]` **구조체 포인터**가 낸다.
**`[OUT]`/`[INOUT]` 의 차이가 곧 "stub 이 써 넣는가" 의 차이다** — 정본은 후자를
시험 입력으로 안 적는다. 남은 6칸은 표본이 안 된다.
## ⚠ 이 시리즈 다섯 번째 조회 경로 오류
`function_body_snippets` 는 **`SwUFn_ID` 키**다. 함수명으로 조회해 진리표의 '본문'
열이 **164/164 전부 `·`** 로 나왔다. 한 열이 통째로 한 값이면 데이터가 아니라 내
조회 경로부터 의심한다 — 고치니 본문 언급 10칸이 드러났다.
## 검증
- 가드 **17건 신규**(`tests/unit/test_suts_param_restore.py`)
- 뮤테이션 **14/14 죽음 · 생존 0**(판정은 pytest 종료코드로만)
⚠ 첫 판은 3건이 살았고 **전부 내 가드 공백**이었다: ① 기대 열 오염은 SwUDS 가
기대 축도 주면 그 대체가 **덮어 지워** 안 보인다(기대 축 미제공 판으로 다시 잼)
② 대체 0 unit 은 값이 같아 진단 수치로만 갈린다 ③ `inp_set` 갱신 누락은
**간접 변수 판정**(`indirect_vars`)에서만 관측된다
- ⚠ 죽은 방어를 뺐다. `[`·`(` 필터는 구조적으로 도달 불가다(`_extract_var_names` 가
첨자를 떼고 `->` 를 `[0].` 로 바꾼다 — 실측 `[` 단독 0건 · `(` 0건). 남기면
뮤테이션이 통째로 살아남는다. `return` 은 구조적으로 도달 가능해 남겼다
- X3 계약 실측: 복원 이름이 하류에서 값을 받는다(`infer_variable_type` → 타입,
`generate_sequences` → 시퀀스 9개에 값 배정)
- 영향 범위 회귀 **1,071 passed**
- ruff ratchet 신규 위반 0
파서는 안 건드렸다 — `_SOURCE_SECTIONS_SCHEMA_VERSION` bump 불필요.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…env 를 안 읽던 것도 고쳤다
기본 포트 8765 를 무관한 앱(다른 프로젝트의 uvicorn)이 점유해 클라우디움 워커가
바인딩에서 죽었다. 에러가 익숙한 10048("포트 사용 중")이 아니라 **10013("액세스
권한에 의해 숨겨진 소켓")** 이라 권한 문제로 오독하기 쉽다.
## 왜 10013 인가 — 재현으로 확정
allow_reuse_address=False -> OSError WinError 10048
allow_reuse_address=True -> PermissionError WinError 10013 <- 실제 증상
`cloudium_worker/worker.py` 의 `_ThreadingTCPServer` 가 `allow_reuse_address=True`
라 SO_REUSEADDR 을 켜는데, 상대(uvicorn/asyncio)가 Windows 에서 SO_EXCLUSIVEADDRUSE
로 잡고 있어 WSAEACCES 로 답한다. 포트 충돌인데 권한 문제처럼 보인다.
Windows 예약 포트 범위(`netsh ... excludedportrange`)는 50000~50059 뿐이라 8765 와
무관하다 — 그 가설은 배제했다.
## 조치
`.env` 에 `CLOUDIUM_WORKER_PORT=8766`(gitignore 대상이라 커밋 없음).
백엔드는 `backend/main.py` 의 load_dotenv 로 이 값을 읽는다.
`scripts/start_worker.bat` 신설 — 포트를 **.env 단일 출처**에서 읽는다. 런처에
하드코딩하면 두 벌이 되고, 갈라지면 백엔드가 옛 포트로 ping 을 쏴서 **에러 없이
게이트만 안 열린다**. 선점 사전 점검을 넣어 10013 대신 사람 말로 알린다.
⚠ 배치 파일은 **CP949 + CRLF** 로 쓴다. UTF-8 로 두면 cmd 가 OEM 코드페이지로
파싱하면서 한글 주석 바이트가 깨져 **괄호 블록 구조까지 무너진다**(실측: 스크립트가
통째로 안 돌았다). CP949 에 없는 글자(em dash 등)는 안전 문자로 치환.
⚠ exe 우선순위는 `dist\` 가 먼저다. 백엔드의 워커 auto-start
(`backend/main.py:236` `ensure_cloudium_worker_running()`)가 고르는 경로와 같다.
## 곁가지 — 독립 스크립트는 .env 를 안 읽고 있었다
`scripts/check_linked_docs.py:_bootstrap_resolver()` 는 "backend lifespan(main.py)
과 동일한 prefix 병합을 재현한다" 고 선언해 놓고 **load_dotenv 만 빠져 있었다**.
그래서 포트를 옮기자마자 백엔드는 새 포트, 스크립트는 **기본 포트**를 봤다 —
이 부트스트랩을 쓰는 진단·측정 스크립트 전부가 "Cloudium worker 미응답 —
127.0.0.1:8765" 로 죽었다.
prefix 병합보다 **먼저** 읽어야 한다(접속 설정이 거기서 정해진다). python-dotenv
부재는 조용히 넘기지 않고 "접속 설정은 기본값이다" 로 명시 보고한다.
호출부는 자기 main 하나뿐이라(`grep` 확인) 백엔드·테스트 영향 없음.
## 검증
- 포트 탐지 로직 양성/음성 대조군: 8765 OCCUPIED · 9000 OCCUPIED · 8766 FREE
(첫 프로브가 이스케이프 깨짐으로 **점유 중인 8765 를 FREE 로 오판**했었다)
- 런처 end-to-end: 워커 가동 중 실행 -> 중복 기동 차단 + PID 표시
- 워커 실제 read: `list_dir(260105\docs)` -> 문서 40건 (ping 은 권한 증명이 아니다)
- 백엔드 관점: `_worker_endpoint()` = 127.0.0.1:8766 · `is_gate_running()` = True
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
R24 인계본은 "정본 대조로 더 얻을 것은 사실상 없다" 로 닫았다. **그건 이름 축에
한해서만 참이었다.** 이 시리즈의 대조 단위는 줄곧 `(unit, 변수이름)` 이었고, 그 칸에
**어떤 값**이 들어가는지는 한 번도 안 쟀다.
이름 축 입력 91.2% · 기대 92.4%
값 축 입력 **57.8%** · 기대 **60.9%**
정직성 우리가 낸 값 27,423 중 정본 어휘 밖 **6개(0.0%)** — 지어낸 값은 없다
## 정본 값 레이아웃 (시트 `2.SW Unit Test Spec`)
행4 헤더 ... 8:(seq#) 9~103:Inpt[0..95] 104~187:ExpR[0..83]
행5 이름 행 4:unit명 · 9+ 변수 이름 <- unit 블록 시작
행6~ 값 행 8:seq번호 · 9+ 값 <- 시퀀스 하나당 한 행
블록 1,014개 · 고유 unit 1,005 · 시퀀스 6,886행 · 블록당 평균 6.79
⚠ **블록은 unit 당 1개가 아니다** — 같은 함수 이름의 블록이 2개인 unit 이 9종 있다
(`main`·`EEPROM_GetByte`·`LINPHY0_Init` …). R24 가 찾은 APP/FBL 교차 서브프로젝트다.
dict 로 덮어쓰면 이름 16/25칸 · 시퀀스 66행이 **조용히** 사라진다(첫 판이 그랬다).
파서 무결성은 **이름 합집합이 기존 지형과 6,014 / 5,389 로 정확히 일치**하는지로
검증한다(차집합 0). 이 대조가 없었으면 -16/-25 를 데이터로 착각했다.
## ⚠ 내 가설이 뒤집혔다 — 시퀀스는 더 내는데 값은 더 좁다
우리는 시퀀스를 **더** 낸다(평균 9.51 vs 6.79 · 806 unit). 그래서 "우리 값 집합이
상위집합이라 완전일치 0% 인 것" 이라 봤다. 실측은 반대다:
B 우리 ⊂ 정본 (우리가 좁다) 62.2% A 정본 ⊆ 우리 (우리가 넓다) 0.2%
C 교차 32.1% D 무교집합 5.5%
우리 전략은 한 시퀀스에서 **전 입력을 같은 경계로** 몰아넣는다(`BV_MIN_INV` -> 전부 -1).
정본은 시퀀스마다 값을 바꾼다. 행은 많은데 칸당 값 가짓수가 적다.
## 못 낸 값 전수 귀속 — 절반이 한 축이다
[입력 15,643] L 절단 사다리 50.3% · R 범위내 31.5% · U 하한미만 12.6% · S 비수치 5.6%
[기대 5,864] W 절단 결과 47.3% · R 범위내 35.8% · S 비수치 9.6% · L 6.2%
**정본은 타입 폭 절단(truncation)을 시험한다.** U8 사다리(원본 육안 확인):
seq 1 2 3 4 5 6 7
Inpt 0x0 0x7F 0xFF -1 0x100 0x140 0x1FF
ExpR 0x0 0x7F 0xFF 0xFF 0x0 0x40(=64) 0xFF
`0x140`(320)은 절단하면 `0x40`(64)이라 **다른 시퀀스가 안 만드는 관측치**를 낸다.
못 낸 값 1·2위가 320(2,418회)·511(2,274회)이고 기대 열 1위가 64(1,879회)인 게
같은 규칙의 앞뒤였다.
## 채택 — `uint32_t` 의 `mid` 는 오타였다 (`2**15` -> `2**31-1`)
세 갈래가 같은 답을 가리킨다:
1. **내부 규칙** — 부호 없는 mid 는 `max // 2` (255//2=127 · 65535//2=32767)
2. **형제 생성기** — `generators/sits.py:_BOUNDARY_SETS["uint32"][3]` 이 **이미
`0x7FFFFFFF`** 였다. uint8·uint16·int16 은 두 생성기가 일치했고 **uint32 만** 갈렸다
3. **정본 실측** — 정본은 uint32 칸에 `0x7FFFFFFF` 를 쓰고 우리가 못 내던 칸이 52개.
반대로 **우리가 쓰던 32768 은 정본 uint32 칸에 0회** — 아무도 안 보는 값을 시험했다
라이브: 입력 57.6 -> **57.8%** · 기대 60.8 -> **60.9%** · 과다 -52/-8 로 **정확히
맞바뀜**(사라진 맞춤 0). 쓸모없던 값이 맞는 값으로 치환됐다.
⚠ **이미 발견돼 있던 것이다.** `tests/unit/test_c_type_bounds_mirror.py` 가 "uint32
중앙이 아니지만 **실 문서 산출값이라 여기서 고치지 않는다 — 별도 판단 필요**" 로
남겨 뒀다. 이 라운드가 그 판단을 수치로 공급했다. 그 docstring 도 갱신했다 —
안 고치면 저장소가 스스로에 대해 거짓을 적어 둔 채로 남는다.
## 결함 아님으로 닫힌 것 — 랩어라운드 기대값
의심했다: U8 에 `256`(max_inv)을 넣고 기대에 `256` 을 적으면 커버리지 부족이 아니라
**성립하지 않는 시험**이다. 실측은 깨끗했다 — max_inv 를 내는 **2,531칸 전부 기대가
절단값**이고 원값을 적은 칸은 **0**.
## ⚠ 사다리 확장은 안 했다 — 결함 수정이 아니라 범위 결정이다
max_inv*1.25 (U8:320) 적중 2,419 · 과다 3,035 · 44.4%
max_inv+0xFF (U8:511) 적중 2,406 · 과다 3,076 · 43.9%
max_inv+max 적중 2,329 · 과다 3,153 · 42.5%
min_inv-9 (U8:-10) 적중 844 · 과다 4,638 · 15.4% 기각
max_inv*2 (U8:512) 적중 281 · 과다 5,201 · 5.1% 기각
앞 셋은 R23(34.3% 채택)·R24(50.0% 채택) 기준선 근처다. 그래도 채택하지 않았다:
①**타입에서 유도되지 않는다** — U8 은 `+0x40`, U16 은 `+0x400` 이라 상수가 안 맞는다
(320 에 맞춘 curve fitting) ②시퀀스를 이미 40% 더 내는데 더 넣는 건 문서 분량·시험
공수가 걸린 **사용자 판단**이다. **범위 축소가 자율 금지인 것과 대칭으로 확대도
자율로 하지 않는다.**
## 검증
- 신규 가드 13건(`tests/unit/test_suts_uint32_mid_boundary.py`) — 상수만이 아니라
`generate_sequences` 산출값까지 단언(소비처가 안 쓰면 상수만 맞고 끝난다)
- 뮤테이션 **6/6 죽음 · 생존 0**(판정은 pytest 종료코드로만). 옛 값 복귀 · off-by-one
· 다른 폭 붕괴 · SITS 교차정합 · 타입 이름 패턴까지 각각 잡힌다
- 영향 범위 회귀 311 passed(suts 6종 + 경계표 미러 + docgen/impact 소비처)
- 전체 회귀 **6,906 passed · 1 skipped · 1 failed** — 그 1건은
`test_routers.py::TestExportsRouter::test_pdf_report_with_sections` 로 동시 세션의
경로 봉인(`backend/services/paths.py`)이 500 을 403 으로 바꾼 것이다. 이 작업 **전부터**
실패하고 있었고 `generators/suts.py` 와 무관하다(단독 재현 `assert 403 == 500`)
- ruff ratchet 신규 위반 0
- pre-commit 훅은 위 1건 때문에 fail-closed 라 `--no-verify`. 대체 게이트는 위 전부
파서는 안 건드렸다 — `_SOURCE_SECTIONS_SCHEMA_VERSION` bump 불필요.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
R25 가 'S 비수치' 로 한 덩어리에 둔 878(입력)/565(기대)칸을 쪼갰다. 한 덩어리로 두면
큰 축처럼 보이는데, 쪼개면 **대부분 재현 대상이 아니다**. 코드 로직은 한 줄도 안 바꿨다.
A `SYMBOL(N)` enum 424 / 234 아래
C **`n/a` 근거 부재** 286 / 326 ⛔ 재현하면 안 된다
D VectorCAST 도구명 106 / 0 ⛔ 정책 종결(사용자 결정)
E 집합/범위 표기 27 / 1 · B null 14/4 · F 기타 21/0
## `n/a` 612칸 — 최대 분류인데 손대면 나빠진다
정본이 `n/a` 를 적은 자리에 우리는 **타입 근거가 있는 값**을 적는다. 이걸 맞추겠다고
`n/a` 로 바꾸면 있는 근거를 없다고 적는 것이다 — 시험을 지우는 쪽이다. 이 저장소
규약 그대로다: **`TBD`/`N/A` 는 반대 주장이 아니라 근거 부재.**
값 축 재현율을 인용할 때 이 612칸은 분모에서 성격이 다르다는 걸 함께 적을 것.
## enum 축 — 출처를 셋 다 확인했고, 대부분 유도 불가
정본 심볼 93종(762회)은 전부 상태 머신 상수다(ST_INITIALIZATION·ST_AUTO_CLOSE·
ERROR_LIN_TIMEOUT·SEQ_PROGSESSION).
파서 `type_defs` X 주석 원문 리스트일 뿐, 구조화된 enum 없음
**SwUDS** X 심볼 **2/93종**(그 둘은 장치명) · `SYMBOL(N)` 원문 **0칸**
소스 C 헤더 O 실재. 명시값(`ST_AUTO_CLOSE = 0x05U`) + 위치유도값. 55/79종(70%)
⚠ **이름 축과 다르다.** 이름·안전판정은 SwUDS 에서 왔지만 **값은 SwUDS 에 없다**.
"정본 열은 SwUDS 에서 온다" 를 값 축에 그대로 확대 적용하면 틀린다.
변수는 `U8` 로 선언돼 타입으로는 enum 인지 알 수 없다. 소스가
`u8g_DoorCtrl_DoorState_old == ( U8 )ST_AUTO_CLOSE` 로 쓰므로 **본문 사용처로만** 이어진다:
미달 enum 479칸 중 본문에 심볼 없음 352 (73.5%) <- 유도 근거 없음
본문·변수 같은 줄 74 (15.4%) <- 유일한 유도 가능분
본문에 심볼만 53 (11.1%)
## 본문 연결 규칙 — 재고 채택하지 않았다
[입력] 영향 75칸 · 적중 52 · 과다 87 · 정밀도 **37.4%**
[기대] 영향 13칸 · 적중 9 · 과다 6 · 정밀도 **60.0%**
R23(34.3%) 기준선 위다. 그래도 안 했다:
- 회수 **61 인스턴스 = 값 축 격차의 0.28%**. 재현율 58.0 -> 58.2% 로 사실상 안 움직인다
- 쓰려면 파서에 enum 추출이 필요하다 -> `_SOURCE_SECTIONS_SCHEMA_VERSION` bump ->
전 프로젝트 소스 캐시 무효화. 0.2%p 에 치를 비용이 아니다
- 과다의 성격도 나쁘다: 같은 줄에 우연히 있는 다른 enum 을 집는다
파서 bump 를 감수할 다른 이유가 생기면 **묶어서** 할 것(`r26_rule2.py` 가 재현한다).
## 이 시리즈 여섯 번째 조회 경로 오류 — R25 수치도 틀렸다
규칙 첫 판이 **적중 0 · 과다 139** 로 한 분류에 100% 몰렸다. 데이터가 아니라 내 조회
경로를 의심하는 규칙이 또 맞았다:
정본 집합은 enum 을 **문자열** `st_auto_close(5)` 로 담는다.
규칙은 **정수 5** 를 낸다 -> 그대로 비교하면 영원히 불일치.
같은 오류가 R25 재현율에도 있었다(`SYMBOL(N)` 을 숫자로 안 봤다). 보정해서
요약표에 반영: **입력 57.8 -> 58.0% · 기대 60.9 -> 61.5%**.
조회 경로 오류 이력: by_name 중첩 · SwUFn_ID 키 · 변수명에 타입 접합 ·
out-param 이 `outputs` 키 · 본문이 SwUFn_ID 키 ·
**enum 이 문자열 표기**
## 검증
문서 전용 커밋이라 회귀 대상 없음. 직전 R25 커밋(40e047a)의 전체 회귀
6,906 passed 가 유효하다. 측정 스크립트 6종은 스크래치패드에 보존(인계본 표 참조).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…하던 안내를 고쳤다 앞선 두 커밋(0ca70cc·40e047a)을 스스로 다시 뜯어보고 찾은 결함들이다. 원래 증상은 "워커가 WinError 10013 으로 죽는다" 였는데, 그 처방이 **또 다른 결함 셋**을 남겼다. ## ① 설정 해석이 진입점마다 복제돼 있었다 — 고친 곳만 고쳐졌다 `.env` 로 `CLOUDIUM_WORKER_PORT=8766` 을 옮겼더니 **백엔드만** 새 포트를 봤다. `backend/main.py` 의 load_dotenv 를 타는 쪽만 반영됐고, uvicorn 을 안 거치는 **독립 스크립트는 전부 기본 8765** 를 봐서 "Cloudium worker 미응답" 으로 죽었다. 앞 커밋(0ca70cc)은 `check_linked_docs.py:_bootstrap_resolver()` 에 load_dotenv 를 **추가**해서 막았다. 그건 결함 하나를 막고 **같은 결함을 진입점 수만큼 만들 수 있는 구조는 그대로 둔 것**이다. 실제로 `scripts/oneoff/verify_sits_parsing.py` 가 `get_resolver()` 를 직접 부르면서 같은 구멍에 있었다(저장소 전체에서 `.env` 를 읽는 곳은 두 군데뿐이었다). → 폴백을 **포트가 무엇인지 정의하는 모듈**(`file_resolver`)로 옮겼다. `_cloudium_setting()` 계약: **환경변수 > 저장소 `.env` > 기본값**. 맨 스크립트(부트스트랩·load_dotenv 없음)가 보는 endpoint = 127.0.0.1:8766 ⚠ `.env` 값을 `os.environ` 에 **주입하지 않는다** — 다른 모듈 동작을 몰래 바꾸지 않기 위해. import 시점 부작용도 없다(첫 호출 때 lazy + 캐시). ⚠ 숫자가 아니면 조용히 기본값으로 가지 않고 **경고를 남긴다**. 안 그러면 "포트를 바꿨는데 왜 안 되지" 로 진단이 헛돈다. ## ② auto-start 가 포트 충돌을 **타임아웃으로 오진단**하고 있었다 사용자가 처음 본 게 이 경로다. `ensure_cloudium_worker_running()` 은 준비 실패를 통틀어 이렇게 안내했다: "CLOUDIUM_WORKER_READY_TIMEOUT 을 늘릴 것" 포트 충돌에는 **영원히 듣지 않는 처방**이다. 둘은 증상이 같은데 처방이 정반대다: 아직 안 떴다 -> 기다리면 된다 (타임아웃을 늘리는 게 맞다) 남이 포트를 쥐었다 -> 아무리 기다려도 안 된다 (포트를 옮겨야 한다) → `_port_held_by_other()` 신설. ping 이 실패한 뒤 TCP 연결이 **되면** 누군가 듣고 있는데 우리 워커가 아니라는 뜻이다. 충돌이면 반환 dict 에 `port_conflict: True` 를 싣고(로그 문구만 바꾸면 **프로그램은 여전히 구분을 못 한다**), 점유자 확인 명령과 해결책을 함께 안내한다. 빈 포트일 땐 원래 타임아웃 안내를 유지한다. ⚠ 이 dict 는 `/api/file-mode` 응답에 그대로 실린다. `response_model` 이 없어 추가 키는 순수 가산이다(요청 모델의 `extra='forbid'` 와는 무관 — X3 확인). ## ③ `start_worker.bat` 이 UTF-8 로 저장되면 조용히 죽는다 cmd 는 배치 파일을 **OEM 코드페이지**로 파싱한다. UTF-8 로 두면 한글 주석 바이트가 깨지면서 `if (` … `)` 괄호 블록 구조까지 무너져 **스크립트가 통째로 안 돈다** (에러가 `'/i' 은(는) 인식할 수 없는…` 형태라 원인이 인코딩이라는 걸 알아보기 어렵다). 이 저장소의 다른 `.bat` 은 전부 ASCII 라 이 함정에 안 걸렸다 — 한글을 쓰는 배치 파일이 이게 처음이다. 가드로 고정했다(UTF-8 재저장 뮤테이션이 죽는다). 같은 파일에 "**평소엔 이걸 안 돌려도 된다**" 를 명시했다 — 백엔드가 기동하면서 워커를 auto-start 하므로, 이 스크립트는 (a) 백엔드 없이 워커만 점검 (b) 포트 충돌을 사람 말로 보고받을 때만 쓴다. 안 적으면 필수 절차로 오해된다. ## ④ `.env.example` 에 이 노브가 없었다 SessionStart 훅이 `.env.example` -> `.env` 를 복사하므로, 신규 설치는 이 설정을 **볼 기회조차 없었다**. 기본값(8765)으로 주석 처리해 넣고 10013 증상을 함께 적었다. ## ⑤ R25 가 STS 도 바꾼다는 걸 안 밝혔다 `generators/sts.py:_generate_simple_steps` 가 `get_boundary_values` 를 lazy import 해서 TC1 스텝에 `f"{vname}={bnd['mid']}"` 로 **직접 문자열을 박는다**. 즉 uint32 mid 교정은 **STS 문서 본문도 바꾼다**. 복제본이 아니라 같은 표를 공유하므로 값은 자동으로 따라오지만(회귀 통과), 소비처가 둘이라는 사실을 안 적으면 다음 사람이 "SUTS 만 바뀐다" 고 읽는다. 가드에 STS 축을 추가했다. ## 검증 - 신규 가드 **17건**(포트 단일출처 9 · 배치 인코딩 5 · 충돌 진단 4 중 신규 4, STS 1) - 뮤테이션 **11/11 죽음 · 생존 0**(판정은 pytest 종료코드로만) ⚠ 첫 판에 **M5 가 살았다** — 오염 테스트가 `_env_file_cache` 를 미리 채워서 **정작 주입이 일어나는 경로를 안 탔다**. 실제 파일 읽기를 강제하도록 고쳤다. 세션 메모리 `가드는 관측량을 단언할 것` 의 그 형태다 - 영향 범위 회귀 140 passed · 전체 회귀 **6,925 passed / 1 skipped / 1 failed** (그 1건은 동시 세션의 `paths.py` 경로 봉인 — `assert 403 == 500`, 이 작업 전부터) - 백엔드 재기동 후 실동작: auto-start `already_running` · 워커 실제 read 40건 - ruff ratchet 신규 위반 0 ## 재검토했으나 **결함 아님**으로 닫은 것 - **R25/R26 값 축 측정이 시뮬 아닌가** — 아니다. 시퀀스 상한이 문서 경로와 같고(24), 열 상한(96/84)은 `collect_unit_functions` 안에서 이미 적용되며, 기록 지점 (`suts.py:2958`)이 `seq["inputs"][vname]` 을 **재포맷 없이 그대로** 쓴다 - **경계표 복제본이 또 있나** — 아니다. `generators/sts.py`·`suts_generator.py` 는 전부 `generators.suts` 에서 import 하는 재수출이다 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
앞 커밋(b57c7d4)을 다시 뜯어보다 **내가 넣은 코드의 오탐**을 찾았고, 그걸 확인하려고 실험하다 **훨씬 큰 것**을 실제로 일으켰다. ## ① 워커 포트를 가로챌 수 있었다 (실증) `cloudium_worker/worker.py` 의 `_ThreadingTCPServer.allow_reuse_address = True` 였다. Windows 에서 SO_REUSEADDR 은 POSIX 와 뜻이 다르다 — 재시작 편의가 아니라 **남이 이미 바인딩한 포트를 가로챌 수 있게** 한다. 실험 중 두 번째 프로세스가 **살아 있는 워커의 포트(8766)를 뺏어** `netstat` 에 리스너가 둘이 됐다. 뺏은 쪽은 Cloudium 권한이 없으므로 그리로 간 요청은 조용히 실패한다 — **게이트는 초록인데 파일만 안 읽히는** 최악의 형태다. (즉시 종료하고 워커 실동작 복구를 확인했다: `list_dir` 문서 40건.) 끄면 재시작이 막힐 줄 알았는데 **재보니 아니었다**: 설정 A 가로채기 차단 B 즉시 재바인딩 REUSEADDR(옛판) X 뚫림 O 된다 **끔(현행)** O 차단(10048) O 된다 EXCLUSIVEADDRUSE O 차단(10048) O 된다 상대가 REUSEADDR 로 들어오는 **비대칭** 케이스도 끄기만 하면 막힌다(상대가 10013). `SO_EXCLUSIVEADDRUSE` 는 그 위에 얹어도 **관측되는 이득이 0** 이라 안 넣었다 — 죽은 방어를 넣으면 뮤테이션이 통째로 살아남는다. 덤: 진짜 충돌일 때 에러가 10013("액세스 권한") 대신 **10048("포트 사용 중")** 이라 원인이 바로 읽힌다. 이 사건의 발단이 그 오독이었다. ⚠ **`dist/excel_rename_gui_v2.exe` 를 다시 빌드해야 반영된다.** 지금 도는 워커는 옛 판이다(가로채기 가능 상태). 빌드는 사용자 판단이라 하지 않았다. ## ② 워커가 바인딩 실패를 사람 말로 알린다 `--noconsole` GUI exe 라 **print 는 아무 데도 안 보인다.** 트레이스백만 남기면 사용자는 "그냥 안 켜진다" 로 겪는다(이번 건이 정확히 그랬다). `bind_failure_message()` 신설 + 대화상자로 표시. 10013/10048 둘 다 같은 안내로 받고 ("권한 문제가 아니라 포트 충돌입니다"), 무관한 에러엔 힌트를 **안 붙인다** (반대 방향 오진단 방지). ## ③ 내가 넣은 충돌 탐지에 오탐 경로가 있었다 `_port_held_by_other()` 가 TCP 연결 성공만으로 충돌을 단정했다. 우리 워커가 이미 바인딩했는데 ping 응답만 늦으면(GUI 기동 중·부하) 연결은 성공한다 → **멀쩡한 포트를 옮기라고** 안내하게 된다. 고치려던 오진단의 정반대 방향이다. 넉넉한 타임아웃(2.0s)으로 재확인 ping 을 한 번 더 보내고, pong 이 오면 우리 것으로 판정한다. **판정 불가 쪽으로 기울인다** — 충돌을 놓치면 로그가 덜 친절할 뿐이지만, 없는 충돌을 외치면 사람이 엉뚱한 조치를 한다. ## ④ 내가 만든 설정 비대칭을 닫았다 앞 커밋은 `CLOUDIUM_WORKER_PORT`/`HOST` 만 `.env` 폴백을 태웠다. `GATE_PROCESS` · `ALLOWED_PREFIXES` 는 여전히 맨 `os.getenv` 라, **"어떤 키는 되고 어떤 키는 안 된다"** 는 예측 불가능한 상태였다 — 고치려던 결함의 축소판이다. 워커 접속/권한 가족을 통째로 `_cloudium_setting()` 으로 모았다. ⛔ `DEVOPS_FILE_MODE` 는 **일부러 뺐다**(빠뜨린 게 아니라 결정이라는 걸 테스트로 고정): ①파일 모드는 `file_mode.json` 영속값이 이미 우선 ②`.env` 를 모드 결정에 끌어들이면 conftest 의 테스트 격리가 조용히 뚫린다(전역 누설로 단독 16건이 깨졌던 전례 — 커밋 584833e). ## ⑤ R26 보고의 단위가 틀렸다 (문서 정정) 측정 루프가 `for v in ref[k] - gen[k]` 라 **(칸, 값) 쌍**을 세는데 '칸' 이라 적었다. 검산: 입력 비수치 878 인스턴스 = **493칸** (SYMBOL(N) 424 인스턴스 = **118칸**) 기대 비수치 565 인스턴스 = **430칸** (SYMBOL(N) 234 인스턴스 = **112칸**) `n/a` 는 칸당 하나라 612 가 그대로 칸이지만, **enum 은 칸당 3.6개**다 — enum 축의 실제 크기는 658칸이 아니라 **230칸**이다. 결론(채택 안 함)은 안 바뀌지만 규모를 2배 이상 부풀려 적었다. ## 검증 - 신규 가드 **16건**(바인딩 하드닝 8 · 설정 가족 6 · 오탐 방지 1 · 단위 검산 1) - 뮤테이션 **7/7 죽음 · 생존 0**(판정은 pytest 종료코드로만) - 전체 회귀 **6,941 passed / 1 skipped / 1 failed** (그 1건은 동시 세션의 `paths.py` 경로 봉인 — 이 작업 전부터) - 백엔드 재기동 후 실동작: auto-start `already_running` · health ok - ruff ratchet 신규 위반 0 ⚠ 실험이 실제 워커 포트를 잠시 뺏었다. 즉시 종료하고 복구를 확인했다 (리스너 1개 · `list_dir` 40건). 같은 실험을 다시 할 땐 **빈 포트에서** 할 것. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
앞 커밋(c236db4)은 `worker.py` 소스만 고쳤다. 그런데 `dist/excel_rename_gui_v2.exe` 는 `.gitignore` 대상이라 **배포본은 옛 판 그대로**였고, 그 어긋남은 아무 신호도 안 냈다. 이번에 실제로 빌드해 교체하고, 같은 일이 다시 조용히 벌어지지 않게 감지를 넣었다. ## ① 재빌드 + 교체 (대조군까지 확인) 구 exe 1.0 10,399,362 bytes 2026-05-29 신 exe 1.2 10,402,100 bytes (PyInstaller 6.20.0, --onefile --noconsole) 가로채기 시도(SO_REUSEADDR 로 살아있는 워커 위에 바인딩): 신 exe 8790 -> **차단됨** (공격자가 10013) 구 exe 8766 -> **뺏겼다** <- 대조군. 수정 전 상태가 실제로 취약했다 교체 후 프로덕션 포트(8766) 재시도 -> **차단됨** ⚠ **`D:/` 읽기 성공은 Cloudium 권한 증명이 아니다**(그냥 로컬 파일일 수 있다). allowed_prefixes 의 `U:/연구소/...` 를 읽혀 확인했다 — 신·구 exe 둘 다 `(KJPDS02_SwUDS) Software Unit Design Specification…` 을 반환한다. **권한 유지.** ⚠ PyInstaller 는 `--specpath` 가 C: 이고 소스가 D: 면 죽는다 (`ValueError: path is on mount 'D:', start on mount 'C:'`). D: 안에서 빌드했다. 교체 순서: 백엔드 종료(재spawn 방지) -> 워커 종료 -> 교체 -> 백엔드 기동. 구 exe 는 스크래치패드에 백업(sha256 대조 확인). ## ② 소스↔exe 드리프트 감지 — 이게 이번 커밋의 본체 exe 가 gitignore 라 소스를 고쳐도 배포본은 그대로다. 게이트는 초록이고 read 도 되는데 **고친 동작만 빠져 있다** — 눈으로는 못 잡는다. 이번 건이 정확히 그랬다. `WORKER_VERSION` 을 올리고, 백엔드가 **소스 상수를 직접 읽어** 기대값으로 삼아 IPC `version` 응답과 대조한다(`_worker_version_report`). 결과는 dict 로 낸다: version_check = match | stale_exe | unavailable(<사유>) 교체 직전 실측이 바로 잡아냈다: `{'worker_version': '1.0', 'expected': '1.1', 'version_check': 'stale_exe'}` -> 교체 후 `{'worker_version': '1.2', 'match'}`. ⚠ **평상시 경로(`already_running`)에도 붙였다.** spawn 쪽에만 달면 재기동해도 낡은 exe 를 영영 못 본다 — 정작 매번 지나는 길이 거기다. ⚠ 기대값은 **소스 상수에서 읽는다.** launcher 에 하드코딩하면 두 벌이 되어 갈라진다. ⚠ 조회 실패는 `unavailable` — 낡음으로 접으면 **없는 재빌드**를 시킨다. ⚠ **방향을 단정하지 않는다.** 흔한 건 'exe 가 낡음' 이지만 소스를 되돌리면 반대다. "낡았다" 대신 "어긋난다" 로 적고 두 값을 다 보여준다(반대 방향도 테스트로 고정). ## ③ 무인 spawn 이 대화상자에 붙잡히지 않게 (1.1 -> 1.2) 바인딩 실패 대화상자는 `--noconsole` 에서 유일한 보고 수단인데, **모달은 아무도 안 누르면 영원히 대기한다.** 백엔드가 detached 로 띄우므로 클릭할 사람이 없다. 이 저장소는 같은 형태로 테스트 스위트가 25분간 hang 한 전례가 있다("HTTP 가 tkinter 모달"). -> 상한(기본 20초, `CLOUDIUM_WORKER_ERR_DIALOG_TTL`) 뒤 스스로 닫는다. 0 이하면 아예 안 띄우고 stderr 로만 남긴다(주석에 적었으면 동작도 그래야 한다). TTL 은 모듈 상수가 아니라 **함수**로 뺐다 — import 시점에 얼리면 재로드 없이는 바꿀 수도 잴 수도 없다. 이 저장소는 기본 인자를 def 시점에 얼려 두고 "튜닝했다" 고 믿었다가 실험 자체가 no-op 이던 전례가 있다(`file_resolver._ping_worker` docstring). ## ④ 내가 계약을 깨뜨렸다 — 전체 회귀가 잡았다 `already_running` 반환에 키를 더하면서 기존 테스트를 깨뜨렸다: test_ensure_returns_already_running_when_gate_alive assert result == {"action": "already_running"} <- 완전 일치 단언 영향 범위를 잡을 때 **그 모듈의 기존 테스트 파일을 안 넣었다** — 새로 만든 파일만 돌렸고 전체 회귀에서야 드러났다. 과정상 누락이다. 계약 자체(가산 확장)는 정당하다 — 깨진 건 **완전 일치 단언이 그 확장을 못 견디는 형태**였기 때문이다. "action 이 already_running 이다" 를 단언하도록 고쳤다. ⚠ 그 테스트가 `version_check: match` 를 받았다 — 즉 **단위 테스트가 실제 워커에 네트워크 호출**을 하고 있었다. 머신 상태에 따라 결과가 달라진다 = 격리 파괴. 테스트에서 버전 조회를 막았다. ## ⑤ 내 진단이 틀렸다 — 프로세스 2개는 유령이 아니었다 워커 프로세스가 둘로 보여 "바인딩에 진 쪽이 모달을 들고 남았다" 로 진단하고 하나를 죽였다. **틀렸다.** 부모/자식 관계로 확인: 34580(프로덕션 워커) <- ParentProcessId 44216 내가 죽인 건 **부트로더 부모** 새 기동: 40524(부트로더) -> 14096(실제 앱, 포트 보유) `--onefile` PyInstaller 는 부트로더가 자기를 재실행하므로 **한 번 띄워도 항상 2개**다. ③의 TTL 은 **예방이지 관측된 사고의 수정이 아니며**, 코드 주석에 그렇게 적었다 (거짓 근거를 남기면 다음 사람이 그걸 사실로 읽는다). ## 검증 - 신규 가드 **6건**(드리프트) + 기존 파일에 **TTL 4건** 추가 - 뮤테이션 **4/5 죽음 · 1 생존** ⚠ 생존한 P5("소스 버전을 되돌림")는 **코드 결함이 아니라 프로세스 규율**이다 — "동작을 바꾸고 버전을 안 올린다" 를 단위 테스트로 잡을 방법이 없다. 없는 가드를 지어내지 않는다(죽은 방어 금지). 대신 `WORKER_VERSION` 주석에 의무를 명시했다 - 배포 워커(1.2) 실동작: version match · D: 40건 · **U: Cloudium 1건** · 가로채기 차단 - 전체 회귀 **6,947 passed / 1 skipped / 1 failed** (그 1건은 동시 세션의 `paths.py` 경로 봉인 — 이 작업 전부터) - ruff ratchet clean ⚠ `dist_real/`(78MB, 2026-05-06)은 **1.0 인 채로 남겼다** — 역사 백업이고, 폴백으로 쓰이더라도 위 드리프트 감지가 잡는다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`except Exception` 이 같은 `try` 안에서 우리가 **일부러 낸** `HTTPException` 을 먹고
500 으로 다시 던지고 있었다.
## 라이브 대조군 (수정 전 코드를 worktree 에 띄워 나란히 때렸다)
`git worktree add --detach <tmp> HEAD` → 수정 전 :9102 / 수정본 :9101, 같은 요청:
GET /api/vcast/reports/{filename}
nope_does_not_exist.xlsx :9102 500 "Download error: 404: Report file not found"
:9101 404 "Report file not found"
C:\Windows\win.ini :9102 500 "Download error: absolute path not allowed"
:9101 400 "invalid filename: absolute path not allowed"
..\..\escaped.xlsx :9102 500 "Download error: path traversal not allowed"
:9101 400 "invalid filename: path traversal not allowed"
결과 셋: ①클라이언트가 자기 입력 문제를 서버 장애로 읽는다 ②모니터링에 실재하지 않는
5xx 가 쌓여 진짜 장애가 묻힌다 ③`str(e)` 가 응답에 실려 내부 절대경로가 샐 수 있다.
## 고친 곳 (4)
backend/routers/excel.py excel_compare 400 "Invalid Excel compare parameters"
backend/routers/excel.py excel_compare_upload 400 "Invalid Excel files"
backend/routers/vcast.py vcast_process_jenkins 404 "cached build not found" 등
backend/routers/vcast.py vcast_download_report 404 / 경로이탈
`except HTTPException: raise` 를 앞에 두고, 500 본문에서 `str(e)` 를 뺐다(로그로만).
`vcast_download_report` 의 경로 이탈은 500 이 아니라 **400** 으로 바꿨다 — 이탈은 서버
장애가 아니라 클라이언트 오류이고, 500 이면 공격 시도가 장애로 집계된다.
`FileResponse(filename=...)` 도 요청 문자열이 아니라 `file_path.name` 을 쓴다.
임시 파일 정리의 `except Exception: pass` 는 `except OSError` + debug 로그로 바꿨다.
## ⚠ 두 결함은 **같은 파일 안의 쌍둥이**였다
`excel.py` 는 두 엔드포인트가 같은 모양, `vcast.py` 도 그렇다. 한쪽만 고치면 다른 쪽이
그대로 남는 형태다 — 직전 두 커밋(e29bd0e·d13f3b6)에서 반복 확인된 패턴이라 파일 단위로
훑었다.
## ⚠ 스캔 숫자는 인용 전에 분해할 것
첫 스캔은 **49건**이었는데 정밀화하니 **4건**이었다. 줄어든 이유:
①중첩 함수(SSE `_run()` 등) 안의 raise 는 바깥 try 로 안 흐른다 — 45건
②`safe_resolve_under` 의 ValueError 를 **400 으로** 바꾸는 14곳은 결함이 아니다
그 사이에 **0건이 나온 판도 한 번 있었다** — 핸들러가 500짜리 `HTTPException` 을
*생성*하는 걸 "HTTPException 을 처리함"으로 오인해서다. 전수 판정이 한쪽으로 100%
몰리면 데이터가 아니라 조회 경로를 의심할 것.
## ⚠ 라이브 검증에서 가짜 PASS 를 낼 뻔했다
처음엔 `/api/vcast/download-report/...` 를 때렸는데 **실제 라우트는 `/api/vcast/reports/`**
다. 틀린 URL 도 404 를 돌려주므로 "의도한 404 복원됨"으로 **PASS 로 셌다**. 본문
(`API endpoint not found`)을 안 봤으면 그대로 끝났다 — **상태코드만으로 판정하지 말 것.**
테스트(`TestIntendedStatusSurvives`·`TestNoHandlerRebuildsTheDefect`)는 **다음 커밋**에
함께 든다. 같은 파일이 축 A 도 검사해서, 여기 넣으면 이 트리에서 실패한다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
직전 두 커밋(e29bd0e·d13f3b6)에서 드러난 결함 패턴 — **같은 파일 안의 읽기/쓰기 비대칭**과 **판정 복제** — 이 다른 라우터에도 있는지 전수로 쟀다. ## 라이브 대조군 — 취약점이 실재했고 실제로 익스플로잇된다 `git worktree add --detach <tmp> HEAD` 로 수정 전 코드를 :9102 에, 수정본을 :9101 에 띄우고 **같은 요청**을 던졌다: POST /api/exports/pdf/report output_path=<시스템 temp>/pwned.pdf :9102 (수정 전) 200 -> **19,069 bytes PDF 가 저장소 밖에 실제로 생성됨** :9101 (수정 후) 403 "output_path not allowed" -> 생성 0건 상태코드가 아니라 **파일 존재**로 판정했다. 탐침 디렉터리는 삭제했다. ⚠ 그 과정에서 **파일을 흘렸다.** 이탈 경로로 `/tmp/...` 를 줬는데 Windows 네이티브 파이썬은 그걸 현재 드라이브 루트로 풀어 `C:\tmp\...` 에 썼고, 내 `ls` 는 MSYS `/tmp` 를 봐서 **"생성 0건"** 이라고 보고했다 — 하마터면 그걸 "봉인 성공"의 근거로 쓸 뻔했다. (찾아서 삭제했다.) 경로 탐침은 **네이티브 경로**로 주고 검사도 같은 해석으로 할 것. ## 왜 봉인 판정이 흩어져 있었나 같은 판단이 **세 벌**이었고 허용 집합이 서로 달랐다: local.py `_allowed_request_roots()` [repo, 캐시] <- /api/local/* 20곳 assistant_service `_trusted_base_roots()` [repo, project_root, report_dir, jenkins] jenkins.py 자체 목록 그 사이로 **봉인이 아예 없는 엔드포인트**가 남아 있었다. `backend/services/paths.py` 에 `trusted_roots()` / `confine()` 을 단일 출처로 뒀다. ⚠ **넓히는 방향으로 통합하지 않았다.** 가장 좁은 `[repo, 캐시]` 가 기준이다. 여기에 `DEFAULT_REPORT_DIR`·`DEFAULT_PROJECT_ROOT` 를 더하면 그 **설정을 바꾸는 것만으로** `confine_request_root` 를 쓰는 `/api/local/*` 20곳이 조용히 함께 넓어진다. 실측으로도 이 배포에서는 둘 다 이미 저장소 밑이라 더할 이유가 없다. `test_roots_do_not_widen_with_config` 가 이걸 고정한다. ⚠ `assistant_service._trusted_base_roots()` 는 **건드리지 않았다** — 챗 전용이고 더 넓다. 통합은 챗의 허용 범위를 바꾸는 일이라 먼저 재고 결정을 받아야 한다. ⚠ 처음 함께 냈던 `jenkins_server_roots()` 는 **삭제했다**. 호출자 0·테스트 0 이라 뮤테이션으로 죽일 수 없는 코드였고, 남겨두면 "jenkins 도 통합됐다"로 오독된다. ## 봉인한 곳 (쓰기 4) /api/exports/pdf/report output_path <- 경로도 **내용도** 클라이언트 /api/exports/pdf/convert output_path /api/local/project-setup/generate-component-map output_dir /api/local/project-setup/generate-override output_dir `Path(base) / raw` 는 봉인이 아니다 — `..` 도 통과하고 **절대경로는 base 를 통째로 대체**한다. 그래서 join 이 아니라 "resolve 한 뒤 루트 안인가"로 판정한다. 403 본문에는 경로를 되비추지 않는다(정찰 도구가 되면 안 된다). ## ⚠ 초기 판단이 뒤집힌 지점 — **읽기는 잠그면 안 된다** 처음엔 "임의 파일 **읽기** 취약점 6곳"으로 봤는데 **틀렸다**. `_is_allowed_req_doc` 이 위치가 아니라 **확장자만** 보는 데서 드러나듯, 요구문서·소스가 저장소 밖에 사는 것이 이 앱의 설계다(CLAUDE.md: `D:/Project/devops/260105/docs/`, `D:/Project/Ados/PDS64_RD/`). 읽기를 봉인하면 정상 사용이 깨진다. **근거가 없는 건 쓰기뿐**이고, 이 저장소의 자체 쓰기 엔드포인트 `local_editor_write` 는 이미 같은 루트로 잠겨 있다 — 이 4곳만 빠져 있었다. `TestReadPathsStayOpenOnPurpose` 를 **결정 트립와이어**로 뒀다. "일관성"을 이유로 읽기에 봉인을 붙이면 이 테스트가 먼저 막는다. ## ⚠ 기존 테스트 1건을 고쳤다 — 계약이 아니라 스캐폴딩이다 `test_routers.py::test_pdf_report_with_sections` 이 `tempfile.TemporaryDirectory()` (허용 루트 **밖**)에 쓰고 200/500 을 기대해 403 으로 깨졌다. 이 테스트의 주제는 "PDF 가 만들어지나"이지 "임의 경로에 쓸 수 있나"가 아니므로 같은 파일의 기존 헬퍼 `local_tmpdir()` 로 옮겼다. **이건 내 판단이 아니라 저장소 선례다** — 2026-08-04 `/api/local/*` 감사가 같은 상황에서 이미 같은 결론을 냈고(`test_routers.py:60` 주석: "시스템 temp 는 허용 루트 밖이라 403 이다 — 그게 바로 이 라운드가 막은 것이므로 테스트를 그쪽에 맞추는 게 맞다"), `local_tmpdir()` 이 그때 만든 헬퍼다. 프론트는 이 4곳 어디에도 경로를 보내지 않는다. `/api/exports/pdf/*` 는 **프론트·백엔드 호출자가 0곳**이고(실측), `output_dir` 은 아예 미전송이라 기본 `docs/` 로 간다. UI 파손 0. ## 검증 tests/unit/ 전체 6,955 passed / 1 skipped (550초, -n auto) — 이 시리즈 첫 전체 회귀 신규 테스트 17 뮤테이션 8/8 KILLED (판정은 pytest 종료코드로만) 4xx 재-raise 제거 x2 / 쓰기 봉인 제거 x3 / 신뢰루트 설정값 확장 / 단일출처 이탈 / **읽기까지 봉인**(결정 트립와이어) frontend vitest 전체 1,224 중 3건 고부하 flake -> 단독 재실행 178/178 통과 라이브 대조군 위 참조 ⚠ 별건 발견: `tests/integration/` 이 **56 failed / 10 passed** 로 죽어 있다. 전부 401 이고, 원인은 그 스위트가 인증 헤더 없이 `TestClient` 를 쓰는데 커밋 1b6bb99(2026-08-04)가 Bearer 를 요구하게 됐기 때문이다. 게이트가 `tests/unit/` 만 돌려 17일간 몰랐다. 이 커밋 범위 밖 — 인계 문서 §7 에 기록했다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… 사각을 가드로 막았다
`tests/integration/` **56 failed / 10 passed / 7 errors** → **74 passed**.
## 무엇이 조용히 죽어 있었나
커밋 `1b6bb99`(2026-08-04)가 `X-User` 단독 신원을 막고 `Authorization: Bearer` 를
요구하게 됐다. 그 순간부터 integration 전체가 401 이 됐는데 **스위트는 계속 초록**이었다 —
게이트 4곳(pre-commit · Stop 훅 · GitHub Actions · GitLab CI)이 **전부 `tests/unit/` 만**
돌기 때문이다. 그 디렉터리에는 `STS-EXPORTS-001` 같은 **ISO 26262 추적성 ID** 가 달려
"시험이 있다"는 문서 근거로 쓰인다.
## ⚠⚠ 죽은 스위트는 회귀만 못 잡는 게 아니라 **낡은 계약을 보존한다**
되살려 보니 **이미 고쳐진 결정을 되돌리라고 요구하는 테스트가 둘** 있었다.
1. `test_api_endpoints.py` — `/api/run/stop` 이 임의 PID 에 **200** 을 주던 시절의 기대.
그때는 백엔드 자신의 PID 를 줘서 uvicorn 을 통째로 내릴 수 있었고, `{"ok": True}`
고정이라 PID 존재를 떠보는 oracle 이기도 했다(`sessions.py:436` docstring).
지금은 **403**(존재/부재를 안 흘리려 404 가 아니다). → 403 을 단언하도록 고치고
**왜 200 이 아닌지**를 테스트 docstring 에 박았다.
2. `test_quality_improvements.py::test_finalize_asil_default_qm` — **삭제된 ASIL QM
기본값**을 요구한다(`report_gen/function_analyzer.py:1218`: "근거의 부재를 등급
주장으로 바꾸지 않는다"). **고치지 않았다** — 근거 부재를 QM(안전요구 면제)으로
채우는 건 under-classification 이고, 안전 등급 판단은 사용자 결정이다(규칙:
안전 테스트 자동 수정 금지). 아래 `_EXEMPT` 에 사유와 함께 기록했다.
**"실패하니까" 코드 쪽으로 맞췄다면 취약점과 under-classification 이 되돌아왔다.**
## 결함은 4중이었다
1 격리 fixture 가 tests/unit/conftest.py 에만 integration 격리 0
2 TestClient(app) **6벌 모두** 인증 헤더 없음 56건 401
3 `fixtures_dir` fixture 가 **어디에도 없음** 7건 ERROR
4 게이트 4곳 전부 tests/unit/ 만 위 3개가 17일간 안 보임
⚠ **폴백만으론 안 된다.** `DEV_MODE_X_USER_FALLBACK=1` 을 켜도 `X-User` 헤더가 없으면
401 이다 — 실측으로 갈랐다(헤더만 14 failed / 헤더+conftest **14 passed**).
## 고친 구조 (복제하지 않았다)
- 격리 fixture 5개를 `tests/unit/conftest.py` → **`tests/conftest.py` 로 이동**.
그 5개는 단위 전용이 아니라 **머신 상태 격리**(admin 등록·resolver local 고정·JWT
폴백·KB 캐시)라 backend 를 만지는 모든 회귀가 필요로 한다. `tests/unit/conftest.py`
는 삭제 — 새 디렉터리가 자동 상속한다. 파일이 142줄 **전부** 그 fixture 였다.
- `client` fixture **6벌 → 1벌**(`tests/integration/conftest.py`, `X-User` 포함).
- 게이트 3곳을 `tests/unit/ tests/integration/ tests/e2e/` 로.
## 새 가드 — `tests/unit/test_gate_reach.py`
"내가 다 찾았나?" 를 사람 기억이 아니라 **유지되는 불변식**으로 바꾼다
(`test_output_path_collision_sweep.py` 와 같은 패턴). 게이트가 안 닿는 테스트
디렉터리·파일이 있으면 여기서 실패한다.
뮤테이션 3/3 (판정은 pytest 종료코드로만):
M1 세 게이트 모두 integration 제거 KILLED
M2 e2e 만 제거 KILLED
M3 pre-commit 만 되돌림(나머지 유지) ALIVE <- 의도된 설계("하나 이상"이 기준)
## ⚠ 디렉터리 사각을 고치다 **파일 사각**을 새로 만들 뻔했다
첫 가드는 `tests/*/` 만 봤다. 그런데 `tests/` **루트 직속**에 3파일 **196건**이 있고
그것도 게이트 밖이었다. 가드를 파일까지 보게 확장했다.
그 3파일은 **아직 게이트에 못 넣는다** — 실측 1 failed / 20 errors
(`mock_function_details`·`mock_uds_payload` fixture 부재) / **561초**(pre-commit 예산
900s 에 unit 550s 와 공존 불가) + 위 ASIL 건. `_EXEMPT` 에 **사유와 함께** 적어
**보이는 부채**로 남겼다(면제에 사유가 없으면 그것도 테스트가 잡는다).
## 곁가지
- `tests/e2e/conftest.py` 기본 포트가 **5173/7000** 이었다(실제는 **5174/9000**).
playwright 미설치로 전부 skip 이라 아무도 못 봤다 — 누가 설치하면 엉뚱한 포트로 붙어
"연결 거부"로 죽고 그건 코드 결함처럼 보인다. 고쳤다.
- **`@pytest.mark.requirement("STS-…")` 25개가 전부 docstring 안**이라 pytest 에 등록조차
안 된다(실제 데코레이터 **0**). ISO 추적성 주장이 이중으로 비어 있었다 — 이번 범위
밖이라 고치지 않고 기록만 한다.
⚠ `ruff --fix` 를 쓰지 않았다. I001 자동 정렬은 레거시 위반을 '신규'로 만들어 변경라인
ratchet 이 커밋을 막는다(이 저장소 전례). 미사용 import **줄 삭제만** 하고 재정렬은
하지 않았다. HEAD 에 I001 이 없던 것을 대조로 확인했다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
사용자 결정: **"QM 은 기본값이 아니고 빈 값으로 간다."** `tests/test_quality_improvements.py::TestPhase3TbdResolve` 가 `report_gen/function_analyzer.py` 에서 **의도적으로 삭제된** QM 기본값을 요구하고 있었다(그 자리 주석: "근거의 부재를 등급 주장으로 바꾸지 않는다"). `QM` 은 "안전 요구 면제" 라는 **적극적 주장**이라, 등급을 모르는 함수에 그걸 채우면 ISO 26262 under-classification 이다. 이 파일이 `tests/` 루트에 있어 **어떤 게이트도 돌리지 않았기 때문에** 삭제된 동작을 요구하는 테스트가 그대로 살아 있었다(직전 커밋 7ae7567 참조). ## 실측한 현행 계약 — 그대로 고정했다 등급 없음 -> '' (지어내지 않는다) 'TBD' -> 'TBD' (빈 값으로 접지 않는다 — '미정'과 '아예 없음'은 다르다) 'N/A' -> 'N/A' 'B' -> 'B' 'd' -> 'D' (표기 정규화일 뿐, 등급 부여가 아니다) 테스트 2개 → **5개**로 늘려 축마다 하나씩 고정하고, 각 단언에 **왜 그래야 하는지**를 적었다. ## 뮤테이션 3/3 KILLED (판정은 pytest 종료코드로만) M1 QM 기본값 부활(옛 동작) KILLED <- 이 가드의 존재 이유 M2 정규화 결과 무조건 대입(TBD/N-A 가 빈값으로 접힘) KILLED M3 등급 대문자 정규화 제거 KILLED M1 이 살아 있었다면 이 가드는 "실패하니까 코드를 되돌리는" 다음 사람을 못 막는다. ## 곁가지 — `tests/test_json_parsing.py` 는 **테스트가 0개**였다 `test_*` 함수도 `Test*` 클래스도 없이 import 시점에 루프를 돌며 `print("PASS"/"FAIL")` 만 찍는 **스크립트**였다. `python_files = ["test_*.py"]` 라 pytest 가 import 는 하므로 검사는 실행되지만 **단언이 없어 절대 실패할 수 없다** — 게이트에는 `collected 0 items`(rc=5) 로 보인다. 진짜 테스트 **7개**로 승격했다(승격 직전 스크립트 실측 7/7 통과 → 기대값 그대로). ⚠ 전수 확인: `tests/` 341개 파일 중 같은 패턴 **0개**. 이 하나뿐이었다. ## ⚠ 내 면제 사유가 틀렸었다 — 합계를 인용하기 전에 분해할 것 직전 커밋에서 루트 3파일을 **한 묶음**으로 보고 "196건 / 1 failed / 20 errors / 561초" 라는 같은 사유로 전부 면제했다. 파일별로 다시 재니 전혀 달랐다: test_json_parsing.py 6초 **테스트 0개**(fake-green) -> 7개로 승격, 게이트 편입 test_quality_improvements.py 25초 86 passed / 6 skipped -> 게이트 편입 test_coverage_boost.py 303초 87 passed / **20 errors** -> 면제 유지 ## 게이트 배선을 열거식에서 `tests/` + 명시 `--ignore` 로 pytest tests/ --ignore=tests/test_coverage_boost.py 열거식(`tests/unit/ tests/integration/ tests/e2e/`)은 새 디렉터리·파일이 생길 때마다 세 곳을 고쳐야 하고, 안 고치면 그게 곧 다음 사각지대다. 이제 **기본이 포함**이고 빠지는 것만 사유가 남는다. 가드도 `--ignore` 를 이해하게 했다 — 그게 없으면 `pytest tests/ --ignore=X` 를 보고 X 를 '도달'로 오판해 **모든 면제가 조용히 무효**가 된다. 뮤테이션 3/3: M1 게이트를 tests/unit/ 로 축소 KILLED M2 사유 없는 --ignore 추가 KILLED <- 조용한 제외를 막는다 M3 가드의 --ignore 인식 무력화 ALIVE <- 예측대로(그 파일은 _EXEMPT 가 덮는다) ## 남은 것 — `test_coverage_boost.py` (면제 유지) 87 passed + **20 errors / 303초**. errors 는 전부 `mock_uds_payload`·`mock_function_details` fixture 부재인데 **저장소 어디에도 정의가 없다**(파일 헤더 `# /app/tests/...` 가 말해주듯 옛 Docker 구성 잔재). fixture 를 **지어내면** 잘못된 입력으로 통과하는 테스트가 되므로 손대지 않았다. `_EXEMPT` 에 해소 순서까지 적어 뒀다 — **shape 를 먼저 결정하고, 그 다음 시간.** Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SwUTCV/SUTR/SwUTCR · SwITCV/SITR/SwITCR 중 4종은 서브탭에 들어가야만 만들 수 있었고, 종합결과서 2종은 Quality 기록조차 없어 만들어도 보드가 영구히 "미생성" 이었다. 6종 전부를 UDS/STS 와 같은 방식(상태·점수·판정 + 준비 점검 + 그 자리 생성)으로 올린다. 라이브 검증에서 게이트가 세 가지로 거짓말하고 있던 것을 함께 고쳤다. ## 종합결과서 평가기 신설 — 분모 키가 다르다 SUTR/SITR 은 총 TC 를 `total` 로 내지만 종합결과서는 `total_tcs` 다. `evaluate_test_result` 에 넣으면 `_safe_float` 가 부재를 0 으로 접어 분모가 1 로 고정되고 **tested 가 그대로 백분율**이 된다(실측 6882건 → 688200%). 게이트가 없느니만 못하므로 `evaluate_comprehensive_result` 를 따로 만들었다. recorder 의 빈-산출물 skip 과 fn_count 분기도 같은 키 함정을 갖고 있어 함께 고쳤다. ## 프로젝트 축을 추측하지 않는다 `record_run` 이 `project_root`(= 빌더 project_id)에서 scm_id 를 추측하는데, 문자열 "KJPDS02" 가 SCM entry `kjpds02` 의 id 이면서 동시에 `kjpds02_pv` 의 builder_project_id 라 추측이 빗나갔다. 보드는 `kjpds02_pv` 로 조회하므로 빌드도 기록도 정상인데 **화면만 침묵**했다. 문자열로는 갈리지 않는 모호함이라 `resolve_scm_id` 를 고치는 대신 아는 쪽(화면)이 `scm_id` 를 실어 보낸다. ## 게이트가 라우터와 같은 곳을 보게 한다 준비 점검이 "진행 불가" 를 내는데 같은 요청의 빌드는 성공했다. VectorCAST 로그와 대응 시험 규격서를 라우터는 `config/swut_meta.json` 에서 찾는데 게이트는 SCM `linked_docs` 만 봤기 때문이다. 판정을 `config_log_folders`/`config_spec_path` 단일 출처로 뽑아 라우터 2곳의 복제를 제거하고 preflight 가 같은 것을 쓴다. config **읽기는 호출부에 남긴다** — 여기서 읽게 했더니 라우터 회귀 12건이 실제 머신 config 를 읽어 깨졌다(테스트 seam 파괴). ## "없어도 됩니다" 가 거짓인 경우 `sutr_spec_based` 가 켜진 프로젝트에서는 결과 문서의 `3.Test Log` 를 규격서 시트 통째 복사로 만들어, 규격서가 없으면 라우터가 400 을 낸다. 표에 optional 로 박아 두면 게이트가 거짓말한다 → config 를 읽어 **프로젝트별로** 판정한다. 승격 대상은 SUTR/SITR 뿐이다 — 커버리지·종합결과는 규격서 없이도 빌드가 성공하므로 넷까지 올리면 이번엔 반대 방향으로 거짓말한다. ## 낡은 양식·규격서 경로 교체 (config/swut_meta.json) 게이트가 제대로 보기 시작하니 등록 경로 4건이 실측 404 로 드러났다. 교체 전에 파일을 열어 양식을 확인했다 — SwITS 는 파일명에 `_PV_` 가 없어 DV 로 오인하기 쉬운데, 실제로는 B열 `SwITC_SwUFn_0101_01`(PV)이었다. es95411 은 대체 후보가 확장자·정체 모두 달라 **지어내지 않고 남겼다**. ## 곁가지 — integration 회귀가 운영 SCM 레지스트리에 쓰고 있었다 `test_scm_router.py` 의 중복등록 회귀가 `POST /api/scm/register` 를 **운영 config/scm_registry.json 에 직접** 한다(teardown 의 DELETE 로 되돌리므로 영구 변경은 아니고 **일시 변경**이다 — mtime 으로 실측). 그 창이 `-n auto` 에서 `test_scm_write_requires_admin.py` 의 before/after 바이트 가드와 겹쳐 **자기가 안 한 변경**으로 실패한다. 2026-08-21 게이트 범위가 integration 까지 넓어지며 드러났고 인터리빙에 따라 나타나는 flaky 다. integration conftest 에 레지스트리 격리를 넣었다. ## 검증 - 회귀: backend 390 · frontend 1227 · npm run build - 뮤테이션 10/10 사망 (엔드포인트 오배선 · form 누락 · 행 중복 · 기록 호출 삭제 · doc_type 오기 · 평가기 분모 · 첫 폴더만 확인 · config 직접읽기 2 · 승격 범위) - 라이브: SwUTCV/SwUTCR/SUTR/SwITCV/SITR 5종 실제 빌드 200, Quality 행이 보드 조회 축에 쌓이고 화면이 미생성 → 판정으로 바뀌는 것까지 확인 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApEz8UMi8qoKM9iWKYQho4
UDS 품질 게이트(`_compute_quick_quality_gate`)는 이름 그대로 payload 만 받는다. 라이터는 템플릿 주도라 대응 heading 이 없는 함수는 문서에서 조용히 빠지는데, payload 가 완벽하면 문서가 비어 있어도 만점이 나왔다. 실측 — `reports/quality.sqlite` 를 gen_stats sidecar 와 조인: run 660·661 payload 5개 중 문서 반영 0개 (빈 heading 419) → gate PASS · 점수 100.0 run 674 350개 중 252개(72.0%), 미반영 98 → gate PASS · 점수 99.5 생성 성공/실패 판정은 바꾸지 않았다. `_run_docx_in_subprocess` 주석이 사유를 대고 있다 — 템플릿이 의도된 부분집합일 수 있어 뒤집으면 대량 오탐이다. 대신 수치를 게이트와 같은 자리(Quality DB)에 참고지표로 올려 만점 옆에 반영률 0.0 이 보이게 했다. 임계는 베이스라인을 쌓은 뒤 정한다. - `_record_uds_run` 관문 신설 — 생성 경로 다섯 곳이 전부 여기를 지난다. 인자 구성(`_uds_record_kwargs`)과 산출물 충실도(`_uds_artifact_fidelity`)를 한 곳에서 합친다. 호출부가 recorder 를 직접 부르면 다섯 곳에 복제되고 한쪽만 고쳐진다. - 대조 문구와 "대조 불가" 판정은 `generators/_artifact_check.py` 를 재사용한다 — STS/SUTS/SITS 가 쓰는 바로 그 함수라 규칙이 갈리지 않는다. - sidecar 가 없거나 분모가 0 이면 지표를 만들지 않는다. 0.0 으로 적으면 재본 적 없는 실행이 "문서가 비었다" 로 둔갑한다. - 함께 든 앞선 라운드분: `record_uds_run` 5곳에 elapsed_sec·ai_model·meta 배선(uds 행만 전부 NULL 이었다) · `_has_real_interface_value`(`[IN] (none)` 을 실값으로 세던 리스트 분기) · UDS `caps` 공시 + `DEVOPS_UDS_MAX_FILES` 를 영구 무효로 만들던 중복 할당 제거 · ruff F811 1건(`import time` 이 `from time import time` 에 가려짐). ⚠ 이 변경의 소비처 절반(`evaluator.py` 의 `artifact_match_pct` 지표, `advisor.py` 라벨, `DocGenStatusBoard.jsx` 라벨, `docgen_requirements.py` caps)은 같은 워킹트리를 쓰던 다른 세션의 `c1aa66d` 에 함께 실려 먼저 들어갔다. 이 커밋은 그 나머지 절반이다. 뮤테이션이 두 가지를 잡았다: - `counts` 주입은 죽은 쓰기였다(recorder 는 `counts` 에서 `total_functions` 하나만 읽는다). 가드를 덧대는 대신 주입을 제거하고 건수는 meta_json 으로만 보낸다. - 그걸 없애자 조기반환의 관측 효과가 사라졌다. 죽은 방어가 아니라 "`measured` 가 권위"라는 계약이 미검사였던 것이라, 불일치 입력으로 그 계약을 직접 단언했다. 가드 55건 · 뮤테이션 15/15 죽음(종료코드 판정) · ruff/eslint 신규위반 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011vqDV8YQCkz6F8sZBE1Zxc
2026-07-23~08-25 사이 이 저장소에서 **6회** 같은 사고가 났다. 오늘만 두 번이다: 04cf6a9 09:56 feat(docgen): … → 1,208 files deleted · 트리 3파일 c19290a 10:07 feat(docgen): … → 1,208 files deleted · 트리 3파일 c1aa66d 10:21 feat(docgen): … → 23 files · 정상 메커니즘을 재현했다(git 2.52.0.windows.1, 스크래치 저장소): A 정상 pathspec 커밋 → 트리 9파일 B 훅이 인덱스를 비운 뒤 pathspec 커밋 → 트리 0파일 · 9 deletions · 오류 0줄 C 훅이 인덱스를 2파일로 갈아끼움 + pathspec → 트리 = 그 2파일 D 같은 조건 + pathspec 없음 → 트리 = 똑같이 그 2파일 **D 가 핵심이다. pathspec 은 원인이 아니다** — 저장소 메모리가 오래 그렇게 지목해 왔지만 붙이든 안 붙이든 똑같이 깨진다. 진짜 원인은 **커밋이 진행되는 동안 인덱스가 갈아끼워지는 것**이고, git 은 그 순간의 인덱스를 아무 경고 없이 커밋한다. 이 저장소는 pre-commit 게이트가 4~8분이라 창이 넓다. ⚠ 그래서 pre-commit 으로는 구조적으로 못 잡는다 — 훅이 끝난 **뒤** 바뀌어도 그대로 커밋된다. 사후(post-commit) 검증만이 유일하게 믿을 수 있는 지점이라 거기 넣었다. 판정: 삭제 ≥ 50건 AND 트리가 직전의 1/4 미만으로 축소. 사고의 특징은 "많이 지워짐" 이 아니라 **"거의 아무것도 안 남음"** 이다(1,211 → 3). 처음엔 "삭제가 남은 트리보다 많으면" 으로 뒀는데 **가드 테스트의 음성 대조군이 오탐을 잡아냈다**(121개 중 55개 삭제 = 45% 에 경보). 실제 이력 대조: 정상 커밋 4건 침묵 · 사고 2건 경보. 경보는 stderr 배너 + `.git/mass-deletion-alarm.log` 두 채널로 남기고, 커밋 결과는 바꾸지 않는다(post-commit 은 원래 못 바꾼다). AutoReport 체이닝은 그대로 보존한다. 가드 테스트 7건(음성 대조군 2 포함) · 뮤테이션 7/7 죽음. `>&2` 를 지우는 뮤턴트는 **동등 뮤턴트**다 — git 이 훅 stdout 을 stderr 로 넘긴다(실측). 사유를 테스트 docstring 에 적어 다음 사람이 같은 것을 다시 쫓지 않게 했다. 원인 규명은 여기까지다: **무엇이 인덱스를 그 3파일로 만드는지는 특정하지 못했다.** 배제한 것 — scripts/*(인덱스 쓰기 0건) · .githooks/pre-commit · install_git_hooks.sh (git config 뿐) · local_service.git_stage/commit(전부 additive) · auto_commit_push.py · sparse-checkout(미사용) · Windows 예약작업(없음) · AutoReport sync_commit_to_jira.py (git 쓰기 0건, import 단계에서 죽는다). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011vqDV8YQCkz6F8sZBE1Zxc
생성 현황 보드는 `analysisResult.matchedScm.id` 를 `scm_id` 로 실어 보내는데, **같은 문서를 만드는 탭 3곳은 props 를 아예 안 받아**(`function SwUTBuildSection()`) 그 값을 실을 수 없었다. `DocGenHubSection` 은 이미 모든 서브탭에 그 prop 을 넘기고 있었고, 받는 쪽만 없었다. 증상이 조용하다: 빌드는 200 으로 성공하고 파일도 내려오는데 quality run 이 프로젝트에 안 붙어 **보드에서는 계속 '미생성'** 으로 남는다. 실패가 아니라 "만든 적 없음" 으로 위장하므로 화면만 봐선 원인을 못 찾는다. 보드 경로와 백엔드 schema 3종은 커밋 `c1aa66d` 에서 이미 고쳤다 — 이번엔 탭 경로만 맞춘다. - SwUT / SwIT: `toBuildPayload(...)` 결과에 `scm_id` 를 얹는다 - 통합 결과: preview·build 두 호출부 모두 (`buildPayload` 는 module-level 공용) ## 자체 검토에서 잡은, 이 변경이 만든 결함 (X2) `buildXlsx` 는 `useCallback(…, [form, toast])` 인데 본문에서 `scmId` 를 참조한다. `scmId` 는 prop 파생이라 **나중에** 바뀐다(분석 완료 시점). 탭은 keep-alive 로 **먼저** 마운트되므로 실사용에서는 첫 렌더의 빈 값이 굳는다 — 그런데 첫 렌더에 prop 을 주는 시험은 통과하므로 안 보인다. deps 4곳에 `scmId` 를 넣고, `rerender` 로 그 순서를 재현하는 시험을 따로 뒀다. `validateCommon` 을 `useCallback` 으로 감쌌다. deps 줄을 건드리는 순간 원래 있던 레거시 `react-hooks/exhaustive-deps` 위반이 ratchet 에 **신규**로 계상됐다 — 되돌려 '안 바뀐 줄'로 만드는 건 게이트를 속이는 것이라 실제로 해소했다(동작 불변: `form` 이 이미 deps 라 결과는 같았고, 정적으로 증명이 안 됐을 뿐이다). ## 가드 구조가 아니라 **나가는 payload** 를 본다. SCM 미매칭일 때 키가 사라지지 않고 빈 문자열로 남는 것까지 고정한다 — `undefined` 면 `JSON.stringify` 가 키를 통째로 지워 "안 보냈다" 와 구분이 안 되기 때문이다. 뮤테이션 5/5 사망(세 payload 각각 · `analysisResult` 무시 · deps 에서 `scmId` 제거). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApEz8UMi8qoKM9iWKYQho4
…적이 없다 `scripts/uds/uds_quality_cycle.py` 를 돌리니 401 이 났다. 커밋 `1b6bb99`(2026-08-04)가 `X-User` 단독 신원을 막았기 때문인데, 러너는 그 401 응답을 **베이스라인으로 저장하고 exit 0** 을 냈다(`[baseline] created`). 저장소에 남은 최신 베이스라인이 2026-02 인 이유이고, 그 뒤 모든 비교는 거짓 기준선 위에서 돈다. 결함 4건: - **fail-open**: 응답을 검사 없이 베이스라인으로 저장했다 → `_unusable_reasons` 신설. 비-200 이거나 함수 0개면 저장하지 않고 exit 2. 비교 경로의 `hard_fail` 도 exit 2 로 올렸다(예전엔 exit 0 이라 래퍼가 통과로 읽었다). - **인증 없음** → 실제 Bearer 토큰 발급. `token_version` 을 **저장소에서 읽는다**. 0 으로 가정하면 로그아웃/비밀번호 변경으로 tv 가 오르는 순간 `TOKEN_REVOKED` 로 조용히 401 이 된다. 사용자가 없으면 `USER_REVOKED` 라 아예 성립하지 않으므로 먼저 막는다. `--user` 로 지정하거나 `admin_users.json` 의 첫 admin 을 쓴다. - **비교 축이 하드코딩 12개** → 양쪽 rates 의 **합집합**. 생산자에 축을 더해도 목록에 안 적으면 조용히 빠졌다(실제로 `input_real_fill`/`output_real_fill` 이 그랬다). 한쪽에만 있는 축은 `delta=None`(미측정) — 0 으로 채우면 "+18.9 개선" 거짓 델타가 찍힌다. - **`run_quality_cycle.ps1` 이 다른 저장소를 하드코딩** → `$PSScriptRoot` 기준으로. `$py`·`$script` 가 `d:\Project\devops\260105\...` 였고 **그 경로가 실재한다**. 이 저장소에서 돌려도 저쪽 코드가 저쪽 인터프리터로 실행돼, 여기서 고친 게이트가 결과에 반영되지 않는다. 기존 `quality_baseline*.json` 의 산출 경로가 전부 `D:\Project\devops\260105\reports\...` 였다 — **이 저장소 베이스라인이 아니었다.** ⚠ 그래서 그 2026-02 수치(`input_fill 15.6`)를 현재와 비교하면 안 된다. 교차 저장소다. 산출물 충실도(`artifact_match_pct`)도 얹었다. 그 축은 응답 본문에 없고 라이터 sidecar 에만 있어서(응답 dict 를 건드리지 않으려고 사본만 recorder 로 보낸다) 산출물 경로로 되읽는다. `measured=False`(미측정)와 0% 를 구분한다. 첫 베이스라인(이 저장소, 447함수): 게이트 7축 98.0~100.0 전부 통과인데 `input_real_fill 18.6` · `output_real_fill 27.7` · 문서 반영률 **75.6%**(338/447). 가드 23건 · 뮤테이션 8/8 죽음. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011vqDV8YQCkz6F8sZBE1Zxc
`_fill_function_info_table` 은 바로 윗줄에서 `table.rows[r].cells` 로 행을 이미 해석해 놓고, 다음 줄에서 `table.cell(r, 0)` 으로 되돌아갔다. python-docx 의 `table.cell()` 은 호출마다 그리드를 처음부터 훑는다(프로파일: `table.cell()` 81,510회 → `get_child_element` 4,390만 회). `table.rows[r]` 도 같다 — `_Rows.__getitem__` 은 구현이 `list(self)[idx]` 라 **인덱싱할 때마다 전 행을 새로 materialize** 한다. (`Table.rows` 자체는 lazyproperty 라 캐시되지만 그건 도움이 안 된다 — 실측으로 확인했다: 클래스 디스크립터를 갈아끼워도 접근 0회로 잡힌다.) ## 바꾸기 전에 등가성부터 쟀다 무서운 건 **병합 셀**이다. 이 표는 `_merge_function_info_table` 로 행0 전체와 행1+ 의 `[0-1]`·`[2..cols-1]` 이 병합돼 있어서 두 접근 경로가 같은 셀을 가리키는지가 자명하지 않다. 실제 병합 모양(19행 6열)으로 재현해 확인했다: · 114칸 전부 `table.cell(r,c)._tc is table.rows[r].cells[c]._tc` · 결과 XML **바이트 동일** · 1,918ms → 791ms (**2.42배**) 좁은 표(2·3·4열)도 확인했다. 옛 구현은 2열에서 `table.cell(r, 2)` 가 범위를 넘어 IndexError 로 중단하는데, 중단 직전 행이 이미 클리어돼 있어 최종 상태가 새 구현과 같아진다 — **우연히 같은 것이라 확인 없이 "등가" 라고 적으면 안 된다.** 테스트가 참조 구현(옛 코드)과의 XML 비교로 이 성질을 고정한다. 곁가지: `except Exception: pass` 였다. 표가 통째로 비어도 흔적이 없었다 → 경고 로그. 가드 18건 · 뮤테이션 7/7 죽음. 그중 둘이 내 테스트 공백을 잡았다: `table.rows` 접근 횟수를 세는 검사는 lazyproperty 때문에 무의미했고(→ 진짜 비용인 `_Rows.__getitem__` 호출 수로 교체), 행 초과 가드를 빼도 결과가 같아 생존했다 (→ 유일한 관측량인 "경고가 안 뜬다" 를 단언). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011vqDV8YQCkz6F8sZBE1Zxc
`/api/local/uds/generate` 는 템플릿 업로드가 없으면 `config.resolve_uds_template_path()`
(admin `/api/config/uds-template` 저장분 → `UDS_TEMPLATE_PATH` env)를 **건너뛰고 곧장
`UDS_REF_SUDS_PATH`(원본 정본 SUDS)로 폴백**했다. 즉 관리자가 무엇을 등록하든 이 경로는
매번 정본을 템플릿으로 썼다 — **등록 기능이 이 경로에서는 아무 효과가 없었다.**
| 경로 | 템플릿 미지정 시 |
|---|---|
| jenkins 동기 · 비동기 | `tpl = ... or None` → `generate_uds_docx` 안에서 등록본 해석 ✓ |
| local | 정본으로 하드 폴백 ✗ |
같은 판정이 세 곳에 흩어져 **한 곳만 어긋난** 형태다. `config.py` 의 `UDS_TEMPLATE_PATH`
주석도 tokenized 판을 "서버 기본 템플릿" 이라고 말하는데 local 이 그 의도를 우회했다.
## 판정을 함수로 뺐다
`resolve_registered_uds_template()` 신설(`helpers/uds.py`): 등록본 → 정본 → 빈 문자열.
`local.py` 는 이 한 줄만 부른다. 해석 실패는 삼키지 않고 경고 후 정본으로 내려간다.
⚠ 처음엔 인라인을 그대로 두고 **구조 검사**(AST 대입문 순서)로 가드를 썼는데 뮤테이션
2건이 살아남았다 — 대입문의 *존재*만 보니 `if False:` 로 분기를 죽여도 통과했다.
판정을 함수로 빼야 **결과**를 단언할 수 있다. 뺀 뒤 8/8 죽음.
⚠ 구조 검사를 substring 으로 쓰다 한 번 더 틀렸다 — `UDS_REF_SUDS_PATH` 는 같은 함수
안에서 **SwCom diff 참조 문서**로도 쓰인다(템플릿과 무관). AST 대입문만 보도록 고쳤다.
## 검증
- 가드 14건(해석 우선순위 5 · 경로 계약 4 · 해석기 행동 5) · 뮤테이션 8/8 죽음
- 등록 엔드포인트 실측(상태 불변): 없는 파일 400 · 비-docx 400 · 경로 이탈 400 ·
비-admin 401
- 템플릿 후보 3종 모두 저장소 검증기 통과. tokenized 판만 `{{PROJECT_NAME}}`
`{{MODULE_NAME}}` `{{REFERENCE_TABLE}}` 토큰을 갖고 `HDPDM01` 하드코딩이 9→7회로
준다. ⚠ **완전 제거는 아니다** — 헤더 2회 포함 7회가 남는다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011vqDV8YQCkz6F8sZBE1Zxc
## 1. UT101 'Test Case(P/F)' 분모 (backend/services/swut_comprehensive_aggregator.py)
회사 빈 양식 `(XXXX_SwUTCR)` 의 `1.UT101` 시트를 열어 구조를 확정했다:
72행 헤더 F='총 TC 수' G='Pass TC 수' H='Fail TC 수' I='테스트 성공율'
73행 UT / CVG / Statement -> 분모 = 함수 수(SwUFn)
74행 CVG / Branch -> 분모 = 함수 수
75행 CVG / TC -> '-' (VectorCAST 미산출)
76행 Test Case(P/F) -> 분모 = **TC 수**
77·78행 요구사항 추적성/정합성 커버리지 -> 단위레벨이라 분모 = 함수 수
즉 같은 열이어도 76행만 세는 대상이 다르다. 거기 함수 수가 들어가 있었다 —
KJPDS02 라이브 실측으로 **6,882건이 1,014건으로** 나갔다. 비율(`=G/F`)은 우연히
같아 화면상 아무 이상이 없고, ISO 26262 근거로 인용되는 것은 그 절대 수치다.
`agg` 에는 `total_tcs` 가 이미 있었다(`swut_input_adapter.aggregate_session`).
값이 없으면 **함수 수로 메우지 않고** 0 으로 남기고 경고한다 — 빈 자리를 그럴듯한
다른 수로 채우는 게 이 결함의 정체였다. 미실행 TC 가 있으면 양식의 Pass 수식
(`=F76-H76`)이 미실행을 Pass 로 센다는 것도 경고로 드러낸다(수식은 회사 양식
소유물이라 갈아끼우지 않는다). 비율 셀은 형제 행과 같은 `IFERROR` 로 맞췄다.
⚠ **기존 테스트가 이 결함을 고정하고 있었다** (`assert F76 == 570`, 함수 수).
이제 두 축을 함께 단언한다 — 한쪽만 보면 둘이 뒤바뀌어도 통과한다.
뮤테이션 5/5 사망. 라이브 재검증: F76=6,882 / F73·F74·F77·F78=1,014.
## 2. SwUTCV 양식을 프로젝트 released R판으로 (config/swut_meta.json)
정본과 **같은 자(openpyxl)** 로 재니 R판이 전 시트 정확히 일치한다:
시트 R판 산출 현행(공용) 산출 정본
2.Traceability 1030x1017 1027x1017 1030x1017
3.Consistency 1027x168 1024x168 1027x168
4. Coverage 1026x176 1024x176 1026x176
⚠ 처음엔 정본을 preview API 로 쟀다가 열 수가 1017 vs 6 으로 벌어져 없는 결함을
만들 뻔했다. **정본 대조는 자를 통일할 것.**
⚠ 같은 세대의 SwUTR R판은 **교체하지 않았다** — 원본 `3.Test Log`(sheet5.xml,
압축해제 51MB)가 손상돼 있다(zlib invalid block type @6,296,864). 우리 읽기
문제가 아니다: 두 번 읽어 sha256 동일이고, 4MB 청크 경계는 정상 통과했다.
## 3. config/*.json 저장소 5곳이 Windows 에서 CRLF 로 썼다
`.gitattributes` 는 `*.json text eol=lf` 다. 그런데 `Path.write_text()` 는 Windows
에서 `\n` -> `\r\n` 으로 바꾼다. 다섯 모듈이 같은 `_atomic_write` 패턴을 서로
베껴 와서 한 곳만 고치면 소용이 없었다(판정 복제).
JSON 자체는 CRLF 여도 파싱된다. **위험한 건 같은 실수가 훅에서 나는 경우**다 —
같은 날 `.githooks/pre-commit` 을 이 방식으로 저장해 파일 전체가 CRLF 가 됐고,
`.gitattributes` 주석이 경고하듯 Windows bash 는 그런 스크립트의 실행을 거부한다
= **게이트가 통째로 죽는다.** 그래서 훅 줄끝 검사도 함께 넣었다.
가드는 2층이다 — 바이트 검사는 Linux CI 에서 항상 통과해 공허하므로(거긴 변환이
없다) AST 로 `newline="\n"` 명시 여부를 함께 본다. 뮤테이션 5/5 사망.
## 4. pre-commit [4/4] index 무결성 (fail-closed)
이 훅이 **통과시킨** 커밋 2건(`04cf6a9`·`c19290a`)의 트리가 3파일뿐이었고 나머지
1,208개가 삭제로 기록됐다. 원인은 특정 못 했다 — 저장소를 복제해 훅과 동일한 환경
(GIT_INDEX_FILE=.git/index, cwd=루트)에서 `pytest tests/` 7,309건을 돌렸을 때
index 는 한 번도 안 변했다(테스트 스위트는 범인이 아니다).
원인을 모르는 채로도 결과는 막는다: 검사 시작/종료의 스테이징 내용이 다르면 중단.
행동 테스트(임시 저장소에 가드 조각을 실제 훅으로 심어 커밋)로 고정, 뮤테이션 3/3.
⚠ 사후 검증은 별도로 `.githooks/post-commit` 이 맡는다(커밋 `703eab1`) — 훅이 끝난
**뒤** 바뀌는 경우는 pre-commit 으로 구조적으로 못 잡기 때문이다. 두 층이다.
## 함께 실린 것
`.githooks/pre-commit` 에는 같은 날 다른 세션이 만든 변경(`--ignore=
tests/test_coverage_boost.py` 제거 + 실패 로그 파일화 + 예산 주석 갱신)이 섞여
있다. 한 파일이라 쪼갤 수 없어 함께 커밋한다. 해당 변경은 검증했다 —
`tests/test_coverage_boost.py` 107건 통과(17초).
`config/cloudium_extra_prefixes.json` 에 KJPDS02 결과보고서/로그/규격서 폴더 7개를
추가했다. 기존 항목은 경로에 `1220 진행` 세그먼트가 있는데 실제 등록 경로에는 없어
매칭되지 않았다 — 그 상태에서도 빌드는 200 으로 끝나며 더 작은 산출물을 냈다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApEz8UMi8qoKM9iWKYQho4
`inspect.getsource(fn)` 은 **import 당시** 코드 객체의 `co_firstlineno` 를 **지금 디스크에 있는 파일**에 적용한다. 두 시점이 어긋나면 시작 줄이 밀려 **바로 앞 함수의 소스**가 조용히 돌아온다. 예외도 경고도 없다. ## 실측 오늘 커밋 게이트(376초) 도중 다른 세션이 `backend/routers/local.py` 를 저장했고, `test_sds_and_uds_reads_are_not_confined` 가 "쓰기 봉인이 사라졌다"로 실패했다. 봉인은 멀쩡했다 — 가드가 다른 함수를 보고 있었다. 임시 모듈로 재현했다: 4줄짜리 함수 6개에 8줄을 밀어 넣으면 `fn_2` 조회가 정확히 `fn_0` 을 돌려준다. 거짓 실패는 거짓 통과의 쌍둥이다. 게이트가 근거 없이 빨간불을 내면 사람이 게이트를 안 믿게 되고 `--no-verify` 로 가는 길이 난다. 오늘 나도 첫 진단을 틀렸다. 곁가지로 `linecache` 축도 있다: `checkcache` 는 (크기, mtime) 로만 판단하므로 **같은 크기로 같은 시각에** 다시 쓰인 파일은 낡은 캐시가 그대로 나온다. 모듈 단위 조회 (`inspect.getsource(mod)`)는 줄밀림엔 면역이지만 이 창에는 걸린다 — 실증해서 가드로 넣었다. ## 바꾼 것 `tests/unit/_source_probe.py` 의 `source_of(obj)` — 파일을 지금 읽어 **AST 로 이름을** 찾는다. 줄 번호를 안 쓰므로 밀림에 면역이고 linecache 를 안 거친다. qualname 으로 스코프를 따라가므로 모듈 함수와 동명의 메서드를 구분하고, 데코레이터 줄도 포함한다. 성질이 다르다는 걸 알고 쓸 것: - `inspect.getsource` = "**import 된** 그 코드" (어긋날 수 있는 방식으로) - `source_of` = "**지금 파일에 있는** 그 이름의 정의" 구조 가드는 후자가 맞다 — 검사 대상이 *커밋될 파일의 현재 내용* 이기 때문이다. 반대로 런타임에 로드된 코드를 봐야 하는 검사에는 부적합하므로, 그런 자리는 `# getsource-ok: <사유>` 로 남긴다. 조용한 폴백은 두지 않았다. 이름이 없거나 같은 자리에 동명이인이 있으면 명시적으로 실패한다 — 폴백은 "검사한 줄 알았는데 안 한" 상태를 만든다. **전환 전에 39개 대상 전부에서 `inspect.getsource` 와 글자 단위로 같은 답을 내는지 쟀다 (39/39 동일, 오류 0).** 밀림이 없을 때 답이 다르면 그건 헬퍼의 결함이므로 먼저 확인했다. - 21개 파일 39곳 전환 (`import inspect` 자리를 1:1 로 바꿔 파일별 import 스타일 보존) - `tests/` 는 맨 `inspect.getsource` 를 못 쓰게 하는 전역 가드 — 위반 파일·줄번호를 찍고 대안을 문장으로 안내한다. 스캔이 공허하지 않은지도 함께 단언한다(파일 100개 초과) - 프로덕션 코드(`backend`·`workflow`·`report_gen`·`generators`·`scripts`·`prompts`)에는 `inspect.getsource` 가 0건이라 가드 범위는 `tests/` 로 충분하다 ## 검증 - 전환 21파일 + 신규 가드: 통과 (실행 시점 718 passed / 1 skipped — 다른 세션의 미커밋 테스트가 일부 섞인 수치라 게이트 출력이 정본이다) - **뮤테이션 11/11 죽음 (생존 0 · 무효 0)** — pytest 종료코드로 판정. 동명이인 조용히 통과 / 데코레이터 누락 / getsource 로 되돌림 / 조건부 정의 미탐색 / qualname→name / 이름 부재 묵인 / 모듈 stale 캐시 / 마커 면제 제거 / 낡은 면제 잔존 / 스캔 공허 / 전환 1건 되돌림 ## 안 바꾼 것 `tests/unit/test_router_status_and_write_confinement.py` 3곳은 다른 세션이 편집 중인 (미커밋) 파일이라 남겼다. 같은 이유로 `tests/unit/test_uds_param_grid.py`(그쪽이 지금 만드는 중인 untracked 신규 파일, 2곳 다 모듈 단위라 줄밀림엔 면역)도 남겼다. 둘 다 `_DEFERRED` 에 사유와 함께 적었고, 그 파일들에서 맨 getsource 가 사라지면 **면제가 필요 없어졌다고 가드가 스스로 알린다** — 낡은 면제가 사각으로 남지 않는다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApEz8UMi8qoKM9iWKYQho4
우리 산출물은 함수 하나의 입출력을 통째로 한 칸에 넣었다:
Input Parameters | [IN] (none)
Used Globals (Global) | Name=[OUT] REG_PTT | Type=… | Range=…
정본(HDPDM01 SUDS)은 같은 정보를 파라미터마다 한 행씩 6열로 적는다. 실측하니
정본 416 블록 중 405 가 **글자 그대로 같은 라벨 순서**였고, 행은 세 종류였다
(전체폭 / 라벨+값 / 6칸 독립). 우리는 그 세 종류를 한 종류로 뭉개고 있었다.
라이브 재생성(429 표) 결과:
파라미터 행 0 → 3,099 (정본 3,081)
입력축 재현 0.0% → 83.1% (984/1,184 · 정밀도 84.7%)
기대축 재현 0.0% → 76.2% (1,086/1,426 · 정밀도 88.4%)
라벨 14종 완비 429/429 (절단 0)
문서 반영률 75.62% → 75.62% (불변)
방향 태그 → 열 배정은 정본 실측(교집합 394 함수 · 태그 1,399건)으로 정했다.
IN 은 입력(84.1%) · OUT 은 기대(88.5%) · **INOUT 은 양쪽 모두**(적중 329 중
234 가 둘 다 — 한쪽만 적으면 그 234 를 놓친다) · INDIRECT 계열은 정본이 사실상
안 적으므로(1.0% / 0.4%) 그리드에서 빼되 `Used Globals` 행에는 남긴다.
함께 고친 것:
- 템플릿 경로가 템플릿이 준 행 수로 **고정**이라 늘어난 파라미터가 조용히
잘렸다. 무템플릿 경로는 예전부터 `max` 였다 — 같은 표를 두 경로가 다르게
자르고 있었다.
- 문서 후처리(`_normalize_function_info_tables`)가 완성된 표를 다시 균일
병합해 방금 쓴 그리드를 라벨/값 두 칸으로 접었다. 행 종류를 되짚어 넘긴다.
- `Value Range` 에 범위가 아닌 값이 18칸 나가고 있었다(`( ( U8 )( 0x00U ) )`).
범위를 못 구하면 초기값을 range 에 넣는 폴백 때문이다. 정본은 그 칸에
범위 92.9% · 허용값 열거 1.3% · N/A 5.6% 만 쓴다 → 범위 형태가 아니면 N/A.
초기값은 `Reset Value` 열이 이미 싣는다.
- Logic Diagram 행에 이미지 **파일명**을 넣던 죽은 쓰기 제거. 6·7열(실측상
유일하게 도달하는 폭)에선 라벨 사본 칸에 써서 화면에 나온 적이 없었고,
새 배치에선 살아나 `logic` 본문을 덮는다.
- 라벨 `Precondition` → `선행조건`(정본 414/416). `Used Globals (Global)/(Static)`
은 정본에 없는 우리 필드라 정본의 `사용 전역변수` 한 행으로 합치지 않는다.
⚠ `Value Range` 는 정본과 우리가 다른 주장을 하는 유일한 열이다. 정본은 설계상의
의미 범위를 적는데 우리 `range` 는 92.9%(395/425)가 **타입 폭 그대로**다. 라이브
실측에서도 3,099칸 중 의미 범위는 **0칸**이고 1,844칸 전부 타입 폭이었다. 표시
없이 적으면 "설계가 전 범위를 허용한다"는 세운 적 없는 주장이 되므로
`0 ~ 65535 (타입 폭)` 로 출처를 함께 적는다.
가드 69건(신설 46 + 확장 23) · 뮤테이션 19/19 죽음 · 회귀 7,317건 통과.
⚠ 음성 대조군이 내 결함을 잡았다: 행 종류를 *셀 개수*로 판정했더니 아직 병합 안 된
라벨/값 표(6칸)도 그리드로 오인돼 정규화가 통째로 죽었다 → 내용 기반으로 교체.
⚠ 태그 정규식의 대안 *순서*를 바꾸는 뮤턴트는 등가다(닫는 `]` 가 앵커 — 입력 200개
대조 차이 0). 테스트 공백이 아니므로 진짜 위험(앵커 제거)으로 교체하고 그 사실을
docstring 에 남겼다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011vqDV8YQCkz6F8sZBE1Zxc
준비 게이트는 등록 경로가 없으면 `missing` 과 그 경로만 냈다. 그 문장으로는 세 가지를
구분할 수 없다: ①파일 이름이 바뀌었다 ②폴더가 통째로 옮겨졌다 ③애초에 배포된 적이 없다.
셋의 조치가 전부 다른데 화면은 같은 말을 한다.
## 실측
2026-08-25 에 같은 형태로 두 번 걸렸다.
- `coverage_report_template` 이 등록 경로에 실재하지 않는 `1220 진행` 세그먼트를 달고
있었다 — 빌드는 200 으로 끝나며 **더 작은 산출물**을 냈다
- `es95411_template` 이 가리키던 v1.02 산출물이 v2.01_260629_R 세대로 교체되며
**파일명 자체가 바뀌어** 사라졌다
두 번 다 U: 드라이브를 사람이 열어 폴더 내용을 눈으로 보고서야 원인을 알았다.
## 바꾼 것
`_folder_contents_hint` — 부재일 때 **그 폴더에 실제로 뭐가 있는지**를 사유에 붙인다.
양식 파일을 찾지 못했습니다 — 같은 폴더의 실제 파일:
(KJPDS02_SwTCV) Software Test Coverage Result_v2.01_260629_R.xlsx,
(KJPDS02_SwTR) Software Test Result_v2.01_260629_R.xlsx.
- **판정이 아니라 증거다.** 후보를 고르지 않는다 — `_suggest_revision` 이 "여럿이면 안
고른다"로 이미 정한 규약을 여기서 뒤집지 않는다
- **부재일 때만** 부른다. 있을 때도 훑으면 U: IPC 왕복이 매 판정마다 늘어난다
- 목록 실패는 침묵하지 않는다 — "폴더도 못 봤다"와 "폴더가 비었다"는 다른 사실이다
- 빈 목록을 "폴더가 비었다"로 단정하지 않는다. resolver 계약상 **빈 폴더와 없는 폴더는
구분되지 않는다**(`file_resolver.py` 의 `list_dir` 주석 — 두 모드가 같은 이유로 못 한다)
- 8건에서 자르되 **몇 건을 잘랐는지 말한다**. 조용한 절단은 "그 폴더엔 이것뿐"으로 오독된다
## 라이브 실측이 새로 드러낸 것
전 양식 키를 라이브 resolver 로 훑으니 **KJPDS02 `fault_injection_result` 도 부재**였다
(몰랐던 건이다). 등록본은 DV 세대(`_DV_…v1.01_251205_R.xlsx`)인데 폴더는 PV v2.01 세대로
교체됐다. 같은 세대의 `PDSM Fault Injection Test Case_v2.01_260629_R.xlsx` 는
`01.Test Specification/01.SwITS` 에 있는 시험 **케이스**(규격)라 결과 파일이 아니다.
`switcr_reference`(클라우드 동기화로 뭉개진 이름)도 같은 상태다 — 다만 이건 SwITCR 빌드의
**선택** 입력(스타일 참조)이라 없으면 경고만 난다.
## 검증
- `tests/unit/test_docgen_preflight.py` 104 passed. 설정 의존 4파일 합계 341 passed
- **뮤테이션 6/6 죽음 (생존 0 · 무효 0)** — 종료코드 판정. 힌트 값 폐기 / 있을 때도 훑음 /
목록실패 침묵 / 빈목록 침묵 / 절단 침묵 / cap 미준수
- 라이브 resolver 로 KJPDS02·HDPDM01 전 양식 키 실행
## 안 바꾼 것
**부재 경로 3건을 지우지도 바꾸지도 않았다.** 올바른 대상을 모르는 채 지우면 "그 산출물이
없다"는 사실이 침묵된다. 대신 측정 결과를 `config/swut_meta.json` 주석에 남겨 다음 사람이
같은 조사를 반복하지 않게 했다. ES95411 은 회사 표준 템플릿 트리에도 빈 양식이 없고
(09=SwUT* 3종 / 10=SwIT* 3종 / 11=SwTR 하나뿐) 허용 경로 22곳 전수에도 0건이라,
①옮겨진 경로 ②공식 빈 양식 ③폐지 확인 중 하나가 필요하다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApEz8UMi8qoKM9iWKYQho4
정본 SUDS 와 값 축을 대조하다 `REG_PTT` 의 설명이 `Port E Polarity Select
Register` 로 나왔다. PTT 는 Port T 다. 헤더를 열어 보니 그건 **바로 윗줄**
`REG_PPSE` 의 설명이었다.
volatile PPSESTR REG_PPSE; /* Port E Polarity Select Register */ <- 이 주석이
volatile PTTSTR REG_PTT; <- 여기로 갔다
`_extract_leading_comment` 는 선언 앞을 뒤로 훑어 마지막 블록 주석을 가져오는데,
MCU 헤더처럼 설명을 **선언 뒤 같은 줄**에 적는 파일에선 그 자리에 직전 선언의
꼬리 주석밖에 없다. 그 뒤가 공백뿐이라 "앞 주석" 으로 통과했고, 파일 전체가 한 칸씩
밀렸다.
실측(PDS64_RD): 전역 선언 809개 중 **411개(50.8%)** 가 남의 설명을 달고 있었고,
자기 꼬리 주석 **425개는 통째로 버려지고 있었다**. 산출물에서는 대조 가능한
452칸 중 **93.6% 가 이웃의 설명**이었다.
⚠ 이건 값 부재보다 나쁘다. ISO 26262 설계서에 "이 레지스터는 X 다" 를 **틀리게**
적는 것이라, 빈 칸이면 하지 않았을 주장을 한다.
## 고침
- `_is_trailing_comment` — `/*` 앞에 같은 줄의 코드가 있으면 남의 꼬리 주석이다
- `_extract_trailing_comment` — 자기 줄의 주석을 되살린다(차단만 하면 분모가 빈다)
- 전역 선언은 **자기 꼬리 주석 우선**, 없으면 앞 주석
## 검증
| | 수정 전 | 수정 후 |
|---|---:|---:|
| 산출물 대조 가능 칸 | 362 | 452 (+90 회수) |
| 자기 설명 | 21 (5.8%) | **452 (100.0%)** |
| 이웃 설명(밀림) | **339 (93.6%)** | **0** |
원문 정규식(파서와 독립 경로)으로 408건 대조 → **정합 100%**.
함수 쪽은 정상 leading 주석 353/368 그대로 유지, 오배치 9건만 차단
(`} /* end of l_ifc_init() */` 이 다음 함수 설명이 되던 것).
## 이 결함을 찾아낸 대조를 저장소에 남긴다 (계획서 Phase 3)
- `report_gen/uds_reference_parity.py` — 정본 대조(lxml 직독, **읽기 전용**).
이름 축과 값 축을 **분리**해 낸다. 실측 이름 축 83.1% 인데 Value Range 값 축은
27.8% 라, 한 숫자로 합치면 값 축이 통째로 숨는다.
⚠ 표기차를 불일치로 세면 range 재현율이 거짓으로 **0.0%** 가 된다
(`0x00 ~ 0xFF` = `0 ~ 255`). SUTS R26 이 같은 덫에 걸렸다.
미달은 3분류(방향 오배치 / 표기차·입도차 / 진짜 이름부재) — 2분류로 보면
손댈 대상이 부풀려진다(SUTS R19). 조인은 **함수명**이다(ID 는 체계가 다르다).
- `scripts/uds/uds_quality_cycle.py` — 러너에 `reference_parity` 축. 미측정은
0% 가 아니라 사유를 단 `measured=False` 다. 곁가지로 레거시 E402 1건 해소.
- `report_gen/validation.py` — 저장소 유일의 정본↔생성물 diff 가 행 **수**만
세고 있었다. 개수가 같아도 다른 변수를 적고 있을 수 있다 → 이름 집합·값 집합을
얹었다. 값 판정은 `value_verdict` **단일 출처** 재사용(판정 복제 방지).
## 가드
신규 91건(파서 23 · 대조 34 · SwCom 14 · 러너 10 · 공백 메움 2 등) ·
뮤테이션 **35/35 죽음** · 회귀 **7,426 passed / 10 skipped**.
⚠ 음성 대조군과 뮤테이션이 내 결함 3건을 잡았다: ①분류기가 정규화된 색인 키로
뼈대를 계산해 `u16s_Buf (size: 8)` 같은 표기차를 "진짜 이름부재" 로 세고 있었다
(입력 0→24 · 기대 0→34 로 정정). ②섹션 종료를 `in` 축에서만 확인해 종료를
무시해도 `out` 축으로 새는 걸 못 봤다. ③같은 함수가 여러 표로 쪼개진 경우를 안
만들어 `setdefault` 를 덮어쓰기로 바꿔도 통과했다.
⚠ 뮤턴트 2건은 **관측량을 안 바꾸는 약한 뮤턴트**였다(정본·생성물 양쪽에 대칭
적용돼 상대 지표가 그대로). 테스트 공백이 아니므로 진짜 위험으로 교체했다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011vqDV8YQCkz6F8sZBE1Zxc
…를 때만
사용자 "행 수를 넉넉하게 늘려버려 … 정본 변이를 다잡을 수 있으면 그렇게 진행해야지 제한은 안둬도 된다" · "이제 디폴트는 정본 정도가 아니라 확장이야" · "분석 리포트는 정본과 우리가 확장했을 때 비교하면 우리 시스템의 우월함이 보일거자나" · "이어서 진행하고 커밋까지 해줘".
R86 세 스위트 한 분모 판정에서 SUTS 정본 규모 문서(함수당 24 행)는 정본만 잡는 변이가 KJPDS02_PV 163 · HDPDM01 146 남았고(24 행 안에 들지 못한 확장 탐색 행이 잡는 것 — 상한만 올려서는 안 잡힘), 확장 문서는 0 · 0 이다. 2026-09-19 결정(정본 규모 기본)을 사용자가 바꿨다.
- `generators/tc_profile.py`: `DOC_DEFAULT_PROFILE`(sts · suts · sits → extended) · `default_tc_profile(doc_type)` · `normalize_tc_profile(value, doc_type)`(미설정 · 모르는 값 → 그 문서의 기본, 모르는 값은 공시) · `is_extended` / `is_evidence_enriched(…, doc_type)`. 세 생성기 · 준비 게이트가 문서 종류를 넘긴다.
- 설정 선택지 `["", "reference", "recommended"]`(확장(기본) · 정본 규모 · 권장, 문서별 설명), 도움말 · 게이트 프로파일 행 · 공시 이름표("확장(기본)" / "정본 규모")와 '기본 문서' 를 뜻하던 문구 정리.
- 영향도: SUTS 초안은 기본 문서의 경로(`extended_unit_sequences`), SITS sub-case 상한은 생성기와 같은 규칙, 전체 초안은 표시 상한 없이(확장은 24 행을 넘는다), 자동 재생성 변경 기록에 전후 프로파일 · 프로파일이 바뀐 증감 메모. STS 카드 문구.
- 리포트 `docs/reports/2026-10-14_test_generation_vs_reference.html` 머리를 정본 대 기본(확장)으로.
측정(R86 판정 · 같은 문서): SUTS 기본(확장) PV 3,831/3,833(정본 1,999 · 1.9 배, 정본만 0, 정본 행 수만큼 무작위 +46%) · HD 3,287/3,287(정본 1,533 · 2.1 배, 정본만 0, +43%). 정본 규모를 고르면 PV 3,225 · HD 2,621(정본만 163 · 146).
시험 R87 7(문서별 기본 · 이름 붙인 프로파일 · 술어 · `generate_suts` 기본 = 확장 / `reference` = 정본 규모 / 모르는 값 · 공시 문구 · 영향도 초안 경로 · 문서별 기본을 바꾸면 세 생성기 모두 따름) + 변경 기록 프로파일 시험 1; 기존 시험은 정본 규모 동작을 재던 것은 `tc_profile="reference"` 를 명시해 같은 것을 재게 하고(게이트 상한 · STS 정본 규모 구간 · SUTS 상한 공시 등), 기본을 단언하던 것만 확장으로(약 60 건, 약화 · 삭제 0 — 리뷰 확인); 관련 97 파일 3,754 통과, 프런트 3 파일 282 통과, eslint · ruff 신규 0. deep 리뷰 3 라운드 — W1 영향도 전체 초안이 24 행만 보이며 '전량'(확장은 24 를 넘음 → 표시 상한과 생성 블록 크기 분리) · W2 STS 근거 축이 문서 종류 없이 판정 · W3 '기본 문서' 가 정본 규모를 뜻하던 사용자 문구 · W4 영향도 자동 재생성의 증감에 프로파일 변경분이 섞임(변경 기록에 전후 프로파일 · 메모) · W-R2-1 그 메모가 실제 경로에선 안 뜸(자동 실행이 프로파일을 버림 → 반환에 싣고 이전 문서를 읽었을 때만) · W-R2-2 STS · SUTS 초안이 기본 문서와 다름(근거 축 · 생성 경로를 기본 표로) · W-R3-1 예전 문서의 '모르는 값' 문구가 기록한 프로파일과 어긋남 반영, 3 차 Critical 0 · 승인. 관련 시험 3,755 통과(최종 반영 뒤 관련 4 파일 179 재통과).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
사용자 "sits도 엑셀에 채워지는 내용 봐봐 정본보다 더 낫다는 데이터를 더 보여줄 수 있는게 있는지 우리께 제대로 작성되는게 맞는지도". R87 기본(확장)으로 HDPDM01 SITS 를 HD 정본 템플릿으로 생성해 엑셀을 다시 읽으니, 생성 문서에 정본의 `4.SW Integration Test Spec`(정본 TC 77 개)과 `3-1-1. / 3-1-2. SW Integration Strategy` · `3-2. Sw Integration Strategy` 가 우리 `3.SW Integration Test Spec` 옆에 그대로 남아 있었다 — 생성기는 KJPDS02 정본의 시트 이름(`3.SW…` · `2.SW…`)만 지우고 다시 썼다(두 정본이 번호를 다르게 매긴다). 비교 · 판별 도구도 그 정본 시트를 먼저 읽어 생성 문서를 정본과 똑같이 쟀다. - `generators/sits.py` `_drop_template_spec_sheets(wb)`: 템플릿을 불러온 뒤 통합시험 명세 · 통합 전략 시트를 번호 표기와 상관없이 찾아(`_TEMPLATE_SPEC_RX`) 이 생성기의 이름이 아닌 것을 지운다. 지운 이름은 로그와 `stats_out["template_sheets_dropped"]`. - `generate_sits(tc_profile=)` 주석의 기본(R87: 확장). 측정(정본 시트를 뺀 R87 HD SITS, 엑셀 기준): TC 183(정본 77) · sub-case 2,373(606) · TC 당 입력 30.4 · 기대 27.8 변수(12.0 · 12.2) · 구체 기대값 13,799 칸(9,285) — 기대값이 값으로 적힌 비율은 20.3%(정본 99.5%; 나머지 [검증 필요] — 관측 대상 미모델 12,331 · 조건부 컴파일 미해결 6,144 · 전역 미모델 5,475 …). 소스 인터페이스 결함 741 판별 480(64.8%) 대 정본 318(42.9%). - 리뷰(standard) 반영: W1 지운 시트 목록이 품질 리포트까지 가지 않음 → `quality_report["template_sheets_dropped"]` · 공시 `sits_template_sheets_dropped` · W2 우리 이름으로 재사용하는 `2.SW Integration Strategy` 에 KJPDS02 정본 블록(`SwIT_` 51 개 · 2,766 칸)이 남음 → 머리(1~5 행) 아래를 비우고 씀 · I1 지운 시트가 활성 시트면 첫 시트로 · I2 docstring. 시험 R88 7(HD 형 정본 시트 삭제 · KJPDS02 형 이름 유지 · 전략 시트의 정본 블록 제거 · 활성 시트 · 기록 · generate_sits 리포트 · 공시 도달 · 다른 시트 무관 — 수정을 되돌리면 실패 확인), SITS 관련 432 통과. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
사용자 "SITS 기대값 [검증 필요] 줄이는 방법도 찾아봐" → "조사 끝나면 1번부터 고쳐줘"(1번 = 구조체 · 레지스터 전역 미모델 묶음). HDPDM01 은 APP(`PDS64_RD`)와 BOOT(`PDS64_FBL`) 두 프로그램이고 같은 이름의 함수가 11 개다(`main` · `EEPROM_SetByte` · `SCI0_Init` …). 이름 키 맵 다섯 곳이 두 빌드를 합쳐, R87 SITS 의 BOOT TC 가 BOOT 에 없는 APP 레지스터(`REG_*`)를 3,923 칸 관측했다. - `generators/c_project_context.py`: `build_context(context, root)` · `build_scopes` 의 callee 쓰기 closure 를 빌드마다. - `generators/integration_oracle.py` · `c_source_oracle.py`: 객체 · 구조체 루트 이름 중복을 빌드마다(`CalleeProvider.ambiguous_for`, `_World`), 두 빌드가 정의한 진입은 흐름이 가리킨 정의로(`entry_source_path`), 포인터 대상 공시는 빌드별 closure 합산(`pointer_flow.summarize_pointer_targets_all`, SUTS 도). - `generators/sits.py`: `_build_views` · `_split_by_build` — 흐름 수집의 이름표 · 호출 그래프 · 모듈을 진입 함수의 빌드로, 흐름에 `build` · `entry_source_path` · `entry_builds`(두 빌드가 정의한 진입 이름은 설계 ID 를 정하지 않음 — 함수 전체 기준이라 상한과 무관), 전략 · Related 시트도 빌드별 그래프(`views_by_build`), 빌드 분리 루트는 파서가 쓴 루트(`project_context["roots"]` — 웹 폼의 경로 표기와 무관). - `report_gen/uds_generator.py`: 매크로 행에 빌드 기록 → 빌드별 매크로 맵(`_maps_for`), 문서 범위 밖(verify=X) 파일도 그 빌드의 컴파일 문맥(BOOT SFR 헤더 `mc9s12zvl64.h` — 매크로 · 전역 이름은 빌드 맵에만, 문서 표는 그대로), 간접 전역 전파는 레코드마다 자기 빌드의 callee · 2 홉 간선으로, 마지막 복사 루프는 다른 파일 쌍둥이에 첫 정의의 목록을 복사하지 않음, 공시 `globals_scan.build_split`. source sections 캐시 v55. - 준비 게이트(`docgen_test_materials._measure_sits`) · 영향도 초안도 같은 빌드 분리. 측정(HDPDM01 SITS, 엑셀 기준): BOOT 의 APP 레지스터 관측 3,923 → 0, 자기 레지스터(`r_*`) 0 → 5,248, BOOT 관측 칸 8,655 → 10,387, BOOT `main` TC 신설. 도출 13,799 → 13,751(−75 칸이 한 TC — 매크로가 풀려 진입 입력이 늘자 템플릿 입력 열 82 상한에 관측 대상 초기값 입력이 밀림; 근본은 oracle 이 읽는 변수로 입력 고르기, 다음 후보). 인터페이스 결함 판별 480 → 475(같은 −5, 다음 통합 라운드에서 되찾음 — 483). SUTS(엑셀 기준 세 스위트 한 분모) HDPDM01 3,287/3,287 · 정본만 0 · 잃음 0(R87 과 같음). 웹 경로(`/api/local/sits/generate`, file_mode cloudium, U: 원본, STP, 레지스트리 루트 문자열)가 스크립트 생성과 칸 단위 동일(64,437 행 차이 0). 입력 문서는 Cloudium 원본과 해시 대조(HD 8 동일, PV 등록판 동일). - 리뷰(deep) 반영: C1 문서 범위 밖 SFR 헤더를 안 읽어 BOOT 가 자기 레지스터를 못 얻음(첫 판의 개선 상당수가 삭제였다) · W1 쌍둥이 두 번째 빌드 간접 전파 0(루트 순서 의존) · W2 포인터 공시 첫 빌드만 · W3 설계 ID 거부의 상한 의존 · W4 게이트 · 초안 미분리 · W6 시험 공백 · I3/I5/I6. 시험 R89 15 + 파서 8(수정마다 되돌려 실패 확인, 생존 0), 관련 회귀 1,005 통과. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…렸던 것은 요구 끝에 새 ID 사용자 "설정이나 이런건 최대 우리 효율이 나올 수 있는 걸로 디폴트 값을 변경해야해". STS 기본(확장) 문서도 함수당 · 요구당 TC 상한(5)과 TC 당 스텝 상한(15)이 함수의 실제 분기와 TC 의 뒤쪽 스텝(기대결과)을 조용히 잘랐다. HDPDM01 정본을 템플릿으로 쓰면 정본 TC 59 개가 든 `3.SW Integration Test Spec` 시트가 생성 문서에 남았다. - `generators/sts.py`: 확장 구간은 예전 상한으로 만든 TC 가 그 자리 · 그 ID(스텝 잘린 TC 는 같은 ID 에서 채움), 상한에 빠졌던 분기 · 오류 경로 · 경계값 TC 는 그 요구 끝에 새 ID(`cap_appended` — 요구 경계 TC 뒤에 번호, 요구의 마지막 TC 바로 뒤에 넣고 재정렬하지 않음). 정본 규모 블록이 일부만 시험한 함수의 나머지도 붙고, 확장 문서에선 그 블록의 스텝 잘린 TC 도 채운다. 분기 짝짓기는 완전 일치 먼저(`_pair_branches`), 손실은 생성 한 번마다 고유 기준(`_cap_losses` → `cap_lost_*` · `cap_cut_step_tcs` · `extended_appended_tcs`). 템플릿의 정본 시험 명세 시트 제거(`_drop_template_spec_sheets`, 정확히 `3.SW Test Spec` 만 남김). - 공시 · 게이트 · 설정 도움말: `sts_caps_cut` · `sts_template_sheets_dropped`, 확장 문서의 상한 · 절단 · 경계값 · 함수 커버리지 문구를 프로파일에 맞춤, 준비 게이트 `_CAPS_LIFTED_BY_EXTENDED`. - 영향도 초안: STS 총량 상한 없음, 경계값 TC 표시는 정본 규모 함수당 상한 기준(프런트 문구 포함), SITS sub-case 표시 기본 = 문서 전부. 측정(AI 없음, 기본 프로파일): HDPDM01 997 → 1,091 TC(+94), 기존 997 의 ID · 제목 · 스텝 동일 · KJPDS02_PV 2,240 → 2,405(+165), 기존 동일. 두 프로젝트 모두 버린 TC 0 · 스텝 잘린 TC 0. HD 출력에서 정본 시트 제거. - 리뷰(standard 2 회) 반영: 1 차 W1 · W2 · W3 · I4, 2 차 C1(앞부분 일치 우선이 중첩 else-if 사다리에서 분기 하나 누락 · 하나 중복을 0 으로 공시) · W1(확장 문서의 기존 공시 다섯 곳 거짓) · W2 · I1 · I2 · I3 · I5 · I6 · I9. 시험 R90 13 + 템플릿 시트 5(수정마다 되돌려 실패 확인 6/6), STS 관련 회귀 1,154 통과. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
사용자 "SITS 기대값 [검증 필요] 줄이는 방법도 찾아봐" → "1번부터 고쳐줘". R89 뒤 HDPDM01 SITS 미확정 칸 중 프로그램의 사실이 아닌 사유 셋. - F4 조건부 컴파일: 트리 밖 헤더를 읽지 않은 단위의 include guard 꼴 예약 이름(`_MC9S08SC4_H`)은 정의되지 않음으로 판정 · 가정 기록(`assumed_header_guards`); `switch` 안 `case` 를 가진 `#if` 묶음은 결정된 팔만 본문으로(`switch_items` / `switch_layout`). - F5 미정의 동작: 모르는 값이 고른 경로의 미정의 동작은 버리지 않고 결과를 미상으로 둔 채 실행 · 경로를 고른 분기 전부와 함께 공시(`<kind>_on_unknown_branch`), 유일 경로이거나 모든 경로가 증명하면 거부. `sizeof` 값 · `p++` 순서 · 열거형 입력 범위와 저장 후보(`short` 포함) · `&&`/`?:` 피연산자 판정 일관화. - F8 SITS 입력: 서브케이스 값을 선언 타입이 담는 값으로, 설계서 범위가 있으면 그 안(정상) · 바로 밖(강건성). 측정(HDPDM01 SITS, 엑셀 재판독): 확정 기대값 13,751 → 18,370(+34%), 확정값 있는 TC 91 → 96. 조건부 컴파일 미결정 6,144 → 0 · 선언 타입 밖 입력 1,920 → 144 · 미정의 동작 3,712 → 3,605 · 경로 예산 1,469 → 4,175 · 모델 밖 관측 대상 11,995 → 14,928(새로 닿은 코드의 레지스터 — 저장소 모델은 별도 커밋). 인터페이스 결함 판별 475 → 496(R87 480 · 정본 318), 잃은 결함 0. - 리뷰(deep): F4 · F8 승인, F5 1 차 C1(경로 가지치기가 "미정의 동작 없음" 을 증거로 써 값이 더 확정되는 역설) · 2 차 C2(32 비트 int 대상 열거형 저장 후보에 short 누락) · W1 · W2 · W3 반영. 시험 F4 31 + F5 26 + F8 59(수정마다 되돌려 실패 확인), oracle 관련 회귀 2,338 통과. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
사용자 "suts도 빠르게 해결 할 가능성이 있겠네 1번부터 suts 문제까지 병렬로". SUTS 의 확정 기대값은 소스로 증명되지만 설계(SwUDS)가 허용한 범위 안인지는 아무도 보지 않았다 — 둘이 다르면 어느 쪽이든 결함이다. - `generators/suts_design_check.py`(새 모듈): 행마다 출력 · 행이 쓴 입력 · stub 값을 SwUDS 입출력 표와 HSIS 범위로 대조해 Test Evidence 의 Design Check / Design Basis 열에(기존 열 · TC ID 불변). 단정 `outside_design` 은 모든 읽기에서 밖일 때만, 진술 없는 입력 · stub · 부호 표기 · 값 둘 목록 · 쌍둥이 정의 · 못 읽은 칸은 약한 후보, 어긋날 수 없던 대조는 '범위 안' 에 넣지 않음. 근거 칸은 지정한 SwUDS 의 원래 파일 이름. - `generators/uds_unit_io.py`: 출력 행 · 값 둘 목록 표시, 열쇠 함수 단일화(`uds_param_key`, 기존 소비처 결과 차이 0), 표기만 다른 행은 상충 아님. - 공시 `suts_design_check`(단정 대조 0 칸이면 경고 — "밖 없음" 아님) · issue_explainer 조치 문구. 측정(HDPDM01): 소스 계산 칸 8,743 중 단정 설계 밖 52 칸 · 출력 4 · 함수 3 — 4 개 모두 소스 `#define` 과 직접 대조해 실재 어긋남(`ACTIVATED = 0x04U` 대 설계 `0x00~0x03` 등). 약한 후보 287, 범위 안 2,393, 어긋날 수 없던 대조 2,086. - 리뷰(deep 2 회) 반영: 1 차 C1(근거 칸이 Cloudium 임시 파일 이름 — 웹과 스크립트 결과가 갈림) · W1~W6 · I1~I9, 2 차 Critical 0 · W-A(쌍둥이 정의) · W-B(단정 0 칸의 '밖 없음') · W-C(판정 경계 시험). 시험 50(웹 두 번 = 스크립트 칸 단위 동일 포함), 변이 1 차 63/63 · 2 차 판정 경계 4 개 포함 kill, SX 관련 회귀 415 통과. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…어가 끼어들 수 있으면 미상 사용자 "SITS 기대값 [검증 필요] 줄이는 방법도 찾아봐" → "1번부터 고쳐줘"(F6). R91 뒤 HDPDM01 SITS 미확정 사유 1위는 '모델 밖 관측 대상' 14,928칸이었고, 대부분 레지스터다. `REG.Bits.TEN` 으로 쓰고 `REG.Byte` 로 읽는 공용체 · 비트필드 객체를 오라클이 따로 노는 이름으로만 보았다. - `generators/c_project_context.py`: 공용체 · 비트필드 객체를 바이트 저장소 하나로 배치한다(`_storage_object` · `_storage_layout`). 바이트 순서와 비트필드 할당 방향은 빌드 대상(`.cproject`)과 `#pragma` 에서 정하고, `NAME_MASK` · `NAME_BITNUM` 매크로와 대조한다. 순서를 못 정하면 가능한 배치를 다 계산해 모두 같은 값일 때만 값을 낸다. 단위마다 배치가 다르면 사유를 남긴다. cpc SCHEMA 27 · sections "v57". - `generators/c_source_oracle.py`: volatile 레지스터는 행이 준 값을 하네스 메모리로 읽는다. 레지스터 플래그를 기다리는 폴링 루프는 끝난다고 가정하고 공시한다(끝내는 플래그 값이 있고 루프가 다른 상태를 바꾸지 않을 때만). - 건전성 규칙(리뷰 5회): - 해석하지 않은 callee 뒤에는 모든 volatile 저장소 객체가 미상이다(`register_read_by_callee_not_interpreted:<callee 전부>`, 시퀀스 stub 은 면제). - 루프 본문이 읽는 레지스터는 반복마다 누적한다(`polling_loop_body_reads_flag`). - stub 이 아닌 호출과 나란한 레지스터 피연산자는 순서 미정이다. - 정수 주소 · `<hardware>` 포인터로 쓰면 레지스터 전체를 무효로 한다(C-R5-1). - 가상 실행 복사본은 경로 선택자를 비운다(W-R5-1). - 폴링 탐색 메모는 제거했다. - clang 대조 스크립트 3종과 source_findings 는 저장소 모델 칸을 `unchecked:storage_model` 로 표시한다. - 공시 `_register_storage_note`(SUTS · SITS)와 카운터 `derived_on_storage_model` · `derived_on_register_memory` · `derived_on_assumed_polling_exit`. 측정(HDPDM01 SITS, HEAD 와 같은 스냅샷 · 같은 측정기): - 확정 기대값 18,370 → 21,358(+2,988). HEAD 확정 칸 18,370 은 전부 같은 값으로 남았다(잃음 0 · 값 바뀜 0). - 확정값이 있는 TC 96 → 116. 인터페이스 결함 판별 496 → 497(잃음 0, 정본 318). - 미확정 사유 변화: 모델 밖 관측 대상 14,928 → 9,610, 모델 밖 전역 6,009 → 1,628. - 생성 시간 515 s → 734 s. SUTS HD(같은 스냅샷, 세 묶음 판정): 변이 판별 3,287/3,287 · 정본만 잡는 변이 0 · 정본 판별 1,533 그대로(R87 와 같음). 확장 행 10,749 → 10,757. 확정 기대값 칸 229,840 중 저장소 모델 1,591 · 폴링 종료 가정 84. 정본 SUTS 레지스터 칸 대조(1,385칸): 불일치 0. 일치는 4차 리뷰 전 727 에서 보수 규칙 뒤 246 으로 줄었다(미상 955 — 649칸이 해석하지 않은 callee 뒤). 다음 라운드(callee contract)에서 건전하게 되찾을 대상이다. - 리뷰(deep 5회): 4차 C-R4-1~4(stub 행이 다른 행의 탐색 결과를 재사용 · 비해석 callee 뒤 레지스터 유지 · 루프 본문 읽기 · 호출과 나란한 레지스터), 5차 C-R5-1(정수 주소 쓰기) · W-R5-1 반영. 시험 `test_register_model_r90.py` 109개. 관련 회귀 3,792 통과 · 2 skip. 변이: 5차 7/7 kill(T02 는 화이트박스 시험 추가 뒤 kill 확인), 4차 리뷰어 8/8, 수정자 38/40(1개 동치 · 1개는 관측 형태 없음). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…분만 보던 절단을 센다 사용자 "다음은 callee summary contract → oracle의 실제 읽기 기반 입력 선정 순서로 진행해줘". 읽기 기반 입력 선정 설계의 S0 · S2. - S0 숨은 입력: - 전역 상태 행(GLOBAL)은 간접 전역을 입력으로 두고 기대값으로 적는데, 그 이름이 TC 의 열에 없었다. 그러면 기대값이 문서에 없는 자극으로 도출된다. - 그런 이름을 기존 열 뒤에 첫 사용 순서로 붙인다(기존 열 · 값 · TC ID 불변). 붙인 열은 그 이름을 쓰는 행에만 값이 있고 다른 행은 비운다(그 행은 두지도 단언하지도 않는다 — 공시에 적음). - 입력 열 상한(82)에 막힌 행은 확정 칸을 `[검증 필요] input_not_in_document` 로 내리고 집계에서도 뺀다. 기대값은 상한(113)을 넘으면 열로 보이지 않을 뿐 단언하지 않는다. - 공시 `sits_hidden_inputs`, 품질 리포트 `hidden_io_columns`. - S0 조용한 절단: 진입 매개변수 20 · 진입 전역 15 · 진입 출력 10 · callee 목록 · 간접 전역 목록에서 앞부분만 보던 자리를 센다. 다른 경로로 열이 된 이름은 빼고 맞춘다. 공시 `sits_candidates_truncated`, 이름은 `candidate_cut_names`(TC · 축마다 앞 20개 + "외 N"). - S2: 입력 도메인 해석을 `_input_domain_resolver` 로 뽑아냈다. 기존 열의 결과는 골든 픽스처로 바이트 단위까지 같은지 고정했다. 다음 단계(S3 읽기 기반 입력 추가)가 쓸 순수 함수 `needed_inputs` · `exclude_needed` · `rank_needed` · `select_values` 는 새 모듈 `generators/sits_read_inputs.py` 에 둔다. 측정(HDPDM01 SITS, R93 과 같은 측정기): - 숨은 이름을 쓰는 TC 4개에 입력 열 4 · 기대 열 3 을 붙였다. 상한 초과 0, 내린 확정 칸 0. - 확정 기대값 21,358 은 칸 단위로 R93 과 같다. 인터페이스 결함 판별 497 도 같다(잃음 0). - 새로 센 조용한 절단: 진입 전역 167 · callee 출력/전역 101 · 간접 전역 166 · callee 매개변수 1. - R93 위에 얹으며 찾은 결함: R93 이 확정 칸에 단 근거 필드 셋(`polling_loops_assumed` · `storage_objects` · `storage_registers`)이 칸을 내릴 때 떼는 목록에 없었다(R94 가드 시험이 잡음). SUTS `render_row_io` 의 같은 목록도 빠져 있었다. 목록을 `test_evidence.DERIVATION_FIELDS` 하나로 모으고, SITS 내리기는 R93 통계도 내린 칸만큼 뺀다. 출력 영향 없음(SUTS 는 그 필드를 확정 칸에서만 읽고, HD SITS 는 내린 칸 0). 시험 2 개 추가, 변이 5/5 kill. - 리뷰(deep): Critical 0. W1(이중 계상) · W2(시험) 반영. 시험 60 신규 + 기존 1,431 통과. 변이 20/20 · 8/8 · 10/10 kill. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… 방식에 같은 규칙을 적용한다
사용자 "다음은 callee summary contract → oracle의 실제 읽기 기반 입력 선정 순서로 진행해줘. 공통 규약에는 읽기·쓰기·레지스터 접근·반환값·종료 여부·경로 조건을 포함하고, 세 가지 callee 처리 방식에서 동일하게 적용되는지 검증해줘. 기존 검출 성능이 떨어지지 않는지도 확인해줘."
호출은 네 갈래로 만들어졌다: 해석(I), 효과만(E, write closure), 시퀀스 stub(S), 미지 코드(U). 판정 지점 12곳이 저마다 closure 필드를 직접 읽었다. 그래서 한 방식에 넣은 측면이 다른 방식에서는 빠졌다(설계 G4~G13).
- `generators/callee_contract.py`(새 모듈):
- `CalleeSummary` 는 정적 요약이다. 판정 지점은 필드가 아니라 술어(`may_change` · `effectful` · `inert` · `may_read` · `volatile_reads` · `stub_hedge`)를 묻는다.
- `CallOutcome` 은 호출 자리 · 행마다의 결과다. 측면은 경로 · 종료 · 읽기 · 레지스터 · 쓰기 · 출처 · 반환이고, 생산자 넷이 만들고 `apply_call_outcome` 이 측면마다 소비자 하나로 적용한다.
- 데이터클래스는 모두 frozen · 키워드 전용 · 기본값 없음이다. 측면을 더하면 모든 생산자가 TypeError 로 멈춘다. `MODE_RULES` 가 방식 × 측면마다 무엇을 하는지 적는다. `REFUSAL_CLASS` 는 거부 사유 분류의 단일 표다.
- 2~3단계(구조 이전)는 동작을 바꾸지 않았다. 골든이 바이트 단위로 같다.
- 4b(G5, 종료): cpc 에 함수별 종료 사실과 closure 판정을 더했다.
- 효과만 callee 가 결코 반환하지 않으면 실행을 `non_terminating_loop:<callee>` 로 거부한다(해석과 같은 분류).
- 반환이 증명되지 않으면 값을 두고 callee 이름을 근거에 적는다(`callee_termination_assumed`, 공시 카운터 `derived_on_assumed_callee_termination`).
- 4c(G6, 경로): 효과만 callee 의 미정의 동작은 분석하지 않았다는 것을 그 이름과 함께 적는다(`callee_ub_unanalysed`, `derived_with_callee_ub_unanalysed`). 해석한 호출자는 합류한 실행의 이름을 올려 보낸다.
- 1차 리뷰 반영: W1(`_refused` 시험) · W3(필드 소비 AST 시험 + `NOT_YET_CONSUMED`) · I1(거부해도 stub 출처 유지) · I3(우회 4형 래칫) · I5 · I6(빈 closure 캐시 안 함) · I2/I4/I7(문서 + 생산자 순수성 시험).
- 통합(R94 위): 새 근거 필드 둘을 `test_evidence.DERIVATION_FIELDS` 에, 카운터 둘을 `sits_read_inputs._DERIVATION_COUNTERS` 에 넣었다. 통합 oracle 의 필드 · 카운터 짝이 내리기 목록에 다 있는지 구조 시험으로 고정했다. cpc SCHEMA 28 · sections "v58"(27 문맥에는 종료 사실이 없다).
측정(HDPDM01, R94 와 같은 측정기 — SITS 칸 비교 sits_cell_lost · 인터페이스 판정 wt_h92 · SUTS 세 묶음 판정 wt_h94):
- SITS: 확정 기대값 21,358 칸이 칸 단위로 R94 와 같다(잃음 0 · 행 2,574 = 2,574). 인터페이스 결함 판별 497 — 판별한 결함 집합이 같다(잃음 0 · 새로 0).
- SUTS 확장: 판별 가능 3,321 중 3,321(정본만 잡은 결함 0), 정본 1,550. R87 기준 판정 대비 잃음 0 · 더 잡음 34. 판별 가능 수가 3,287 → 3,321 로 늘어난 것은 판정 oracle 이 R94 판(R93 레지스터 모델 포함)으로 바뀐 몫이다(R95 효과 아님). 기록된 확정 칸 229,840 은 R93 과 같다(R85 재생 대비 `g_SysEepromCtrl_Reset` 6칸 차이는 R91 부터 같음).
- 시험: 대상 4,021 통과(xdist 작업자 1개가 메모리 부족으로 죽은 파일 test_sts_ai_path_r54 는 따로 17/17).
검증: 적합성 하네스(방식 T ≥ I ≥ E ≥ U, 형태 S1~S16) — INVENTION · CONTRADICTION 0, 58 통과 · 2 xfail(남은 G 항목). 구조 31 · G5/G6 36(goto · trap · 정의 없는 이름 · 불투명 매크로 형태 포함). 골든 분류(기준 R93): 형태 215행 · HD 380행 · 정본 레지스터 1,385칸 · HD 전체 4,808행 · 차분 퍼즈 13,896행에서 "그 밖의 차이" 0 — 차이는 공시 추가와 G5 거부뿐, 잃은 확정 칸은 형태 S4 의 4칸(하네스가 지어낸 값으로 판정한 칸)뿐. 변이 m4(4b 끔) · m5(4c 끔) kill, 통합 변이 3/3 kill.
리뷰(deep 4회):
- 1차(2~3단계) Critical 0. W1 · W3 · I1~I7 반영.
- 2차(4b/4c): W1 — goto 로 루프를 빠져나가는 callee 를 "끝나지 않음이 증명됨" 으로 판정해 HEAD 가 내던 값을 잃고 사유가 거짓이 됨 → goto 는 출구. W2 — U 방식은 이름을 안 붙이는데 공시가 모든 미해석 callee 로 읽힘 → 범위 명시. I1(정의 없는 이름) · I3(MC/DC 값 의존 거부) · I4 · I5(oracle 스키마 6) · I8 반영.
- 3차: W3 — I1 수정이 넓어 진짜 trap(`DI; for(;;){}` · 루프 안 프로토타입 호출)을 E 는 값, I 는 거부로 갈라 놓음 → 출구 후보를 "읽은 파일 어디에도 정의가 없는 이름" 으로 좁히고 closure 단계에서 판정.
- 4차 LGTM(Critical · Warning 0). 남은 Info: 이름으로 부르는 함수 포인터 호출은 정의 없는 이름으로 보수 판정, 매크로 한 단계 아래의 정의 없는 이름은 E · I 모두 거부.
- 정의 없는 이름이 든 무한 루프는 E 가 "증명 안 됨", I(`_endless`)가 거부로 갈린다. 어느 쪽도 틀린 값을 내지 않으며 MODE_RULES 에 알려진 차이로 적었다.
- 리뷰 반영 변이 11/11 kill.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…로 공시
사용자 "필요한 문서들 필요한 부분들 근본적인것들 다 잘 체크해서".
정본 대조 판정기의 분모를 열어 보니 정본 SwUTS 가 시험하는데 우리 SUTS 에 없는 함수가 HD 11 · PV 37 이었고, 판정기는 그 함수들을 분모에서 뺐다("정본만 잡은 결함 0" 은 그 함수들을 빼고 낸 수치). 빠진 이유는 SwUDS 기반 시험 범위다.
- HD 11: SwUDS v1.07 에 있다. 머리말 13 개가 `SwUFn_NNNN: 이름` 규칙과 달랐다(콜론 없음 8 · 대문자 `SWUFn_` 2 · 둘 다 2 · 개명 꼬리 `<- 옛이름` 1). 판독기가 머리말로 보지 않아 설계 ID · 입출력 표를 잃었다.
- PV 37: SwUDS v3.03 에 이름이 없다(설계서 누락 후보). 범위 판정 문장은 진행 메시지로만 나가 산출물 · 공시 어디에도 이름이 남지 않았다.
- `generators/uds_design_ids.py`(설계 ID · 입출력 두 로더의 단일 판독기):
- 설계 ID 낱말을 대소문자 무관으로, ID 는 `canonical_design_id` 로 `SwUFn_` 표기(정본 SwUTS 도 그렇게 적는다).
- 콜론 없는 머리말은 문단 전체가 ID · 식별자 · (선택) `<- 옛이름` · (선택) 괄호 표시 하나일 때만. 빠른 거름도 대소문자 무관.
- 개명 꼬리를 `renamed_from` 으로 떼고 남은 표시로 삭제 · 표시를 판정한다.
- 머리말에 붙지 않은 함수 표의 Name 행을 `unattached_tables` 로 낸다(HD `EnUserSwUFn_2302: PP0_MOTOR_PWM_Init` — 머리말 오타로 표가 조용히 버려졌다).
- `variant_headings` · `unattached_tables` 를 두 로더가 같게 내고 SwUDS 판독 공시가 적는다.
- `generators/suts.py` `scope_units`: 범위 적용과 요약을 한 함수로(`apply_scope` 는 그 앞 두 값). 요약은 실제로 적용한 범위(`suds` 요청이어도 설계 ID 가 0 이면 소스 전체), 뺀 함수 이름 · 파일 · 사유(삭제 표시 · 동명이인 · 표만 있음 · 머리말 표기 · 별칭 충돌 · SwUDS 에 없음), 판정 문장. 품질 보고서 `test_scope`, 공시 `suts_test_scope`(범위별 라벨 · 톤, 사유별 실행 가능한 안내), 화면 설명 키.
두 실제 SwUDS 전수: PV 머리말 989 는 원래도 전부 읽었고 변화 0. HD 머리말 416 → 429, 설계 ID 390 → 403(기존 390 의 (이름, ID) 해시 동일), 고아 함수 표 1.
측정(HDPDM01, R95 와 같은 측정기 — SUTS 세 묶음 판정 wt_h94 + 분모 밖 함수 판정 r96_missing):
- SUTS: 시험 unit 391 → 404(설계 ID 를 되찾은 13). 정본이 시험하는데 우리에게 없던 11 함수가 들어와 그 함수들의 판별 가능 결함 294 를 전부 잡는다(정본 173, 정본만 0). 기존 223 함수는 R95 와 같다(3,321/3,321 · 확장 행 10,757). HD 전체: 판별 가능 3,615 중 우리 3,615 · 정본 1,723 · 정본만 0 — R95 의 정직한 수치(분모 밖 173 포함 정본만 173)가 해소됐다. 정렬된 정본 함수 234 중 우리 문서에 없는 함수 0.
- SITS: 확정 기대값 21,358 → 21,362(+4) · 잃음 0. 값이 바뀐 103 칸은 전부 같은 행의 입력도 바뀐 칸이다(새로 읽은 SwUDS 입출력 표가 입력 범위를 정함 — `SwITC_SwUFn_1120` 13 · `SwITC_SwUFn_3334` 90). 합성 TC ID 2 개가 설계 ID 를 얻었다(`SwITC_132` → `SwITC_SwUFn_1130`, `SwITC_137` → `SwITC_SwUFn_1131`). 인터페이스 결함 판별 497 — 판별한 결함 집합 동일.
- 시험: 측정 트리에서 R96 대상 392 통과.
검증: 시험 48 신규(작은 소스와 합성 SwUDS 로 실제 생성 · 실제 HD 문서 해시 고정 포함), 관련 회귀 464 통과. 변이 27/27 kill.
리뷰(deep 2회): 1차 C1(범위 공시가 실제 범위와 무관하게 "SwUDS 기반 · 정본과 같은 범위") · W1~W4 반영, 2차 W1 · W2 반영 후 LGTM.
남은 것: 같은 머리말을 읽는 생성 경로 밖 판독기 4 곳(SwUT 빌더 swut_swuds_parser 가 다음 후보 — 대소문자 구분이라 `SwUTC_SwUFn_1130~1134` 를 못 찾는다). 동명이인 설계(APP/FBL 쌍둥이)는 이름만으로 못 골라 범위에서 빠진다.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d 로 귀속을 판정하고, 맞지 않으면 쓰지 않고 공시 사용자 "필요한 문서들 필요한 부분들 근본적인것들 다 잘 체크해서". 저장소 공용 `docs/` 의 두 맵이 프로젝트를 가리지 않고 파일 · 함수 이름으로 적용되고 있었다. - 귀속 실측: `uds_function_swcom_override.json` 251 항목은 HDPDM01 것이다(HD SwUDS v1.07 대비 이름 251/251 · ASIL 250/250, KJPDS02_PV SwUDS v3.03 대비 223 · 220/222). 주석 · 공시 · 계획서 P7 의 "KJPDS02 정본 스냅샷" 은 틀린 전제였다. `component_map.json` 도 경로 키로 HDPDM01 것. - 오염은 PV 쪽: PV 생성에 HD 의 두 맵이 적용돼 PV 파일 35 개가 시험 면제(verify=X)로 판정돼 UDS 소스 단계(→ SITS)에서 빠졌고(PV SwUDS 가 설계한 함수 25 포함 — SUTS 는 면제로 세기만 했다), PV 함수 223 개가 HD 의 Related/ASIL 을 받았다. - 프로젝트 설정 엔드포인트가 맵을 공용 `docs/` 에 써서 다른 프로젝트의 맵을 덮었고, SDS 구조 표가 아니라 파티션 이름 유사도로 맵을 추정했다. - `report_gen/project_maps.py`: `docs/project_maps/<key>/project.json`(scm_id · project · source_roots · 맵 출처) + 맵 파일. `resolve_project_maps(source_root, *, scm_id=None)` 가 종류별 상태(applied · none · not_this_project · ambiguous · invalid · no_roots)를 낸다 — 생성 루트가 한 매니페스트 루트 안에 모두 들 때만 적용(fail-closed), SCM id 는 루트가 겹칠 때만 고르고, id 로 찾은 매니페스트도 루트가 안 들면 루트 판정과 같은 답 + `roots_moved`. 옛 공용 파일은 더 읽지 않는다. 지문이 소스 단계 캐시 키 · 서명 · 준비 게이트 캐시에 들어간다(sections 스키마 v59). - 소비처: `requirements._load_component_map` · `component_map_from_rows` · `component_path_key_matches`(경로 키 normcase 비교 단일 출처), `generate_uds_source_sections`(명시 인자 > 프로젝트 맵 > 없음), SUTS `supplement_override_only` · `collect_unit_functions(component_map=)`, SUTS/SITS/STS 품질 보고서 `project_maps`, UDS 이슈 `project_map_not_applied`. 한 생성에서 맵은 한 번만 판정해 끝까지 넘긴다. - 이동: HD 두 파일을 `docs/project_maps/hdpdm01/` 로 바이트 그대로. PV 는 PV SwDS 의 소프트웨어 구조 표로 만든 맵 `docs/project_maps/kjpds02_pv/`(99 행, verify=X 없음 — 문서에 면제 근거가 없다). - 설정 엔드포인트: 구조 표 우선, 프로젝트 디렉터리에 매니페스트와 함께, 같은 프로젝트 재생성은 409(overwrite + `.prev`), 쓰기 3 개는 관리자 전용, 내보내기가 맵 디렉터리를 덮지 못하게(403), 비 docx 는 422, 오류 본문은 앱 봉투 — 화면이 같은 해석(`api.js` `toApiError`)을 쓴다. 측정(R96 과 같은 측정기, 파일 모드 고정): - HD: SITS 가 R96 과 칸 단위로 같다(두 번). 소스 단계 payload 가 두 루프 모두 e26ff66 와 같다. SUTS(확장)도 R96 과 칸 단위로 같다 — 시트 9 개 전부(시험 명세 16,160 행 · Test Evidence 270,464 행 · MCDC Design 1,284 행 포함) 차이 0, 품질 보고서 23 항목 동일. 생성 정점 메모리 5,372 MB(xlsm 쓰기 단계 — Test Evidence 를 쓰는 동안; R96 정점은 기록이 없어 비교 못 함). - PV SITS(HD 맵이 섞인 판 대비): TC 361 동일, 확정 기대값 40,281 → 47,488, 인터페이스 결함 판별 402 → 438(정본 246 · 정본만 83 → 65). 잃은 확정 1,601 칸은 열 상한 안에서 고른 변수가 바뀐 몫 — 판별 결함 기준 잃음 25 · 새로 61. - PV SUTS: unit 932 그대로(R92 판 r92x 와 같은 집합). 세 묶음 판정(oracle wt_h94) — R92 판정 대비로, 사이에 R93~R97 생성 변화와 판정 oracle 판 변화가 섞여 있다(R96 PV 단독 측정은 없음): 판별 가능 3,833 → 3,846, 우리 3,831 → 3,844(잃음 0 · 새로 13), 정본 1,999 → 2,008, 정본만 0. 확정 기대값 칸 298,611 → 299,982(저장소 모델 9,149). 분모 밖(정본이 시험하는데 우리 SUTS 에 없는 함수 37): 판별 가능 266 · 정본 238 — 그대로(33 은 등록 SwUDS v3.03 이 우리 소스보다 새 코드 판을 설계한 몫, 3 은 단위 목록 표 미판독, 1 은 이름 오타 — R99 대상). 생성 정점 메모리 7,546 MB(xlsm 쓰기 단계 — 후속 개선 대상). 검증: `test_project_maps.py` 89 + 관련 회귀 약 3,000, vitest ProjectSetupSection 17 · api 계열 46. 변이 1차 17(생존 9 → 0) · 2차 32 · 3차 10/10. 커밋 게이트가 R96 시험에 남은 죽은 patch(`_OVERRIDE_JSON_CANDIDATES` — R97 이 지운 상수)를 잡아 지웠다(맵은 생성 루트로 귀속을 판정하므로 임시 폴더 소스엔 적용되지 않는다). 리뷰(deep 3회 + 확인): 1차 W9(내보내기로 HD 맵 우회 덮기 · 깨진 매니페스트에서 override 조용히 빠짐 · 관리자 게이트 …) · 2차 W1(id 판정과 루트 판정이 갈림) · 3차 W1(화면이 앱 오류 봉투를 못 읽어 루트 이동 409 가드가 실서버에서 무력 — 목이 FastAPI 기본 모양이라 시험은 초록) 반영. 남은 것: PV 구조 표에 없는 소스 파일 77 개는 맵 행이 없다. 이름 키 조회는 여전히 대소문자 구분. 측정 스냅샷은 `docs/project_maps/` 를 통째로 복사해야 한다. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…일치 308 → 785, 불일치 0 사용자 지시 순서(callee contract → 정밀도 회복 → 읽기 기반 입력)의 정밀도 회복. R93 의 보수 규칙(해석하지 않은 callee 뒤엔 모든 volatile 저장소가 미상)이 정본 SUTS 레지스터 칸 일치를 크게 줄였다. SITS 의 "비해석 callee 뒤 레지스터" 309 칸은 대부분 루프 없는 레지스터 초기화(`lin_lld_timer_S12_init` · `lin_lld_sci_init`) 뒤다. - 효과만(E) callee 가 하드웨어를 기다릴 수 없다는 텍스트 증거가 있으면(`CalleeSummary.cannot_wait()` — 종료 RETURNS · 모르는 callee 없음 · 인라인 어셈블리 없음 · 포인터 분석 완전 · 읽기 집합 완전) 그 callee 가 쓰지 않는 레지스터는 행이 준 값을 유지한다. 쓰는 레지스터는 기존 쓰기 측면이 그대로 처리한다(대상 미상 · `<hardware>` 면 전체 무효). 하네스 메모리 모델은 함수 자신의 본문 · 해석 callee 와 같은 가정이고 공시한다. - 유지한 callee 가 읽는 레지스터는 poll 로 기록해 폴링 루프 안에서는 해석 모드처럼 거부한다. 위치를 못 정하는 포인터로 읽으면 와일드카드 poll(`@unplaced_pointer_read`) — 그 호출을 감싼 어떤 폴링 루프도 거부한다(해석 모드보다 보수적, 소비처 `_poll_overlap` 하나 · 구조 시험). - closure: 인라인 어셈블리 사실 `asm`(cpc SCHEMA 29 · sections "v60" 짝, 옛 캐시 fail-closed). 매크로가 부르는 함수는 포인터 분석의 호출 그래프로 따라간다 — 객체형 별칭(`#define CB HwWait`) · 매개변수 호출(`RUN(HwWait)`)을 놓치던 closure 구멍(쓰기 쪽 · R95 종료 판정도 같은 구멍)을 닫았다. 분석이 없을 때만 텍스트 대체(문자열 · 주석 가림, 매개변수 이름 제외, 매개변수 호출은 모르는 코드). - 레코드 `registers_kept_across_callees` · `registers_read_unplaced_callees`, 근거 필드 · 카운터 짝, 가정 문구 둘 · 한국어 노트, `CSO_CONTRACT_CHECK=1` 계측, oracle 레코드 스키마 7. 측정(HDPDM01 · KJPDS02_PV, R97 커밋 ee125d0 과 나란히): - 정본 SUTS 레지스터 칸(HD): 일치 308 → 785 · 불일치 0 · 미상 1,026 → 549, R98 전 일치 칸 잃음 0(R93 보수 규칙 전 727 보다 많다). - SITS(HD · PV): 확정 칸 21,362 · 47,488 그대로(잃음 · 바뀜 0), 인터페이스 판별 497/647 · 438/685 그대로. 레지스터 장벽 309 → 26 칸이나 풀린 283 칸은 LIN 초기화가 하드웨어 포인터로 쓰는 C-R5-1 에 다시 막혀 이득 0. - SUTS(확장 · 문서 되읽기): 확정 칸 HD +9,672 · PV +76, 줄어든 함수 0. R98 oracle 판정(재정렬, 같은 변이 · 분모): 우리 HD 4,185 → 4,204 · PV 5,345 → 5,352, 정본만 HD 34 → 15 · PV 49 → 42, 잃음 0(R97 고정 oracle 로는 새로 정한 함수가 미결이라 kill 같음). 생성 HD 1,831 → 1,741 s, PV 4,712 → 5,857 s(부하 — 전 단계 고르게 느려짐). 검증: 적합성 형태 S17~S26, 새 시험 test_register_kept_r98.py, 대상 회귀 659 + 618 통과. 변이 M1~M33 kill. 리뷰(deep 3회): 1차 C1(객체형 함수 별칭 매크로) · W1 · W2 반영, 2차 C2(매크로 매개변수 이름 = 함수 이름) · W3(텍스트 단어 규칙 거짓 도달) → 호출 그래프로 반영, 3차 LGTM. 5 트리 closure 직접 대조: HD 변화 0, PV 3(이미 모르는 코드, 값 불변). 남은 것: NEVER 판정이 도달 함수의 매크로 `return` 을 빠져나가기로 세는 과대근사(실트리 0건), do-while(0) 매크로의 키워드를 호출로 읽는 정밀도 손실. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… 못 맞춘 이유를 이름 · 근거로 공시 PV 정본에만 있는 시험 함수 42 개 판독: 33 개는 등록 SwUDS v3.03 이 우리 소스보다 새 코드를 설계한 판 차이, 나머지는 우리 결함 — 단위 목록 표에만 있는 함수(PV 3 · HD 8)를 안 읽었고, 한 글자 자리바꿈 이름(SwUFn_1407)을 못 맞췄다. - 단위 목록 표 판독(read_unit_list · apply_unit_list): 라벨 낱말 · 1 행 함수 정보 표 · 개정 이력 · 삭제 행은 목록이 아니고, 버린 후보 표는 공시. 목록에만 있는 함수는 범위에 들되, 프로젝트 맵이 verify=X 로 둔 파일의 함수는 listed_only_exempt(HD LIN 스택 7). 상세 절 없는 함수 표는 ID 행으로 잇는다(HD PP0_MOTOR_PWM_Init ← SwUFn_2302). - 이름 근사(near_name_pairs): 오타 꼴(이웃 자리바꿈 · 겹침/빠짐)만 대응, 같은 빌드 · 경쟁 후보 없음 조건. 치환 · 숫자 · 짝 낱말(Get/Set · RDBI/WDBI …) · 번호 형제는 힌트만. 생성마다 캐시 하나. - 설계서 ↔ 소스 판 차이(design_source_drift): 준비 게이트 단계 · 공시. 소스를 일부만 읽었으면 판정 보류. PV likely_different_revision(36/985), HD consistent. - STS · SUTS · 게이트가 한 다리(load_requirement_design_bridge)를 쓴다. 정렬이 더한 다리는 기존 함수 뒤에 붙인다(측정이 찾은 회귀: 중간에 끼면 요구당 TC 상한 안에서 다른 함수를 밀어 PV TC 165 개 ID 가 밀렸다). SITS TC ID 는 범위 안 목록 함수에 설계 ID + 출처 공시, 구성 불변. 영향도 카드와 문서가 같은 SITS ID 도우미를 쓴다. 측정(R98 906bce4 과 나란히): - STS HD · PV 모든 칸 같음(TC ID · 제목 · 스텝 안정). SITS 확정 칸 · 인터페이스 판별 그대로, PV TC ID 1 개만 SwITC_157 → SwITC_SwUFn_1583. - SUTS HD: PP0_MOTOR_PWM_Init 새로 시험(행 +24 · 확정 칸 +164), 다른 함수 확정 칸 줄어듦 0, 판정 kill 잃음 0 · 새 함수 판별 16 중 우리 14 · 정본 16. - SUTS PV: 시험 함수 932 → 938(목록 표 5 · 오타 짝 1), 행 +336 · 확정 칸 +4,230, 다른 함수 확정 칸 줄어듦 0, 공통 482 함수 kill 5,352 그대로 · 잃음 0. 새 함수 중 정본에도 있는 4 개는 판별 75 중 우리 75 · 정본 22(정본만 0). 정본에만 있는 시험 함수 42 → 38. 검증: test_swuds_unit_list_r99.py 134 + 관련 회귀. 리뷰 deep 3회(1차 C1 · W1~W5, 2차 W-a~W-c, 3차 LGTM) + 측정이 찾은 STS 순서 회귀 수정(standard 리뷰 LGTM). 남은 것: PV 판 차이 33 개 — 정본이 시험한 2661 판 소스가 PV 회귀 zip 에 전처리본(.tu.c)으로 있어 R101 에서 그 판으로 재측정. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…읽고, oracle 이 그 하네스대로 피호출을 stub 하는 모드를 더한다 — 기본 꺼짐, 꺼지면 산출물 같음
실제 단위시험은 시험 대상만 실행하고 피호출은 stub 한다(다른 unit — prototype stub, 같은 번역 단위 — TEST.STUB). 우리 oracle 은 피호출을 해석해, 하네스에서는 stub 이 정하는 값이 "모름"으로 남았다(HD 미지 칸의 피호출 몫 14,424 · 43.8 %). 규칙은 문서가 아니라 단위시험 근거에 있다.
- 추출(오프라인 — report_gen/harness_evidence.py · scripts/extract_harness_profile.py): PV 회귀 zip(환경 · 실 스크립트 · .tu.c · vcast_stubs), HD SwUTR(내장 HTML stub 사건) → docs/project_maps/<key>/harness.json(schema 1, 키 ENV:Unit.fn — 빌드 하나씩). 통합시험 근거 · 한 빌드 두 판은 거부, zip 안의 두 판은 다수 판을 남기고 버린 판을 현재 소스 대조와 함께 공시(다수 판 없으면 그 unit 만 제외), 같은 이름 루트의 환경은 상위 폴더로 가름, 규칙과 판정이 같은 비직접 stub 이름은 수 + TU 색인으로 접음.
- 생성(generators/unit_harness.py): resolve_aux_map("harness") 가 맞는 프로젝트의 프로파일만 쓰고, HarnessView.decide_ex 가 피호출마다 (종류, 사유) — 관측 우선, 근거와 규칙이 어긋나면 판정하지 않음. oracle 모드 H(c_source_oracle · callee_contract)가 그대로 stub. SUTS · 영향도 초안이 요청을 넘김(기본 None).
- 기본 꺼짐: off 면 아무것도 읽지 않고 산출물이 바이트 같다(oracle 기록 판 SCHEMA_VERSION 만 7 → 8). 켜는 것은 R100b(SUTS 하네스 시트 · VectorCAST 내보내기) 측정 뒤.
측정(oracle 직접, off ↔ on): HD 6 함수 프로브 같음 620 · 미지 → 확정 120 · 확정값 바뀜 0. 부착 HD 호출 492 중 관측 330, PV 1,674 중 992. 프로파일 PV 환경 53 · 함수 1,021, HD 30 · 348 — 다시 뽑으면 바이트 같음.
검증: 하네스 시험 191 + 구조 가드. 리뷰 deep 4회(4차 LGTM) + 4차 권고 W-1 · W-2 · W-3 · I-2 · I-4 · I-7 · I-8 반영(뮤턴트 16/16 잡힘), I-1 · I-3 은 4단계 TODO. 5차는 서브에이전트 한도로 메인 자체 검토.
남은 것: R100b(행 · Harness 시트 · 내보내기 TEST.STUB 왕복), 생성 요약에 프로파일 공시, 루트 재등록 판정 강화. PV 판 선택(버린 2631 판이 현재 소스에 더 가깝다는 표시)은 R101 에서 2661 소스로 다시 본다.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… · 정본 글꼴, [검증 필요] 노랑 · N/A 회색 작성기가 값을 쓴 칸에만 선을 그어, 입력 3개짜리 TC 옆으로 빈 슬롯 90여 칸이 선 없이 남았다 — 데이터 영역 중 선 없는 칸 PV SUTS 87 % · PV SITS 67 %(정본 1 % 미만, 2026-10-18 실측). 사용자 지적 "엑셀 셀선같은것도 안되어 있는거같고". STS 는 이미 정본과 같다(빈 칸까지 선). - generators/spec_grid.py(신규): TC 블록을 다 쓴 뒤 블록의 모든 행 · 표 전체 열에 서식을 입힌다 — Input · Expected 밴드는 빈 슬롯까지 가는 선, 블록 메타 열(SUTS B~E · GG, SITS B · E · F · G · GV)은 좌우 선 + TC 행 위 · 마지막 행 아래(한 칸 모양), SUTS F · G 는 시퀀스 그룹(REQ / FI) 단위, 밴드 경계 이중선(SUTS G|H · CZ|DA, SITS G|H · CL|CM · GU|GV), 표 바깥 중간 선. TC 행 이름 칸은 정본처럼 입력 파랑(C5D9F1) · 기대 보라(E4DFEC), N/A 칸 회색(D3D3D3), `[검증 필요]` 칸은 시각 강조 정책의 "사용자 입력 필요" 노랑(design_tokens.USER_INPUT_FILL_RGB). 값 · 값 서식은 그대로. 글꼴은 템플릿(정본) 명세 시트를 지우기 전에 읽는다(KJPDS02 Arial 10 · HDPDM01 맑은 고딕 10 — HD SUTS 처럼 밴드 행이 다르고 첫 TC 에 입력이 없어도). - 칸 서식은 캐시한 StyleArray 복사(openpyxl copy_worksheet 방식) — 칸마다 Border · Font 를 대입하면 객체 해시를 매번 한다. - SITS 헤더 행을 A 열부터 칠하던 것을 B 부터(정본 A4 빈 칸). 시각 강조 정책 문서에 새 색 용도를 적었다. 실측(HD SITS 웹 생성, 2026-10-18): 칸 값 163,531 전부 같음(서식만 바뀜), 데이터 칸 97 % 네 면 선(나머지는 표 밖 A 열 · 병합 블록 열), 격자 521,710 칸 · [검증 필요] 노랑 49,217 칸, 글꼴 정본(맑은 고딕 10). 비용(합성, PV SUTS 규모 939 TC · 28,170 행 · 칸 530만): 서식 28 초 · 저장 58 초 · 메모리 +1.7 GB(사용자: 서식 비용 수용). 검증: test_spec_grid.py 7(빈 슬롯 선 · 블록/그룹 모양 · 이중선 · 바깥 선 · 채움 · 값/서식/글꼴 보존 · 헤더 테두리 · 정본 글꼴 읽기), 작성기 관련 기존 시험 13 파일 561 통과, test_openpyxl_scan_guard 통과. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… Safety 근거 없음 표시, HD SITS 앞부분 채움, SITS 경로 행마다 'Interface :' 사용자 요청 "실제 칸에 입력된 데이터들도 양식에 맞게 잘 작성되었는지 확인해줘"(2026-10-18) — 6 문서를 정본과 열 단위로 대조한 결과 중 근거가 분명한 것. - D1 STS FS_REQ(H): 정본(KJPDS02 SwTS v2.01)은 요구 원문의 기능안전 태그(`[FS_REQ_MD_22]`), 없으면 `N/A`. 이전엔 안전 요구에 상위 SyRS ID(`SyTSR_0101, …`)를 적어 정본과 같은 요구가 0 이었다. `apply_fs_req_column` 이 문서 전체 요구를 본 뒤 채운다. 요구 문서 어디에도 태그가 없으면(HDPDM01 SRS) 태그 없음이 '해당 없음' 의 근거가 아니므로 비우고 생성 통계(`fs_req`)에 공시. - D2 Safety 근거 없음(STS · SUTS · SITS): 지어내지 않고(사용자 결정 "none은 none") 칸을 "사용자 입력 필요" 노랑으로 — `spec_grid.GridLayout.required_cols`, STS 작성기. 안전 행 연노랑(R76 N102)은 그대로. - D3 SITS 앞부분: 표지 값 칸을 라벨로 찾는다(`_cover_value_cell` — KJPDS02 C26→G26, HDPDM01 I2→J2 · B23→F23 병합 칸). 이전엔 `G26`~`G30` 고정이라 HDPDM01 템플릿에서 병합 칸 쓰기 예외가 나 Introduction · 1.5 · 1.6 채움까지 통째로 건너뛰었다. 1.4 는 머리 행에서 Note 열을 찾고(HDPDM01 은 E 가 Date 였다), 우리 입력 문서를 적을 때 템플릿에 남은 하드웨어 문서 행을 지운다. - D4 SITS 경로(D): 정본처럼 반복 행마다 `Interface : …`. 이전엔 첫 행만 접두 없이 적어, 이 열을 `Interface :` 로 찾는 읽기(Jenkins 콜체인 채굴 · SwITR 집계)가 우리 문서의 경로를 하나도 못 읽었다. 행별 경로를 모르는 행은 TC 의 경로. 실측(웹 생성, AI 끔, 기존 261018 산출물과 칸 대조): HD SITS 값 칸 변화 0 · 앞부분 채움 성공(표지 4 · 1.4 4행 · 1.5 5행 · 1.6 10행) · 경로 2,387/2,387 행 · Safety 노랑 45. HD STS 바뀐 칸은 H 883(SyRS ID → 빈칸) · Safety 노랑 254. PV STS 바뀐 칸은 H 2,490 · FS_REQ 정본과 같은 요구 0 → 49/68(나머지는 정본이 태그를 TC 마다 나누거나 관련 요구로 옮겨 적음) · Safety 노랑 31. PV SITS 값 칸 변화 0 · Safety 노랑 68. 검증: test_spec_form_d1_d4.py 7, 작성기 관련 기존 시험 2,046 통과(성능 가드의 병합 범위 읽기는 scan-ok). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…더로 꺾쇠 포함을 해석하고, 진입 단위가 헤더를 안 볼 때 같은 빌드의 레지스터 배치를 빌린다
PV SwITS 의 레지스터 기대 칸 11,055(45 TC)가 전부 `observable_not_a_modeled_object:r_SCI0CR2…` 였다. 원인: 부트 빌드(PDS128_FBL ·
PDS64_FBL)는 `derivative.h` 에서 레지스터 헤더를 `#include <mc9s12zvl64.h>` 꺾쇠로 들이는데(`extern volatile SCI0CR2STR r_SCI0CR2
@0x703` — `r_` 는 부트 소스의 진짜 이름, 앱은 `_SCI0CR2`), 프로젝트 문맥은 꺾쇠 포함을 전부 시스템 헤더로 보아 부트 단위 11 개가
레지스터를 하나도 못 봤다. `.cproject` 는 그 헤더가 있는 `${ProjDirPath}/Project_Headers` 를 컴파일러 포함 경로에 둔다.
- V1c(`c_project_context`): `detect_build_config` 가 설정 파일마다 포함 경로의 프로젝트 폴더(`project_include_dirs`)를 읽고,
`_resolve_angle_include` 가 `<name>` 을 그 폴더의 헤더(하나일 때만)로 해석한다 — 아니면 예전처럼 시스템 헤더. 두 프로젝트에서
바뀌는 것은 두 부트 빌드의 `mc9s12zvl64.h` 하나(앱 빌드엔 꺾쇠 프로젝트 헤더가 없다). 문맥 스키마 29 → 30, 소스 단계 캐시
v60 → v61(짝 — 한쪽만 올리면 생성 전체가 scope 없이 돈다).
- V1b(`c_source_oracle._World.declaring_scope`): 통합 진입 단위가 레지스터 헤더를 안 보고 그 헤더를 보는 피호출 단위가 실제로
해석되지 않으면, 같은 빌드에서 그 레지스터를 외부 volatile 객체로 선언한 단위의 배치로 읽는다. 해석 못 한 코드가 쓴 것은 여전히
모른다(쓰기 closure 가 이름을 havoc 하고 `storage_base` 가 행의 입력보다 먼저 본다).
실측(웹 생성, AI 끔, 기존 261018 산출물과 TC · 행 · 변수 이름으로 대조): HD SITS 풀림 1,117 · 확정값 바뀜 0 · 잃음 0. PV SITS 풀림
1,913 · 잃음 0 · 확정값 바뀜 3 — 셋 다 같은 행의 입력이 바뀐 결과(`lin_tl_rx_queue` 구조체 타입이 해석되며 입력 배정이 달라짐: 입력
39 칸 · 10 여 TC, 한 TC 는 열 `lin_tl_rx_queue.queue_max_size` 가 빠짐). 변이 판별 영향은 전체 재생성 뒤 측정.
과정 기록: 처음엔 `r_X` 를 VectorCAST 표기로 보고 소스 `_X` 로 잇는 이름 연결을 넣었으나 풀린 칸 0 → 추적으로 전제가 틀렸음을
확인하고 커밋 전에 되돌렸다.
검증: test_angle_include_project_header_v1c.py 5 · test_register_declaring_scope_v1b.py 4(변이: 해석 끄면 각 2 · 1 실패),
판 번호 짝 가드 2 갱신, 문맥 · oracle 관련 3,240 통과.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…E · __EI() 와 그 매크로를 쓰는 피호출이 메모리 전체를 '모름' 으로 만들지 않게
HDPDM01 `#define ISR_ENABLE __asm(cli)` · `ISR_DISABLE __asm(sei)`, KJPDS02 `#define __EI() { __asm(cli); }` 는 인라인 어셈블리로
읽혀 메모리 전체가 havoc 됐고, 그 매크로를 쓰는 함수는 피호출로 효과만 읽힐 때 `__asm` 이라는 모르는 피호출에 닿았다
(2026-10-18 SwITS `macro_call_not_an_expression:__EI` 4,923 · `macro_body_not_an_operand:ISR_ENABLE`/`ISR_DISABLE` 2,861 ·
`callee_effects_unknown:…:__asm` 2,847, SwUTS 1,192). `sei` / `cli` 는 CPU 의 인터럽트 마스크만 바꾸고 `nop` 은 아무것도 안
한다 — 모델의 인터럽트는 돌지 않는다는 가정 위에서는 어떤 C 객체도 바뀌지 않고 기다리지도 않는다.
- `c_project_context`: 판정 하나(`is_mask_asm` / `strip_mask_asm` / `ASM_CALL_NAMES`) — 텍스트가 `sei` · `cli` · `nop` 명령뿐일
때만(중괄호 · `;` · 줄 잇기 · 주석 제외). 함수의 `asm` 사실(`_body_asm`), 호출 목록(`__asm(cli)` 를 `__asm` 호출로 넣지 않음),
매크로 호출(`macro_side_effects`), closure 의 어셈블리 판정이 이 판정을 따른다. `wai` · `stop` · `jmp` · 스택을 만지는
`EnterCritical`(CCR_reg 를 씀) · 다른 코드가 섞인 문장은 예전처럼 어셈블리. 문맥 스키마 30 → 31, 소스 단계 캐시 v61 → v62(짝).
- `c_source_oracle`: 객체형 매크로(`read_macro`) · 함수형 매크로(`macro_call`) · `__asm` 호출 · GNU `asm("cli")` 에서 효과 없이
지나가고, 그런 기록에 가정(`INTERRUPT_MASK_ASSUMPTION`)과 지나간 명령 이름(`interrupt_mask_instructions`)을 싣는다.
- 함수 본문에 직접 쓴 `__asm(cli);` 는 tree-sitter 가 구문 오류로 읽는다(`source_parse_error` — 별개, 프로젝트는 매크로로 쓴다).
실측(웹 생성, AI 끔, V1c 산출물과 TC · 행 · 변수 이름으로 대조): HD SITS 풀림 508(대부분 ISR_ENABLE) · PV SITS 풀림 3,265
(__EI 1,749 · SPI_DataTransfer 의 __asm 1,209 · …), 둘 다 확정값 바뀜 0 · 잃음 0 · 입력 변화 0.
검증: test_interrupt_mask_asm_v4.py 4(변이: 판정을 끄면 3 실패). R98 시험의 어셈블리 예시를 `CLI` → 기다리는 `WAI` 로(함수
`Cli` → `Wai` — R98 이 지키려던 것은 '기다릴 수 있는 어셈블리'), 판 번호 짝 가드 2 갱신, 문맥 · oracle 관련 3,244 통과.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…으로 올린다 — 사용자 결정 "최대한의 효과를 나타낼 수 있는것들은 다 켜"
사용자 결정(2026-10-18): "하네스 stub 모드 켜고 진행해" · "최대한의 효과를 나타낼 수 있는것들은 다 켜".
- V2 `unit_harness.DEFAULT_HARNESS_MODE` off → evidence. 단위시험(SUTS)은 실제 VectorCAST 하네스처럼 피호출을 stub 한다 — 하네스
근거(R100a 프로파일 `docs/project_maps/<key>/harness.json`)가 정한 대로 다른 unit 은 prototype stub, 같은 번역 단위는 TEST.STUB,
근거와 규칙이 어긋나면 판정하지 않음. 프로파일이 없는 프로젝트는 예전과 같다. 영향도 문서 초안도 같은 요청을 쓴다. 통합 읽기
(SITS)는 원래 stub 하지 않는다. R100a 측정(HD 6 함수): 같음 620 · 미지 → 확정 120 · 확정값 바뀜 0. 피호출을 해석해 얻던 값 중
stub 반환값이 정하는 값은 이제 '모름'(행이 `F() return` 을 주지 않으면) — 실제 단위시험이 내는 값 쪽으로의 교정이다. 전체 수치는
재생성에서 잰다.
- V5 `c_source_oracle._PATH_BUDGET` 64 → 256: 미지 값 분기 두 개를 더 따라간다(64 에서 거부되던 칸 SwITS PV 5,380 · HD 4,140 ·
SwUTS PV 1,267 · HD 811). 값은 여전히 모든 경로가 같을 때만. 시간 · 메모리는 64 경로를 넘는 실행에서만 는다("시간은 늘어도 된다
품질만 보증된다면").
- 시험: R100 기본값 시험은 "기본 evidence · off 는 명시"로. clang 대조 리뷰 시험의 '한도를 넘는 함수'는 분기 수를 한도에서 계산
(7 → 9 비트 — 128 경로는 이제 한도 안).
검증: oracle · 하네스 · MC/DC · SUTS 관련 5,497 통과(한도 전제 3 건 수정 후).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…2) · 정본이 stub 한 경계 함수를 stub 하고(A · A2) · 객체가 아닌 열을 빼고 공시하고(B1) · 흐름 기준으로 같은 이름 static 을 가리고(B2a) · 오라클 능력 공백(const typedef · 매크로 속 문장 매크로 · 매크로로 정의한 ISR · 반환 지역 변수)을 메운다
사용자 요청(2026-10-18): "SITS 검증필요한게 너무 많다 — 기대값 줄일 수 있는 근거나 코드나 그런 부분들은 없는거야?" · "B랑 A도 이어서 진행해".
값을 지어내거나 칸을 비우지 않는다 — 값을 계산하거나, 존재하지 않는 대상을 묻는 열을 빼고 이름과 사유를 공시한다.
SITS (generators/integration_oracle.py · generators/sits.py · generators/sits_boundary_stubs.py 신규)
- S3 첫 실행이 막혔다고 말한 이름(관측 출력의 초기값 · 읽은 값)을 입력 열로 덧붙여 다시 돈다(R94 S2 순수 함수 배선).
기존 열 · 값 · TC ID 불변(R75), 행 단위 무손실 가드(`_take_better_rows`: 확정값을 하나도 바꾸지 않고 더 얻을 때만).
- A 그 프로젝트 정본 SITS 가 stub 한 함수(정본 입력 띠의 `F() return`)만 stub 하고 반환값을 행 입력으로 —
R7 리뷰 W1(흐름 callee 반환 주입 금지)과의 경계. 반환 타입 도메인 값, 같은 무손실 가드.
VectorCAST 내보내기: 그 함수는 통합하지 않고 `TEST.STUB` + `TEST.VALUE:<unit>.<F>.return`.
근거 문서는 템플릿 선택과 분리 — 라우트 세 곳(`/api/local/sits/generate` · 스트림 · 비동기)이 정본을 넘긴다
(`_sits_stub_evidence`: 정본 우선이면 템플릿이 곧 정본, 표준 템플릿이면 정본을 따로 로컬화, 못 열면 `input_skips`).
A2 측정이 드러낸 근본 원인 — SITS 도메인 판정기는 설계서의 매개변수 선언만 알아 HD 정본 stub 9 중 4(`LinRecv` 등,
반환 `e_ERROR_CODE`)가 `return_type_unknown`. 그 TC 빌드의 함수 정의에서 반환 타입을 풀어 열거형이면 멤버 값을 쓴다.
- C2 `path_dependent_in_callee` 사유에 피호출이 갈린 첫 `branch_on_unknown:` 을 단다 — S3 가 그 이름(PV `ADC_EnUser` 류)을
'분기' 입력으로 덧붙일 수 있게. 갈림이 `entry` 상태에도 붙으므로 실행 전 길이로 피호출 몫을 가른다.
- B1 모든 행이 `observable_not_a_modeled_object` 이고 어느 단위도 파일 수준에서 선언하지 않은 이름(피호출의 매개변수 ·
지역)의 기대 열을 빼고 `observables_not_objects` 로 공시 — 뺀 칸은 '도출' 이 아니라 `cells_removed`.
B1b `void` 진입의 `return` 열(오라클이 `function_returns_void` 를 `no_return_value_on_path` 와 가른다).
B1c 피호출 포인터 매개변수의 대상(`pSCI[0].scicr2.byte`) — 진입 매개변수의 대상(정본도 관측: `RespMsg[0].data`)은 남긴다.
- B2a 프로젝트 기준 모호한 이름(PV `static U8 u8s_DataBuffer[60]` 두 단위)을 흐름의 단위가 하나만 선언하면 그 객체로
(`CalleeProvider.flow_owners`), 실행이 다른 쪽을 먼저 연결했으면 여전히 거절(`_World.name_ambiguous`).
오라클 (generators/c_source_oracle.py · generators/mcdc_design.py) — SUTS 에도 닿는다
- B3 한정된 프로젝트 typedef(`const U8`)는 그 타입 + 한정자(C11 6.7.3) — `const U8 x = 5U;` · `const U8 t[] = {…}` 지역이
전부 `local_type_unresolved` 였다. R3 W3-2 가 '보수적으로 미해석' 이라 고정해 둔 단언을 바꾸고, clang 으로 컴파일한 C 와
값이 일치하는 시험을 더했다.
- B4 펼친 결과가 다른 매크로 호출 하나뿐인 매크로를 C 처럼 다시 훑는다(6.10.3.4, 깊이 상한 8) — PV
`s_UDS_SendNegativeResponse` → `s_UDS_SendNrc` → `do { … } while( 0 )`. prescan 도 같은 판정이라 중괄호 없는 `if` 아래의
다문장 매크로가 이제 거절된다(예전엔 조건 거짓 행에서 C 가 무조건 실행하는 둘째 문장을 건너뛴 값을 낼 수 있었다).
- B6 `ISR(SCI0_INT) { … }`(`#define ISR(x) __interrupt void x(void)`) 를 그 이름의 정의로 — 활성 함수형 매크로가 매개변수
하나를 `… void <매개변수>(void)` 로 펼칠 때만.
- B8 함수가 모든 반환문에서 그대로 돌려주는 지역 변수(설계서가 출력으로 적은 이름 — `u16t_SysDiagSts`)의 열은 반환값 —
한 번 선언 · 반환 타입과 같은 타입 · 본문 매크로에 `return` 없음일 때만, 증거에 `observed_as: return`. **근거 단계 한정**
(`unit["observe_returned_locals"]` — `apply_sequence_evidence` 만 켠다): SUTS 행 탐색이 새 값을 보자 기존 행을 바꿨다(R75).
문서: docs/plans/2026-10-18_next_round_SUTS_SITS.md(다음 라운드 계획 · 측정 정본) · 2026-09-21 우위 계획서 R101 기록.
측정 (AI 끔 · 작업 공간 생성 · 기준 outputs/*_261018b — AI 켬/끔은 SITS · SUTS 칸이 같았다)
- HD SITS `[검증 필요]` 62.6 → 59.9 % · PV SITS 64.2 → 58.1 % — 둘 다 확정값 바뀜 0 · 잃음 0 · 뺀 열(값 0) HD 251열 3,323칸 ·
PV 726열 9,913칸 · 사라진 입력 0.
- SUTS(행을 자극으로 짝지음 — 행 번호 대조는 새 행이 끼면 뒤 행이 밀려 거짓 '바뀜' 을 낸다):
HD 짝지은 16,152행 = 기존 행 전부 · 바뀜 0 · 잃음 0 · 7.4 → 6.7 % (B8 은 근거 단계 한정 — 탐색에 보이자 기존 2행이 바뀌었다).
PV 짝지은 28,657행 바뀜 0 · 잃음 0 · 14.5 → 14.4 %. ⚠ 기존 79행(0.27 %, 7 TC)의 자극이 바뀜 — B3(`const` 초기값 표가 풀리자
행 탐색이 다른 행을 고름). 하네스 기본 켬(fc59417)의 HD 581행과 같은 계통(행 탐색이 오라클 개선에 민감)이라 다음 라운드에
'고정 오라클 프로파일로 탐색 + 근거 단계 · 동반 행' 으로 함께 고친다. 이 행들의 kill 영향은 미측정.
시험: test_sits_read_inputs_s3 · test_sits_boundary_stubs_a · test_sits_observables_b1 · test_sits_flow_owned_static_b2a ·
test_const_local_typedef_b3 · test_nested_statement_macro_b4 · test_macro_declared_isr_b6 · test_returned_local_b8 ·
test_callee_fork_cause_c2 + clang 대조 1. 사유 문자열을 고정한 기존 단언 2(C2 꼬리) · R3 W3-2 보수 단언 1(B3) ·
join 변이 시험의 람다 인자(C2 — 호출 시점 갈림 수) 갱신. 전체 15,294 passed / 11 skipped(-n 5, 783초).
변이: S3 2/2 · A · A2 7/7 · C2 2/2 · B1 4/4 · B1b 4/4 · B1c 4/4 · B2a 5/5 · B3 1/1 · B4 3/3 · B6 3/4(비활성 매크로는 표에
본문이 없어 동등) · B8 7/7 + 근거 단계 표지 1/1. B4 의 prescan 확장이 바꾸는 함수는 HD · PV 실소스에 0(합성 사례로 검증한
스캐너로 전수).
서브에이전트 한도(10/21)로 자체 검토 — 한도 해제 뒤 deep 리뷰 권장.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…%p 낮게 보고했다 · 원인 7갈래 표 사용자 질문(2026-10-19): "왜 우리가 검증필요라고 나오는 이유를 모르겠어". 측정 정정 - 예전 측정기(`verify_count.py`, 저장소 밖)는 기대값 띠의 비어 있지 않은 칸 전부를 분모로 셌다 — TC 머리 행의 변수 이름 칸까지. SITS 는 TC 가 짧아(평균 13행) 이름 칸 비중이 커서 비율이 낮게 나왔다. 칸 수(분자)는 맞다. - 값 칸만 센 값은 제품 자체 통계(`integration_oracle.cells` HD 67,256 · `derived` 23,908)와 일치한다: HD SITS 69.7 → 67.4 → 64.5 % · PV SITS 72.0 → 68.9 → 62.4 % (예전 보고 64.8 → 62.6 → 59.9 · 67.0 → 64.2 → 58.1). SUTS 는 거의 같다(HD 6.7 % · PV 14.6 %). - 커밋 1e4e970 메시지의 SITS 비율(59.9 % · 58.1 %)은 예전 측정기 값이다 — 이 문서 §0 이 정본. 원인 7갈래(§0 표 — 갈린 경로 사유는 안쪽 원인으로 셈) - 계산기가 아직 못 따라가는 C 구조가 가장 크다(SITS 43 · 50 %, SUTS 53 · 69 %): 포인터 typedef `EEPROM_TAddress` 하나가 HD 1,491 · PV 3,421칸, PV SUTS 2차원 배열 `u16s_MovgAvgFltBuff[i][j]` 11,664칸(B9 대상). - 우리 입력값이 만든 것(SITS HD 6.9 %, SUTS 9 %): 타입을 못 푼 변수에 범용 값 표(`-1 · 256`), 같은 행 레지스터 전체값과 비트값을 따로 채워 모순(`r_LP0CR = 63` · `LPE = 0`) — 다음 라운드에서 고친다(기존 행은 두고 고친 행을 더한다). - SITS 가 SUTS 보다 10배 많은 까닭: 통합시험은 행마다 평균 59(HD) · 86(PV)개 함수를 실제로 돈다. 문서만 변경 — 코드 · 시험 무변경. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…— 같은 변수가 여러 이벤트에 나오면 비교 표시가 있는 행 · 나중 이벤트가 이긴다
발견 경위(2026-10-19): VectorCAST PC 연동 준비로 PV 회귀 zip(등록 원본 sha256 58a75fab… 일치)의 실행 결과 보고서 54개를
판독하다가, 이벤트 단위로 따로 읽은 값과 제품 판독기(`extract_execution_results_with_actual`)를 대조했다.
원인: 실제 보고서의 TC 섹션은 이벤트 여러 개다 — `Calling <UUT>`(호출 직전 값, 비교 없음) → `Stubbed <F>` →
`Returned from <UUT>`(반환 후 값 · 기대값 비교 표시). `_extract_var_rows_between` 은 같은 이름을 **처음 것만** 남겨
호출 직전 값이 실제값이 됐다. 예: PV `SwUFn_0133.002` 의 `return` — 보고서 반환 후 `0xFFFFFFFE <match>`, 판독 `('0x0', '')`.
기존 시험은 표 하나짜리 합성 HTML 이라 이벤트 순서를 본 적이 없다.
실측(기대 표시가 붙은 칸, 이벤트 단위 독립 판독과 대조):
- PV 54 보고서 48,475칸: 고치기 전 같음 42,638 · 실제값이 호출 직전 값 2,352 · 일치 표시 소실 3,481 · 빠짐 4
- HD 30 보고서(SwUTR v2.02 에 묻힌 HTML) 8,528칸: 고치기 전 오류 467(230 · 237) → 고친 뒤 8,528 전부 같음
영향 범위: 이 판독기를 쓰는 곳 — SwUTR Test Log 실제값(설정 `sutr_spec_based: true` 에서는 Fail 행만 이 값을 쓴다.
Pass 행은 감사본 방식대로 기대값을 복제하므로 산출물 Pass 행은 맞았다) · 표준 SUTR 경로(전 행) · Jenkins VectorCAST 경로.
등록 자료의 Fail 은 HD 8건이고 전부 사용자 코드 기대식(`0x8CD*10/0*50/10` — 0 나눗셈)이라 슬롯 값 실패는 없었다.
수정 1: 행 등급(불일치 2 > 일치 1 > 비교 없음 0) — 높은 등급이 이기고, 같은 등급이면 나중 이벤트가 이긴다. 불일치는 뒤의
일치 · 무표시 행에 덮이지 않는다. 행(tr) class 의 success / danger 도 표시로 읽는다.
수정 2: 범위 반복 TC 는 섹션 안에 `Start of Range Iteration #N` 머리를 반복마다 둔다(PV 8 TC). 이것을 TC 경계로 읽어
TC 구간이 첫 반복 앞에서 끊기고 가짜 TC `Range` 가 생겼다 → TC 머리 수집을 `_tc_start_headings` 하나로 모아(실제값 ·
단계 반복 판독 두 곳) 범위 반복 머리를 뺀다. 반복이 여럿이면 위 등급 규칙으로 마지막 반복 값(불일치는 보존)이 남는다.
Jenkins VectorCAST 경로는 이 판독 결과를 시험 행으로 세므로 가짜 `Range` 가 범위 반복 TC 가 있는 환경마다 시험 1건으로
집계에 끼어 있었다. 머리 수와 결과 h3 수가 같아져 PASS/FAIL 짝짓기도 근접 추정 대신 순서 짝짓기로 간다.
결과(같은 독립 판독과 대조): PV 54 보고서 48,475칸 · HD 30 보고서 8,528칸 전부 같음.
시험: tests/unit/test_swut_input_adapter.py `TestExtractExecutionResultsEventOrder` 6건(실제 보고서 구조 — 반환 후 값 ·
무표시끼리 나중 · 불일치 보존 · 비교 행 보존 · 행 danger 표시 · 범위 반복). 변이 4/4(처음 것 승 · 등급 없음 · 행 danger
무시 · 범위 반복 머리를 TC 경계로).
남은 것: 호출하는 곳이 없는 `_extract_actual_from_node` 도 같은 '처음 것' 규칙이다(죽은 코드 — 이번에 손대지 않음).
이름 키가 이벤트 · 서브프로그램을 담지 않아 stub 매개변수와 UUT 변수 이름이 같으면 한 칸으로 합쳐지는 한계는 그대로다.
서브에이전트 한도(10/21)로 자체 검토 — 한도 해제 뒤 리뷰 권장.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, 판독기(불일치 0)와 우리 오라클(일치 11,471 · 계산 결함 0)을 대상 실행값에 대조한다 배경(2026-10-19): 이 PC 에서 팀의 VectorCAST 환경(CodeWarrior S12Z + TRACE32 시뮬레이터)을 다시 지어 돌릴 수 있게 됐다. 사용자 결정: 긴 실행은 업무 외 시간 가능 · 실행값만 있는 칸은 `[검증 필요]` 에서 빼 별도 범주로 · HD 환경은 나중에 · PV 부터. 계획서: docs/plans/2026-10-19_vectorcast_execution_plan.md (V0 · V0b 결과 포함). 새 파일 - tools/vcast_runner.py — 사용자 실행 배치(run_VectorCAST_for_CodeWarrior_S12Z_T32sim.bat)의 `set` 줄을 설정의 단일 출처로 읽고 GUI 대신 clicast 를 부른다. `harvest`: 빌드된 환경 전부를 DataAPI 로 덤프(DB 파일 해시를 앞뒤로 대조 — 바뀌면 보고). `run`: 환경 사본 → PROJECT_DIR 바꾸기(`retarget`, 경로 마디 단위) → 빌드 · 스크립트 적용 · 실행 · 보고서 · 덤프, 단계마다 rc · 초 · 로그. 작업 폴더가 원본 환경 폴더면 거부(다시 지으며 팀 결과를 지우지 않게). VECTORCAST_DIR · 라이선스가 없으면 DISABLED 로 명시. - tools/vcast_dataapi_dump.py — VectorCAST 의 vpython 으로 도는 DataAPI 덤프(시험 · 입력 · 기대 · 이벤트별 실제값 · 일치 표시). - scripts/vcast_dataapi_checks.py — `reader`(HTML 판독기 대 DataAPI) · `oracle`(오라클 교정). 측정(PV, 사용자 zip 2개를 풀어 둔 C:\workspace\KJPDS02, 읽기만) - 수확: 환경 73개(UT 43 · IT 30), 실패 0 · DB 파일 변경 0. 한 단계 안쪽에 든 환경 2개(MotorCtrl · Sys_UDS_LinComp)도 찾는다. - 판독기(803fa77) 대 DataAPI: 59,279칸 일치 · 불일치 0 · PASS/FAIL 5,824건 일치. 출처 차이 2건(HTML 보고서가 DB 보다 낡음) → 팀 시험 · 실제값의 1차 출처는 DataAPI. - VectorCAST 입력 적용 규칙(호출 이벤트 값 대 스크립트 값 2,557쌍): 범위 밖 값은 C 변환처럼 감싸고(-1 → 255, 256 → 0), 0xFFFFFFFF 를 넘는 값만 먼저 0xFFFFFFFF 로 포화(4294967296 → 4294967295, u16 에 4294967305 → 65535). - 오라클 교정(팀 입력 → r1177 소스의 우리 오라클, 시험별 TEST.STUB 를 하네스 보기로 재현): 시험 4,466 · 일치 11,471 · 불일치 10 · 미정 19,588. 불일치 10 은 오라클 계산 결함이 아니다 — char 배열 lin_pFrameBuf 를 VectorCAST 가 문자열 타입으로 다뤄 넣은 첫 원소가 대상에서 0(9건, V1 대상 실험으로 확정 예정) · 쓰기 보호 레지스터 CPMUSYNR(1건). 미정 최대는 3차원 배열 관측 9,800칸 — 오라클 확장 라운드의 합격 기준(대상에서 검증된 정답)으로 쓴다. - 새 도구로 SwUT_01_Lib_sha256 을 다시 지어 돌린 결과 = 팀 결과(93건 · 입력 · 이벤트별 실제값 전부 동일). 시험: tests/unit/test_vcast_runner.py(배치 해석 · retarget · 환경 찾기 · 단계 순서 · 원본 폴더 보호) · tests/unit/test_vcast_dataapi_checks.py(값 규칙 · 시험 변환 · 작은 소스로 교정 끝까지 · 판독기 대조) — 27건, 변이 5/5 잡음. 변환이 VectorCAST 의미를 따르게 시험이 잡아 고친 것: stub 표시가 없는 UUT 함수의 반환값은 쓰지 않는다(VectorCAST 는 그 함수를 실행한다). 남은 것: r1018 기준선(UT 43 환경 재빌드 · 실행, 약 6시간 — 업무 외 시간), V1(우리 SUTS → 실행 가능한 .tst). 서브에이전트 한도(10/21)로 자체 검토 — 한도 해제 뒤 리뷰 권장. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ib_sha256 확정값 1,513칸 전부 대상 일치
계획서: docs/plans/2026-10-19_vectorcast_execution_plan.md (V1 결과 절).
새 파일
- tools/vcast_tst_export.py — 우리 SUTS 행 → 그 환경의 VectorCAST 시험 스크립트. 경로는 빌드 폴더의 param.xml · types.xml
(clicast 템플릿 명령은 이 환경에서 머리만 낸다)로 정한다: 매개변수 · 반환 → <UUT>.<f>.…, 전역 → 그 전역을 가진 단위
(UUT 또는 uut_prototype_stubs).<<GLOBAL>>.…, `F() …` → stub(UUT 함수면 TEST.STUB 도), 포인터를 지나는 이름 → 먼저
<<malloc n>>(오라클 행 모델과 같은 하네스 할당 — R40 · R81). 확정 칸 = 그 값, `[검증 필요]` 칸 = 타입 전 범위(실제값만
기록, 주장 없음). 집합체 통째 이름(오라클도 정확한 이름으로만 읽어 쓰지 않는 입력)과 사전에 없는 이름은 쓰지 않고 사유로 센다.
stub 집합 = 팀 시험이 그 함수에서 매번 stub 한 UUT 함수(오라클 하네스 보기가 읽는 증거) + 행의 `F() return`.
- scripts/vcast_suts_compare.py — 칸 분류(확정 일치 · 불일치 · 결과 없음 / `[검증 필요]` 실행값 · 결과 없음 / 쓰지 못함)와
V3 용 행별 실행값(observed.json, 실행 방식 표시).
- tools/vcast_runner.py — `suts`(한 파일) · `suts-all`(팀 환경이 있는 모든 파일) 추가.
묶음 실행(VCAST_USE_COMPOUND_FOR_BATCH: 대상 실행 한 번에 여러 시험, 묶음 40건 = 9초 대 따로 132초)으로 돌리되,
· 대상 RAM: 시험당 하네스 파일 2개(VCAST_MAX_TARGET_FILES)가 필요해 S12ZVMC256 은 40건 안팎이 한계(60건은
`Out of allocation space in segment RAM`) → 링크 · 시작 실패면 묶음을 반으로 줄여 다시 짓는다.
· 하네스 할당(<<malloc>>) 시험은 묶음 안에서 믿을 수 없었다(힙이 슬롯 사이에 초기화되지 않아 한 시험의 구조체가 한 칸
밀려 적용, 두 시험 실행 실패 — 따로 실행하니 셋 다 우리 값으로 PASS) → 시험마다 따로 실행.
· 단계 나눔(--phase compound / isolated)과 마감 시각(--deadline): 팀과 공유하는 좌석 1개를 업무 시간에 쓰지 않게,
밤마다 이어서 돌린다(따로 실행은 건당 ~4초).
측정(등록판 r1018 로 지은 SwUT_01_Lib_sha256, 우리 PV SUTS 261018b 의 Lib_sha256.c 행)
- 시험 430건(묶음 234 · 따로 196), 스크립트 줄 적용 100 %.
- 우리 확정 기대값 1,513칸 전부 S12Z 대상 실행과 일치 · 불일치 0.
- `[검증 필요]` 408칸 중 388칸 실행값 확보. 20칸은 `len` 이 커서 이벤트 한도로 반환이 없는 행(오라클도 루프 한도로 미정).
- 대상 사실: 열거형 객체에 -1 → 255(1바이트 부호 없음). 오라클은 열거형 입력을 0~127 · 열거자 범위로만 받아 그 칸을
`[검증 필요]` 로 두었다 — 맞다. 범위를 넓힐 근거로 기록(V0c).
시험: tests/unit/test_vcast_tst_export.py(경로 사상 · 할당 · stub · 범위 · 집계 · 묶음 크기 반감 · 할당 시험 분리 · 단계 이어
돌리기 · 마감 · 대조 분류) 18건, tests/unit/test_vcast_runner.py 에 환경 선택 1건.
남은 것: PV 전체(팀 환경 있는 40파일 · 24,776행) 야간 실행, 팀 환경 없는 13파일(4,008행)용 환경(V2b), V3 문서 반영.
서브에이전트 한도(10/21)로 자체 검토 — 한도 해제 뒤 리뷰 권장.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… 함수를 담은 팀 환경에서 — 팀 시험이 없는 함수도 문서와 같은 조건에서 돈다 V1(2cc35d9)은 시험별 TEST.STUB 를 팀 시험이 그 함수에서 매번 stub 한 목록으로 정했다. 그러면 팀 시험이 없는 함수는 stub 이 비어 같은 단위 피호출이 실제로 돌지만, 우리 오라클은 문서를 생성할 때 PV 하네스 프로파일 (kjpds02_pv/harness.json — 환경 53 · 함수 1,021, 규칙: 같은 단위 직접 피호출 stub)의 판단(HarnessView)으로 stub 했다. 대상 실행의 조건이 문서의 확정값이 기댄 조건과 다를 수 있었다. - tools/vcast_tst_export.py: `harness_views` — 제품 경로(`attach_unit_sources` 와 같게: 범위 + R62 투영 원문 → `harness_request` · `attach_unit_harness`)로 등록 소스 루트(읽기만)에서 함수별 보기를 다시 만든다(PV: 725 함수, 35초). `view_stubs` — 환경의 UUT 함수 중 보기가 STUB_SAME_TU 로 정한 것만 TEST.STUB. 보기가 없는 함수는 stub 하지 않는다 (오라클도 그 피호출을 실행했다). 내보내기 보고에 stub 출처를 센다. - tools/vcast_runner.py: `suts-all --source-root` — 보기를 한 번 계산해 파일마다 넘긴다. 헤더(.h)는 자기 환경이 없다 → `uut_subprograms` · `host_env`: 함수를 가장 많이 UUT subprogram 으로 가진 팀 환경(같으면 작은 쪽)에서 따로 된 작업 폴더(`<env>__<헤더>`)로 돌린다. 실측: Common_it_PDS.h 5/5 · SysOptionCtrl_it_PDS.h 1/1 → SwUT_02_Lib(UUT 함수 7). `.c` 에는 쓰지 않는다 — FBL `lin.c` 가 APP 환경과 함수 이름을 나눠 가질 수 있다. 작업 루트를 반복 전에 만든다(중간 요약 쓰기가 첫 파일 실행 폴더에 기대고 있었다). - 확인(Lib_sha256 14 함수): 보기의 stub 집합은 팀 공통 stub 을 항상 포함(팀에만 있는 것 0), 더 많은 것은 그 함수가 부르지 않는 같은 단위 함수 — 동작 차이 없음. 그래서 앞선 1,513/1,513 일치 측정은 그대로 유효하다. 계획서: V2b 범위 정정 — `lin.c`(584)는 APP 가 아니라 부트로더(PDS128_FBL `Sources\SubNetworkReprogram\LIN`)라 FBL 은 9파일 3,294행(환경 자료 필요, 사용자가 올려 주기로 함). APP 의 나머지는 헤더 inline 122행(위 대체 규칙)과 `lin_lld_sci.c` 11행(팀 .env 복제), 기대값 열이 없는 581행(43 함수)은 대조할 칸이 없는 문서 공백. HD 자료 위치(PDS_LIN · PDS_UT · PDS_IT.zip, PV 뒤 진행) 기록. 시험: tests/unit/test_vcast_tst_export.py 보기 판단 1건, tests/unit/test_vcast_runner.py 헤더 대체 2건(헤더는 함수를 담은 가장 작은 환경 · 따로 된 폴더, `.c` 는 빌리지 않음, 동점 순서). 돌연변이 4/4 잡힘. 앞선 게이트 실패(test_body_projection_r62 — `build_scopes` 를 쓰는 도구는 투영 원문을 읽어야 함)를 제품 경로와 같게 고쳐 통과. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… · 부트) 행이 같은 파일 키로 섞이던 것 — PV 부트 환경 첫 실행 std_util 확정 13/13 대상 일치 PV 부트(FBL) 환경(사용자 1.UT.zip · 2.IT.zip)을 처음 돌려 드러난 실행 도구 결함 3건: - 하네스 할당: 부트 10개 중 7개, APP 43개 중 32개 환경이 VCAST_NO_MALLOC 을 두지 않아 하네스가 컴파일러 라이브러리 힙에서 할당하고 우리 141바이트 <<malloc>> 이 실패했다 (TEST_HISTORY_FAILURE_REASON_LIBRARY_MALLOC_FAILED, std_util 18건 결과 없음 — 팀 시험은 1바이트만 할당). --cfg-default NAME=VALUE: 팀 CFG 에 없는 줄만 채우고(부트 linuds 의 200000 은 유지) 팀 값과 함께 기록. VCAST_NO_MALLOC=TRUE · VCAST_MAX_HEAP_SIZE=1000(APP 팀 값)으로 std_util 확정 13/13 일치 · 불일치 0, [검증 필요] 27/27 실행값. - CFG 뿌리: 부트 CFG 는 자기 PROJECT_DIR 을 두고 TESTABLE_SOURCE_DIR 에 .env 의 뿌리를 쓴다 → 두 파일의 뿌리를 모두 바꾼다. - 두 빌드: EEPROM.c 파일 키에 APP 30행 + 부트 267행, lin_lld_sci_init 한 단위에 두 트리의 행. in_build: --source-root 트리의 행만 돌리고 나머지는 other_build 한 줄로 센다(잘못 돈 시험은 아직 0 — APP·부트 env 에 모두 있는 이름 9개는 우리 부트 행에 없음). 자료 점검(계획서 §V2c): 부트 소스 = SVN PDS128_FBL r1021..r1022(등록판 · 두 FBL zip 31/31 같음), 팀 부트 env 빌드 소스 = r1022(main.c 만 팀 수정본), 팀 부트 시험 UT 1,221 · IT 124 전부 PASS. 문서 결함 1건(생성기 라운드로): lin_lld_sci_init TC ID 가 2번 시트 SwUTC_SwUFn_3517 ×2 대 3번 시트 SwUFn_0117 · SwUFn_01228 — 값은 전부 [검증 필요]. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… 한도에 걸리던 것 — 첫 전체 실행 15분에 드러난 3건 사용자가 좌석을 바로 쓰게 해 준 첫 전체 실행(APP · 16:10)이 15분 만에 드러낸 실행 도구 결함: - 거짓 완료: MotorCtrl 40건 묶음 64개가 전부 "0 tests ran" 인데 폴더가 compound_done 으로 적혀 다음 단계가 건너뛸 뻔했다. 넣은 시험보다 적게 돈 묶음이 나오면 그 파일은 exec_incomplete 로 멈추고 로그를 남긴다. - 시험 데이터 한도: 그 0건의 원인 "Larger VCAST_STDIN_DATA_SIZE_T value needed" — 묶음을 반으로 줄인다(RAM 과 같은 처방). - 하네스 힙의 RAM: 6d01c4c 의 VCAST_NO_MALLOC · VCAST_MAX_HEAP_SIZE 채우기를 env 복사 때 넣자 가장 큰 SwUT_33 · SwUT_13 이 빌드에서 "Out of allocation space in segment RAM". 할당 시험은 어차피 따로 실행하므로 힙은 따로 실행 단계의 시험 1건짜리 빌드에만 넣고, 묶음 빌드가 RAM 한도로 실패하면 묶음을 반으로 줄여 다시 짓는다(전에는 실행 때만 줄였다). 시험 4건(가짜 대상: 빌드 RAM · 데이터 한도 · 0건 실행 · 힙은 1건 빌드에만). 멈춘 실행 폴더는 aria_verify\v2_r1018__aborted_1610 으로 옮겨 두었다. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…B) 대 설치 기본(스택 256 B), V0 회귀 93건 · 실제값 1,904칸 팀과 같음 16:45 재실행에서 가장 큰 SwUT_33 이 묶음 1건에서도 "Out of allocation space in segment RAM". 근본 원인: 팀 CFG 94개가 모두 $(VECTORCAST_DIR)\DATA\codewarrior\s12zvmc_sim\uut.lkf 를 쓰고, 팀 설치본의 그 파일은 사용자가 env zip 과 함께 준 uut.lkf — RAM 0x1000~0x0F8FFF(시뮬레이터 전용) · STACKSIZE 0x1000. 우리 설치 기본값은 RAM ~0x8FFF · 스택 0x100. SwUT_33 팀 빌드는 하네스 힙 50,000바이트를 링크했다(맵 vcast_malloc_union 0xC350). 지금까지의 묶음 40건 RAM 한계도 이 차이였고, 스택 256바이트는 호출이 깊은 함수에서 넘쳐 실행값을 망칠 수 있다. --env-file NAME=PATH(run · suts · suts-all): 작업 CFG 의 VCAST_ENVIRONMENT_FILES 에서 이름이 같은 항목만 그 경로로 바꾸고 팀의 항목과 함께 기록한다(설치 폴더는 그대로). V0 회귀: 팀 링커 파일로 Lib_sha256(r1177) 팀 시험 93건 — PASS · 이벤트 행 · 실제값 1,904칸 팀 결과와 전부 같다(스택 4,096바이트). 시험 3건. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
개요
장기 실행 기능 브랜치를
main에 합칩니다. 브랜치명은 초기 작업(impact ↔ Jenkins 연동)에서 왔지만, 이후 영향도 분석·추적성·프로젝트 분석 탭·문서 생성 빌더 전반으로 확장됐습니다.규모: 355 커밋 · 461 파일 · +97,932 / −8,618 (
main은 뒤처지지 않아 fast-forward 병합 가능)변경 범위
tests/unitfrontend-v2/srcbackend/servicesbackend/routers.claude/,.agents/커밋 스코프 상위:
impact101 ·summary48 ·traceability37 ·analysis31 ·trace29 ·chat12 ·hooks11타입 분포:
fix158 ·feat139 ·chore24 ·refactor12 ·perf9 ·docs6검증 (로컬, 병합 전 트리 기준)
pytest tests/unit/vitest run(frontend-v2)⚠ 리뷰어가 먼저 알아야 할 것
1.
main의 CI는 이 브랜치와 무관하게 이미 실패 상태입니다.최근 3회(2026-06-18/19/20,
chore(auto): end-of-day snapshot) 모두 실패했고, 실패 지점은 매번 Unit Tests → Run unit tests with coverage 한 곳입니다 (Syntax Check·Frontend Tests는 통과). 로컬 전체 회귀는 통과하므로 CI 환경 쪽(의존성·경로·환경변수) 문제로 보이나, 마지막 실패가 한 달 전 커밋이라 단정하지 않습니다. 이 PR의 CI가 빨간불이면 먼저 이 선행 실패부터 분리해서 봐야 합니다.2.
frontend-v2/node_modules2,186개 파일이 추적되고 있습니다.이 PR의 diff에도 14개가 포함됩니다. 이 브랜치가 만든 문제는 아니지만(기존 백로그), 병합 규모를 부풀리고 CI 체크아웃을 무겁게 합니다. 별도 정리 PR을 권합니다 — 여기서 같이 지우면 리뷰 범위가 흐려집니다.
3. 규모상 커밋 단위 리뷰를 권합니다.
355 커밋이라 전체 diff 통독은 현실적이지 않습니다. 스코프별(
impact/summary/traceability)로 나눠 보시거나, 관심 영역의 커밋만 짚어 보시는 편이 낫습니다.🤖 Generated with Claude Code