Files
ExperionCrawler/plans/안전피드램프-advisory-작업지시서.md
windpacer 60946f3c47 feat: Sim Override를 FF 엔진까지 확장 (S7/§10/front 자율검증)
- FeedforwardSupervisor.BuildSnapshotAsync Sample/SampleExact: override 우선(신선) → /api/ff/advisory(엔진)도 override 반영
- 안전가드: _sim.Enabled 시 auto-write 억제(가짜 입력→실제 OPC 쓰기 방지)
- 해소: S7(mbState)·§10/front 자율검증 가능. 잔여: S6(override=fresh)·P4(FeedMoveThresholdPerMin=0)
- 작업지시서 WP0 한계 갱신

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:30:32 +09:00

25 KiB
Raw Permalink Blame History

작업지시서 — 안전 피드램프 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. 게이팅: appsettingsFeedforward: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.BuildSnapshotAsyncSample/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-6111bpica-6111) 크게 주입 + floodLimit 설정 ceiling이 flooding으로 더 낮아짐, binding=flooding
S5 TI-6103 하강(현열FF) 피드 고정(900), ti-6103 150→140, sensibleGain·feedTempRef 지정 steamTarget↑ = sensibleGain×900×(150140) 만큼. 피드 무관 외란 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 "물질수지 불일치" 플래그
  1. 각 시나리오에 검증 방법(주입 태그 목록 + 관찰 엔드포인트/SQL) 명기.
  2. 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=pi6111bpica; 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_bT_feed)+c·F]/λs에서 비례항을 빼면 잔차 = (Cp/λs)·F·(T0T1) → 비점 불필요, 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) 구성 → GetRealtimeRecordsByTagNamesAsyncPvSnapshot+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  = (1100900)/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 SimOverrideStoreConcurrentDictionary + 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.jsonFeedforward: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·(T0T1) → 비점 불필요, 게인 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