/* * 규칙 판정이 **실제 엔진·저장소에 닿는 자리** — 한 벌. * * 리졸버(「지금 돌려 본다」)와 주기 루프가 같은 배선을 써야 한다. 두 벌이면 화면에서 돌려 봤을 때와 * 주기가 돌 때가 다르게 동작하고, 사람은 「돌려 봤을 때는 됐는데」를 겪는다. */ import { TwinEngine } from '../../engine/twin-engine.js' import { getRepository } from '@things-factory/shell' import { TwinActuationRule } from './actuation-rule.js' import type { ActuationRuleSpec, AttentionLike } from './rule-evaluate.js' import type { OwnedTwin, TwinRunReport } from './rule-runner.js' /** 이 트윈에 걸린 규칙 — 꺼 둔 것도 함께 준다(판정이 거른다. 「꺼져 있다」는 판정의 규율이다). */ export async function rulesOf(twin: OwnedTwin): Promise { const rows = await getRepository(TwinActuationRule).find({ where: { domain: { id: twin.domainId }, instanceId: twin.instanceId } }) return rows.map(r => ({ id: r.id, instanceId: r.instanceId, attentionKind: r.attentionKind, ...(r.minSeverity ? { minSeverity: r.minSeverity as any } : {}), ...(r.minHeldMs ? { minHeldMs: r.minHeldMs } : {}), ...(r.cooldownMs === null || r.cooldownMs === undefined ? {} : { cooldownMs: r.cooldownMs }), commandType: r.commandType, ...(r.payloadTemplate ? { payloadTemplate: r.payloadTemplate } : {}), enabled: r.enabled !== false })) } /** * 이 트윈이 관측을 비추나 가정을 돌리나 — 조치를 낼 수 있는지가 여기서 갈린다. * * 판정과 같은 술어를 쓴다(`TwinEngine.modeOf`). 두 벌이면 어긋난다. */ export function modeOf(twin: OwnedTwin): 'live' | 'sim' | undefined { return TwinEngine.modeOf(twin.domainId, twin.instanceId) } /** * 지금 서 있는 주의들 — **못 읽으면 `undefined`.** * * 빈 배열로 답하지 않는다. 멈춘 트윈을 「조건이 하나도 없다」로 읽으면 규칙이 도는 줄로 보인다. * 주의 질의와 같은 원천을 쓴다(§`twin-attention-query.ts`) — 두 곳이 다른 것을 보면 화면에 보이는 * 조건과 규칙이 보는 조건이 어긋난다. */ export function attentionsOf(twin: OwnedTwin): AttentionLike[] | undefined { if (!TwinEngine.owns(twin.domainId, twin.instanceId)) return undefined const kernel = TwinEngine.kernel(twin.domainId, twin.instanceId) const snap = kernel?.getSnapshot?.() ?? TwinEngine.snapshot(twin.domainId, twin.instanceId) if (!snap) return undefined return (snap.attentions ?? []) as AttentionLike[] } /** * 돈 결과를 규칙 행에 남긴다 — **조용히 안 도는 규칙과 조건이 없어서 안 내는 규칙을 구별하려면 필요하다.** * * 이 트윈의 모든 규칙에 `lastRunAt` 을 찍는다. 이유는 그 규칙 것만 적는다. */ export async function noteRun(twin: OwnedTwin, report: TwinRunReport): Promise { const repo = getRepository(TwinActuationRule) const rows = await repo.find({ where: { domain: { id: twin.domainId }, instanceId: twin.instanceId } }) if (!rows.length) return const at = new Date().toISOString() const reasonOf = (id: string) => report.skips.filter(s => s.ruleId === id).map(s => s.reason)[0] ?? null await repo.save( rows.map(row => ({ ...row, lastRunAt: at, proposedCount: (row.proposedCount ?? 0) + (report.proposedByRule[row.id] ?? 0), /* * 이유는 그 규칙 것만 적는다. 트윈 전체가 못 돈 경우(§`failed`)는 모든 규칙에 같은 이유가 붙는다 * — 그때는 규칙 하나의 문제가 아니기 때문이다. */ lastSkipReason: report.failed ?? reasonOf(row.id) })) as any ) }