export * from './engine/index.js' export * from './service/index.js' /* 스키마 이력 — 프레임워크가 `migrations` export 를 모아 실행한다(`run migration`). */ export { migrations } from './migrations/index.js' import { twinLog, twinError } from './engine/log.js' import './routes.js' import { TwinEngine } from './engine/index.js' import { resumeReferenceLiveFeeds } from './service/reference/reference-live.js' import { installIngestWindowWriter } from './service/twin-ingest-window/index.js' import { installReferenceLinkResolver } from './service/reference/reference-links.js' import { installAreaResolver } from './service/twin-space/area-resolver.js' import { startActuationRuleLoop } from './service/actuation/rule-loop.js' import { startFillLoop } from './service/reference/fill-loop.js' /* * 모듈 부팅 — **도는 트윈을 되살린다.** * * 두 층이 순서대로다: ① 엔진이 등록부의 `running` 행을 선언된 `restartPolicy` 그대로 되살리고 * (`bootstrap`), ② 레퍼런스 계층이 미러 트윈의 커넥터 피드를 다시 붙인다. 순서가 뒤집히면 피드가 * 아직 없는 커널에 계측을 붓게 된다. * * 예전에는 ①도 ②도 없었다 — 재기동 한 번에 등록부만 「도는 중」이라 말하는 트윈이 남았다. */ process.on('bootstrap-module-start' as any, async ({ app, config, client }: any) => { try { /* * 닫힌 인제스트 창을 표로 옮기는 문을 먼저 연다 — 트윈을 되살리기 **전에** 등록해야 첫 창부터 * 남는다. 커널 층은 저장 계층을 모르므로 이 등록이 그 둘을 잇는 유일한 자리다. */ installIngestWindowWriter() /* * 원본에만 있는 관계를 조회하는 문을 연다 — MES 의 생산 실적·정비 계획처럼 트윈이 담지 않는 * 것들. 조회용 참조는 트윈으로 복제하지 않고 그때 물어본다. */ installReferenceLinkResolver() /* * 구역 이름을 답하는 문 — 자리의 부모가 구역일 때 화면이 id 대신 이름을 보인다. 등록이 없던 * 동안에는 모든 자리의 부모가 「풀 수 있는 부품이 없다」로 나왔다(2026-09-18 실측). */ installAreaResolver(m => twinLog(m)) /* * **저널 보존 기간은 선언에서 온다** — 코드에 박아 두지 않는다(2026-08-22). * * 실측: 개발 저널이 시간당 581,794행 · 0.85GB(하루 20GB) 로 자랐고 지우는 것이 없었다. 그런데 * 「며칠까지 남기나」는 **배포마다 갈리는 사실**이다 — 개발은 짧아도 되고 실증은 길어야 한다. * * 선언이 없으면 **아무것도 지우지 않는다.** 알리지 않고 지우는 편이 알리지 않고 쌓는 것보다 나쁘다. * 값은 스냅샷 TTL(7일)보다 짧을 수 없다 — 엔진이 그것을 올려 주고 그 사실을 말한다. */ /* * 읽는 순서: **환경변수 → 설정 → 없음.** * * 설정(`config.twin.journalRetentionDays`)이 배포의 선언이고, 환경변수 * (`TWIN_JOURNAL_RETENTION_DAYS`)는 그것을 **그 자리에서** 덮는다. 컨테이너로 도는 배포에서는 * 파일을 고치지 않고 값을 바꿀 수 있어야 하고, 개발에서도 그 편이 빠르다. * * 둘 다 없으면 **아무것도 지우지 않는다.** */ const fromEnv = Number(process.env.TWIN_JOURNAL_RETENTION_DAYS ?? 0) const fromConfig = Number(config?.get?.('twin')?.journalRetentionDays ?? 0) const retentionDays = fromEnv > 0 ? fromEnv : fromConfig if (retentionDays > 0) { TwinEngine.JOURNAL_RETENTION_DAYS = retentionDays twinLog(`[twin] journal retention ${retentionDays}일 (${fromEnv > 0 ? '환경변수' : '설정'})`) } await TwinEngine.bootstrap() await resumeReferenceLiveFeeds() /* * 빠진 구간을 다시 받는 주기 — 기동에서 한 번, 그 뒤 10분마다(§`fill-loop.ts`). * 피드를 붙인 **뒤에** 세운다: 커널이 되살아나기 전에 부르면 도는 트윈이 0개다. */ startFillLoop() /* * 조치 규칙 주기 — **트윈이 다 선 뒤에** 세운다. 앞에 세우면 첫 바퀴가 아직 안 선 트윈들을 돌고 * 「상태를 읽을 수 없다」를 규칙 행에 적는다. 그것은 사실이지만 사람에게는 오해다. * * 커널 틱과 따로 돈다 — 틱은 예외가 나면 그 트윈을 멈추고, 규칙 하나가 잘못됐다고 공장이 멈추면 * 안 된다(§`rule-loop.ts`). */ startActuationRuleLoop() twinLog('Headless Twin host has started.') } catch (ex) { twinError('Headless Twin host failed to start.', ex) } })