/* * 대상 프로파일 — **스위트가 갈리는 근거.** * * ── 왜 프로파일이 먼저인가 ────────────────────────────────────────────────── * 처음 만든 시퀀스는 직선이었다. 모든 대상에게 같은 순서를 강요했고, 그래서 두 방향으로 거짓말을 * 했다: push 소스에 커서 프로브를 들이대 빨간 칸을 만들고(결함이 아닌데), 반대로 스냅샷 소스에는 * 반출을 못 본 이유를 「아직 안 돌렸다」로 적었다(구조적으로 못 내는 것인데). * * 갈림의 근거는 **대상의 성격**이고, 그 성격은 **우리가 정하는 것이 아니라 발견하는 것**이다. * 그래서 프로파일의 모든 축은 `'unknown'` 에서 시작해 프로브가 좁힌다. 모르는 축으로는 라우팅하지 * 않는다 — 모르면 그 축에 걸린 프로브는 「아직 판단할 수 없음」이고, 짐작으로 건너뛰지 않는다. * * ── 축을 고른 기준 ───────────────────────────────────────────────────────── * 실제로 **다른 프로브를 요구하는 것만** 축이 된다. 「벤더 이름」은 축이 아니다(같은 벤더가 REST 도 * 파일도 준다). 아래 일곱은 각각 적어도 한 프로브의 적용 여부를 바꾼다. */ /** 무엇으로 붙나 — 페이지·상한·질의 형태가 여기서 갈린다. */ export type Transport = 'rest' | 'graphql' | 'db' | 'file' | 'mqtt' | 'opc' | 'unknown' /** * 변화를 어떻게 아나 — **라이브 단계의 성격이 통째로 이 축에 달려 있다.** * * · `push` 원천이 밀어 준다(구독·웹훅·큐·CDC). 커서가 필요 없다. * · `delta-poll` 거래 이력을 커서로 읽는다. **커서 정합이 급소다.** * · `snapshot-poll` 현재 상태를 되풀어 읽는다. **반출을 원리적으로 낼 수 없다** — 라이브 단계는 * 「아직」이 아니라 **구조적으로 불가**이고, 그 사실을 그대로 말한다. * · `none` 변화를 알 길이 없다(구조 전용 소스). */ export type FeedKind = 'push' | 'delta-poll' | 'snapshot-poll' | 'none' | 'unknown' /** 물건을 무엇이라 부르나 — 발명해야 하는지가 여기서 갈린다. */ export type IdentityKind = 'gs1' | 'local' | 'none' | 'unknown' /** * 양을 어떻게 세나 — **개수냐 연속량이냐.** * * 식품·화학·제약은 KG·L 로 오고, 조립 제조는 대수로 온다. 커널·미러는 둘 다 다루지만(비직렬 * `MaterialSubLot`), **요구하는 프로브가 다르다**: 연속량이면 단위 어휘와 환산을 반드시 묻고, * 직렬이면 그 프로브는 해당 없음이다. */ export type Quantization = 'serial' | 'continuous' | 'mixed' | 'unknown' /** 이 원천이 겸하는 버티컬 — 둘 이상이면 트윈이 여럿이고 **핸드오버 경계**를 물어야 한다. */ export type Vertical = 'wms' | 'mes' | 'yms' | 'ems' /** 밖으로 나가는 문 — 폐루프 프로브의 적용 여부. */ export type WriteSurface = 'none' | 'command' | 'unknown' export interface TargetProfile { transport: Transport feed: FeedKind multiSite: boolean | null identity: IdentityKind quantization: Quantization /** 빈 배열 = 아직 모른다(없다는 뜻이 아니다). */ verticals: readonly Vertical[] write: WriteSurface } /** 아무것도 모르는 상태 — 스위트는 여기서 시작한다. */ export function unknownProfile(): TargetProfile { return { transport: 'unknown', feed: 'unknown', multiSite: null, identity: 'unknown', quantization: 'unknown', verticals: [], write: 'unknown' } } /** 이 축을 아직 모르나 — 모르는 축으로는 라우팅하지 않는다. */ export function isUnknown(p: TargetProfile, axis: keyof TargetProfile): boolean { const v = p[axis] if (axis === 'multiSite') return v === null if (axis === 'verticals') return (v as readonly Vertical[]).length === 0 return v === 'unknown' } /** * 프로브가 알아낸 것을 프로파일에 반영한다 — **좁히기만 한다.** * * 이미 알아낸 축을 나중 프로브가 `'unknown'` 으로 덮지 못하게 한다. 스위트는 여러 번 돌고, 뒤에 * 돌린 프로브가 그 축을 다루지 않는다는 이유로 앞의 발견을 지우면 라우팅이 왕복한다. */ export function narrowProfile(base: TargetProfile, found: Partial): TargetProfile { const out: TargetProfile = { ...base } for (const k of Object.keys(found) as (keyof TargetProfile)[]) { const v = found[k] if (v === undefined) continue if (k === 'multiSite') { if (v !== null) out.multiSite = v as boolean; continue } if (k === 'verticals') { const list = v as readonly Vertical[]; if (list.length) out.verticals = [...new Set([...out.verticals, ...list])]; continue } if (v !== 'unknown') (out as any)[k] = v } return out } /** * 이 프로파일에서 **라이브가 구조적으로 가능한가.** * * `snapshot-poll` 은 불가다 — 사라진 것은 그냥 없어지고 `OBSERVE` 만 남는다. 이걸 「아직 반출을 못 * 봤다」로 적으면 사람이 더 기다리게 되는데, 더 기다려도 오지 않는다. 방식을 바꿔야 한다는 것이 답이다. */ export function liveFeasible(p: TargetProfile): { ok: boolean; reason?: string } { if (p.feed === 'unknown') return { ok: false, reason: 'feed-unknown' } if (p.feed === 'none') return { ok: false, reason: 'no-feed' } if (p.feed === 'snapshot-poll') return { ok: false, reason: 'snapshot-only-feed' } return { ok: true } }