# 작업지시서 — 안전 피드램프 Advisory 후속 (WP4~WP7)

> 작성 2026-06-01. 다른 LLM/개발자 단독 실행용. 사전 맥락 없이 이 문서 + 참조로 수행.
> 컬럼 C-6111. **선행 통독 필수**: `docs/안전피드램프-한계치-브레인스토밍.md`(§0~§10), `plans/안전피드램프-advisory-작업지시서.md`(WP0~WP3), `docs/안전피드램프-검증시나리오매트릭스.md`.

---

## 0. 현재 상태 (main, 커밋 ~4e1dfc1까지 완료)

이미 구현·검증된 것:
- **버그픽스**: FeedforwardConfigStore ordinal off-by-one, FeedforwardEngine.ApplyFront front 부호(상단−하단).
- **WP0 Sim Override**: `SimOverrideStore`(ISimOverrideStore), `GET/POST/DELETE /api/ff/sim/override`, `appsettings Feedforward:SimOverrideEnabled=true`(DEMO 게이트). **엔진까지 통합**(`FeedforwardSupervisor.BuildSnapshotAsync` override 우선) + 안전가드(`_sim.Enabled` 시 auto-write 억제).
- **WP3 FeedRampAdvisor**: `FeedRampCalculator`(순수), `FeedRampAdvisorService`, `GET /api/ff/ramp-advisor`, DTO `FeedRampModels.cs`.
- **§10 역전판정**: `TempProfileJudge`(순수), 엔진 `JudgeTempProfile`(탑정 D 제외, spanRef 시드, 역전/붕괴 시 front trim HOLD), `AdvisoryResult.{TempProfileState,InversionPair,TempSpan,TempSpanRef}` + 컨트롤러 노출.
- 테스트 37/37, 라이브 S1~S5·S7·front·역전 검증 완료.

## 0.1 공통 제약 (전 WP)
1. **쓰기 금지**(OPC/제어 SP)는 advisory 전부에 유지. WriteGuard·ARM 경유만 실쓰기(기존). 신규 advisory는 계산/표시만.
2. **DEMO**: 실시간 값 = SP + 일정변동 반복(합성). 물리 동특성 추정 금지. temps는 비물리(공간구배 미인코딩) → 물리 검증은 override로 단조/특정 프로파일 주입해야.
3. **Sim Override로 자율검증**: `POST /api/ff/sim/override {enabled,values:{"tag.pv":v}}` → 엔진/ramp-advisor 모두 반영(~2s tick). 단 stale(S6)은 불가(override=항상 fresh), transient(P4)는 `FeedMoveThresholdPerMin>0` 필요.
4. **JSON**: 신규 응답은 컨트롤러에서 **익명객체 camelCase 매핑**(기존 `MapColumn`/`MapRamp` 패턴). 비유한(NaN/Inf)은 null로.
5. **빌드/테스트**: `dotnet build src/Web/ExperionCrawler.csproj`(경고0/에러0), `dotnet test tests/ExperionCrawler.Tests/ExperionCrawler.Tests.csproj`.
6. **커밋**: 기본 브랜치(main)면 브랜치 먼저. WP별 분리 커밋 권장.

## 0.2 C-6111 핵심 (변경 없음)
- 스트림: P(ficq-6118,Commanded,K0.95,θ60,τ900,sp_max2000,rate30), R(ficq-6113,Commanded,K0.8), D(ficq-6114,LevelDriven,K0.02,lica-6113), B(ficq-6116,LevelDriven,K0.03,li-6111). feed=ficq-6101.
- 컬럼: pressure=pica-6111, **dtdp=0(PCT off)**, delta_p_tag=null, sensitive_tray_tag=null, temp_tags=`tica-6111a,ti-6111b,ti-6111c,ti-6111d`.
- 추가 태그: `pi-6111b.pv`(≈리보일러 진공압), `ti-6103.pv`(프리히터 공급온도), `ficq-6115.pv`(스팀 flow 측정), `tica-6111a.op`(스팀밸브 OP 직결).
- 레이아웃(위→아래): pica-6111/REFLUX DIST(R)/TI-6111D/상부PACKING(정류,D)/제품추출 P(70~75%)/TI-6111C(제품 pivot)/긴중간PACKING(주분리,B)/TI-6111B/PREHEATER DIST(피드,TI-6103)/하부짧은PACKING(stripping)/TICA-6111A 리보일러(pi-6111b≈여기)→B.
- **정상 온도 A>B>C>D 단조감소**. TI-6111D는 환류 서브쿨 오염(조성 proxy 부적합).

## 0.3 코드 색인
| 역할 | 경로 |
|---|---|
| 엔진 | `src/Infrastructure/Control/FeedforwardEngine.cs` (Tick/ComputeStream/ApplyFront/ApplyRecovery/JudgeTempProfile) |
| supervisor | `src/Infrastructure/Control/FeedforwardSupervisor.cs` (BuildSnapshotAsync, AutoWriteAsync) |
| 계산기/판정 | `FeedRampCalculator.cs`, `TempProfileJudge.cs` |
| 모델 | `src/Core/Application/Feedforward/FeedforwardModels.cs`, `FeedRampModels.cs` |
| config store/DDL | `FeedforwardConfigStore.cs`, `src/Infrastructure/Database/ExperionDbContext.cs` |
| 컨트롤러 | `src/Web/Controllers/FeedforwardController.cs` (route api/ff) |
| 프론트 | `src/Web/wwwroot/js/ff.js`, `index.html`, `css` |
| 테스트 | `tests/ExperionCrawler.Tests/` |

---

## WP4 — §10 역전을 ApplyRecovery에 연동 + 코러보레이션 (난이도 中) ✅ 2026-06-01

### 목적
온도 역전/붕괴를 전환류 복귀 트리거 severity에 추가하되, **단발 센서이상이 곧장 recovery를 트리거하지 않도록 코러보레이션**(ΔP/vloss 동반 시에만 공정으로 인정).

### 단계
1. `FeedforwardEngine.ApplyRecovery` 시그니처에 `string? tempProfileState` 추가. `Tick`에서 `tp.State` 전달(JudgeTempProfile 결과; 이미 계산됨).
2. 신호 정의:
   - `sigInv = tempProfileState=="온도역전"`, `sigCollapse=="프로파일붕괴"`.
   - **코러보레이션**: `corroborated = sigVloss || sigDp`(기존 severity 신호). 
   - `tempSevere = (sigInv||sigCollapse) && corroborated` → `severe |= tempSevere`.
   - **비코러보(온도만)**: `sigInv && !corroborated` → severe 아님. 대신 advisory 메시지 "온도역전(센서 점검 권고 — ΔP/물질수지 정상)" 노출(새 필드 또는 modeReason 보강).
3. `SeverityText()`에 `tempSevere`면 "온도역전/붕괴" 추가.
4. (주의) ΔP 코러보는 WP6(pi-6111b→ΔP) 이후에야 유효. 그 전엔 vloss 코러보만 동작 → 코드는 둘 다 지원하되 ΔP 없으면 vloss로.

### 산출물/합격
- 단위테스트(`FeedforwardRecoveryTests`에 추가): ① 역전+vloss불균형 → severe(전환류 진입 경로), ② 역전 단독 → severe 아님 + 센서점검 메시지.
- 라이브: override로 (a)역전+B과추출(vloss) → recovery 트리거(RecoveryEnabled 필요), (b)역전만 → 트리거 안 됨.
- 빌드0/테스트 통과.

---

## WP6 — 압력 서브시스템 깨우기 (§9) (난이도 中, WP4·WP5 선행 권장) ✅ 2026-06-01

### 목적
pi-6111b를 활용해 **전탑 ΔP**(flooding) + **압력 프로파일 기반 PCT**를 활성화. 현재 dtdp=0·delta_p_tag=null로 dormant.

### 단계
1. **ΔP 파생**: `pi-6111b.pv − pica-6111.pv` = 전탑 ΔP. 
   - 옵션A(권장): supervisor/엔진에서 계산해 `pv.DeltaP`에 주입(현재 `cfg.DeltaPTag`로 읽는 구조). `BuildSnapshotAsync`에서 DeltaPTag 없으면 `pi-6111b − pica-6111`로 합성.
   - flooding ceiling(ramp-advisor)·ApplyRecovery sigDp가 이 ΔP 사용. `DeltaPFloodLimit`(config, 현재 MaxValue) 운전값으로 설정.
2. **압력 프로파일 PCT**: 현재 `BuildTemps`가 단일 압력(cfg.PressureTag)+dtdp. 확장:
   - 각 temp에 **국소 압력** 부여 = 높이 보간(또는 패킹가중): ti-6111d≈pica, tica-6111a≈pi-6111b, ti-6111c/ti-6111b=보간. 보간 비율은 config 또는 const(예: 높이 0/0.33/0.66/1.0 → 실제 표고 미상이라 우선 균등).
   - `TempCorrection.PressureCompensated(raw, p_local, pRef_local, dtdp)`. pRef도 프로파일(또는 단일 pRef + 동일 dtdp).
   - `dtdp`를 config에서 실측 보정계수로(현재 0). **dtdp=0이면 PCT=raw(무변경)** 유지.
3. config: `delta_p_tag`·`dtdp`·`delta_p_flood_limit`는 기존 컬럼 존재 → 값만 세팅(운전원/UI). 프로파일 보간 비율은 신규 const/config.

### 산출물/합격
- 단위테스트: dtdp≠0 + 프로파일 압력 → 각 temp pct가 국소압 반영(상·하 다른 보정). ΔP 합성 동작.
- 라이브: override로 pica/pi-6111b·temps 주입 → `/api/ff/advisory` temps.pct ≠ raw, flooding ceiling이 합성 ΔP 사용.
- **주의**: PCT 활성화는 §10 역전판정·front 입력을 바꾸므로 WP4 이후 회귀 확인.

---

## WP5 — 편차 trim + 2-point sensitive tray (§8·§8.8) (난이도 高) — **완료 ✅ 2026-06-01 (1·2·3단계)**

> **1단계**: 2-point front(`ApplyFront2Point`, 상부 ΔT(C−D)·하부 ΔT(C−B), C=pivot temps[n-2], 중립 상태).
> **2단계**: `ApplyCompositionTrim` — LevelDriven 드로우에 유계 trim(±5% clamp, B=하부dev·D=상부dev). 과도/역전 시 trim 0. `StreamAdvisory.{CompositionBase,Trim,RecommendedSpComposition,TrimSource}`.
> **3단계**: `CompositionStore`(랩 분율 수동입력, `/api/ff/composition` GET·POST·DELETE) → supervisor가 LevelDriven TargetCoeff 치환(없으면 config K). `B_SP = 분율×feed + trim`.
> 단위 4건 추가(49/49 통과). **advisory(쓰기 없음)**.
> **현장 calibrate 필요(데모 검증 불가)**: trim 게인(현 1%/°C placeholder)·부호·조성 분율값. rate제한(듀티 대역폭)은 후속(현재 clamp만).

### 목적
LevelDriven 드로우 권장을 `K×feed`(무한상승)에서 **[조성목표(분율×feed)] + [bounded 편차 trim]** 구조로. 편차는 §8.4 2-타임스케일.

### 선행 결정 (확정 2026-06-01)
- **조성 분율 = 랩/원료분석 수동입력**: 배치별 중비물/경비물 분율을 운전원 입력(신규 필드/태그). base = 분율×feed.
- **D 처리 = ΔT(C−D) 그대로 사용**: TI-6111D는 "오염" 아님 — (상승 기상물+환류 비산액) 혼합으로 하부 대비 ~10°C 낮은 유효온도(정정). 상부 front에 D 정상 사용.
- **편차 clamp = ±5%(보수)** + rate ≤ 듀티 대역폭(상호작용 방지).
- **config role = LevelDriven 유지** + trim은 **advisory 표시만(쓰기 없음)**. 재분류·level_tag 강등 안 함.

### 2-point front 부호 spec (C=제품 pivot=temps[n-2], 하단→상단 [A,B,C,D])
- **하부(B/중질) = ΔT(C−B)=temps[n-2]−temps[n-3]** (긴 중간 패킹). 정상 C<B(음수). 중질 상승→C 가열→C−B↑(dev>0)→**B↑**.
- **상부(D/경질) = ΔT(C−D)=temps[n-2]−temps[n-1]** (상부 정류 패킹). 정상 C>D(양수). 경질 침투→C−D↓(dev<0)→**환류↑**. (방향 calibrate.)
- 각 별도 `FrontPositionIndicator`(ColumnState 2개). n≥3 필요. trim(2단계): 하부dev→B, 상부dev→D, ±5% clamp + rate≤듀티대역폭. 데모는 단조 temps override로 calibrate.

### 단계 (결정 후)
1. config 확장: `UpperSensitiveTrayTag`+`LowerSensitiveTrayTag`(ff_column_config 신규 컬럼 + ExperionDbContext boot DDL + FeedforwardConfigStore SELECT/INSERT/UPDATE + MapConfig). 또는 섹션별 차온쌍.
2. 2-point front: `FrontPositionIndicator` 2개 인스턴스(ColumnState). 상부=ΔT(TI-6111C−TI-6111D보정), 하부=ΔT(TI-6111C−TI-6111B). ApplyFront 섹션별 반환.
3. fast trim(초~분): 섹션 front 편차 → bounded trim(clamp+rate). B=하부, D=상부.
4. 귀속: vloss를 "프론트 드리프트하는 스트림"에 배분(상부 고정·하부 이동=B).
5. slow re-baseline(시간): vLossMa+랩분석 → base 분율 재보정(운전원 리뷰, 자동 아님).
6. SP 산출: `B_SP = 분율×feed + trim`(advisory; 쓰기는 기존 정책).

### 산출물/합격
- 단위테스트: 상/하 front 분리, trim 부호·clamp, 귀속.
- 라이브: override로 하부 front 드리프트 → B trim만, 상부 → D trim만.
- **범위 주의**: 가장 크고 설계 미확정. 1단계(2-point front + 표시) → 2단계(trim) → 3단계(조성base SP)로 **분할 권장**.

---

## WP7 — 프론트 UI (ff.js) (난이도 中, 독립 진행 가능) ✅ 2026-06-01

### 목적
운전원 화면에 ① 피드 램프 계산기 ② 온도 프로파일 상태(역전) 표시 ③ (개발용) sim override 패널.

### 단계
1. **램프 계산기 패널**(`#pane-ff` 또는 카드 상단): 입력 targetFeed/deltaIAllow/sensibleGain/feedTempRef/floodLimit → `GET /api/ff/ramp-advisor?columnId=&...` → 결과표(clampedTarget, ceiling{value,binding}, rampRate{value,binding}, rampTimeMin, steam{from,to}, warnings). camelCase 응답 그대로.
2. **tempProfileState 뱃지**: 카드에 `tempProfileState`(정상/약화/붕괴/온도역전) 색상 뱃지 + `inversionPair`·`tempSpan`. 역전 시 경고색 + frontTrim "HOLD" 표기.
3. **sim override 패널**(개발/데모 전용, `SimOverrideEnabled` 시만 노출): 태그-값 입력 → `POST /api/ff/sim/override`, `DELETE`로 해제. `simOverrideActive` 표시.
4. 기존 ff.js 패턴(`ffCard`, fetch, esc) 재사용. `node -c ff.js` 문법 확인.

### 산출물/합격
- 브라우저에서 램프 계산기 동작(입력→결과), 역전 뱃지 표시, sim 패널로 주입/해제.
- `dotnet build` 영향 없음(정적자산). Ctrl+F5 후 확인.

---

## 권장 순서
1. **WP4**(역전→recovery, 작음) → 2. **WP6**(압력/ΔP/PCT — WP4 코러보 ΔP 공급) → 3. **WP7**(UI, 독립) → 4. **WP5**(편차 trim, 설계결정 후 분할).

## 검증 공통 (sim override 자율 루프)
```
curl -s -XPOST localhost:5000/api/ff/sim/override -H 'Content-Type: application/json' -d '{"enabled":true,"values":{...}}'
sleep 3; curl -s localhost:5000/api/ff/advisory   # 엔진 반영
curl -s 'localhost:5000/api/ff/ramp-advisor?columnId=1&targetFeed=1100&...'
curl -s -XDELETE localhost:5000/api/ff/sim/override
```
**앱 재기동** 필요(코드 변경 반영): 운전원이 `dotnet run` 재시작.

## 참조 / 메모리
- 메모리: `project_safe_feed_ramp_brainstorm`, `project_demo_system_synthetic_data`, `reference_json_serializer_pascalcase`, `project_sidedraw_ff_advisory`, `project_realtime_collector_stall`.
- `docs/안전피드램프-한계치-브레인스토밍.md` §7(편차/스팀)·§8(편차소스)·§9(압력)·§10(역전).
