@things-factory/headless-twin 10.1.223 → 10.1.227

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist-server/engine/kpi-query.js +1 -1
  2. package/dist-server/engine/kpi-query.js.map +1 -1
  3. package/dist-server/engine/twin-engine.d.ts +1 -1
  4. package/dist-server/engine/twin-engine.js +1 -1
  5. package/dist-server/engine/twin-engine.js.map +1 -1
  6. package/dist-server/service/actuation/command-dispatcher.d.ts +1 -1
  7. package/dist-server/service/actuation/command-dispatcher.js.map +1 -1
  8. package/dist-server/service/reference/actuation-routing.d.ts +1 -1
  9. package/dist-server/service/reference/actuation-routing.js.map +1 -1
  10. package/dist-server/service/reference/connection-portability.d.ts +1 -1
  11. package/dist-server/service/reference/connection-portability.js +2 -2
  12. package/dist-server/service/reference/connection-portability.js.map +1 -1
  13. package/dist-server/service/reference/hook-contract.js +1 -1
  14. package/dist-server/service/reference/hook-contract.js.map +1 -1
  15. package/dist-server/service/reference/reference-adapter.d.ts +1 -1
  16. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  17. package/dist-server/service/reference/reference-live.js +1 -1
  18. package/dist-server/service/reference/reference-live.js.map +1 -1
  19. package/dist-server/service/reference/reference-progress.js +1 -1
  20. package/dist-server/service/reference/reference-progress.js.map +1 -1
  21. package/dist-server/service/twin-model/choose-recipe.d.ts +1 -1
  22. package/dist-server/service/twin-model/choose-recipe.js +1 -1
  23. package/dist-server/service/twin-model/choose-recipe.js.map +1 -1
  24. package/dist-server/service/twin-model/twin-lineage-query.js +1 -1
  25. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  26. package/package.json +6 -6
  27. package/server/engine/kpi-query.ts +1 -1
  28. package/server/engine/twin-engine.ts +1 -1
  29. package/server/service/actuation/command-dispatcher.ts +1 -1
  30. package/server/service/reference/actuation-routing.ts +1 -1
  31. package/server/service/reference/connection-portability.ts +2 -2
  32. package/server/service/reference/hook-contract.ts +1 -1
  33. package/server/service/reference/reference-adapter.ts +1 -1
  34. package/server/service/reference/reference-live.ts +1 -1
  35. package/server/service/reference/reference-progress.ts +1 -1
  36. package/server/service/twin-model/choose-recipe.ts +1 -1
  37. package/server/service/twin-model/twin-lineage-query.ts +1 -1
  38. package/server/service/twin-subject/twin-subject-backfill.ts +1 -1
  39. package/test/ack-shape-alignment.test.ts +1 -1
  40. package/test/canonical-ingest-vocabularies.test.ts +1 -1
  41. package/test/checkpoint-refuses-empty.test.ts +1 -1
  42. package/test/choose-recipe.test.ts +1 -1
  43. package/test/event-type-split.test.ts +1 -1
  44. package/test/live-cursor-wiring.test.ts +1 -1
  45. package/test/master-to-twin.test.ts +1 -1
  46. package/test/read-failure-visible.test.ts +1 -1
  47. package/tsconfig.shared.tsbuildinfo +1 -1
  48. package/tsconfig.tsbuildinfo +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"reference-adapter.js","sourceRoot":"","sources":["../../../server/service/reference/reference-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ApprovedCommand, RefusalCode, RefusalParams } from '@operato/ops-contract'\n\nimport type { ReferenceStep } from './reference-progress.js'\nimport type { ReferenceMaster } from './reference-master.js'\n/*\n * `fetchLinks` 가 돌려주는 관계 그룹. 정의는 이것을 소비하는 쪽에 하나만 둔다\n * (`external-resolver.ts`). 여기서 같은 모양을 다시 선언하면 한쪽만 수정되는 날이 온다.\n */\nimport type { ExternalIncoming as ReferenceLinkGroup } from '../twin-model/external-resolver.js'\nexport type { ReferenceLinkGroup }\nimport { twinWarn } from '../../engine/log.js'\n\n/*\n * 레퍼런스 어댑터 계약 (reference-management P1) — 정적 마스터 등록(`registerReference`)을\n * \"어댑터 타입\" 등록으로 일반화한다. 레퍼런스 = 데이터(TwinReference 행), 어댑터 = 동작.\n * 아웃바운드 액추에이션은 **별개 어댑터가 아니라 이 어댑터의 반대 면**이다(`actuate`) — 같은 연결이\n * 사실을 읽고 조치를 내리므로 인증과 설정이 한 자리에 있어야 한다(command-routing §8.1).\n * seam: openLiveFeed 레코드 → 커널 face2-adapter.ingest → CanonicalEnvelope 까지가 여기,\n * 이후 projector 구동은 face2-inbound-live 소유.\n * 설계 SoT: operato-twin/design/plans/reference-management.md §2.2·§4.2.\n */\n\n/** 연결 설정(어댑터별 스키마) — 엔드포인트·자격·파라미터. */\nexport type ConnectionConfig = Record<string, any>\n\n/** discoverSites 가 반환하는 사이트/공장 서술자 — N 발견(§2.2). 1:1 은 길이 1 배열. */\nexport interface SiteDescriptor {\n /** 안정적 사이트 정체성 키(소스의 plant code 등) — 공간 정합 1차 키. */\n siteId: string\n name?: string\n /** 사이트별 시스템(소스가 시스템을 넘나들 때). 없으면 레퍼런스 system 사용. */\n system?: string\n geo?: { lat: number; lon: number }\n hint?: any\n}\n\n/** 연결 폼 필드 서술 — UI 가 이 스키마로 connectionConfig 입력 폼을 렌더한다(어댑터가 스키마 제공). */\nexport interface AdapterConfigField {\n key: string\n label: string\n placeholder?: string\n required?: boolean\n secret?: boolean // 비밀번호/토큰 — 마스킹 입력\n multiline?: boolean // JSON/매핑 등 여러 줄 입력(textarea)\n help?: string // 필드 아래 도움말\n default?: string // 폼 프리필 기본값(예시 매핑/URL 등)\n /*\n * 값이 정해진 목록 안에 있을 때 — 화면이 고르게 그린다(타이핑하지 않는다).\n *\n * 손으로 적는 칸에 닫힌 값을 받으면 오타가 그대로 저장되고, 그 오타는 오류로 나오지 않는다.\n * 발전소 종류(`sell` | `self-consumption`)가 그렇다 — 틀리면 계량 방향이 반대가 되는데 어디에도\n * 표시되지 않는다.\n *\n * `label` 은 번역 키를 쓸 수 있다(화면이 `meta()` 로 푼다). `value` 는 저장되는 값이라 번역하지 않는다.\n */\n options?: { value: string; label: string }[]\n /*\n * 값의 종류가 정해져 있지만 목록이 너무 커서 `options` 로 실을 수 없을 때 — 화면이 그 종류에 맞는\n * 고르는 칸을 그린다.\n *\n * 'timezone' IANA 시간대. 화면이 `Intl.supportedValuesOf('timeZone')` 로 목록을 만든다.\n * 400개가 넘고 표준이 갱신되므로 우리가 목록을 들고 있지 않는다.\n * 이름이 곧 값이자 표시라 번역하지 않는다.\n *\n * 자유 입력으로 두면 `Asia/seoul` 같은 오타가 저장되고, 그 오타는 오류로 나오지 않는다.\n */\n kind?: 'timezone'\n}\n/** 커넥터 메타데이터 — picker·연결 폼용 표시 정보. */\nexport interface AdapterMeta {\n label?: string\n domain?: string // 'wms' | 'yms' | 'mes' (분류)\n available?: boolean // false = 준비 중(picker 비활성)\n configSchema?: AdapterConfigField[]\n ootbNote?: string // OOTB 매핑 안내 문구\n /*\n * 근거 수준(정직성 1급 표기 — \"실제 API 명세에 근거하는가\"를 UI·피치가 오해 없이 보이게).\n * 'vendor-doc' = 벤더 공개 API 문서의 엔드포인트·필드명에 정합(docUrl 로 출처 명시). 실 시스템 검증은 별도.\n * 'standard' = 개방 표준(EPCIS 2.0 / ISA-95)에 정합 — 벤더 독립.\n * 'facsimile' = 시스템 어휘를 흉내낸 근사치(공개 문서 미정합). 실 연동 시 매핑 재정합 필요.\n * 미지정은 facsimile 로 간주(가장 보수적).\n */\n grounding?: 'vendor-doc' | 'standard' | 'facsimile'\n docUrl?: string // grounding='vendor-doc' 의 근거 문서 URL(출처 추적)\n}\n\n/**\n * 원본이 **선언하는 능력** — 「이 원본으로 무엇을 할 수 있나」 (ADR-0029 §8, 2026-08-19).\n *\n * ── 왜 능력으로 선언하나 ────────────────────────────────────────────────────\n * 실 WMS 는 스스로 사실을 낸다. 시뮬레이터는 **구동해 줘야** 움직인다 — 자극·속도·정지·초기화. 그런데\n * 계약에 그 자리가 없어서 통제가 `TwinEngine` 안에 뚫려 있었다(`controlTwinScenario` 가 런타임의 시나리오\n * 엔진을 직접 잡았다). 그러면 ADR-0029 의 판정 문장(*\"실물이 같은 계약으로 할 수 있는가\"*)에 걸린다:\n * 실 WMS 는 일시정지될 수 없으니 그 통제면은 영원히 「어댑터 규약의 결손」으로 남고, 규칙이 그 면을\n * **숨게** 만든다.\n *\n * 그래서 뒤집는다: 통제는 **선언된 능력**이다. 선언하지 않은 원본에는 그 문이 **아예 열리지 않는다** —\n * 그것이 결손이 아니라 **사실**이다(실 WMS 는 정지되지 않는다). 이 레포가 이미 그 규율로 굴러간다\n * (`capability-contract`: concrete 강결합 대신 능력 선언).\n *\n * · `live` — 돌아가는 피드를 낸다(`openLiveFeed`).\n * · `control` — 구동을 받는다(`control`) — 자극·속도·정지·초기화.\n */\n/**\n * 이 원본에 무엇을 요구할 수 있나.\n *\n * · `live` — 관측을 이어서 낸다\n * · `control` — 시뮬레이터를 움직인다(자극·배속·정지). 실 시스템은 갖지 않는다\n * · `actuate` — **승인된 조치를 받는다.** 현장에 작업지시가 나가는 유일한 축이다\n *\n * ── `actuate` 를 늦게 넣은 이유 (2026-09-02) ────────────────────────────────\n * 어댑터에 `actuate` 구현은 있었는데 이 축에 그 이름이 없었다. 그래서 셋이 동시에 조용했다.\n *\n * 화면 「이 원본이 지시를 받나」를 물을 수 없다 — 능력 목록에 그 이름이 없다\n * 등록 구현했는데 선언 안 한 것을 알려 주지 않는다 — 검사 목록에 그 이름이 없다\n * 넘김 리졸버가 어댑터를 찾지도 않고 무조건 거절했다 — 커넥터는 이미 만들어져 있었다\n *\n * 실제로 MES 커넥터가 `actuate` 를 다 만들어 두었는데, 넘기면 「구현이 없습니다」라고 답했다.\n * 자리는 있고 길이 없는 상태였고, 이 저장소에서 가장 자주 나는 결함 부류다.\n */\n/**\n * 커넥터가 선언하는 능력.\n *\n * `idempotent-actuation` 은 다른 셋과 성질이 다르다 — 무엇을 할 수 있나가 아니라 **두 번 해도\n * 같은가**다. 일꾼이 실패한 조치를 다시 넘길지 정할 때 이것만 본다(§`retryDecisionOf`).\n *\n * 선언하지 않으면 **거짓으로 읽는다.** 이 한 자리에서만 「모르면 안전한 쪽」이 거짓이고, 그\n * 안전한 쪽이 「재시도하지 않음」이다 — 참으로 가정하면 현장에 지시가 두 건 선다.\n *\n * 선언에는 근거가 있어야 한다. operato-plant 는 MES 가 `correlationId` 로 기존 행을 찾아 같은\n * 이름을 돌려주기 때문에 참이다 — 우리가 조심해서가 아니라 저쪽 동작이 그래서다. 저쪽이 그것을\n * 바꾸면 이 선언이 거짓이 된다.\n */\nexport type ReferenceCapability = 'live' | 'control' | 'actuate' | 'idempotent-actuation'\n\n/** 원본에 보내는 구동 명령 — **시뮬레이터를 움직이는 말**이지 트윈을 고치는 말이 아니다. */\nexport interface ControlCommand {\n /**\n * · `stimulus` — 자극을 싣는다(`scenario`). 선언은 원본 설정에 살고 이 명령은 지금 태우는 것이다.\n * · `start`·`pause` — 구동을 켜고 멈춘다.\n * · `speed` — 시각의 배속(`speed`).\n * · `reset` — 씨앗부터 다시(원본의 상태를 되돌린다 — 트윈의 저널은 재기동 정책이 정한다).\n */\n action: 'stimulus' | 'start' | 'pause' | 'speed' | 'reset'\n /** 자극 선언 — `action: 'stimulus'` 에만. 커널 규약(`validateScenario`)으로 검사된 값이어야 한다. */\n scenario?: any\n /** 배속 — `action: 'speed'` 에만. */\n speed?: number\n /**\n * 어느 트윈을 위한 구동인가 — 원본이 여러 트윈에 먹일 수 있으므로 대상을 함께 말한다.\n *\n * 인프로세스 시뮬레이터는 이 둘로 자기 런타임을 찾는다(테넌트까지 있어야 같은 id 의 두 트윈이 갈린다).\n * 원격 원본은 대개 무시한다(자기 세계가 하나다).\n */\n instanceId?: string\n domainId?: string\n}\n\n/** 구동 결과 — **왜 안 됐는지**를 코드로 말한다(화면이 옮긴다). */\nexport interface ControlResult {\n ok: boolean\n /** 언어중립 사유 — `not-declared`(능력 없음) · `unsupported-action` · `invalid`(선언 거절) · `no-target`. */\n code?: string\n detail?: string\n}\n\n/**\n * 한 흐름의 읽기 커서 — **어디까지 읽었나.**\n *\n * `since` 와 `seen` 이 **함께** 있어야 한다. 이어 읽기는 경계 시각을 겹쳐 읽고(`gte`), 그 겹침을\n * `seen` 이 걸러 낸다. `gt` 로 좁히면 같은 밀리초의 다른 행이 사라진다 — 들어온 것이 없는 누락이다.\n *\n * `seen` 이 예상보다 커지면(한 시각에 몰린 행 수가 늘면) 그것은 원본 쪽 신호다. 어댑터가 그 크기를\n * 말해야 한다 — 알리지 않고 커지게 두면 이어 읽기가 막히는 순간까지 아무도 모른다.\n */\nexport interface LiveFeedCursor {\n /** 그 흐름에서 마지막으로 본 시각(원본의 시각, ISO). */\n since?: string\n /** 그 시각에 이미 소비한 행 id — 겹쳐 읽은 것을 걸러 낸다. 저장되는 모양이므로 배열이다. */\n seen: string[]\n}\n\n/**\n * **트윈이 지금 들고 있는 물품 하나** — 붙는 커넥터가 견줄 근거(§`LiveFeedContinuity.observedItems`).\n *\n * 커널 어휘로 낸다. 원본의 낱말(그 시스템의 상태 코드)로 내지 않는 이유가 있다: 원본 낱말은 여러 개가\n * 커널의 한 값으로 모이므로(예: 네 상태가 「진행 중」 하나가 된다) 되돌릴 수 없다. 그리고 되돌릴\n * 필요도 없다 — 두 원본 상태가 커널에서 같은 값이면 **트윈이 든 사실은 바뀌지 않았다.**\n */\nexport interface ObservedItemFact {\n /** 트윈이 든 물품의 키 — 비직렬 재고는 `클래스@자리`. */\n id: string\n /** 클래스 식별자(원문). */\n epcClass?: string\n location?: string\n qty?: number\n uom?: string\n /** 표준 처분(CBV) — 원본 상태 코드가 아니다. */\n disposition?: string\n /** 유통기한(밀리초). */\n expiry?: number\n lot?: string\n}\n\n/**\n * 밀어 받은 본문 하나를 옮긴 결과 (2026-08-31).\n *\n * 레코드만 돌려주던 것을 넓혔다. 번호를 함께 받아야 프레임워크가 빠진 것을 알아챈다.\n *\n * **`scope` 가 없으면 `seq` 도 없는 것으로 다룬다.** 번호만 있고 그 번호가 어느 줄의 것인지 모르면\n * 커서를 무엇으로 잡을지 짐작해야 하고, 짐작한 열쇠는 보내는 쪽이 줄을 하나 더 늘리는 날 어긋난다.\n * 실제로 operato-plant 가 `[domain, channel]` 마다 번호를 매기면서 봉투에 `channel` 을 싣지 않고 있었다.\n */\n/** 번호 하나에 대해 원본이 아는 것. */\nexport interface SeqEntry {\n seq: number\n /**\n * `unknown` 은 결함이 아니다 — 채번을 따로 들지 않는 원본은 「행이 없다」에서 잃은 것과 아직 안\n * 간 것을 가를 수 없다. 그것을 `lost` 로 단정하면 사람이 없는 사실을 찾으러 간다.\n */\n state: 'present' | 'lost' | 'unissued' | 'unknown'\n /** 그 봉투의 발생 시각 — `present` 일 때만. 무엇이었는지 사람이 알아보는 데 쓴다. */\n at?: string\n /** 그 봉투 자체 — `present` 이고 원본이 낼 수 있을 때만. 없어도 `state` 는 참이다. */\n record?: unknown\n}\n\n/** 번호 구간에 대해 원본이 아는 것 — **원본이 아는 것만 낸다. 판정은 사람이 한다.** */\nexport interface SeqReport {\n scope: string\n entries: SeqEntry[]\n /** 상한에 닿아 구간을 다 못 냈다 — 알리지 않고 자르지 않는다. */\n truncated?: boolean\n}\n\nexport interface InboundBatch {\n /**\n * Receiver path for this batch. Omitted is FACTS, preserving every existing\n * connector; ATTENTIONS is a product-level analysis input and must not be\n * folded as a canonical fact.\n */\n lane?: 'FACTS' | 'ATTENTIONS'\n /**\n * 옮긴 것들. 우리와 무관한 본문이면 빈 배열이다.\n *\n * **한 요청에 봉투가 여럿 온다.** 밀어 주는 쪽은 한 건씩 보내지 않는다 — 번호도 봉투마다 붙는다.\n * 배치를 대표하는 번호 하나로는 가운데가 빠진 것을 알아채지 못한다.\n */\n items: InboundItem[]\n /**\n * 이 요청의 번호가 어느 줄의 것인가. 번호를 매기지 않는 원본은 주지 않는다.\n *\n * 요청 하나가 한 줄에만 속한다 — 한 요청에 두 줄을 섞으면 어느 줄의 어디까지 받았는지를 응답\n * 하나로 말할 수 없다.\n */\n scope?: string\n /**\n * (선택) **원본이 지금 가진 번호의 범위** — 우리 커서와 견주려면 이것이 있어야 한다.\n *\n * ── 왜 어댑터가 주나 (인티그레이션 레인 제안, 2026-09-06) ──────────────────\n * 「가장 낮은 번호」를 묻는 방법이 원본마다 다르다. operato-plant 는\n * `mesOutboxEvents(since: 0, limit: 1)` 이고, ppms·chef 는 아웃박스가 없어 번호 범위 자체가\n * 없다. 프레임워크가 이것을 알 방법이 없다.\n *\n * ── 모르면 주지 않는다 ────────────────────────────────────────────────────\n * **0 이나 추측을 넣지 않는다.** 0 을 넣으면 「원본이 1번부터 다 갖고 있다」로 읽히고, 그것이\n * 거짓이면 「우리가 다 받았다」는 잘못된 결론이 나온다.\n *\n * `last` 에는 조건이 하나 붙는다. 쪽 상한에서 멈췄으면 그때의 번호는 **원본의 끝이 아니라\n * 「여기까지 읽었다」**이므로 주지 않는다 — 주면 「원본에 더 없다」로 읽히는데 실제로는 더 있다.\n */\n sourceRange?: { first?: number; last?: number }\n}\n\nexport interface InboundItem {\n /** 우리 어휘로 옮긴 레코드 하나. */\n record: unknown\n /**\n * 원본이 이 봉투에 붙인 id — **떨어졌을 때 그쪽이 자기 행을 찾는 열쇠다.**\n *\n * ── 왜 필요한가 (2026-09-07 측정) ──────────────────────────────────────────\n * 훅 응답의 떨어진 목록이 `{ record, errors }` 였고, plant 은 그것을 **봉투 id 문자열 목록**으로\n * 읽고 있었다. `Array.isArray` 는 통과하니 그쪽 guard 도 안 걸렸다 — id 를 맞추는데 객체라 하나도\n * 안 맞고 **묶음 전체가 「보냈음」으로 찍혔다.** 422 와 `ok: false` 를 보냈는데도 그쪽 행은 재시도\n * 0 · 오류 없음이었다.\n *\n * 이 값을 주면 응답이 `{ eventId, errors }` 로 나가고 그쪽이 그 행만 표시할 수 있다. 안 주면\n * 레코드가 그대로 실려 나간다 — 무엇이 떨어졌는지 알 길이 그것뿐이고, 빈 id 를 실으면 보내는 쪽이\n * 아무 행도 못 찾으면서 「알았다」고 여긴다.\n */\n eventId?: string\n /**\n * 이 봉투의 번호.\n *\n * **한 배치 안에서 있거나 없거나 하나로 통일한다.** 섞이면 빠진 것이 있는지 판단할 근거가 없다.\n */\n seq?: number\n /**\n * 이 봉투가 말하는 **발생 시각** — 레코드가 자기 시각을 안 말할 때 쓴다.\n *\n * ── 왜 필요한가 (2026-09-06 측정) ─────────────────────────────────────────\n * 시각을 안 싣는 레코드는 `defaultEventTime`(= ingest 시각)으로 떨어진다. 그 값이 **실행할 때마다\n * 다르다.** 그런데 fact identity 가 시각을 포함하므로, 같은 사실을 다시 받으면 dedupe 가 안 되고\n * 새 사실로 앉는다.\n *\n * 실제로 그렇게 됐다. `fillTwinScope(from: 0)` 을 두 번 돌렸더니 mes-line-a 저널이\n * 40 → 69건이 되고 중복이 27가지 생겼다. 시각을 말하는 레코드 12건만 되풀이로 걸러졌다.\n *\n * ── 봉투는 그 시각을 알고 있었다 ──────────────────────────────────────────\n * plant 아웃박스가 `eventTime` 을 실어 보낸다. 그것이 갈 자리가 없어서 버려지고 있었다 —\n * connector 주석이 그 위험을 적어 두고 자리를 요청해 두었다(`operato-plant.ts`).\n *\n * **레코드가 자기 시각을 말하면 그것이 이긴다.** 봉투의 시각은 「그 사실을 언제 보냈나」에 가깝고,\n * 레코드의 시각은 「언제 일어났나」다. 둘이 다르면 뒤엣것이 맞다.\n */\n at?: string\n}\n\n/**\n * 재기동을 넘어 이어지는 읽기 상태 — 참조 계층이 소유하고 어댑터에 건넨다(§`openLiveFeed`).\n *\n * 흐름 열쇠는 **어댑터가 정한다**(`Record`). 계약이 이름을 닫으면 새 원본마다 계약을 고치게 된다.\n */\nexport interface LiveFeedContinuity {\n /**\n * The name to take a task lease under, so only one instance polls this feed.\n *\n * ── Why this layer composes it ────────────────────────────────────────────\n * `TaskLease` has no domain column on purpose: what is made exclusive is a\n * loop, not a tenant's slice of one, and a task that does need to be\n * per-tenant puts the tenant in the name so the unique index keeps meaning\n * what it says.\n *\n * An adapter cannot follow that rule. It is handed `cfg` and `site`, and\n * neither carries a domain — so a name built from `siteId` alone would let\n * two domains that happen to use the same plant code block each other's\n * channel. Silently, and looking exactly like \"someone else holds it\".\n *\n * So the name is composed here, where `domainId` and `instanceId` are known,\n * and the rule lives in one place.\n *\n * ── Always present, unlike `cursor` ───────────────────────────────────────\n * A feed needs its lease on the very first attach, before there is any\n * cursor to carry. So this object is now handed over even when nothing has\n * been read yet; `cursor` being absent is what still says \"first attach\",\n * which is how every connector already reads it (`continuity?.cursor`).\n */\n leaseName: string\n /** 지난번에 어디까지 읽었나 — 없으면 첫 붙음이다(그때만 되돌아볼 날수로 창을 만든다). */\n cursor?: { streams?: Record<string, LiveFeedCursor>; firstAttachedAt?: string }\n /**\n * **트윈이 지금 들고 있는 물품** — 붙는 커넥터가 「무엇이 바뀌었나」를 견줄 근거 (2026-08-28).\n *\n * ── 무엇을 고치나 ───────────────────────────────────────────────────────────\n * 전량 절대값을 읽는 커넥터는 지난 주기와 견주어 **바뀐 것만** 낸다. 그 비교 표가 프로세스 안에만\n * 있어서, 재기동하면 커넥터가 자기 기억을 잃고 **전량을 「바뀐 것」으로 판정**한다. 승화푸드에서 그\n * 한 번이 2,665건이었고, 재기동마다 지난 기록을 덮었다.\n *\n * **트윈은 잃지 않는다** — 저장본에서 되세우고 뜬다(실측 로그: `items 2665 · orders 4794`). 잃는\n * 쪽은 커넥터다. 그래서 트윈이 아는 것을 건네면 첫 주기부터 바뀐 것만 나간다.\n *\n * 없으면 `undefined` 다 — 그것이 「트윈이 아무것도 모른다」는 사실이고, 그때만 전량이 새 사실이다.\n * 빈 배열로 메우지 않는다(그러면 「모른다」와 「없다」가 같아진다).\n */\n observedItems?: ObservedItemFact[]\n /**\n * 커서가 움직였다고 알린다 — 이 층이 저장한다.\n *\n * 어댑터가 **매 행마다 부르지 않는다**: 저장은 이 층의 몫이고, 잦으면 이 층이 눌린다. 흐름 하나가\n * 한 묶음을 소비한 뒤 한 번이면 충분하다.\n */\n onCursor?: (cursor: { streams: Record<string, LiveFeedCursor>; firstAttachedAt?: string }) => void\n /**\n * **원본에 닿지 못했다** — 어댑터가 읽기 실패를 알린다(2026-08-23).\n *\n * ── 무엇이 아무 신호도 내지 않았나 ─────────────────────────────────────────────────────────\n * 실 원본이 끊겼을 때(접속 시간 초과) 트윈의 **조회 가능한 상태 어디에도** 그 사실이 없었다.\n * `onRecords` 가 불리지 않으면 유입 장부에 아무 일도 일어나지 않고, `TwinReference.lastError` 는\n * 마스터 동기·접속 시험에만 적힌다. 그래서 화면이 볼 수 있는 것은 「새 사실이 없다」뿐이었고,\n * 그것은 **「연결된 시스템에서 들어온 것이 없다」와 구별되지 않는다.**\n *\n * 어댑터는 로그로 말하고 있었다(재시도 경고). 그러나 **로그는 사람이 볼 때만 값이 있다** — 화면이\n * 말하려면 상태에 있어야 한다. 이 통로가 그 자리다.\n *\n * ── 무엇을 알리고 무엇을 알리지 않나 ────────────────────────────────────────\n * **재시도를 다 쓰고 그 주기를 포기했을 때** 부른다 — 재시도마다 부르면 한 번의 흔들림이 단절로 보인다.\n * 흐름 여럿을 읽는 원본은 흐름 이름을 함께 준다(어느 표가 막혔나가 원인 찾기의 절반이다).\n *\n * **빈 읽기는 실패가 아니다** — 원본이 「새 것이 없다」고 답한 것은 닿았다는 뜻이므로 `onRecords([])`\n * 로 알린다. 그 둘을 섞으면 들어온 것이 없는 연결이 끊긴 원본으로 보인다(고치려던 것의 반대 방향으로 틀린다).\n *\n * ── 무엇을 어댑터가 보내고 무엇을 이 층이 세나 (2026-08-27) ─────────────────\n * 어댑터는 **이유**만 보낸다 — 그리고 물러섰다면 다음에 다시 물을 때까지의 시간을 함께 보낸다.\n *\n * 연달아 몇 번 실패했는지와 언제부터 실패하고 있는지는 **이 층이 이미 센다**(`recordReadFailure`).\n * 그것을 어댑터가 함께 보내면 같은 수를 두 곳에서 세게 되고, 갈라지면 어느 쪽이 사실인지 알 수 없다.\n *\n * `reason` 에 **완성된 문장을 담지 않는다.** 이 값은 화면에 그려지고, 화면은 다섯 언어로 그린다.\n * 「3회 연달아 …부터 읽지 못했습니다」처럼 한 언어의 문장을 넣으면 그 문장은 번역되지 않고, 이 층이\n * 세는 수와 어긋날 수도 있다. 이유 하나만 짧게 준다(접속 시간 초과 · 401 · 형식 오류).\n */\n onReadFailure?: (info: {\n reason: string\n stream?: string\n /** 다음에 다시 물을 때까지 남은 시간(ms) — 물러섰을 때만 준다. */\n nextRetryMs?: number\n }) => void\n /**\n * **끊겼다가 돌아왔다** — 어댑터가 회복을 알린다 (2026-08-27).\n *\n * ── 왜 따로 알려야 하나 ─────────────────────────────────────────────────────\n * 읽기가 다시 성공하면 실패 기록은 지워진다. 그것만 하면 「끊긴 적이 있었다」가 화면에서 사라지고,\n * 밤새 두 시간 끊겼던 연결과 한 번도 끊기지 않은 연결이 아침에 똑같이 보인다.\n *\n * 읽기 성공(`onRecords`)만으로 이 층이 회복을 유추하지 않는다 — 몇 번 실패한 뒤였는지와 얼마나\n * 끊겨 있었는지는 재시도를 관리하는 어댑터가 안다.\n */\n onRecovered?: (info: { afterFailures: number; downMs: number }) => void\n /**\n * **읽었는데 창을 넘길 수 없다** — 어댑터가 커서 정체를 알린다 (2026-08-24).\n *\n * ── 왜 `onReadFailure` 와 갈라야 하나 ───────────────────────────────────────\n * 이 문을 만들기 전에는 두 사실이 **같은 이름으로** 나갔다.\n *\n * 원본에 닿지 못했다 접속 실패·시간 초과·형식 오류 → 기다리면 풀린다\n * 읽었는데 커서가 못 넘어간다 한 시각에 한 페이지보다 많은 행이 몰려 있다 → **기다려도 안 풀린다**\n *\n * 둘째는 **읽기가 성공한 실패**다. 원본은 답했고, 그 답의 모양이 커서를 이긴 것이다. 그런데 화면이\n * 「원본에 닿지 못한다」고 말하면 사람을 반대 방향으로 보낸다 — 원본을 의심하고 기다린다. 실제로\n * 필요한 조치는 **페이지를 키우거나 같은 시각 안에서 순서를 정하는 것**이고, 기다림으로는 영원히\n * 풀리지 않는다.\n *\n * 조치가 반대인 두 사실을 한 이름으로 부르면, 그 이름은 정보가 아니라 오해다.\n *\n * ── 무엇을 알리나 ───────────────────────────────────────────────────────────\n * 그 주기를 포기했을 때 부른다(`onReadFailure` 와 같은 규율). 어느 흐름인지 알면 함께 준다 —\n * 밀도가 높은 표는 원본마다 다르므로 그 이름이 조치의 절반이다.\n *\n * **닿지 못한 것과 섞어 부르지 않는다.** 하나의 주기가 두 이유로 실패할 수는 없다(먼저 닿아야 읽는다).\n */\n onCursorStall?: (info: { reason: string; stream?: string }) => void\n /**\n * **원본에 있는데 세우지 않은 것을 알린다** — 이유와 수.\n *\n * ── 왜 이 통로가 필요한가 (2026-08-24) ─────────────────────────────────────\n * 어댑터가 원본의 사실 일부를 **일부러 받지 않는 일이 정상이다**(입자가 안 맞는다 · 받으면 상태가\n * 거짓이 된다 · 커널 어휘로 옮길 수 없다). 그때 어댑터는 경고를 냈는데, 그 경고가 **프로비저닝\n * 화면에서 한 번 스쳐 지나갈 뿐**이었다 — 그 뒤로는 어디에서도 볼 수 없었다.\n *\n * 그래서 사용자가 「원본보다 이것이 적다」를 물으면 답이 어디에도 없었다. 그 사실은 한 번의 사건이\n * 아니라 **상태의 성질**이다: 세우지 않은 것은 **지금도** 트윈에 없다.\n *\n * **주기마다 불러도 된다** — 장부가 이유로 묶어 세고 마지막 수로 덮는다(누적하지 않는다). 그래서\n * 「지금 세우지 않은 것이 8건」이 8로 남고 800으로 부풀지 않는다.\n *\n * **풀리면 부르지 않는 것으로 끝나지 않는다** — 이유가 사라졌으면 `count: 0` 으로 부르는 것이 아니라\n * 그 이유를 더 이상 보내지 않으면 된다… 가 **아니다.** 장부는 마지막 값을 들고 있으므로, 풀린 것을\n * 알리려면 그 이유로 `count: 0` 을 한 번 보내라(그때 줄이 사라진다). 알리지 않고 그치면 낡은 수가 남는다.\n *\n * 이유는 **자유 문자열**이다 — 무엇을 왜 안 받는지는 원본마다 다르고, 계약이 목록을 닫으면 새 원본을\n * 붙일 때마다 계약을 고치게 된다(흐름 열쇠와 같은 규율). 사람이 읽을 문장으로 적어라.\n */\n onWithheld?: (info: { reason: string; count: number }) => void\n}\n\n/**\n * 저쪽이 조치를 받을 준비가 됐나 — **「모른다」를 「안 된다」로 답하지 않는다.**\n *\n * `ready` 가 거짓이면 `reason` 이 사람의 말로 무엇이 없는지 말하고, `code` 가 화면이 다음 할 일을\n * 가를 값이다(문장을 파싱하지 않게 — 번역하면 문장이 바뀐다).\n */\nexport interface ActuationReadiness {\n /** 받을 수 있어 보이나. **모르면 `undefined`** — 거짓이 아니다. */\n ready?: boolean\n /** 어디까지 봤나. `local` = 우리 설정만 봤다(망을 타지 않았다). `remote` = 저쪽에 물었다. */\n checked: 'local' | 'remote'\n /** 무엇이 없나 — 사람의 말로. */\n reason?: string\n /** 화면이 가를 값. 커넥터가 정한다(예: `no-endpoint` · `no-secret` · `unreachable` · `not-configured`). */\n code?: string\n}\n\nimport type { LiveCadence } from './live-cadence.js'\n\n/**\n * 조치를 넘긴 결과 — **어댑터가 돌려주는 것.**\n *\n * 이름을 붙인 이유는 두 가지다. 익명 타입이라 소비처가 그 모양을 말할 수 없었고(`actuate` 를 감싸는\n * 쪽이 반환형을 다시 적어야 했다), 그리고 **거절 사유를 코드로 실을 자리가 없었다** — `error` 는 사람\n * 문장이라 저장하면 번역과 판독이 함께 굳는다.\n *\n * `errorCode`·`errorParams` 는 `CommandAck` 과 **같은 낱말**이다(ADR-0037 ④ · ADR-0042 ③). 셋째 이름을\n * 만들지 않는다 — 같은 것을 세 이름으로 부르면 매핑 표가 생기고, 그 표는 커넥터가 새 코드를 내는 날\n * 「알 수 없는 사유」로 떨어진다.\n */\nexport interface ActuationResult {\n ok: boolean\n ref?: string\n /**\n * **왜 거절됐나 — 언어 중립 코드.** 저장되는 값은 이것이고 `error` 는 아니다.\n *\n * `lastError` 가 사람 문장을 저장해 두고 화면이 그것을 그대로 뿌렸다. 그러면 번역도 못 하고,\n * 무엇이 났는지 세지도 못하고, 문장을 고치는 순간 옛 행과 새 행이 달라진다.\n */\n errorCode?: RefusalCode\n /** 그 코드가 가리키는 원시값들 — 표현층이 문장에 끼운다. */\n errorParams?: RefusalParams\n /**\n * **받아들였으나 남은 것이 있다** — 실패가 아니다. 사람이 저쪽에서 해야 할 일이 있으면 여기 적는다.\n *\n * `error` 에 적지 말 것 — 실패로 읽혀 커맨드가 `failed` 로 앉고, 다시 넘기게 된다. 실제로 MES 가\n * 「지시서를 못 붙였다」를 답했을 때 커넥터가 적을 칸이 없어 로그로만 남겼고, 트윈 쪽에서는 성공한\n * 조치와 구별되지 않았다.\n */\n note?: string\n /**\n * 그 말 중 **다음에 할 일** 한 줄 — 원인은 위 `note` 다.\n *\n * 붙여 보내지 말 것. 읽는 사람은 「그래서 내가 뭘 해야 하나」를 먼저 찾고, 한 문장으로 오면\n * **화면이 자르게 되며 자르는 규칙이 화면마다 생긴다.** 어댑터는 이미 둘로 알고 있다.\n */\n noteNext?: string\n error?: string\n /**\n * **다시 해서 될 일인가** — 실패했을 때만.\n *\n * again 그대로 다시 해 볼 만하다 못 닿았거나 저쪽이 잠깐 흔들렸다\n * after-fix 사람이 고친 뒤 그대로 나간다 설정 문제\n * never 이 조치로는 영원히 안 된다 지시 내용이 틀렸다\n *\n * 가르는 자리는 **「조치를 다시 낼 필요가 있나」**다. 설정이 틀린 것은 조치가 멀쩡하므로\n * `after-fix`, 지시 내용이 틀린 것은 그 조치가 영원히 틀렸으므로 `never` 다.\n *\n * **문장에 담지 말 것** — 일꾼이 그것을 쓰려면 파싱해야 하고, 번역되면 깨진다.\n *\n * 말하지 않으면 일꾼이 **집지 않는다.** 모르는 것을 `never` 로 접으면 고칠 수 있는 것을 사람이\n * 포기하고, `again` 으로 접으면 없는 자재를 끝없이 두드린다.\n */\n retry?: 'again' | 'after-fix' | 'never'\n}\n\nexport interface ReferenceAdapter {\n /** 레지스트리 키 (예: 'virtual' | 'sap-ewm' | 'custom-rest'). */\n type: string\n /** picker·연결 폼용 메타데이터(선택). 없으면 type 만 노출. */\n meta?: AdapterMeta\n /** 연결 확인 — UI '연결 테스트'. */\n testConnection(cfg: ConnectionConfig): Promise<{ ok: boolean; error?: string }>\n /** N 발견 — 사이트/공장 열거. 단일 소스는 길이 1 반환. */\n discoverSites(cfg: ConnectionConfig): Promise<SiteDescriptor[]>\n /** 사이트별 마스터(구조) — 기존 masterToTwin 이 소비. */\n /**\n * 이 어댑터가 마스터를 읽으며 **보낼 단계를 미리 선언한다** — 그것이 진행률의 분모다.\n *\n * 표준 낱말만 쓴다(`REFERENCE_STEPS`). 선언하지 않으면 화면은 퍼센트를 보이지 않고 단계 이름만\n * 보인다 — **모르는 진행률을 지어내지 않는다.**\n *\n * 원본에 없는 자리는 선언하지 않는다. 그러면 「이 원본으로 채울 수 있는 만큼」이 곧 분모가 된다.\n */\n masterSteps?: readonly ReferenceStep[]\n\n /**\n * 이 원천이 **축마다 언제·얼마나 자주 내놓나** — 유입 건강이 이 선언으로 견준다.\n *\n * 선언하지 않은 축은 **지금까지처럼** 창 하나(10분)로 견준다. 그러면 밤에 발전하지 않는\n * 태양광과 업무시간만 도는 원천이 **정상인데 매일 밤 빨갛게** 난다 — 거짓 빨강이 쌓이면 진짜\n * 빨강도 같이 안 읽힌다.\n *\n * `masterSteps` 와 같은 규율이다: **어댑터만 자기 시간의 결을 안다.** 호스트가 짐작하면 그것은\n * 지어낸 판정이다.\n *\n * 자세한 것은 §`live-cadence.ts` — 특히 「해 있는 동안」을 **고정 시각으로 박지 말 것**\n * (오늘 잰 일몰은 두 달 뒤에 틀리다).\n */\n liveCadence?: readonly LiveCadence[]\n\n /**\n * @param onStep 진행을 알리는 통로 — **선택이다.** 주지 않으면 예전과 같이 동작한다.\n *\n * 마스터 읽기는 원본에 여러 질의를 보내는 긴 작업이고(창고·자리·설비·공정·경로·품목·BOM), 화면은 그\n * 사이에 아무것도 알 수 없었다. 어댑터만 자기 단계를 아므로 어댑터가 말해야 한다 — 호스트가 짐작하면\n * 그것은 지어낸 진행률이다.\n *\n * `total` 은 **그 어댑터가 보낼 단계 수**다. 모르면 주지 않는다(화면이 퍼센트를 만들지 않는다).\n */\n fetchMaster(\n cfg: ConnectionConfig,\n site: SiteDescriptor,\n onStep?: (step: { key: ReferenceStep; done: number; total?: number; detail?: string }) => void\n ): Promise<ReferenceMaster>\n /**\n * (선택) 사이트별 실 이벤트 스트림 — 없으면 sim. onRecords 로 레코드 push, unsubscribe 반환.\n *\n * ── 네 번째 인자: **읽기 커서** (2026-08-23) ────────────────────────────────\n *\n * 왜 이 층이 커서를 드나: 커널이 그 갈림을 이미 적어 두었다(§`hydrateContinuity`) — 「원천이 **애초에\n * 다시 말해 주지 않는 축**」은 재기동 연속성으로 이어받는다. **「우리가 어디까지 읽었나」가 정확히 그\n * 성질이다.** 원본은 그것을 되풀어 주지 않는다.\n *\n * 커서가 없던 동안 어댑터는 붙을 때마다 `Date.now() − 되돌아볼 날수` 로 창을 새로 만들었다. 그래서\n * **재기동마다 미러의 과거가 잘렸다** — 실측으로 작업 2,855 → 2,820(8시간 흐른 만큼). 미러가 아는\n * 것이 「원본의 사실」이 아니라 「창의 함수」였다.\n *\n * 그런데 미러의 상태는 **원천으로 서야 한다**(같은 절: 「미러의 진실은 원천이다 … 심으면 떠난 물건이\n * 되살아난다」). 그러니 저널을 계산해 상태를 세우는 것으로 고칠 일이 아니다 — 그 선언의 **전제**가\n * 「원천이 다시 말해 준다」이고, 창이 좁아지면 그 전제가 깨진다. 창을 고치는 것이 그 원칙을 지키는 길이다.\n *\n * ── 계약의 모양 ─────────────────────────────────────────────────────────────\n * · **흐름 열쇠는 어댑터가 정한다.** 원본마다 흐름 수와 뜻이 다르다(이 원본은 재고·오더·로트·투입 넷).\n * 계약이 이름을 닫으면 새 원본마다 계약을 고치게 된다 — 원본의 스키마가 계약을 끌고 가는 그 모양이다.\n * · **저장되는 모양으로 넘긴다**(`seen` 은 배열). 이 층은 저장·복원만 하고 안을 해석하지 않는다.\n * `Set` 을 받으면 이 층이 직렬화를 알게 되고, 커서 모양이 바뀔 때 두 곳을 고친다.\n * · `firstAttachedAt` — **첫 창을 만든 시각.** 이후 재기동에서 바뀌지 않는다. 커널은 아는 구간의\n * 상한만 안다(`nowTime` = 마지막으로 들은 시각). 이 값이 하한이고, 둘이 「이 트윈이 아는 구간」이다.\n * 화면이 수를 보일 때 그 구간을 함께 말해야 한다 — 말하지 않으면 알리지 않고 자르는 것이 된다.\n * · **커서가 없을 때만** 되돌아볼 날수를 쓴다. 그 순간이 `firstAttachedAt` 이다.\n *\n * 주지 않는 호출자와도 함께 선다(선택 인자) — 커서를 모르는 배포는 예전처럼 동작한다.\n */\n /**\n * (선택) **연결된 시스템이 밀어 준 것**을 우리 레코드로 옮긴다 — 웹훅.\n *\n * ── 왜 `openLiveFeed` 와 나누나 (2026-08-26) ───────────────────────────────\n * `openLiveFeed` 는 「가져오기」와 「옮기기」를 한 함수에 담고 있다. 그래서 가져오는 방식이 바뀌면\n * (그쪽이 밀어 주면) 옮기는 코드를 쓸 수 없었다 — 같은 파일 안에 있는데 밖에서 부를 수 없다.\n *\n * 이 함수는 **옮기기만** 한다. 밖에서 받은 본문을 받아 우리 레코드 목록을 돌려준다. 입구·인증·유입은\n * 프레임워크가 맡는다(§`reference-hook`).\n *\n * ── 옮길 수 없으면 던진다 ─────────────────────────────────────────────────\n * 빈 목록과 「옮길 수 없다」는 다르다. 빈 목록은 「우리와 무관한 것이 왔다」이고, 던지는 것은 「그쪽\n * 모양이 우리가 아는 것과 다르다」다. 그 둘을 같게 답하면 그쪽 모양이 바뀐 것을 아무도 모른다.\n *\n * ── 판단은 여기서 한다 ────────────────────────────────────────────────────\n * 통신이 끊긴 설비의 값을 보내지 않는 것, 잰 시각이 나아가지 않은 줄을 거르는 것 — 폴링에서 하던\n * 판단을 여기서도 한다. 그래야 두 길이 같은 답을 낸다.\n *\n * ── 번호는 프레임워크가 본다 (2026-08-31) ─────────────────────────────────\n * 빠진 번호를 알아채는 일은 밀어 주는 모든 연결에 같은 규율이라야 한다. 커넥터마다 만들면 한 곳이\n * 빠지고, 빠진 그 연결에서만 사실이 없어진다. 그래서 커넥터는 **번호를 꺼내 주기만** 하고 판정은\n * 프레임워크가 한다(§`reference-hook`).\n */\n handleInbound?(cfg: ConnectionConfig, site: { siteId: string }, body: unknown, headers: Record<string, unknown>): InboundBatch\n\n /**\n * **현장에 조치를 내린다** — 인바운드의 반대 면(§`command-routing.md` §8.1).\n *\n * 별개 어댑터를 두지 않는다. 같은 연결이 사실을 읽고 조치를 내리므로, 인증과 설정이 한 자리에 있어야\n * 한다. 둘로 나누면 같은 시스템에 두 벌의 연결 설정이 생긴다.\n *\n * ── 승인은 이미 지났다 (2026-08-31) ───────────────────────────────────────\n * 인자가 `ApprovedCommand` 다 — 그 표식은 승인을 지난 커맨드에만 붙고, 디스패처가 붙인다\n * (§`command-dispatcher.ts`). **어댑터는 승인을 다시 확인하지 않는다.** 확인이 두 곳에 있으면\n * 규칙이 두 벌이 되고 한쪽만 고쳐지는 날이 온다.\n *\n * 이 자리를 게이트보다 먼저 열지 않은 이유가 있다 — 자리가 있으면 누군가 부르고, 그때 사람 승인 없이\n * 현장에 작업지시가 나간다. 작업지시는 취소해도 이미 만든 것이 남는다.\n *\n * ── 돌려줄 것 ─────────────────────────────────────────────────────────────\n * `ref` 를 **반드시** 돌려준다. 그것이 없으면 넘긴 것이 저쪽에서 무엇이 되었는지 되짚을 수 없고,\n * 되돌릴 때 무엇을 되돌릴지 말할 수 없다.\n */\n actuate?(cfg: ConnectionConfig, site: SiteDescriptor, command: ApprovedCommand): Promise<ActuationResult>\n\n /**\n * **저쪽이 조치를 받을 준비가 됐나** — 보내기 전에 묻는다. 선택이다.\n *\n * ── 왜 필요한가 (2026-09-03 실측) ──────────────────────────────────────────\n * 승인된 조치를 넘겼더니 저쪽이 503 을 답했다 — **저쪽 프로세스에 비밀값이 실려 있지 않았다.**\n * 트윈 쪽 연결 설정에는 있었고, 주소도 맞았고, 서명도 맞았다. 저쪽이 재기동되면서 환경 변수에만\n * 살던 값을 잃은 것이다.\n *\n * 그것을 보내 보기 전에는 알 수 없었다. 그래서 사람이 승인 화면까지 가서 누르고, 실패를 보고,\n * 다시 로그인했다. **승인이 사라지지는 않는다**(`failed → dispatch` 가 열려 있다) — 없어진 것은\n * 사람의 시간이다.\n *\n * ── 문이 아니다. 알림이다 ──────────────────────────────────────────────────\n * **이 답으로 넘김을 막지 않는다.** 막으면 새 실패 방식이 생긴다 — 점검이 틀렸거나 잠깐 못 닿은\n * 사이에, 성공할 수 있었던 조치가 못 나간다. 화면이 「지금 저쪽이 못 받는 것으로 보입니다」를\n * 미리 말하는 데까지가 이 얼굴의 일이다.\n *\n * ── 두 겹을 구별한다 ──────────────────────────────────────────────────────\n * 우리 쪽에 무엇이 없는 것(주소·비밀값을 설정하지 않았다)은 **망을 타지 않고** 알 수 있다.\n * 저쪽이 받을 준비가 됐는지는 물어야 안다. 앞엣것만 답하고 뒤엣것을 안 물어도 되고, 그때\n * `checked: 'local'` 로 그 사실을 말한다 — 「살아 있다」고 말한 적 없는 것과 「죽었다」는 다르다.\n *\n * 화면이 그릴 때마다 부를 수 있으므로 **망을 타는 구현은 짧게 끝내야 한다.**\n */\n actuationReadiness?(cfg: ConnectionConfig, site: SiteDescriptor): Promise<ActuationReadiness>\n\n /**\n * **놓친 구간을 backfill 한다** — 밀어 주는 길의 짝.\n *\n * ── 왜 필요한가 (2026-08-31, 인티그레이션 레인 지적) ──────────────────────\n * 훅은 반드시 놓친다 — 우리가 내려가 있을 때, 그쪽이 못 보냈을 때, 네트워크가 끊겼을 때. 받는 쪽이\n * 번호로 그것을 **알아채게** 되었지만(§`takeInSequence`), 알아챈 뒤 **그 사이를 채우는 길이 없었다.**\n * 설계가 그 길을 적어 두었는데 만들지 않았다.\n *\n * ── 왜 `since` 가 번호인가 ────────────────────────────────────────────────\n * 커서에 사는 것이 번호이고, 보내는 쪽이 되풀어 주는 단위도 번호다. 시각으로 두면 두 축이 섞이고,\n * 같은 밀리초의 두 행이 갈리지 않는다.\n *\n * ── 왜 `scope` 를 프레임워크가 주나 ───────────────────────────────────────\n * 그것이 커서 열쇠의 절반이다. 커넥터가 만들면 열쇠를 만드는 자리가 둘이 되고, 어긋난 그곳은 조용하다.\n * 그래서 프레임워크가 커서에서 읽어 그대로 넘긴다.\n *\n * 답이 `InboundBatch` 인 이유도 하나다 — **backfill 한 것이 밀어 받은 것과 같은 번호 검사를 지난다.**\n * 다른 모양으로 돌려주면 그 검사를 다시 만들게 되고, 두 길이 다른 규칙으로 받는다.\n */\n fetchSince?(cfg: ConnectionConfig, site: SiteDescriptor, scope: string, since: number): Promise<InboundBatch>\n\n /**\n * (선택) **원본에만 있는 관계를 조회한다** — 트윈으로 복제하지 않고 그때그때 물어본다.\n *\n * ── 왜 필요한가 ─────────────────────────────────────────────────────────────\n * 트윈은 표준 어휘로 옮길 수 있는 것만 담는다. 원본에는 그 밖의 관계가 있다 — MES 의 생산 실적,\n * 설비 상태 구간, 정비 계획. 설비 상세 화면에서 사람이 실제로 묻는 것이 그런 것들이다.\n *\n * 이것을 트윈으로 복제하면 원본의 표를 전부 미러링하게 된다. 조회용 참조는 복제하지 않고 그때\n * 물어보는 것이 맞다.\n *\n * ── 왜 어댑터가 답하나 ──────────────────────────────────────────────────────\n * 어느 테이블에 무엇이 있는지는 원본마다 다르다. 커널이 그것을 알면 원본 하나의 스키마에 묶이고,\n * 다음 원본이 다른 구조를 쓰면 커널을 또 고친다.\n *\n * 어댑터는 이미 그 원본에 붙는 방법과 인증을 갖고 있다. 연결 설정이 한 자리에 있어야 한다는 것은\n * `actuate` 를 별개 어댑터로 두지 않은 것과 같은 이유다.\n *\n * ── 실패하면 그것을 알린다 ──────────────────────────────────────────────────\n * 예외를 던져도 됩니다. 호출하는 쪽이 잡아서 「조회 실패」로 표시하고, 나머지 관계는 그대로\n * 표시합니다. 빈 배열은 「가리키는 것이 없다」는 뜻이고 실패와 다릅니다.\n *\n * @param axis `equipment` · `locations` 등 계약의 축 이름\n * @param itemId 그 축에서의 식별자. 원본이 아는 이름이다(설비는 `ops_equipment.name`)\n */\n fetchLinks?(cfg: ConnectionConfig, site: SiteDescriptor | undefined, axis: string, itemId: string): Promise<ReferenceLinkGroup[]>\n\n /**\n * (선택) **그 번호가 무엇이었나** — 구멍을 만났을 때 묻는다.\n *\n * ── 왜 필요한가 (2026-09-06 실물) ──────────────────────────────────────────\n * 커넥터가 구멍을 만나면 「418 다음이 420」이라고만 말한다. **419 가 무엇이었는지 알 방법이\n * 없다.** 그날 plant 레인이 자기 아웃박스를 손으로 뒤져서 그것이 부하 시험의 흔적임을 찾았다.\n *\n * 구멍은 채널을 세운다 — 그 뒤의 사실이 통째로 못 온다. 그래서 「이게 무엇이었나」가 급한 물음인데\n * 물을 자리가 없었다.\n *\n * ── 세 가지를 가른다. 넷째는 「모른다」다 ──────────────────────────────────\n * ```\n * present 그 행이 있다 — 봉투를 함께 낸다\n * lost 번호는 나갔는데 행이 없다 — 쓰기 하나를 잃었다\n * unissued 아직 그 번호까지 안 갔다 — 잃은 것이 아니다\n * unknown 원본이 그 셋을 구별하지 못한다\n * ```\n *\n * **`unknown` 이 있어야 한다.** 채번을 따로 들지 않는 원본은 「행이 없다」에서 `lost` 와\n * `unissued` 를 가를 수 없다. 그때 `lost` 로 단정하면 사람이 없는 사실을 찾으러 간다.\n *\n * ── 판정을 여기서 하지 않는다 ─────────────────────────────────────────────\n * 이 문은 **원본이 아는 것을 그대로 낸다.** 「그러니 이 번호는 포기해도 된다」는 판정은 사람이\n * 한다 — 그 판단은 되돌릴 수 없어서(커서가 지나가면 끝이다) 코드가 대신할 자리가 아니다.\n *\n * @param from 이 번호부터(포함)\n * @param to 이 번호까지(포함). 원본이 상한을 두면 그만큼만 내고 `truncated` 로 말한다\n */\n seqReport?(cfg: ConnectionConfig, site: SiteDescriptor | undefined, scope: string, from: number, to: number): Promise<SeqReport>\n\n openLiveFeed?(\n cfg: ConnectionConfig,\n site: SiteDescriptor,\n onRecords: (records: unknown[]) => void,\n continuity?: LiveFeedContinuity\n ): () => void\n /**\n * 이 원본이 **선언하는 능력** — 없으면 「구조만 준다」는 뜻이다(ADR-0029 §8).\n *\n * 선언과 구현이 어긋나지 않게 `capabilitiesOf` 가 둘을 함께 본다: `control` 을 선언했는데 `control`\n * 구현이 없으면 그 능력은 **없는 것으로 읽는다**(선언만 있는 능력은 사용자에게 거짓말이 된다).\n */\n capabilities?: ReferenceCapability[]\n /**\n * 이 커넥터가 **무엇에 대고 확인됐나** — 능력과 다른 축이다(2026-08-23).\n *\n * ── 왜 필요한가 ─────────────────────────────────────────────────────────────\n * `capabilities` 는 「무엇을 할 수 있다고 선언하나」이고, 이 자리는 **「그 선언이 무엇으로 확인됐나」**다.\n * 둘이 갈리는 것이 지금 상태다: 등록된 커넥터 열넷 중 실 시스템에 붙어 본 것은 **하나**이고, 나머지는\n * 목(mock)에 대고 만들었다. 그런데 화면의 커넥터 목록에 그 구분이 없어서 **목으로 확인한 것이 확인으로\n * 보인다.**\n *\n * 그것은 이 저장소가 다른 자리에서 계속 거절하는 모양이다 — 「선언만 있는 능력은 사용자에게\n * 거짓말이 된다」(§`capabilitiesOf`). 능력에 세운 그 규율을 근거에도 세운다.\n *\n * ── 왜 계약이고 배포 노트가 아닌가 ──────────────────────────────────────────\n * 노트는 다음 사람이 보지 않는다. 계약에 있으면 새 커넥터가 그 값을 **빠뜨릴 수 없고**, 빠뜨리면\n * `groundingOf` 가 `unknown` 으로 답해 화면이 그 사실을 말한다.\n *\n * ── 자동으로 알 수 없는 값이다 ──────────────────────────────────────────────\n * 능력은 구현이 있는지 보면 알 수 있다. 「실 시스템에 붙여 봤나」는 **볼 수 없다** — 사람이 아는 사실이다.\n * 그래서 이 선언은 낡을 수 있고, 그것을 막는 것이 `verifiedAgainst` 다: **무엇에 대고 확인했는지 적지\n * 않으면 그 주장은 주장이 아니다**(`groundingOf` 가 `unknown` 으로 내린다).\n */\n grounding?: ReferenceGrounding\n /**\n * (선택) 구동 — 자극·속도·정지·초기화. **`capabilities` 에 `control` 을 선언한 원본만** 가진다.\n *\n * 실 시스템은 이것을 갖지 않는다(실 WMS 는 일시정지되지 않는다). 그 사실이 결손이 아니라 이 축의 요점이다.\n */\n control?(cfg: ConnectionConfig, site: SiteDescriptor | undefined, command: ControlCommand): Promise<ControlResult>\n}\n\n/**\n * 이 원본이 **실제로 할 수 있는 것** — 선언과 구현의 교집합.\n *\n * 선언만 있고 구현이 없으면 화면이 「할 수 있다」고 말한 뒤 아무 일도 일어나지 않는다. 반대로 구현이\n * 있는데 선언이 없으면 그 능력은 **의도적으로 닫아 둔 것**이므로 열지 않는다 — 선언이 권위다.\n */\n/**\n * 근거 수준 — **무엇에 대고 확인했나.** 넷이고 닫혀 있다.\n *\n * `synthetic` 실 시스템이 아니다 — 스스로 만들어 낸다(가상 원본·시뮬레이터). 한계가 아니라 성질이다.\n * `mock` 목·목 REST 에 대고만 확인했다. **실 시스템에 붙어 본 적이 없다.**\n * `instance` 실 시스템 **한 배포**에 붙여 확인했다. 그 배포의 성질과 제품의 성질을 아직 가르지 못했다.\n * `product` 실 시스템 **여러 배포**에 붙여 확인했다 — 그때 비로소 제품의 성질이라 말할 수 있다.\n *\n * `instance` 와 `product` 를 가르는 이유: 첫 실 연동에서 그 배포만의 성질이 넷 드러났고(관계 리솔버가\n * 다른 테넌트의 행을 답한다 · 모르는 필터를 알리지 않고 무시한다 · 지워진 품목의 BOM 이 남아 있다 ·\n * 사실상 무한을 뜻하는 표식) **그것이 그 제품의 성질인지 그 인스턴스의 성질인지 아무도 모른다.**\n * 두 번째 배포가 붙는 날 갈린다. 그 사실을 어휘가 담는다.\n */\nexport type ReferenceGroundingLevel = 'synthetic' | 'mock' | 'instance' | 'product'\n\nexport interface ReferenceGrounding {\n level: ReferenceGroundingLevel\n /**\n * **무엇에 대고 확인했나** — 사람이 읽는 한 줄(원본의 판·인스턴스·목의 이름).\n *\n * `instance`·`product` 를 주장하면서 이것이 비어 있으면 `groundingOf` 가 `unknown` 으로 내린다 —\n * 가리키는 대상이 없는 주장은 주장이 아니고, 그런 주장은 낡아도 아무도 알 수 없다.\n */\n verifiedAgainst?: string\n}\n\n/**\n * 이 커넥터의 근거 — **주장을 그대로 믿지 않는다**(`capabilitiesOf` 와 같은 규율).\n *\n * 능력은 구현이 있는지 보아 선언을 검증할 수 있다. 근거는 **볼 수 없다** — 사람이 아는 사실이다.\n * 그래서 검증할 수 있는 것 하나만 검증한다: **실 시스템을 주장하면 무엇에 대고인지 말해야 한다.**\n *\n * 선언이 없으면 `unknown` 이다 — 「목이다」가 아니라 **「모른다」**다. 둘을 같게 두면 아직 선언하지 않은\n * 새 커넥터가 목으로 낙인찍히고, 그것도 없는 사실이다.\n */\nexport function groundingOf(adapter: ReferenceAdapter | undefined): {\n level: ReferenceGroundingLevel | 'unknown'\n verifiedAgainst?: string\n /** 주장이 내려갔으면 그 이유 — 화면이 그대로 옮긴다. */\n downgraded?: string\n} {\n const g = adapter?.grounding\n if (!g?.level) return { level: 'unknown' }\n const against = g.verifiedAgainst?.trim()\n if ((g.level === 'instance' || g.level === 'product') && !against) {\n return {\n level: 'unknown',\n downgraded: `claims \"${g.level}\" but does not say what it was verified against`\n }\n }\n return { level: g.level, ...(against ? { verifiedAgainst: against } : {}) }\n}\n\n/**\n * 능력마다 **그것을 증명하는 메서드들** — 하나라도 있으면 성립한다.\n *\n * ── `live` 의 증명이 왜 둘인가 (2026-09-16, ADR-0036 ③ 개정) ─────────────────\n * 예전에는 `openLiveFeed` 하나였다. 그런데 사실이 흘러 들어오는 방식은 **상대 시스템의 성질**이지\n * 우리 능력의 종류가 아니다 — 우리가 당겨 오면 `openLiveFeed`, 상대가 웹훅으로 밀어 넣으면\n * `handleInbound`(계약 일급 문)다. 둘 다 「이 원본에서 사실이 계속 온다」를 증명한다.\n *\n * 실측: `operato-plant` 은 outbox 가 밀어 넣는 쪽이라 `openLiveFeed` 가 없다. 그래서 **실 생산\n * 사실을 나르는 유일한 커넥터가 판정에서 live 가 아니었다.** 등록 경고는 「구현했는데 선언 안 함」\n * 한 방향만 봐서 아무 말도 하지 않았다.\n */\nconst PROVED_BY: Record<ReferenceCapability, readonly string[]> = {\n live: ['openLiveFeed', 'handleInbound'],\n control: ['control'],\n actuate: ['actuate'],\n /*\n * **선언만으로 성립한다** — 부를 메서드가 없다. 「두 번 해도 같은가」는 어댑터가 하는 일이 아니고\n * 저쪽 시스템의 성질이다. 그래서 구현 검사를 하지 않는다.\n */\n 'idempotent-actuation': []\n}\n\nconst CAPABILITIES = Object.keys(PROVED_BY) as ReferenceCapability[]\n\n/** 그 능력을 증명하는 메서드가 실제로 붙어 있나. 증명이 필요 없는 능력은 언제나 참이다. */\nfunction implemented(adapter: ReferenceAdapter, capability: ReferenceCapability): boolean {\n const methods = PROVED_BY[capability]\n if (methods.length === 0) return true\n return methods.some(name => typeof (adapter as any)[name] === 'function')\n}\n\nexport function capabilitiesOf(adapter: ReferenceAdapter | undefined): ReferenceCapability[] {\n if (!adapter) return []\n const declared = new Set(adapter.capabilities ?? [])\n return CAPABILITIES.filter(capability => declared.has(capability) && implemented(adapter, capability))\n}\n\n/* 어댑터 \"타입\" 레지스트리(전역·인메모리) — 레퍼런스 데이터는 DB(TwinReference), 어댑터 동작은 여기. */\nconst adapters = new Map<string, ReferenceAdapter>()\n\n/**\n * 어댑터를 등록한다 — 그리고 **선언과 구현이 어긋나면 그 자리에서 말한다.**\n *\n * ── 왜 필요한가 (2026-08-26 실측) ──────────────────────────────────────────\n * 능력 판정(`capabilitiesOf`)은 **한 방향만** 본다: 선언했는데 구현이 없는 것은 걸러 내고, **구현했는데\n * 선언이 없는 것은 아무 말도 하지 않는다.** 그래서 다음이 오류 없이 일어난다.\n *\n * 커넥터가 `openLiveFeed` 를 구현한다 → `capabilities: ['live']` 를 잊는다\n * → 연결 화면이 「이 원본은 라이브를 못 낸다」로 읽는다\n * → 재기동 정책을 정할 수 없어 트윈 생성이 거절된다\n * → 사람이 보는 것은 「재기동 정책이 없습니다」뿐이다\n *\n * 실제로 그렇게 막혔고(태양광 발전소), 원인을 찾는 데 코드를 뒤져야 했다. 지금 저장소에서 같은 상태인\n * 커넥터가 일곱이다.\n *\n * ── 왜 막지 않고 알리기만 하나 ──────────────────────────────────────────────\n * 등록에서 던지면 커넥터 하나 때문에 호스트가 못 뜬다. 만들다 만 커넥터를 들고 있는 사람이 아무것도\n * 못 하게 된다. 그리고 이 어긋남은 **고칠 수 있는 것**이지 위험한 것이 아니다 — 말해 주면 된다.\n */\nexport function registerAdapterType(adapter: ReferenceAdapter): void {\n const declared = new Set(adapter.capabilities ?? [])\n\n /*\n * **어긋남은 양쪽으로 본다** (2026-09-16, ADR-0036 ③ 개정).\n *\n * 이 경고는 「구현했는데 선언 안 함」 한 방향만 봤다. 그래서 반대쪽 — 선언했는데 증명이 없는 것 —\n * 은 `capabilitiesOf` 가 조용히 걸러 내기만 하고 아무도 그 사실을 듣지 못했다. 한 방향만 보면\n * **있는 것을 없다고(이번 plant) 또는 없는 것을 있다고** 말하게 된다. 같은 무게로 말한다.\n */\n const undeclared = CAPABILITIES.filter(c => PROVED_BY[c].length > 0 && implemented(adapter, c) && !declared.has(c))\n const unproven = CAPABILITIES.filter(c => PROVED_BY[c].length > 0 && declared.has(c) && !implemented(adapter, c))\n\n if (undeclared.length) {\n twinWarn(\n `[reference] connector \"${adapter.type}\" implements ${undeclared.join(' and ')} but does not declare it — ` +\n `add capabilities: [${undeclared.map(c => `'${c}'`).join(', ')}] or remove the implementation. ` +\n 'Until then the screens treat this connector as unable to do it, and a live twin cannot be created from it.'\n )\n }\n if (unproven.length) {\n twinWarn(\n `[reference] connector \"${adapter.type}\" declares ${unproven.join(' and ')} but implements none of ` +\n `${unproven.map(c => PROVED_BY[c].map(m => `${m}()`).join(' or ')).join(', ')} — ` +\n 'the declaration is dropped, so the screens treat this connector as unable to do it. ' +\n 'Implement one of those methods or remove the capability.'\n )\n }\n\n adapters.set(adapter.type, adapter)\n}\nexport function getAdapter(type: string): ReferenceAdapter | undefined {\n return adapters.get(type)\n}\nexport function listAdapterTypes(): string[] {\n return [...adapters.keys()]\n}\n/** 커넥터 메타데이터 목록(picker·연결 폼용). */\n/**\n * 등록된 어댑터 목록 — **능력은 판정값이다. 선언 원본이 아니다.**\n *\n * 능력은 이 목록을 읽는 화면이 「이 원본에 무엇을 요구할 수 있나」를 판단하는 근거다.\n *\n * ── 왜 선언을 그대로 내지 않나 (2026-09-16, ADR-0036 ③ 개정) ────────────────\n * 여기서 `a.capabilities` 를 그대로 냈다. 그래서 `capabilitiesOf` — 선언과 구현이 어긋나면 **없는\n * 것으로 읽는다**는 그 문 — 의 판정값을 **읽는 자리가 하나도 없었다.** 화면은 선언을 보고 있었고,\n * 어느 커넥터든 구현 없이 능력을 적기만 하면 가능한 것으로 보였다. 선언을 믿지 않으려고 만든 문을\n * 아무도 지나지 않은 셈이다.\n *\n * 커넥터 쪽에도 흔적이 남아 있었다 — `operato-plant` 주석이 「어댑터 목록은 선언한 값을 그대로\n * 보여 주므로 남겨 두면 화면이 없는 능력을 있다고 말한다」며 선언을 손으로 줄여 두었다. 사람이\n * 기억해서 맞추는 것은 다음 커넥터에서 끊긴다.\n */\nexport function listAdapters(): { type: string; meta?: AdapterMeta; capabilities?: ReferenceCapability[] }[] {\n return [...adapters.values()].map(a => ({ type: a.type, meta: a.meta, capabilities: capabilitiesOf(a) }))\n}\n"]}
1
+ {"version":3,"file":"reference-adapter.js","sourceRoot":"","sources":["../../../server/service/reference/reference-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ApprovedCommand, RefusalCode, RefusalParams } from '@operato/ops-contract'\n\nimport type { ReferenceStep } from './reference-progress.js'\nimport type { ReferenceMaster } from './reference-master.js'\n/*\n * `fetchLinks` 가 돌려주는 관계 그룹. 정의는 이것을 소비하는 쪽에 하나만 둔다\n * (`external-resolver.ts`). 여기서 같은 모양을 다시 선언하면 한쪽만 수정되는 날이 온다.\n */\nimport type { ExternalIncoming as ReferenceLinkGroup } from '../twin-model/external-resolver.js'\nexport type { ReferenceLinkGroup }\nimport { twinWarn } from '../../engine/log.js'\n\n/*\n * 레퍼런스 어댑터 계약 (reference-management P1) — 정적 마스터 등록(`registerReference`)을\n * \"어댑터 타입\" 등록으로 일반화한다. 레퍼런스 = 데이터(TwinReference 행), 어댑터 = 동작.\n * 아웃바운드 액추에이션은 **별개 어댑터가 아니라 이 어댑터의 반대 면**이다(`actuate`) — 같은 연결이\n * 사실을 읽고 조치를 내리므로 인증과 설정이 한 자리에 있어야 한다(command-routing §8.1).\n * seam: openLiveFeed 레코드 → 커널 face2-adapter.ingest → CanonicalEnvelope 까지가 여기,\n * 이후 projector 구동은 face2-inbound-live 소유.\n * 설계 SoT: operato-twin/design/plans/reference-management.md §2.2·§4.2.\n */\n\n/** 연결 설정(어댑터별 스키마) — 엔드포인트·자격·파라미터. */\nexport type ConnectionConfig = Record<string, any>\n\n/** discoverSites 가 반환하는 사이트/공장 서술자 — N 발견(§2.2). 1:1 은 길이 1 배열. */\nexport interface SiteDescriptor {\n /** 안정적 사이트 정체성 키(소스의 plant code 등) — 공간 정합 1차 키. */\n siteId: string\n name?: string\n /** 사이트별 시스템(소스가 시스템을 넘나들 때). 없으면 레퍼런스 system 사용. */\n system?: string\n geo?: { lat: number; lon: number }\n hint?: any\n}\n\n/** 연결 폼 필드 서술 — UI 가 이 스키마로 connectionConfig 입력 폼을 렌더한다(어댑터가 스키마 제공). */\nexport interface AdapterConfigField {\n key: string\n label: string\n placeholder?: string\n required?: boolean\n secret?: boolean // 비밀번호/토큰 — 마스킹 입력\n multiline?: boolean // JSON/매핑 등 여러 줄 입력(textarea)\n help?: string // 필드 아래 도움말\n default?: string // 폼 프리필 기본값(예시 매핑/URL 등)\n /*\n * 값이 정해진 목록 안에 있을 때 — 화면이 고르게 그린다(타이핑하지 않는다).\n *\n * 손으로 적는 칸에 닫힌 값을 받으면 오타가 그대로 저장되고, 그 오타는 오류로 나오지 않는다.\n * 발전소 종류(`sell` | `self-consumption`)가 그렇다 — 틀리면 계량 방향이 반대가 되는데 어디에도\n * 표시되지 않는다.\n *\n * `label` 은 번역 키를 쓸 수 있다(화면이 `meta()` 로 푼다). `value` 는 저장되는 값이라 번역하지 않는다.\n */\n options?: { value: string; label: string }[]\n /*\n * 값의 종류가 정해져 있지만 목록이 너무 커서 `options` 로 실을 수 없을 때 — 화면이 그 종류에 맞는\n * 고르는 칸을 그린다.\n *\n * 'timezone' IANA 시간대. 화면이 `Intl.supportedValuesOf('timeZone')` 로 목록을 만든다.\n * 400개가 넘고 표준이 갱신되므로 우리가 목록을 들고 있지 않는다.\n * 이름이 곧 값이자 표시라 번역하지 않는다.\n *\n * 자유 입력으로 두면 `Asia/seoul` 같은 오타가 저장되고, 그 오타는 오류로 나오지 않는다.\n */\n kind?: 'timezone'\n}\n/** 커넥터 메타데이터 — picker·연결 폼용 표시 정보. */\nexport interface AdapterMeta {\n label?: string\n domain?: string // 'wms' | 'yms' | 'mes' (분류)\n available?: boolean // false = 준비 중(picker 비활성)\n configSchema?: AdapterConfigField[]\n ootbNote?: string // OOTB 매핑 안내 문구\n /*\n * 근거 수준(정직성 1급 표기 — \"실제 API 명세에 근거하는가\"를 UI·피치가 오해 없이 보이게).\n * 'vendor-doc' = 벤더 공개 API 문서의 엔드포인트·필드명에 정합(docUrl 로 출처 명시). 실 시스템 검증은 별도.\n * 'standard' = 개방 표준(EPCIS 2.0 / ISA-95)에 정합 — 벤더 독립.\n * 'facsimile' = 시스템 어휘를 흉내낸 근사치(공개 문서 미정합). 실 연동 시 매핑 재정합 필요.\n * 미지정은 facsimile 로 간주(가장 보수적).\n */\n grounding?: 'vendor-doc' | 'standard' | 'facsimile'\n docUrl?: string // grounding='vendor-doc' 의 근거 문서 URL(출처 추적)\n}\n\n/**\n * 원본이 **선언하는 능력** — 「이 원본으로 무엇을 할 수 있나」 (ADR-0029 §8, 2026-08-19).\n *\n * ── 왜 능력으로 선언하나 ────────────────────────────────────────────────────\n * 실 WMS 는 스스로 사실을 낸다. 시뮬레이터는 **구동해 줘야** 움직인다 — 자극·속도·정지·초기화. 그런데\n * 계약에 그 자리가 없어서 통제가 `TwinEngine` 안에 뚫려 있었다(`controlTwinScenario` 가 런타임의 시나리오\n * 엔진을 직접 잡았다). 그러면 ADR-0029 의 판정 문장(*\"실물이 같은 계약으로 할 수 있는가\"*)에 걸린다:\n * 실 WMS 는 일시정지될 수 없으니 그 통제면은 영원히 「어댑터 규약의 결손」으로 남고, 규칙이 그 면을\n * **숨게** 만든다.\n *\n * 그래서 뒤집는다: 통제는 **선언된 능력**이다. 선언하지 않은 원본에는 그 문이 **아예 열리지 않는다** —\n * 그것이 결손이 아니라 **사실**이다(실 WMS 는 정지되지 않는다). 이 레포가 이미 그 규율로 굴러간다\n * (`capability-contract`: concrete 강결합 대신 능력 선언).\n *\n * · `live` — 돌아가는 피드를 낸다(`openLiveFeed`).\n * · `control` — 구동을 받는다(`control`) — 자극·속도·정지·초기화.\n */\n/**\n * 이 원본에 무엇을 요구할 수 있나.\n *\n * · `live` — 관측을 이어서 낸다\n * · `control` — 시뮬레이터를 움직인다(자극·배속·정지). 실 시스템은 갖지 않는다\n * · `actuate` — **승인된 조치를 받는다.** 현장에 작업지시가 나가는 유일한 축이다\n *\n * ── `actuate` 를 늦게 넣은 이유 (2026-09-02) ────────────────────────────────\n * 어댑터에 `actuate` 구현은 있었는데 이 축에 그 이름이 없었다. 그래서 셋이 동시에 조용했다.\n *\n * 화면 「이 원본이 지시를 받나」를 물을 수 없다 — 능력 목록에 그 이름이 없다\n * 등록 구현했는데 선언 안 한 것을 알려 주지 않는다 — 검사 목록에 그 이름이 없다\n * 넘김 리졸버가 어댑터를 찾지도 않고 무조건 거절했다 — 커넥터는 이미 만들어져 있었다\n *\n * 실제로 MES 커넥터가 `actuate` 를 다 만들어 두었는데, 넘기면 「구현이 없습니다」라고 답했다.\n * 자리는 있고 길이 없는 상태였고, 이 저장소에서 가장 자주 나는 결함 부류다.\n */\n/**\n * 커넥터가 선언하는 능력.\n *\n * `idempotent-actuation` 은 다른 셋과 성질이 다르다 — 무엇을 할 수 있나가 아니라 **두 번 해도\n * 같은가**다. 일꾼이 실패한 조치를 다시 넘길지 정할 때 이것만 본다(§`retryDecisionOf`).\n *\n * 선언하지 않으면 **거짓으로 읽는다.** 이 한 자리에서만 「모르면 안전한 쪽」이 거짓이고, 그\n * 안전한 쪽이 「재시도하지 않음」이다 — 참으로 가정하면 현장에 지시가 두 건 선다.\n *\n * 선언에는 근거가 있어야 한다. operato-plant 는 MES 가 `correlationId` 로 기존 행을 찾아 같은\n * 이름을 돌려주기 때문에 참이다 — 우리가 조심해서가 아니라 저쪽 동작이 그래서다. 저쪽이 그것을\n * 바꾸면 이 선언이 거짓이 된다.\n */\nexport type ReferenceCapability = 'live' | 'control' | 'actuate' | 'idempotent-actuation'\n\n/** 원본에 보내는 구동 명령 — **시뮬레이터를 움직이는 말**이지 트윈을 고치는 말이 아니다. */\nexport interface ControlCommand {\n /**\n * · `stimulus` — 자극을 싣는다(`scenario`). 선언은 원본 설정에 살고 이 명령은 지금 태우는 것이다.\n * · `start`·`pause` — 구동을 켜고 멈춘다.\n * · `speed` — 시각의 배속(`speed`).\n * · `reset` — 씨앗부터 다시(원본의 상태를 되돌린다 — 트윈의 저널은 재기동 정책이 정한다).\n */\n action: 'stimulus' | 'start' | 'pause' | 'speed' | 'reset'\n /** 자극 선언 — `action: 'stimulus'` 에만. 커널 규약(`validateScenario`)으로 검사된 값이어야 한다. */\n scenario?: any\n /** 배속 — `action: 'speed'` 에만. */\n speed?: number\n /**\n * 어느 트윈을 위한 구동인가 — 원본이 여러 트윈에 먹일 수 있으므로 대상을 함께 말한다.\n *\n * 인프로세스 시뮬레이터는 이 둘로 자기 런타임을 찾는다(테넌트까지 있어야 같은 id 의 두 트윈이 갈린다).\n * 원격 원본은 대개 무시한다(자기 세계가 하나다).\n */\n instanceId?: string\n domainId?: string\n}\n\n/** 구동 결과 — **왜 안 됐는지**를 코드로 말한다(화면이 옮긴다). */\nexport interface ControlResult {\n ok: boolean\n /** 언어중립 사유 — `not-declared`(능력 없음) · `unsupported-action` · `invalid`(선언 거절) · `no-target`. */\n code?: string\n detail?: string\n}\n\n/**\n * 한 흐름의 읽기 커서 — **어디까지 읽었나.**\n *\n * `since` 와 `seen` 이 **함께** 있어야 한다. 이어 읽기는 경계 시각을 겹쳐 읽고(`gte`), 그 겹침을\n * `seen` 이 걸러 낸다. `gt` 로 좁히면 같은 밀리초의 다른 행이 사라진다 — 들어온 것이 없는 누락이다.\n *\n * `seen` 이 예상보다 커지면(한 시각에 몰린 행 수가 늘면) 그것은 원본 쪽 신호다. 어댑터가 그 크기를\n * 말해야 한다 — 알리지 않고 커지게 두면 이어 읽기가 막히는 순간까지 아무도 모른다.\n */\nexport interface LiveFeedCursor {\n /** 그 흐름에서 마지막으로 본 시각(원본의 시각, ISO). */\n since?: string\n /** 그 시각에 이미 소비한 행 id — 겹쳐 읽은 것을 걸러 낸다. 저장되는 모양이므로 배열이다. */\n seen: string[]\n}\n\n/**\n * **트윈이 지금 들고 있는 물품 하나** — 붙는 커넥터가 견줄 근거(§`LiveFeedContinuity.observedItems`).\n *\n * 커널 어휘로 낸다. 원본의 낱말(그 시스템의 상태 코드)로 내지 않는 이유가 있다: 원본 낱말은 여러 개가\n * 커널의 한 값으로 모이므로(예: 네 상태가 「진행 중」 하나가 된다) 되돌릴 수 없다. 그리고 되돌릴\n * 필요도 없다 — 두 원본 상태가 커널에서 같은 값이면 **트윈이 든 사실은 바뀌지 않았다.**\n */\nexport interface ObservedItemFact {\n /** 트윈이 든 물품의 키 — 비직렬 재고는 `클래스@자리`. */\n id: string\n /** 클래스 식별자(원문). */\n epcClass?: string\n location?: string\n qty?: number\n uom?: string\n /** 표준 처분(CBV) — 원본 상태 코드가 아니다. */\n disposition?: string\n /** 유통기한(밀리초). */\n expiry?: number\n lot?: string\n}\n\n/**\n * 밀어 받은 본문 하나를 옮긴 결과 (2026-08-31).\n *\n * 레코드만 돌려주던 것을 넓혔다. 번호를 함께 받아야 프레임워크가 빠진 것을 알아챈다.\n *\n * **`scope` 가 없으면 `seq` 도 없는 것으로 다룬다.** 번호만 있고 그 번호가 어느 줄의 것인지 모르면\n * 커서를 무엇으로 잡을지 짐작해야 하고, 짐작한 열쇠는 보내는 쪽이 줄을 하나 더 늘리는 날 어긋난다.\n * 실제로 operato-plant 가 `[domain, channel]` 마다 번호를 매기면서 봉투에 `channel` 을 싣지 않고 있었다.\n */\n/** 번호 하나에 대해 원본이 아는 것. */\nexport interface SeqEntry {\n seq: number\n /**\n * `unknown` 은 결함이 아니다 — 채번을 따로 들지 않는 원본은 「행이 없다」에서 잃은 것과 아직 안\n * 간 것을 가를 수 없다. 그것을 `lost` 로 단정하면 사람이 없는 사실을 찾으러 간다.\n */\n state: 'present' | 'lost' | 'unissued' | 'unknown'\n /** 그 봉투의 발생 시각 — `present` 일 때만. 무엇이었는지 사람이 알아보는 데 쓴다. */\n at?: string\n /** 그 봉투 자체 — `present` 이고 원본이 낼 수 있을 때만. 없어도 `state` 는 참이다. */\n record?: unknown\n}\n\n/** 번호 구간에 대해 원본이 아는 것 — **원본이 아는 것만 낸다. 판정은 사람이 한다.** */\nexport interface SeqReport {\n scope: string\n entries: SeqEntry[]\n /** 상한에 닿아 구간을 다 못 냈다 — 알리지 않고 자르지 않는다. */\n truncated?: boolean\n}\n\nexport interface InboundBatch {\n /**\n * Receiver path for this batch. Omitted is FACTS, preserving every existing\n * connector; ATTENTIONS is a product-level analysis input and must not be\n * folded as a canonical fact.\n */\n lane?: 'FACTS' | 'ATTENTIONS'\n /**\n * 옮긴 것들. 우리와 무관한 본문이면 빈 배열이다.\n *\n * **한 요청에 봉투가 여럿 온다.** 밀어 주는 쪽은 한 건씩 보내지 않는다 — 번호도 봉투마다 붙는다.\n * 배치를 대표하는 번호 하나로는 가운데가 빠진 것을 알아채지 못한다.\n */\n items: InboundItem[]\n /**\n * 이 요청의 번호가 어느 줄의 것인가. 번호를 매기지 않는 원본은 주지 않는다.\n *\n * 요청 하나가 한 줄에만 속한다 — 한 요청에 두 줄을 섞으면 어느 줄의 어디까지 받았는지를 응답\n * 하나로 말할 수 없다.\n */\n scope?: string\n /**\n * (선택) **원본이 지금 가진 번호의 범위** — 우리 커서와 견주려면 이것이 있어야 한다.\n *\n * ── 왜 어댑터가 주나 (인티그레이션 레인 제안, 2026-09-06) ──────────────────\n * 「가장 낮은 번호」를 묻는 방법이 원본마다 다르다. operato-plant 는\n * `mesOutboxEvents(since: 0, limit: 1)` 이고, ppms·chef 는 아웃박스가 없어 번호 범위 자체가\n * 없다. 프레임워크가 이것을 알 방법이 없다.\n *\n * ── 모르면 주지 않는다 ────────────────────────────────────────────────────\n * **0 이나 추측을 넣지 않는다.** 0 을 넣으면 「원본이 1번부터 다 갖고 있다」로 읽히고, 그것이\n * 거짓이면 「우리가 다 받았다」는 잘못된 결론이 나온다.\n *\n * `last` 에는 조건이 하나 붙는다. 쪽 상한에서 멈췄으면 그때의 번호는 **원본의 끝이 아니라\n * 「여기까지 읽었다」**이므로 주지 않는다 — 주면 「원본에 더 없다」로 읽히는데 실제로는 더 있다.\n */\n sourceRange?: { first?: number; last?: number }\n}\n\nexport interface InboundItem {\n /** 우리 어휘로 옮긴 레코드 하나. */\n record: unknown\n /**\n * 원본이 이 봉투에 붙인 id — **떨어졌을 때 그쪽이 자기 행을 찾는 열쇠다.**\n *\n * ── 왜 필요한가 (2026-09-07 측정) ──────────────────────────────────────────\n * 훅 응답의 떨어진 목록이 `{ record, errors }` 였고, plant 은 그것을 **봉투 id 문자열 목록**으로\n * 읽고 있었다. `Array.isArray` 는 통과하니 그쪽 guard 도 안 걸렸다 — id 를 맞추는데 객체라 하나도\n * 안 맞고 **묶음 전체가 「보냈음」으로 찍혔다.** 422 와 `ok: false` 를 보냈는데도 그쪽 행은 재시도\n * 0 · 오류 없음이었다.\n *\n * 이 값을 주면 응답이 `{ eventId, errors }` 로 나가고 그쪽이 그 행만 표시할 수 있다. 안 주면\n * 레코드가 그대로 실려 나간다 — 무엇이 떨어졌는지 알 길이 그것뿐이고, 빈 id 를 실으면 보내는 쪽이\n * 아무 행도 못 찾으면서 「알았다」고 여긴다.\n */\n eventId?: string\n /**\n * 이 봉투의 번호.\n *\n * **한 배치 안에서 있거나 없거나 하나로 통일한다.** 섞이면 빠진 것이 있는지 판단할 근거가 없다.\n */\n seq?: number\n /**\n * 이 봉투가 말하는 **발생 시각** — 레코드가 자기 시각을 안 말할 때 쓴다.\n *\n * ── 왜 필요한가 (2026-09-06 측정) ─────────────────────────────────────────\n * 시각을 안 싣는 레코드는 `defaultEventTime`(= ingest 시각)으로 떨어진다. 그 값이 **실행할 때마다\n * 다르다.** 그런데 fact identity 가 시각을 포함하므로, 같은 사실을 다시 받으면 dedupe 가 안 되고\n * 새 사실로 앉는다.\n *\n * 실제로 그렇게 됐다. `fillTwinScope(from: 0)` 을 두 번 돌렸더니 mes-line-a 저널이\n * 40 → 69건이 되고 중복이 27가지 생겼다. 시각을 말하는 레코드 12건만 되풀이로 걸러졌다.\n *\n * ── 봉투는 그 시각을 알고 있었다 ──────────────────────────────────────────\n * plant 아웃박스가 `eventTime` 을 실어 보낸다. 그것이 갈 자리가 없어서 버려지고 있었다 —\n * connector 주석이 그 위험을 적어 두고 자리를 요청해 두었다(`operato-plant.ts`).\n *\n * **레코드가 자기 시각을 말하면 그것이 이긴다.** 봉투의 시각은 「그 사실을 언제 보냈나」에 가깝고,\n * 레코드의 시각은 「언제 일어났나」다. 둘이 다르면 뒤엣것이 맞다.\n */\n at?: string\n}\n\n/**\n * 재기동을 넘어 이어지는 읽기 상태 — 참조 계층이 소유하고 어댑터에 건넨다(§`openLiveFeed`).\n *\n * 흐름 열쇠는 **어댑터가 정한다**(`Record`). 계약이 이름을 닫으면 새 원본마다 계약을 고치게 된다.\n */\nexport interface LiveFeedContinuity {\n /**\n * The name to take a task lease under, so only one instance polls this feed.\n *\n * ── Why this layer composes it ────────────────────────────────────────────\n * `TaskLease` has no domain column on purpose: what is made exclusive is a\n * loop, not a tenant's slice of one, and a task that does need to be\n * per-tenant puts the tenant in the name so the unique index keeps meaning\n * what it says.\n *\n * An adapter cannot follow that rule. It is handed `cfg` and `site`, and\n * neither carries a domain — so a name built from `siteId` alone would let\n * two domains that happen to use the same plant code block each other's\n * channel. Silently, and looking exactly like \"someone else holds it\".\n *\n * So the name is composed here, where `domainId` and `instanceId` are known,\n * and the rule lives in one place.\n *\n * ── Always present, unlike `cursor` ───────────────────────────────────────\n * A feed needs its lease on the very first attach, before there is any\n * cursor to carry. So this object is now handed over even when nothing has\n * been read yet; `cursor` being absent is what still says \"first attach\",\n * which is how every connector already reads it (`continuity?.cursor`).\n */\n leaseName: string\n /** 지난번에 어디까지 읽었나 — 없으면 첫 붙음이다(그때만 되돌아볼 날수로 창을 만든다). */\n cursor?: { streams?: Record<string, LiveFeedCursor>; firstAttachedAt?: string }\n /**\n * **트윈이 지금 들고 있는 물품** — 붙는 커넥터가 「무엇이 바뀌었나」를 견줄 근거 (2026-08-28).\n *\n * ── 무엇을 고치나 ───────────────────────────────────────────────────────────\n * 전량 절대값을 읽는 커넥터는 지난 주기와 견주어 **바뀐 것만** 낸다. 그 비교 표가 프로세스 안에만\n * 있어서, 재기동하면 커넥터가 자기 기억을 잃고 **전량을 「바뀐 것」으로 판정**한다. 승화푸드에서 그\n * 한 번이 2,665건이었고, 재기동마다 지난 기록을 덮었다.\n *\n * **트윈은 잃지 않는다** — 저장본에서 되세우고 뜬다(실측 로그: `items 2665 · orders 4794`). 잃는\n * 쪽은 커넥터다. 그래서 트윈이 아는 것을 건네면 첫 주기부터 바뀐 것만 나간다.\n *\n * 없으면 `undefined` 다 — 그것이 「트윈이 아무것도 모른다」는 사실이고, 그때만 전량이 새 사실이다.\n * 빈 배열로 메우지 않는다(그러면 「모른다」와 「없다」가 같아진다).\n */\n observedItems?: ObservedItemFact[]\n /**\n * 커서가 움직였다고 알린다 — 이 층이 저장한다.\n *\n * 어댑터가 **매 행마다 부르지 않는다**: 저장은 이 층의 몫이고, 잦으면 이 층이 눌린다. 흐름 하나가\n * 한 묶음을 소비한 뒤 한 번이면 충분하다.\n */\n onCursor?: (cursor: { streams: Record<string, LiveFeedCursor>; firstAttachedAt?: string }) => void\n /**\n * **원본에 닿지 못했다** — 어댑터가 읽기 실패를 알린다(2026-08-23).\n *\n * ── 무엇이 아무 신호도 내지 않았나 ─────────────────────────────────────────────────────────\n * 실 원본이 끊겼을 때(접속 시간 초과) 트윈의 **조회 가능한 상태 어디에도** 그 사실이 없었다.\n * `onRecords` 가 불리지 않으면 유입 장부에 아무 일도 일어나지 않고, `TwinReference.lastError` 는\n * 마스터 동기·접속 시험에만 적힌다. 그래서 화면이 볼 수 있는 것은 「새 사실이 없다」뿐이었고,\n * 그것은 **「연결된 시스템에서 들어온 것이 없다」와 구별되지 않는다.**\n *\n * 어댑터는 로그로 말하고 있었다(재시도 경고). 그러나 **로그는 사람이 볼 때만 값이 있다** — 화면이\n * 말하려면 상태에 있어야 한다. 이 통로가 그 자리다.\n *\n * ── 무엇을 알리고 무엇을 알리지 않나 ────────────────────────────────────────\n * **재시도를 다 쓰고 그 주기를 포기했을 때** 부른다 — 재시도마다 부르면 한 번의 흔들림이 단절로 보인다.\n * 흐름 여럿을 읽는 원본은 흐름 이름을 함께 준다(어느 표가 막혔나가 원인 찾기의 절반이다).\n *\n * **빈 읽기는 실패가 아니다** — 원본이 「새 것이 없다」고 답한 것은 닿았다는 뜻이므로 `onRecords([])`\n * 로 알린다. 그 둘을 섞으면 들어온 것이 없는 연결이 끊긴 원본으로 보인다(고치려던 것의 반대 방향으로 틀린다).\n *\n * ── 무엇을 어댑터가 보내고 무엇을 이 층이 세나 (2026-08-27) ─────────────────\n * 어댑터는 **이유**만 보낸다 — 그리고 물러섰다면 다음에 다시 물을 때까지의 시간을 함께 보낸다.\n *\n * 연달아 몇 번 실패했는지와 언제부터 실패하고 있는지는 **이 층이 이미 센다**(`recordReadFailure`).\n * 그것을 어댑터가 함께 보내면 같은 수를 두 곳에서 세게 되고, 갈라지면 어느 쪽이 사실인지 알 수 없다.\n *\n * `reason` 에 **완성된 문장을 담지 않는다.** 이 값은 화면에 그려지고, 화면은 다섯 언어로 그린다.\n * 「3회 연달아 …부터 읽지 못했습니다」처럼 한 언어의 문장을 넣으면 그 문장은 번역되지 않고, 이 층이\n * 세는 수와 어긋날 수도 있다. 이유 하나만 짧게 준다(접속 시간 초과 · 401 · 형식 오류).\n */\n onReadFailure?: (info: {\n reason: string\n stream?: string\n /** 다음에 다시 물을 때까지 남은 시간(ms) — 물러섰을 때만 준다. */\n nextRetryMs?: number\n }) => void\n /**\n * **끊겼다가 돌아왔다** — 어댑터가 회복을 알린다 (2026-08-27).\n *\n * ── 왜 따로 알려야 하나 ─────────────────────────────────────────────────────\n * 읽기가 다시 성공하면 실패 기록은 지워진다. 그것만 하면 「끊긴 적이 있었다」가 화면에서 사라지고,\n * 밤새 두 시간 끊겼던 연결과 한 번도 끊기지 않은 연결이 아침에 똑같이 보인다.\n *\n * 읽기 성공(`onRecords`)만으로 이 층이 회복을 유추하지 않는다 — 몇 번 실패한 뒤였는지와 얼마나\n * 끊겨 있었는지는 재시도를 관리하는 어댑터가 안다.\n */\n onRecovered?: (info: { afterFailures: number; downMs: number }) => void\n /**\n * **읽었는데 창을 넘길 수 없다** — 어댑터가 커서 정체를 알린다 (2026-08-24).\n *\n * ── 왜 `onReadFailure` 와 갈라야 하나 ───────────────────────────────────────\n * 이 문을 만들기 전에는 두 사실이 **같은 이름으로** 나갔다.\n *\n * 원본에 닿지 못했다 접속 실패·시간 초과·형식 오류 → 기다리면 풀린다\n * 읽었는데 커서가 못 넘어간다 한 시각에 한 페이지보다 많은 행이 몰려 있다 → **기다려도 안 풀린다**\n *\n * 둘째는 **읽기가 성공한 실패**다. 원본은 답했고, 그 답의 모양이 커서를 이긴 것이다. 그런데 화면이\n * 「원본에 닿지 못한다」고 말하면 사람을 반대 방향으로 보낸다 — 원본을 의심하고 기다린다. 실제로\n * 필요한 조치는 **페이지를 키우거나 같은 시각 안에서 순서를 정하는 것**이고, 기다림으로는 영원히\n * 풀리지 않는다.\n *\n * 조치가 반대인 두 사실을 한 이름으로 부르면, 그 이름은 정보가 아니라 오해다.\n *\n * ── 무엇을 알리나 ───────────────────────────────────────────────────────────\n * 그 주기를 포기했을 때 부른다(`onReadFailure` 와 같은 규율). 어느 흐름인지 알면 함께 준다 —\n * 밀도가 높은 표는 원본마다 다르므로 그 이름이 조치의 절반이다.\n *\n * **닿지 못한 것과 섞어 부르지 않는다.** 하나의 주기가 두 이유로 실패할 수는 없다(먼저 닿아야 읽는다).\n */\n onCursorStall?: (info: { reason: string; stream?: string }) => void\n /**\n * **원본에 있는데 세우지 않은 것을 알린다** — 이유와 수.\n *\n * ── 왜 이 통로가 필요한가 (2026-08-24) ─────────────────────────────────────\n * 어댑터가 원본의 사실 일부를 **일부러 받지 않는 일이 정상이다**(입자가 안 맞는다 · 받으면 상태가\n * 거짓이 된다 · 커널 어휘로 옮길 수 없다). 그때 어댑터는 경고를 냈는데, 그 경고가 **프로비저닝\n * 화면에서 한 번 스쳐 지나갈 뿐**이었다 — 그 뒤로는 어디에서도 볼 수 없었다.\n *\n * 그래서 사용자가 「원본보다 이것이 적다」를 물으면 답이 어디에도 없었다. 그 사실은 한 번의 사건이\n * 아니라 **상태의 성질**이다: 세우지 않은 것은 **지금도** 트윈에 없다.\n *\n * **주기마다 불러도 된다** — 장부가 이유로 묶어 세고 마지막 수로 덮는다(누적하지 않는다). 그래서\n * 「지금 세우지 않은 것이 8건」이 8로 남고 800으로 부풀지 않는다.\n *\n * **풀리면 부르지 않는 것으로 끝나지 않는다** — 이유가 사라졌으면 `count: 0` 으로 부르는 것이 아니라\n * 그 이유를 더 이상 보내지 않으면 된다… 가 **아니다.** 장부는 마지막 값을 들고 있으므로, 풀린 것을\n * 알리려면 그 이유로 `count: 0` 을 한 번 보내라(그때 줄이 사라진다). 알리지 않고 그치면 낡은 수가 남는다.\n *\n * 이유는 **자유 문자열**이다 — 무엇을 왜 안 받는지는 원본마다 다르고, 계약이 목록을 닫으면 새 원본을\n * 붙일 때마다 계약을 고치게 된다(흐름 열쇠와 같은 규율). 사람이 읽을 문장으로 적어라.\n */\n onWithheld?: (info: { reason: string; count: number }) => void\n}\n\n/**\n * 저쪽이 조치를 받을 준비가 됐나 — **「모른다」를 「안 된다」로 답하지 않는다.**\n *\n * `ready` 가 거짓이면 `reason` 이 사람의 말로 무엇이 없는지 말하고, `code` 가 화면이 다음 할 일을\n * 가를 값이다(문장을 파싱하지 않게 — 번역하면 문장이 바뀐다).\n */\nexport interface ActuationReadiness {\n /** 받을 수 있어 보이나. **모르면 `undefined`** — 거짓이 아니다. */\n ready?: boolean\n /** 어디까지 봤나. `local` = 우리 설정만 봤다(망을 타지 않았다). `remote` = 저쪽에 물었다. */\n checked: 'local' | 'remote'\n /** 무엇이 없나 — 사람의 말로. */\n reason?: string\n /** 화면이 가를 값. 커넥터가 정한다(예: `no-endpoint` · `no-secret` · `unreachable` · `not-configured`). */\n code?: string\n}\n\nimport type { LiveCadence } from './live-cadence.js'\n\n/**\n * 조치를 넘긴 결과 — **어댑터가 돌려주는 것.**\n *\n * 이름을 붙인 이유는 두 가지다. 익명 타입이라 소비처가 그 모양을 말할 수 없었고(`actuate` 를 감싸는\n * 쪽이 반환형을 다시 적어야 했다), 그리고 **거절 사유를 코드로 실을 자리가 없었다** — `error` 는 사람\n * 문장이라 저장하면 번역과 판독이 함께 굳는다.\n *\n * `errorCode`·`errorParams` 는 `CommandAck` 과 **같은 낱말**이다(ADR-0037 ④ · ADR-0042 ③). 셋째 이름을\n * 만들지 않는다 — 같은 것을 세 이름으로 부르면 매핑 표가 생기고, 그 표는 커넥터가 새 코드를 내는 날\n * 「알 수 없는 사유」로 떨어진다.\n */\nexport interface ActuationResult {\n ok: boolean\n ref?: string\n /**\n * **왜 거절됐나 — 언어 중립 코드.** 저장되는 값은 이것이고 `error` 는 아니다.\n *\n * `lastError` 가 사람 문장을 저장해 두고 화면이 그것을 그대로 뿌렸다. 그러면 번역도 못 하고,\n * 무엇이 났는지 세지도 못하고, 문장을 고치는 순간 옛 행과 새 행이 달라진다.\n */\n errorCode?: RefusalCode\n /** 그 코드가 가리키는 원시값들 — 표현층이 문장에 끼운다. */\n errorParams?: RefusalParams\n /**\n * **받아들였으나 남은 것이 있다** — 실패가 아니다. 사람이 저쪽에서 해야 할 일이 있으면 여기 적는다.\n *\n * `error` 에 적지 말 것 — 실패로 읽혀 커맨드가 `failed` 로 앉고, 다시 넘기게 된다. 실제로 MES 가\n * 「지시서를 못 붙였다」를 답했을 때 커넥터가 적을 칸이 없어 로그로만 남겼고, 트윈 쪽에서는 성공한\n * 조치와 구별되지 않았다.\n */\n note?: string\n /**\n * 그 말 중 **다음에 할 일** 한 줄 — 원인은 위 `note` 다.\n *\n * 붙여 보내지 말 것. 읽는 사람은 「그래서 내가 뭘 해야 하나」를 먼저 찾고, 한 문장으로 오면\n * **화면이 자르게 되며 자르는 규칙이 화면마다 생긴다.** 어댑터는 이미 둘로 알고 있다.\n */\n noteNext?: string\n error?: string\n /**\n * **다시 해서 될 일인가** — 실패했을 때만.\n *\n * again 그대로 다시 해 볼 만하다 못 닿았거나 저쪽이 잠깐 흔들렸다\n * after-fix 사람이 고친 뒤 그대로 나간다 설정 문제\n * never 이 조치로는 영원히 안 된다 지시 내용이 틀렸다\n *\n * 가르는 자리는 **「조치를 다시 낼 필요가 있나」**다. 설정이 틀린 것은 조치가 멀쩡하므로\n * `after-fix`, 지시 내용이 틀린 것은 그 조치가 영원히 틀렸으므로 `never` 다.\n *\n * **문장에 담지 말 것** — 일꾼이 그것을 쓰려면 파싱해야 하고, 번역되면 깨진다.\n *\n * 말하지 않으면 일꾼이 **집지 않는다.** 모르는 것을 `never` 로 접으면 고칠 수 있는 것을 사람이\n * 포기하고, `again` 으로 접으면 없는 자재를 끝없이 두드린다.\n */\n retry?: 'again' | 'after-fix' | 'never'\n}\n\nexport interface ReferenceAdapter {\n /** 레지스트리 키 (예: 'virtual' | 'sap-ewm' | 'custom-rest'). */\n type: string\n /** picker·연결 폼용 메타데이터(선택). 없으면 type 만 노출. */\n meta?: AdapterMeta\n /** 연결 확인 — UI '연결 테스트'. */\n testConnection(cfg: ConnectionConfig): Promise<{ ok: boolean; error?: string }>\n /** N 발견 — 사이트/공장 열거. 단일 소스는 길이 1 반환. */\n discoverSites(cfg: ConnectionConfig): Promise<SiteDescriptor[]>\n /** 사이트별 마스터(구조) — 기존 masterToTwin 이 소비. */\n /**\n * 이 어댑터가 마스터를 읽으며 **보낼 단계를 미리 선언한다** — 그것이 진행률의 분모다.\n *\n * 표준 낱말만 쓴다(`REFERENCE_STEPS`). 선언하지 않으면 화면은 퍼센트를 보이지 않고 단계 이름만\n * 보인다 — **모르는 진행률을 지어내지 않는다.**\n *\n * 원본에 없는 자리는 선언하지 않는다. 그러면 「이 원본으로 채울 수 있는 만큼」이 곧 분모가 된다.\n */\n masterSteps?: readonly ReferenceStep[]\n\n /**\n * 이 원천이 **축마다 언제·얼마나 자주 내놓나** — 유입 건강이 이 선언으로 견준다.\n *\n * 선언하지 않은 축은 **지금까지처럼** 창 하나(10분)로 견준다. 그러면 밤에 발전하지 않는\n * 태양광과 업무시간만 도는 원천이 **정상인데 매일 밤 빨갛게** 난다 — 거짓 빨강이 쌓이면 진짜\n * 빨강도 같이 안 읽힌다.\n *\n * `masterSteps` 와 같은 규율이다: **어댑터만 자기 시간의 결을 안다.** 호스트가 짐작하면 그것은\n * 지어낸 판정이다.\n *\n * 자세한 것은 §`live-cadence.ts` — 특히 「해 있는 동안」을 **고정 시각으로 박지 말 것**\n * (오늘 잰 일몰은 두 달 뒤에 틀리다).\n */\n liveCadence?: readonly LiveCadence[]\n\n /**\n * @param onStep 진행을 알리는 통로 — **선택이다.** 주지 않으면 예전과 같이 동작한다.\n *\n * 마스터 읽기는 원본에 여러 질의를 보내는 긴 작업이고(창고·자리·설비·공정·경로·품목·BOM), 화면은 그\n * 사이에 아무것도 알 수 없었다. 어댑터만 자기 단계를 아므로 어댑터가 말해야 한다 — 호스트가 짐작하면\n * 그것은 지어낸 진행률이다.\n *\n * `total` 은 **그 어댑터가 보낼 단계 수**다. 모르면 주지 않는다(화면이 퍼센트를 만들지 않는다).\n */\n fetchMaster(\n cfg: ConnectionConfig,\n site: SiteDescriptor,\n onStep?: (step: { key: ReferenceStep; done: number; total?: number; detail?: string }) => void\n ): Promise<ReferenceMaster>\n /**\n * (선택) 사이트별 실 이벤트 스트림 — 없으면 sim. onRecords 로 레코드 push, unsubscribe 반환.\n *\n * ── 네 번째 인자: **읽기 커서** (2026-08-23) ────────────────────────────────\n *\n * 왜 이 층이 커서를 드나: 커널이 그 갈림을 이미 적어 두었다(§`hydrateContinuity`) — 「원천이 **애초에\n * 다시 말해 주지 않는 축**」은 재기동 연속성으로 이어받는다. **「우리가 어디까지 읽었나」가 정확히 그\n * 성질이다.** 원본은 그것을 되풀어 주지 않는다.\n *\n * 커서가 없던 동안 어댑터는 붙을 때마다 `Date.now() − 되돌아볼 날수` 로 창을 새로 만들었다. 그래서\n * **재기동마다 미러의 과거가 잘렸다** — 실측으로 작업 2,855 → 2,820(8시간 흐른 만큼). 미러가 아는\n * 것이 「원본의 사실」이 아니라 「창의 함수」였다.\n *\n * 그런데 미러의 상태는 **원천으로 서야 한다**(같은 절: 「미러의 진실은 원천이다 … 심으면 떠난 물건이\n * 되살아난다」). 그러니 저널을 계산해 상태를 세우는 것으로 고칠 일이 아니다 — 그 선언의 **전제**가\n * 「원천이 다시 말해 준다」이고, 창이 좁아지면 그 전제가 깨진다. 창을 고치는 것이 그 원칙을 지키는 길이다.\n *\n * ── 계약의 모양 ─────────────────────────────────────────────────────────────\n * · **흐름 열쇠는 어댑터가 정한다.** 원본마다 흐름 수와 뜻이 다르다(이 원본은 재고·오더·로트·투입 넷).\n * 계약이 이름을 닫으면 새 원본마다 계약을 고치게 된다 — 원본의 스키마가 계약을 끌고 가는 그 모양이다.\n * · **저장되는 모양으로 넘긴다**(`seen` 은 배열). 이 층은 저장·복원만 하고 안을 해석하지 않는다.\n * `Set` 을 받으면 이 층이 직렬화를 알게 되고, 커서 모양이 바뀔 때 두 곳을 고친다.\n * · `firstAttachedAt` — **첫 창을 만든 시각.** 이후 재기동에서 바뀌지 않는다. 커널은 아는 구간의\n * 상한만 안다(`nowTime` = 마지막으로 들은 시각). 이 값이 하한이고, 둘이 「이 트윈이 아는 구간」이다.\n * 화면이 수를 보일 때 그 구간을 함께 말해야 한다 — 말하지 않으면 알리지 않고 자르는 것이 된다.\n * · **커서가 없을 때만** 되돌아볼 날수를 쓴다. 그 순간이 `firstAttachedAt` 이다.\n *\n * 주지 않는 호출자와도 함께 선다(선택 인자) — 커서를 모르는 배포는 예전처럼 동작한다.\n */\n /**\n * (선택) **연결된 시스템이 밀어 준 것**을 우리 레코드로 옮긴다 — 웹훅.\n *\n * ── 왜 `openLiveFeed` 와 나누나 (2026-08-26) ───────────────────────────────\n * `openLiveFeed` 는 「가져오기」와 「옮기기」를 한 함수에 담고 있다. 그래서 가져오는 방식이 바뀌면\n * (그쪽이 밀어 주면) 옮기는 코드를 쓸 수 없었다 — 같은 파일 안에 있는데 밖에서 부를 수 없다.\n *\n * 이 함수는 **옮기기만** 한다. 밖에서 받은 본문을 받아 우리 레코드 목록을 돌려준다. 입구·인증·유입은\n * 프레임워크가 맡는다(§`reference-hook`).\n *\n * ── 옮길 수 없으면 던진다 ─────────────────────────────────────────────────\n * 빈 목록과 「옮길 수 없다」는 다르다. 빈 목록은 「우리와 무관한 것이 왔다」이고, 던지는 것은 「그쪽\n * 모양이 우리가 아는 것과 다르다」다. 그 둘을 같게 답하면 그쪽 모양이 바뀐 것을 아무도 모른다.\n *\n * ── 판단은 여기서 한다 ────────────────────────────────────────────────────\n * 통신이 끊긴 설비의 값을 보내지 않는 것, 잰 시각이 나아가지 않은 줄을 거르는 것 — 폴링에서 하던\n * 판단을 여기서도 한다. 그래야 두 길이 같은 답을 낸다.\n *\n * ── 번호는 프레임워크가 본다 (2026-08-31) ─────────────────────────────────\n * 빠진 번호를 알아채는 일은 밀어 주는 모든 연결에 같은 규율이라야 한다. 커넥터마다 만들면 한 곳이\n * 빠지고, 빠진 그 연결에서만 사실이 없어진다. 그래서 커넥터는 **번호를 꺼내 주기만** 하고 판정은\n * 프레임워크가 한다(§`reference-hook`).\n */\n handleInbound?(cfg: ConnectionConfig, site: { siteId: string }, body: unknown, headers: Record<string, unknown>): InboundBatch\n\n /**\n * **현장에 조치를 내린다** — 인바운드의 반대 면(§`command-routing.md` §8.1).\n *\n * 별개 어댑터를 두지 않는다. 같은 연결이 사실을 읽고 조치를 내리므로, 인증과 설정이 한 자리에 있어야\n * 한다. 둘로 나누면 같은 시스템에 두 벌의 연결 설정이 생긴다.\n *\n * ── 승인은 이미 지났다 (2026-08-31) ───────────────────────────────────────\n * 인자가 `ApprovedCommand` 다 — 그 표식은 승인을 지난 커맨드에만 붙고, 디스패처가 붙인다\n * (§`command-dispatcher.ts`). **어댑터는 승인을 다시 확인하지 않는다.** 확인이 두 곳에 있으면\n * 규칙이 두 벌이 되고 한쪽만 고쳐지는 날이 온다.\n *\n * 이 자리를 게이트보다 먼저 열지 않은 이유가 있다 — 자리가 있으면 누군가 부르고, 그때 사람 승인 없이\n * 현장에 작업지시가 나간다. 작업지시는 취소해도 이미 만든 것이 남는다.\n *\n * ── 돌려줄 것 ─────────────────────────────────────────────────────────────\n * `ref` 를 **반드시** 돌려준다. 그것이 없으면 넘긴 것이 저쪽에서 무엇이 되었는지 되짚을 수 없고,\n * 되돌릴 때 무엇을 되돌릴지 말할 수 없다.\n */\n actuate?(cfg: ConnectionConfig, site: SiteDescriptor, command: ApprovedCommand): Promise<ActuationResult>\n\n /**\n * **저쪽이 조치를 받을 준비가 됐나** — 보내기 전에 묻는다. 선택이다.\n *\n * ── 왜 필요한가 (2026-09-03 실측) ──────────────────────────────────────────\n * 승인된 조치를 넘겼더니 저쪽이 503 을 답했다 — **저쪽 프로세스에 비밀값이 실려 있지 않았다.**\n * 트윈 쪽 연결 설정에는 있었고, 주소도 맞았고, 서명도 맞았다. 저쪽이 재기동되면서 환경 변수에만\n * 살던 값을 잃은 것이다.\n *\n * 그것을 보내 보기 전에는 알 수 없었다. 그래서 사람이 승인 화면까지 가서 누르고, 실패를 보고,\n * 다시 로그인했다. **승인이 사라지지는 않는다**(`failed → dispatch` 가 열려 있다) — 없어진 것은\n * 사람의 시간이다.\n *\n * ── 문이 아니다. 알림이다 ──────────────────────────────────────────────────\n * **이 답으로 넘김을 막지 않는다.** 막으면 새 실패 방식이 생긴다 — 점검이 틀렸거나 잠깐 못 닿은\n * 사이에, 성공할 수 있었던 조치가 못 나간다. 화면이 「지금 저쪽이 못 받는 것으로 보입니다」를\n * 미리 말하는 데까지가 이 얼굴의 일이다.\n *\n * ── 두 겹을 구별한다 ──────────────────────────────────────────────────────\n * 우리 쪽에 무엇이 없는 것(주소·비밀값을 설정하지 않았다)은 **망을 타지 않고** 알 수 있다.\n * 저쪽이 받을 준비가 됐는지는 물어야 안다. 앞엣것만 답하고 뒤엣것을 안 물어도 되고, 그때\n * `checked: 'local'` 로 그 사실을 말한다 — 「살아 있다」고 말한 적 없는 것과 「죽었다」는 다르다.\n *\n * 화면이 그릴 때마다 부를 수 있으므로 **망을 타는 구현은 짧게 끝내야 한다.**\n */\n actuationReadiness?(cfg: ConnectionConfig, site: SiteDescriptor): Promise<ActuationReadiness>\n\n /**\n * **놓친 구간을 backfill 한다** — 밀어 주는 길의 짝.\n *\n * ── 왜 필요한가 (2026-08-31, 인티그레이션 레인 지적) ──────────────────────\n * 훅은 반드시 놓친다 — 우리가 내려가 있을 때, 그쪽이 못 보냈을 때, 네트워크가 끊겼을 때. 받는 쪽이\n * 번호로 그것을 **알아채게** 되었지만(§`takeInSequence`), 알아챈 뒤 **그 사이를 채우는 길이 없었다.**\n * 설계가 그 길을 적어 두었는데 만들지 않았다.\n *\n * ── 왜 `since` 가 번호인가 ────────────────────────────────────────────────\n * 커서에 사는 것이 번호이고, 보내는 쪽이 되풀어 주는 단위도 번호다. 시각으로 두면 두 축이 섞이고,\n * 같은 밀리초의 두 행이 갈리지 않는다.\n *\n * ── 왜 `scope` 를 프레임워크가 주나 ───────────────────────────────────────\n * 그것이 커서 열쇠의 절반이다. 커넥터가 만들면 열쇠를 만드는 자리가 둘이 되고, 어긋난 그곳은 조용하다.\n * 그래서 프레임워크가 커서에서 읽어 그대로 넘긴다.\n *\n * 답이 `InboundBatch` 인 이유도 하나다 — **backfill 한 것이 밀어 받은 것과 같은 번호 검사를 지난다.**\n * 다른 모양으로 돌려주면 그 검사를 다시 만들게 되고, 두 길이 다른 규칙으로 받는다.\n */\n fetchSince?(cfg: ConnectionConfig, site: SiteDescriptor, scope: string, since: number): Promise<InboundBatch>\n\n /**\n * (선택) **원본에만 있는 관계를 조회한다** — 트윈으로 복제하지 않고 그때그때 물어본다.\n *\n * ── 왜 필요한가 ─────────────────────────────────────────────────────────────\n * 트윈은 표준 어휘로 옮길 수 있는 것만 담는다. 원본에는 그 밖의 관계가 있다 — MES 의 생산 실적,\n * 설비 상태 구간, 정비 계획. 설비 상세 화면에서 사람이 실제로 묻는 것이 그런 것들이다.\n *\n * 이것을 트윈으로 복제하면 원본의 표를 전부 미러링하게 된다. 조회용 참조는 복제하지 않고 그때\n * 물어보는 것이 맞다.\n *\n * ── 왜 어댑터가 답하나 ──────────────────────────────────────────────────────\n * 어느 테이블에 무엇이 있는지는 원본마다 다르다. 커널이 그것을 알면 원본 하나의 스키마에 묶이고,\n * 다음 원본이 다른 구조를 쓰면 커널을 또 고친다.\n *\n * 어댑터는 이미 그 원본에 붙는 방법과 인증을 갖고 있다. 연결 설정이 한 자리에 있어야 한다는 것은\n * `actuate` 를 별개 어댑터로 두지 않은 것과 같은 이유다.\n *\n * ── 실패하면 그것을 알린다 ──────────────────────────────────────────────────\n * 예외를 던져도 됩니다. 호출하는 쪽이 잡아서 「조회 실패」로 표시하고, 나머지 관계는 그대로\n * 표시합니다. 빈 배열은 「가리키는 것이 없다」는 뜻이고 실패와 다릅니다.\n *\n * @param axis `equipment` · `locations` 등 계약의 축 이름\n * @param itemId 그 축에서의 식별자. 원본이 아는 이름이다(설비는 `ops_equipment.name`)\n */\n fetchLinks?(cfg: ConnectionConfig, site: SiteDescriptor | undefined, axis: string, itemId: string): Promise<ReferenceLinkGroup[]>\n\n /**\n * (선택) **그 번호가 무엇이었나** — 구멍을 만났을 때 묻는다.\n *\n * ── 왜 필요한가 (2026-09-06 실물) ──────────────────────────────────────────\n * 커넥터가 구멍을 만나면 「418 다음이 420」이라고만 말한다. **419 가 무엇이었는지 알 방법이\n * 없다.** 그날 plant 레인이 자기 아웃박스를 손으로 뒤져서 그것이 부하 시험의 흔적임을 찾았다.\n *\n * 구멍은 채널을 세운다 — 그 뒤의 사실이 통째로 못 온다. 그래서 「이게 무엇이었나」가 급한 물음인데\n * 물을 자리가 없었다.\n *\n * ── 세 가지를 가른다. 넷째는 「모른다」다 ──────────────────────────────────\n * ```\n * present 그 행이 있다 — 봉투를 함께 낸다\n * lost 번호는 나갔는데 행이 없다 — 쓰기 하나를 잃었다\n * unissued 아직 그 번호까지 안 갔다 — 잃은 것이 아니다\n * unknown 원본이 그 셋을 구별하지 못한다\n * ```\n *\n * **`unknown` 이 있어야 한다.** 채번을 따로 들지 않는 원본은 「행이 없다」에서 `lost` 와\n * `unissued` 를 가를 수 없다. 그때 `lost` 로 단정하면 사람이 없는 사실을 찾으러 간다.\n *\n * ── 판정을 여기서 하지 않는다 ─────────────────────────────────────────────\n * 이 문은 **원본이 아는 것을 그대로 낸다.** 「그러니 이 번호는 포기해도 된다」는 판정은 사람이\n * 한다 — 그 판단은 되돌릴 수 없어서(커서가 지나가면 끝이다) 코드가 대신할 자리가 아니다.\n *\n * @param from 이 번호부터(포함)\n * @param to 이 번호까지(포함). 원본이 상한을 두면 그만큼만 내고 `truncated` 로 말한다\n */\n seqReport?(cfg: ConnectionConfig, site: SiteDescriptor | undefined, scope: string, from: number, to: number): Promise<SeqReport>\n\n openLiveFeed?(\n cfg: ConnectionConfig,\n site: SiteDescriptor,\n onRecords: (records: unknown[]) => void,\n continuity?: LiveFeedContinuity\n ): () => void\n /**\n * 이 원본이 **선언하는 능력** — 없으면 「구조만 준다」는 뜻이다(ADR-0029 §8).\n *\n * 선언과 구현이 어긋나지 않게 `capabilitiesOf` 가 둘을 함께 본다: `control` 을 선언했는데 `control`\n * 구현이 없으면 그 능력은 **없는 것으로 읽는다**(선언만 있는 능력은 사용자에게 거짓말이 된다).\n */\n capabilities?: ReferenceCapability[]\n /**\n * 이 커넥터가 **무엇에 대고 확인됐나** — 능력과 다른 축이다(2026-08-23).\n *\n * ── 왜 필요한가 ─────────────────────────────────────────────────────────────\n * `capabilities` 는 「무엇을 할 수 있다고 선언하나」이고, 이 자리는 **「그 선언이 무엇으로 확인됐나」**다.\n * 둘이 갈리는 것이 지금 상태다: 등록된 커넥터 열넷 중 실 시스템에 붙어 본 것은 **하나**이고, 나머지는\n * 목(mock)에 대고 만들었다. 그런데 화면의 커넥터 목록에 그 구분이 없어서 **목으로 확인한 것이 확인으로\n * 보인다.**\n *\n * 그것은 이 저장소가 다른 자리에서 계속 거절하는 모양이다 — 「선언만 있는 능력은 사용자에게\n * 거짓말이 된다」(§`capabilitiesOf`). 능력에 세운 그 규율을 근거에도 세운다.\n *\n * ── 왜 계약이고 배포 노트가 아닌가 ──────────────────────────────────────────\n * 노트는 다음 사람이 보지 않는다. 계약에 있으면 새 커넥터가 그 값을 **빠뜨릴 수 없고**, 빠뜨리면\n * `groundingOf` 가 `unknown` 으로 답해 화면이 그 사실을 말한다.\n *\n * ── 자동으로 알 수 없는 값이다 ──────────────────────────────────────────────\n * 능력은 구현이 있는지 보면 알 수 있다. 「실 시스템에 붙여 봤나」는 **볼 수 없다** — 사람이 아는 사실이다.\n * 그래서 이 선언은 낡을 수 있고, 그것을 막는 것이 `verifiedAgainst` 다: **무엇에 대고 확인했는지 적지\n * 않으면 그 주장은 주장이 아니다**(`groundingOf` 가 `unknown` 으로 내린다).\n */\n grounding?: ReferenceGrounding\n /**\n * (선택) 구동 — 자극·속도·정지·초기화. **`capabilities` 에 `control` 을 선언한 원본만** 가진다.\n *\n * 실 시스템은 이것을 갖지 않는다(실 WMS 는 일시정지되지 않는다). 그 사실이 결손이 아니라 이 축의 요점이다.\n */\n control?(cfg: ConnectionConfig, site: SiteDescriptor | undefined, command: ControlCommand): Promise<ControlResult>\n}\n\n/**\n * 이 원본이 **실제로 할 수 있는 것** — 선언과 구현의 교집합.\n *\n * 선언만 있고 구현이 없으면 화면이 「할 수 있다」고 말한 뒤 아무 일도 일어나지 않는다. 반대로 구현이\n * 있는데 선언이 없으면 그 능력은 **의도적으로 닫아 둔 것**이므로 열지 않는다 — 선언이 권위다.\n */\n/**\n * 근거 수준 — **무엇에 대고 확인했나.** 넷이고 닫혀 있다.\n *\n * `synthetic` 실 시스템이 아니다 — 스스로 만들어 낸다(가상 원본·시뮬레이터). 한계가 아니라 성질이다.\n * `mock` 목·목 REST 에 대고만 확인했다. **실 시스템에 붙어 본 적이 없다.**\n * `instance` 실 시스템 **한 배포**에 붙여 확인했다. 그 배포의 성질과 제품의 성질을 아직 가르지 못했다.\n * `product` 실 시스템 **여러 배포**에 붙여 확인했다 — 그때 비로소 제품의 성질이라 말할 수 있다.\n *\n * `instance` 와 `product` 를 가르는 이유: 첫 실 연동에서 그 배포만의 성질이 넷 드러났고(관계 리솔버가\n * 다른 테넌트의 행을 답한다 · 모르는 필터를 알리지 않고 무시한다 · 지워진 품목의 BOM 이 남아 있다 ·\n * 사실상 무한을 뜻하는 표식) **그것이 그 제품의 성질인지 그 인스턴스의 성질인지 아무도 모른다.**\n * 두 번째 배포가 붙는 날 갈린다. 그 사실을 어휘가 담는다.\n */\nexport type ReferenceGroundingLevel = 'synthetic' | 'mock' | 'instance' | 'product'\n\nexport interface ReferenceGrounding {\n level: ReferenceGroundingLevel\n /**\n * **무엇에 대고 확인했나** — 사람이 읽는 한 줄(원본의 버전·인스턴스·목의 이름).\n *\n * `instance`·`product` 를 주장하면서 이것이 비어 있으면 `groundingOf` 가 `unknown` 으로 내린다 —\n * 가리키는 대상이 없는 주장은 주장이 아니고, 그런 주장은 낡아도 아무도 알 수 없다.\n */\n verifiedAgainst?: string\n}\n\n/**\n * 이 커넥터의 근거 — **주장을 그대로 믿지 않는다**(`capabilitiesOf` 와 같은 규율).\n *\n * 능력은 구현이 있는지 보아 선언을 검증할 수 있다. 근거는 **볼 수 없다** — 사람이 아는 사실이다.\n * 그래서 검증할 수 있는 것 하나만 검증한다: **실 시스템을 주장하면 무엇에 대고인지 말해야 한다.**\n *\n * 선언이 없으면 `unknown` 이다 — 「목이다」가 아니라 **「모른다」**다. 둘을 같게 두면 아직 선언하지 않은\n * 새 커넥터가 목으로 낙인찍히고, 그것도 없는 사실이다.\n */\nexport function groundingOf(adapter: ReferenceAdapter | undefined): {\n level: ReferenceGroundingLevel | 'unknown'\n verifiedAgainst?: string\n /** 주장이 내려갔으면 그 이유 — 화면이 그대로 옮긴다. */\n downgraded?: string\n} {\n const g = adapter?.grounding\n if (!g?.level) return { level: 'unknown' }\n const against = g.verifiedAgainst?.trim()\n if ((g.level === 'instance' || g.level === 'product') && !against) {\n return {\n level: 'unknown',\n downgraded: `claims \"${g.level}\" but does not say what it was verified against`\n }\n }\n return { level: g.level, ...(against ? { verifiedAgainst: against } : {}) }\n}\n\n/**\n * 능력마다 **그것을 증명하는 메서드들** — 하나라도 있으면 성립한다.\n *\n * ── `live` 의 증명이 왜 둘인가 (2026-09-16, ADR-0036 ③ 개정) ─────────────────\n * 예전에는 `openLiveFeed` 하나였다. 그런데 사실이 흘러 들어오는 방식은 **상대 시스템의 성질**이지\n * 우리 능력의 종류가 아니다 — 우리가 당겨 오면 `openLiveFeed`, 상대가 웹훅으로 밀어 넣으면\n * `handleInbound`(계약 일급 문)다. 둘 다 「이 원본에서 사실이 계속 온다」를 증명한다.\n *\n * 실측: `operato-plant` 은 outbox 가 밀어 넣는 쪽이라 `openLiveFeed` 가 없다. 그래서 **실 생산\n * 사실을 나르는 유일한 커넥터가 판정에서 live 가 아니었다.** 등록 경고는 「구현했는데 선언 안 함」\n * 한 방향만 봐서 아무 말도 하지 않았다.\n */\nconst PROVED_BY: Record<ReferenceCapability, readonly string[]> = {\n live: ['openLiveFeed', 'handleInbound'],\n control: ['control'],\n actuate: ['actuate'],\n /*\n * **선언만으로 성립한다** — 부를 메서드가 없다. 「두 번 해도 같은가」는 어댑터가 하는 일이 아니고\n * 저쪽 시스템의 성질이다. 그래서 구현 검사를 하지 않는다.\n */\n 'idempotent-actuation': []\n}\n\nconst CAPABILITIES = Object.keys(PROVED_BY) as ReferenceCapability[]\n\n/** 그 능력을 증명하는 메서드가 실제로 붙어 있나. 증명이 필요 없는 능력은 언제나 참이다. */\nfunction implemented(adapter: ReferenceAdapter, capability: ReferenceCapability): boolean {\n const methods = PROVED_BY[capability]\n if (methods.length === 0) return true\n return methods.some(name => typeof (adapter as any)[name] === 'function')\n}\n\nexport function capabilitiesOf(adapter: ReferenceAdapter | undefined): ReferenceCapability[] {\n if (!adapter) return []\n const declared = new Set(adapter.capabilities ?? [])\n return CAPABILITIES.filter(capability => declared.has(capability) && implemented(adapter, capability))\n}\n\n/* 어댑터 \"타입\" 레지스트리(전역·인메모리) — 레퍼런스 데이터는 DB(TwinReference), 어댑터 동작은 여기. */\nconst adapters = new Map<string, ReferenceAdapter>()\n\n/**\n * 어댑터를 등록한다 — 그리고 **선언과 구현이 어긋나면 그 자리에서 말한다.**\n *\n * ── 왜 필요한가 (2026-08-26 실측) ──────────────────────────────────────────\n * 능력 판정(`capabilitiesOf`)은 **한 방향만** 본다: 선언했는데 구현이 없는 것은 걸러 내고, **구현했는데\n * 선언이 없는 것은 아무 말도 하지 않는다.** 그래서 다음이 오류 없이 일어난다.\n *\n * 커넥터가 `openLiveFeed` 를 구현한다 → `capabilities: ['live']` 를 잊는다\n * → 연결 화면이 「이 원본은 라이브를 못 낸다」로 읽는다\n * → 재기동 정책을 정할 수 없어 트윈 생성이 거절된다\n * → 사람이 보는 것은 「재기동 정책이 없습니다」뿐이다\n *\n * 실제로 그렇게 막혔고(태양광 발전소), 원인을 찾는 데 코드를 뒤져야 했다. 지금 저장소에서 같은 상태인\n * 커넥터가 일곱이다.\n *\n * ── 왜 막지 않고 알리기만 하나 ──────────────────────────────────────────────\n * 등록에서 던지면 커넥터 하나 때문에 호스트가 못 뜬다. 만들다 만 커넥터를 들고 있는 사람이 아무것도\n * 못 하게 된다. 그리고 이 어긋남은 **고칠 수 있는 것**이지 위험한 것이 아니다 — 말해 주면 된다.\n */\nexport function registerAdapterType(adapter: ReferenceAdapter): void {\n const declared = new Set(adapter.capabilities ?? [])\n\n /*\n * **어긋남은 양쪽으로 본다** (2026-09-16, ADR-0036 ③ 개정).\n *\n * 이 경고는 「구현했는데 선언 안 함」 한 방향만 봤다. 그래서 반대쪽 — 선언했는데 증명이 없는 것 —\n * 은 `capabilitiesOf` 가 조용히 걸러 내기만 하고 아무도 그 사실을 듣지 못했다. 한 방향만 보면\n * **있는 것을 없다고(이번 plant) 또는 없는 것을 있다고** 말하게 된다. 같은 무게로 말한다.\n */\n const undeclared = CAPABILITIES.filter(c => PROVED_BY[c].length > 0 && implemented(adapter, c) && !declared.has(c))\n const unproven = CAPABILITIES.filter(c => PROVED_BY[c].length > 0 && declared.has(c) && !implemented(adapter, c))\n\n if (undeclared.length) {\n twinWarn(\n `[reference] connector \"${adapter.type}\" implements ${undeclared.join(' and ')} but does not declare it — ` +\n `add capabilities: [${undeclared.map(c => `'${c}'`).join(', ')}] or remove the implementation. ` +\n 'Until then the screens treat this connector as unable to do it, and a live twin cannot be created from it.'\n )\n }\n if (unproven.length) {\n twinWarn(\n `[reference] connector \"${adapter.type}\" declares ${unproven.join(' and ')} but implements none of ` +\n `${unproven.map(c => PROVED_BY[c].map(m => `${m}()`).join(' or ')).join(', ')} — ` +\n 'the declaration is dropped, so the screens treat this connector as unable to do it. ' +\n 'Implement one of those methods or remove the capability.'\n )\n }\n\n adapters.set(adapter.type, adapter)\n}\nexport function getAdapter(type: string): ReferenceAdapter | undefined {\n return adapters.get(type)\n}\nexport function listAdapterTypes(): string[] {\n return [...adapters.keys()]\n}\n/** 커넥터 메타데이터 목록(picker·연결 폼용). */\n/**\n * 등록된 어댑터 목록 — **능력은 판정값이다. 선언 원본이 아니다.**\n *\n * 능력은 이 목록을 읽는 화면이 「이 원본에 무엇을 요구할 수 있나」를 판단하는 근거다.\n *\n * ── 왜 선언을 그대로 내지 않나 (2026-09-16, ADR-0036 ③ 개정) ────────────────\n * 여기서 `a.capabilities` 를 그대로 냈다. 그래서 `capabilitiesOf` — 선언과 구현이 어긋나면 **없는\n * 것으로 읽는다**는 그 문 — 의 판정값을 **읽는 자리가 하나도 없었다.** 화면은 선언을 보고 있었고,\n * 어느 커넥터든 구현 없이 능력을 적기만 하면 가능한 것으로 보였다. 선언을 믿지 않으려고 만든 문을\n * 아무도 지나지 않은 셈이다.\n *\n * 커넥터 쪽에도 흔적이 남아 있었다 — `operato-plant` 주석이 「어댑터 목록은 선언한 값을 그대로\n * 보여 주므로 남겨 두면 화면이 없는 능력을 있다고 말한다」며 선언을 손으로 줄여 두었다. 사람이\n * 기억해서 맞추는 것은 다음 커넥터에서 끊긴다.\n */\nexport function listAdapters(): { type: string; meta?: AdapterMeta; capabilities?: ReferenceCapability[] }[] {\n return [...adapters.values()].map(a => ({ type: a.type, meta: a.meta, capabilities: capabilitiesOf(a) }))\n}\n"]}
@@ -211,7 +211,7 @@ instanceId, source
211
211
  *
212
212
  * 부팅 재부착은 `status: 'connected'` 인 레퍼런스만 되살린다(§`resumeReferenceLiveFeeds`). 그래서
213
213
  * 원본이 **잠깐** 끊긴 것이 그 행을 `error` 로 만들고, 다음 재기동에서 그 피드는 아예 붙지 않았다 —
214
- * 실측으로 그렇게 났다(09:25 판에 `[twin-live]` 줄이 한 줄도 없었다).
214
+ * 실측으로 그렇게 났다(09:25 버전에 `[twin-live]` 줄이 한 줄도 없었다).
215
215
  *
216
216
  * 두 축을 한 칸에 섞은 것이 원인이다:
217
217
  * `status` **연결 설정이 성립하나** — 접속 시험·마스터 동기가 정하는 축(재부착의 자격이다)
@@ -1 +1 @@
1
- {"version":3,"file":"reference-live.js","sourceRoot":"","sources":["../../../server/service/reference/reference-live.ts"],"names":[],"mappings":";;;AA6BA,wDAiIC;AAmJD,sDAMC;AAGD,wDAEC;AAkBD,4DA4EC;AA1ZD,gDAAkE;AAClE,iDAAqD;AAErD,iEAA+I;AAC/I,4DAA4D;AAC5D,6DAAwD;AACxD,2DAAwD;AAA/C,uHAAA,iBAAiB,OAAA;AAC1B,oEAAoE;AACpE,sEAA8D;AAC9D,oDAAiG;AAEjG,wEAAgE;AAChE,2DAAmD;AAEnD;;;;;;GAMG;AAEH,iEAAiE;AACjE,MAAM,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAA;AAE3C;;;GAGG;AACI,KAAK,UAAU,sBAAsB,CAC1C,QAAgB,EAChB,WAAmB,EACnB,GAAqB,EACrB,IAAoB,EACpB,UAAkB;AAClB;;;;;;;;;GASG;AACH,MAAe;IAEf,MAAM,OAAO,GAAG,IAAA,iCAAU,EAAC,WAAW,CAAC,CAAA;IACvC,IAAI,CAAC,OAAO,EAAE,YAAY;QAAE,OAAO,KAAK,CAAA;IAExC,MAAM,GAAG,GAAG,MAAM,qBAAU,CAAC,MAAM,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAA;IACzD,IAAI,CAAC,GAAG,EAAE,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,aAAa,UAAU,gEAAgE,CAAC,CAAA;IAEzH,wFAAwF;IACxF,uDAAuD;IACvD,qBAAU,CAAC,SAAS,CAAC,UAAU,EAAE,QAAQ,EAAE,GAAG,CAAC,IAAI,EAAE,MAAM,qBAAU,CAAC,iBAAiB,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAA;IAE7G,qBAAqB,CAAC,UAAU,CAAC,CAAA,CAAC,iBAAiB;IACnD;;;;;;;;;;;;OAYG;IACH,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,QAAQ,EAAE,UAAU,EAAE,MAAM,CAAC,CAAA;IAClE,MAAM,UAAU,GAAuB,EAAE,GAAG,OAAO,EAAE,SAAS,EAAE,IAAA,sCAAiB,EAAC,QAAQ,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,CAAA;IACjH;;;;;OAKG;IACH;;;;;;;;OAQG;IACH,UAAU,CAAC,aAAa,GAAG,eAAe,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAA;IAChE,MAAM,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,OAAkB,EAAE,EAAE;QACzE,IAAI,CAAC,qBAAU,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,EAAE,CAAC;YAC3C,qBAAqB,CAAC,UAAU,CAAC,CAAA,CAAC,uBAAuB;YACzD,OAAM;QACR,CAAC;QACD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QACzE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,IAAA,iCAAsB,EAC/D,OAA4B,EAC5B,QAAQ,EACR,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EACxB,qBAAU,CAAC,SAAS,CAAC,QAAQ,EAAE,UAAU,CAAC,CAC3C,CAAA;QACD;;;;;;;;WAQG;QACH,MAAM,aAAa,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,qBAAU,CAAC,eAAe,CAAC,QAAQ,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QAC1G,IAAI,UAAU,CAAC,MAAM,IAAI,aAAa,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC;YAC3D,IAAA,iBAAQ,EACN,gBAAgB,UAAU,MAAM,UAAU,CAAC,MAAM,GAAG,aAAa,IAAI,UAAU,CAAC,MAAM,wCAAwC;gBAC5H,8EAA8E,CACjF,CAAA;QACH,CAAC;QACD;;;WAGG;QACH,0DAA0D;QAC1D,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,qBAAU,CAAC,UAAU,CAAC,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QAExG;;;WAGG;QACH,KAAK,IAAA,iCAAa,EAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAA,uCAAoB,GAAE,EAAE,CAAC,CAAA;QACpF,MAAM,WAAW,GAAG,QAAQ,CAAC,MAAM,GAAG,OAAO,CAAA;QAC7C;;;;;;;WAOG;QACH;;;WAGG;QACH,qBAAU,CAAC,sBAAsB,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAA;QACvD,qBAAU,CAAC,kBAAkB,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,WAAW,CAAC,CAAA;QAC/F,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;YACpB,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,SAAS,CAAA;YACzD,IAAA,iBAAQ,EAAC,gBAAgB,UAAU,MAAM,QAAQ,CAAC,MAAM,IAAI,OAAO,8BAA8B,GAAG,EAAE,CAAC,CAAA;QACzG,CAAC;QACD,2DAA2D;QAC3D,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;YACpB,IAAA,iBAAQ,EACN,gBAAgB,UAAU,MAAM,WAAW,IAAI,OAAO,2DAA2D;gBAC/G,sEAAsE,CACzE,CAAA;QACH,CAAC;IACH,CAAC,EAAE,UAAU,CAAC,CAAA;IACd,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,WAAW,CAAC,CAAA;IAClC,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;GAKG;AACH,SAAS,eAAe,CAAC,QAAgB,EAAE,UAAkB;IAC3D,MAAM,KAAK,GAAG,qBAAU,CAAC,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAA;IACvD,MAAM,KAAK,GAAI,KAAa,EAAE,KAAK,CAAA;IACnC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IAC3C,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC;QAC5B,EAAE,EAAE,MAAM,CAAC,CAAC,EAAE,QAAQ,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC;QACvC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChD,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACxD,GAAG,CAAC,OAAO,CAAC,EAAE,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrD,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzC,GAAG,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjE,GAAG,CAAC,OAAO,CAAC,EAAE,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9D,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC1C,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAmB,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;AAC7C,CAAC;AAED;;;;;;;;GAQG;AACH,KAAK,UAAU,cAAc,CAC3B,QAAgB;AAChB,wEAAwE;AACxE,UAAkB,EAClB,MAAe;AACf;;;GAGG;;IAEH,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,+DAA+D;QAC/D,IAAA,iBAAQ,EAAC,mGAAmG,CAAC,CAAA;QAC7G,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,MAAM,IAAI,GAAG,IAAA,qBAAa,EAAC,iCAAa,CAAC,CAAA;IACzC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAS,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAA;IACxG,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,IAAA,iBAAQ,EAAC,sEAAsE,MAAM,GAAG,CAAC,CAAA;QACzF,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,MAAM,MAAM,GAAG,GAAG,CAAC,UAAU,CAAA;IAC7B,IAAI,MAAM,EAAE,eAAe,EAAE,CAAC;QAC5B,IAAA,gBAAO,EAAC,gBAAgB,MAAM,qDAAqD,MAAM,CAAC,eAAe,EAAE,CAAC,CAAA;IAC9G,CAAC;IACD,OAAO;QACL,MAAM,EAAE,MAAM,IAAI,SAAS;QAC3B;;;;;;;;;;;;;;;;;;;;WAoBG;QACH,aAAa,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,EAAE;YACjD,qBAAU,CAAC,uBAAuB,CAAC,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,WAAW,CAAC,CAAA;YACjG,IAAA,iBAAQ,EAAC,gBAAgB,MAAM,iCAAiC,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,MAAM,MAAM,EAAE,CAAC,CAAA;YAC3G,IAAI;iBACD,MAAM,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,SAAS,MAAM,EAAE,EAAS,CAAC;iBAC/D,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,IAAA,iBAAQ,EAAC,gBAAgB,MAAM,kCAAkC,GAAG,EAAE,OAAO,IAAI,GAAG,EAAE,CAAC,CAAC,CAAA;QAC1G,CAAC;QACD;;;;;;;WAOG;QACH,WAAW,EAAE,CAAC,EAAE,aAAa,EAAE,MAAM,EAAE,EAAE,EAAE;YACzC,qBAAU,CAAC,qBAAqB,CAAC,QAAQ,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;YACzF,IAAA,gBAAO,EACL,gBAAgB,MAAM,0BAA0B,aAAa,uCAAuC,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CACjI,CAAA;YACD,IAAI;iBACD,MAAM,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAS,CAAC;iBAClD,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,IAAA,iBAAQ,EAAC,gBAAgB,MAAM,8BAA8B,GAAG,EAAE,OAAO,IAAI,GAAG,EAAE,CAAC,CAAC,CAAA;QACtG,CAAC;QACD;;;;;;;;;WASG;QACH;;;;;;;;WAQG;QACH,UAAU,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE;YAChC,qBAAU,CAAC,oBAAoB,CAAC,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,CAAA;QACtE,CAAC;QACD,aAAa,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE;YACpC,qBAAU,CAAC,uBAAuB,CAAC,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,CAAA;YACpF,IAAA,iBAAQ,EACN,gBAAgB,MAAM,kDAAkD;gBACtE,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,MAAM,MAAM,gCAAgC,CAC9E,CAAA;YACD,IAAI;iBACD,MAAM,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,wBAAwB,MAAM,EAAE,EAAS,CAAC;iBAC9E,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,IAAA,iBAAQ,EAAC,gBAAgB,MAAM,kCAAkC,GAAG,EAAE,OAAO,IAAI,GAAG,EAAE,CAAC,CAAC,CAAA;QAC1G,CAAC;QACD,QAAQ,EAAE,IAAI,CAAC,EAAE;YACf,yDAAyD;YACzD,IAAI;iBACD,MAAM,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,EAAS,CAAC;iBACnD,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,IAAA,iBAAQ,EAAC,gBAAgB,MAAM,8BAA8B,GAAG,EAAE,OAAO,IAAI,GAAG,EAAE,CAAC,CAAC,CAAA;QACtG,CAAC;KACF,CAAA;AACH,CAAC;AAED,8BAA8B;AAC9B,SAAgB,qBAAqB,CAAC,UAAkB;IACtD,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;IACnC,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAA;IACxB,IAAI,CAAC;QAAC,KAAK,EAAE,CAAA;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,UAAU,CAAC,CAAC;IACpC,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;IACxB,OAAO,IAAI,CAAA;AACb,CAAC;AAED,oBAAoB;AACpB,SAAgB,sBAAsB;IACpC,OAAO,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,CAAA;AAC1B,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACI,KAAK,UAAU,wBAAwB;IAC5C,MAAM,OAAO,GAAa,EAAE,CAAA;IAC5B,MAAM,MAAM,GAA4C,EAAE,CAAA;IAC1D;;;;;;;;;;OAUG;IACH,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAA,CAAC,0BAA0B;IACrE,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,IAAA,qBAAa,EAAC,iCAAa,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,SAAS,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;QAC/G,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,MAAM,QAAQ,GAAI,GAAW,CAAC,MAAM,EAAE,EAAE,IAAK,GAAW,CAAC,QAAQ,CAAA;YACjE,MAAM,OAAO,GAAG,IAAA,iCAAU,EAAC,GAAG,CAAC,WAAW,CAAC,CAAA;YAC3C,IAAI,CAAC,QAAQ,IAAI,CAAC,OAAO,EAAE,YAAY;gBAAE,SAAQ,CAAC,0BAA0B;YAE5E,MAAM,QAAQ,GAAU,CAAE,GAAG,CAAC,SAAiB,EAAE,QAAQ,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,EAAE,UAAU,IAAI,CAAC,EAAE,MAAM,CAAC,CAAA;YAC/G,IAAI,CAAC,QAAQ,CAAC,MAAM;gBAAE,SAAQ;YAE9B,gCAAgC;YAChC,MAAM,IAAI,GAAG,MAAM,IAAA,qBAAa,EAAC,+BAAY,CAAC,CAAC,IAAI,CAAC;gBAClD,KAAK,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,CAAQ;aACjG,CAAC,CAAA;YACF,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAM,EAAE,EAAE,CACtC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,CAAC,aAAa,KAAK,QAAQ,CAAC,CACxG,CAAA;YACD,IAAI,CAAC,IAAI,CAAC,MAAM;gBAAE,SAAQ;YAE1B,IAAI,KAAuB,CAAA;YAC3B,IAAI,CAAC;gBACH,KAAK,GAAG,MAAM,OAAO,CAAC,aAAa,CAAC,GAAG,CAAC,gBAAgB,IAAI,EAAE,CAAC,CAAA;YACjE,CAAC;YAAC,OAAO,CAAM,EAAE,CAAC;gBAChB,6DAA6D;gBAC7D,KAAK,MAAM,CAAC,IAAI,IAAI;oBAAE,MAAM,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,KAAK,EAAE,oBAAoB,CAAC,EAAE,OAAO,IAAI,SAAS,EAAE,EAAE,CAAC,CAAA;gBACrH,SAAQ;YACV,CAAC;YAED,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;gBACrB,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,CAAC,CAAA;gBACnD,IAAI,CAAC,IAAI,EAAE,CAAC;oBACV,MAAM,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,MAAM,0BAA0B,EAAE,CAAC,CAAA;oBAC7F,SAAQ;gBACV,CAAC;gBACD,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,CAAA;gBACxC,IAAI,KAAK,EAAE,CAAC;oBACV,oEAAoE;oBACpE,MAAM,CAAC,IAAI,CAAC;wBACV,UAAU,EAAE,CAAC,CAAC,UAAU;wBACxB,KAAK,EAAE,8BAA8B,GAAG,CAAC,MAAM,2BAA2B,KAAK,6DAA6D;qBAC7I,CAAC,CAAA;oBACF,SAAQ;gBACV,CAAC;gBACD,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;gBACtC,IAAI,CAAC;oBACH,MAAM,EAAE,GAAG,MAAM,sBAAsB,CAAC,QAAQ,EAAE,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,gBAAgB,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;oBAC9H,IAAI,EAAE;wBAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAA;;wBAC7B,MAAM,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,KAAK,EAAE,6BAA6B,EAAE,CAAC,CAAA;gBACtF,CAAC;gBAAC,OAAO,CAAM,EAAE,CAAC;oBAChB,MAAM,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,IAAI,iBAAiB,EAAE,CAAC,CAAA;gBACnF,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAAC,OAAO,CAAM,EAAE,CAAC;QAChB,IAAA,kBAAS,EAAC,qCAAqC,EAAE,CAAC,EAAE,OAAO,CAAC,CAAA;IAC9D,CAAC;IAED,IAAI,OAAO,CAAC,MAAM;QAAE,IAAA,gBAAO,EAAC,0BAA0B,OAAO,CAAC,MAAM,aAAa,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACtG,2DAA2D;IAC3D,KAAK,MAAM,CAAC,IAAI,MAAM;QAAE,IAAA,iBAAQ,EAAC,gBAAgB,CAAC,CAAC,UAAU,2BAA2B,CAAC,CAAC,KAAK,EAAE,CAAC,CAAA;IAClG,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAA;AAC5B,CAAC;AAED;;;GAGG;AACH,IAAA,gCAAqB,EAAC,GAAG,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;AAE9C,kCAAkC;AAClC,qBAAU,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC,qBAAqB,CAAC,UAAU,CAAC,CAAC,CAAA","sourcesContent":["import { twinLog, twinWarn, twinError } from '../../engine/log.js'\nimport { getRepository } from '@things-factory/shell'\n\nimport { getAdapter, type ConnectionConfig, type LiveFeedContinuity, type ObservedItemFact, type SiteDescriptor } from './reference-adapter.js'\n/* 이름 짓는 규칙은 가벼운 파일에 있다 — 테스트가 붙게(§`live-feed-lease.ts`). */\nimport { liveFeedLeaseName } from './live-feed-lease.js'\nexport { liveFeedLeaseName } from './live-feed-lease.js'\nimport { databaseCommandStore } from '../actuation/command-store.js'\nimport { settleEffects } from '../actuation/settle-effects.js'\nimport { TwinEngine, ingestCanonicalRecords, registerLiveFeedProbe } from '../../engine/index.js'\nimport { type CanonicalRecord } from '@operato/ops-contract'\nimport { TwinInstance } from '../twin-instance/twin-instance.js'\nimport { TwinReference } from './twin-reference.js'\n\n/*\n * 커넥터 기반 라이브 피드 결선(reference → live, §0 프레임 ① restartPolicy='resync').\n * 흐름: adapter.openLiveFeed(cfg, site, records) → ingestCanonicalRecords(정규→봉투) → TwinEngine.ingestLive(projector 미러).\n * 커넥터가 \"벤더 이벤트 → 정규 레코드\" 매핑을 소유(코드)한다 — preset 기반 startLiveFeed(operato-twin, http-get+jsonata)와\n * 대칭이되, 매핑 위치만 다르다. 실 시스템 전환은 connectionConfig.baseUrl 교체뿐(코드 경로 동일).\n * 생명주기: 인스턴스 정지(TwinEngine.stop)에 훅으로 묶여 피드도 함께 unsubscribe.\n */\n\n/** 활성 라이브 피드 — instanceId → unsubscribe(어댑터 openLiveFeed 반환). */\nconst feeds = new Map<string, () => void>()\n\n/**\n * 커넥터 라이브 기동 — 어댑터 openLiveFeed 를 인스턴스에 결선.\n * 반환: 라이브 결선 성공 여부(어댑터가 openLiveFeed 미지원이면 false → 호출측이 sim 폴백).\n */\nexport async function startReferenceLiveFeed(\n domainId: string,\n adapterType: string,\n cfg: ConnectionConfig,\n site: SiteDescriptor,\n instanceId: string,\n /**\n * **어느 레퍼런스에서 읽는 피드인가** — 읽기 커서를 저장할 자리를 가리킨다(§`loadLiveCursor`).\n *\n * 부르는 쪽이 `TwinReference` 를 이미 들고 있으므로 그 이름을 넘긴다. 처음에는 `cfg`·`site` 에서\n * 찾으려 했는데 **둘 다 그 값을 가질 수 없다**: `cfg` 는 사람이 화면에서 입력한 접속 설정이고\n * (`endpoint`·`token`…), `site` 는 커넥터가 원본에서 발견한 것이다 — **레퍼런스의 이름은 호스트가\n * 붙이는 것이고 원본은 그것을 모른다.** 커넥터가 채울 수 없는 값을 커넥터에서 읽으려 한 것이 잘못이었다.\n *\n * 주지 않으면 커서를 잇지 않고 **그 사실을 말한다**(알리지 않고 창이 미끄러지지 않게).\n */\n source?: string\n): Promise<boolean> {\n const adapter = getAdapter(adapterType)\n if (!adapter?.openLiveFeed) return false\n\n const det = await TwinEngine.detail(domainId, instanceId)\n if (!det?.model) throw new Error(`instance \"${instanceId}\" not provisioned (no model) — ingest master before live start`)\n\n // 관측 기동 — 커널 tick 없이 projector 가 실 이벤트를 반영한다(startLive 가 restartPolicy='resync' 로 적는다).\n // 시각 기준은 **공간**이 갖는다(테넌트 아님) — 교대의 HH:MM 을 어느 기준으로 읽나.\n TwinEngine.startLive(instanceId, domainId, det.kind, await TwinEngine.withSpaceTimeBase(det.model, domainId))\n\n stopReferenceLiveFeed(instanceId) // 재기동 시 이전 피드 정리\n /*\n * **어디까지 읽었나를 이 층이 들고 건넨다**(§`LiveFeedContinuity`, 2026-08-23).\n *\n * 어댑터가 이 값을 자기 사물함에 두면 원본이 늘 때마다 저장 기제가 늘고, 그중 하나가 알리지 않고 다르게\n * 동작한다. 그리고 커널이 이미 갈라 두었다: 「원천이 애초에 다시 말해 주지 않는 축」은 재기동\n * 연속성으로 이어받는다(§`hydrateContinuity`) — 읽기 커서가 정확히 그 성질이다.\n *\n * The object itself is handed over even on a first attach, because the feed needs its lease name\n * before it has read anything. `cursor` staying absent is what says \"first attach\" — which is how\n * every connector already reads it (`continuity?.cursor`), so nothing changes for them. What is\n * never filled in with an empty value is the cursor: an empty one would make a first attach look\n * like a resumed one, and the adapter would build no look-back window.\n */\n const carried = await loadLiveCursor(domainId, instanceId, source)\n const continuity: LiveFeedContinuity = { ...carried, leaseName: liveFeedLeaseName(domainId, instanceId, source) }\n /*\n * The lease name is composed here, not in the adapter, because it has to be unique across\n * domains and an adapter is handed neither `domainId` nor `instanceId` (§`LiveFeedContinuity`).\n * Built from `source` rather than `site.siteId`: one reference is one connection to one system,\n * and that is the thing a second instance must not open twice.\n */\n /*\n * ── **트윈이 아는 것을 함께 건넨다** (2026-08-28) ───────────────────────────\n *\n * 전량 절대값을 읽는 커넥터는 지난 주기와 견주어 바뀐 것만 낸다. 그 비교 표가 프로세스 안에만\n * 있어서 재기동하면 커넥터가 전량을 「바뀐 것」으로 판정했다 — 승화푸드에서 2,665건이다.\n *\n * 트윈은 저장본에서 되세우므로 잃지 않았다. 그래서 그 사실을 건네면 첫 주기부터 바뀐 것만 나간다.\n * 커널 어휘로 건넨다 — 원본 낱말로 되돌리는 일은 여러 낱말이 한 값으로 모여 불가능하고, 필요도 없다.\n */\n continuity.observedItems = observedItemsOf(domainId, instanceId)\n const unsubscribe = adapter.openLiveFeed(cfg, site, (records: unknown[]) => {\n if (!TwinEngine.owns(domainId, instanceId)) {\n stopReferenceLiveFeed(instanceId) // 인스턴스 사라지면 자가 정지(안전망)\n return\n }\n const offered = Array.isArray(records) ? records.length : records ? 1 : 0\n const { accepted, rejected, masterData } = ingestCanonicalRecords(\n records as CanonicalRecord[],\n domainId,\n new Date().toISOString(),\n TwinEngine.factScope(domainId, instanceId)\n )\n /*\n * ── **사건이 아닌 것은 상태만 세운다** (2026-08-28) ─────────────────────────\n *\n * 마스터데이터 — 변하지 않는 속성(유통기한·로트번호). 시각축이 없는 값이므로 지난 기록에 적지\n * 않는다(§`applyMasterData`). 이 문이 없던 동안 그 값은 사건에 실려 왔고, 표준이 그것을\n * 「들어왔다」에만 허용하므로 전량을 읽는 원본은 **들어오지 않은 것을 들어왔다고** 말해야 했다.\n *\n * 반영된 건수를 함께 센다 — 라이브로 돌지 않는 트윈은 0 을 내므로, 그 차이가 「바닥에 버려진 수」다.\n */\n const masterApplied = masterData.length ? TwinEngine.applyMasterData(domainId, instanceId, masterData) : 0\n if (masterData.length && masterApplied < masterData.length) {\n twinWarn(\n `[twin-live] \"${instanceId}\": ${masterData.length - masterApplied}/${masterData.length} master data element(s) NOT applied — ` +\n 'the twin is not running live; lot attributes such as expiry will stay empty.'\n )\n }\n /*\n * 커널이 **몇 건을 실제로 받았나**를 함께 본다. 트윈이 라이브로 돌지 않으면 `ingestLive` 가 0 을\n * 돌려주고, 그 차이가 곧 「바닥에 버려진 수」다. 이것을 통과로 세면 멈춘 트윈이 100% 로 보인다.\n */\n /* 어떤 길로 들어왔는지 함께 적는다 — 화면이 「이 트윈은 무엇을 통해 받나」를 말할 수 있게. */\n const applied = accepted.length ? TwinEngine.ingestLive(domainId, instanceId, accepted, 'connector') : 0\n\n /*\n * **나간 지시와 잇는다** — 폐루프. 훅과 같은 자리다(§`routes.ts`). 기다리지 않는다: 라이브 묶음\n * 처리가 이것 때문에 늦으면 뒤의 묶음이 밀리고, 밀린 것이 크래시로 이어진 적이 있다.\n */\n void settleEffects({ domainId, envelopes: accepted, store: databaseCommandStore() })\n const undelivered = accepted.length - applied\n /*\n * **거부한 것을 알리지 않고 버리지 않는다.** 예전에는 `rejected` 를 받지도 않았다 — 커넥터의 매핑이\n * 틀리면 트윈은 「아무 일도 일어나지 않는」 모습이 되고, 로그에도 아무 흔적이 없었다.\n *\n * 그다음 자리였던 「화면까지 나르기」를 이제 채운다. 건마다 브로드캐스팅하지 않고 **집계**한다 — 통과분까지\n * 함께 세는 이유는, 거부만 세면 「유입 없음」과 「정상」이 둘 다 거부 0 으로 같아 보이기 때문이다.\n * 조회는 `twinIngestHealth`.\n */\n /*\n * **읽었다는 것 자체가 사실이다** — 빈 읽기도 성공이므로 단절 기록을 지운다(§`onReadFailure`).\n * 그 둘을 섞으면 들어온 것이 없는 연결이 끊긴 원본으로 보인다.\n */\n TwinEngine.clearIngestReadFailure(domainId, instanceId)\n TwinEngine.recordIngestResult(domainId, instanceId, offered, rejected, Date.now(), undelivered)\n if (rejected.length) {\n const why = rejected[0]?.errors?.join(' · ') ?? 'unknown'\n twinWarn(`[twin-live] \"${instanceId}\": ${rejected.length}/${offered} record(s) rejected — e.g. ${why}`)\n }\n /* 버려진 것은 로그에도 남긴다 — 화면을 보지 않는 사람도 알아야 한다(사실이 사라지는 중이다). */\n if (undelivered > 0) {\n twinWarn(\n `[twin-live] \"${instanceId}\": ${undelivered}/${offered} record(s) NOT delivered — the twin is not running live; ` +\n `records are being discarded. Start the twin, or disconnect the feed.`\n )\n }\n }, continuity)\n feeds.set(instanceId, unsubscribe)\n return true\n}\n\n/**\n * 트윈이 지금 들고 있는 물품을 커널 어휘로 낸다 — 없으면 `undefined`.\n *\n * 상태가 없으면(트윈이 아직 아무것도 모르면) `undefined` 다. 빈 배열로 메우지 않는다 — 「모른다」와\n * 「하나도 없다」는 다른 사실이고, 커넥터가 그 둘에 다르게 반응해야 한다.\n */\nfunction observedItemsOf(domainId: string, instanceId: string): ObservedItemFact[] | undefined {\n const state = TwinEngine.snapshot(domainId, instanceId)\n const items = (state as any)?.items\n if (!Array.isArray(items)) return undefined\n return items.map((i: any) => ({\n id: String(i?.subLotId ?? i?.epc ?? ''),\n ...(i?.gtin ? { epcClass: String(i.gtin) } : {}),\n ...(i?.location ? { location: String(i.location) } : {}),\n ...(typeof i?.qty === 'number' ? { qty: i.qty } : {}),\n ...(i?.uom ? { uom: String(i.uom) } : {}),\n ...(i?.disposition ? { disposition: String(i.disposition) } : {}),\n ...(typeof i?.expiry === 'number' ? { expiry: i.expiry } : {}),\n ...(i?.lot ? { lot: String(i.lot) } : {})\n })).filter((i: ObservedItemFact) => !!i.id)\n}\n\n/**\n * 그 원본의 읽기 커서를 불러오고, 움직이면 저장하는 통로를 만든다.\n *\n * 커서는 **원본(`TwinReference`)의 것**이지 인스턴스의 것이 아니다 — 한 원본이 여러 인스턴스를 낳고\n * (`scopeSpec.produced`) 읽는 것은 한 번이다. 인스턴스마다 커서를 두면 같은 표를 여러 번 읽는다.\n *\n * 저장 실패는 **삼키지 않고 말한다.** 오류 없이 실패하면 다음 재기동에서 창이 다시 미끄러지고, 그 손실은\n * 오류 없이 사라진다 — 이 컬럼을 만든 이유가 바로 그것이다.\n */\nasync function loadLiveCursor(\n domainId: string,\n /** 읽기 실패를 적을 트윈 — 커서는 레퍼런스의 것이지만 **건강은 인스턴스의 것**이다(한 레퍼런스가 여럿을 낳는다). */\n instanceId: string,\n source?: string\n /*\n * `leaseName` is not this function's to know — it is composed from the domain and instance by the\n * caller. Returning the rest keeps that rule in one place instead of two.\n */\n): Promise<Omit<LiveFeedContinuity, 'leaseName'> | undefined> {\n if (!source) {\n /* 원본을 가리킬 수 없으면 저장할 자리도 없다 — 그 사실을 말한다(알리지 않고 커서 없이 돌지 않게). */\n twinWarn('[twin-live] live cursor not carried — the caller did not say which reference this feed reads from')\n return undefined\n }\n const repo = getRepository(TwinReference)\n const row = await repo.findOne({ where: { domain: { id: domainId } as any, source } }).catch(() => null)\n if (!row) {\n twinWarn(`[twin-live] live cursor not carried — no reference row for source \"${source}\"`)\n return undefined\n }\n const stored = row.liveCursor\n if (stored?.firstAttachedAt) {\n twinLog(`[twin-live] \"${source}\": resuming from stored cursor — knowledge starts ${stored.firstAttachedAt}`)\n }\n return {\n cursor: stored ?? undefined,\n /*\n * **원본에 닿지 못했다** — 어댑터가 알리면 상태에 적는다(§`onReadFailure`).\n *\n * 로그로는 어댑터가 이미 말하고 있었다(재시도 경고). 그러나 **로그는 사람이 볼 때만 값이 있다** —\n * 화면이 말하려면 조회 가능한 상태에 있어야 한다.\n *\n * ── `status` 를 건드리지 않는다 (2026-08-23, 되돌린 자리) ────────────────────\n * 처음에는 레퍼런스 행의 `status` 를 `'error'` 로도 적었다 — 「목록이 「연결됨」이라 말하면서 실제로는\n * 못 읽는 상태를 없애려고」. **그것이 피드를 영구히 껐다.**\n *\n * 부팅 재부착은 `status: 'connected'` 인 레퍼런스만 되살린다(§`resumeReferenceLiveFeeds`). 그래서\n * 원본이 **잠깐** 끊긴 것이 그 행을 `error` 로 만들고, 다음 재기동에서 그 피드는 아예 붙지 않았다 —\n * 실측으로 그렇게 났다(09:25 판에 `[twin-live]` 줄이 한 줄도 없었다).\n *\n * 두 축을 한 칸에 섞은 것이 원인이다:\n * `status` **연결 설정이 성립하나** — 접속 시험·마스터 동기가 정하는 축(재부착의 자격이다)\n * 읽기 실패 **지금 닿나** — 오갈 수 있는 사실이고, 유입 장부가 그 축을 갖는다\n *\n * 그러니 여기서는 `lastError` 에만 적는다(사람이 목록에서 이유를 볼 수 있게). 「지금 닿나」는\n * `ingestHealth().readFailure` 가 답하고, 성공한 주기가 그것을 지운다.\n */\n onReadFailure: ({ reason, stream, nextRetryMs }) => {\n TwinEngine.recordIngestReadFailure(domainId, instanceId, reason, Date.now(), stream, nextRetryMs)\n twinWarn(`[twin-live] \"${source}\": cannot read from the source${stream ? ` (${stream})` : ''} — ${reason}`)\n repo\n .update({ id: row.id }, { lastError: `live: ${reason}` } as any)\n .catch(err => twinWarn(`[twin-live] \"${source}\": read failure not recorded — ${err?.message ?? err}`))\n },\n /*\n * **끊겼다가 돌아왔다** — 그 사실을 장부에 남긴다 (2026-08-27).\n *\n * 실패 기록을 지우기만 하면 「끊긴 적이 있었다」가 화면에서 사라진다. 그러면 밤새 두 시간 끊겼던\n * 연결과 한 번도 끊기지 않은 연결이 아침에 똑같이 보인다.\n *\n * `lastError` 도 함께 지운다 — 목록에 남은 옛 이유가 지금의 상태로 읽힌다.\n */\n onRecovered: ({ afterFailures, downMs }) => {\n TwinEngine.recordIngestRecovered(domainId, instanceId, afterFailures, downMs, Date.now())\n twinLog(\n `[twin-live] \"${source}\": reading again after ${afterFailures} failed attempt(s) — it was out for ${Math.round(downMs / 1000)}s`\n )\n repo\n .update({ id: row.id }, { lastError: null } as any)\n .catch(err => twinWarn(`[twin-live] \"${source}\": recovery not recorded — ${err?.message ?? err}`))\n },\n /**\n * **읽었는데 창을 넘길 수 없다** — 위와 조치가 반대인 사실이다 (2026-08-24).\n *\n * 이 문을 만들기 전에는 이 사실이 `onReadFailure` 로 나갔다. 그래서 화면이 「원본에 닿지 못한다」고\n * 말했는데 실제로는 원본이 **답한** 상태였고, 그 답의 모양이 커서를 이긴 것이었다. 사람은 원본을\n * 의심하고 기다리는데 기다림으로는 영원히 풀리지 않는다 — **원인을 반대 방향으로 가리켰다.**\n *\n * `status` 를 건드리지 않는 규율은 위와 같다(재부착의 자격이다). `lastError` 에는 적는다 — 목록에서\n * 이유를 볼 수 있어야 하고, 「닿지 못한다」와 다른 문장이어야 한다.\n */\n /*\n * **세우지 않은 것을 장부에 적는다** — 원본에 있는데 트윈에 세우지 않은 것(이유와 수).\n *\n * 로그로도 남기지만 **로그는 사람이 볼 때만 값이 있다.** 이 사실은 상태의 성질이라(세우지 않은 것은\n * 지금도 없다) 조회되는 자리에 있어야 한다 — 그러지 않으면 프로비저닝 화면에서 한 번 스쳐 지나간다.\n *\n * `status`·`lastError` 를 건드리지 않는다: 이것은 **실패가 아니다.** 어댑터가 옳은 판단으로 안 받은\n * 것이고, 오류로 적으면 화면이 원본을 의심하게 만든다.\n */\n onWithheld: ({ reason, count }) => {\n TwinEngine.recordIngestWithheld(domainId, instanceId, reason, count)\n },\n onCursorStall: ({ reason, stream }) => {\n TwinEngine.recordIngestCursorStall(domainId, instanceId, reason, Date.now(), stream)\n twinWarn(\n `[twin-live] \"${source}\": read the source but the cursor cannot advance` +\n `${stream ? ` (${stream})` : ''} — ${reason}. Waiting will not clear this.`\n )\n repo\n .update({ id: row.id }, { lastError: `live cursor stalled: ${reason}` } as any)\n .catch(err => twinWarn(`[twin-live] \"${source}\": cursor stall not recorded — ${err?.message ?? err}`))\n },\n onCursor: next => {\n /* 어댑터가 준 모양을 그대로 적는다 — 이 층은 흐름의 뜻을 모른다(열쇠는 어댑터가 정한다). */\n repo\n .update({ id: row.id }, { liveCursor: next } as any)\n .catch(err => twinWarn(`[twin-live] \"${source}\": live cursor not saved — ${err?.message ?? err}`))\n }\n }\n}\n\n/** 라이브 피드 중지(unsubscribe). */\nexport function stopReferenceLiveFeed(instanceId: string): boolean {\n const unsub = feeds.get(instanceId)\n if (!unsub) return false\n try { unsub() } catch { /* noop */ }\n feeds.delete(instanceId)\n return true\n}\n\n/** 활성 피드 목록(관측). */\nexport function listReferenceLiveFeeds(): string[] {\n return [...feeds.keys()]\n}\n\n/**\n * 부팅 때 **끊어진 피드를 다시 붙인다** — 도는 미러 트윈에 계측이 계속 들어오게.\n *\n * ── 무엇이 알리지 않고 끊겼나 (2026-08-14) ───────────────────────────────────────\n * `startReferenceLiveFeed` 를 부르는 곳은 `importTwinReference` **하나뿐**이었다. 그래서 서버를 한 번\n * 재기동하면 미러 트윈은 「도는 중」이라 말하면서 아무 계측도 받지 않았다 — 오류도 로그도 없이,\n * 사람은 「값이 안 변하네」로 알게 된다. 실 배포에서 재기동 한 번에 계측이 멈추는 셈이다.\n *\n * ── 되살릴 재료는 이미 영속돼 있다 ─────────────────────────────────────────\n * `TwinReference` 가 어댑터 종류·접속 설정·그 레퍼런스가 만든 인스턴스(`scopeSpec.produced`)를 들고 있다.\n * 사이트 서술자만 없는데(`hint` 는 남기지 않았다) **다시 찾아서** 얻는다(`discoverSites`) — 인제스트가\n * 걷는 것과 같은 길이므로 규칙이 갈라지지 않는다. 되살릴 인스턴스가 없으면 그 호출도 하지 않는다.\n *\n * 재개 대상은 **등록부가 도는 미러라고 말하는 것**뿐이다: 멈춘 트윈을 되살리면 사람이 멈춘 것을\n * 우리가 다시 켜는 것이고, 시뮬 트윈에 실 계측을 부으면 두 현실이 섞인다.\n */\nexport async function resumeReferenceLiveFeeds(): Promise<{ resumed: string[]; failed: { instanceId: string; error: string }[] }> {\n const resumed: string[] = []\n const failed: { instanceId: string; error: string }[] = []\n /*\n * **한 트윈에 피드는 하나다** (2026-08-18).\n *\n * 같은 사이트를 두 레퍼런스가 각각 인제스트하면 둘 다 그 인스턴스를 자기가 만든 것(`produced`)으로\n * 들고 있다. 기동 때 그 목록을 그대로 돌면 같은 트윈에 피드를 두 번 붙이는데, 붙이는 함수가 미러를\n * 다시 세우므로(`startLive`) **먼저 쌓인 수요 구간이 리셋된다** — 실제로 그렇게 났다\n * (로그: `reattached 2 feed(s): ems-plant-1, ems-plant-1`).\n *\n * 그러니 한 번만 붙이고, **두 레퍼런스가 같은 트윈을 주장한다는 사실은 말한다** — 알리지 않고 하나를\n * 고르면 어느 접속이 그 트윈을 먹이고 있는지 아무도 모르게 된다.\n */\n const attached = new Map<string, string>() // instanceId → 먼저 붙인 레퍼런스\n try {\n const refs = await getRepository(TwinReference).find({ where: { status: 'connected' }, relations: ['domain'] })\n for (const ref of refs) {\n const domainId = (ref as any).domain?.id ?? (ref as any).domainId\n const adapter = getAdapter(ref.adapterType)\n if (!domainId || !adapter?.openLiveFeed) continue // 관측만 하는 어댑터에는 되살릴 피드가 없다\n\n const produced: any[] = ((ref.scopeSpec as any)?.produced ?? []).filter((p: any) => p?.instanceId && p?.siteId)\n if (!produced.length) continue\n\n /* 등록부가 「도는 미러」라고 말하는 것만 고른다. */\n const rows = await getRepository(TwinInstance).find({\n where: produced.map((p: any) => ({ domain: { id: domainId }, instanceId: p.instanceId })) as any\n })\n const live = produced.filter((p: any) =>\n rows.some(r => r.instanceId === p.instanceId && r.status === 'running' && r.restartPolicy === 'resync')\n )\n if (!live.length) continue\n\n let sites: SiteDescriptor[]\n try {\n sites = await adapter.discoverSites(ref.connectionConfig ?? {})\n } catch (e: any) {\n /* 원 시스템이 아직 안 떴을 수 있다 — 지어내지 않고 그렇게 적는다(트윈은 계속 「도는 미러」다). */\n for (const p of live) failed.push({ instanceId: p.instanceId, error: `discover failed: ${e?.message ?? 'unknown'}` })\n continue\n }\n\n for (const p of live) {\n const site = sites.find(s => s.siteId === p.siteId)\n if (!site) {\n failed.push({ instanceId: p.instanceId, error: `site \"${p.siteId}\" no longer discoverable` })\n continue\n }\n const owner = attached.get(p.instanceId)\n if (owner) {\n /* 두 접속이 같은 트윈을 주장한다 — 먼저 붙은 것을 지키고, 그 사실을 남긴다(둘 다 붙이면 미러가 리셋된다). */\n failed.push({\n instanceId: p.instanceId,\n error: `also claimed by reference \"${ref.source}\" — kept the feed from \"${owner}\" (two connections feeding one twin would reset the mirror)`\n })\n continue\n }\n attached.set(p.instanceId, ref.source)\n try {\n const ok = await startReferenceLiveFeed(domainId, ref.adapterType, ref.connectionConfig ?? {}, site, p.instanceId, ref.source)\n if (ok) resumed.push(p.instanceId)\n else failed.push({ instanceId: p.instanceId, error: 'adapter has no openLiveFeed' })\n } catch (e: any) {\n failed.push({ instanceId: p.instanceId, error: e?.message ?? 'reattach failed' })\n }\n }\n }\n } catch (e: any) {\n twinError('[twin-live] feed resume scan failed', e?.message)\n }\n\n if (resumed.length) twinLog(`[twin-live] reattached ${resumed.length} feed(s): ${resumed.join(', ')}`)\n /* **알리지 않고 넘기지 않는다** — 붙지 않은 미러는 「도는데 아무것도 오지 않는」 트윈이다. */\n for (const f of failed) twinWarn(`[twin-live] \"${f.instanceId}\" feed NOT reattached — ${f.error}`)\n return { resumed, failed }\n}\n\n/*\n * 이 계층의 피드 장부를 등록한다 — 화면이 「끊김」을 말할 근거는 **묻는 자리 하나**여야 한다.\n * (프리셋 기반 피드는 앱이 자기 장부를 따로 등록한다. 한쪽만 보면 흐르는 트윈을 끊겼다고 부른다.)\n */\nregisterLiveFeedProbe(() => [...feeds.keys()])\n\n/* 인스턴스 정지 시 피드 자동 정리(생명주기 대칭). */\nTwinEngine.onStop(instanceId => stopReferenceLiveFeed(instanceId))\n"]}
1
+ {"version":3,"file":"reference-live.js","sourceRoot":"","sources":["../../../server/service/reference/reference-live.ts"],"names":[],"mappings":"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","sourcesContent":["import { twinLog, twinWarn, twinError } from '../../engine/log.js'\nimport { getRepository } from '@things-factory/shell'\n\nimport { getAdapter, type ConnectionConfig, type LiveFeedContinuity, type ObservedItemFact, type SiteDescriptor } from './reference-adapter.js'\n/* 이름 짓는 규칙은 가벼운 파일에 있다 — 테스트가 붙게(§`live-feed-lease.ts`). */\nimport { liveFeedLeaseName } from './live-feed-lease.js'\nexport { liveFeedLeaseName } from './live-feed-lease.js'\nimport { databaseCommandStore } from '../actuation/command-store.js'\nimport { settleEffects } from '../actuation/settle-effects.js'\nimport { TwinEngine, ingestCanonicalRecords, registerLiveFeedProbe } from '../../engine/index.js'\nimport { type CanonicalRecord } from '@operato/ops-contract'\nimport { TwinInstance } from '../twin-instance/twin-instance.js'\nimport { TwinReference } from './twin-reference.js'\n\n/*\n * 커넥터 기반 라이브 피드 결선(reference → live, §0 프레임 ① restartPolicy='resync').\n * 흐름: adapter.openLiveFeed(cfg, site, records) → ingestCanonicalRecords(정규→봉투) → TwinEngine.ingestLive(projector 미러).\n * 커넥터가 \"벤더 이벤트 → 정규 레코드\" 매핑을 소유(코드)한다 — preset 기반 startLiveFeed(operato-twin, http-get+jsonata)와\n * 대칭이되, 매핑 위치만 다르다. 실 시스템 전환은 connectionConfig.baseUrl 교체뿐(코드 경로 동일).\n * 생명주기: 인스턴스 정지(TwinEngine.stop)에 훅으로 묶여 피드도 함께 unsubscribe.\n */\n\n/** 활성 라이브 피드 — instanceId → unsubscribe(어댑터 openLiveFeed 반환). */\nconst feeds = new Map<string, () => void>()\n\n/**\n * 커넥터 라이브 기동 — 어댑터 openLiveFeed 를 인스턴스에 결선.\n * 반환: 라이브 결선 성공 여부(어댑터가 openLiveFeed 미지원이면 false → 호출측이 sim 폴백).\n */\nexport async function startReferenceLiveFeed(\n domainId: string,\n adapterType: string,\n cfg: ConnectionConfig,\n site: SiteDescriptor,\n instanceId: string,\n /**\n * **어느 레퍼런스에서 읽는 피드인가** — 읽기 커서를 저장할 자리를 가리킨다(§`loadLiveCursor`).\n *\n * 부르는 쪽이 `TwinReference` 를 이미 들고 있으므로 그 이름을 넘긴다. 처음에는 `cfg`·`site` 에서\n * 찾으려 했는데 **둘 다 그 값을 가질 수 없다**: `cfg` 는 사람이 화면에서 입력한 접속 설정이고\n * (`endpoint`·`token`…), `site` 는 커넥터가 원본에서 발견한 것이다 — **레퍼런스의 이름은 호스트가\n * 붙이는 것이고 원본은 그것을 모른다.** 커넥터가 채울 수 없는 값을 커넥터에서 읽으려 한 것이 잘못이었다.\n *\n * 주지 않으면 커서를 잇지 않고 **그 사실을 말한다**(알리지 않고 창이 미끄러지지 않게).\n */\n source?: string\n): Promise<boolean> {\n const adapter = getAdapter(adapterType)\n if (!adapter?.openLiveFeed) return false\n\n const det = await TwinEngine.detail(domainId, instanceId)\n if (!det?.model) throw new Error(`instance \"${instanceId}\" not provisioned (no model) — ingest master before live start`)\n\n // 관측 기동 — 커널 tick 없이 projector 가 실 이벤트를 반영한다(startLive 가 restartPolicy='resync' 로 적는다).\n // 시각 기준은 **공간**이 갖는다(테넌트 아님) — 교대의 HH:MM 을 어느 기준으로 읽나.\n TwinEngine.startLive(instanceId, domainId, det.kind, await TwinEngine.withSpaceTimeBase(det.model, domainId))\n\n stopReferenceLiveFeed(instanceId) // 재기동 시 이전 피드 정리\n /*\n * **어디까지 읽었나를 이 층이 들고 건넨다**(§`LiveFeedContinuity`, 2026-08-23).\n *\n * 어댑터가 이 값을 자기 사물함에 두면 원본이 늘 때마다 저장 기제가 늘고, 그중 하나가 알리지 않고 다르게\n * 동작한다. 그리고 커널이 이미 갈라 두었다: 「원천이 애초에 다시 말해 주지 않는 축」은 재기동\n * 연속성으로 이어받는다(§`hydrateContinuity`) — 읽기 커서가 정확히 그 성질이다.\n *\n * The object itself is handed over even on a first attach, because the feed needs its lease name\n * before it has read anything. `cursor` staying absent is what says \"first attach\" — which is how\n * every connector already reads it (`continuity?.cursor`), so nothing changes for them. What is\n * never filled in with an empty value is the cursor: an empty one would make a first attach look\n * like a resumed one, and the adapter would build no look-back window.\n */\n const carried = await loadLiveCursor(domainId, instanceId, source)\n const continuity: LiveFeedContinuity = { ...carried, leaseName: liveFeedLeaseName(domainId, instanceId, source) }\n /*\n * The lease name is composed here, not in the adapter, because it has to be unique across\n * domains and an adapter is handed neither `domainId` nor `instanceId` (§`LiveFeedContinuity`).\n * Built from `source` rather than `site.siteId`: one reference is one connection to one system,\n * and that is the thing a second instance must not open twice.\n */\n /*\n * ── **트윈이 아는 것을 함께 건넨다** (2026-08-28) ───────────────────────────\n *\n * 전량 절대값을 읽는 커넥터는 지난 주기와 견주어 바뀐 것만 낸다. 그 비교 표가 프로세스 안에만\n * 있어서 재기동하면 커넥터가 전량을 「바뀐 것」으로 판정했다 — 승화푸드에서 2,665건이다.\n *\n * 트윈은 저장본에서 되세우므로 잃지 않았다. 그래서 그 사실을 건네면 첫 주기부터 바뀐 것만 나간다.\n * 커널 어휘로 건넨다 — 원본 낱말로 되돌리는 일은 여러 낱말이 한 값으로 모여 불가능하고, 필요도 없다.\n */\n continuity.observedItems = observedItemsOf(domainId, instanceId)\n const unsubscribe = adapter.openLiveFeed(cfg, site, (records: unknown[]) => {\n if (!TwinEngine.owns(domainId, instanceId)) {\n stopReferenceLiveFeed(instanceId) // 인스턴스 사라지면 자가 정지(안전망)\n return\n }\n const offered = Array.isArray(records) ? records.length : records ? 1 : 0\n const { accepted, rejected, masterData } = ingestCanonicalRecords(\n records as CanonicalRecord[],\n domainId,\n new Date().toISOString(),\n TwinEngine.factScope(domainId, instanceId)\n )\n /*\n * ── **사건이 아닌 것은 상태만 세운다** (2026-08-28) ─────────────────────────\n *\n * 마스터데이터 — 변하지 않는 속성(유통기한·로트번호). 시각축이 없는 값이므로 지난 기록에 적지\n * 않는다(§`applyMasterData`). 이 문이 없던 동안 그 값은 사건에 실려 왔고, 표준이 그것을\n * 「들어왔다」에만 허용하므로 전량을 읽는 원본은 **들어오지 않은 것을 들어왔다고** 말해야 했다.\n *\n * 반영된 건수를 함께 센다 — 라이브로 돌지 않는 트윈은 0 을 내므로, 그 차이가 「바닥에 버려진 수」다.\n */\n const masterApplied = masterData.length ? TwinEngine.applyMasterData(domainId, instanceId, masterData) : 0\n if (masterData.length && masterApplied < masterData.length) {\n twinWarn(\n `[twin-live] \"${instanceId}\": ${masterData.length - masterApplied}/${masterData.length} master data element(s) NOT applied — ` +\n 'the twin is not running live; lot attributes such as expiry will stay empty.'\n )\n }\n /*\n * 커널이 **몇 건을 실제로 받았나**를 함께 본다. 트윈이 라이브로 돌지 않으면 `ingestLive` 가 0 을\n * 돌려주고, 그 차이가 곧 「바닥에 버려진 수」다. 이것을 통과로 세면 멈춘 트윈이 100% 로 보인다.\n */\n /* 어떤 길로 들어왔는지 함께 적는다 — 화면이 「이 트윈은 무엇을 통해 받나」를 말할 수 있게. */\n const applied = accepted.length ? TwinEngine.ingestLive(domainId, instanceId, accepted, 'connector') : 0\n\n /*\n * **나간 지시와 잇는다** — 폐루프. 훅과 같은 자리다(§`routes.ts`). 기다리지 않는다: 라이브 묶음\n * 처리가 이것 때문에 늦으면 뒤의 묶음이 밀리고, 밀린 것이 크래시로 이어진 적이 있다.\n */\n void settleEffects({ domainId, envelopes: accepted, store: databaseCommandStore() })\n const undelivered = accepted.length - applied\n /*\n * **거부한 것을 알리지 않고 버리지 않는다.** 예전에는 `rejected` 를 받지도 않았다 — 커넥터의 매핑이\n * 틀리면 트윈은 「아무 일도 일어나지 않는」 모습이 되고, 로그에도 아무 흔적이 없었다.\n *\n * 그다음 자리였던 「화면까지 나르기」를 이제 채운다. 건마다 브로드캐스팅하지 않고 **집계**한다 — 통과분까지\n * 함께 세는 이유는, 거부만 세면 「유입 없음」과 「정상」이 둘 다 거부 0 으로 같아 보이기 때문이다.\n * 조회는 `twinIngestHealth`.\n */\n /*\n * **읽었다는 것 자체가 사실이다** — 빈 읽기도 성공이므로 단절 기록을 지운다(§`onReadFailure`).\n * 그 둘을 섞으면 들어온 것이 없는 연결이 끊긴 원본으로 보인다.\n */\n TwinEngine.clearIngestReadFailure(domainId, instanceId)\n TwinEngine.recordIngestResult(domainId, instanceId, offered, rejected, Date.now(), undelivered)\n if (rejected.length) {\n const why = rejected[0]?.errors?.join(' · ') ?? 'unknown'\n twinWarn(`[twin-live] \"${instanceId}\": ${rejected.length}/${offered} record(s) rejected — e.g. ${why}`)\n }\n /* 버려진 것은 로그에도 남긴다 — 화면을 보지 않는 사람도 알아야 한다(사실이 사라지는 중이다). */\n if (undelivered > 0) {\n twinWarn(\n `[twin-live] \"${instanceId}\": ${undelivered}/${offered} record(s) NOT delivered — the twin is not running live; ` +\n `records are being discarded. Start the twin, or disconnect the feed.`\n )\n }\n }, continuity)\n feeds.set(instanceId, unsubscribe)\n return true\n}\n\n/**\n * 트윈이 지금 들고 있는 물품을 커널 어휘로 낸다 — 없으면 `undefined`.\n *\n * 상태가 없으면(트윈이 아직 아무것도 모르면) `undefined` 다. 빈 배열로 메우지 않는다 — 「모른다」와\n * 「하나도 없다」는 다른 사실이고, 커넥터가 그 둘에 다르게 반응해야 한다.\n */\nfunction observedItemsOf(domainId: string, instanceId: string): ObservedItemFact[] | undefined {\n const state = TwinEngine.snapshot(domainId, instanceId)\n const items = (state as any)?.items\n if (!Array.isArray(items)) return undefined\n return items.map((i: any) => ({\n id: String(i?.subLotId ?? i?.epc ?? ''),\n ...(i?.gtin ? { epcClass: String(i.gtin) } : {}),\n ...(i?.location ? { location: String(i.location) } : {}),\n ...(typeof i?.qty === 'number' ? { qty: i.qty } : {}),\n ...(i?.uom ? { uom: String(i.uom) } : {}),\n ...(i?.disposition ? { disposition: String(i.disposition) } : {}),\n ...(typeof i?.expiry === 'number' ? { expiry: i.expiry } : {}),\n ...(i?.lot ? { lot: String(i.lot) } : {})\n })).filter((i: ObservedItemFact) => !!i.id)\n}\n\n/**\n * 그 원본의 읽기 커서를 불러오고, 움직이면 저장하는 통로를 만든다.\n *\n * 커서는 **원본(`TwinReference`)의 것**이지 인스턴스의 것이 아니다 — 한 원본이 여러 인스턴스를 낳고\n * (`scopeSpec.produced`) 읽는 것은 한 번이다. 인스턴스마다 커서를 두면 같은 표를 여러 번 읽는다.\n *\n * 저장 실패는 **삼키지 않고 말한다.** 오류 없이 실패하면 다음 재기동에서 창이 다시 미끄러지고, 그 손실은\n * 오류 없이 사라진다 — 이 컬럼을 만든 이유가 바로 그것이다.\n */\nasync function loadLiveCursor(\n domainId: string,\n /** 읽기 실패를 적을 트윈 — 커서는 레퍼런스의 것이지만 **건강은 인스턴스의 것**이다(한 레퍼런스가 여럿을 낳는다). */\n instanceId: string,\n source?: string\n /*\n * `leaseName` is not this function's to know — it is composed from the domain and instance by the\n * caller. Returning the rest keeps that rule in one place instead of two.\n */\n): Promise<Omit<LiveFeedContinuity, 'leaseName'> | undefined> {\n if (!source) {\n /* 원본을 가리킬 수 없으면 저장할 자리도 없다 — 그 사실을 말한다(알리지 않고 커서 없이 돌지 않게). */\n twinWarn('[twin-live] live cursor not carried — the caller did not say which reference this feed reads from')\n return undefined\n }\n const repo = getRepository(TwinReference)\n const row = await repo.findOne({ where: { domain: { id: domainId } as any, source } }).catch(() => null)\n if (!row) {\n twinWarn(`[twin-live] live cursor not carried — no reference row for source \"${source}\"`)\n return undefined\n }\n const stored = row.liveCursor\n if (stored?.firstAttachedAt) {\n twinLog(`[twin-live] \"${source}\": resuming from stored cursor — knowledge starts ${stored.firstAttachedAt}`)\n }\n return {\n cursor: stored ?? undefined,\n /*\n * **원본에 닿지 못했다** — 어댑터가 알리면 상태에 적는다(§`onReadFailure`).\n *\n * 로그로는 어댑터가 이미 말하고 있었다(재시도 경고). 그러나 **로그는 사람이 볼 때만 값이 있다** —\n * 화면이 말하려면 조회 가능한 상태에 있어야 한다.\n *\n * ── `status` 를 건드리지 않는다 (2026-08-23, 되돌린 자리) ────────────────────\n * 처음에는 레퍼런스 행의 `status` 를 `'error'` 로도 적었다 — 「목록이 「연결됨」이라 말하면서 실제로는\n * 못 읽는 상태를 없애려고」. **그것이 피드를 영구히 껐다.**\n *\n * 부팅 재부착은 `status: 'connected'` 인 레퍼런스만 되살린다(§`resumeReferenceLiveFeeds`). 그래서\n * 원본이 **잠깐** 끊긴 것이 그 행을 `error` 로 만들고, 다음 재기동에서 그 피드는 아예 붙지 않았다 —\n * 실측으로 그렇게 났다(09:25 버전에 `[twin-live]` 줄이 한 줄도 없었다).\n *\n * 두 축을 한 칸에 섞은 것이 원인이다:\n * `status` **연결 설정이 성립하나** — 접속 시험·마스터 동기가 정하는 축(재부착의 자격이다)\n * 읽기 실패 **지금 닿나** — 오갈 수 있는 사실이고, 유입 장부가 그 축을 갖는다\n *\n * 그러니 여기서는 `lastError` 에만 적는다(사람이 목록에서 이유를 볼 수 있게). 「지금 닿나」는\n * `ingestHealth().readFailure` 가 답하고, 성공한 주기가 그것을 지운다.\n */\n onReadFailure: ({ reason, stream, nextRetryMs }) => {\n TwinEngine.recordIngestReadFailure(domainId, instanceId, reason, Date.now(), stream, nextRetryMs)\n twinWarn(`[twin-live] \"${source}\": cannot read from the source${stream ? ` (${stream})` : ''} — ${reason}`)\n repo\n .update({ id: row.id }, { lastError: `live: ${reason}` } as any)\n .catch(err => twinWarn(`[twin-live] \"${source}\": read failure not recorded — ${err?.message ?? err}`))\n },\n /*\n * **끊겼다가 돌아왔다** — 그 사실을 장부에 남긴다 (2026-08-27).\n *\n * 실패 기록을 지우기만 하면 「끊긴 적이 있었다」가 화면에서 사라진다. 그러면 밤새 두 시간 끊겼던\n * 연결과 한 번도 끊기지 않은 연결이 아침에 똑같이 보인다.\n *\n * `lastError` 도 함께 지운다 — 목록에 남은 옛 이유가 지금의 상태로 읽힌다.\n */\n onRecovered: ({ afterFailures, downMs }) => {\n TwinEngine.recordIngestRecovered(domainId, instanceId, afterFailures, downMs, Date.now())\n twinLog(\n `[twin-live] \"${source}\": reading again after ${afterFailures} failed attempt(s) — it was out for ${Math.round(downMs / 1000)}s`\n )\n repo\n .update({ id: row.id }, { lastError: null } as any)\n .catch(err => twinWarn(`[twin-live] \"${source}\": recovery not recorded — ${err?.message ?? err}`))\n },\n /**\n * **읽었는데 창을 넘길 수 없다** — 위와 조치가 반대인 사실이다 (2026-08-24).\n *\n * 이 문을 만들기 전에는 이 사실이 `onReadFailure` 로 나갔다. 그래서 화면이 「원본에 닿지 못한다」고\n * 말했는데 실제로는 원본이 **답한** 상태였고, 그 답의 모양이 커서를 이긴 것이었다. 사람은 원본을\n * 의심하고 기다리는데 기다림으로는 영원히 풀리지 않는다 — **원인을 반대 방향으로 가리켰다.**\n *\n * `status` 를 건드리지 않는 규율은 위와 같다(재부착의 자격이다). `lastError` 에는 적는다 — 목록에서\n * 이유를 볼 수 있어야 하고, 「닿지 못한다」와 다른 문장이어야 한다.\n */\n /*\n * **세우지 않은 것을 장부에 적는다** — 원본에 있는데 트윈에 세우지 않은 것(이유와 수).\n *\n * 로그로도 남기지만 **로그는 사람이 볼 때만 값이 있다.** 이 사실은 상태의 성질이라(세우지 않은 것은\n * 지금도 없다) 조회되는 자리에 있어야 한다 — 그러지 않으면 프로비저닝 화면에서 한 번 스쳐 지나간다.\n *\n * `status`·`lastError` 를 건드리지 않는다: 이것은 **실패가 아니다.** 어댑터가 옳은 판단으로 안 받은\n * 것이고, 오류로 적으면 화면이 원본을 의심하게 만든다.\n */\n onWithheld: ({ reason, count }) => {\n TwinEngine.recordIngestWithheld(domainId, instanceId, reason, count)\n },\n onCursorStall: ({ reason, stream }) => {\n TwinEngine.recordIngestCursorStall(domainId, instanceId, reason, Date.now(), stream)\n twinWarn(\n `[twin-live] \"${source}\": read the source but the cursor cannot advance` +\n `${stream ? ` (${stream})` : ''} — ${reason}. Waiting will not clear this.`\n )\n repo\n .update({ id: row.id }, { lastError: `live cursor stalled: ${reason}` } as any)\n .catch(err => twinWarn(`[twin-live] \"${source}\": cursor stall not recorded — ${err?.message ?? err}`))\n },\n onCursor: next => {\n /* 어댑터가 준 모양을 그대로 적는다 — 이 층은 흐름의 뜻을 모른다(열쇠는 어댑터가 정한다). */\n repo\n .update({ id: row.id }, { liveCursor: next } as any)\n .catch(err => twinWarn(`[twin-live] \"${source}\": live cursor not saved — ${err?.message ?? err}`))\n }\n }\n}\n\n/** 라이브 피드 중지(unsubscribe). */\nexport function stopReferenceLiveFeed(instanceId: string): boolean {\n const unsub = feeds.get(instanceId)\n if (!unsub) return false\n try { unsub() } catch { /* noop */ }\n feeds.delete(instanceId)\n return true\n}\n\n/** 활성 피드 목록(관측). */\nexport function listReferenceLiveFeeds(): string[] {\n return [...feeds.keys()]\n}\n\n/**\n * 부팅 때 **끊어진 피드를 다시 붙인다** — 도는 미러 트윈에 계측이 계속 들어오게.\n *\n * ── 무엇이 알리지 않고 끊겼나 (2026-08-14) ───────────────────────────────────────\n * `startReferenceLiveFeed` 를 부르는 곳은 `importTwinReference` **하나뿐**이었다. 그래서 서버를 한 번\n * 재기동하면 미러 트윈은 「도는 중」이라 말하면서 아무 계측도 받지 않았다 — 오류도 로그도 없이,\n * 사람은 「값이 안 변하네」로 알게 된다. 실 배포에서 재기동 한 번에 계측이 멈추는 셈이다.\n *\n * ── 되살릴 재료는 이미 영속돼 있다 ─────────────────────────────────────────\n * `TwinReference` 가 어댑터 종류·접속 설정·그 레퍼런스가 만든 인스턴스(`scopeSpec.produced`)를 들고 있다.\n * 사이트 서술자만 없는데(`hint` 는 남기지 않았다) **다시 찾아서** 얻는다(`discoverSites`) — 인제스트가\n * 걷는 것과 같은 길이므로 규칙이 갈라지지 않는다. 되살릴 인스턴스가 없으면 그 호출도 하지 않는다.\n *\n * 재개 대상은 **등록부가 도는 미러라고 말하는 것**뿐이다: 멈춘 트윈을 되살리면 사람이 멈춘 것을\n * 우리가 다시 켜는 것이고, 시뮬 트윈에 실 계측을 부으면 두 현실이 섞인다.\n */\nexport async function resumeReferenceLiveFeeds(): Promise<{ resumed: string[]; failed: { instanceId: string; error: string }[] }> {\n const resumed: string[] = []\n const failed: { instanceId: string; error: string }[] = []\n /*\n * **한 트윈에 피드는 하나다** (2026-08-18).\n *\n * 같은 사이트를 두 레퍼런스가 각각 인제스트하면 둘 다 그 인스턴스를 자기가 만든 것(`produced`)으로\n * 들고 있다. 기동 때 그 목록을 그대로 돌면 같은 트윈에 피드를 두 번 붙이는데, 붙이는 함수가 미러를\n * 다시 세우므로(`startLive`) **먼저 쌓인 수요 구간이 리셋된다** — 실제로 그렇게 났다\n * (로그: `reattached 2 feed(s): ems-plant-1, ems-plant-1`).\n *\n * 그러니 한 번만 붙이고, **두 레퍼런스가 같은 트윈을 주장한다는 사실은 말한다** — 알리지 않고 하나를\n * 고르면 어느 접속이 그 트윈을 먹이고 있는지 아무도 모르게 된다.\n */\n const attached = new Map<string, string>() // instanceId → 먼저 붙인 레퍼런스\n try {\n const refs = await getRepository(TwinReference).find({ where: { status: 'connected' }, relations: ['domain'] })\n for (const ref of refs) {\n const domainId = (ref as any).domain?.id ?? (ref as any).domainId\n const adapter = getAdapter(ref.adapterType)\n if (!domainId || !adapter?.openLiveFeed) continue // 관측만 하는 어댑터에는 되살릴 피드가 없다\n\n const produced: any[] = ((ref.scopeSpec as any)?.produced ?? []).filter((p: any) => p?.instanceId && p?.siteId)\n if (!produced.length) continue\n\n /* 등록부가 「도는 미러」라고 말하는 것만 고른다. */\n const rows = await getRepository(TwinInstance).find({\n where: produced.map((p: any) => ({ domain: { id: domainId }, instanceId: p.instanceId })) as any\n })\n const live = produced.filter((p: any) =>\n rows.some(r => r.instanceId === p.instanceId && r.status === 'running' && r.restartPolicy === 'resync')\n )\n if (!live.length) continue\n\n let sites: SiteDescriptor[]\n try {\n sites = await adapter.discoverSites(ref.connectionConfig ?? {})\n } catch (e: any) {\n /* 원 시스템이 아직 안 떴을 수 있다 — 지어내지 않고 그렇게 적는다(트윈은 계속 「도는 미러」다). */\n for (const p of live) failed.push({ instanceId: p.instanceId, error: `discover failed: ${e?.message ?? 'unknown'}` })\n continue\n }\n\n for (const p of live) {\n const site = sites.find(s => s.siteId === p.siteId)\n if (!site) {\n failed.push({ instanceId: p.instanceId, error: `site \"${p.siteId}\" no longer discoverable` })\n continue\n }\n const owner = attached.get(p.instanceId)\n if (owner) {\n /* 두 접속이 같은 트윈을 주장한다 — 먼저 붙은 것을 지키고, 그 사실을 남긴다(둘 다 붙이면 미러가 리셋된다). */\n failed.push({\n instanceId: p.instanceId,\n error: `also claimed by reference \"${ref.source}\" — kept the feed from \"${owner}\" (two connections feeding one twin would reset the mirror)`\n })\n continue\n }\n attached.set(p.instanceId, ref.source)\n try {\n const ok = await startReferenceLiveFeed(domainId, ref.adapterType, ref.connectionConfig ?? {}, site, p.instanceId, ref.source)\n if (ok) resumed.push(p.instanceId)\n else failed.push({ instanceId: p.instanceId, error: 'adapter has no openLiveFeed' })\n } catch (e: any) {\n failed.push({ instanceId: p.instanceId, error: e?.message ?? 'reattach failed' })\n }\n }\n }\n } catch (e: any) {\n twinError('[twin-live] feed resume scan failed', e?.message)\n }\n\n if (resumed.length) twinLog(`[twin-live] reattached ${resumed.length} feed(s): ${resumed.join(', ')}`)\n /* **알리지 않고 넘기지 않는다** — 붙지 않은 미러는 「도는데 아무것도 오지 않는」 트윈이다. */\n for (const f of failed) twinWarn(`[twin-live] \"${f.instanceId}\" feed NOT reattached — ${f.error}`)\n return { resumed, failed }\n}\n\n/*\n * 이 계층의 피드 장부를 등록한다 — 화면이 「끊김」을 말할 근거는 **묻는 자리 하나**여야 한다.\n * (프리셋 기반 피드는 앱이 자기 장부를 따로 등록한다. 한쪽만 보면 흐르는 트윈을 끊겼다고 부른다.)\n */\nregisterLiveFeedProbe(() => [...feeds.keys()])\n\n/* 인스턴스 정지 시 피드 자동 정리(생명주기 대칭). */\nTwinEngine.onStop(instanceId => stopReferenceLiveFeed(instanceId))\n"]}
@@ -27,7 +27,7 @@ exports.referencePercent = referencePercent;
27
27
  * 단계를 선언하지도 보내지도 않는다.
28
28
  */
29
29
  exports.REFERENCE_STEPS = [
30
- /** 원본 스키마 확인 — 무엇을 물을 수 있는지 먼저 본다(판이 다를 수 있다). */
30
+ /** 원본 스키마 확인 — 무엇을 물을 수 있는지 먼저 본다(버전이 다를 수 있다). */
31
31
  'schema',
32
32
  /** 공간·좌표 — 자리가 놓일 좌표계. */
33
33
  'space',
@@ -1 +1 @@
1
- {"version":3,"file":"reference-progress.js","sourceRoot":"","sources":["../../../server/service/reference/reference-progress.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;GAmBG;;;AAsEH,4CAKC;AAzED;;;GAGG;AACU,QAAA,eAAe,GAAG;IAC7B,kDAAkD;IAClD,QAAQ;IACR,0BAA0B;IAC1B,OAAO;IACP,+BAA+B;IAC/B,WAAW;IACX,kBAAkB;IAClB,WAAW;IACX,sBAAsB;IACtB,YAAY;IACZ,qBAAqB;IACrB,WAAW;IACX,mBAAmB;IACnB,QAAQ;IACR,oBAAoB;IACpB,SAAS;IACT,6BAA6B;IAC7B,oBAAoB;IACpB,UAAU;IACV,SAAS;IACT,YAAY;IACZ,QAAQ;IACR,gDAAgD;IAChD,WAAW;IACX,UAAU;IACV,OAAO;CACC,CAAA;AAIV,uDAAuD;AAC1C,QAAA,UAAU,GAAoB,CAAC,WAAW,EAAE,OAAO,CAAC,CAAA;AA2BjE;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,CAAoB;IACnD,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,IAAI,CAAC;QAAE,OAAO,SAAS,CAAA;IAClE,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,SAAS,CAAA;IAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAA;IAClD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,GAAG,CAAC,CAAA;AACrE,CAAC","sourcesContent":["/*\n * 유입 진행 단계 — **모든 커넥터가 같은 낱말로 말한다.**\n *\n * ── 왜 표준화하나 (2026-08-21) ──────────────────────────────────────────────\n * 마스터 읽기는 원본에 여러 질의를 보내는 긴 작업이고, 그 사이 화면은 아무것도 알 수 없었다. 커넥터마다\n * 자기 낱말로 말하게 두면 화면이 커넥터 수만큼 문구를 알아야 하고, 새 커넥터가 생기면 화면에 낯선 글자가\n * 나온다.\n *\n * 그래서 낱말을 고정한다. 그리고 그 낱말은 **트윈의 자리 이름**이다 — 커넥터가 지금 채우는 자리가 곧\n * 지금 하는 일이다(`design/integration/connector-checklist.md` 의 마스터 축과 같은 이름).\n *\n * 그러면 셋이 함께 얻어진다.\n * · 화면은 **단계마다 하나의 번역**만 두면 모든 커넥터를 보여 준다.\n * · 사람은 원본이 달라도 **같은 말로** 진행을 읽는다.\n * · 실패했을 때 「어느 자리를 읽다 실패했다」가 곧 「트윈의 무엇이 비었다」가 된다.\n *\n * ── 퍼센트를 만들려면 분모가 있어야 한다 ────────────────────────────────────\n * 어댑터가 **자기가 보낼 단계를 미리 선언**한다(`ReferenceAdapter.masterSteps`). 그것이 분모다. 선언하지\n * 않으면 화면은 퍼센트를 보이지 않고 단계 이름만 보인다 — **모르는 진행률을 지어내지 않는다.**\n */\n\n/**\n * 표준 단계. 순서는 대개 이 차례이지만 **강제하지 않는다** — 원본에 따라 없는 자리가 있고, 그때는 그\n * 단계를 선언하지도 보내지도 않는다.\n */\nexport const REFERENCE_STEPS = [\n /** 원본 스키마 확인 — 무엇을 물을 수 있는지 먼저 본다(판이 다를 수 있다). */\n 'schema',\n /** 공간·좌표 — 자리가 놓일 좌표계. */\n 'space',\n /** 자리 — 물건이 놓이고 작업이 일어나는 곳. */\n 'locations',\n /** 설비 — 일하는 것. */\n 'equipment',\n /** 공정 — 무슨 일을 하는가. */\n 'operations',\n /** 자재 — 무엇이 흐르는가. */\n 'materials',\n /** 경로 — 공정의 순서. */\n 'routes',\n /** 레시피 — 투입과 산출. */\n 'recipes',\n /** 시험 명세 — 무엇을 어떻게 검사하는가. */\n 'testSpecifications',\n /** 인원. */\n 'persons',\n /** 물리자산. */\n 'assets',\n /** 트윈 생성 — 여기부터는 호스트가 하는 일이다(어댑터가 선언하지 않는다). */\n 'provision',\n /** 기동. */\n 'start'\n] as const\n\nexport type ReferenceStep = (typeof REFERENCE_STEPS)[number]\n\n/** 호스트가 마스터 읽기 뒤에 하는 단계 — 어댑터의 선언에 이만큼을 더해 분모를 만든다. */\nexport const HOST_STEPS: ReferenceStep[] = ['provision', 'start']\n\n/**\n * 한 단계의 진행 — 구독으로 흐르는 메시지의 몸통.\n *\n * `detail` 은 **그 단계 안에서 사람이 알아야 하는 것**이다(읽은 건수 등). 비어 있어도 된다 — 지어내지\n * 않는다.\n */\nexport interface ReferenceProgress {\n /** 어느 사이트를 만드는 중인가. */\n siteId: string\n step: ReferenceStep\n /** 이 사이트에서 끝낸 단계 수. */\n done: number\n /** 이 사이트의 전체 단계 수 — 어댑터가 선언하지 않으면 없다(그때 퍼센트를 만들지 않는다). */\n total?: number\n /** 여러 사이트를 한 번에 만들 때, 지금 몇 번째인가. */\n siteIndex: number\n siteCount: number\n /** 그 단계 안의 사실 — 「자리 128건」처럼. */\n detail?: string\n /** 이 단계에서 실패했는가. 실패해도 화면이 그 자리를 그대로 보여 줄 수 있어야 한다. */\n failed?: boolean\n /** 실패 이유 — 실패했을 때만. */\n error?: string\n}\n\n/**\n * 전체 진행률 — **분모를 알 때만 낸다.**\n *\n * 사이트가 여럿이면 사이트 사이의 진행도 함께 센다: 앞선 사이트는 끝난 것으로 본다.\n */\nexport function referencePercent(p: ReferenceProgress): number | undefined {\n if (!p.total || p.total <= 0 || p.siteCount <= 0) return undefined\n const perSite = 1 / p.siteCount\n const within = Math.min(p.done, p.total) / p.total\n return Math.round((p.siteIndex * perSite + within * perSite) * 100)\n}\n"]}
1
+ {"version":3,"file":"reference-progress.js","sourceRoot":"","sources":["../../../server/service/reference/reference-progress.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;GAmBG;;;AAsEH,4CAKC;AAzED;;;GAGG;AACU,QAAA,eAAe,GAAG;IAC7B,mDAAmD;IACnD,QAAQ;IACR,0BAA0B;IAC1B,OAAO;IACP,+BAA+B;IAC/B,WAAW;IACX,kBAAkB;IAClB,WAAW;IACX,sBAAsB;IACtB,YAAY;IACZ,qBAAqB;IACrB,WAAW;IACX,mBAAmB;IACnB,QAAQ;IACR,oBAAoB;IACpB,SAAS;IACT,6BAA6B;IAC7B,oBAAoB;IACpB,UAAU;IACV,SAAS;IACT,YAAY;IACZ,QAAQ;IACR,gDAAgD;IAChD,WAAW;IACX,UAAU;IACV,OAAO;CACC,CAAA;AAIV,uDAAuD;AAC1C,QAAA,UAAU,GAAoB,CAAC,WAAW,EAAE,OAAO,CAAC,CAAA;AA2BjE;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,CAAoB;IACnD,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,IAAI,CAAC;QAAE,OAAO,SAAS,CAAA;IAClE,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,SAAS,CAAA;IAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAA;IAClD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,GAAG,CAAC,CAAA;AACrE,CAAC","sourcesContent":["/*\n * 유입 진행 단계 — **모든 커넥터가 같은 낱말로 말한다.**\n *\n * ── 왜 표준화하나 (2026-08-21) ──────────────────────────────────────────────\n * 마스터 읽기는 원본에 여러 질의를 보내는 긴 작업이고, 그 사이 화면은 아무것도 알 수 없었다. 커넥터마다\n * 자기 낱말로 말하게 두면 화면이 커넥터 수만큼 문구를 알아야 하고, 새 커넥터가 생기면 화면에 낯선 글자가\n * 나온다.\n *\n * 그래서 낱말을 고정한다. 그리고 그 낱말은 **트윈의 자리 이름**이다 — 커넥터가 지금 채우는 자리가 곧\n * 지금 하는 일이다(`design/integration/connector-checklist.md` 의 마스터 축과 같은 이름).\n *\n * 그러면 셋이 함께 얻어진다.\n * · 화면은 **단계마다 하나의 번역**만 두면 모든 커넥터를 보여 준다.\n * · 사람은 원본이 달라도 **같은 말로** 진행을 읽는다.\n * · 실패했을 때 「어느 자리를 읽다 실패했다」가 곧 「트윈의 무엇이 비었다」가 된다.\n *\n * ── 퍼센트를 만들려면 분모가 있어야 한다 ────────────────────────────────────\n * 어댑터가 **자기가 보낼 단계를 미리 선언**한다(`ReferenceAdapter.masterSteps`). 그것이 분모다. 선언하지\n * 않으면 화면은 퍼센트를 보이지 않고 단계 이름만 보인다 — **모르는 진행률을 지어내지 않는다.**\n */\n\n/**\n * 표준 단계. 순서는 대개 이 차례이지만 **강제하지 않는다** — 원본에 따라 없는 자리가 있고, 그때는 그\n * 단계를 선언하지도 보내지도 않는다.\n */\nexport const REFERENCE_STEPS = [\n /** 원본 스키마 확인 — 무엇을 물을 수 있는지 먼저 본다(버전이 다를 수 있다). */\n 'schema',\n /** 공간·좌표 — 자리가 놓일 좌표계. */\n 'space',\n /** 자리 — 물건이 놓이고 작업이 일어나는 곳. */\n 'locations',\n /** 설비 — 일하는 것. */\n 'equipment',\n /** 공정 — 무슨 일을 하는가. */\n 'operations',\n /** 자재 — 무엇이 흐르는가. */\n 'materials',\n /** 경로 — 공정의 순서. */\n 'routes',\n /** 레시피 — 투입과 산출. */\n 'recipes',\n /** 시험 명세 — 무엇을 어떻게 검사하는가. */\n 'testSpecifications',\n /** 인원. */\n 'persons',\n /** 물리자산. */\n 'assets',\n /** 트윈 생성 — 여기부터는 호스트가 하는 일이다(어댑터가 선언하지 않는다). */\n 'provision',\n /** 기동. */\n 'start'\n] as const\n\nexport type ReferenceStep = (typeof REFERENCE_STEPS)[number]\n\n/** 호스트가 마스터 읽기 뒤에 하는 단계 — 어댑터의 선언에 이만큼을 더해 분모를 만든다. */\nexport const HOST_STEPS: ReferenceStep[] = ['provision', 'start']\n\n/**\n * 한 단계의 진행 — 구독으로 흐르는 메시지의 몸통.\n *\n * `detail` 은 **그 단계 안에서 사람이 알아야 하는 것**이다(읽은 건수 등). 비어 있어도 된다 — 지어내지\n * 않는다.\n */\nexport interface ReferenceProgress {\n /** 어느 사이트를 만드는 중인가. */\n siteId: string\n step: ReferenceStep\n /** 이 사이트에서 끝낸 단계 수. */\n done: number\n /** 이 사이트의 전체 단계 수 — 어댑터가 선언하지 않으면 없다(그때 퍼센트를 만들지 않는다). */\n total?: number\n /** 여러 사이트를 한 번에 만들 때, 지금 몇 번째인가. */\n siteIndex: number\n siteCount: number\n /** 그 단계 안의 사실 — 「자리 128건」처럼. */\n detail?: string\n /** 이 단계에서 실패했는가. 실패해도 화면이 그 자리를 그대로 보여 줄 수 있어야 한다. */\n failed?: boolean\n /** 실패 이유 — 실패했을 때만. */\n error?: string\n}\n\n/**\n * 전체 진행률 — **분모를 알 때만 낸다.**\n *\n * 사이트가 여럿이면 사이트 사이의 진행도 함께 센다: 앞선 사이트는 끝난 것으로 본다.\n */\nexport function referencePercent(p: ReferenceProgress): number | undefined {\n if (!p.total || p.total <= 0 || p.siteCount <= 0) return undefined\n const perSite = 1 / p.siteCount\n const within = Math.min(p.done, p.total) / p.total\n return Math.round((p.siteIndex * perSite + within * perSite) * 100)\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 짐작하지 않는 규칙.
3
3
  *
4
- * order 오더가 말했다. 판이 둘이든 열이든 그것을 쓴다
4
+ * order 오더가 말했다. 버전이 둘이든 열이든 그것을 쓴다
5
5
  * only-candidate 오더는 말 안 했지만 후보가 하나다 — 고를 것이 없으면 짐작이 아니다
6
6
  * ambiguous 후보가 둘 이상인데 말해 주는 것이 없다 → **답하지 않는다**
7
7
  * said-but-undeclared 오더는 말했는데 그 레시피가 선언에 없다 — 모르는 것이 아니라 어긋난 것이다
@@ -11,7 +11,7 @@ exports.chooseRecipe = chooseRecipe;
11
11
  /**
12
12
  * 짐작하지 않는 규칙.
13
13
  *
14
- * order 오더가 말했다. 판이 둘이든 열이든 그것을 쓴다
14
+ * order 오더가 말했다. 버전이 둘이든 열이든 그것을 쓴다
15
15
  * only-candidate 오더는 말 안 했지만 후보가 하나다 — 고를 것이 없으면 짐작이 아니다
16
16
  * ambiguous 후보가 둘 이상인데 말해 주는 것이 없다 → **답하지 않는다**
17
17
  * said-but-undeclared 오더는 말했는데 그 레시피가 선언에 없다 — 모르는 것이 아니라 어긋난 것이다