@things-factory/headless-twin 10.0.18 → 10.0.19

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 (43) hide show
  1. package/dist-server/routes.js +12 -1
  2. package/dist-server/routes.js.map +1 -1
  3. package/dist-server/service/actuation/command-dispatcher.d.ts +32 -0
  4. package/dist-server/service/actuation/command-dispatcher.js +92 -0
  5. package/dist-server/service/actuation/command-dispatcher.js.map +1 -0
  6. package/dist-server/service/actuation/command-store.d.ts +3 -0
  7. package/dist-server/service/actuation/command-store.js +56 -0
  8. package/dist-server/service/actuation/command-store.js.map +1 -0
  9. package/dist-server/service/actuation/index.d.ts +6 -0
  10. package/dist-server/service/actuation/index.js +11 -0
  11. package/dist-server/service/actuation/index.js.map +1 -0
  12. package/dist-server/service/actuation/twin-command.d.ts +30 -0
  13. package/dist-server/service/actuation/twin-command.js +110 -0
  14. package/dist-server/service/actuation/twin-command.js.map +1 -0
  15. package/dist-server/service/index.d.ts +1 -1
  16. package/dist-server/service/index.js +20 -17
  17. package/dist-server/service/index.js.map +1 -1
  18. package/dist-server/service/reference/hook-contract.d.ts +64 -21
  19. package/dist-server/service/reference/hook-contract.js +134 -22
  20. package/dist-server/service/reference/hook-contract.js.map +1 -1
  21. package/dist-server/service/reference/hook-store.d.ts +3 -0
  22. package/dist-server/service/reference/hook-store.js +28 -0
  23. package/dist-server/service/reference/hook-store.js.map +1 -0
  24. package/dist-server/service/reference/reference-adapter.d.ts +65 -1
  25. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  26. package/dist-server/service/reference/reference-hook.d.ts +42 -6
  27. package/dist-server/service/reference/reference-hook.js +185 -27
  28. package/dist-server/service/reference/reference-hook.js.map +1 -1
  29. package/package.json +7 -7
  30. package/server/routes.ts +12 -1
  31. package/server/service/actuation/command-dispatcher.ts +130 -0
  32. package/server/service/actuation/command-store.ts +62 -0
  33. package/server/service/actuation/index.ts +8 -0
  34. package/server/service/actuation/twin-command.ts +105 -0
  35. package/server/service/index.ts +3 -0
  36. package/server/service/reference/hook-contract.ts +99 -21
  37. package/server/service/reference/hook-store.ts +28 -0
  38. package/server/service/reference/reference-adapter.ts +67 -2
  39. package/server/service/reference/reference-hook.ts +249 -32
  40. package/test/actuation-dispatch.test.ts +196 -0
  41. package/test/hook-sequence.test.ts +333 -0
  42. package/test/reference-hook.test.ts +114 -23
  43. package/tsconfig.tsbuildinfo +1 -1
@@ -20,6 +20,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
20
20
  const canonical_ingest_js_1 = require("./engine/canonical-ingest.js");
21
21
  const twin_engine_js_1 = require("./engine/twin-engine.js");
22
22
  const reference_hook_js_1 = require("./service/reference/reference-hook.js");
23
+ const hook_store_js_1 = require("./service/reference/hook-store.js");
23
24
  process.on('bootstrap-module-domain-public-route', (_app, router) => {
24
25
  /*
25
26
  * 주소에 트윈까지 적는다 — 어느 트윈에 넣을지 짐작하지 않는다.
@@ -40,6 +41,12 @@ process.on('bootstrap-module-domain-public-route', (_app, router) => {
40
41
  instanceId: String(ctx.params.instanceId ?? ''),
41
42
  headers: (ctx.request?.headers ?? {}),
42
43
  body: ctx.request?.body,
44
+ /*
45
+ * 받은 바이트 그대로 — 서명을 확인하려면 이것이 있어야 한다. `koa-bodyparser` 가 파싱한 본문과
46
+ * 함께 남겨 준다. 파싱본을 다시 문자열로 만들면 키 순서와 공백이 달라져 서명이 맞지 않는다.
47
+ */
48
+ rawBody: typeof ctx.request?.rawBody === 'string' ? ctx.request.rawBody : undefined,
49
+ store: (0, hook_store_js_1.databaseStore)(),
43
50
  /*
44
51
  * 폴링이 쓰는 길을 그대로 쓴다 — 검증도 유입도 같은 자리다. 그래서 트윈 쪽에서는 물어서 받은
45
52
  * 것과 밀어 받은 것이 구별되지 않는다. 구별되는 것은 유입 장부의 `sources` 한 줄뿐이다.
@@ -56,7 +63,11 @@ process.on('bootstrap-module-domain-public-route', (_app, router) => {
56
63
  */
57
64
  const undelivered = Math.max(0, accepted.length - applied - duplicates);
58
65
  twin_engine_js_1.TwinEngine.recordIngestResult(domain.id, instanceId, records.length, rejected, Date.now(), undelivered);
59
- return { applied, duplicates, rejected: rejected.length };
66
+ /*
67
+ * 떨어진 것을 **수가 아니라 목록으로** 넘긴다 — 응답에 실어야 보내는 쪽이 그것만 따로 둘 수
68
+ * 있다. 수만 주면 배치 전체를 다시 보내거나 전부 버리는 두 갈래밖에 없다.
69
+ */
70
+ return { applied, duplicates, rejected };
60
71
  }
61
72
  });
62
73
  ctx.status = outcome.status;
@@ -1 +1 @@
1
- {"version":3,"file":"routes.js","sourceRoot":"","sources":["../server/routes.ts"],"names":[],"mappings":";;AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,sEAAqE;AACrE,4DAAoD;AACpD,6EAAkE;AAElE,OAAO,CAAC,EAAE,CAAC,sCAA6C,EAAE,CAAC,IAAS,EAAE,MAAW,EAAE,EAAE;IACnF;;;;;OAKG;IACH,MAAM,CAAC,IAAI,CAAC,gCAAgC,EAAE,KAAK,EAAE,GAAQ,EAAE,EAAE;QAC/D,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,EAAE,MAAM,CAAA;QAChC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,GAAG,CAAC,MAAM,GAAG,GAAG,CAAA;YAChB,GAAG,CAAC,IAAI,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAA;YACpD,OAAM;QACR,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,IAAA,8BAAU,EAAC;YAC/B,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC;YACvC,UAAU,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC;YAC/C,OAAO,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,IAAI,EAAE,CAA4B;YAChE,IAAI,EAAE,GAAG,CAAC,OAAO,EAAE,IAAI;YACvB;;;eAGG;YACH,MAAM,EAAE,CAAC,UAAU,EAAE,OAAO,EAAE,EAAE;gBAC9B,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,IAAA,4CAAsB,EAC/D,OAAc,EACd,MAAM,CAAC,EAAE,EACT,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EACxB,2BAAU,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,CAC5C,CAAA;gBACD,+DAA+D;gBAC/D,IAAI,UAAU,CAAC,MAAM;oBAAE,2BAAU,CAAC,eAAe,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,EAAE,UAAU,CAAC,CAAA;gBACpF,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,2BAAU,CAAC,gBAAgB,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAA;gBACpG;;;mBAGG;gBACH,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC,CAAA;gBACvE,2BAAU,CAAC,kBAAkB,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,EAAE,OAAO,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,WAAW,CAAC,CAAA;gBACvG,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAA;YAC3D,CAAC;SACF,CAAC,CAAA;QACF,GAAG,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAA;QAC3B,GAAG,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAA;IACzB,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,2DAA2D;AAC3D,OAAO,CAAC,EAAE,CAAC,sCAA6C,EAAE,CAAC,IAAS,EAAE,OAAY,EAAE,EAAE,GAAE,CAAC,CAAC,CAAA;AAC1F,OAAO,CAAC,EAAE,CAAC,uCAA8C,EAAE,CAAC,IAAS,EAAE,OAAY,EAAE,EAAE,GAAE,CAAC,CAAC,CAAA;AAC3F,OAAO,CAAC,EAAE,CAAC,uCAA8C,EAAE,CAAC,IAAS,EAAE,OAAY,EAAE,EAAE,GAAE,CAAC,CAAC,CAAA","sourcesContent":["/*\n * 이 패키지가 여는 HTTP 자리 — 지금은 웹훅 하나다.\n *\n * ── 왜 여기인가 ─────────────────────────────────────────────────────────────\n * 커넥터는 주소를 만들 수 없다. 어댑터 계약에 주소를 등록하는 자리가 없고, 있어서도 안 된다 — 주소는\n * 앱의 것이고 커넥터마다 제각기 열면 인증도 제각기가 된다.\n *\n * 그래서 입구는 **하나**다. 어느 연결의 것인지는 주소의 이름으로 가려내고, 본문을 우리 어휘로 옮기는\n * 것은 그 연결의 커넥터가 한다(§`reference-hook`).\n *\n * ── 왜 도메인 공개 라우터인가 ───────────────────────────────────────────────\n * 밀어 주는 쪽은 사람이 아니다. 사람 인증(JWT)을 요구하면 연결된 시스템이 우리 토큰을 발급받아\n * 갱신하며 관리해야 하고, 대부분의 현장에서 그것 때문에 연동이 서지 않는다.\n *\n * 대신 **연결마다 비밀값**을 두고 그것으로 확인한다. 비밀값을 선언하지 않은 연결은 훅을 받지 않는다 —\n * 인증 없는 입구를 열어 두는 것보다 훅을 못 쓰는 것이 낫다.\n */\nimport { ingestCanonicalRecords } from './engine/canonical-ingest.js'\nimport { TwinEngine } from './engine/twin-engine.js'\nimport { handleHook } from './service/reference/reference-hook.js'\n\nprocess.on('bootstrap-module-domain-public-route' as any, (_app: any, router: any) => {\n /*\n * 주소에 트윈까지 적는다 — 어느 트윈에 넣을지 짐작하지 않는다.\n *\n * 한 연결이 여러 사이트를 만들면 트윈도 여럿이다. 본문을 보고 고르게 하면 그 판단이 커넥터마다\n * 달라지고, 틀리면 다른 공장의 사실이 이 공장에 들어간다. 주소가 말하게 하면 그 일이 없다.\n */\n router.post('/twin/hook/:source/:instanceId', async (ctx: any) => {\n const domain = ctx.state?.domain\n if (!domain) {\n ctx.status = 400\n ctx.body = { ok: false, error: 'no domain context' }\n return\n }\n const outcome = await handleHook({\n domainId: domain.id,\n source: String(ctx.params.source ?? ''),\n instanceId: String(ctx.params.instanceId ?? ''),\n headers: (ctx.request?.headers ?? {}) as Record<string, unknown>,\n body: ctx.request?.body,\n /*\n * 폴링이 쓰는 길을 그대로 쓴다 — 검증도 유입도 같은 자리다. 그래서 트윈 쪽에서는 물어서 받은\n * 것과 밀어 받은 것이 구별되지 않는다. 구별되는 것은 유입 장부의 `sources` 한 줄뿐이다.\n */\n ingest: (instanceId, records) => {\n const { accepted, rejected, masterData } = ingestCanonicalRecords(\n records as any,\n domain.id,\n new Date().toISOString(),\n TwinEngine.factScope(domain.id, instanceId)\n )\n /* 사건이 아닌 것은 상태만 세운다 — 밀어 받는 길도 같은 규율이다(§`applyMasterData`). */\n if (masterData.length) TwinEngine.applyMasterData(domain.id, instanceId, masterData)\n const { applied, duplicates } = TwinEngine.ingestLiveResult(domain.id, instanceId, accepted, 'hook')\n /*\n * 커널이 받지 못한 수를 이름으로 남긴다 — 트윈이 멈춘 사이 버려진 레코드를 「통과」로 세면\n * 화면이 100% 라고 말한다. 중복으로 걸러진 것은 버려진 것이 아니므로 빼고 센다.\n */\n const undelivered = Math.max(0, accepted.length - applied - duplicates)\n TwinEngine.recordIngestResult(domain.id, instanceId, records.length, rejected, Date.now(), undelivered)\n return { applied, duplicates, rejected: rejected.length }\n }\n })\n ctx.status = outcome.status\n ctx.body = outcome.body\n })\n})\n\n/* 나머지 셋은 이 패키지가 쓰지 않는다 — 자리는 남겨 둔다(다른 모듈이 같은 신호를 기다린다). */\nprocess.on('bootstrap-module-global-public-route' as any, (_app: any, _router: any) => {})\nprocess.on('bootstrap-module-global-private-route' as any, (_app: any, _router: any) => {})\nprocess.on('bootstrap-module-domain-private-route' as any, (_app: any, _router: any) => {})\n"]}
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../server/routes.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 이 패키지가 여는 HTTP 자리 — 지금은 웹훅 하나다.\n *\n * ── 왜 여기인가 ─────────────────────────────────────────────────────────────\n * 커넥터는 주소를 만들 수 없다. 어댑터 계약에 주소를 등록하는 자리가 없고, 있어서도 안 된다 — 주소는\n * 앱의 것이고 커넥터마다 제각기 열면 인증도 제각기가 된다.\n *\n * 그래서 입구는 **하나**다. 어느 연결의 것인지는 주소의 이름으로 가려내고, 본문을 우리 어휘로 옮기는\n * 것은 그 연결의 커넥터가 한다(§`reference-hook`).\n *\n * ── 왜 도메인 공개 라우터인가 ───────────────────────────────────────────────\n * 밀어 주는 쪽은 사람이 아니다. 사람 인증(JWT)을 요구하면 연결된 시스템이 우리 토큰을 발급받아\n * 갱신하며 관리해야 하고, 대부분의 현장에서 그것 때문에 연동이 서지 않는다.\n *\n * 대신 **연결마다 비밀값**을 두고 그것으로 확인한다. 비밀값을 선언하지 않은 연결은 훅을 받지 않는다 —\n * 인증 없는 입구를 열어 두는 것보다 훅을 못 쓰는 것이 낫다.\n */\nimport { ingestCanonicalRecords } from './engine/canonical-ingest.js'\nimport { TwinEngine } from './engine/twin-engine.js'\nimport { handleHook } from './service/reference/reference-hook.js'\nimport { databaseStore } from './service/reference/hook-store.js'\n\nprocess.on('bootstrap-module-domain-public-route' as any, (_app: any, router: any) => {\n /*\n * 주소에 트윈까지 적는다 — 어느 트윈에 넣을지 짐작하지 않는다.\n *\n * 한 연결이 여러 사이트를 만들면 트윈도 여럿이다. 본문을 보고 고르게 하면 그 판단이 커넥터마다\n * 달라지고, 틀리면 다른 공장의 사실이 이 공장에 들어간다. 주소가 말하게 하면 그 일이 없다.\n */\n router.post('/twin/hook/:source/:instanceId', async (ctx: any) => {\n const domain = ctx.state?.domain\n if (!domain) {\n ctx.status = 400\n ctx.body = { ok: false, error: 'no domain context' }\n return\n }\n const outcome = await handleHook({\n domainId: domain.id,\n source: String(ctx.params.source ?? ''),\n instanceId: String(ctx.params.instanceId ?? ''),\n headers: (ctx.request?.headers ?? {}) as Record<string, unknown>,\n body: ctx.request?.body,\n /*\n * 받은 바이트 그대로 — 서명을 확인하려면 이것이 있어야 한다. `koa-bodyparser` 가 파싱한 본문과\n * 함께 남겨 준다. 파싱본을 다시 문자열로 만들면 키 순서와 공백이 달라져 서명이 맞지 않는다.\n */\n rawBody: typeof ctx.request?.rawBody === 'string' ? ctx.request.rawBody : undefined,\n store: databaseStore(),\n /*\n * 폴링이 쓰는 길을 그대로 쓴다 — 검증도 유입도 같은 자리다. 그래서 트윈 쪽에서는 물어서 받은\n * 것과 밀어 받은 것이 구별되지 않는다. 구별되는 것은 유입 장부의 `sources` 한 줄뿐이다.\n */\n ingest: (instanceId, records) => {\n const { accepted, rejected, masterData } = ingestCanonicalRecords(\n records as any,\n domain.id,\n new Date().toISOString(),\n TwinEngine.factScope(domain.id, instanceId)\n )\n /* 사건이 아닌 것은 상태만 세운다 — 밀어 받는 길도 같은 규율이다(§`applyMasterData`). */\n if (masterData.length) TwinEngine.applyMasterData(domain.id, instanceId, masterData)\n const { applied, duplicates } = TwinEngine.ingestLiveResult(domain.id, instanceId, accepted, 'hook')\n /*\n * 커널이 받지 못한 수를 이름으로 남긴다 — 트윈이 멈춘 사이 버려진 레코드를 「통과」로 세면\n * 화면이 100% 라고 말한다. 중복으로 걸러진 것은 버려진 것이 아니므로 빼고 센다.\n */\n const undelivered = Math.max(0, accepted.length - applied - duplicates)\n TwinEngine.recordIngestResult(domain.id, instanceId, records.length, rejected, Date.now(), undelivered)\n /*\n * 떨어진 것을 **수가 아니라 목록으로** 넘긴다 — 응답에 실어야 보내는 쪽이 그것만 따로 둘 수\n * 있다. 수만 주면 배치 전체를 다시 보내거나 전부 버리는 두 갈래밖에 없다.\n */\n return { applied, duplicates, rejected }\n }\n })\n ctx.status = outcome.status\n ctx.body = outcome.body\n })\n})\n\n/* 나머지 셋은 이 패키지가 쓰지 않는다 — 자리는 남겨 둔다(다른 모듈이 같은 신호를 기다린다). */\nprocess.on('bootstrap-module-global-public-route' as any, (_app: any, _router: any) => {})\nprocess.on('bootstrap-module-global-private-route' as any, (_app: any, _router: any) => {})\nprocess.on('bootstrap-module-domain-private-route' as any, (_app: any, _router: any) => {})\n"]}
@@ -0,0 +1,32 @@
1
+ import { type ApprovedCommand, type CommandApproval, type TwinCommand } from '@operato/ops-contract';
2
+ /** 조치의 결과 — 어댑터가 돌려준다. */
3
+ export interface CommandAck {
4
+ ok: boolean;
5
+ /** 저쪽 문서 · 작업 식별자. 되돌릴 때 무엇을 되돌릴지 말하려면 이것이 있어야 한다. */
6
+ ref?: string;
7
+ error?: string;
8
+ }
9
+ /** 커맨드가 사는 자리 — 재기동을 넘어야 한다. */
10
+ export interface CommandStore {
11
+ load(domainId: string, id: string): Promise<TwinCommand | null>;
12
+ save(domainId: string, command: TwinCommand): Promise<void>;
13
+ }
14
+ /**
15
+ * 승인한다.
16
+ *
17
+ * 승인할 수 없는 상태면 계약이 던진다 — 여기서 답으로 돌려주지 않는 이유는, 실패를 답으로 주면 부르는
18
+ * 쪽이 그것을 무시할 수 있기 때문이다.
19
+ */
20
+ export declare function approve(store: CommandStore, domainId: string, id: string, approval: CommandApproval): Promise<TwinCommand>;
21
+ /** 거절한다 — 여기서 끝난다. 되살아나지 않는다. */
22
+ export declare function reject(store: CommandStore, domainId: string, id: string, approval: CommandApproval): Promise<TwinCommand>;
23
+ /**
24
+ * 어댑터에 넘긴다 — **승인을 지나지 않은 것은 여기서 막힌다.**
25
+ *
26
+ * `actuate` 는 `ApprovedCommand` 만 받으므로, 승인 없이 이 함수를 지나가는 길이 없다. 저장소에서
27
+ * 되세운 커맨드는 타입이 지워진 채로 오므로 `asApproved` 로 다시 확인한다 — 확인 없이 표식을 붙이면
28
+ * **재기동이 게이트를 여는 길**이 된다.
29
+ *
30
+ * 되돌릴 수 있는 커맨드는 사람 없이 지난다(시뮬·what-if). 그 판단도 계약이 한다.
31
+ */
32
+ export declare function dispatch(store: CommandStore, domainId: string, id: string, actuate: (command: ApprovedCommand) => Promise<CommandAck>): Promise<TwinCommand>;
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.approve = approve;
4
+ exports.reject = reject;
5
+ exports.dispatch = dispatch;
6
+ /*
7
+ * **승인을 지난 커맨드만 어댑터에 넘긴다** — 게이트가 서는 자리.
8
+ *
9
+ * ── 왜 이 자리인가 (2026-08-31) ──────────────────────────────────────────
10
+ * 설계가 「기본 = propose-only」를 정해 두었는데 코드에 그 규율이 없었다. 인티그레이션 레인이 어댑터에
11
+ * 조치를 붙이려다 그것을 보고 멈췄다 — 게이트 없이 붙이면 트윈이 사람 승인 없이 현장에 작업지시를
12
+ * 내린다. 작업지시는 취소해도 이미 만든 것이 남는다.
13
+ *
14
+ * ── 확인을 여기서 한 번만 한다 ───────────────────────────────────────────
15
+ * 어댑터는 승인을 확인하지 않는다. 그 확인은 이 파일이 했고 **타입이 그것을 증명한다**
16
+ * (`ApprovedCommand` 는 `approveCommand`·`asApproved` 로만 만들어진다). 어댑터가 또 확인하면 규칙이
17
+ * 두 벌이 되고, 한쪽만 고쳐지는 날이 온다.
18
+ *
19
+ * ── 저장소에 닿는 자리를 밖으로 낸다 ─────────────────────────────────────
20
+ * 훅에서와 같은 이유다(§`HookStore`). 시험이 **재기동을 넣을 수 있어야** 한다 — 승인이 프로세스 안에만
21
+ * 살아 있으면 「승인된다」는 보이지만 「다시 세운 뒤에도 승인이 남아 있나」는 보이지 않는다.
22
+ */
23
+ const ops_contract_1 = require("@operato/ops-contract");
24
+ /**
25
+ * 승인한다.
26
+ *
27
+ * 승인할 수 없는 상태면 계약이 던진다 — 여기서 답으로 돌려주지 않는 이유는, 실패를 답으로 주면 부르는
28
+ * 쪽이 그것을 무시할 수 있기 때문이다.
29
+ */
30
+ async function approve(store, domainId, id, approval) {
31
+ const command = await store.load(domainId, id);
32
+ if (!command)
33
+ throw new Error(`모르는 커맨드다 (${id})`);
34
+ const approved = (0, ops_contract_1.approveCommand)(command, approval);
35
+ await store.save(domainId, approved);
36
+ return approved;
37
+ }
38
+ /** 거절한다 — 여기서 끝난다. 되살아나지 않는다. */
39
+ async function reject(store, domainId, id, approval) {
40
+ const command = await store.load(domainId, id);
41
+ if (!command)
42
+ throw new Error(`모르는 커맨드다 (${id})`);
43
+ const to = (0, ops_contract_1.nextCommandState)(command.state, 'reject');
44
+ if (!to)
45
+ throw new Error(`${command.state} 상태의 커맨드는 거절할 수 없다 (${id})`);
46
+ const next = { ...command, state: to, approval };
47
+ await store.save(domainId, next);
48
+ return next;
49
+ }
50
+ /**
51
+ * 어댑터에 넘긴다 — **승인을 지나지 않은 것은 여기서 막힌다.**
52
+ *
53
+ * `actuate` 는 `ApprovedCommand` 만 받으므로, 승인 없이 이 함수를 지나가는 길이 없다. 저장소에서
54
+ * 되세운 커맨드는 타입이 지워진 채로 오므로 `asApproved` 로 다시 확인한다 — 확인 없이 표식을 붙이면
55
+ * **재기동이 게이트를 여는 길**이 된다.
56
+ *
57
+ * 되돌릴 수 있는 커맨드는 사람 없이 지난다(시뮬·what-if). 그 판단도 계약이 한다.
58
+ */
59
+ async function dispatch(store, domainId, id, actuate) {
60
+ const command = await store.load(domainId, id);
61
+ if (!command)
62
+ throw new Error(`모르는 커맨드다 (${id})`);
63
+ /*
64
+ * 되돌릴 수 있는 것은 제안 상태에서 바로 지난다. 되돌릴 수 없는 것은 승인 기록이 있어야 하고,
65
+ * 없으면 여기서 던진다 — 조용히 건너뛰면 그 커맨드가 승인 없이 나간 것이 된다.
66
+ */
67
+ const approved = command.state === 'proposed' && !(0, ops_contract_1.requiresApproval)(command) ? (0, ops_contract_1.passThroughReversible)(command) : (0, ops_contract_1.asApproved)(command);
68
+ const to = (0, ops_contract_1.nextCommandState)(approved.state, 'dispatch');
69
+ if (!to)
70
+ throw new Error(`${approved.state} 상태의 커맨드는 넘길 수 없다 (${id})`);
71
+ /*
72
+ * **넘기기 전에 「넘기는 중」을 먼저 적는다.** 넘기고 나서 적으면, 그 사이에 프로세스가 죽었을 때
73
+ * 저쪽은 받았는데 우리는 안 보낸 줄 알고 다시 보낸다. 현장이 두 번 움직인다.
74
+ */
75
+ const dispatching = { ...approved, state: to };
76
+ await store.save(domainId, dispatching);
77
+ let ack;
78
+ try {
79
+ ack = await actuate(approved);
80
+ }
81
+ catch (e) {
82
+ const failed = { ...dispatching, state: 'failed', error: String(e?.message ?? e) };
83
+ await store.save(domainId, failed);
84
+ return failed;
85
+ }
86
+ const settled = ack?.ok
87
+ ? { ...dispatching, state: 'acked', dispatchRef: ack.ref }
88
+ : { ...dispatching, state: 'failed', error: ack?.error ?? '어댑터가 이유를 말하지 않았다' };
89
+ await store.save(domainId, settled);
90
+ return settled;
91
+ }
92
+ //# sourceMappingURL=command-dispatcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command-dispatcher.js","sourceRoot":"","sources":["../../../server/service/actuation/command-dispatcher.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **승인을 지난 커맨드만 어댑터에 넘긴다** — 게이트가 서는 자리.\n *\n * ── 왜 이 자리인가 (2026-08-31) ──────────────────────────────────────────\n * 설계가 「기본 = propose-only」를 정해 두었는데 코드에 그 규율이 없었다. 인티그레이션 레인이 어댑터에\n * 조치를 붙이려다 그것을 보고 멈췄다 — 게이트 없이 붙이면 트윈이 사람 승인 없이 현장에 작업지시를\n * 내린다. 작업지시는 취소해도 이미 만든 것이 남는다.\n *\n * ── 확인을 여기서 한 번만 한다 ───────────────────────────────────────────\n * 어댑터는 승인을 확인하지 않는다. 그 확인은 이 파일이 했고 **타입이 그것을 증명한다**\n * (`ApprovedCommand` 는 `approveCommand`·`asApproved` 로만 만들어진다). 어댑터가 또 확인하면 규칙이\n * 두 벌이 되고, 한쪽만 고쳐지는 날이 온다.\n *\n * ── 저장소에 닿는 자리를 밖으로 낸다 ─────────────────────────────────────\n * 훅에서와 같은 이유다(§`HookStore`). 시험이 **재기동을 넣을 수 있어야** 한다 — 승인이 프로세스 안에만\n * 살아 있으면 「승인된다」는 보이지만 「다시 세운 뒤에도 승인이 남아 있나」는 보이지 않는다.\n */\nimport {\n approveCommand,\n asApproved,\n nextCommandState,\n passThroughReversible,\n requiresApproval,\n type ApprovedCommand,\n type CommandApproval,\n type TwinCommand\n} from '@operato/ops-contract'\n\n/** 조치의 결과 — 어댑터가 돌려준다. */\nexport interface CommandAck {\n ok: boolean\n /** 저쪽 문서 · 작업 식별자. 되돌릴 때 무엇을 되돌릴지 말하려면 이것이 있어야 한다. */\n ref?: string\n error?: string\n}\n\n/** 커맨드가 사는 자리 — 재기동을 넘어야 한다. */\nexport interface CommandStore {\n load(domainId: string, id: string): Promise<TwinCommand | null>\n save(domainId: string, command: TwinCommand): Promise<void>\n}\n\n/**\n * 승인한다.\n *\n * 승인할 수 없는 상태면 계약이 던진다 — 여기서 답으로 돌려주지 않는 이유는, 실패를 답으로 주면 부르는\n * 쪽이 그것을 무시할 수 있기 때문이다.\n */\nexport async function approve(\n store: CommandStore,\n domainId: string,\n id: string,\n approval: CommandApproval\n): Promise<TwinCommand> {\n const command = await store.load(domainId, id)\n if (!command) throw new Error(`모르는 커맨드다 (${id})`)\n\n const approved = approveCommand(command, approval)\n await store.save(domainId, approved)\n return approved\n}\n\n/** 거절한다 — 여기서 끝난다. 되살아나지 않는다. */\nexport async function reject(\n store: CommandStore,\n domainId: string,\n id: string,\n approval: CommandApproval\n): Promise<TwinCommand> {\n const command = await store.load(domainId, id)\n if (!command) throw new Error(`모르는 커맨드다 (${id})`)\n\n const to = nextCommandState(command.state, 'reject')\n if (!to) throw new Error(`${command.state} 상태의 커맨드는 거절할 수 없다 (${id})`)\n\n const next: TwinCommand = { ...command, state: to, approval }\n await store.save(domainId, next)\n return next\n}\n\n/**\n * 어댑터에 넘긴다 — **승인을 지나지 않은 것은 여기서 막힌다.**\n *\n * `actuate` 는 `ApprovedCommand` 만 받으므로, 승인 없이 이 함수를 지나가는 길이 없다. 저장소에서\n * 되세운 커맨드는 타입이 지워진 채로 오므로 `asApproved` 로 다시 확인한다 — 확인 없이 표식을 붙이면\n * **재기동이 게이트를 여는 길**이 된다.\n *\n * 되돌릴 수 있는 커맨드는 사람 없이 지난다(시뮬·what-if). 그 판단도 계약이 한다.\n */\nexport async function dispatch(\n store: CommandStore,\n domainId: string,\n id: string,\n actuate: (command: ApprovedCommand) => Promise<CommandAck>\n): Promise<TwinCommand> {\n const command = await store.load(domainId, id)\n if (!command) throw new Error(`모르는 커맨드다 (${id})`)\n\n /*\n * 되돌릴 수 있는 것은 제안 상태에서 바로 지난다. 되돌릴 수 없는 것은 승인 기록이 있어야 하고,\n * 없으면 여기서 던진다 — 조용히 건너뛰면 그 커맨드가 승인 없이 나간 것이 된다.\n */\n const approved: ApprovedCommand =\n command.state === 'proposed' && !requiresApproval(command) ? passThroughReversible(command) : asApproved(command)\n\n const to = nextCommandState(approved.state, 'dispatch')\n if (!to) throw new Error(`${approved.state} 상태의 커맨드는 넘길 수 없다 (${id})`)\n\n /*\n * **넘기기 전에 「넘기는 중」을 먼저 적는다.** 넘기고 나서 적으면, 그 사이에 프로세스가 죽었을 때\n * 저쪽은 받았는데 우리는 안 보낸 줄 알고 다시 보낸다. 현장이 두 번 움직인다.\n */\n const dispatching: TwinCommand = { ...approved, state: to }\n await store.save(domainId, dispatching)\n\n let ack: CommandAck\n try {\n ack = await actuate(approved)\n } catch (e: any) {\n const failed: TwinCommand = { ...dispatching, state: 'failed', error: String(e?.message ?? e) }\n await store.save(domainId, failed)\n return failed\n }\n\n const settled: TwinCommand = ack?.ok\n ? { ...dispatching, state: 'acked', dispatchRef: ack.ref }\n : { ...dispatching, state: 'failed', error: ack?.error ?? '어댑터가 이유를 말하지 않았다' }\n await store.save(domainId, settled)\n return settled\n}\n"]}
@@ -0,0 +1,3 @@
1
+ import type { CommandStore } from './command-dispatcher.js';
2
+ /** 실제 저장소를 쓰는 구현 — 리졸버가 이것을 넣는다. */
3
+ export declare function databaseCommandStore(): CommandStore;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.databaseCommandStore = databaseCommandStore;
4
+ /*
5
+ * 커맨드가 저장소에 닿는 두 자리 — 찾는 것과 적는 것.
6
+ *
7
+ * 디스패처가 엔티티를 직접 가져오면 그 파일을 부르는 시험이 TypeORM 데코레이터를 지나게 되고, 시험이
8
+ * 서지 않는다. 그러면 **게이트의 배선을 아무 시험도 걷지 않게 된다** — 훅에서 이미 겪은 부류다
9
+ * (§`hook-store.ts`).
10
+ */
11
+ const shell_1 = require("@things-factory/shell");
12
+ const read_time_js_1 = require("../../engine/read-time.js");
13
+ const twin_command_js_1 = require("./twin-command.js");
14
+ /**
15
+ * 행 → 커맨드. 상태 어휘는 계약이 정한 문자열 그대로다.
16
+ *
17
+ * `createdAt` 은 드라이버가 준 값이라 **직접 날짜로 만들지 않는다** — 드라이버마다 모양이 다르고,
18
+ * 문자열로 오는 드라이버에서는 시간대가 없는 값이 된다. `hydratedDate` 가 그 컬럼의 정의를 보고 푼다.
19
+ */
20
+ function toCommand(repo, row) {
21
+ return {
22
+ id: row.id,
23
+ instanceId: row.instanceId,
24
+ type: row.type,
25
+ ...(row.payload ? { payload: row.payload } : {}),
26
+ origin: row.origin,
27
+ proposedAt: (0, read_time_js_1.hydratedDate)(repo, 'createdAt', row.createdAt)?.toISOString() ?? '',
28
+ /* 없는 것을 거짓으로 채우지 않는다 — 비어 있음이 「되돌릴 수 없다」의 뜻이다. */
29
+ ...(row.reversible === null || row.reversible === undefined ? {} : { reversible: row.reversible }),
30
+ state: row.state,
31
+ ...(row.decidedBy ? { approval: { by: row.decidedBy, at: row.decidedAt ?? '', ...(row.decisionNote ? { note: row.decisionNote } : {}) } } : {}),
32
+ ...(row.dispatchRef ? { dispatchRef: row.dispatchRef } : {}),
33
+ ...(row.error ? { error: row.error } : {})
34
+ };
35
+ }
36
+ /** 실제 저장소를 쓰는 구현 — 리졸버가 이것을 넣는다. */
37
+ function databaseCommandStore() {
38
+ return {
39
+ load: async (domainId, id) => {
40
+ const repo = await (0, shell_1.getRepository)(twin_command_js_1.TwinCommandRecord);
41
+ const row = await repo.findOne({ where: { domain: { id: domainId }, id } }).catch(() => null);
42
+ return row ? toCommand(repo, row) : null;
43
+ },
44
+ save: async (domainId, command) => {
45
+ await (0, shell_1.getRepository)(twin_command_js_1.TwinCommandRecord).update({ id: command.id, domain: { id: domainId } }, {
46
+ state: command.state,
47
+ decidedBy: command.approval?.by ?? null,
48
+ decidedAt: command.approval?.at ?? null,
49
+ decisionNote: command.approval?.note ?? null,
50
+ dispatchRef: command.dispatchRef ?? null,
51
+ error: command.error ?? null
52
+ });
53
+ }
54
+ };
55
+ }
56
+ //# sourceMappingURL=command-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command-store.js","sourceRoot":"","sources":["../../../server/service/actuation/command-store.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 커맨드가 저장소에 닿는 두 자리 — 찾는 것과 적는 것.\n *\n * 디스패처가 엔티티를 직접 가져오면 그 파일을 부르는 시험이 TypeORM 데코레이터를 지나게 되고, 시험이\n * 서지 않는다. 그러면 **게이트의 배선을 아무 시험도 걷지 않게 된다** — 훅에서 이미 겪은 부류다\n * (§`hook-store.ts`).\n */\nimport { getRepository } from '@things-factory/shell'\n\nimport type { TwinCommand } from '@operato/ops-contract'\n\nimport { hydratedDate } from '../../engine/read-time.js'\n\nimport { TwinCommandRecord } from './twin-command.js'\nimport type { CommandStore } from './command-dispatcher.js'\n\n/**\n * 행 → 커맨드. 상태 어휘는 계약이 정한 문자열 그대로다.\n *\n * `createdAt` 은 드라이버가 준 값이라 **직접 날짜로 만들지 않는다** — 드라이버마다 모양이 다르고,\n * 문자열로 오는 드라이버에서는 시간대가 없는 값이 된다. `hydratedDate` 가 그 컬럼의 정의를 보고 푼다.\n */\nfunction toCommand(repo: any, row: TwinCommandRecord): TwinCommand {\n return {\n id: row.id,\n instanceId: row.instanceId,\n type: row.type,\n ...(row.payload ? { payload: row.payload } : {}),\n origin: row.origin as TwinCommand['origin'],\n proposedAt: hydratedDate(repo, 'createdAt', row.createdAt)?.toISOString() ?? '',\n /* 없는 것을 거짓으로 채우지 않는다 — 비어 있음이 「되돌릴 수 없다」의 뜻이다. */\n ...(row.reversible === null || row.reversible === undefined ? {} : { reversible: row.reversible }),\n state: row.state as TwinCommand['state'],\n ...(row.decidedBy ? { approval: { by: row.decidedBy, at: row.decidedAt ?? '', ...(row.decisionNote ? { note: row.decisionNote } : {}) } } : {}),\n ...(row.dispatchRef ? { dispatchRef: row.dispatchRef } : {}),\n ...(row.error ? { error: row.error } : {})\n }\n}\n\n/** 실제 저장소를 쓰는 구현 — 리졸버가 이것을 넣는다. */\nexport function databaseCommandStore(): CommandStore {\n return {\n load: async (domainId: string, id: string) => {\n const repo = await getRepository(TwinCommandRecord)\n const row = await repo.findOne({ where: { domain: { id: domainId }, id } }).catch(() => null)\n return row ? toCommand(repo, row) : null\n },\n save: async (domainId: string, command: TwinCommand) => {\n await getRepository(TwinCommandRecord).update(\n { id: command.id, domain: { id: domainId } },\n {\n state: command.state,\n decidedBy: command.approval?.by ?? null,\n decidedAt: command.approval?.at ?? null,\n decisionNote: command.approval?.note ?? null,\n dispatchRef: command.dispatchRef ?? null,\n error: command.error ?? null\n } as any\n )\n }\n }\n}\n"]}
@@ -0,0 +1,6 @@
1
+ import { TwinCommandRecord } from './twin-command.js';
2
+ export declare const entities: (typeof TwinCommandRecord)[];
3
+ export declare const resolvers: any[];
4
+ export * from './twin-command.js';
5
+ export * from './command-dispatcher.js';
6
+ export * from './command-store.js';
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolvers = exports.entities = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const twin_command_js_1 = require("./twin-command.js");
6
+ exports.entities = [twin_command_js_1.TwinCommandRecord];
7
+ exports.resolvers = [];
8
+ tslib_1.__exportStar(require("./twin-command.js"), exports);
9
+ tslib_1.__exportStar(require("./command-dispatcher.js"), exports);
10
+ tslib_1.__exportStar(require("./command-store.js"), exports);
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/actuation/index.ts"],"names":[],"mappings":";;;;AAAA,uDAAqD;AAExC,QAAA,QAAQ,GAAG,CAAC,mCAAiB,CAAC,CAAA;AAC9B,QAAA,SAAS,GAAG,EAAE,CAAA;AAE3B,4DAAiC;AACjC,kEAAuC;AACvC,6DAAkC","sourcesContent":["import { TwinCommandRecord } from './twin-command.js'\n\nexport const entities = [TwinCommandRecord]\nexport const resolvers = []\n\nexport * from './twin-command.js'\nexport * from './command-dispatcher.js'\nexport * from './command-store.js'\n"]}
@@ -0,0 +1,30 @@
1
+ import { Domain } from '@things-factory/shell';
2
+ export declare class TwinCommandRecord {
3
+ readonly id: string;
4
+ domain?: Domain;
5
+ domainId?: string;
6
+ instanceId: string;
7
+ type: string;
8
+ payload?: any;
9
+ origin: string;
10
+ /**
11
+ * **되돌릴 수 있는가** — 게이트가 걸리는 기준이다.
12
+ *
13
+ * 비어 있으면 「되돌릴 수 없다」로 읽는다(§`requiresApproval`). 모름을 되돌릴 수 있다로 읽으면 그
14
+ * 커맨드가 승인 없이 나간다.
15
+ */
16
+ reversible?: boolean;
17
+ state: string;
18
+ decidedBy?: string;
19
+ decidedAt?: string;
20
+ decisionNote?: string;
21
+ /**
22
+ * 어댑터가 돌려준 것 — 저쪽에서 무엇이 되었는지 되짚을 수 있어야 한다.
23
+ *
24
+ * 이것이 없으면 되돌릴 때 **무엇을 되돌릴지 말할 수 없다.**
25
+ */
26
+ dispatchRef?: string;
27
+ error?: string;
28
+ createdAt?: Date;
29
+ updatedAt?: Date;
30
+ }
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TwinCommandRecord = void 0;
4
+ const tslib_1 = require("tslib");
5
+ /*
6
+ * **현장에 내리는 조치 하나** — 제안 · 승인 · 넘김 · 응답이 한 행에 남는다.
7
+ *
8
+ * ── 왜 표인가 (2026-08-31) ───────────────────────────────────────────────
9
+ * 승인이 프로세스 안에만 있으면 트윈이 재기동한 순간 승인 대기 줄이 사라진다. 그때 사람은 **자기가
10
+ * 승인한 것이 실행됐는지 알 수 없다.** 조치는 되돌리기 어려운 것이므로 그 모름이 그대로 사고가 된다.
11
+ *
12
+ * 그리고 이 행은 **누가 언제 승인했나**의 기록이기도 하다. 사고 뒤에 그것을 물을 자리가 여기다.
13
+ *
14
+ * ── 판정은 여기 없다 ─────────────────────────────────────────────────────
15
+ * 어느 상태에서 어느 사건이 허용되는지는 계약이 든다(`@operato/ops-contract` §`nextCommandState`).
16
+ * 이 파일은 그 값을 담기만 한다 — 판정이 두 벌이 되면 화면과 서버가 다른 답을 낸다.
17
+ */
18
+ const typeorm_1 = require("typeorm");
19
+ const type_graphql_1 = require("type-graphql");
20
+ const shell_1 = require("@things-factory/shell");
21
+ let TwinCommandRecord = class TwinCommandRecord {
22
+ };
23
+ exports.TwinCommandRecord = TwinCommandRecord;
24
+ tslib_1.__decorate([
25
+ (0, typeorm_1.PrimaryGeneratedColumn)('uuid'),
26
+ (0, type_graphql_1.Field)(type => type_graphql_1.ID, { description: 'Unique identifier of the command.' }),
27
+ tslib_1.__metadata("design:type", String)
28
+ ], TwinCommandRecord.prototype, "id", void 0);
29
+ tslib_1.__decorate([
30
+ (0, typeorm_1.ManyToOne)(type => shell_1.Domain),
31
+ (0, type_graphql_1.Field)(type => shell_1.Domain, { nullable: true, description: 'Tenant this command belongs to.' }),
32
+ tslib_1.__metadata("design:type", shell_1.Domain)
33
+ ], TwinCommandRecord.prototype, "domain", void 0);
34
+ tslib_1.__decorate([
35
+ (0, typeorm_1.RelationId)((e) => e.domain),
36
+ tslib_1.__metadata("design:type", String)
37
+ ], TwinCommandRecord.prototype, "domainId", void 0);
38
+ tslib_1.__decorate([
39
+ (0, typeorm_1.Column)(),
40
+ (0, type_graphql_1.Field)({ description: 'Twin instance this command acts on.' }),
41
+ tslib_1.__metadata("design:type", String)
42
+ ], TwinCommandRecord.prototype, "instanceId", void 0);
43
+ tslib_1.__decorate([
44
+ (0, typeorm_1.Column)(),
45
+ (0, type_graphql_1.Field)({ description: 'Command type in twin vocabulary. Translating it into a legacy call is the adapter job.' }),
46
+ tslib_1.__metadata("design:type", String)
47
+ ], TwinCommandRecord.prototype, "type", void 0);
48
+ tslib_1.__decorate([
49
+ (0, typeorm_1.Column)({ type: 'simple-json', nullable: true }),
50
+ (0, type_graphql_1.Field)(type => shell_1.ScalarObject, { nullable: true, description: 'Command payload in twin vocabulary.' }),
51
+ tslib_1.__metadata("design:type", Object)
52
+ ], TwinCommandRecord.prototype, "payload", void 0);
53
+ tslib_1.__decorate([
54
+ (0, typeorm_1.Column)(),
55
+ (0, type_graphql_1.Field)({ description: 'Who proposed it: operator, rule, ai or schedule. A person weighs this when approving.' }),
56
+ tslib_1.__metadata("design:type", String)
57
+ ], TwinCommandRecord.prototype, "origin", void 0);
58
+ tslib_1.__decorate([
59
+ (0, typeorm_1.Column)({ nullable: true }),
60
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Whether the effect can be undone. Absent means it cannot, and a person must approve.' }),
61
+ tslib_1.__metadata("design:type", Boolean)
62
+ ], TwinCommandRecord.prototype, "reversible", void 0);
63
+ tslib_1.__decorate([
64
+ (0, typeorm_1.Column)(),
65
+ (0, type_graphql_1.Field)({ description: 'proposed · approved · rejected · dispatched · acked · failed.' }),
66
+ tslib_1.__metadata("design:type", String)
67
+ ], TwinCommandRecord.prototype, "state", void 0);
68
+ tslib_1.__decorate([
69
+ (0, typeorm_1.Column)({ nullable: true }),
70
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Who approved or rejected it.' }),
71
+ tslib_1.__metadata("design:type", String)
72
+ ], TwinCommandRecord.prototype, "decidedBy", void 0);
73
+ tslib_1.__decorate([
74
+ (0, typeorm_1.Column)({ nullable: true }),
75
+ (0, type_graphql_1.Field)({ nullable: true, description: 'When it was approved or rejected (ISO 8601).' }),
76
+ tslib_1.__metadata("design:type", String)
77
+ ], TwinCommandRecord.prototype, "decidedAt", void 0);
78
+ tslib_1.__decorate([
79
+ (0, typeorm_1.Column)({ nullable: true }),
80
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Why — free text the approver wrote.' }),
81
+ tslib_1.__metadata("design:type", String)
82
+ ], TwinCommandRecord.prototype, "decisionNote", void 0);
83
+ tslib_1.__decorate([
84
+ (0, typeorm_1.Column)({ nullable: true }),
85
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Reference the adapter returned. Without it a reversal cannot name what to undo.' }),
86
+ tslib_1.__metadata("design:type", String)
87
+ ], TwinCommandRecord.prototype, "dispatchRef", void 0);
88
+ tslib_1.__decorate([
89
+ (0, typeorm_1.Column)({ nullable: true }),
90
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Why the dispatch failed. The server sentence, kept as it came.' }),
91
+ tslib_1.__metadata("design:type", String)
92
+ ], TwinCommandRecord.prototype, "error", void 0);
93
+ tslib_1.__decorate([
94
+ (0, typeorm_1.CreateDateColumn)(),
95
+ (0, type_graphql_1.Field)({ nullable: true, description: 'When the command was proposed.' }),
96
+ tslib_1.__metadata("design:type", Date)
97
+ ], TwinCommandRecord.prototype, "createdAt", void 0);
98
+ tslib_1.__decorate([
99
+ (0, typeorm_1.UpdateDateColumn)(),
100
+ (0, type_graphql_1.Field)({ nullable: true, description: 'When the row last changed.' }),
101
+ tslib_1.__metadata("design:type", Date)
102
+ ], TwinCommandRecord.prototype, "updatedAt", void 0);
103
+ exports.TwinCommandRecord = TwinCommandRecord = tslib_1.__decorate([
104
+ (0, typeorm_1.Entity)()
105
+ /* 대기 줄을 여는 질의가 이 세 칸으로 묻는다 — 어느 테넌트의 · 어느 트윈의 · 어느 상태인가. */
106
+ ,
107
+ (0, typeorm_1.Index)('ix_twin_command_0', (e) => [e.domain, e.instanceId, e.state]),
108
+ (0, type_graphql_1.ObjectType)({ description: 'One actuation command proposed against a twin, with its approval trail.' })
109
+ ], TwinCommandRecord);
110
+ //# sourceMappingURL=twin-command.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"twin-command.js","sourceRoot":"","sources":["../../../server/service/actuation/twin-command.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **현장에 내리는 조치 하나** — 제안 · 승인 · 넘김 · 응답이 한 행에 남는다.\n *\n * ── 왜 표인가 (2026-08-31) ───────────────────────────────────────────────\n * 승인이 프로세스 안에만 있으면 트윈이 재기동한 순간 승인 대기 줄이 사라진다. 그때 사람은 **자기가\n * 승인한 것이 실행됐는지 알 수 없다.** 조치는 되돌리기 어려운 것이므로 그 모름이 그대로 사고가 된다.\n *\n * 그리고 이 행은 **누가 언제 승인했나**의 기록이기도 하다. 사고 뒤에 그것을 물을 자리가 여기다.\n *\n * ── 판정은 여기 없다 ─────────────────────────────────────────────────────\n * 어느 상태에서 어느 사건이 허용되는지는 계약이 든다(`@operato/ops-contract` §`nextCommandState`).\n * 이 파일은 그 값을 담기만 한다 — 판정이 두 벌이 되면 화면과 서버가 다른 답을 낸다.\n */\nimport { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, RelationId, UpdateDateColumn } from 'typeorm'\nimport { Field, ID, ObjectType } from 'type-graphql'\n\nimport { Domain, ScalarObject } from '@things-factory/shell'\n\n@Entity()\n/* 대기 줄을 여는 질의가 이 세 칸으로 묻는다 — 어느 테넌트의 · 어느 트윈의 · 어느 상태인가. */\n@Index('ix_twin_command_0', (e: TwinCommandRecord) => [e.domain, e.instanceId, e.state])\n@ObjectType({ description: 'One actuation command proposed against a twin, with its approval trail.' })\nexport class TwinCommandRecord {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique identifier of the command.' })\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true, description: 'Tenant this command belongs to.' })\n domain?: Domain\n\n @RelationId((e: TwinCommandRecord) => e.domain)\n domainId?: string\n\n @Column()\n @Field({ description: 'Twin instance this command acts on.' })\n instanceId: string\n\n @Column()\n @Field({ description: 'Command type in twin vocabulary. Translating it into a legacy call is the adapter job.' })\n type: string\n\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, { nullable: true, description: 'Command payload in twin vocabulary.' })\n payload?: any\n\n @Column()\n @Field({ description: 'Who proposed it: operator, rule, ai or schedule. A person weighs this when approving.' })\n origin: string\n\n /**\n * **되돌릴 수 있는가** — 게이트가 걸리는 기준이다.\n *\n * 비어 있으면 「되돌릴 수 없다」로 읽는다(§`requiresApproval`). 모름을 되돌릴 수 있다로 읽으면 그\n * 커맨드가 승인 없이 나간다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Whether the effect can be undone. Absent means it cannot, and a person must approve.' })\n reversible?: boolean\n\n @Column()\n @Field({ description: 'proposed · approved · rejected · dispatched · acked · failed.' })\n state: string\n\n /* ── 승인의 기록 ─────────────────────────────────────────────────────────\n * 한 칸에 JSON 으로 담지 않고 세 칸으로 나눈다 — 「누가 승인했나」로 묻는 질의가 실제로 온다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Who approved or rejected it.' })\n decidedBy?: string\n\n /*\n * ISO 문자열로 든다 — `datetime` 은 드라이버 방언이라 다섯 드라이버를 그대로 지나지 못하고,\n * sqlite 는 그것을 검증하지 않아 개발에서 잡히지 않는다. 이 저장소의 다른 시각 칸도 그 모양이다\n * (§`TwinReference.lastSyncedAt`). 계약의 `CommandApproval.at` 도 ISO 문자열이라 옮길 것이 없다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'When it was approved or rejected (ISO 8601).' })\n decidedAt?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Why — free text the approver wrote.' })\n decisionNote?: string\n\n /**\n * 어댑터가 돌려준 것 — 저쪽에서 무엇이 되었는지 되짚을 수 있어야 한다.\n *\n * 이것이 없으면 되돌릴 때 **무엇을 되돌릴지 말할 수 없다.**\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Reference the adapter returned. Without it a reversal cannot name what to undo.' })\n dispatchRef?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Why the dispatch failed. The server sentence, kept as it came.' })\n error?: string\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'When the command was proposed.' })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'When the row last changed.' })\n updatedAt?: Date\n}\n"]}
@@ -17,7 +17,7 @@ export * from './reference/template-registry.js';
17
17
  export * from './reference/reference-probe.js';
18
18
  export * from './reference/reference-assessment.js';
19
19
  export * from './twin-lifecycle/domain-catalog.js';
20
- export declare const entities: (typeof import("./twin-event/twin-event.js").TwinEvent | typeof import("./twin-subject/twin-subject-event.js").TwinSubjectEvent | typeof import("./twin-instance/twin-instance.js").TwinInstance | typeof import("./reference/twin-reference.js").TwinReference | typeof import("./twin-structure/twin-structure.js").TwinStructure | typeof import("./twin-space/twin-space.js").TwinSpace | typeof import("./twin-space/twin-space-representation.js").TwinSpaceRepresentation | typeof import("./twin-space/twin-space-area.js").TwinSpaceArea | typeof import("./twin-space/twin-area.js").TwinArea | typeof import("./twin-model/twin-location.js").TwinLocation | typeof import("./twin-model/twin-equipment.js").TwinEquipment | typeof import("./twin-model/twin-operation.js").TwinOperation | typeof import("./twin-target/twin-target.js").TwinTarget | typeof import("./twin-audit/twin-audit-event.js").TwinAuditEvent | typeof import("./twin-ingest-window/twin-ingest-window.js").TwinIngestWindow)[];
20
+ export declare const entities: (typeof import("./twin-event/twin-event.js").TwinEvent | typeof import("./twin-subject/twin-subject-event.js").TwinSubjectEvent | typeof import("./twin-instance/twin-instance.js").TwinInstance | typeof import("./reference/twin-reference.js").TwinReference | typeof import("./twin-structure/twin-structure.js").TwinStructure | typeof import("./twin-space/twin-space.js").TwinSpace | typeof import("./twin-space/twin-space-representation.js").TwinSpaceRepresentation | typeof import("./twin-space/twin-space-area.js").TwinSpaceArea | typeof import("./twin-space/twin-area.js").TwinArea | typeof import("./twin-model/twin-location.js").TwinLocation | typeof import("./twin-model/twin-equipment.js").TwinEquipment | typeof import("./twin-model/twin-operation.js").TwinOperation | typeof import("./twin-target/twin-target.js").TwinTarget | typeof import("./twin-audit/twin-audit-event.js").TwinAuditEvent | typeof import("./twin-ingest-window/twin-ingest-window.js").TwinIngestWindow | typeof import("./actuation/twin-command.js").TwinCommandRecord)[];
21
21
  export declare const subscribers: any[];
22
22
  export declare const schema: {
23
23
  typeDefs: {};
@@ -34,50 +34,53 @@ const index_js_5 = require("./twin-structure/index.js");
34
34
  const index_js_6 = require("./twin-space/index.js");
35
35
  const index_js_7 = require("./twin-target/index.js");
36
36
  const index_js_8 = require("./reference/index.js");
37
- const index_js_9 = require("./twin-state/index.js");
38
- const index_js_10 = require("./twin-control/index.js");
39
- const index_js_11 = require("./twin-model/index.js");
37
+ /* 현장에 내리는 조치 — 승인이 재기동을 넘어 살아야 하므로 표로 든다(§`actuation/twin-command.ts`). */
38
+ const index_js_9 = require("./actuation/index.js");
39
+ const index_js_10 = require("./twin-state/index.js");
40
+ const index_js_11 = require("./twin-control/index.js");
41
+ const index_js_12 = require("./twin-model/index.js");
40
42
  /* 인제스트 창 — 동기화 추이를 재기동 뒤에도 이어 보기 위한 행(리졸버 없음: 메모리 장부가 조회를 답한다). */
41
- const index_js_12 = require("./twin-ingest-window/index.js");
42
- const index_js_13 = require("./twin-lifecycle/index.js");
43
- const index_js_14 = require("./twin-journal/index.js");
44
- const index_js_15 = require("./twin-forecast/index.js");
45
- const index_js_16 = require("./twin-attention/index.js");
46
- const index_js_17 = require("./twin-metrics/index.js");
47
- const index_js_18 = require("./twin-readiness/index.js");
43
+ const index_js_13 = require("./twin-ingest-window/index.js");
44
+ const index_js_14 = require("./twin-lifecycle/index.js");
45
+ const index_js_15 = require("./twin-journal/index.js");
46
+ const index_js_16 = require("./twin-forecast/index.js");
47
+ const index_js_17 = require("./twin-attention/index.js");
48
+ const index_js_18 = require("./twin-metrics/index.js");
49
+ const index_js_19 = require("./twin-readiness/index.js");
48
50
  /* 지난 기록 채우기 — 저널에만 쓰는 문. 라이브 유입과 판단이 달라 문을 따로 둔다. */
49
- const index_js_19 = require("./twin-backfill/index.js");
51
+ const index_js_20 = require("./twin-backfill/index.js");
50
52
  exports.entities = [
51
53
  /* ENTITIES */
52
54
  ...index_js_1.entities,
53
55
  ...index_js_2.entities,
54
- ...index_js_12.entities,
56
+ ...index_js_13.entities,
55
57
  ...index_js_3.entities,
56
58
  ...index_js_4.entities,
57
59
  ...index_js_5.entities,
58
60
  ...index_js_6.entities,
59
61
  ...index_js_7.entities,
60
62
  ...index_js_8.entities,
63
+ ...index_js_9.entities,
61
64
  /* 구조 투영 — model JSON 에서 풀어낸 표준 엔티티(ADR-0032). 원본에서 온 캐시. */
62
- ...index_js_11.entities
65
+ ...index_js_12.entities
63
66
  ];
64
67
  exports.subscribers = [];
65
68
  exports.schema = {
66
69
  typeDefs: {},
67
70
  resolverClasses: [
68
71
  /* RESOLVER CLASSES */
69
- ...index_js_9.resolvers,
70
72
  ...index_js_10.resolvers,
71
- ...index_js_13.resolvers,
73
+ ...index_js_11.resolvers,
72
74
  ...index_js_14.resolvers,
73
- ...index_js_3.resolvers,
74
75
  ...index_js_15.resolvers,
76
+ ...index_js_3.resolvers,
75
77
  ...index_js_16.resolvers,
76
78
  ...index_js_17.resolvers,
77
79
  ...index_js_18.resolvers,
78
80
  ...index_js_19.resolvers,
81
+ ...index_js_20.resolvers,
82
+ ...index_js_13.resolvers,
79
83
  ...index_js_12.resolvers,
80
- ...index_js_11.resolvers,
81
84
  ...index_js_6.resolvers,
82
85
  ...index_js_7.resolvers,
83
86
  ...index_js_8.resolvers,
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;;AAAA,yBAAyB;AACzB,qEAA0C;AAC1C,2EAAgD;AAChD,qFAA0D;AAC1D,2EAAgD;AAChD,qEAA0C;AAC1C,oFAAyD;AACzD,0EAA+C;AAC/C,uEAA4C;AAC5C,6EAAkD;AAClD,kFAAuD;AACvD,0EAA+C;AAC/C,4EAAiD;AACjD,2EAAgD;AAChD,wEAA6C;AAC7C,wEAA6C;AAC7C,2EAAgD;AAChD,4DAA4D;AAC5D,yEAA8C;AAC9C,iEAAiE;AACjE,8EAAmD;AACnD,sEAAsE;AACtE,6EAAkD;AAElD,mCAAmC;AACnC,oDAAqE;AACrE,sDAA4G;AAC5G,oDAAsG;AACtG,uDAA2E;AAC3E,wDAA6E;AAC7E,oDAAsG;AACtG,qDAAyG;AACzG,mDAA6G;AAC7G,oDAAuE;AACvE,uDAA2E;AAC3E,qDAAsG;AACtG,mEAAmE;AACnE,6DAA4H;AAC5H,yDAA+E;AAC/E,uDAA2E;AAC3E,wDAA6E;AAC7E,yDAA+E;AAC/E,uDAA2E;AAC3E,yDAA+E;AAC/E,qDAAqD;AACrD,wDAA6E;AAEhE,QAAA,QAAQ,GAAG;IACtB,cAAc;IACd,GAAG,mBAAiB;IACpB,GAAG,mBAAmB;IACtB,GAAG,oBAAwB;IAC3B,GAAG,mBAAiB;IACpB,GAAG,mBAAoB;IACvB,GAAG,mBAAqB;IACxB,GAAG,mBAAiB;IACpB,GAAG,mBAAkB;IACrB,GAAG,mBAAqB;IACxB,4DAA4D;IAC5D,GAAG,oBAAiB;CACrB,CAAA;AAEY,QAAA,WAAW,GAAG,EAAE,CAAA;AAEhB,QAAA,MAAM,GAAG;IACpB,QAAQ,EAAE,EAAE;IACZ,eAAe,EAAE;QACf,sBAAsB;QACtB,GAAG,oBAAkB;QACrB,GAAG,qBAAoB;QACvB,GAAG,qBAAsB;QACzB,GAAG,qBAAoB;QACvB,GAAG,oBAAkB;QACrB,GAAG,qBAAqB;QACxB,GAAG,qBAAsB;QACzB,GAAG,qBAAoB;QACvB,GAAG,qBAAsB;QACzB,GAAG,qBAAqB;QACxB,GAAG,qBAAyB;QAC5B,GAAG,qBAAkB;QACrB,GAAG,oBAAkB;QACrB,GAAG,oBAAmB;QACtB,GAAG,oBAAsB;QACzB,GAAG,oBAAoB;KACxB;IACD,UAAU,EAAE,EAAE;CACf,CAAA","sourcesContent":["/* EXPORT ENTITY TYPES */\nexport * from './twin-event/twin-event.js'\nexport * from './twin-audit/twin-audit-event.js'\nexport * from './twin-ingest-window/twin-ingest-window.js'\nexport * from './twin-instance/twin-instance.js'\nexport * from './twin-space/twin-space.js'\nexport * from './twin-space/twin-space-representation.js'\nexport * from './twin-space/twin-space-area.js'\nexport * from './twin-target/twin-target.js'\nexport * from './twin-structure/twin-structure.js'\nexport * from './twin-state/twin-state-subscription.js'\nexport * from './reference/reference-master.js'\nexport * from './reference/reference-registry.js'\nexport * from './reference/reference-adapter.js'\nexport * from './reference/reference-live.js'\nexport * from './reference/twin-reference.js'\nexport * from './reference/template-registry.js'\n/* 커넥터 점검 하네스 — 실 원본에 붙여 무엇이 채워지고 무엇이 비는지 센다(앱을 띄우지 않는다). */\nexport * from './reference/reference-probe.js'\n/* 읽은 뒤의 총평(사실 층) — 화면이 표로 그리고, 더 알고 싶을 때 AI 가 이 사실만 근거로 서술한다. */\nexport * from './reference/reference-assessment.js'\n/* 도메인 어휘 카탈로그 조합(커널 SSOT 노출) — 서버측 소비자(board-ai 도구 등)가 같은 형태를 쓰도록. */\nexport * from './twin-lifecycle/domain-catalog.js'\n\n/* IMPORT ENTITIES AND RESOLVERS */\nimport { entities as TwinEventEntities } from './twin-event/index.js'\nimport { entities as TwinSubjectEntities, resolvers as TwinSubjectResolvers } from './twin-subject/index.js'\nimport { entities as TwinAuditEntities, resolvers as TwinAuditResolvers } from './twin-audit/index.js'\nimport { entities as TwinInstanceEntities } from './twin-instance/index.js'\nimport { entities as TwinStructureEntities } from './twin-structure/index.js'\nimport { entities as TwinSpaceEntities, resolvers as TwinSpaceResolvers } from './twin-space/index.js'\nimport { entities as TwinTargetEntities, resolvers as TwinTargetResolvers } from './twin-target/index.js'\nimport { entities as TwinReferenceEntities, resolvers as TwinReferenceResolvers } from './reference/index.js'\nimport { resolvers as TwinStateResolvers } from './twin-state/index.js'\nimport { resolvers as TwinControlResolvers } from './twin-control/index.js'\nimport { entities as TwinModelEntities, resolvers as TwinModelResolvers } from './twin-model/index.js'\n/* 인제스트 창 — 동기화 추이를 재기동 뒤에도 이어 보기 위한 행(리졸버 없음: 메모리 장부가 조회를 답한다). */\nimport { entities as TwinIngestWindowEntities, resolvers as TwinIngestWindowResolvers } from './twin-ingest-window/index.js'\nimport { resolvers as TwinLifecycleResolvers } from './twin-lifecycle/index.js'\nimport { resolvers as TwinJournalResolvers } from './twin-journal/index.js'\nimport { resolvers as TwinForecastResolvers } from './twin-forecast/index.js'\nimport { resolvers as TwinAttentionResolvers } from './twin-attention/index.js'\nimport { resolvers as TwinMetricsResolvers } from './twin-metrics/index.js'\nimport { resolvers as TwinReadinessResolvers } from './twin-readiness/index.js'\n/* 지난 기록 채우기 — 저널에만 쓰는 문. 라이브 유입과 판단이 달라 문을 따로 둔다. */\nimport { resolvers as TwinBackfillResolvers } from './twin-backfill/index.js'\n\nexport const entities = [\n /* ENTITIES */\n ...TwinEventEntities,\n ...TwinSubjectEntities,\n ...TwinIngestWindowEntities,\n ...TwinAuditEntities,\n ...TwinInstanceEntities,\n ...TwinStructureEntities,\n ...TwinSpaceEntities,\n ...TwinTargetEntities,\n ...TwinReferenceEntities,\n /* 구조 투영 — model JSON 에서 풀어낸 표준 엔티티(ADR-0032). 원본에서 온 캐시. */\n ...TwinModelEntities\n]\n\nexport const subscribers = []\n\nexport const schema = {\n typeDefs: {},\n resolverClasses: [\n /* RESOLVER CLASSES */\n ...TwinStateResolvers,\n ...TwinControlResolvers,\n ...TwinLifecycleResolvers,\n ...TwinJournalResolvers,\n ...TwinAuditResolvers,\n ...TwinForecastResolvers,\n ...TwinAttentionResolvers,\n ...TwinMetricsResolvers,\n ...TwinReadinessResolvers,\n ...TwinBackfillResolvers,\n ...TwinIngestWindowResolvers,\n ...TwinModelResolvers,\n ...TwinSpaceResolvers,\n ...TwinTargetResolvers,\n ...TwinReferenceResolvers,\n ...TwinSubjectResolvers\n ],\n directives: {}\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;;AAAA,yBAAyB;AACzB,qEAA0C;AAC1C,2EAAgD;AAChD,qFAA0D;AAC1D,2EAAgD;AAChD,qEAA0C;AAC1C,oFAAyD;AACzD,0EAA+C;AAC/C,uEAA4C;AAC5C,6EAAkD;AAClD,kFAAuD;AACvD,0EAA+C;AAC/C,4EAAiD;AACjD,2EAAgD;AAChD,wEAA6C;AAC7C,wEAA6C;AAC7C,2EAAgD;AAChD,4DAA4D;AAC5D,yEAA8C;AAC9C,iEAAiE;AACjE,8EAAmD;AACnD,sEAAsE;AACtE,6EAAkD;AAElD,mCAAmC;AACnC,oDAAqE;AACrE,sDAA4G;AAC5G,oDAAsG;AACtG,uDAA2E;AAC3E,wDAA6E;AAC7E,oDAAsG;AACtG,qDAAyG;AACzG,mDAA6G;AAC7G,2EAA2E;AAC3E,mDAAwE;AACxE,qDAAuE;AACvE,uDAA2E;AAC3E,qDAAsG;AACtG,mEAAmE;AACnE,6DAA4H;AAC5H,yDAA+E;AAC/E,uDAA2E;AAC3E,wDAA6E;AAC7E,yDAA+E;AAC/E,uDAA2E;AAC3E,yDAA+E;AAC/E,qDAAqD;AACrD,wDAA6E;AAEhE,QAAA,QAAQ,GAAG;IACtB,cAAc;IACd,GAAG,mBAAiB;IACpB,GAAG,mBAAmB;IACtB,GAAG,oBAAwB;IAC3B,GAAG,mBAAiB;IACpB,GAAG,mBAAoB;IACvB,GAAG,mBAAqB;IACxB,GAAG,mBAAiB;IACpB,GAAG,mBAAkB;IACrB,GAAG,mBAAqB;IACxB,GAAG,mBAAqB;IACxB,4DAA4D;IAC5D,GAAG,oBAAiB;CACrB,CAAA;AAEY,QAAA,WAAW,GAAG,EAAE,CAAA;AAEhB,QAAA,MAAM,GAAG;IACpB,QAAQ,EAAE,EAAE;IACZ,eAAe,EAAE;QACf,sBAAsB;QACtB,GAAG,qBAAkB;QACrB,GAAG,qBAAoB;QACvB,GAAG,qBAAsB;QACzB,GAAG,qBAAoB;QACvB,GAAG,oBAAkB;QACrB,GAAG,qBAAqB;QACxB,GAAG,qBAAsB;QACzB,GAAG,qBAAoB;QACvB,GAAG,qBAAsB;QACzB,GAAG,qBAAqB;QACxB,GAAG,qBAAyB;QAC5B,GAAG,qBAAkB;QACrB,GAAG,oBAAkB;QACrB,GAAG,oBAAmB;QACtB,GAAG,oBAAsB;QACzB,GAAG,oBAAoB;KACxB;IACD,UAAU,EAAE,EAAE;CACf,CAAA","sourcesContent":["/* EXPORT ENTITY TYPES */\nexport * from './twin-event/twin-event.js'\nexport * from './twin-audit/twin-audit-event.js'\nexport * from './twin-ingest-window/twin-ingest-window.js'\nexport * from './twin-instance/twin-instance.js'\nexport * from './twin-space/twin-space.js'\nexport * from './twin-space/twin-space-representation.js'\nexport * from './twin-space/twin-space-area.js'\nexport * from './twin-target/twin-target.js'\nexport * from './twin-structure/twin-structure.js'\nexport * from './twin-state/twin-state-subscription.js'\nexport * from './reference/reference-master.js'\nexport * from './reference/reference-registry.js'\nexport * from './reference/reference-adapter.js'\nexport * from './reference/reference-live.js'\nexport * from './reference/twin-reference.js'\nexport * from './reference/template-registry.js'\n/* 커넥터 점검 하네스 — 실 원본에 붙여 무엇이 채워지고 무엇이 비는지 센다(앱을 띄우지 않는다). */\nexport * from './reference/reference-probe.js'\n/* 읽은 뒤의 총평(사실 층) — 화면이 표로 그리고, 더 알고 싶을 때 AI 가 이 사실만 근거로 서술한다. */\nexport * from './reference/reference-assessment.js'\n/* 도메인 어휘 카탈로그 조합(커널 SSOT 노출) — 서버측 소비자(board-ai 도구 등)가 같은 형태를 쓰도록. */\nexport * from './twin-lifecycle/domain-catalog.js'\n\n/* IMPORT ENTITIES AND RESOLVERS */\nimport { entities as TwinEventEntities } from './twin-event/index.js'\nimport { entities as TwinSubjectEntities, resolvers as TwinSubjectResolvers } from './twin-subject/index.js'\nimport { entities as TwinAuditEntities, resolvers as TwinAuditResolvers } from './twin-audit/index.js'\nimport { entities as TwinInstanceEntities } from './twin-instance/index.js'\nimport { entities as TwinStructureEntities } from './twin-structure/index.js'\nimport { entities as TwinSpaceEntities, resolvers as TwinSpaceResolvers } from './twin-space/index.js'\nimport { entities as TwinTargetEntities, resolvers as TwinTargetResolvers } from './twin-target/index.js'\nimport { entities as TwinReferenceEntities, resolvers as TwinReferenceResolvers } from './reference/index.js'\n/* 현장에 내리는 조치 — 승인이 재기동을 넘어 살아야 하므로 표로 든다(§`actuation/twin-command.ts`). */\nimport { entities as TwinActuationEntities } from './actuation/index.js'\nimport { resolvers as TwinStateResolvers } from './twin-state/index.js'\nimport { resolvers as TwinControlResolvers } from './twin-control/index.js'\nimport { entities as TwinModelEntities, resolvers as TwinModelResolvers } from './twin-model/index.js'\n/* 인제스트 창 — 동기화 추이를 재기동 뒤에도 이어 보기 위한 행(리졸버 없음: 메모리 장부가 조회를 답한다). */\nimport { entities as TwinIngestWindowEntities, resolvers as TwinIngestWindowResolvers } from './twin-ingest-window/index.js'\nimport { resolvers as TwinLifecycleResolvers } from './twin-lifecycle/index.js'\nimport { resolvers as TwinJournalResolvers } from './twin-journal/index.js'\nimport { resolvers as TwinForecastResolvers } from './twin-forecast/index.js'\nimport { resolvers as TwinAttentionResolvers } from './twin-attention/index.js'\nimport { resolvers as TwinMetricsResolvers } from './twin-metrics/index.js'\nimport { resolvers as TwinReadinessResolvers } from './twin-readiness/index.js'\n/* 지난 기록 채우기 — 저널에만 쓰는 문. 라이브 유입과 판단이 달라 문을 따로 둔다. */\nimport { resolvers as TwinBackfillResolvers } from './twin-backfill/index.js'\n\nexport const entities = [\n /* ENTITIES */\n ...TwinEventEntities,\n ...TwinSubjectEntities,\n ...TwinIngestWindowEntities,\n ...TwinAuditEntities,\n ...TwinInstanceEntities,\n ...TwinStructureEntities,\n ...TwinSpaceEntities,\n ...TwinTargetEntities,\n ...TwinReferenceEntities,\n ...TwinActuationEntities,\n /* 구조 투영 — model JSON 에서 풀어낸 표준 엔티티(ADR-0032). 원본에서 온 캐시. */\n ...TwinModelEntities\n]\n\nexport const subscribers = []\n\nexport const schema = {\n typeDefs: {},\n resolverClasses: [\n /* RESOLVER CLASSES */\n ...TwinStateResolvers,\n ...TwinControlResolvers,\n ...TwinLifecycleResolvers,\n ...TwinJournalResolvers,\n ...TwinAuditResolvers,\n ...TwinForecastResolvers,\n ...TwinAttentionResolvers,\n ...TwinMetricsResolvers,\n ...TwinReadinessResolvers,\n ...TwinBackfillResolvers,\n ...TwinIngestWindowResolvers,\n ...TwinModelResolvers,\n ...TwinSpaceResolvers,\n ...TwinTargetResolvers,\n ...TwinReferenceResolvers,\n ...TwinSubjectResolvers\n ],\n directives: {}\n}\n"]}