# MITC4 구현 회고 및 보충 기록

## 1. 문서 목적과 범위

이 문서는 `linear-static-mitc4-shell` 기능을 요구조건부터 `dev` 병합 검증까지
진행하면서 실제로 겪은 시행착오, 실수, 어려움과 그 해결 방법을 기록한다. 새 계약을
정의하는 문서가 아니라, 이미 승인된 요구조건·정식화·I/O·reference·release 문서를
보충하는 회고 자료다. 계약이 충돌하면 이 문서가 아니라 다음 문서를 우선한다.

- `docs/requirements/linear-static-mitc4-shell.md`
- `docs/formulations/mitc4-shell-formulation.md`
- `docs/io-definitions/linear-static-mitc4-shell-io.md`
- `docs/reference-models/linear-static-mitc4-shell-reference-models.md`
- `docs/numerical-reviews/linear-static-mitc4-shell-review.md`
- `docs/releases/linear-static-mitc4-shell-release.md`

라인 참조는 회고 작성 시점의 기준 커밋 `6c41cde41af4d6cd474b008e5ae81769fff0aa79`
을 사용한다. `path:line`은 이 기준 커밋의 파일과 라인을 뜻한다. 중간 실패가 최종
phase ledger에서 정상 완료 상태로 교체된 경우에는 `commit:path:line`으로 historical
snapshot을 표시한다. 이후 파일이 수정되면 라인이 이동할 수 있으므로 커밋과 검색어를
함께 확인해야 한다.

이 문서는 다음 세 범주를 구분한다.

1. **실수 또는 결함**: 승인된 의미와 다른 동작, 잘못된 가정, 환경 실패의 오분류.
2. **계약 변경에 따른 재작업**: 구현 결함이 아니라 승인 범위나 tolerance가 바뀌어
   이미 작성한 문서·테스트·비교기를 수정한 경우.
3. **예상된 TDD RED와 수치적 어려움**: 계획된 실패로 아직 없는 동작을 확인하거나,
   shell 요소 특성상 조심해서 해결해야 했던 문제. 이것을 구현 실수로 과장하지 않는다.

## 2. 주요 시행착오 요약

| ID | 분류 | 문제 또는 어려움 | 핵심 해결 | 대표 근거 |
| --- | --- | --- | --- | --- |
| `RET-01` | 프로세스 실수 | numerical review가 수치 정식화 외의 bundle 행정 정보를 readiness blocker로 취급함 | FESA 독립 솔버 원칙과 실제 비교에 필요한 최소 계약으로 gate를 재정의함 | `docs/ADR.md:189`, `docs/numerical-reviews/linear-static-mitc4-shell-review.md:80` |
| `RET-02` | 프로세스 어려움 | Planning Agent, Implementation Agent, Executor, hook의 소유권이 모호했음 | 승인·materialize·Step 실행·상태/커밋 책임을 분리함 | `.codex/agents/implementation-agent.toml:46`, `.agents/skills/harness/SKILL.md:16` |
| `RET-03` | 환경 결함 | Windows sandbox helper 실패가 프로세스 종료 코드 `0` 안에 숨고 Step 0이 갱신되지 않음 | 진단 marker를 종료 코드보다 먼저 검사하고 승인된 sandbox override를 추가함 | `scripts/execute.py:247`, `scripts/execute.py:306` |
| `RET-04` | 수치적 어려움 | 20 physical DOF MITC4를 외부 24 DOF shell 계약에 넣으면서 drilling을 분리해야 했음 | physical과 drilling congruence를 분리하고 회전 대각항만으로 고정 안정화를 계산함 | `docs/formulations/mitc4-shell-formulation.md:759`, `src/fesa/elements/mitc4_shell.cpp:597` |
| `RET-05` | 기하 처리 어려움 | warped mesh와 공유 절점에서 initial director의 방향·순서·유효성을 결정해야 했음 | source-order 정렬, 면적 가중 평균, 양의 incident-normal 방향, 전 적분점 `J>0` 검사를 사용함 | `src/fesa/model/shell_geometry.cpp:275`, `src/fesa/model/shell_geometry.cpp:317` |
| `RET-06` | 범위 경계 | 선형 구현에 future geometric-nonlinear tangent 정식화가 섞일 위험이 있었음 | 선형 24 DOF 구현과 조건부 nonlinear 20-to-24 pullback을 명시적으로 분리함 | `docs/formulations/mitc4-shell-formulation.md:1112`, `docs/numerical-reviews/linear-static-mitc4-shell-review.md:71` |
| `RET-07` | parser 결함 | 같은 이름의 Abaqus `NSET`과 `ELSET`을 중복 entity로 잘못 거부함 | node-set과 element-set namespace를 분리함 | `src/fesa/io/abaqus/domain_mapper.cpp:401`, `tests/unit/io/abaqus/domain_mapper_test.cpp:439` |
| `RET-08` | comparator 결함 | Abaqus CSV와 HDF5 instance name의 대소문자 차이가 row identity mismatch를 만듦 | 비교 key만 ASCII 대문자로 정규화하고 source identity 출력은 보존함 | `tests/reference/mitc4_reference_comparison.cpp:122`, `tests/reference/mitc4_reference_comparison_test.cpp:539` |
| `RET-09` | 계약 재작업 | 동일한 FESA kernel 결과를 Abaqus S4와 S4R 두 reference에 동시에 맞출 수 없었음 | full-integration FESA의 blocking reference를 S4 하나로 제한하고 S4R은 비-reference test로 검증함 | `docs/requirements/linear-static-mitc4-shell.md:201`, `docs/requirements/linear-static-mitc4-shell.md:205` |
| `RET-10` | 계약 재작업 | B33 혼합 tolerance가 독립 shell 정식화 비교에 지나치게 엄격해 kernel 결함과 계약 차이를 혼동함 | invariant/patch 검증 후 MITC4 고정 절대 tolerance `1.0e-5`로 계약과 비교기를 함께 변경함 | `docs/ADR.md:215`, `tests/reference/mitc4_reference_comparison.cpp:35` |
| `RET-11` | 통합 어려움 | prescribed-only shell에서 이미 상쇄된 `K*d`만으로 residual을 정규화하면 정상 roundoff가 단위 residual이 됨 | `Kff*df`, `Kfc*dc`, `Ff`의 물리적 항 크기로 정규화함 | `src/fesa/results/result_recovery.cpp:674`, `phases/linear-static-mitc4-shell/index.json:102` |
| `RET-12` | 상태/출력 안전성 | recovery 또는 HDF5 inventory가 뒤에서 실패할 때 기존 정상 state/output을 손상할 위험이 있었음 | candidate state와 temporary HDF5를 완전히 검증한 뒤 atomic commit/replace함 | `src/fesa/results/result_recovery.cpp:925`, `src/fesa/io/hdf5/hdf5_results_writer.cpp:2579` |
| `RET-13` | 병합 환경 오염 | `dev` worktree의 ignored B33 `.h5`가 exact reference inventory test를 실패시킴 | reference를 수정하지 않고 ignored 파일을 quarantine으로 이동한 뒤 전체 검증함 | `.gitignore:20`, `tests/reference/b33_reference_comparison_test.cpp:178` |

## 3. 계약과 개발 프로세스에서의 시행착오

### 3.1 Numerical Review가 행정 메타데이터에 과도하게 의존함 (`RET-01`)

#### 문제

초기 numerical review는 정식화의 수학적 준비 상태뿐 아니라 canonical bundle 이름,
`README.md`, provenance, units, step/frame 설명까지 갖춰야 implementation planning으로
넘어갈 수 있다고 판단했다. `metadata.json` 자체는 optional로 정리했지만, 그 주변의
행정 정보가 여전히 primary verdict를 `needs-reference-model`로 유지했다.

Historical evidence는 다음과 같다.

- `ebb2657:docs/numerical-reviews/linear-static-mitc4-shell-review.md:12` — status가
  `needs-reference-model`이었다.
- `0428759:docs/numerical-reviews/linear-static-mitc4-shell-review.md:94` — alias,
  README, provenance, unit, step/frame, schema, tolerance를 open blocker로 묶었다.
- `0428759:docs/numerical-reviews/linear-static-mitc4-shell-review.md:702` — `NR-O01`부터
  `NR-O04`까지 calibration을 implementation-planning blocker로 남겼다.

#### 원인

Reference artifact의 재현성 감사와 현재 기능의 수치 readiness를 같은 gate로 취급했다.
또한 Abaqus가 제공하는 bundle 관리 방식과 FESA가 실제로 비교에 필요한 observable
quantity 계약을 충분히 분리하지 못했다. 결과적으로 정식화 결함이 아닌 문서 형식이
수치 검토를 막았다.

#### 해결

프로젝트 정책을 다음처럼 다시 고정했다.

- FESA는 Abaqus의 내부 알고리즘을 재현하는 솔버가 아니다
  (`docs/ADR.md:189-195`).
- readiness에 필요한 것은 선언된 input/CSV, deterministic source row mapping,
  component와 tolerance다 (`docs/ADR.md:197-202`).
- `NR-O01`은 고정 drilling 계수 결정으로 닫고, `NR-O02`부터 `NR-O04`까지는 승인
  범위에서 제거했다 (`docs/numerical-reviews/linear-static-mitc4-shell-review.md:74-77`).
- 과거 blocker였던 canonical naming, README, `metadata.json`, provenance와 expanded
  portfolio가 formulation verdict를 막지 않는다고 명시했다
  (`docs/numerical-reviews/linear-static-mitc4-shell-review.md:80-83`).

관련 정책 변경 커밋은 `5c08f1c`(independent reference validation policy),
`73df844`(MITC4 verification/drilling scope 단순화), `60b42f4`(numerical review pass)다.

#### 교훈

Reference gate에는 “이 정보가 실제 row matching 또는 수치 판정에 사용되는가?”를 먼저
물어야 한다. 사용되지 않는 행정 메타데이터를 모든 기능의 blocking requirement로
자동 승격하면 독립 솔버의 물리 검증보다 형식 준수가 우선된다.

### 3.2 Harness 역할과 Step 소유권이 처음부터 충분히 명확하지 않았음 (`RET-02`)

#### 문제

구현 계획 승인, phase 파일 materialize, Harness 실행, Step 선택, 구현, timestamp와
commit 기록의 주체가 여러 agent 문서에 분산되어 있었다. 이 상태에서는 Planning Agent가
실행까지 시작하거나, Implementation Agent가 다음 Step으로 넘어가거나, hook을 수동
검증 명령처럼 실행하는 실수가 생길 수 있었다.

#### 해결

다음 소유권을 agent와 Harness 문서에 중복 없이 고정했다.

- Planning Agent는 multi-Step draft 승인 후에만 phase 파일을 만들고 Step을 실행하지
  않는다 (`.codex/agents/implementation-planning-agent.toml:52-60`).
- Implementation Agent는 Executor가 선택한 현재 `stepN.md` 하나만
  `RED -> GREEN -> VERIFY`로 수행한다 (`.codex/agents/implementation-agent.toml:46-60`).
- branch, retry, timestamp, commit과 next-Step selection은 Executor 소유다
  (`.codex/agents/implementation-agent.toml:64-67`).
- hook은 등록된 PreToolUse/Stop lifecycle로 자동 실행하며 수동 호출로 대체하지 않는다
  (`.agents/skills/harness/SKILL.md:16-25`).
- 계획 승인은 실행 승인이 아니며 별도의 사용자 요청이 있어야 한다
  (`docs/HARNESS_WORKFLOW.md:41-42`).

이 개선은 `85cd17d`, `ab69a3d`, `713b41b`, `65d5e07`, `18296a1`, `a058ef7`
커밋에 걸쳐 정리됐다.

#### 교훈

Agentic phase에서 “무엇을 할 것인가”뿐 아니라 “누가 상태를 바꾸는가”를 계약해야 한다.
특히 implementation summary와 executor timestamp/commit을 한 주체가 모두 소유하게 하면
재시도와 감사 이력이 쉽게 꼬인다.

### 3.3 Windows sandbox 실패가 정상 종료처럼 보임 (`RET-03`)

#### 문제

Step 0의 첫 Harness 실행은 세 번 재시도한 뒤에도 Step status를 갱신하지 못했다.
historical ledger는 `8dd4d72:phases/linear-static-mitc4-shell/index.json:10`에
`[3회 시도 후 실패] Step did not update status`를 기록한다. 실제 원인은 구현 코드가
아니라 Windows sandbox helper의 `orchestrator_helper_launch_failed`였다. 더 까다로운 점은
이 진단이 Codex JSON output 안에 있으면서 process exit code는 `0`일 수 있었다는 것이다.

#### 해결

- `_codex_environment_failure()`가 exit code를 보기 전에 diagnostic text에서 sandbox
  helper marker를 검사하도록 바꿨다 (`scripts/execute.py:306-314`).
- 기본 `workspace-write`는 유지하되, 승인된 격리 worktree에 한해서
  `FESA_HARNESS_CODEX_SANDBOX=danger-full-access`를 허용했다
  (`scripts/execute.py:247-254`, `docs/HARNESS_WORKFLOW.md:247-252`).
- unknown mode는 fail-closed로 거부하고, exit code `0` 안의 helper failure도 잡는
  회귀 테스트를 추가했다 (`tests/test_execute.py:77-111`).

수정 커밋은 `0d50625`이며, 이후 Harness Python suite `7/7`이 통과했다
(`docs/releases/linear-static-mitc4-shell-release.md:176-178`).

#### 교훈

Agent runner에서는 OS process exit code만 신뢰하면 안 된다. 하위 orchestrator가 구조화된
출력 안에 fatal environment error를 기록하는 경우가 있으므로, 알려진 환경 실패 marker와
상태 전이를 함께 검사해야 한다.

## 4. 정식화와 수치 구현의 어려움

### 4.1 20 physical DOF와 24 global DOF 사이의 경계 (`RET-04`)

#### 어려움

MITC4의 physical field는 절점당 translation 3개와 director-tangent rotation 2개, 즉
총 20 DOF다. 반면 FESA shell 외부 계약은 절점당 6 DOF, 총 24 DOF다. 남는 네 개의
director-parallel 회전은 drilling 좌표이며 physical MITC4 strain에 들어가면 안 된다.

초기의 “작은 diagonal stiffness를 더한다”는 수준의 설명만으로는 다음 실수가 가능했다.

- translation과 rotation diagonal을 섞어 단위가 다른 값으로 `k_d`를 정하는 문제;
- drilling energy를 physical shell energy나 stress recovery에 섞는 문제;
- 20 DOF weak form에 24 DOF external work를 직접 결합하는 문제;
- 작은 값이므로 rigid mode나 rank 검증을 생략하는 문제.

Numerical Review에서 이 위험은 `NR-C02`부터 `NR-C04`로 추적됐다
(`docs/numerical-reviews/linear-static-mitc4-shell-review.md:68-70`).

#### 해결

정식화와 코드를 두 경로로 나눴다.

1. `K20`은 MITC tying shear와 physical constitutive matrix만으로 적분한다
   (`src/fesa/elements/mitc4_shell.cpp:597-636`).
2. `k_d` reference는 절점당 두 개, 총 여덟 개 physical tangent-rotation diagonal의
   finite positive 값만 사용한다 (`src/fesa/elements/mitc4_shell.cpp:638-655`).
3. `k_d = 1.0e-3 * min(R+)`를 적용한다
   (`src/fesa/elements/mitc4_shell.cpp:662-676`).
4. physical과 drilling을 별도 congruence로 24 DOF에 올린 후 마지막에만 합한다
   (`src/fesa/elements/mitc4_shell.cpp:669-681`).
5. recovery는 global 24 DOF를 physical 20 DOF로 projection한 뒤 계산하므로 pure drill이
   strain/resultant/stress/physical energy에 기여하지 않는다
   (`src/fesa/elements/mitc4_shell.cpp:699-714`).

이를 energy congruence, rank와 six rigid modes, patch field, fixed drilling factor,
pure-drill exclusion 테스트로 각각 검증했다
(`tests/unit/elements/mitc4_shell_test.cpp:618`, `:647`, `:697`, `:752`, `:828`).

#### 교훈

수치 안정화는 크기만 작은 physical stiffness가 아니다. coordinate map, 단위, virtual
work와 output 의미를 별도 경계로 정의해야 한다. 안정화 항을 physical result에서 제외하는
테스트가 coefficient 자체의 테스트만큼 중요하다.

### 4.2 Initial director와 warped/shared-node geometry의 결정성 (`RET-05`)

#### 어려움

“초기 법선벡터를 두께 방향으로 사용한다”는 결정은 단일 평면 요소에는 단순하지만,
공유 절점과 warped mesh에서는 다음 선택을 추가로 요구했다.

- element normal의 부호를 어느 source connectivity 순서로 정할지;
- 여러 incident element normal을 어떤 순서와 weight로 평균할지;
- 반대 방향 normal, fold, zero-area, local reversal을 허용할지;
- stiffness, tying, recovery가 서로 다른 geometry inventory를 사용하지 않게 할지.

#### 해결

- source connectivity로 center normal과 element orientation을 결정했다.
- required surface points에서 finite/nonzero measure와 positive orientation을 먼저
  검사했다 (`src/fesa/model/shell_geometry.cpp:275-300`).
- 공유 절점의 incident element를 stable source identity 순서로 정렬했다
  (`src/fesa/model/shell_geometry.cpp:317-327`).
- 모든 incident normal 쌍이 같은 positive hemisphere에 있는지 확인한 다음 면적 가중
  평균을 계산했다 (`src/fesa/model/shell_geometry.cpp:328-364`).
- geometry validity를 center, stiffness, tying, recovery point inventory 전체에서
  fail-closed로 검사했다. Calibration angle이나 distortion cutoff를 새로 만들지는 않았다
  (`docs/formulations/mitc4-shell-formulation.md:593`,
  `docs/numerical-reviews/linear-static-mitc4-shell-review.md:76-77`).

평면·회전·warped geometry, stable area weighting, invalid Jacobian/opposed normal,
validation-point inventory 테스트가 각각 존재한다
(`tests/unit/model/shell_geometry_test.cpp:101`, `:153`, `:184`, `:244`).

#### 교훈

자동 normal 생성은 단순한 vector 계산이 아니라 mesh topology와 deterministic reduction
계약이다. 순서를 명시하지 않으면 병렬화나 input order 변화가 director와 결과에 영향을
줄 수 있다.

### 4.3 Geometric-nonlinear tangent를 선형 구현으로 오인할 위험 (`RET-06`)

#### 어려움

정식화 문서에는 future geometric-nonlinear tangent도 포함했지만, 현재 제품 범위는 선형
정적이다. 20-coordinate director chart의 material/geometric tangent만 적는 것으로
global 24 DOF nonlinear element가 완성되는 것은 아니다. finite rotation update,
`Phi: R24 -> R20`, 그 1·2차 미분, chart recentering과 objective drilling potential이
필요하다.

#### 해결

- nonlinear 식이 Section 15.3까지 20-coordinate physical chart에만 존재하며 완전한
  global 24 DOF element가 아니라고 명시했다
  (`docs/formulations/mitc4-shell-formulation.md:1112-1113`).
- conditional pullback에는 map-curvature Hessian 항을 유지하고, 미정인 coordinate map과
  drilling을 future-only blocker로 남겼다
  (`docs/numerical-reviews/linear-static-mitc4-shell-review.md:71`).
- current review의 open question은 nonlinear rotation/map/output/state 계약뿐이며 현재
  선형 planning을 차단하지 않는다
  (`docs/numerical-reviews/linear-static-mitc4-shell-review.md:85-90`).
- 구현 phase에는 linear `K20 -> K24`와 고정 drilling만 넣고 nonlinear state나 tangent
  API를 만들지 않았다.

#### 교훈

문서에 수식이 있다는 사실과 제품 계약이 닫혔다는 사실은 다르다. 특히 nonlinear
coordinate map의 Hessian을 생략한 채 “consistent tangent”라고 부르면 이후 Newton
iteration의 일관성을 잘못 주장하게 된다.

## 5. Parser와 결과 파이프라인의 실제 결함

### 5.1 Abaqus NSET/ELSET namespace를 하나로 처리함 (`RET-07`)

#### 증상

Step 13 reference E2E는 comparator contract test가 통과한 뒤에도 모든 S4/S4R case에서
HDF5 생성 전에 `duplicate-entity`로 실패했다. historical ledger
`32f0e59:phases/linear-static-mitc4-shell/index.json:113`은 part `NSET Set-1`과
`ELSET Set-1`을 같은 namespace로 검사한 것이 원인임을 기록한다. Step 13의 test-only
소유 범위에서 production parser를 고칠 수 없었기 때문에 세 번 재시도 후 upstream
correction이 필요했다.

#### 원인과 해결

기존 `containsSetName()`은 part의 node set과 element set 양쪽을 함께 검색했다.
Abaqus input에서는 두 종류가 별도 namespace이므로 같은 이름이 유효하다.

- part helper가 전달받은 set 종류 하나만 검색하도록 변경했다
  (`src/fesa/io/abaqus/domain_mapper.cpp:401-408`).
- assembly helper도 `isNodeSet`이 같은 항목만 비교하도록 변경했다
  (`src/fesa/io/abaqus/domain_mapper.cpp:410-418`).
- part와 assembly에서 같은 이름의 NSET/ELSET을 허용하되 같은 종류의 실제 중복은
  계속 거부하는 회귀 테스트를 추가했다
  (`tests/unit/io/abaqus/domain_mapper_test.cpp:439`).

수정 커밋은 `b805683`이다.

#### 교훈

Source label text가 같다는 이유만으로 semantic identity가 같은 것은 아니다. parser의
duplicate 검사는 `(entity kind, scope, source name)` 전체 identity를 사용해야 한다.

### 5.2 Reference instance identity의 대소문자 차이를 그대로 비교함 (`RET-08`)

#### 증상과 원인

Parser namespace를 고친 뒤 reference precheck에서 Abaqus CSV의 `part-1-1`과 FESA HDF5의
`Part-1-1`이 다른 key로 취급됐다. CSV field whitespace는 이미 trim했지만 Abaqus name의
case-insensitive 의미를 row identity comparator에 적용하지 않았다.

#### 해결

- comparator ordering/equality key에서 instance name을 ASCII uppercase로 정규화했다
  (`tests/reference/mitc4_reference_comparison.cpp:122-142`).
- report에는 HDF5가 보존한 stable source identity를 그대로 남겼다. 즉 비교를 위해 source
  artifact를 rename하거나 rewrite하지 않았다.
- lowercase CSV identity가 정상 match되는 회귀 테스트를 추가했다
  (`tests/reference/mitc4_reference_comparison_test.cpp:539`).

수정 커밋은 `56833ab`이다.

#### 교훈

Identity normalization은 어느 계층에서 어떤 목적으로 하는지 제한해야 한다. comparator
key의 case normalization과 source identity 보존을 분리하면 matching은 견고해지면서도
진단·HDF5 identity가 조용히 바뀌는 문제를 피할 수 있다.

### 5.3 Prescribed-only free residual normalization (`RET-11`)

#### 증상

Step 12의 nonzero prescribed-displacement shell case는 실제 equilibrium을 만족했지만
free-residual 검증에서 실패했다. 이미 상쇄된 `K*d` 또는 외력만 normalization scale로
쓰면 `Ff=0`이고 `Kff*df`와 `Kfc*dc`가 서로 상쇄되는 case에서 작은 roundoff가 상대적으로
`1`에 가까운 residual처럼 보일 수 있었다. Phase ledger가 이 실패와 해결을 기록한다
(`phases/linear-static-mitc4-shell/index.json:102-107`).

#### 해결

free residual `Kff*df + Kfc*dc - Ff`의 세 물리 항을 따로 평가하고 그 최대 norm을
denominator로 사용했다 (`src/fesa/results/result_recovery.cpp:674-697`). 임의의
`max(1, ...)` floor는 넣지 않았고, scale과 residual이 둘 다 정확히 0일 때만 normalized
residual을 0으로 처리했다.

Integration test는 `Ff=0`인데 `-Kfc*dc`만으로 effective RHS가 생기는 것을 직접 확인한다
(`tests/integration/analysis/linear_static_analysis_test.cpp:440-464`).

#### 교훈

Residual normalization은 최종 합의 크기만 보면 안 된다. 서로 상쇄되는 원래 방정식 항의
물리적 scale을 보존해야 prescribed load, zero load와 mixed constraint를 같은 기준으로
검증할 수 있다.

### 5.4 Recovery state와 HDF5 output의 실패 원자성 (`RET-12`)

#### 어려움

Shell 결과는 displacement 하나가 아니라 residual/reaction, GP strain/resultant,
BOTTOM/MIDDLE/TOP stress, frame, physical energy와 equilibrium evidence를 함께 commit한다.
뒤쪽 row가 invalid일 때 일부 vector만 기존 `AnalysisState`에 반영하거나, 기존 정상
`results.h5`를 불완전한 새 파일로 바꾸면 실패가 이전 성공 상태를 손상한다.

TDD 과정에서 다음 문제가 드러났다.

- invalid shell candidate가 이전 state를 덮을 수 있음
  (`phases/linear-static-mitc4-shell/index.json:78-83`);
- beam-only recovery 뒤 stale shell evidence가 남을 수 있음
  (`phases/linear-static-mitc4-shell/index.json:85-91`);
- invalid HDF5 inventory가 기존 final output을 안전하지 않게 교체할 수 있음
  (`phases/linear-static-mitc4-shell/index.json:93-99`).

#### 해결

- recovery는 완전한 `candidateState`를 복사해 모든 shell row를 검증하고 마지막에 한 번만
  move-commit한다 (`src/fesa/results/result_recovery.cpp:925-940`).
- HDF5 writer는 unique temporary file에 쓰고 닫은 뒤 read-only로 reopen/self-check한
  파일만 final path로 교체한다 (`src/fesa/io/hdf5/hdf5_results_writer.cpp:2579-2592`).
- drilling energy와 unsupported point stress가 accidental output으로 생기지 않았는지도
  self-check한다 (`src/fesa/io/hdf5/hdf5_results_writer.cpp:2449-2455`).
- invalid shell inventory가 기존 final bytes를 보존하는 회귀 테스트를 추가했다
  (`tests/unit/io/hdf5/hdf5_results_writer_test.cpp:1359`).

#### 교훈

Solver output의 원자성은 HDF5 backend만의 책임이 아니다. recovery candidate의 의미적
완전성, temporary file의 구조적 완전성, final replacement 순서를 모두 지켜야 한다.

## 6. Reference comparison에서의 재작업

### 6.1 S4와 S4R을 동시에 blocking reference로 사용한 모순 (`RET-09`)

#### 증상

초기 reference plan은 S4와 S4R 두 Abaqus bundle을 모두 blocking comparison으로
사용했다. 그러나 FESA 계약은 두 source type을 같은 full-integration FESA-MITC4 kernel에
매핑한다. 실제 Abaqus S4와 S4R은 동일한 내부 정식화가 아니므로 두 Abaqus reference
결과도 달랐다.

Historical ledger `cf769aa:phases/linear-static-mitc4-shell/index.json:113`은 다음 불가능한
상태를 수치로 기록한다.

- 두 FESA deck은 heading과 `TYPE=S4/S4R` 외에는 같고 동일 stiffness를 생성함;
- Abaqus center `U3`는 두 reference 사이에 `8.190036e-7` 차이가 남;
- 당시 더 큰 tolerance도 약 `1.02456e-9`여서 하나의 공통 FESA 결과가 두 reference를
  동시에 통과할 수 없음.

이는 kernel을 S4와 S4R에 따라 다르게 만들라는 신호가 아니었다. 그렇게 하면 “둘 다
동일한 FESA formulation”이라는 승인 계약과 Abaqus 비동등성 원칙을 위반한다.

#### 해결

- blocking Abaqus comparison은 `reference/shell/`의 S4 input/displacement CSV 하나로
  고정했다 (`docs/requirements/linear-static-mitc4-shell.md:201-205`).
- `reference/shellR/`은 acceptance에서 소비하지 않는다
  (`docs/requirements/linear-static-mitc4-shell.md:202`).
- S4R 지원은 parser identity, 동일 sparse stiffness, HDF5 source metadata test로
  검증한다 (`tests/unit/io/abaqus/domain_mapper_test.cpp:538`,
  `tests/unit/assembly/sparse_assembler_test.cpp:324`,
  `tests/unit/io/hdf5/hdf5_results_writer_test.cpp:1167`).
- S4 E2E test만 유지했다 (`tests/reference/mitc4_reference_cases_test.cpp:159-169`).

문서 변경은 `91b2df9`, test 변경은 `f92c17d`에 기록됐다.

#### 교훈

입력 label을 지원하는 것과 그 label 이름을 가진 외부 solver element를 reference로
사용하는 것은 별도 결정이다. 공통 internal formulation이면 acceptance reference도 그
formulation의 observable 목적에 맞는 하나를 선택해야 한다.

### 6.2 Reference 실패를 kernel 결함과 tolerance 결함으로 분리하기 (`RET-10`)

#### 증상

S4-only gate로 줄인 뒤에도 25개의 nonzero `U3` row가 당시 B33식 tolerance를 넘었다.
historical ledger `8994ce9:phases/linear-static-mitc4-shell/index.json:113`은 worst row
`Part-1-1/node 2/U3`에서 다음을 기록한다.

- absolute error: `1.90378534915144e-7`;
- 당시 tolerance: `1.0237408203e-9`;
- normalized error: `185.9636`.

이 시점에 tolerance만 즉시 키우면 실제 MITC4 kernel 부호·tying·Jacobian 결함을 숨길 수
있고, 반대로 Abaqus S4와 동일하지 않은 FESA-MITC4에 B33 수준의 엄격한 tolerance를 계속
강제하면 올바른 독립 정식화를 실패로 오판할 수 있었다.

#### 독립 조사와 해결

먼저 reference 결과와 독립적인 다음 검사를 통과시켰다.

- 20-to-24 energy congruence와 six rigid modes/rank
  (`tests/unit/elements/mitc4_shell_test.cpp:618-647`);
- membrane, bending, transverse shear와 twist patch
  (`tests/unit/elements/mitc4_shell_test.cpp:697`);
- pure drilling의 physical recovery/energy exclusion
  (`tests/unit/elements/mitc4_shell_test.cpp:828`);
- S4/S4R common stiffness와 deterministic assembly
  (`tests/unit/assembly/sparse_assembler_test.cpp:324`);
- authoritative FESA HDF5에서 같은 stable error가 발생하며 comparator 자체의 row mismatch가
  아님을 확인함.

그 뒤 user-approved contract를 MITC4 고정 절대 tolerance `1.0e-5`로 변경했다.

- ADR은 U blocking, UR warning-only와 reference scale의 diagnostic-only 성격을 고정한다
  (`docs/ADR.md:215-222`).
- comparator는 모든 row에 하나의 fixed tolerance를 적용한다
  (`tests/reference/mitc4_reference_comparison.cpp:35`, `:755-767`).
- 경계 바로 아래와 위, UR warning-only를 회귀 테스트한다
  (`tests/reference/mitc4_reference_comparison_test.cpp:662`, `:712`).
- final reference verification에서 같은 worst absolute error는 tolerance의 약 `0.0190`이고
  U `147/147`, UR `147/147`, warning `0`으로 통과했다
  (`docs/reference-verifications/linear-static-mitc4-shell-reference-verification.md:138-157`).

문서 변경 커밋은 `f1be807`, comparator 완료 커밋은 `426cce1`이다.

#### 교훈

Reference mismatch가 보이면 먼저 element invariant와 patch test로 kernel을 독립 검증하고,
row identity/schema와 authoritative HDF5를 확인한 다음 tolerance 계약을 판단해야 한다.
Tolerance 완화가 kernel 조사보다 먼저 오면 결함 은폐가 되고, 외부 solver 내부 동등성을
가정한 과도한 tolerance는 false failure가 된다.

## 7. 정상적인 TDD RED와 실제 재작업의 구분

Phase ledger의 대부분 RED는 계획된 TDD 증거다. 다음 실패들은 “처음부터 구현이 잘못됐다”는
뜻이 아니라, 해당 Step이 소유한 behavior가 아직 없음을 테스트가 정확히 검출한 것이다.

| Step | 예상된 RED | GREEN에서 추가한 핵심 동작 | 근거 |
| --- | --- | --- | --- |
| 0-1 | shell semantic type과 mapping 부재 | S4/S4R source identity와 공통 FESA-MITC4 model | `phases/linear-static-mitc4-shell/index.json:5-19` |
| 2 | `shell_geometry.hpp` 부재 | deterministic normal/director와 geometry validation | `phases/linear-static-mitc4-shell/index.json:21-27` |
| 3 | `mitc4_shell.hpp` 부재 | 20 DOF kinematics, tying, constitutive, fixed quadrature | `phases/linear-static-mitc4-shell/index.json:29-35` |
| 4-5 | stiffness와 recovery API 부재 | physical/drilling stiffness 분리와 pure-drill-free recovery | `phases/linear-static-mitc4-shell/index.json:37-51` |
| 6-7 | shell scatter/assembly 부재 | 24-entry scatter와 source-ordered 576-entry COO reduction | `phases/linear-static-mitc4-shell/index.json:53-67` |
| 8 | drilling direction moment를 허용함 | aggregate moment projection 검사와 RHS 이전 거부 | `phases/linear-static-mitc4-shell/index.json:69-75` |
| 9-11 | state/recovery/HDF5 shell inventory 부재 | atomic state commit과 self-checked HDF5 replace | `phases/linear-static-mitc4-shell/index.json:77-99` |
| 12 | prescribed-only normalization 실패 | `Kff/Kfc/Ff` term-scale normalization | `phases/linear-static-mitc4-shell/index.json:101-107` |

반면 `RET-03`, `RET-07`, `RET-08`은 실제 환경 또는 코드 결함이었다. `RET-01`,
`RET-09`, `RET-10`은 upstream 정책·범위·tolerance 결정이 바뀌어 발생한 재작업이다.
이 구분을 유지해야 TDD의 의도된 실패를 품질 문제로 잘못 집계하지 않는다.

## 8. `dev` 병합 검증에서 발견한 ignored artifact 오염 (`RET-13`)

### 8.1 증상

Feature branch 자체와 병합 직후 MITC4 test는 통과했지만, `C:\git\FESADev`의 `dev`
worktree에서 full CTest를 실행하자 기존 B33 reference test 하나가 실패했다.

```text
B33ReferenceComparison.GeneratesAuthoritativeHdf5AndComparisonEvidence
referenceBefore.size() = 5, expected = 4
```

MITC4 변경으로 B33 reference가 바뀐 것이 아니라, 해당 worktree의
`reference/cantilever beam/`에 pre-existing `cantilever beam.h5`가 남아 있었다.
`.gitignore`가 모든 `.h5`를 무시하므로 일반 `git status`는 이 파일을 보여주지 않았다
(`.gitignore:20`). B33 test는 reference tree를 snapshot하고 정확히 네 파일만 있어야
한다고 검사한다 (`tests/reference/b33_reference_comparison_test.cpp:73`, `:178-191`).

### 8.2 해결

Reference artifact를 삭제하거나 수정하지 않았다. ignored HDF5를 다음 복구 가능한 위치로
이동했다.

```text
C:\git\FESADev\.harness\quarantine\preexisting-reference-artifacts\cantilever beam.h5
SHA-256: 233DF10D6A13941A477BC5133A0D686D939FEF598B3748B6DE69D2684320E01F
```

이동 뒤 focused B33 test `1/1`, full CTest `144/144`, Harness pytest `7/7`을 다시 실행했다.
Tracked reference path에는 변경이 없었다.

### 8.3 교훈

- clean worktree 감사에는 `git status`뿐 아니라 reference directory의 ignored file inventory도
  포함해야 한다.
- 테스트가 exact artifact count를 요구하면 build/output extension이 ignore되어 있는지
  함께 확인해야 한다.
- 사용자 또는 이전 실행이 만든 ignored file은 바로 삭제하지 말고 hash를 기록해 workspace
  내부 quarantine으로 이동하는 것이 안전하다.

## 9. 재사용할 개발 체크리스트

### 9.1 Requirements와 Numerical Review

- [ ] 외부 solver의 input label, internal algorithm과 observable reference quantity를
      분리했는가?
- [ ] readiness blocker가 실제 수치 판정에 필요한 정보인지 확인했는가?
- [ ] optional metadata, provenance, bundle naming을 자동으로 blocking gate로 올리지 않았는가?
- [ ] future nonlinear 식과 현재 executable scope를 명확히 분리했는가?

### 9.2 Shell kernel

- [ ] physical DOF와 numerical stabilization DOF의 transform, energy와 output을 분리했는가?
- [ ] translation/rotation 혼합 단위의 raw diagonal 또는 spectrum으로 계수를 정하지 않는가?
- [ ] normal/director reduction order와 invalid orientation predicate가 deterministic한가?
- [ ] reference 전에 rigid mode, rank, energy congruence와 independent patch를 통과하는가?

### 9.3 Parser와 reference comparison

- [ ] identity가 `(kind, scope, source label)`을 모두 포함하는가?
- [ ] source format의 case/whitespace 규칙을 comparator key에만 정확히 적용하는가?
- [ ] missing, extra, duplicate, nonfinite row를 tolerance 전에 거부하는가?
- [ ] source type 지원과 그 source type의 Abaqus artifact consumption을 별도 계약으로
      판단하는가?
- [ ] tolerance 변경 전에 authoritative HDF5와 kernel invariant를 확인했는가?

### 9.4 State, HDF5와 Harness

- [ ] complete candidate를 검증한 뒤 state를 한 번만 commit하는가?
- [ ] HDF5 temporary file을 close/reopen/self-check한 뒤 final을 교체하는가?
- [ ] Agent는 Executor가 선택한 Step 하나만 수행하고 Executor-owned 상태를 쓰지 않는가?
- [ ] process exit code와 structured diagnostic의 fatal error를 모두 검사하는가?
- [ ] 병합 전후 reference directory에 ignored generated artifact가 없는가?

## 10. 최종 결과와 남은 한계

최종 release readiness는 `ready-for-release`다
(`docs/releases/linear-static-mitc4-shell-release.md:250`). 근거는 다음과 같다.

- clean MSVC x64 Debug build와 compiler/linker warning `0`;
- full CTest `144/144`, Harness pytest `7/7`;
- Harness Step `0-13` 완료와 output `14/14` exit code `0`;
- S4 reference U `147/147` pass, UR `147/147` 비교와 warning `0`;
- exact 294 comparison rows, invalid row `0`;
- physics equilibrium, direction, symmetry, recovery와 energy sanity pass
  (`docs/releases/linear-static-mitc4-shell-release.md:168-181`).

다만 이 결과가 다음을 의미하지는 않는다.

- Abaqus S4/S4R 내부 정식화 동등성;
- reduced integration 또는 hourglass control 지원;
- S4R Abaqus reference acceptance;
- curved/distorted/thin-thick mesh 전체의 convergence 보장;
- geometric-nonlinear global 24 DOF tangent 구현 완료.

이 한계는 release 문서에 승인된 boundary로 남아 있다
(`docs/releases/linear-static-mitc4-shell-release.md:201-209`). 이번 작업의 가장 중요한
결론은 “Abaqus처럼 작동하게 만들기”가 아니라, FESA의 독립 정식화와 외부 observable
comparison 사이의 경계를 먼저 고정하고 각 계층의 invariant로 결함을 좁혀야 한다는
점이다.
