Files
ExperionCrawler/docs/PhaseII-분석엔진+전환류복귀-to-WO-1.md
windpacer 7c26aa7361 feat: Phase II auto-write (WriteGuard, audit, auth) + WO-2~7 완료
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
2026-05-31 20:30:06 +09:00

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·PvSnapshotpositional 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 + GradeReasonwith { 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