@things-factory/headless-twin 10.1.27 → 10.1.29
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.
- package/dist-server/index.js +6 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/service/reference/actuation-verification.d.ts +22 -0
- package/dist-server/service/reference/actuation-verification.js +37 -0
- package/dist-server/service/reference/actuation-verification.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.js +32 -2
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/twin-model/external-resolver.d.ts +13 -3
- package/dist-server/service/twin-model/external-resolver.js +2 -2
- package/dist-server/service/twin-model/external-resolver.js.map +1 -1
- package/dist-server/service/twin-model/twin-lineage-query.js +14 -0
- package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-item-query.js +41 -12
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-query.js +14 -0
- package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
- package/dist-server/service/twin-space/area-resolver.d.ts +6 -0
- package/dist-server/service/twin-space/area-resolver.js +68 -0
- package/dist-server/service/twin-space/area-resolver.js.map +1 -0
- package/package.json +8 -8
- package/server/index.ts +6 -0
- package/server/service/reference/actuation-verification.ts +42 -0
- package/server/service/reference/reference-resolver.ts +33 -2
- package/server/service/twin-model/external-resolver.ts +15 -3
- package/server/service/twin-model/twin-lineage-query.ts +13 -0
- package/server/service/twin-model/twin-model-item-query.ts +62 -12
- package/server/service/twin-model/twin-model-query.ts +14 -0
- package/server/service/twin-space/area-resolver.ts +75 -0
- package/test/actuation-verification-state.test.ts +46 -1
- package/test/area-resolver-db.test.ts +110 -0
- package/test/axis-declaration-passthrough-db.test.ts +152 -0
- package/test/external-resolver.test.ts +11 -5
- package/test/lineage-survives-restart.test.ts +15 -0
- package/test/twin-model-item-db.test.ts +18 -1
- package/tsconfig.tsbuildinfo +1 -1
package/dist-server/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const index_js_2 = require("./engine/index.js");
|
|
|
13
13
|
const reference_live_js_1 = require("./service/reference/reference-live.js");
|
|
14
14
|
const index_js_3 = require("./service/twin-ingest-window/index.js");
|
|
15
15
|
const reference_links_js_1 = require("./service/reference/reference-links.js");
|
|
16
|
+
const area_resolver_js_1 = require("./service/twin-space/area-resolver.js");
|
|
16
17
|
const rule_loop_js_1 = require("./service/actuation/rule-loop.js");
|
|
17
18
|
const fill_loop_js_1 = require("./service/reference/fill-loop.js");
|
|
18
19
|
/*
|
|
@@ -36,6 +37,11 @@ process.on('bootstrap-module-start', async ({ app, config, client }) => {
|
|
|
36
37
|
* 것들. 조회용 참조는 트윈으로 복제하지 않고 그때 물어본다.
|
|
37
38
|
*/
|
|
38
39
|
(0, reference_links_js_1.installReferenceLinkResolver)();
|
|
40
|
+
/*
|
|
41
|
+
* 구역 이름을 답하는 문 — 자리의 부모가 구역일 때 화면이 id 대신 이름을 보인다. 등록이 없던
|
|
42
|
+
* 동안에는 모든 자리의 부모가 「풀 수 있는 부품이 없다」로 나왔다(2026-09-18 실측).
|
|
43
|
+
*/
|
|
44
|
+
(0, area_resolver_js_1.installAreaResolver)(m => (0, log_js_1.twinLog)(m));
|
|
39
45
|
/*
|
|
40
46
|
* **저널 보존 기간은 선언에서 온다** — 코드에 박아 두지 않는다(2026-08-22).
|
|
41
47
|
*
|
package/dist-server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../server/index.ts"],"names":[],"mappings":";;;;AAAA,4DAAiC;AACjC,6DAAkC;AAClC,qEAAqE;AACrE,kDAAkD;AAAzC,sGAAA,UAAU,OAAA;AAEnB,4CAAoD;AACpD,uBAAoB;AAEpB,gDAA8C;AAC9C,6EAAgF;AAChF,oEAAiF;AACjF,+EAAqF;AACrF,mEAAyE;AACzE,mEAAgE;AAEhE;;;;;;;;GAQG;AACH,OAAO,CAAC,EAAE,CAAC,wBAA+B,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAO,EAAE,EAAE;IACjF,IAAI,CAAC;QACH;;;WAGG;QACH,IAAA,oCAAyB,GAAE,CAAA;QAC3B;;;WAGG;QACH,IAAA,iDAA4B,GAAE,CAAA;QAC9B;;;;;;;;WAQG;QACH;;;;;;;;WAQG;QACH,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,2BAA2B,IAAI,CAAC,CAAC,CAAA;QACpE,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,EAAE,oBAAoB,IAAI,CAAC,CAAC,CAAA;QAC3E,MAAM,aAAa,GAAG,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAA;QACxD,IAAI,aAAa,GAAG,CAAC,EAAE,CAAC;YACtB,qBAAU,CAAC,sBAAsB,GAAG,aAAa,CAAA;YACjD,IAAA,gBAAO,EAAC,4BAA4B,aAAa,MAAM,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAA;QACxF,CAAC;QAED,MAAM,qBAAU,CAAC,SAAS,EAAE,CAAA;QAC5B,MAAM,IAAA,4CAAwB,GAAE,CAAA;QAChC;;;WAGG;QACH,IAAA,4BAAa,GAAE,CAAA;QAEf;;;;;;WAMG;QACH,IAAA,qCAAsB,GAAE,CAAA;QAExB,IAAA,gBAAO,EAAC,iCAAiC,CAAC,CAAA;IAC5C,CAAC;IAAC,OAAO,EAAE,EAAE,CAAC;QACZ,IAAA,kBAAS,EAAC,qCAAqC,EAAE,EAAE,CAAC,CAAA;IACtD,CAAC;AACH,CAAC,CAAC,CAAA","sourcesContent":["export * from './engine/index.js'\nexport * from './service/index.js'\n/* 스키마 이력 — 프레임워크가 `migrations` export 를 모아 실행한다(`run migration`). */\nexport { migrations } from './migrations/index.js'\n\nimport { twinLog, twinError } from './engine/log.js'\nimport './routes.js'\n\nimport { TwinEngine } from './engine/index.js'\nimport { resumeReferenceLiveFeeds } from './service/reference/reference-live.js'\nimport { installIngestWindowWriter } from './service/twin-ingest-window/index.js'\nimport { installReferenceLinkResolver } from './service/reference/reference-links.js'\nimport { startActuationRuleLoop } from './service/actuation/rule-loop.js'\nimport { startFillLoop } from './service/reference/fill-loop.js'\n\n/*\n * 모듈 부팅 — **도는 트윈을 되살린다.**\n *\n * 두 층이 순서대로다: ① 엔진이 등록부의 `running` 행을 선언된 `restartPolicy` 그대로 되살리고\n * (`bootstrap`), ② 레퍼런스 계층이 미러 트윈의 커넥터 피드를 다시 붙인다. 순서가 뒤집히면 피드가\n * 아직 없는 커널에 계측을 붓게 된다.\n *\n * 예전에는 ①도 ②도 없었다 — 재기동 한 번에 등록부만 「도는 중」이라 말하는 트윈이 남았다.\n */\nprocess.on('bootstrap-module-start' as any, async ({ app, config, client }: any) => {\n try {\n /*\n * 닫힌 인제스트 창을 표로 옮기는 문을 먼저 연다 — 트윈을 되살리기 **전에** 등록해야 첫 창부터\n * 남는다. 커널 층은 저장 계층을 모르므로 이 등록이 그 둘을 잇는 유일한 자리다.\n */\n installIngestWindowWriter()\n /*\n * 원본에만 있는 관계를 조회하는 문을 연다 — MES 의 생산 실적·정비 계획처럼 트윈이 담지 않는\n * 것들. 조회용 참조는 트윈으로 복제하지 않고 그때 물어본다.\n */\n installReferenceLinkResolver()\n /*\n * **저널 보존 기간은 선언에서 온다** — 코드에 박아 두지 않는다(2026-08-22).\n *\n * 실측: 개발 저널이 시간당 581,794행 · 0.85GB(하루 20GB) 로 자랐고 지우는 것이 없었다. 그런데\n * 「며칠까지 남기나」는 **배포마다 갈리는 사실**이다 — 개발은 짧아도 되고 실증은 길어야 한다.\n *\n * 선언이 없으면 **아무것도 지우지 않는다.** 알리지 않고 지우는 편이 알리지 않고 쌓는 것보다 나쁘다.\n * 값은 스냅샷 TTL(7일)보다 짧을 수 없다 — 엔진이 그것을 올려 주고 그 사실을 말한다.\n */\n /*\n * 읽는 순서: **환경변수 → 설정 → 없음.**\n *\n * 설정(`config.twin.journalRetentionDays`)이 배포의 선언이고, 환경변수\n * (`TWIN_JOURNAL_RETENTION_DAYS`)는 그것을 **그 자리에서** 덮는다. 컨테이너로 도는 배포에서는\n * 파일을 고치지 않고 값을 바꿀 수 있어야 하고, 개발에서도 그 편이 빠르다.\n *\n * 둘 다 없으면 **아무것도 지우지 않는다.**\n */\n const fromEnv = Number(process.env.TWIN_JOURNAL_RETENTION_DAYS ?? 0)\n const fromConfig = Number(config?.get?.('twin')?.journalRetentionDays ?? 0)\n const retentionDays = fromEnv > 0 ? fromEnv : fromConfig\n if (retentionDays > 0) {\n TwinEngine.JOURNAL_RETENTION_DAYS = retentionDays\n twinLog(`[twin] journal retention ${retentionDays}일 (${fromEnv > 0 ? '환경변수' : '설정'})`)\n }\n\n await TwinEngine.bootstrap()\n await resumeReferenceLiveFeeds()\n /*\n * 빠진 구간을 다시 받는 주기 — 기동에서 한 번, 그 뒤 10분마다(§`fill-loop.ts`).\n * 피드를 붙인 **뒤에** 세운다: 커널이 되살아나기 전에 부르면 도는 트윈이 0개다.\n */\n startFillLoop()\n\n /*\n * 조치 규칙 주기 — **트윈이 다 선 뒤에** 세운다. 앞에 세우면 첫 바퀴가 아직 안 선 트윈들을 돌고\n * 「상태를 읽을 수 없다」를 규칙 행에 적는다. 그것은 사실이지만 사람에게는 오해다.\n *\n * 커널 틱과 따로 돈다 — 틱은 예외가 나면 그 트윈을 멈추고, 규칙 하나가 잘못됐다고 공장이 멈추면\n * 안 된다(§`rule-loop.ts`).\n */\n startActuationRuleLoop()\n\n twinLog('Headless Twin host has started.')\n } catch (ex) {\n twinError('Headless Twin host failed to start.', ex)\n }\n})\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../server/index.ts"],"names":[],"mappings":";;;;AAAA,4DAAiC;AACjC,6DAAkC;AAClC,qEAAqE;AACrE,kDAAkD;AAAzC,sGAAA,UAAU,OAAA;AAEnB,4CAAoD;AACpD,uBAAoB;AAEpB,gDAA8C;AAC9C,6EAAgF;AAChF,oEAAiF;AACjF,+EAAqF;AACrF,4EAA2E;AAC3E,mEAAyE;AACzE,mEAAgE;AAEhE;;;;;;;;GAQG;AACH,OAAO,CAAC,EAAE,CAAC,wBAA+B,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAO,EAAE,EAAE;IACjF,IAAI,CAAC;QACH;;;WAGG;QACH,IAAA,oCAAyB,GAAE,CAAA;QAC3B;;;WAGG;QACH,IAAA,iDAA4B,GAAE,CAAA;QAC9B;;;WAGG;QACH,IAAA,sCAAmB,EAAC,CAAC,CAAC,EAAE,CAAC,IAAA,gBAAO,EAAC,CAAC,CAAC,CAAC,CAAA;QACpC;;;;;;;;WAQG;QACH;;;;;;;;WAQG;QACH,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,2BAA2B,IAAI,CAAC,CAAC,CAAA;QACpE,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,EAAE,oBAAoB,IAAI,CAAC,CAAC,CAAA;QAC3E,MAAM,aAAa,GAAG,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAA;QACxD,IAAI,aAAa,GAAG,CAAC,EAAE,CAAC;YACtB,qBAAU,CAAC,sBAAsB,GAAG,aAAa,CAAA;YACjD,IAAA,gBAAO,EAAC,4BAA4B,aAAa,MAAM,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAA;QACxF,CAAC;QAED,MAAM,qBAAU,CAAC,SAAS,EAAE,CAAA;QAC5B,MAAM,IAAA,4CAAwB,GAAE,CAAA;QAChC;;;WAGG;QACH,IAAA,4BAAa,GAAE,CAAA;QAEf;;;;;;WAMG;QACH,IAAA,qCAAsB,GAAE,CAAA;QAExB,IAAA,gBAAO,EAAC,iCAAiC,CAAC,CAAA;IAC5C,CAAC;IAAC,OAAO,EAAE,EAAE,CAAC;QACZ,IAAA,kBAAS,EAAC,qCAAqC,EAAE,EAAE,CAAC,CAAA;IACtD,CAAC;AACH,CAAC,CAAC,CAAA","sourcesContent":["export * from './engine/index.js'\nexport * from './service/index.js'\n/* 스키마 이력 — 프레임워크가 `migrations` export 를 모아 실행한다(`run migration`). */\nexport { migrations } from './migrations/index.js'\n\nimport { twinLog, twinError } from './engine/log.js'\nimport './routes.js'\n\nimport { TwinEngine } from './engine/index.js'\nimport { resumeReferenceLiveFeeds } from './service/reference/reference-live.js'\nimport { installIngestWindowWriter } from './service/twin-ingest-window/index.js'\nimport { installReferenceLinkResolver } from './service/reference/reference-links.js'\nimport { installAreaResolver } from './service/twin-space/area-resolver.js'\nimport { startActuationRuleLoop } from './service/actuation/rule-loop.js'\nimport { startFillLoop } from './service/reference/fill-loop.js'\n\n/*\n * 모듈 부팅 — **도는 트윈을 되살린다.**\n *\n * 두 층이 순서대로다: ① 엔진이 등록부의 `running` 행을 선언된 `restartPolicy` 그대로 되살리고\n * (`bootstrap`), ② 레퍼런스 계층이 미러 트윈의 커넥터 피드를 다시 붙인다. 순서가 뒤집히면 피드가\n * 아직 없는 커널에 계측을 붓게 된다.\n *\n * 예전에는 ①도 ②도 없었다 — 재기동 한 번에 등록부만 「도는 중」이라 말하는 트윈이 남았다.\n */\nprocess.on('bootstrap-module-start' as any, async ({ app, config, client }: any) => {\n try {\n /*\n * 닫힌 인제스트 창을 표로 옮기는 문을 먼저 연다 — 트윈을 되살리기 **전에** 등록해야 첫 창부터\n * 남는다. 커널 층은 저장 계층을 모르므로 이 등록이 그 둘을 잇는 유일한 자리다.\n */\n installIngestWindowWriter()\n /*\n * 원본에만 있는 관계를 조회하는 문을 연다 — MES 의 생산 실적·정비 계획처럼 트윈이 담지 않는\n * 것들. 조회용 참조는 트윈으로 복제하지 않고 그때 물어본다.\n */\n installReferenceLinkResolver()\n /*\n * 구역 이름을 답하는 문 — 자리의 부모가 구역일 때 화면이 id 대신 이름을 보인다. 등록이 없던\n * 동안에는 모든 자리의 부모가 「풀 수 있는 부품이 없다」로 나왔다(2026-09-18 실측).\n */\n installAreaResolver(m => twinLog(m))\n /*\n * **저널 보존 기간은 선언에서 온다** — 코드에 박아 두지 않는다(2026-08-22).\n *\n * 실측: 개발 저널이 시간당 581,794행 · 0.85GB(하루 20GB) 로 자랐고 지우는 것이 없었다. 그런데\n * 「며칠까지 남기나」는 **배포마다 갈리는 사실**이다 — 개발은 짧아도 되고 실증은 길어야 한다.\n *\n * 선언이 없으면 **아무것도 지우지 않는다.** 알리지 않고 지우는 편이 알리지 않고 쌓는 것보다 나쁘다.\n * 값은 스냅샷 TTL(7일)보다 짧을 수 없다 — 엔진이 그것을 올려 주고 그 사실을 말한다.\n */\n /*\n * 읽는 순서: **환경변수 → 설정 → 없음.**\n *\n * 설정(`config.twin.journalRetentionDays`)이 배포의 선언이고, 환경변수\n * (`TWIN_JOURNAL_RETENTION_DAYS`)는 그것을 **그 자리에서** 덮는다. 컨테이너로 도는 배포에서는\n * 파일을 고치지 않고 값을 바꿀 수 있어야 하고, 개발에서도 그 편이 빠르다.\n *\n * 둘 다 없으면 **아무것도 지우지 않는다.**\n */\n const fromEnv = Number(process.env.TWIN_JOURNAL_RETENTION_DAYS ?? 0)\n const fromConfig = Number(config?.get?.('twin')?.journalRetentionDays ?? 0)\n const retentionDays = fromEnv > 0 ? fromEnv : fromConfig\n if (retentionDays > 0) {\n TwinEngine.JOURNAL_RETENTION_DAYS = retentionDays\n twinLog(`[twin] journal retention ${retentionDays}일 (${fromEnv > 0 ? '환경변수' : '설정'})`)\n }\n\n await TwinEngine.bootstrap()\n await resumeReferenceLiveFeeds()\n /*\n * 빠진 구간을 다시 받는 주기 — 기동에서 한 번, 그 뒤 10분마다(§`fill-loop.ts`).\n * 피드를 붙인 **뒤에** 세운다: 커널이 되살아나기 전에 부르면 도는 트윈이 0개다.\n */\n startFillLoop()\n\n /*\n * 조치 규칙 주기 — **트윈이 다 선 뒤에** 세운다. 앞에 세우면 첫 바퀴가 아직 안 선 트윈들을 돌고\n * 「상태를 읽을 수 없다」를 규칙 행에 적는다. 그것은 사실이지만 사람에게는 오해다.\n *\n * 커널 틱과 따로 돈다 — 틱은 예외가 나면 그 트윈을 멈추고, 규칙 하나가 잘못됐다고 공장이 멈추면\n * 안 된다(§`rule-loop.ts`).\n */\n startActuationRuleLoop()\n\n twinLog('Headless Twin host has started.')\n } catch (ex) {\n twinError('Headless Twin host failed to start.', ex)\n }\n})\n"]}
|
|
@@ -20,3 +20,25 @@ export interface ActuationOutcomePatch {
|
|
|
20
20
|
* 서로 다른 말을 한다. 모르는 것은 안 적는다.
|
|
21
21
|
*/
|
|
22
22
|
export declare function actuationOutcomePatch(result: ActuationResult, at: string): ActuationOutcomePatch;
|
|
23
|
+
/**
|
|
24
|
+
* 저장이 조치의 주소를 바꾸나 — **바꿨으면 검증은 저장 시각부터 다시 없다**(ADR-0046 ②).
|
|
25
|
+
*
|
|
26
|
+
* 2026-09-18 실측: 칸 셋은 서 있고 왕복의 결과를 적는 자리도 있는데, 주소를 바꿔도 `verified` 가
|
|
27
|
+
* 그대로 남았다. 화면은 다른 공장을 가리키는 연결을 「확인됨」으로 그린다 — 그 초록이 가장 위험한
|
|
28
|
+
* 거짓이다.
|
|
29
|
+
*
|
|
30
|
+
* `unverified` 는 실패가 아니다. 아직 그 주소로 넘겨 본 적이 없다는 사실이고, 사람이 할 일은 한 번
|
|
31
|
+
* 넘겨 보는 것이다.
|
|
32
|
+
*/
|
|
33
|
+
export declare function actuationResetPatch(before: Record<string, unknown> | null | undefined, after: Record<string, unknown> | null | undefined): {
|
|
34
|
+
actuationState: ActuationState;
|
|
35
|
+
actuationVerifiedAt: null;
|
|
36
|
+
actuationFailure: null;
|
|
37
|
+
} | null;
|
|
38
|
+
/**
|
|
39
|
+
* 이 연결이 조치를 **보낼 수 있게 적혀 있나** — 주소가 없으면 어디로도 안 간다.
|
|
40
|
+
*
|
|
41
|
+
* 검증 상태를 화면에 보일지 가르는 값이다. 읽기만 하는 연결(마스터 동기만 하는 커넥터)에 「아직
|
|
42
|
+
* 확인 안 됨」을 그리면, 할 일이 없는 사람에게 할 일이 있는 것처럼 보인다.
|
|
43
|
+
*/
|
|
44
|
+
export declare function actuationAddressed(cfg: Record<string, unknown> | null | undefined): boolean;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.actuationOutcomePatch = actuationOutcomePatch;
|
|
4
|
+
exports.actuationResetPatch = actuationResetPatch;
|
|
5
|
+
exports.actuationAddressed = actuationAddressed;
|
|
4
6
|
/**
|
|
5
7
|
* 결과 → 적을 것.
|
|
6
8
|
*
|
|
@@ -25,4 +27,39 @@ function actuationOutcomePatch(result, at) {
|
|
|
25
27
|
: null
|
|
26
28
|
};
|
|
27
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* 조치가 가는 주소를 이루는 칸들 — 이 중 하나라도 바뀌면 지난 왕복은 **다른 주소의 이야기**다.
|
|
32
|
+
*
|
|
33
|
+
* `intakeSiteId` 가 없으면 `siteId` 가 그 자리를 대신한다(부딪힘 판정과 같은 규칙이다 —
|
|
34
|
+
* `connection-portability.ts`). 비밀값을 함께 보는 이유는, 주소가 같아도 저쪽이 우리를 못 알아보면
|
|
35
|
+
* 그 왕복이 다시 성립하지 않기 때문이다.
|
|
36
|
+
*/
|
|
37
|
+
const ACTUATION_ADDRESS = ['intakeEndpoint', 'intakeSiteId', 'siteId', 'intakeSecret'];
|
|
38
|
+
const addressOf = (cfg) => ACTUATION_ADDRESS.map(k => String(cfg?.[k] ?? '').trim().replace(/\/$/, '')).join('\u0000');
|
|
39
|
+
/**
|
|
40
|
+
* 저장이 조치의 주소를 바꾸나 — **바꿨으면 검증은 저장 시각부터 다시 없다**(ADR-0046 ②).
|
|
41
|
+
*
|
|
42
|
+
* 2026-09-18 실측: 칸 셋은 서 있고 왕복의 결과를 적는 자리도 있는데, 주소를 바꿔도 `verified` 가
|
|
43
|
+
* 그대로 남았다. 화면은 다른 공장을 가리키는 연결을 「확인됨」으로 그린다 — 그 초록이 가장 위험한
|
|
44
|
+
* 거짓이다.
|
|
45
|
+
*
|
|
46
|
+
* `unverified` 는 실패가 아니다. 아직 그 주소로 넘겨 본 적이 없다는 사실이고, 사람이 할 일은 한 번
|
|
47
|
+
* 넘겨 보는 것이다.
|
|
48
|
+
*/
|
|
49
|
+
function actuationResetPatch(before, after) {
|
|
50
|
+
if (addressOf(before) === addressOf(after))
|
|
51
|
+
return null;
|
|
52
|
+
return { actuationState: 'unverified', actuationVerifiedAt: null, actuationFailure: null };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 이 연결이 조치를 **보낼 수 있게 적혀 있나** — 주소가 없으면 어디로도 안 간다.
|
|
56
|
+
*
|
|
57
|
+
* 검증 상태를 화면에 보일지 가르는 값이다. 읽기만 하는 연결(마스터 동기만 하는 커넥터)에 「아직
|
|
58
|
+
* 확인 안 됨」을 그리면, 할 일이 없는 사람에게 할 일이 있는 것처럼 보인다.
|
|
59
|
+
*/
|
|
60
|
+
function actuationAddressed(cfg) {
|
|
61
|
+
const endpoint = String(cfg?.intakeEndpoint ?? '').trim();
|
|
62
|
+
const site = String(cfg?.intakeSiteId ?? cfg?.siteId ?? '').trim();
|
|
63
|
+
return !!endpoint && !!site;
|
|
64
|
+
}
|
|
28
65
|
//# sourceMappingURL=actuation-verification.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actuation-verification.js","sourceRoot":"","sources":["../../../server/service/reference/actuation-verification.ts"],"names":[],"mappings":";;AA+BA,sDAWC;
|
|
1
|
+
{"version":3,"file":"actuation-verification.js","sourceRoot":"","sources":["../../../server/service/reference/actuation-verification.ts"],"names":[],"mappings":";;AA+BA,sDAWC;AAwBD,kDAMC;AAQD,gDAIC;AAlED;;;;;;;;;;;;GAYG;AACH,SAAgB,qBAAqB,CAAC,MAAuB,EAAE,EAAU;IACvE,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC;QACd,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,mBAAmB,EAAE,EAAE,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAA;IACxF,CAAC;IAED,OAAO;QACL,cAAc,EAAE,QAAQ;QACxB,gBAAgB,EAAE,MAAM,CAAC,SAAS;YAChC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,SAAS,EAAE,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE;YAC/F,CAAC,CAAC,IAAI;KACT,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,iBAAiB,GAAG,CAAC,gBAAgB,EAAE,cAAc,EAAE,QAAQ,EAAE,cAAc,CAAU,CAAA;AAE/F,MAAM,SAAS,GAAG,CAAC,GAA+C,EAAE,EAAE,CACpE,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAE,GAAW,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;AAEtG;;;;;;;;;GASG;AACH,SAAgB,mBAAmB,CACjC,MAAkD,EAClD,KAAiD;IAEjD,IAAI,SAAS,CAAC,MAAM,CAAC,KAAK,SAAS,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACvD,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,mBAAmB,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAA;AAC5F,CAAC;AAED;;;;;GAKG;AACH,SAAgB,kBAAkB,CAAC,GAA+C;IAChF,MAAM,QAAQ,GAAG,MAAM,CAAE,GAAW,EAAE,cAAc,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;IAClE,MAAM,IAAI,GAAG,MAAM,CAAE,GAAW,EAAE,YAAY,IAAK,GAAW,EAAE,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;IACpF,OAAO,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,CAAA;AAC7B,CAAC","sourcesContent":["/*\n * 조치 왕복의 결과를 레퍼런스에 어떻게 적을 것인가 — **순수하게.**\n *\n * `actuation-routing.ts` 에 두면 시험이 그 파일을 들여야 하고, 그 파일은 엔티티를 값으로 들인다\n * (데코레이터가 붙은 클래스). 가벼운 모듈로 떼어 두면 판단만 따로 확인할 수 있다.\n *\n * 여기서 들이는 것은 **타입뿐**이다 — 런타임에 남지 않는다.\n */\nimport type { ActuationFailure, ActuationState } from './twin-reference.js'\nimport type { ActuationResult } from './reference-adapter.js'\n\n/** 레퍼런스에 적을 칸들. `actuationVerifiedAt` 이 없는 것은 「건드리지 않는다」는 뜻이다. */\nexport interface ActuationOutcomePatch {\n actuationState: ActuationState\n actuationVerifiedAt?: string\n actuationFailure: ActuationFailure | null\n}\n\n/**\n * 결과 → 적을 것.\n *\n * ── 세 가지를 지킨다 ───────────────────────────────────────────────────────\n * **받아들여지면 지난 거절을 지운다.** 남겨 두면 화면이 「됐는데 실패 사유가 있다」를 그린다.\n *\n * **거절은 성공 시각을 건드리지 않는다.** 「전에 한 번 닿았다」와 「이번에 안 됐다」는 둘 다 사실이고,\n * 지우면 앞엣것이 사라진다 — 사람이 「한 번이라도 닿은 적 있나」를 물을 자리가 없어진다.\n *\n * **코드가 없으면 사유를 비운다.** 자리를 채우려고 `error` 문장을 옮기지 않는다. `lastError` 가 이미\n * 겪었다 — 저장된 사람 문장은 번역도 집계도 안 되고, 커넥터가 그 문장을 고치는 날 옛 행과 새 행이\n * 서로 다른 말을 한다. 모르는 것은 안 적는다.\n */\nexport function actuationOutcomePatch(result: ActuationResult, at: string): ActuationOutcomePatch {\n if (result.ok) {\n return { actuationState: 'verified', actuationVerifiedAt: at, actuationFailure: null }\n }\n\n return {\n actuationState: 'failed',\n actuationFailure: result.errorCode\n ? { code: result.errorCode, at, ...(result.errorParams ? { params: result.errorParams } : {}) }\n : null\n }\n}\n\n/**\n * 조치가 가는 주소를 이루는 칸들 — 이 중 하나라도 바뀌면 지난 왕복은 **다른 주소의 이야기**다.\n *\n * `intakeSiteId` 가 없으면 `siteId` 가 그 자리를 대신한다(부딪힘 판정과 같은 규칙이다 —\n * `connection-portability.ts`). 비밀값을 함께 보는 이유는, 주소가 같아도 저쪽이 우리를 못 알아보면\n * 그 왕복이 다시 성립하지 않기 때문이다.\n */\nconst ACTUATION_ADDRESS = ['intakeEndpoint', 'intakeSiteId', 'siteId', 'intakeSecret'] as const\n\nconst addressOf = (cfg: Record<string, unknown> | null | undefined) =>\n ACTUATION_ADDRESS.map(k => String((cfg as any)?.[k] ?? '').trim().replace(/\\/$/, '')).join('\\u0000')\n\n/**\n * 저장이 조치의 주소를 바꾸나 — **바꿨으면 검증은 저장 시각부터 다시 없다**(ADR-0046 ②).\n *\n * 2026-09-18 실측: 칸 셋은 서 있고 왕복의 결과를 적는 자리도 있는데, 주소를 바꿔도 `verified` 가\n * 그대로 남았다. 화면은 다른 공장을 가리키는 연결을 「확인됨」으로 그린다 — 그 초록이 가장 위험한\n * 거짓이다.\n *\n * `unverified` 는 실패가 아니다. 아직 그 주소로 넘겨 본 적이 없다는 사실이고, 사람이 할 일은 한 번\n * 넘겨 보는 것이다.\n */\nexport function actuationResetPatch(\n before: Record<string, unknown> | null | undefined,\n after: Record<string, unknown> | null | undefined\n): { actuationState: ActuationState; actuationVerifiedAt: null; actuationFailure: null } | null {\n if (addressOf(before) === addressOf(after)) return null\n return { actuationState: 'unverified', actuationVerifiedAt: null, actuationFailure: null }\n}\n\n/**\n * 이 연결이 조치를 **보낼 수 있게 적혀 있나** — 주소가 없으면 어디로도 안 간다.\n *\n * 검증 상태를 화면에 보일지 가르는 값이다. 읽기만 하는 연결(마스터 동기만 하는 커넥터)에 「아직\n * 확인 안 됨」을 그리면, 할 일이 없는 사람에게 할 일이 있는 것처럼 보인다.\n */\nexport function actuationAddressed(cfg: Record<string, unknown> | null | undefined): boolean {\n const endpoint = String((cfg as any)?.intakeEndpoint ?? '').trim()\n const site = String((cfg as any)?.intakeSiteId ?? (cfg as any)?.siteId ?? '').trim()\n return !!endpoint && !!site\n}\n"]}
|
|
@@ -19,6 +19,7 @@ const reference_registry_js_1 = require("./reference-registry.js");
|
|
|
19
19
|
const reference_adapter_js_1 = require("./reference-adapter.js");
|
|
20
20
|
const discovery_result_js_1 = require("./discovery-result.js");
|
|
21
21
|
const ingest_space_js_1 = require("./ingest-space.js");
|
|
22
|
+
const actuation_verification_js_1 = require("./actuation-verification.js");
|
|
22
23
|
const connection_portability_js_1 = require("./connection-portability.js");
|
|
23
24
|
const reference_live_js_1 = require("./reference-live.js");
|
|
24
25
|
const template_registry_js_1 = require("./template-registry.js");
|
|
@@ -425,7 +426,21 @@ let TwinReferenceResolver = class TwinReferenceResolver {
|
|
|
425
426
|
return {
|
|
426
427
|
source: r.source, system: r.system, siteName: r.siteName, description: r.description,
|
|
427
428
|
adapterType: r.adapterType, status: r.status, lastSyncedAt: r.lastSyncedAt, lastError: r.lastError,
|
|
428
|
-
producedCount: produced.length, produced
|
|
429
|
+
producedCount: produced.length, produced,
|
|
430
|
+
/*
|
|
431
|
+
* **조치가 저쪽에 닿아 봤나** (ADR-0046 ②). 칸 셋은 2026-09 부터 서 있었는데 목록도 상세도
|
|
432
|
+
* 이 값을 싣지 않아 **화면이 볼 길이 없었다** — 저장된 사실이 아무에게도 닿지 않았다.
|
|
433
|
+
*
|
|
434
|
+
* 보낼 주소가 적힌 연결에만 싣는다. 읽기만 하는 연결에 「아직 확인 안 됨」이 서면 할 일이 없는
|
|
435
|
+
* 사람에게 할 일이 있는 것처럼 보인다.
|
|
436
|
+
*/
|
|
437
|
+
...((0, actuation_verification_js_1.actuationAddressed)(r.connectionConfig)
|
|
438
|
+
? {
|
|
439
|
+
actuationState: r.actuationState ?? 'unverified',
|
|
440
|
+
actuationVerifiedAt: r.actuationVerifiedAt ?? null,
|
|
441
|
+
actuationFailure: r.actuationFailure ?? null
|
|
442
|
+
}
|
|
443
|
+
: {})
|
|
429
444
|
};
|
|
430
445
|
});
|
|
431
446
|
}
|
|
@@ -504,7 +519,15 @@ let TwinReferenceResolver = class TwinReferenceResolver {
|
|
|
504
519
|
return {
|
|
505
520
|
source: r.source, system: r.system, siteName: r.siteName, description: r.description,
|
|
506
521
|
adapterType: r.adapterType, connectionConfig: cfg, status: r.status, lastSyncedAt: r.lastSyncedAt, lastError: r.lastError,
|
|
507
|
-
producedInstanceIds
|
|
522
|
+
producedInstanceIds,
|
|
523
|
+
/* 목록과 같은 규칙 — 보낼 주소가 적힌 연결만 검증 상태를 싣는다(ADR-0046 ②). */
|
|
524
|
+
...((0, actuation_verification_js_1.actuationAddressed)(r.connectionConfig)
|
|
525
|
+
? {
|
|
526
|
+
actuationState: r.actuationState ?? 'unverified',
|
|
527
|
+
actuationVerifiedAt: r.actuationVerifiedAt ?? null,
|
|
528
|
+
actuationFailure: r.actuationFailure ?? null
|
|
529
|
+
}
|
|
530
|
+
: {})
|
|
508
531
|
};
|
|
509
532
|
}
|
|
510
533
|
referenceAdapters() {
|
|
@@ -537,8 +560,15 @@ let TwinReferenceResolver = class TwinReferenceResolver {
|
|
|
537
560
|
const taken = (0, connection_portability_js_1.refuseIntakeCollision)(source, connectionConfig, others);
|
|
538
561
|
if (taken)
|
|
539
562
|
return { ok: false, source, error: taken.message, errorCode: taken.code, errorParams: taken.params };
|
|
563
|
+
/*
|
|
564
|
+
* **주소를 바꾸는 저장은 검증을 되돌린다** (ADR-0046 ②). 지난 왕복은 지난 주소의 이야기라 새
|
|
565
|
+
* 주소에 대해서는 아무 말도 하지 않는다. 화면이 그 초록을 그대로 두면 다른 공장을 가리키는 연결이
|
|
566
|
+
* 「확인됨」으로 보인다. 되돌린 상태는 실패가 아니다 — 아직 안 해 본 것이다.
|
|
567
|
+
*/
|
|
568
|
+
const reset = (0, actuation_verification_js_1.actuationResetPatch)(existing?.connectionConfig, connectionConfig);
|
|
540
569
|
const saved = await repo.save(repo.create({
|
|
541
570
|
...(existing ?? {}),
|
|
571
|
+
...(reset ?? {}),
|
|
542
572
|
domain: { id: domainId },
|
|
543
573
|
source,
|
|
544
574
|
system: patch.system ?? existing?.system,
|