diff --git a/plans/안전피드램프-advisory-작업지시서.md b/plans/안전피드램프-advisory-작업지시서.md index 14f8cab..5e747df 100644 --- a/plans/안전피드램프-advisory-작업지시서.md +++ b/plans/안전피드램프-advisory-작업지시서.md @@ -111,12 +111,11 @@ pica-6111 탑정 / REFLUX DIST(R) / TI-6111D / 상부PACKING(정류,D) / - `SimOverrideEnabled=false`(기본)에서 sim 엔드포인트 403, advisor는 live만 사용. - 코드 grep: sim 경로에서 `WriteTagAsync`/SQL write **0건**. -### ⚠ 한계 (구현 후 확인, 2026-06-01) -**Sim Override는 `FeedRampAdvisorService`(ramp-advisor)만 통합. 엔진(`FeedforwardSupervisor.BuildSnapshotAsync`)은 DB를 직접 읽어 override 미반영.** -- 결과: **S7(mbState 과추출)·S6(stale) 라이브 검증은 override로 불가** — `/api/ff/advisory`(엔진 산출)는 override 무시. -- S7 라이브 = 운전원이 실제 DCS/realtime_table에 합성값 주입 / 또는 Engine 통합테스트. S6 라이브 = stale 태그(운전원) / 또는 단위테스트(`pv.Feed.Good=false`, 구현됨). -- 라이브 override로 검증 가능한 건 **S1~S5(ramp-advisor 경로)뿐** — 전부 통과 확인(2026-06-01). -- 후속 옵션: 엔진 스냅샷 빌드도 `ISimOverrideStore` 경유하도록 확장하면 S6·S7도 override로 자율 검증 가능(현재 미적용). +### 한계 → 엔진 확장으로 일부 해소 (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만으론 미발화). ### 실행자 사용 예 (자율 루프) ``` diff --git a/src/Infrastructure/Control/FeedforwardSupervisor.cs b/src/Infrastructure/Control/FeedforwardSupervisor.cs index 6e1ee3c..ca00a81 100644 --- a/src/Infrastructure/Control/FeedforwardSupervisor.cs +++ b/src/Infrastructure/Control/FeedforwardSupervisor.cs @@ -17,6 +17,7 @@ public sealed class FeedforwardSupervisor : BackgroundService private readonly IFeedforwardWriteGuard _writeGuard; private readonly ILogger _logger; private readonly Microsoft.Extensions.Configuration.IConfiguration _appConfig; + private readonly ISimOverrideStore _sim; // WP0 확장: 엔진 스냅샷 입력 치환(DEMO) private readonly Dictionary _states = new(); // Phase II: 마지막 쓰기 시각(스트림별 rate-limit) 및 결과 private readonly ConcurrentDictionary<(int colId, string streamKey), DateTime> _lastWriteTimes = new(); @@ -26,8 +27,9 @@ public sealed class FeedforwardSupervisor : BackgroundService IServiceScopeFactory scopeFactory, FeedforwardEngine engine, IFeedforwardAdvisoryStore store, IFeedforwardWriteGuard writeGuard, ILogger logger, - Microsoft.Extensions.Configuration.IConfiguration appConfig) - { _scopeFactory = scopeFactory; _engine = engine; _store = store; _writeGuard = writeGuard; _logger = logger; _appConfig = appConfig; } + Microsoft.Extensions.Configuration.IConfiguration appConfig, + ISimOverrideStore sim) + { _scopeFactory = scopeFactory; _engine = engine; _store = store; _writeGuard = writeGuard; _logger = logger; _appConfig = appConfig; _sim = sim; } // Phase II: 쓰기 결과 조회 (Controller에서 사용) public (double? sp, string? error, DateTime? at) GetLastWrite(int colId, string streamKey) @@ -59,11 +61,14 @@ public sealed class FeedforwardSupervisor : BackgroundService var st = GetState(cfg.Id); var res = _engine.Tick(cfg, snap, st, DateTime.UtcNow); // Phase II: auto-write - if (!cfg.AdvisoryOnly && writeClient is not null && auditService is not null) + // 안전가드: Sim Override 활성 시 입력이 가짜이므로 실제 쓰기 금지(advisory-only로 강등) + if (!cfg.AdvisoryOnly && writeClient is not null && auditService is not null && !_sim.Enabled) { await AutoWriteAsync(cfg, res, st, writeClient, auditService, ct); res = res with { AutoWriteActive = true }; } + else if (!cfg.AdvisoryOnly && _sim.Enabled) + _logger.LogWarning("[FF] Sim Override 활성 — col {Id} auto-write 억제(가짜 입력)", cfg.Id); _store.Set(res); } catch (Exception ex) @@ -184,6 +189,8 @@ public sealed class FeedforwardSupervisor : BackgroundService TagSample Sample(string baseTag) { var tag = PvTag(baseTag); + if (_sim.Enabled && _sim.TryGet(tag, out var sov)) // WP0 확장: override 우선(신선 처리) + return new TagSample(tag, sov, Good: true, DateTime.UtcNow); if (rows.TryGetValue(tag.ToLowerInvariant(), out var r) && double.TryParse(r.LiveValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var v)) { @@ -197,6 +204,8 @@ public sealed class FeedforwardSupervisor : BackgroundService TagSample SampleExact(string rawTag) { var tag = rawTag.ToLowerInvariant(); + if (_sim.Enabled && _sim.TryGet(tag, out var sov)) // WP0 확장: override 우선 + return new TagSample(tag, sov, Good: true, DateTime.UtcNow); if (rows.TryGetValue(tag, out var r) && double.TryParse(r.LiveValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var v)) {