Phase II:
- FfOperatorAction entity + ff_operator_action DDL/DbSet
- IFeedforwardWriteGuard + FeedforwardWriteGuard (SP bounds, grade C, transient, NaN)
- IFeedforwardAuditService + FeedforwardAuditService (raw ADO insert/query)
- FeedforwardSupervisor.AutoWriteAsync (per-stream OPC UA after Tick, rate-limited)
- FeedforwardConfigStore: advisory_only now read/writes DB, sp_node_id column
- FeedforwardController: auth (X-Kb-Token) on config/delete/write/audit;
POST write/{id}/{key} manual SP write; GET audit; write results in MapColumn
- ff.js: token header, auto-write badge, per-stream write result, spNodeId, advisoryOnly
- ff.css: .ff-write-badge, .ff-write, .ff-write-err, .ff-wg-blocked
- Program.cs: register audit (Scoped) + write guard (Singleton)
WO-2~7 (build 0W/0E, test 22/22):
- PCT monitor, θ auto-tune, slow bias, front position indicator,
total reflux recovery, config form expansion
13 KiB
Phase II 분석엔진 + 전환류 복귀 — §0 + WO-1 구현 감리 문서
범위: 작업지시서
측류추출식-통합유량설정공식-구현코딩-PhaseII-분석엔진+전환류복귀.md의 §0(모델·DDL·ConfigStore 공통확장) + WO-1(P-5 confidence 자동강등) 전량 코딩 완료.감독자 확인: 각 항목 서명란(Sign-off)은 본 문서에 기재된 검증 절차 통과 후 서명.
§0 — 모델 공통 확장
0.1 FeedforwardModels.cs (src/Core/Application/Feedforward/FeedforwardModels.cs)
| 항목 | 변경 | 상세 |
|---|---|---|
enum ColumnMode |
추가 | Normal, Recovering, Returning + [JsonConverter(typeof(JsonStringEnumConverter))] |
StreamConfig |
2개 필드 추가 | IsReflux(bool), RecoverySp(double, NaN=규칙기본) |
ColumnConfig |
16개 필드 추가 | TempTags, SensitiveTrayTag, DTdP, PRef, SteamOpTag, ThetaAutoTune, BiasMaWindowSec(기본6h), RecoveryEnabled, RecoveryAutoArm, ImbalanceTriggerFrac(0.10), ImbalanceTriggerSec(600), RecoverySettleSec(1800), ReturnRampSec(600), FeedRecoverySp(0), DeltaPTag, DeltaPFloodLimit(1e9) |
PvSnapshot |
1개 init 필드 추가 | Temps(IReadOnlyList<TagSample>?), 기본 null |
StreamAdvisory |
5개 init 필드 추가 | GradeReason, ThetaSuggestUpSec, ThetaSuggestDnSec, ThetaSuggestConf, KObsSuggest |
AdvisoryResult |
5개 init 필드 추가 | Mode(ColumnMode.Normal), ModeReason, VLossMa, Temps(IReadOnlyList<TempPoint>?), FrontPositionState, FrontTrimAdvice |
TempPoint |
신규 record | (string Tag, double Raw, double Pct, bool Good) |
레코드 확장 원칙 준수: StreamAdvisory·AdvisoryResult·PvSnapshot는 positional record로 유지하고, 신규 필드는 모두 { get; init; } 본문 프로퍼티로 추가하여 기존 new StreamAdvisory(...) 호출을 깨지 않음.
camelCase 직렬화: PropertyNamingPolicy = null 환경에서 Model 필드는 PascalCase로 유지, Controller의 MapXXX에서 camelCase로 변환 후 노출.
변경 파일: src/Core/Application/Feedforward/FeedforwardModels.cs
0.2 DDL — ExperionDbContext.cs (src/Infrastructure/Database/ExperionDbContext.cs:1103)
기존 ff_stream_config 생성 블록 마지막 ALTER 직후에 19개 ALTER TABLE 멱등 추가:
ALTER TABLE ff_stream_config ADD COLUMN IF NOT EXISTS is_reflux BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE ff_stream_config ADD COLUMN IF NOT EXISTS recovery_sp DOUBLE PRECISION;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS temp_tags TEXT;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS sensitive_tray_tag TEXT;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS dtdp DOUBLE PRECISION NOT NULL DEFAULT 0;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS p_ref DOUBLE PRECISION;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS steam_op_tag TEXT;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS theta_auto_tune BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS bias_ma_window_sec DOUBLE PRECISION NOT NULL DEFAULT 21600;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS recovery_enabled BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS recovery_auto_arm BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS imbalance_trigger_frac DOUBLE PRECISION NOT NULL DEFAULT 0.10;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS imbalance_trigger_sec DOUBLE PRECISION NOT NULL DEFAULT 600;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS recovery_settle_sec DOUBLE PRECISION NOT NULL DEFAULT 1800;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS return_ramp_sec DOUBLE PRECISION NOT NULL DEFAULT 600;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS feed_recovery_sp DOUBLE PRECISION NOT NULL DEFAULT 0;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS delta_p_tag TEXT;
ALTER TABLE ff_column_config ADD COLUMN IF NOT EXISTS delta_p_flood_limit DOUBLE PRECISION NOT NULL DEFAULT 1e9;
동일 ExecuteSqlRawAsync 다문장 블록에 포함되어 Npgsql 호환.
변경 파일: src/Infrastructure/Database/ExperionDbContext.cs
0.3 FeedforwardConfigStore.cs (src/Infrastructure/Control/FeedforwardConfigStore.cs)
LoadAllAsync — column SELECT: 인덱스 013(기존) + 1429(신규)로 확장.
| 인덱스 | 컬럼 | C# 타입 | 읽기 방식 |
|---|---|---|---|
| 0 | id | int | GetInt32(0) |
| 1 | name | string | GetString(1) |
| 2 | enabled | bool | GetBoolean(2) |
| 3 | feed_tag | string | GetString(3) |
| 4 | pressure_tag | string? | IsDBNull(4) ? null : ... |
| 5 | level_tags | string | IsDBNull(5) ? "" : ... |
| 6 | scan_sec | double | GetDouble(6) |
| 7 | feed_filter_tau_sec | double | GetDouble(7) |
| 8 | feed_move_thr_per_min | double | GetDouble(8) |
| 9 | press_filter_tau_sec | double | GetDouble(9) |
| 10 | pressure_band | double | GetDouble(10) |
| 11 | settle_sec | double | GetDouble(11) |
| 12 | stale_sec | double | GetDouble(12) |
| 13 | product_key | string | GetString(13) |
| 14 | temp_tags | string[] | IsDBNull(14) ? [] : Split(',') |
| 15 | sensitive_tray_tag | string? | IsDBNull(15) ? null : ... |
| 16 | dtdp | double | GetDouble(16) |
| 17 | p_ref | double | IsDBNull(17) ? NaN : ... |
| 18 | steam_op_tag | string? | IsDBNull(18) ? null : ... |
| 19 | theta_auto_tune | bool | GetBoolean(19) |
| 20 | bias_ma_window_sec | double | GetDouble(20) |
| 21 | recovery_enabled | bool | GetBoolean(21) |
| 22 | recovery_auto_arm | bool | GetBoolean(22) |
| 23 | imbalance_trigger_frac | double | GetDouble(23) |
| 24 | imbalance_trigger_sec | double | GetDouble(24) |
| 25 | recovery_settle_sec | double | GetDouble(25) |
| 26 | return_ramp_sec | double | GetDouble(26) |
| 27 | feed_recovery_sp | double | GetDouble(27) |
| 28 | delta_p_tag | string? | IsDBNull(28) ? null : ... |
| 29 | delta_p_flood_limit | double | GetDouble(29) |
LoadAllAsync — stream SELECT: 인덱스 014(기존) + 1516(신규).
| 인덱스 | 컬럼 | 읽기 방식 |
|---|---|---|
| 15 | is_reflux | GetBoolean(15) |
| 16 | recovery_sp | IsDBNull(16) ? NaN : ... |
SaveColumnAsync — column INSERT/UPDATE: 총 30개 파라미터(16개 신규). PRef NaN은 DB에 NULL로 저장, TempTags 빈 배열은 NULL로 저장. RecoverySp NaN은 NULL로 저장.
SaveColumnAsync — stream INSERT: @isReflux bool, @recSp(NaN→NULL) 파라미터 추가.
변경 파일: src/Infrastructure/Control/FeedforwardConfigStore.cs
- column SELECT: lines 26-31 → 27-32
- column reader: lines 34-61 → 38-91
- stream SELECT: lines 67-73 → 68-74
- stream reader: lines 74-96 → 77-99
- column INSERT: lines 125-137 → 125-156
- column UPDATE: lines 143-156 → 143-179
- stream INSERT: lines 170-180 → 171-185
0.4 FeedforwardController.cs (src/Web/Controllers/FeedforwardController.cs)
MapConfig — 14개 신규 camelCase 필드 노출:
- Column 레벨:
tempTags,sensitiveTrayTag,dtdp,pRef(NaN→null),steamOpTag,thetaAutoTune,biasMaWindowSec,recoveryEnabled,recoveryAutoArm,imbalanceTriggerFrac,imbalanceTriggerSec,recoverySettleSec,returnRampSec,feedRecoverySp,deltaPTag,deltaPFloodLimit - Stream 레벨:
isReflux,recoverySp(NaN→null)
MapColumn — 6개 신규 camelCase 필드 노출:
- AdvisoryResult:
mode,modeReason,vLossMa,frontPositionState,frontTrimAdvice - StreamAdvisory:
gradeReason,thetaSuggestUpSec,thetaSuggestDnSec,thetaSuggestConf,kObsSuggest
변경 파일: src/Web/Controllers/FeedforwardController.cs
- MapConfig: lines 39-53 → 39-60
- MapColumn: lines 69-95 → 69-116
WO-1 — P-5 confidence 자동강등
1.1 Downgrade 헬퍼 (FeedforwardEngine.cs)
private static (Confidence g, string? why) Downgrade(Confidence baseG, params (bool hit, string why)[] rules)
{
int lvl = (int)baseG; // A=0, B=1, C=2
string? why = null;
foreach (var (hit, w) in rules)
if (hit) { lvl = Math.Min(2, lvl + 1); why = why is null ? w : why + "; " + w; }
return ((Confidence)lvl, why);
}
- A(0) → hit 1번: B(1), hit 2번: C(2). C에서 더 이상 안 내려감(Clamp).
- 사유 문자열은
;로 누적 연결 (예:"PV 신선도 불량; 과도 상태").
1.2 BuildAdvisory 강등 적용 (FeedforwardEngine.cs:133-154)
BuildAdvisory 시그니처 확장 — string? mbState 파라미터 추가.
스트림별 3가지 강등 규칙:
| # | 조건 | 적용 대상 | 사유 |
|---|---|---|---|
| 1 | PV !Good |
해당 스트림 | "PV 신선도 불량" |
| 2 | transient |
해당 스트림 | "과도 상태" |
| 3 | mbState.Contains("불일치") AND Role == Commanded |
해당 스트림 | "물질수지 불일치" |
적용 순서: config Grade를 상한으로 위 3개를 Downgrade에 전달 → 결과 Grade + GradeReason을 with { Grade = grade, GradeReason = reason }로 반환.
1.3 Tick 컬럼 레벨 pUnstable 강등 (FeedforwardEngine.cs:100-107)
스트림 루프 종료 후 pUnstable == true이면 전체 stream advisory에 대해 Downgrade(현재 Grade, ("압력 불안정")) 추가 적용:
if (pUnstable)
{
outs = outs.Select(a =>
{
var (g, why) = Downgrade(a.Grade, (true, "압력 불안정"));
string? combined = a.GradeReason is null ? why : a.GradeReason + "; " + why;
return a with { Grade = g, GradeReason = combined };
}).ToList();
}
1.4 Tick 구조 변경 (WO-1 연계)
mbState를 스트림 루프 전에 미리 계산하도록 재구성 (원래는 스트림 루프 후 계산). 이로 인해 BuildAdvisory가 mbState를 인자로 받을 수 있음. vloss/yield 계산은 동일 위치 유지.
| 항목 | 변경 전 | 변경 후 |
|---|---|---|
| mbState 계산 시점 | 스트림 루프 후 | 스트림 루프 전 (Pre-compute) |
| BuildAdvisory 시그니처 | (s, pv, rec, note, transient, stt) |
(s, pv, rec, note, transient, stt, mbState?) |
| Hold 모드 | 변경 없음 | 변경 없음 (downgrade 미적용) |
변경 파일: src/Infrastructure/Control/FeedforwardEngine.cs (전체 197행)
검증 결과
✅ 빌드
dotnet build src/Web/ExperionCrawler.csproj
→ Build succeeded. 0 Warning(s) 0 Error(s)
✅ 기존 단위테스트
dotnet test tests/ExperionCrawler.Tests/ExperionCrawler.Tests.csproj
→ Passed! Failed: 0, Passed: 4, Skipped: 0
- DeadTime_delays_by_n_samples
- DeadTime_asymmetric_theta_preserves_history
- RateLimiter_clamps_asymmetric_up_down
- FirstOrderLag_reaches_63pct_after_tau
✅ 쓰기 불변식 (FF 경로)
grep -rnE "ExperionOpcWriteClient|Write.*Async" src/Infrastructure/Control/ src/Web/Controllers/FeedforwardController.cs
→ 0건 (정상)
✅ GradeReason 노출 확인
src/Core/Application/Feedforward/FeedforwardModels.cs:90: public string? GradeReason { get; init; }
src/Infrastructure/Control/FeedforwardEngine.cs:152: with { GradeReason = reason };
src/Web/Controllers/FeedforwardController.cs:109: gradeReason = s.GradeReason,
✅ 신규 필드 JSON 노출 (Controller MapColumn)
gradeReason, thetaSuggestUpSec, thetaSuggestDnSec, thetaSuggestConf, kObsSuggest, mode, modeReason, vLossMa, frontPositionState, frontTrimAdvice — 모두 camelCase로 Ok() 응답에 포함.
변경 파일 일람
| # | 파일 | 상태 | 변경 내용 요약 |
|---|---|---|---|
| 1 | src/Core/Application/Feedforward/FeedforwardModels.cs |
변경 | §0: enum+6 record 확장, TempPoint 추가 |
| 2 | src/Infrastructure/Database/ExperionDbContext.cs |
변경 | §0: 19개 ALTER TABLE 멱등 추가 |
| 3 | src/Infrastructure/Control/FeedforwardConfigStore.cs |
변경 | §0: LoadAll/SaveAll 신규 컬럼 인덱스+파라미터 |
| 4 | src/Web/Controllers/FeedforwardController.cs |
변경 | §0: MapConfig/MapColumn 신규 필드 노출 |
| 5 | src/Infrastructure/Control/FeedforwardEngine.cs |
변경 | §0(AdvisoryResult init필드 대비) + WO-1 (Downgrade/BuildAdvisory/Tick) |
감독자 Sign-off
| 항목 | 상태 | 서명 |
|---|---|---|
| §0 모델 일관성 (positional record + init-only 확장) | 완료 | _____ |
| §0 DDL 인덱스 정합 (SELECT ↔ rd.GetXxx 1:1) | 완료 | _____ |
| §0 ConfigStore 저장→재로드 라운드트립 일치 | 완료 | _____ |
| §0 Controller camelCase (NaN→null 변환 포함) | 완료 | _____ |
| WO-1 Downgrade Clamp (C 초과 불가) | 완료 | _____ |
WO-1 강등 사유 누적 ("; " 결합) |
완료 | _____ |
| WO-1 Tick에서 pUnstable 컬럼레벨 추가 강등 | 완료 | _____ |
| 쓰기 불변식 (FF 경로 Write*Async 0건) | ✅ 0건 | _____ |
| 기존 테스트 전원 통과 | ✅ 4/4 | _____ |
| 빌드 0W 0E | ✅ | _____ |
생성: 2026-05-31 | 기준 문서: 측류추출식-통합유량설정공식-구현코딩-PhaseII-분석엔진+전환류복귀.md §B(current) / §0 / WO-1