# 작업지시서 — 안전 피드램프 Advisory (WP1·WP2·WP3)

> 대상: 다른 LLM/개발자가 단독 실행 가능하도록 작성. 사전 맥락 없이 이 문서 + 참조 문서만으로 수행.
> 작성: 2026-06-01. 컬럼: C-6111 PGMEA 측류추출 증류탑.
> 설계 근거: `docs/안전피드램프-한계치-브레인스토밍.md` (§0~§9) — **반드시 먼저 통독**.

---

## 0. 배경 (요약)

LevelDriven 드로우가 `recommendedSp=K×피드`를 무제한 상승 표시하는 문제에서 출발 → "피드를 평형 깨지 않고 얼마나·얼마나 빨리 올리나"를 계산하는 **advisory(쓰기 없음)** 로 전환. 본 작업은 그 첫 구현 + 검증 체계.

작업 패키지:
- **WP0** Sim Override Layer (실행자가 입력값을 직접 조정해 자율 시뮬레이션; 제어 쓰기 아님)
- **WP1** 검증 시나리오 매트릭스 (문서, 구현 spec 겸 합격기준)
- **WP2** 현행 FF 엔진 프로빙 (시뮬레이션으로 baseline 거동 캡처)
- **WP3** Feed Ramp Advisor 구현 (read-only 계산기 + 엔드포인트 + 단위테스트)

실행 순서: **WP1(spec) + WP0(테스트 하네스) → WP2(자율 프로빙) → WP3(구현) → WP3를 WP1로 검증**.

---

## 0.1 공통 제약 (전 WP 적용, 위반 금지)

1. **쓰기 절대 금지**: OPC UA 쓰기·DB 쓰기 없음. 현 시스템은 AdvisoryOnly 정책. WP3는 순수 계산 + 조회만.
2. **DEMO 시스템**: 모든 실시간 값은 운전원이 주입한 합성값(테스트 벤치). 로직/기능 검증엔 유효하나, 데모 값으로 실제 플랜트 τ/θ·플러딩 임계를 단정하지 말 것. (메모리 `project_demo_system_synthetic_data` 참조.)
3. **시뮬레이션 절차 (2가지 경로)**:
   - **(자율, 권장) Sim Override** — WP0에서 만드는 in-app 입력 오버라이드. 실행자(LLM)가 `POST /api/ff/sim/override`로 입력 태그값을 직접 세팅 → `GET /api/ff/ramp-advisor`/`advisory` 관찰. **운전원 없이 루프 자율 구동.** OPC/제어 SP 쓰기 아님(입력값 치환만).
   - **(실계측 경로) 운전원 주입** — 운전원이 합성 DCS 값을 맞춰줌. 실행자는 "태그=값" 요청 후 `GET /api/ff/advisory` 또는 `run_sql`(realtime_table)로 관찰.
   - 두 경로 결과는 일치해야 함(오버라이드는 동일 입력을 in-app으로 대체).
4. **JSON 직렬화**: 신규 DTO 반환 시 PascalCase 패스스루 주의 → `[JsonPropertyName("camelCase")]` 명시 (메모리 `reference_json_serializer_pascalcase`). 기존 컨트롤러는 익명객체로 수동 camelCase 처리하는 패턴도 있음 — `GetDashboard` 응답 빌더 참고.
5. **빌드/테스트 명령**:
   - 빌드: `dotnet build src/Web/ExperionCrawler.csproj` → 경고 0 / 에러 0 필수
   - 테스트: `dotnet test tests/ExperionCrawler.Tests/ExperionCrawler.Tests.csproj`
6. **건드리지 말 것**: 기존 `FeedforwardEngine.Tick` 로직, `FeedforwardSupervisor.AutoWriteAsync`, config 스키마(ff_column_config/ff_stream_config). WP3는 **신규 파일 추가** 위주.

---

## 0.2 핵심 시스템 사실 (C-6111, column_id=1)

### 스트림 config (ff_stream_config)
| key | flow_tag | role | K(target_coeff) | θ_up/dn(s) | τ(s) | sp_min/max | rate_up/dn(kg/hr·min) | level_tag |
|---|---|---|---|---|---|---|---|---|
| P | ficq-6118 | Commanded | 0.95 | 60/60 | 900 | 0/2000 | 30/60 | – |
| R | ficq-6113 | Commanded | 0.80 | 0/0 | 0 | 0/2000 | 30/30 | – |
| D | ficq-6114 | LevelDriven | 0.02 | – | – | 0/1000 | 0/0 | lica-6113 |
| B | ficq-6116 | LevelDriven | 0.03 | – | – | 0/500 | 0/0 | li-6111 |

ProductKey = P. 피드 = ficq-6101. (K합 P+D+B=1.00 물질수지 폐합, R=내부환류.)

### 컬럼 config (ff_column_config id=1) — 현재 dormant 항목 주의
- pressure_tag=**pica-6111만**, sensitive_tray_tag=**null**, delta_p_tag=**null**, steam_op_tag=**null**, p_ref=**null**, dtdp=**0.0**, temp_tags=`tica-6111a,ti-6111b,ti-6111c,ti-6111d`
- → 압력/PCT/ΔP/front 서브시스템 전부 비활성

### 추가 태그 (수집됨, FF 미사용)
- `pi-6111b.pv` ≈ 리보일러 진공압 (탑저). `pica-6111.pv` = 탑정 진공압. **전탑 ΔP = pi-6111b − pica-6111**.
- `ti-6103.pv` = 원료 프리히터 공급온도. `ficq-6115.pv` = 스팀 투입 flow(측정전용). `tica-6111a.op` = 스팀밸브 OP(밸브직결).
- 태그 번호: P&ID는 10111, DCS는 6111 (동일계기, 10111→6111 치환).

### 물리 레이아웃 (위→아래, 패킹 3구간)
```
pica-6111 탑정 / REFLUX DIST(R) / TI-6111D / 상부PACKING(정류,D) /
제품추출 P(ficq-6118, 70~75%높이) / TI-6111C(제품 pivot) /
긴 중간PACKING(주분리,B front) / TI-6111B / PREHEATER DIST(피드,TI-6103) /
하부 짧은PACKING(stripping) / TICA-6111A 리보일러(pi-6111b≈여기) → B
```

### 코드 색인
| 역할 | 경로 |
|---|---|
| 엔진(수정금지) | `src/Infrastructure/Control/FeedforwardEngine.cs` |
| supervisor (스냅샷 빌드 참고) | `src/Infrastructure/Control/FeedforwardSupervisor.cs` (`BuildSnapshotAsync`) |
| config store | `src/Infrastructure/Control/FeedforwardConfigStore.cs` (`LoadAllAsync`) |
| 모델 | `src/Core/Application/Feedforward/FeedforwardModels.cs` (ColumnConfig/StreamConfig/PvSnapshot/TagSample) |
| stores 인터페이스 | `src/Core/Application/Feedforward/IFeedforwardStores.cs` |
| 컨트롤러 | `src/Web/Controllers/FeedforwardController.cs` (route `api/ff`, `GET dashboard`=`_store.GetAll()`) |
| 실시간 조회 | `IExperionDbService.GetRealtimeRecordsByTagNamesAsync(tags)` (반환 row: TagName/LiveValue/Timestamp) |
| DI 등록 | `src/Web/Program.cs` |
| 프론트 | `src/Web/wwwroot/js/ff.js` |
| 테스트 | `tests/ExperionCrawler.Tests/` |

---

## WP0 — Sim Override Layer (Claude 자율 테스트용 입력 변수)

### 목적
실행자(LLM)가 **운전원 없이 직접 입력값을 조정**해 시뮬레이션 루프를 자율 구동. 임의의 입력 태그(피드/압력/온도/스팀/스트림 PV)를 HTTP로 세팅 → advisor가 그 값을 읽음. **제어 쓰기 아님 — 입력 데이터 치환 계층.**

### 안전 원칙 (필수)
1. **OPC UA·제어 SP 쓰기 절대 없음**. 오버라이드는 *advisor가 읽는 입력값*만 in-memory로 대체.
2. **DB 쓰기 없음** (realtime_table 불변). 순수 in-memory 오버라이드 스토어.
3. **게이팅**: `appsettings`의 `Feedforward:SimOverrideEnabled=true`(기본 false, DEMO 전용)일 때만 동작. production 플래그면 엔드포인트 403.
4. 오버라이드 활성 시 advisory 응답/로그에 `simOverrideActive=true` 명시(은폐 금지).

### 설계
- **`SimOverrideStore`** (싱글톤, in-memory): `volatile bool Enabled`, `ConcurrentDictionary<string,(double value,DateTime ts)> Values`. 메서드: `SetMany(dict)`, `Clear()`, `TryGet(tag,out v)`, `Snapshot()`.
  - **⚠ thread-safety 필수**: Singleton이므로 동시 HTTP 요청 간 Dictionary 경합 가능. `Dictionary`는 thread-safe하지 않음 → 반드시 `ConcurrentDictionary` 사용. `Enabled`도 ARM64 메모리 모델 고려해 `volatile` 또는 `Interlocked`로 보호.
  - 경로: `src/Infrastructure/Control/SimOverrideStore.cs` (+ 인터페이스 `ISimOverrideStore` in Core).
- **읽기측 통합**: `FeedRampAdvisorService`(WP3-C)가 라이브 태그를 읽을 때, `SimOverrideStore.Enabled && TryGet(tag)` 이면 **오버라이드값 우선**(ts=now로 신선 처리), 없으면 live. 한 곳(태그 read 헬퍼)에서만 분기.
- **엔드포인트** (`FeedforwardController`, DEMO 게이트):
  - `POST /api/ff/sim/override` body `{ "enabled":true, "values":{"ficq-6101.pv":1100,"pica-6111.pv":40,"pi-6111b.pv":80,"ti-6103.pv":150,"ficq-6115.pv":300} }`
  - `GET /api/ff/sim/override` → 현재 스냅샷
  - `DELETE /api/ff/sim/override` → Clear + Enabled=false
- **DI**: `SimOverrideStore` 싱글톤 등록(Program.cs).

### 산출물
- `ISimOverrideStore`/`SimOverrideStore`, 3개 엔드포인트, DI 등록, appsettings 플래그.

### 합격기준
- `SimOverrideEnabled=true`에서 `POST`로 ficq-6101.pv 세팅 → `GET /api/ff/ramp-advisor`가 그 값을 currentFeed로 사용.
- `SimOverrideEnabled=false`(기본)에서 sim 엔드포인트 403, advisor는 live만 사용.
- 코드 grep: sim 경로에서 `WriteTagAsync`/SQL write **0건**.

### 한계 → 엔진 확장으로 일부 해소 (2026-06-01)
초기: Sim Override가 `FeedRampAdvisorService`(ramp-advisor)만 통합, 엔진 미반영.
**확장 적용**: `FeedforwardSupervisor.BuildSnapshotAsync`의 `Sample`/`SampleExact`가 override 우선 → `/api/ff/advisory`(엔진 산출)도 override 반영. **안전가드**: `_sim.Enabled` 시 auto-write 억제(가짜 입력이 실제 OPC 쓰기 유발 방지).
- **해소**: S7(mbState 과추출)·§10/front 물리검증은 이제 **override로 자율 가능**(B·temps 주입 → 다음 supervisor tick~2s 반영).
- **여전히 불가**: S6(stale) — override는 항상 Good=fresh라 stale 시뮬 불가(단위테스트 `pv.Feed.Good=false`로 커버). P4(transient) — `FeedMoveThresholdPerMin=0`이라 config 임계 설정 필요(override만으론 미발화).

### 실행자 사용 예 (자율 루프)
```
curl -s -XPOST :5000/api/ff/sim/override -H 'Content-Type: application/json' \
  -d '{"enabled":true,"values":{"ficq-6101.pv":900}}'
curl -s ':5000/api/ff/ramp-advisor?columnId=1&targetFeed=1100&deltaIAllow=50'
# → 기대치(WP1 S1)와 대조
curl -s -XDELETE :5000/api/ff/sim/override
```

---

## WP1 — 검증 시나리오 매트릭스 (문서)

### 목적
WP3 구현 **전에** "입력값 → 기대 advisory 출력"을 명세. 이 문서가 곧 WP3의 구현 spec이자 합격기준(WP3 단위테스트 = 이 시나리오들).

### 선행조건
- §0.2 숙지, 브레인스토밍 §3·§7 공식 숙지.

### 산출물
- `docs/안전피드램프-검증시나리오매트릭스.md`

### 상세 단계
1. 시나리오 표 형식 정의(열): `ID | 목적 | 주입값(태그=값) | 기대 출력(ceiling/binding, rampRate/binding, rampTimeMin, steamTarget, warnings) | 합격기준`.
2. 아래 시나리오를 채울 것 (값·기대치 계산 포함). **숫자는 §0.2 config로 직접 계산해 명시**:

| ID | 목적 | 주입 | 기대(요지) |
|---|---|---|---|
| S0 | baseline 평형 | 현 설정값(피드 ~900 등) | 변화요청 없음 → 현상태 표시, warning에 dormant 항목 |
| S1 | 피드 증량(동특성 binding) | targetFeed=1100, 압력·조성 고정 | rampRate≈3.29(ΔI=50), binding=**dynamic**, rampTime≈61min, steam ×1.222 |
| S2 | 밸브 슬루 binding | (가정) P rate_up=2 로 낮춤 + targetFeed=1100 | rampRate=2/0.95=2.1, binding=**valveSlew** |
| S3 | ceiling 초과 목표 | targetFeed=2200 | clampedTarget=2105(=2000/0.95), binding=**P sp_max** |
| S4 | flooding ceiling | ΔP(pi-6111b−pica-6111) 크게 주입 + floodLimit 설정 | ceiling이 flooding으로 더 낮아짐, binding=**flooding** |
| S5 | TI-6103 하강(현열FF) | 피드 고정(900), ti-6103 150→140, sensibleGain·feedTempRef 지정 | steamTarget↑ = sensibleGain×900×(150−140) 만큼. 피드 무관 외란 FF |
| S6 | stale/stall | 태그 timestamp frozen(>stale_sec) | advisory HOLD/무효 + warning=stale |
| S7 | 과추출 감지(현행엔진) | B의 ficq-6116.pv 를 0.03×feed 대비 크게 주입, D+P+B≠feed | (현행) mbState "물질수지 불일치" 플래그 |

3. 각 시나리오에 **검증 방법**(주입 태그 목록 + 관찰 엔드포인트/SQL) 명기.
4. S1 worked example 계산을 부록으로 첨부(아래 §부록 공식 사용).

### 합격기준
- 7개 시나리오 모두 주입값·기대출력·합격기준·검증방법이 구체 수치로 채워짐.
- 기대 수치가 §부록 공식과 일치(재현 가능).

---

## WP2 — 현행 FF 엔진 프로빙 (baseline 캡처)

### 목적
시뮬레이션으로 현행 엔진의 실제 거동을 캡처 → (a) 코드리딩 주장 검증, (b) WP3 전 baseline 확보, (c) dormant 서브시스템 동작상 확인.

### 선행조건
- 웹앱 가동(`:5000`), `GET /api/ff/advisory` 응답 확인. 운전원에게 값 주입 요청 가능 상태.

### 산출물
- `docs/안전피드램프-현행엔진-프로빙결과.md` (각 프로브: 주입값 → 관찰된 advisory 표)

### 상세 단계 (각 프로브 = 운전원 주입 요청 → dashboard/SQL 관찰 → 기록)
| 프로브 | 주입 요청 | 관찰 포인트 | 예상(코드기준) |
|---|---|---|---|
| P0 | (없음) 현 상태 | dashboard 전체 | 각 스트림 recommendedSp/valid/grade, vloss/mbState 스냅샷 |
| P1 | ficq-6101.pv 900→1100 | B(ficq-6116) recommendedSp | 0.03×ff로 **상승**, clamp/rate 미적용 확인 |
| P2 | pica-6111.pv 변동 | transient/pUnstable | PressureBand=max라 **pUnstable 미발화**, PCT=raw(dtdp=0) |
| P3 | pi-6111b.pv 변동 | advisory 전체 | **무반응**(미사용) 확인 |
| P4 | ficq-6101.pv 급변 | c.transient, valid | transient=true, 스트림 valid=false |
| P5 | D+P+B ≠ feed 주입 | mbState, vloss | "물질수지 불일치" |
| P6 | 정상 온도프로파일 + front 부호 | frontPositionState/trimAdvice, temps | 정상 A>B>C>D 확인. **DiffTemp 부호버그(브레인스토밍 §10.2-A) 재현**: 프론트 상승/하강 라벨·환류↑/boilup↑ 권고가 반전됐는지. D의 환류 서브쿨 기여 측정 |

### 합격기준
- 6개 프로브 결과가 표로 기록됨.
- P1·P2·P3가 코드리딩 주장(B 무제한 상승 / 압력 dormant / pi-6111b 미사용)을 **동작상 확인 또는 반증**. 반증 시 차이를 문서화.

---

## WP3 — Feed Ramp Advisor 구현 (read-only)

### 목적
목표 피드 입력 → **한계치(ceiling) · 램프율(rate) · 예상시간 · binding 제약 · 스팀(FIQ-6115) 목표**를 계산해 반환하는 advisory. **쓰기 없음**. WP1 시나리오를 통과해야 함.

### 설계

#### (A) 순수 계산기 (단위테스트 대상) — `FeedRampCalculator`
신규: `src/Infrastructure/Control/FeedRampCalculator.cs`. **순수 함수**(부수효과 0)로 작성해 테스트 용이하게.

```
public static FeedRampAdvisory Compute(
    ColumnConfig cfg,
    PvSnapshot pv,            // 현재 PV (feed/streams/pressure 등)
    double targetFeed,
    double deltaIAllow,       // 허용 순간 불균형 kg/hr (기본 50)
    double sensibleGain,      // (a) 현열 게인 ≈ Cp_feed/λ_steam [kg스팀/(kg피드·°C)]. NaN/0=현열보정 off+warning
    double feedTempRef,       // (a) 앵커 기준 feed 온도(평형). NaN=현열보정 off+warning
    RampExtraInputs extra);   // pica/pi6111b/ti6103/fiq6115/op 등 부가 태그 값 (옵션)
```

계산 순서:
1. `currentFeed = pv.Feed.Value` (Good 아니면 HOLD 반환 + warning=feed-bad).
2. **Ceiling**:
   - `valveCeiling = min over (Commanded ∪ K>0 streams) of (sp_max_i / K_i)`; binding=해당 key.
   - `floodingCeiling`: extra에 pica·pi6111b 둘 다 있고 `floodLimit`(파라미터/옵션) 있으면: `ΔPnow=pi6111b−pica`; `feedFlood=currentFeed×(floodLimit/ΔPnow)^(1/n)` (n 기본 1.8). 없으면 skip + warning="flooding ceiling 미산정(ΔP/limit 부재)".
   - `steamCeiling`: 산정 근거(최대 OP/스팀) 없으면 skip + warning.
   - `ceiling = min(존재하는 것들)`; binding 라벨.
3. **Ramp rate** (kg/hr per min):
   - `(a) valveSlew = min over Commanded (rate_up_i / K_i)`
   - `(b) dynamic = deltaIAllow × 60 / (K_P × (τ_P + θ_P))` (P=ProductKey, θ_P=ThetaUpSec, τ_P=TauSec)
   - `(c) energyLoop`: 에너지루프 시상수 입력 있으면 산정, 없으면 skip + warning.
   - `R_feed = min(존재)`; binding 라벨.
4. `clampedTarget = min(targetFeed, ceiling)`.
5. `rampTimeMin = max(0, (clampedTarget − currentFeed)) / R_feed` (target<current면 down-ramp: rate_dn 사용 분기 + 별도 표기).
6. **Steam target** (extra.fiq6115 있을 때) — (a)안 현열 보정 포함:
   ```
   fiq6115To = fiq6115Cur × (clampedTarget / currentFeed)              // 처리량 비례(앵커 온도 기준)
             + sensibleGain × clampedTarget × (feedTempRef − ti6103Now) // 현열 보정(앵커 대비 편차)
   ```
   - **T_b 소거 유도**: 전량식 `FIQ=[F·Cp·(T_b−T_feed)+c·F]/λs`에서 비례항을 빼면 잔차 = `(Cp/λs)·F·(T0−T1)` → 비점 불필요, `sensibleGain`만으로 충분.
   - `sensibleGain` 또는 `feedTempRef`가 NaN/0이면 현열항=0 + warning="현열보정 off". `ti6103Now`=extra의 ti-6103 값.
   - S1(온도 고정: ti6103Now≈feedTempRef)→편차≈0→순수 비례. S5(피드 고정·온도↓)→편차>0→steam↑.
   - startOP 제안은 local gain 부재 시 omit + warning.
7. `warnings[]`: dormant/부재 항목 전부 적재(dtdp=0 PCT off, pi-6111b 미사용, flood/energy/steam 근거 부재 등).

#### (B) DTO — `src/Core/Application/Feedforward/FeedRampModels.cs`
record + `[JsonPropertyName]` camelCase:
```
FeedRampAdvisory(columnId, currentFeed, targetFeed, clampedTarget,
  Bound ceiling, Bound rampRate, double rampTimeMin,
  SteamTarget steam, bool simOverrideActive, string[] warnings)
Bound(double value, string binding)          // binding 예: "valveSlew@P","dynamic","P sp_max","flooding"
SteamTarget(double? fiq6115From, double? fiq6115To, double? startOpPct)
```

#### (C) 서비스 — `FeedRampAdvisorService`
신규 `src/Infrastructure/Control/FeedRampAdvisorService.cs`. 라이브 데이터 수집 + 계산기 호출.
- 주입: `IFeedforwardConfigStore`, `IExperionDbService`, **`ISimOverrideStore`(WP0)**.
- `LoadAllAsync`로 cfg 찾기 → 필요한 태그 목록(feed/streams/pica-6111/pi-6111b/ti-6103/ficq-6115/tica-6111a.op) 구성 → `GetRealtimeRecordsByTagNamesAsync` → `PvSnapshot`+`RampExtraInputs` 빌드(스냅샷 빌드는 `FeedforwardSupervisor.BuildSnapshotAsync` 패턴 참고, **단 복붙 말고 필요한 태그만**) → `FeedRampCalculator.Compute` 호출.
- **태그 read 헬퍼 단일 분기**: `SimOverrideStore.Enabled && TryGet(tag)` 면 오버라이드값(ts=now), 아니면 live. 응답에 `simOverrideActive` 플래그 전달.
- 신선도: timestamp가 stale_sec 초과면 해당 태그 Good=false → 계산기에서 HOLD/warning.

#### (D) 엔드포인트 — `FeedforwardController`
신규 `[HttpGet("ramp-advisor")]`:
```
GET /api/ff/ramp-advisor?columnId=1&targetFeed=1100&deltaIAllow=50&floodLimit=&n=1.8&sensibleGain=&feedTempRef=
→ FeedRampAdvisory (JSON camelCase)
- sensibleGain·feedTempRef: 미전달 시 현열보정 off(+warning). config 필드화는 후속(현재 query/기본).
- ⚠ `floodLimit` query param과 `ColumnConfig.DeltaPFloodLimit`(DB 저장) 중복: query param이 config보다 우선(config 있으면 config 값이 기본; query param 전달 시 override). 문서화 필수.
```
- 읽기 전용. 컨트롤러에 `FeedRampAdvisorService` 주입(생성자 추가).

#### (E) DI 등록 — `Program.cs`
`FeedRampAdvisorService`를 scoped 등록(다른 FF 서비스 등록부 근처).

#### (F) 단위테스트 — `tests/ExperionCrawler.Tests/FeedRampCalculatorTests.cs`
- WP1 시나리오 S1~S5를 `FeedRampCalculator.Compute`에 대한 단위테스트로 작성(합성 ColumnConfig + PvSnapshot 직접 구성, 라이브 불요).
  - **S5는 `sensibleGain`·`feedTempRef` 인자를 명시 전달**해 현열보정 검증(부록A S5 예제: sensibleGain=0.001, feedTempRef=150, ti6103=140 → steam +9.0).
  - S7(물질수지 불일치)은 **`FeedRampCalculator` 대상 아님** — mbState는 `FeedforwardEngine`에서만 산출. S7은 WP2(현행엔진 프로빙) 전용. WP3에 포함하려면 통합테스트(`FeedforwardEngine` 경유)로 별도 작성.
- 예) S1: C-6111 config + currentFeed=900, targetFeed=1100, ΔI=50 → `rampRate.value≈3.29`(허용오차), `rampRate.binding=="dynamic"`, `rampTimeMin≈60.8`, `ceiling.value≈2105`, `ceiling.binding` P 관련, `steam.fiq6115To≈fiq6115From×1.2222`.

### 상세 단계 (순서)
1. (B) DTO 작성 → 빌드.
2. (A) 계산기 작성 → 빌드.
3. (F) 단위테스트 작성 → `dotnet test` 통과(= WP1 충족) 까지 (A) 보정.
4. (C) 서비스 → (E) DI → (D) 엔드포인트.
5. 빌드 경고 0/에러 0.
6. (시뮬레이션) 운전원에 S1 값 주입 요청 → `GET /api/ff/ramp-advisor?columnId=1&targetFeed=1100` 응답이 WP1 기대치와 일치 확인.
7. (선택, WP3b) `ff.js`에 "피드 램프 계산기" 패널(입력칸+결과표) 추가 — 별도 승인 후.

### 합격기준
- `dotnet build` 경고 0 / 에러 0, `dotnet test` 전건 통과.
- WP1 시나리오 S1~S5 단위테스트 통과. (S7은 Engine 레벨 통합테스트로 별도 — 이 문서 범위 외.)
- 엔드포인트가 S1 라이브 주입에 대해 기대 JSON 반환(rampRate binding=dynamic, time≈61min, steam ×1.22).
- 쓰기(OPC/DB) 호출 **0건** (코드 grep으로 확인: WriteTagAsync/Insert/Update 미사용).
- 모든 미산정 항목이 `warnings[]`에 노출(은폐 금지).

### 비범위 (이번 WP3 제외 — 후속)
- 편차 trim / sensitive tray 2-point / 압력 서브시스템 깨우기 / 에너지루프 시상수 식별 / 자동쓰기. (브레인스토밍 §7.5·§8·§9 — 별도 작업지시서.)

---

## 부록 A. 공식 요약 (재현용)

```
[Ceiling]
 valveCeiling = min_i( sp_max_i / K_i )
 floodingCeiling = currentFeed × (floodLimit / ΔPnow)^(1/n),  ΔPnow = pi6111b − pica6111
 ceiling = min(available)

[Ramp rate, kg/hr per min]
 (a) valveSlew = min_i( rate_up_i / K_i )            (Commanded)
 (b) dynamic   = ΔI_allow × 60 / (K_P × (τ_P + θ_P)) (P=ProductKey, 초 단위 τ,θ)
 (c) energyLoop= (에너지루프 시상수 기반, 입력시)
 R_feed = min(available)

[Time]  rampTimeMin = (clampedTarget − currentFeed) / R_feed
[Steam] fiq6115To = fiq6115Cur × (clampedTarget/currentFeed)
                  + sensibleGain × clampedTarget × (feedTempRef − ti6103Now)   // (a) 현열보정
        sensibleGain ≈ Cp_feed/λ_steam [kg스팀/(kg피드·°C)]; feedTempRef=앵커 feed온도
```

### C-6111 worked example (S1: 900→1100, ΔI=50)
```
valveSlew = 30/0.95 = 31.6
dynamic   = 50×60 / (0.95×(900+60)) = 3000/912 = 3.29   ← binding
R_feed    = 3.29 kg/hr·min
rampTime  = (1100−900)/3.29 = 60.8 min
valveCeiling = min(2000/0.95, 2000/0.8, 1000/0.02, 500/0.03) = 2105 (P)
steam     = fiq6115Cur × 1100/900 = ×1.2222  (S1: ti6103=feedTempRef → 현열항 0)
```

### C-6111 worked example (S5: 피드 900 고정, TI-6103 150→140, sensibleGain=0.001, feedTempRef=150)
```
처리량비례 = fiq6115Cur × 900/900 = fiq6115Cur (변화 0)
현열보정   = 0.001 × 900 × (150 − 140) = 9.0 kg/hr
fiq6115To  = fiq6115Cur + 9.0   ← steam↑ (피드 변화 없이 TI-6103 외란만으로)
※ sensibleGain=0.001은 DEMO placeholder. 실제는 Cp_feed/λ_steam로 캘리브레이션.
```

## 부록 B. 진단 결과 (2026-06-01, diagnosis-checklist.md §1~§8 수행)

### 발견된 문제점

| # | 항목 | 등급 | 상태 |
|---|------|------|------|
| 1 | `SimOverrideStore`에 `ConcurrentDictionary` + `volatile bool` 미명시 — Singleton 동시성 경합 위험(ARM64) | 🟠 MED | WP0 §96에 수정 반영 |
| 2 | S7(mbState)를 `FeedRampCalculator.Compute` 단위테스트 대상으로 지정 — S7은 Engine 전용, Calculator로 테스트 불가 | 🟠 MED | WP3 §253·§267 수정 반영 |
| 3 | `floodLimit` query param과 `ColumnConfig.DeltaPFloodLimit`(DB) 이중 소스 — 우선순위 불명확 | 🟡 LOW | WP3 §244에 주석 추가 |
| 4 | `appsettings.json`에 `Feedforward:SimOverrideEnabled` 섹션 없음 — 구현 시 누락 주의 | 🟡 LOW | 사전 공유로 대체 |
| 5 | `RampExtraInputs`가 C-6111 태그 hardcode — 타 컬럼 확장 시 ColumnConfig 확장 필요 | 🟡 LOW | 현 스코프(C-6111) 한정 |
| 6 | **S5(스팀/TI-6103)가 #2와 동형 모순** — 스팀 1차식은 피드 고정 시 변화=0인데 S5는 steam↑ 기대. 현열부하는 앵커링으로 소거 불가 → 별도 게인 필수 | 🟠 MED | **해소 (a 확정)** |

### #6 해소 = (a) 확정 (2026-06-01)
- **`sensibleGain`(≈Cp/λ_steam) + `feedTempRef`(앵커 feed온도) 파라미터 도입**. 현열보정 = `sensibleGain × clampedTarget × (feedTempRef − ti6103Now)`.
- **T_b 소거 유도**: 전량식에서 처리량비례항을 빼면 잔차 = `(Cp/λs)·F·(T0−T1)` → 비점 불필요, 게인 1개로 충분.
- S5 mandatory 유지(WP3-F·§270). 두 파라미터 NaN/0이면 현열항 0 + warning(안전 기본).
- 반영: Compute 시그니처(§198~), step6(§220~), 엔드포인트 query param, 부록A 공식·S5 예제.

### 진단 보강 (Opus 재검토)
- **#1 보강**: `ConcurrentDictionary`는 per-key 안전만 보장. `SetMany`+`Snapshot` 그룹 원자성은 미보장 → 일관된 다중태그 스냅샷이 필요하면 lock 또는 immutable-dict swap. (순차 테스트 사용엔 무방.)
- **#5 보강**: `steam_op_tag`는 ColumnConfig에 이미 존재 → 그건 config에서 읽고, config에 없는 태그(pi-6111b/ti-6103/ficq-6115)만 하드코딩+TODO.
- **(b) 공식 일반화(minor)**: dynamic 제약은 P만이 아니라 lagging Commanded 전체 합 `ΔI×60/Σ K_i(τ_i+θ_i)`이 정확. C-6111은 P만 τ≠0이라 결과 동일.

### 교차 검증 결과 (STEP 6)
- 이미 수정된 문제(Q1): 없음
- 다른 레이어에서 처리(Q2): 없음
- 의도적 설계(Q3): #4·#5는 Phase I 스코프 한정으로 의도적
- 재현 불가(Q4): 없음
- **누락(Opus 추가)**: #6 (S5/스팀 현열항 모순) — 진단 체크리스트가 #2(S7)는 잡았으나 동형인 S5는 놓침

---

## 부록 C. 참조 문서
- `docs/안전피드램프-한계치-브레인스토밍.md` (설계 전문 §0~§9)
- 메모리: `project_safe_feed_ramp_brainstorm`, `project_demo_system_synthetic_data`, `reference_json_serializer_pascalcase`, `project_sidedraw_ff_advisory`
