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>
This commit is contained in:
windpacer
2026-06-01 16:30:32 +09:00
parent 9065b19a0a
commit 60946f3c47
2 changed files with 17 additions and 9 deletions

View File

@@ -111,12 +111,11 @@ pica-6111 탑정 / REFLUX DIST(R) / TI-6111D / 상부PACKING(정류,D) /
- `SimOverrideEnabled=false`(기본)에서 sim 엔드포인트 403, advisor는 live만 사용. - `SimOverrideEnabled=false`(기본)에서 sim 엔드포인트 403, advisor는 live만 사용.
- 코드 grep: sim 경로에서 `WriteTagAsync`/SQL write **0건**. - 코드 grep: sim 경로에서 `WriteTagAsync`/SQL write **0건**.
### 한계 (구현 후 확인, 2026-06-01) ### 한계 → 엔진 확장으로 일부 해소 (2026-06-01)
**Sim Override `FeedRampAdvisorService`(ramp-advisor)만 통합. 엔진(`FeedforwardSupervisor.BuildSnapshotAsync`)은 DB를 직접 읽어 override 미반영.** 초기: Sim Override `FeedRampAdvisorService`(ramp-advisor)만 통합, 엔진 미반영.
- 결과: **S7(mbState 과추출)·S6(stale) 라이브 검증은 override로 불가** `/api/ff/advisory`(엔진 산출) override 무시. **확장 적용**: `FeedforwardSupervisor.BuildSnapshotAsync``Sample`/`SampleExact`가 override 우선 → `/api/ff/advisory`(엔진 산출) override 반영. **안전가드**: `_sim.Enabled` 시 auto-write 억제(가짜 입력이 실제 OPC 쓰기 유발 방지).
- S7 라이브 = 운전원이 실제 DCS/realtime_table에 합성값 주입 / 또는 Engine 통합테스트. S6 라이브 = stale 태그(운전원) / 또는 단위테스트(`pv.Feed.Good=false`, 구현됨). - **해소**: S7(mbState 과추출)·§10/front 물리검증은 이제 **override로 자율 가능**(B·temps 주입 → 다음 supervisor tick~2s 반영).
- 라이브 override로 검증 가능한 건 **S1~S5(ramp-advisor 경로)뿐** — 전부 통과 확인(2026-06-01). - **여전히 불가**: S6(stale) — override는 항상 Good=fresh라 stale 시뮬 불가(단위테스트 `pv.Feed.Good=false`로 커버). P4(transient) — `FeedMoveThresholdPerMin=0`이라 config 임계 설정 필요(override만으론 미발화).
- 후속 옵션: 엔진 스냅샷 빌드도 `ISimOverrideStore` 경유하도록 확장하면 S6·S7도 override로 자율 검증 가능(현재 미적용).
### 실행자 사용 예 (자율 루프) ### 실행자 사용 예 (자율 루프)
``` ```

View File

@@ -17,6 +17,7 @@ public sealed class FeedforwardSupervisor : BackgroundService
private readonly IFeedforwardWriteGuard _writeGuard; private readonly IFeedforwardWriteGuard _writeGuard;
private readonly ILogger<FeedforwardSupervisor> _logger; private readonly ILogger<FeedforwardSupervisor> _logger;
private readonly Microsoft.Extensions.Configuration.IConfiguration _appConfig; private readonly Microsoft.Extensions.Configuration.IConfiguration _appConfig;
private readonly ISimOverrideStore _sim; // WP0 확장: 엔진 스냅샷 입력 치환(DEMO)
private readonly Dictionary<int, ColumnState> _states = new(); private readonly Dictionary<int, ColumnState> _states = new();
// Phase II: 마지막 쓰기 시각(스트림별 rate-limit) 및 결과 // Phase II: 마지막 쓰기 시각(스트림별 rate-limit) 및 결과
private readonly ConcurrentDictionary<(int colId, string streamKey), DateTime> _lastWriteTimes = new(); private readonly ConcurrentDictionary<(int colId, string streamKey), DateTime> _lastWriteTimes = new();
@@ -26,8 +27,9 @@ public sealed class FeedforwardSupervisor : BackgroundService
IServiceScopeFactory scopeFactory, FeedforwardEngine engine, IServiceScopeFactory scopeFactory, FeedforwardEngine engine,
IFeedforwardAdvisoryStore store, IFeedforwardWriteGuard writeGuard, IFeedforwardAdvisoryStore store, IFeedforwardWriteGuard writeGuard,
ILogger<FeedforwardSupervisor> logger, ILogger<FeedforwardSupervisor> logger,
Microsoft.Extensions.Configuration.IConfiguration appConfig) Microsoft.Extensions.Configuration.IConfiguration appConfig,
{ _scopeFactory = scopeFactory; _engine = engine; _store = store; _writeGuard = writeGuard; _logger = logger; _appConfig = appConfig; } ISimOverrideStore sim)
{ _scopeFactory = scopeFactory; _engine = engine; _store = store; _writeGuard = writeGuard; _logger = logger; _appConfig = appConfig; _sim = sim; }
// Phase II: 쓰기 결과 조회 (Controller에서 사용) // Phase II: 쓰기 결과 조회 (Controller에서 사용)
public (double? sp, string? error, DateTime? at) GetLastWrite(int colId, string streamKey) 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 st = GetState(cfg.Id);
var res = _engine.Tick(cfg, snap, st, DateTime.UtcNow); var res = _engine.Tick(cfg, snap, st, DateTime.UtcNow);
// Phase II: auto-write // 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); await AutoWriteAsync(cfg, res, st, writeClient, auditService, ct);
res = res with { AutoWriteActive = true }; 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); _store.Set(res);
} }
catch (Exception ex) catch (Exception ex)
@@ -184,6 +189,8 @@ public sealed class FeedforwardSupervisor : BackgroundService
TagSample Sample(string baseTag) TagSample Sample(string baseTag)
{ {
var tag = PvTag(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) if (rows.TryGetValue(tag.ToLowerInvariant(), out var r)
&& double.TryParse(r.LiveValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var v)) && double.TryParse(r.LiveValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var v))
{ {
@@ -197,6 +204,8 @@ public sealed class FeedforwardSupervisor : BackgroundService
TagSample SampleExact(string rawTag) TagSample SampleExact(string rawTag)
{ {
var tag = rawTag.ToLowerInvariant(); 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) if (rows.TryGetValue(tag, out var r)
&& double.TryParse(r.LiveValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var v)) && double.TryParse(r.LiveValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var v))
{ {