@things-factory/headless-twin 10.0.7 → 10.0.9
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/engine/attention-digest.d.ts +52 -0
- package/dist-server/engine/attention-digest.js +76 -0
- package/dist-server/engine/attention-digest.js.map +1 -0
- package/dist-server/engine/board-vocabulary.d.ts +17 -0
- package/dist-server/engine/board-vocabulary.js +63 -0
- package/dist-server/engine/board-vocabulary.js.map +1 -0
- package/dist-server/engine/command-routing.d.ts +33 -0
- package/dist-server/engine/command-routing.js +53 -0
- package/dist-server/engine/command-routing.js.map +1 -0
- package/dist-server/engine/entity-delta.d.ts +13 -6
- package/dist-server/engine/entity-delta.js +38 -12
- package/dist-server/engine/entity-delta.js.map +1 -1
- package/dist-server/engine/index.d.ts +6 -0
- package/dist-server/engine/index.js +10 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/kpi-baseline.d.ts +78 -0
- package/dist-server/engine/kpi-baseline.js +123 -0
- package/dist-server/engine/kpi-baseline.js.map +1 -0
- package/dist-server/engine/kpi-broadcast.d.ts +4 -0
- package/dist-server/engine/kpi-broadcast.js +16 -0
- package/dist-server/engine/kpi-broadcast.js.map +1 -0
- package/dist-server/engine/kpi-query.d.ts +31 -0
- package/dist-server/engine/kpi-query.js +50 -2
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/live-attentions.d.ts +1 -0
- package/dist-server/engine/live-attentions.js +7 -1
- package/dist-server/engine/live-attentions.js.map +1 -1
- package/dist-server/engine/runtime-key.d.ts +15 -0
- package/dist-server/engine/runtime-key.js +64 -0
- package/dist-server/engine/runtime-key.js.map +1 -0
- package/dist-server/engine/state-axes.d.ts +16 -0
- package/dist-server/engine/state-axes.js +54 -0
- package/dist-server/engine/state-axes.js.map +1 -0
- package/dist-server/engine/twin-engine.d.ts +92 -16
- package/dist-server/engine/twin-engine.js +335 -77
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/engine/twin-level.d.ts +23 -0
- package/dist-server/engine/twin-level.js +52 -0
- package/dist-server/engine/twin-level.js.map +1 -0
- package/dist-server/engine/warm-start.d.ts +58 -12
- package/dist-server/engine/warm-start.js +80 -9
- package/dist-server/engine/warm-start.js.map +1 -1
- package/dist-server/service/reference/discovery-result.d.ts +34 -0
- package/dist-server/service/reference/discovery-result.js +84 -0
- package/dist-server/service/reference/discovery-result.js.map +1 -0
- package/dist-server/service/reference/ingest-space.d.ts +30 -0
- package/dist-server/service/reference/ingest-space.js +63 -0
- package/dist-server/service/reference/ingest-space.js.map +1 -0
- package/dist-server/service/reference/knob-defaults.d.ts +20 -0
- package/dist-server/service/reference/knob-defaults.js +59 -0
- package/dist-server/service/reference/knob-defaults.js.map +1 -0
- package/dist-server/service/reference/reference-live.js +2 -2
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +37 -2
- package/dist-server/service/reference/reference-master.js +55 -5
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.d.ts +2 -2
- package/dist-server/service/reference/reference-resolver.js +71 -15
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/twin-attention/twin-attention-query.d.ts +8 -1
- package/dist-server/service/twin-attention/twin-attention-query.js +39 -8
- package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
- package/dist-server/service/twin-control/twin-control-mutation.d.ts +2 -0
- package/dist-server/service/twin-control/twin-control-mutation.js +29 -10
- package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js +2 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
- package/dist-server/service/twin-instance/twin-instance.js +4 -2
- package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
- package/dist-server/service/twin-journal/twin-journal-query.d.ts +6 -2
- package/dist-server/service/twin-journal/twin-journal-query.js +25 -7
- package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +8 -0
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +23 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
- package/dist-server/service/twin-metrics/twin-metrics-query.js +1 -1
- package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
- package/dist-server/service/twin-space/twin-space-resolver.js +9 -0
- package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
- package/dist-server/service/twin-state/twin-state-subscription.js +1 -1
- package/dist-server/service/twin-state/twin-state-subscription.js.map +1 -1
- package/dist-server/service/twin-structure/twin-structure.js +2 -1
- package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
- package/dist-server/service/twin-target/twin-target-resolver.js +21 -4
- package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/server/engine/attention-digest.ts +102 -0
- package/server/engine/board-vocabulary.ts +61 -0
- package/server/engine/command-routing.ts +67 -0
- package/server/engine/entity-delta.ts +33 -11
- package/server/engine/index.ts +10 -0
- package/server/engine/kpi-baseline.ts +202 -0
- package/server/engine/kpi-broadcast.ts +13 -0
- package/server/engine/kpi-query.ts +81 -3
- package/server/engine/live-attentions.ts +7 -2
- package/server/engine/runtime-key.ts +58 -0
- package/server/engine/state-axes.ts +55 -0
- package/server/engine/twin-engine.ts +350 -77
- package/server/engine/twin-level.ts +48 -0
- package/server/engine/warm-start.ts +130 -16
- package/server/service/reference/discovery-result.ts +95 -0
- package/server/service/reference/ingest-space.ts +70 -0
- package/server/service/reference/knob-defaults.ts +59 -0
- package/server/service/reference/reference-live.ts +2 -2
- package/server/service/reference/reference-master.ts +94 -8
- package/server/service/reference/reference-resolver.ts +80 -16
- package/server/service/twin-attention/twin-attention-query.ts +43 -6
- package/server/service/twin-control/twin-control-mutation.ts +31 -12
- package/server/service/twin-forecast/twin-forecast-query.ts +3 -2
- package/server/service/twin-instance/twin-instance.ts +6 -2
- package/server/service/twin-journal/twin-journal-query.ts +35 -5
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +18 -2
- package/server/service/twin-metrics/twin-metrics-query.ts +1 -1
- package/server/service/twin-space/twin-space-resolver.ts +10 -1
- package/server/service/twin-state/twin-state-subscription.ts +1 -1
- package/server/service/twin-structure/twin-structure.ts +4 -1
- package/server/service/twin-target/twin-target-resolver.ts +22 -4
- package/test/attention-digest.test.ts +135 -0
- package/test/board-vocabulary.test.ts +114 -0
- package/test/capability-mapping.test.ts +4 -4
- package/test/command-routing.test.ts +61 -0
- package/test/discovery-result.test.ts +75 -0
- package/test/entity-delta.test.ts +25 -25
- package/test/ingest-bench.test.ts +3 -3
- package/test/ingest-space.test.ts +50 -0
- package/test/knob-defaults.test.ts +72 -0
- package/test/kpi-baseline-db.test.ts +214 -0
- package/test/kpi-baseline.test.ts +196 -0
- package/test/kpi-query-bench.test.ts +128 -0
- package/test/live-mirror-parity.test.ts +35 -2
- package/test/master-to-twin.test.ts +7 -3
- package/test/mutation-gate.test.ts +108 -0
- package/test/oee-accumulator.test.ts +65 -1
- package/test/registry-key-guard.test.ts +80 -0
- package/test/runtime-key.test.ts +66 -0
- package/test/scale-twin-bench.test.ts +2 -2
- package/test/state-axes.test.ts +74 -0
- package/test/streamline-e2e.test.ts +2 -2
- package/test/structure-revision-db.test.ts +5 -4
- package/test/tenant-registry-db.test.ts +149 -0
- package/test/warm-start-seam.test.ts +140 -0
- package/test/warm-start.test.ts +138 -4
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 성과를 접을 수 있는 단계 — 좁은 것부터.
|
|
3
|
+
*
|
|
4
|
+
* `equipment` 는 커널 축 이름으로 `resource` 다(설비·인원·자산을 아우르는 이름). 화면과 AI 는
|
|
5
|
+
* "설비" 로 말하고, 커널에 물을 때만 축 이름으로 바꾼다 — 그 변환이 아래 한 곳에 있다.
|
|
6
|
+
*/
|
|
7
|
+
export declare const TWIN_LEVELS: readonly ["space", "twin", "area", "location", "equipment"];
|
|
8
|
+
export type TwinLevel = (typeof TWIN_LEVELS)[number];
|
|
9
|
+
export declare function isTwinLevel(v: unknown): v is TwinLevel;
|
|
10
|
+
/**
|
|
11
|
+
* 단계 → 커널의 나누는 축(`KpiGroupBy`). **전체를 보는 단계는 축이 없다** — 나누지 않고 통째로 접는다.
|
|
12
|
+
*
|
|
13
|
+
* `space` 와 `twin` 이 축 없이 같은 이유: 둘의 차이는 "무엇을 대상으로 삼는가"(공간 전체 대 트윈 하나)이지
|
|
14
|
+
* "어떻게 나누는가" 가 아니다. 대상은 질의의 `spaceId`/`instanceId` 가 정한다.
|
|
15
|
+
*/
|
|
16
|
+
export declare function kpiGroupByOfLevel(level: TwinLevel): 'area' | 'location' | 'resource' | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* 그 단계의 카드가 **대상 하나마다 한 장**인가.
|
|
19
|
+
*
|
|
20
|
+
* 구역·자리·설비는 여럿이라 카드가 그 수만큼 생긴다. 공간·트윈은 하나뿐이다. 보드를 자동으로 조립할 때
|
|
21
|
+
* 몇 장이 생길지 미리 알아야 한다 — 자리 200개 × 지표 4개면 800장이다.
|
|
22
|
+
*/
|
|
23
|
+
export declare function isPerTargetLevel(level: TwinLevel): boolean;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* 공간 계층의 **단계** — 성과를 어느 단위로 보는가. 순수(import 없음).
|
|
4
|
+
*
|
|
5
|
+
* ── 왜 여기 있나 ───────────────────────────────────────────────────────────
|
|
6
|
+
* 이 목록을 세 곳이 쓴다: 씬 표현이 "나는 어느 단계에 붙는다" 고 선언할 때(클라이언트), 성과 카드가
|
|
7
|
+
* "이 단계로 나눠 달라" 고 물을 때(클라이언트), AI 가 "구역별로 보여 줘" 를 제안할 때(서버).
|
|
8
|
+
* 세 곳이 각자 목록을 적으면 하나가 늘 때 나머지가 조용히 뒤처진다 — 집약 태그에서 이미 겪었다.
|
|
9
|
+
*
|
|
10
|
+
* 그래서 **한 곳**에 두고 셋이 가져다 쓴다. 이 파일은 import 가 없어 클라이언트 번들에 서버 코드를
|
|
11
|
+
* 끌고 들어가지 않는다.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.TWIN_LEVELS = void 0;
|
|
15
|
+
exports.isTwinLevel = isTwinLevel;
|
|
16
|
+
exports.kpiGroupByOfLevel = kpiGroupByOfLevel;
|
|
17
|
+
exports.isPerTargetLevel = isPerTargetLevel;
|
|
18
|
+
/**
|
|
19
|
+
* 성과를 접을 수 있는 단계 — 좁은 것부터.
|
|
20
|
+
*
|
|
21
|
+
* `equipment` 는 커널 축 이름으로 `resource` 다(설비·인원·자산을 아우르는 이름). 화면과 AI 는
|
|
22
|
+
* "설비" 로 말하고, 커널에 물을 때만 축 이름으로 바꾼다 — 그 변환이 아래 한 곳에 있다.
|
|
23
|
+
*/
|
|
24
|
+
exports.TWIN_LEVELS = ['space', 'twin', 'area', 'location', 'equipment'];
|
|
25
|
+
function isTwinLevel(v) {
|
|
26
|
+
return typeof v === 'string' && exports.TWIN_LEVELS.includes(v);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 단계 → 커널의 나누는 축(`KpiGroupBy`). **전체를 보는 단계는 축이 없다** — 나누지 않고 통째로 접는다.
|
|
30
|
+
*
|
|
31
|
+
* `space` 와 `twin` 이 축 없이 같은 이유: 둘의 차이는 "무엇을 대상으로 삼는가"(공간 전체 대 트윈 하나)이지
|
|
32
|
+
* "어떻게 나누는가" 가 아니다. 대상은 질의의 `spaceId`/`instanceId` 가 정한다.
|
|
33
|
+
*/
|
|
34
|
+
function kpiGroupByOfLevel(level) {
|
|
35
|
+
if (level === 'area')
|
|
36
|
+
return 'area';
|
|
37
|
+
if (level === 'location')
|
|
38
|
+
return 'location';
|
|
39
|
+
if (level === 'equipment')
|
|
40
|
+
return 'resource';
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 그 단계의 카드가 **대상 하나마다 한 장**인가.
|
|
45
|
+
*
|
|
46
|
+
* 구역·자리·설비는 여럿이라 카드가 그 수만큼 생긴다. 공간·트윈은 하나뿐이다. 보드를 자동으로 조립할 때
|
|
47
|
+
* 몇 장이 생길지 미리 알아야 한다 — 자리 200개 × 지표 4개면 800장이다.
|
|
48
|
+
*/
|
|
49
|
+
function isPerTargetLevel(level) {
|
|
50
|
+
return kpiGroupByOfLevel(level) !== undefined;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=twin-level.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"twin-level.js","sourceRoot":"","sources":["../../server/engine/twin-level.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;;AAWH,kCAEC;AAQD,8CAMC;AAQD,4CAEC;AAnCD;;;;;GAKG;AACU,QAAA,WAAW,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,WAAW,CAAU,CAAA;AAGtF,SAAgB,WAAW,CAAC,CAAU;IACpC,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAK,mBAAiC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAA;AAChF,CAAC;AAED;;;;;GAKG;AACH,SAAgB,iBAAiB,CAAC,KAAgB;IAChD,IAAI,KAAK,KAAK,MAAM;QAAE,OAAO,MAAM,CAAA;IACnC,IAAI,KAAK,KAAK,UAAU;QAAE,OAAO,UAAU,CAAA;IAC3C,IAAI,KAAK,KAAK,WAAW;QAAE,OAAO,UAAU,CAAA;IAE5C,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;;;;;GAKG;AACH,SAAgB,gBAAgB,CAAC,KAAgB;IAC/C,OAAO,iBAAiB,CAAC,KAAK,CAAC,KAAK,SAAS,CAAA;AAC/C,CAAC","sourcesContent":["/*\n * 공간 계층의 **단계** — 성과를 어느 단위로 보는가. 순수(import 없음).\n *\n * ── 왜 여기 있나 ───────────────────────────────────────────────────────────\n * 이 목록을 세 곳이 쓴다: 씬 표현이 \"나는 어느 단계에 붙는다\" 고 선언할 때(클라이언트), 성과 카드가\n * \"이 단계로 나눠 달라\" 고 물을 때(클라이언트), AI 가 \"구역별로 보여 줘\" 를 제안할 때(서버).\n * 세 곳이 각자 목록을 적으면 하나가 늘 때 나머지가 조용히 뒤처진다 — 집약 태그에서 이미 겪었다.\n *\n * 그래서 **한 곳**에 두고 셋이 가져다 쓴다. 이 파일은 import 가 없어 클라이언트 번들에 서버 코드를\n * 끌고 들어가지 않는다.\n */\n\n/**\n * 성과를 접을 수 있는 단계 — 좁은 것부터.\n *\n * `equipment` 는 커널 축 이름으로 `resource` 다(설비·인원·자산을 아우르는 이름). 화면과 AI 는\n * \"설비\" 로 말하고, 커널에 물을 때만 축 이름으로 바꾼다 — 그 변환이 아래 한 곳에 있다.\n */\nexport const TWIN_LEVELS = ['space', 'twin', 'area', 'location', 'equipment'] as const\nexport type TwinLevel = (typeof TWIN_LEVELS)[number]\n\nexport function isTwinLevel(v: unknown): v is TwinLevel {\n return typeof v === 'string' && (TWIN_LEVELS as readonly string[]).includes(v)\n}\n\n/**\n * 단계 → 커널의 나누는 축(`KpiGroupBy`). **전체를 보는 단계는 축이 없다** — 나누지 않고 통째로 접는다.\n *\n * `space` 와 `twin` 이 축 없이 같은 이유: 둘의 차이는 \"무엇을 대상으로 삼는가\"(공간 전체 대 트윈 하나)이지\n * \"어떻게 나누는가\" 가 아니다. 대상은 질의의 `spaceId`/`instanceId` 가 정한다.\n */\nexport function kpiGroupByOfLevel(level: TwinLevel): 'area' | 'location' | 'resource' | undefined {\n if (level === 'area') return 'area'\n if (level === 'location') return 'location'\n if (level === 'equipment') return 'resource'\n\n return undefined\n}\n\n/**\n * 그 단계의 카드가 **대상 하나마다 한 장**인가.\n *\n * 구역·자리·설비는 여럿이라 카드가 그 수만큼 생긴다. 공간·트윈은 하나뿐이다. 보드를 자동으로 조립할 때\n * 몇 장이 생길지 미리 알아야 한다 — 자리 200개 × 지표 4개면 800장이다.\n */\nexport function isPerTargetLevel(level: TwinLevel): boolean {\n return kpiGroupByOfLevel(level) !== undefined\n}\n"]}
|
|
@@ -1,15 +1,42 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* 커널에 심을 관측 상태 — 구조가 아니라 "무엇이 어디에 얼마나".
|
|
3
|
+
*
|
|
4
|
+
* 커널 `hydrateObserved` 가 받는 스냅샷 모양을 그대로 따른다. 여기서 골라 담지 않는 이유: 무엇을
|
|
5
|
+
* 심을 수 있는지는 커널이 알고, 그 규칙(고아 작업 제외·이행 완료 오더 제외 등)도 커널에 있다.
|
|
6
|
+
* 호스트가 미리 골라내면 규칙이 두 곳으로 갈린다.
|
|
7
|
+
*/
|
|
2
8
|
export interface ObservedSeed {
|
|
3
9
|
locations: unknown[];
|
|
4
10
|
items: unknown[];
|
|
5
11
|
equipment: unknown[];
|
|
12
|
+
persons?: unknown[];
|
|
13
|
+
assets?: unknown[];
|
|
14
|
+
tasks?: unknown[];
|
|
15
|
+
orders?: unknown[];
|
|
16
|
+
/** 확인해 둔 주목 신호 id — 상태에서 파생되지 않는 유일한 축이라 반드시 함께 넘긴다. */
|
|
17
|
+
acked?: string[];
|
|
6
18
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
seed: ObservedSeed;
|
|
19
|
+
/** 심은 것의 내역 — 로그와 테스트가 같은 숫자를 본다. */
|
|
20
|
+
export interface WarmStartCounts {
|
|
10
21
|
itemCount: number;
|
|
11
22
|
equipmentCount: number;
|
|
23
|
+
personCount: number;
|
|
24
|
+
assetCount: number;
|
|
25
|
+
taskCount: number;
|
|
26
|
+
/** 원값(requested)이 있어 커널이 실제로 심을 수 있는 오더. */
|
|
27
|
+
orderCount: number;
|
|
28
|
+
/** 이어받은 확인 처리 수 — 잃으면 확인해 둔 신호가 다시 빨개진다. */
|
|
29
|
+
ackedCount: number;
|
|
30
|
+
/**
|
|
31
|
+
* 원값이 없어 심지 못하는 오더 — **0 이 아니면 반드시 말한다.**
|
|
32
|
+
* progress 만 있는 오더는 남은 수량을 알 수 없어 재계획의 근거가 되지 못한다. 지어내지 않고 뺀다.
|
|
33
|
+
*/
|
|
34
|
+
ordersWithoutDemand: number;
|
|
12
35
|
}
|
|
36
|
+
export type WarmStartPlan = ({
|
|
37
|
+
action: 'hydrate';
|
|
38
|
+
seed: ObservedSeed;
|
|
39
|
+
} & WarmStartCounts)
|
|
13
40
|
/** 벤치 트윈 — 새 시작에서 용량을 재는 게 목적이라 현재 상태를 심으면 측정이 오염된다. */
|
|
14
41
|
| {
|
|
15
42
|
action: 'skip';
|
|
@@ -25,15 +52,34 @@ export type WarmStartPlan = {
|
|
|
25
52
|
action: 'skip';
|
|
26
53
|
reason: 'unsupported';
|
|
27
54
|
};
|
|
55
|
+
/** 스냅샷이 들고 올 수 있는 관측 축 — 어느 것이든 있으면 심을 값어치가 있다. */
|
|
56
|
+
interface ObservedState {
|
|
57
|
+
locations?: unknown[];
|
|
58
|
+
items?: unknown[];
|
|
59
|
+
equipment?: unknown[];
|
|
60
|
+
persons?: unknown[];
|
|
61
|
+
assets?: unknown[];
|
|
62
|
+
tasks?: unknown[];
|
|
63
|
+
orders?: unknown[];
|
|
64
|
+
acked?: string[];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 봉투를 벗긴다 — **체크포인트가 겹포장돼 복원이 조용히 죽던 자리.**
|
|
68
|
+
*
|
|
69
|
+
* `TwinEngine.snapshot()` 은 라이브에서는 상태를, 시뮬에서는 `runtime.resync()` 봉투(`{revision,state}`)를
|
|
70
|
+
* 돌려준다. 그 봉투를 다시 `{revision, state}` 로 감싸 캐시에 넣었으므로, 꺼낸 `state` 는 축이 하나도
|
|
71
|
+
* 없는 껍데기였다 → `planWarmStart` 가 "심을 것이 없다" 로 판정하고 **조용히** 넘어갔다. 저널에 수천 건이
|
|
72
|
+
* 있고 캐시도 최신인데 트윈이 빈 채로 떴다(오류도 로그도 없다).
|
|
73
|
+
*
|
|
74
|
+
* 반환 모양을 통일하는 것이 뿌리 수정이지만, 그 값은 State 구독 페이로드로도 나가므로 클라이언트 계약을
|
|
75
|
+
* 건드린다. 그래서 **복원 경로에서만** 벗긴다. 겹겹이 싸여 있어도 벗긴다(과거에 저장된 값이 남아 있다).
|
|
76
|
+
*/
|
|
77
|
+
export declare function unwrapState(x: any, depth?: number): any;
|
|
28
78
|
/**
|
|
29
79
|
* 무엇을 할지 정한다.
|
|
30
80
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* 넘기지 않는다 — 재고·노드·무버만 복원되고 진행 중 오더는 비어서 시작하는 편이 정직하다.
|
|
81
|
+
* 오더·작업·사람·자산도 함께 심는다. 골라내는 규칙은 커널이 들고 있고(이행 완료 오더, 고아 작업),
|
|
82
|
+
* 여기서는 **심을 수 없는 오더의 수만 센다** — 호출부가 그 사실을 말할 수 있어야 하기 때문이다.
|
|
34
83
|
*/
|
|
35
|
-
export declare function planWarmStart(state:
|
|
36
|
-
|
|
37
|
-
items?: unknown[];
|
|
38
|
-
equipment?: unknown[];
|
|
39
|
-
} | null | undefined, purpose: string | undefined, canHydrate: boolean): WarmStartPlan;
|
|
84
|
+
export declare function planWarmStart(state: ObservedState | null | undefined, purpose: string | undefined, canHydrate: boolean): WarmStartPlan;
|
|
85
|
+
export {};
|
|
@@ -7,15 +7,54 @@
|
|
|
7
7
|
* 이 판정이 틀리면 증상이 정반대 두 방향으로 나온다: 심어야 할 때 안 심으면 **있는 재고가 0 으로**
|
|
8
8
|
* 보이고(2026-07-31 hatiolab-wms: 저널에 입고 540·출고 94 인데 재고 화면이 비어 있었다), 심지
|
|
9
9
|
* 말아야 할 벤치에 심으면 **용량 측정이 오염된다.** 둘 다 조용히 틀리는 종류라 규칙을 고정한다.
|
|
10
|
+
*
|
|
11
|
+
* ── 오더를 버리던 전제가 낡았다 (2026-08-05) ────────────────────────────────
|
|
12
|
+
* 예전 주석은 "스냅샷의 오더에는 progress 밖에 없어서 역산하면 없는 숫자를 지어내는 셈이므로
|
|
13
|
+
* 넘기지 않는다" 였다. 그때는 맞았다. 그러나 커널 `OrderState` 가 **원값을 되찾은 뒤**
|
|
14
|
+
* (`requested`·`fulfilled`·`lines`) 이 판정만 그대로 남아, 재기동하면 재고는 맞는데 **진행 중
|
|
15
|
+
* 주문이 통째로 사라진** 화면이 나왔다. 오류는 나지 않는다 — 그래서 오래 남아 있었다.
|
|
16
|
+
*
|
|
17
|
+
* 두 스냅샷 생산 경로가 모두 원값을 담는다: 시뮬은 커널 `snapshot()`, 라이브(미러)는
|
|
18
|
+
* `StateProjector` 다. 사람·자산·작업도 같은 스냅샷에 있고 커널이 심을 수 있는데 넘기지 않고 있었다.
|
|
19
|
+
* 그래서 씨앗을 **관측 스냅샷 전체**로 넓힌다.
|
|
20
|
+
*
|
|
21
|
+
* 다만 넓히면서 **조용한 누락을 만들지 않는다.** 원값이 없는 오더는 커널이 걸러 내므로(지어내지
|
|
22
|
+
* 않는다), 그 수를 여기서 세어 호출부가 말할 수 있게 한다. 세지 않으면 "주문이 없다" 와 "주문을
|
|
23
|
+
* 못 심었다" 가 화면에서 같아진다.
|
|
10
24
|
*/
|
|
11
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.unwrapState = unwrapState;
|
|
12
27
|
exports.planWarmStart = planWarmStart;
|
|
28
|
+
/** 관측 상태가 실려 있는지 — 축 하나라도 있으면 상태다(봉투에는 없다). */
|
|
29
|
+
const looksLikeState = (x) => !!x && typeof x === 'object' && ['locations', 'items', 'equipment', 'orders', 'tasks', 'persons', 'assets', 'acked'].some(k => k in x);
|
|
30
|
+
/**
|
|
31
|
+
* 봉투를 벗긴다 — **체크포인트가 겹포장돼 복원이 조용히 죽던 자리.**
|
|
32
|
+
*
|
|
33
|
+
* `TwinEngine.snapshot()` 은 라이브에서는 상태를, 시뮬에서는 `runtime.resync()` 봉투(`{revision,state}`)를
|
|
34
|
+
* 돌려준다. 그 봉투를 다시 `{revision, state}` 로 감싸 캐시에 넣었으므로, 꺼낸 `state` 는 축이 하나도
|
|
35
|
+
* 없는 껍데기였다 → `planWarmStart` 가 "심을 것이 없다" 로 판정하고 **조용히** 넘어갔다. 저널에 수천 건이
|
|
36
|
+
* 있고 캐시도 최신인데 트윈이 빈 채로 떴다(오류도 로그도 없다).
|
|
37
|
+
*
|
|
38
|
+
* 반환 모양을 통일하는 것이 뿌리 수정이지만, 그 값은 State 구독 페이로드로도 나가므로 클라이언트 계약을
|
|
39
|
+
* 건드린다. 그래서 **복원 경로에서만** 벗긴다. 겹겹이 싸여 있어도 벗긴다(과거에 저장된 값이 남아 있다).
|
|
40
|
+
*/
|
|
41
|
+
function unwrapState(x, depth = 4) {
|
|
42
|
+
if (!x || typeof x !== 'object')
|
|
43
|
+
return x;
|
|
44
|
+
if (looksLikeState(x))
|
|
45
|
+
return x;
|
|
46
|
+
if (depth > 0 && x.state && typeof x.state === 'object')
|
|
47
|
+
return unwrapState(x.state, depth - 1);
|
|
48
|
+
return x;
|
|
49
|
+
}
|
|
50
|
+
const arr = (v) => (Array.isArray(v) ? v : []);
|
|
51
|
+
/** 원값이 있는 오더인가 — 커널 `hydrateObserved` 의 판정(`requested !== undefined`)과 같은 기준. */
|
|
52
|
+
const hasDemand = (o) => !!o && typeof o === 'object' && o.requested !== undefined;
|
|
13
53
|
/**
|
|
14
54
|
* 무엇을 할지 정한다.
|
|
15
55
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* 넘기지 않는다 — 재고·노드·무버만 복원되고 진행 중 오더는 비어서 시작하는 편이 정직하다.
|
|
56
|
+
* 오더·작업·사람·자산도 함께 심는다. 골라내는 규칙은 커널이 들고 있고(이행 완료 오더, 고아 작업),
|
|
57
|
+
* 여기서는 **심을 수 없는 오더의 수만 센다** — 호출부가 그 사실을 말할 수 있어야 하기 때문이다.
|
|
19
58
|
*/
|
|
20
59
|
function planWarmStart(state, purpose, canHydrate) {
|
|
21
60
|
/* 벤치 판정이 먼저다 — 상태가 있든 없든 벤치에는 심지 않는다는 사실이 바뀌지 않는다. */
|
|
@@ -23,15 +62,47 @@ function planWarmStart(state, purpose, canHydrate) {
|
|
|
23
62
|
return { action: 'skip', reason: 'bench' };
|
|
24
63
|
if (!state)
|
|
25
64
|
return { action: 'skip', reason: 'no-state' };
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
65
|
+
/* 봉투가 들어오면 스스로 벗는다 — 호출부가 한 곳이라도 잊으면 그 경로가 조용히 죽는다(실제로 그랬다). */
|
|
66
|
+
state = unwrapState(state);
|
|
67
|
+
const locations = arr(state.locations);
|
|
68
|
+
const items = arr(state.items);
|
|
69
|
+
const equipment = arr(state.equipment);
|
|
70
|
+
const persons = arr(state.persons);
|
|
71
|
+
const assets = arr(state.assets);
|
|
72
|
+
const tasks = arr(state.tasks);
|
|
73
|
+
const orders = arr(state.orders);
|
|
74
|
+
/* 확인 처리는 문자열 id 목록이다 — 배열이 아니면 없는 것으로 본다(지어내지 않는다). */
|
|
75
|
+
const acked = Array.isArray(state.acked) ? state.acked.filter((x) => typeof x === 'string') : [];
|
|
76
|
+
/* 어느 축도 없으면 심을 것이 없다 — 빈 주입으로 로그만 남기지 않는다. */
|
|
77
|
+
if (!locations.length && !items.length && !equipment.length && !persons.length && !assets.length && !tasks.length && !orders.length && !acked.length) {
|
|
31
78
|
return { action: 'skip', reason: 'no-state' };
|
|
79
|
+
}
|
|
32
80
|
/* 지원 여부는 마지막에 본다 — 심을 게 있는데 못 심는 상황이라야 경고할 값어치가 있다. */
|
|
33
81
|
if (!canHydrate)
|
|
34
82
|
return { action: 'skip', reason: 'unsupported' };
|
|
35
|
-
|
|
83
|
+
const seedable = orders.filter(hasDemand);
|
|
84
|
+
return {
|
|
85
|
+
action: 'hydrate',
|
|
86
|
+
seed: {
|
|
87
|
+
locations,
|
|
88
|
+
items,
|
|
89
|
+
equipment,
|
|
90
|
+
...(persons.length ? { persons } : {}),
|
|
91
|
+
...(assets.length ? { assets } : {}),
|
|
92
|
+
...(tasks.length ? { tasks } : {}),
|
|
93
|
+
/* 원값 없는 오더는 넘기지 않는다 — 커널도 걸러 내지만, 넘기지 않으면 세어 둔 수와 어긋날 일이 없다. */
|
|
94
|
+
...(seedable.length ? { orders: seedable } : {}),
|
|
95
|
+
/* 확인 처리는 조건이 사라진 id 가 섞여 있어도 해롭지 않다 — 그 신호가 없으면 표시할 대상이 없다. */
|
|
96
|
+
...(acked.length ? { acked } : {})
|
|
97
|
+
},
|
|
98
|
+
itemCount: items.length,
|
|
99
|
+
equipmentCount: equipment.length,
|
|
100
|
+
personCount: persons.length,
|
|
101
|
+
assetCount: assets.length,
|
|
102
|
+
taskCount: tasks.length,
|
|
103
|
+
orderCount: seedable.length,
|
|
104
|
+
ordersWithoutDemand: orders.length - seedable.length,
|
|
105
|
+
ackedCount: acked.length
|
|
106
|
+
};
|
|
36
107
|
}
|
|
37
108
|
//# sourceMappingURL=warm-start.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"warm-start.js","sourceRoot":"","sources":["../../server/engine/warm-start.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"warm-start.js","sourceRoot":"","sources":["../../server/engine/warm-start.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 웜스타트 판정 — **순수**. \"기동하는 커널에 직전 상태를 심을 것인가, 심는다면 무엇을\" 만 정한다.\n * 실제 주입(hydrateObserved 호출)과 로그는 엔진이 한다.\n *\n * ── 왜 떼어냈나 ─────────────────────────────────────────────────────────────\n * 이 판정이 틀리면 증상이 정반대 두 방향으로 나온다: 심어야 할 때 안 심으면 **있는 재고가 0 으로**\n * 보이고(2026-07-31 hatiolab-wms: 저널에 입고 540·출고 94 인데 재고 화면이 비어 있었다), 심지\n * 말아야 할 벤치에 심으면 **용량 측정이 오염된다.** 둘 다 조용히 틀리는 종류라 규칙을 고정한다.\n *\n * ── 오더를 버리던 전제가 낡았다 (2026-08-05) ────────────────────────────────\n * 예전 주석은 \"스냅샷의 오더에는 progress 밖에 없어서 역산하면 없는 숫자를 지어내는 셈이므로\n * 넘기지 않는다\" 였다. 그때는 맞았다. 그러나 커널 `OrderState` 가 **원값을 되찾은 뒤**\n * (`requested`·`fulfilled`·`lines`) 이 판정만 그대로 남아, 재기동하면 재고는 맞는데 **진행 중\n * 주문이 통째로 사라진** 화면이 나왔다. 오류는 나지 않는다 — 그래서 오래 남아 있었다.\n *\n * 두 스냅샷 생산 경로가 모두 원값을 담는다: 시뮬은 커널 `snapshot()`, 라이브(미러)는\n * `StateProjector` 다. 사람·자산·작업도 같은 스냅샷에 있고 커널이 심을 수 있는데 넘기지 않고 있었다.\n * 그래서 씨앗을 **관측 스냅샷 전체**로 넓힌다.\n *\n * 다만 넓히면서 **조용한 누락을 만들지 않는다.** 원값이 없는 오더는 커널이 걸러 내므로(지어내지\n * 않는다), 그 수를 여기서 세어 호출부가 말할 수 있게 한다. 세지 않으면 \"주문이 없다\" 와 \"주문을\n * 못 심었다\" 가 화면에서 같아진다.\n */\n\n/**\n * 커널에 심을 관측 상태 — 구조가 아니라 \"무엇이 어디에 얼마나\".\n *\n * 커널 `hydrateObserved` 가 받는 스냅샷 모양을 그대로 따른다. 여기서 골라 담지 않는 이유: 무엇을\n * 심을 수 있는지는 커널이 알고, 그 규칙(고아 작업 제외·이행 완료 오더 제외 등)도 커널에 있다.\n * 호스트가 미리 골라내면 규칙이 두 곳으로 갈린다.\n */\nexport interface ObservedSeed {\n locations: unknown[]\n items: unknown[]\n equipment: unknown[]\n persons?: unknown[]\n assets?: unknown[]\n tasks?: unknown[]\n orders?: unknown[]\n /** 확인해 둔 주목 신호 id — 상태에서 파생되지 않는 유일한 축이라 반드시 함께 넘긴다. */\n acked?: string[]\n}\n\n/** 심은 것의 내역 — 로그와 테스트가 같은 숫자를 본다. */\nexport interface WarmStartCounts {\n itemCount: number\n equipmentCount: number\n personCount: number\n assetCount: number\n taskCount: number\n /** 원값(requested)이 있어 커널이 실제로 심을 수 있는 오더. */\n orderCount: number\n /** 이어받은 확인 처리 수 — 잃으면 확인해 둔 신호가 다시 빨개진다. */\n ackedCount: number\n /**\n * 원값이 없어 심지 못하는 오더 — **0 이 아니면 반드시 말한다.**\n * progress 만 있는 오더는 남은 수량을 알 수 없어 재계획의 근거가 되지 못한다. 지어내지 않고 뺀다.\n */\n ordersWithoutDemand: number\n}\n\nexport type WarmStartPlan =\n | ({ action: 'hydrate'; seed: ObservedSeed } & WarmStartCounts)\n /** 벤치 트윈 — 새 시작에서 용량을 재는 게 목적이라 현재 상태를 심으면 측정이 오염된다. */\n | { action: 'skip'; reason: 'bench' }\n /** 심을 상태가 없다 — 처음 만든 트윈이거나 저널·체크포인트가 비었다. 정상이다. */\n | { action: 'skip'; reason: 'no-state' }\n /** 커널이 관측 주입을 지원하지 않는다 — 구조만으로 시작하므로 보유량은 0 으로 읽힌다(알려야 한다). */\n | { action: 'skip'; reason: 'unsupported' }\n\n/** 스냅샷이 들고 올 수 있는 관측 축 — 어느 것이든 있으면 심을 값어치가 있다. */\ninterface ObservedState {\n locations?: unknown[]\n items?: unknown[]\n equipment?: unknown[]\n persons?: unknown[]\n assets?: unknown[]\n tasks?: unknown[]\n orders?: unknown[]\n acked?: string[]\n}\n\n/** 관측 상태가 실려 있는지 — 축 하나라도 있으면 상태다(봉투에는 없다). */\nconst looksLikeState = (x: any): boolean =>\n !!x && typeof x === 'object' && ['locations', 'items', 'equipment', 'orders', 'tasks', 'persons', 'assets', 'acked'].some(k => k in x)\n\n/**\n * 봉투를 벗긴다 — **체크포인트가 겹포장돼 복원이 조용히 죽던 자리.**\n *\n * `TwinEngine.snapshot()` 은 라이브에서는 상태를, 시뮬에서는 `runtime.resync()` 봉투(`{revision,state}`)를\n * 돌려준다. 그 봉투를 다시 `{revision, state}` 로 감싸 캐시에 넣었으므로, 꺼낸 `state` 는 축이 하나도\n * 없는 껍데기였다 → `planWarmStart` 가 \"심을 것이 없다\" 로 판정하고 **조용히** 넘어갔다. 저널에 수천 건이\n * 있고 캐시도 최신인데 트윈이 빈 채로 떴다(오류도 로그도 없다).\n *\n * 반환 모양을 통일하는 것이 뿌리 수정이지만, 그 값은 State 구독 페이로드로도 나가므로 클라이언트 계약을\n * 건드린다. 그래서 **복원 경로에서만** 벗긴다. 겹겹이 싸여 있어도 벗긴다(과거에 저장된 값이 남아 있다).\n */\nexport function unwrapState(x: any, depth = 4): any {\n if (!x || typeof x !== 'object') return x\n if (looksLikeState(x)) return x\n if (depth > 0 && x.state && typeof x.state === 'object') return unwrapState(x.state, depth - 1)\n return x\n}\n\nconst arr = (v: unknown[] | undefined): unknown[] => (Array.isArray(v) ? v : [])\n\n/** 원값이 있는 오더인가 — 커널 `hydrateObserved` 의 판정(`requested !== undefined`)과 같은 기준. */\nconst hasDemand = (o: unknown): boolean =>\n !!o && typeof o === 'object' && (o as { requested?: unknown }).requested !== undefined\n\n/**\n * 무엇을 할지 정한다.\n *\n * 오더·작업·사람·자산도 함께 심는다. 골라내는 규칙은 커널이 들고 있고(이행 완료 오더, 고아 작업),\n * 여기서는 **심을 수 없는 오더의 수만 센다** — 호출부가 그 사실을 말할 수 있어야 하기 때문이다.\n */\nexport function planWarmStart(state: ObservedState | null | undefined, purpose: string | undefined, canHydrate: boolean): WarmStartPlan {\n /* 벤치 판정이 먼저다 — 상태가 있든 없든 벤치에는 심지 않는다는 사실이 바뀌지 않는다. */\n if (purpose === 'bench') return { action: 'skip', reason: 'bench' }\n if (!state) return { action: 'skip', reason: 'no-state' }\n /* 봉투가 들어오면 스스로 벗는다 — 호출부가 한 곳이라도 잊으면 그 경로가 조용히 죽는다(실제로 그랬다). */\n state = unwrapState(state)\n\n const locations = arr(state.locations)\n const items = arr(state.items)\n const equipment = arr(state.equipment)\n const persons = arr(state.persons)\n const assets = arr(state.assets)\n const tasks = arr(state.tasks)\n const orders = arr(state.orders)\n /* 확인 처리는 문자열 id 목록이다 — 배열이 아니면 없는 것으로 본다(지어내지 않는다). */\n const acked = Array.isArray(state.acked) ? state.acked.filter((x): x is string => typeof x === 'string') : []\n\n /* 어느 축도 없으면 심을 것이 없다 — 빈 주입으로 로그만 남기지 않는다. */\n if (!locations.length && !items.length && !equipment.length && !persons.length && !assets.length && !tasks.length && !orders.length && !acked.length) {\n return { action: 'skip', reason: 'no-state' }\n }\n\n /* 지원 여부는 마지막에 본다 — 심을 게 있는데 못 심는 상황이라야 경고할 값어치가 있다. */\n if (!canHydrate) return { action: 'skip', reason: 'unsupported' }\n\n const seedable = orders.filter(hasDemand)\n\n return {\n action: 'hydrate',\n seed: {\n locations,\n items,\n equipment,\n ...(persons.length ? { persons } : {}),\n ...(assets.length ? { assets } : {}),\n ...(tasks.length ? { tasks } : {}),\n /* 원값 없는 오더는 넘기지 않는다 — 커널도 걸러 내지만, 넘기지 않으면 세어 둔 수와 어긋날 일이 없다. */\n ...(seedable.length ? { orders: seedable } : {}),\n /* 확인 처리는 조건이 사라진 id 가 섞여 있어도 해롭지 않다 — 그 신호가 없으면 표시할 대상이 없다. */\n ...(acked.length ? { acked } : {})\n },\n itemCount: items.length,\n equipmentCount: equipment.length,\n personCount: persons.length,\n assetCount: assets.length,\n taskCount: tasks.length,\n orderCount: seedable.length,\n ordersWithoutDemand: orders.length - seedable.length,\n ackedCount: acked.length\n }\n}\n"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** 발견이 왜 아무것도 주지 못했나 — 언어 중립 코드. */
|
|
2
|
+
export type DiscoveryErrorCode = 'reference-not-found' | 'adapter-unknown' | 'discovery-failed' | 'discovery-timeout';
|
|
3
|
+
export interface DiscoveryError {
|
|
4
|
+
code: DiscoveryErrorCode;
|
|
5
|
+
params?: Record<string, string | number>;
|
|
6
|
+
/** 영어 canonical — 로그에 남고, 번역이 없으면 그대로 보인다. */
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
export interface DiscoveryResult<S = unknown> {
|
|
10
|
+
/** 발견된 사이트들. 실패했으면 빈 배열이고 `error` 가 이유를 말한다. */
|
|
11
|
+
sites: S[];
|
|
12
|
+
/** 실패 사유. **없으면 성공이다** — 빈 배열 + 사유 없음 = "연결은 됐지만 사이트가 없다". */
|
|
13
|
+
error?: DiscoveryError;
|
|
14
|
+
}
|
|
15
|
+
/** 등록된 레퍼런스가 없다 — 목록이 낡았거나 다른 테넌트의 것이다. */
|
|
16
|
+
export declare const referenceNotFound: (source: string) => DiscoveryError;
|
|
17
|
+
/** 어댑터 종류를 모른다 — 배포에 그 커넥터가 없다(패키지 미설치·오타). */
|
|
18
|
+
export declare const adapterUnknown: (adapterType: string, source: string) => DiscoveryError;
|
|
19
|
+
/** 연결·조회가 실패했다 — 원 시스템이 준 사유를 그대로 싣는다. */
|
|
20
|
+
export declare const discoveryFailed: (source: string, detail: string) => DiscoveryError;
|
|
21
|
+
/** 기다려도 답이 없었다 — 무한히 도는 화면보다 사유가 낫다. */
|
|
22
|
+
export declare const discoveryTimedOut: (source: string, seconds: number) => DiscoveryError;
|
|
23
|
+
/**
|
|
24
|
+
* 어댑터가 준 것을 결과로 접는다.
|
|
25
|
+
*
|
|
26
|
+
* **사이트가 없는 것을 실패로 만들지 않는다** — 연결이 되고 조회가 성공했는데 공장이 하나도 없는
|
|
27
|
+
* 시스템은 있을 수 있다(권한이 좁거나 아직 등록되지 않은 경우). 그것은 사실이고, 화면은 "없다" 고
|
|
28
|
+
* 말해야 한다. 실패로 뭉개면 사용자가 있지도 않은 문제를 찾는다.
|
|
29
|
+
*/
|
|
30
|
+
export declare function discovered<S>(sites: readonly S[] | undefined | null): DiscoveryResult<S>;
|
|
31
|
+
/** 실패를 결과로 접는다 — 사이트는 비어 있다(반쪽 목록을 성공처럼 보이게 하지 않는다). */
|
|
32
|
+
export declare function failed<S>(error: DiscoveryError): DiscoveryResult<S>;
|
|
33
|
+
/** 어댑터가 던진 것에서 사람이 읽을 사유를 뽑는다 — 형태를 가정하지 않는다. */
|
|
34
|
+
export declare function detailOf(e: unknown): string;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* 사이트 발견 결과 — **"못 찾았다" 와 "없다" 를 가른다.** 순수.
|
|
4
|
+
*
|
|
5
|
+
* ── 왜 있나 (2026-08-06) ────────────────────────────────────────────────────
|
|
6
|
+
* 실 시스템을 연결해 "거기 있는 것" 으로 트윈을 만드는 경로에서, 발견이 실패하면 화면이
|
|
7
|
+
* **"사이트를 찾지 못했습니다"** 라고만 말했다. 네 가지가 그 한 문장으로 접혀 있었다.
|
|
8
|
+
*
|
|
9
|
+
* ① 그 레퍼런스가 등록돼 있지 않다 (`!ref → []`)
|
|
10
|
+
* ② 어댑터 종류를 모른다 (`!adapter → []`)
|
|
11
|
+
* ③ 연결·조회가 실패했다 (`catch { return [] }`)
|
|
12
|
+
* ④ 정말로 사이트가 없다 (빈 배열)
|
|
13
|
+
*
|
|
14
|
+
* ①②③ 은 **고칠 수 있는 문제**이고 ④ 는 사실이다. 그런데 화면이 같은 말을 하므로 사용자는 자격이
|
|
15
|
+
* 틀렸는지, 주소가 틀렸는지, 정말 공장이 없는지 알 수 없다 — 실 시스템 연결에서 가장 자주 막히는
|
|
16
|
+
* 자리인데 거기서 아무 단서도 주지 않았다.
|
|
17
|
+
*
|
|
18
|
+
* ── 왜 문장이 아니라 코드인가 ───────────────────────────────────────────────
|
|
19
|
+
* 이 프로젝트가 이미 쓰는 규약을 따른다(주의 신호·커맨드 거절·인제스트 경고와 같은 모양):
|
|
20
|
+
* `code`(언어 중립) + `params`(문장에 끼울 값) + `message`(영어 canonical, 로그·폴백).
|
|
21
|
+
* 화면은 `twin.connect.discover.<code>` 로 사람 말을 만든다.
|
|
22
|
+
*
|
|
23
|
+
* ── 원 시스템의 오류 문장을 감추지 않는다 ────────────────────────────────────
|
|
24
|
+
* 어댑터가 준 사유(자격 거부·타임아웃·404)는 **그 시스템만 아는 사실**이라 우리가 다시 쓸 수 없다.
|
|
25
|
+
* `detail` 로 그대로 실어 보내 화면이 보조 문장으로 보인다 — 원문이 있으면 사용자가 자기 관리자에게
|
|
26
|
+
* 물어볼 수 있고, 없으면 우리 짐작만 남는다.
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.discoveryTimedOut = exports.discoveryFailed = exports.adapterUnknown = exports.referenceNotFound = void 0;
|
|
30
|
+
exports.discovered = discovered;
|
|
31
|
+
exports.failed = failed;
|
|
32
|
+
exports.detailOf = detailOf;
|
|
33
|
+
/** 등록된 레퍼런스가 없다 — 목록이 낡았거나 다른 테넌트의 것이다. */
|
|
34
|
+
const referenceNotFound = (source) => ({
|
|
35
|
+
code: 'reference-not-found',
|
|
36
|
+
params: { source },
|
|
37
|
+
message: `reference "${source}" is not registered in this tenant`
|
|
38
|
+
});
|
|
39
|
+
exports.referenceNotFound = referenceNotFound;
|
|
40
|
+
/** 어댑터 종류를 모른다 — 배포에 그 커넥터가 없다(패키지 미설치·오타). */
|
|
41
|
+
const adapterUnknown = (adapterType, source) => ({
|
|
42
|
+
code: 'adapter-unknown',
|
|
43
|
+
params: { adapterType, source },
|
|
44
|
+
message: `no connector of type "${adapterType}" is available for reference "${source}"`
|
|
45
|
+
});
|
|
46
|
+
exports.adapterUnknown = adapterUnknown;
|
|
47
|
+
/** 연결·조회가 실패했다 — 원 시스템이 준 사유를 그대로 싣는다. */
|
|
48
|
+
const discoveryFailed = (source, detail) => ({
|
|
49
|
+
code: 'discovery-failed',
|
|
50
|
+
params: { source, detail },
|
|
51
|
+
message: `could not read sites from "${source}": ${detail}`
|
|
52
|
+
});
|
|
53
|
+
exports.discoveryFailed = discoveryFailed;
|
|
54
|
+
/** 기다려도 답이 없었다 — 무한히 도는 화면보다 사유가 낫다. */
|
|
55
|
+
const discoveryTimedOut = (source, seconds) => ({
|
|
56
|
+
code: 'discovery-timeout',
|
|
57
|
+
params: { source, seconds },
|
|
58
|
+
message: `"${source}" did not answer within ${seconds}s`
|
|
59
|
+
});
|
|
60
|
+
exports.discoveryTimedOut = discoveryTimedOut;
|
|
61
|
+
/**
|
|
62
|
+
* 어댑터가 준 것을 결과로 접는다.
|
|
63
|
+
*
|
|
64
|
+
* **사이트가 없는 것을 실패로 만들지 않는다** — 연결이 되고 조회가 성공했는데 공장이 하나도 없는
|
|
65
|
+
* 시스템은 있을 수 있다(권한이 좁거나 아직 등록되지 않은 경우). 그것은 사실이고, 화면은 "없다" 고
|
|
66
|
+
* 말해야 한다. 실패로 뭉개면 사용자가 있지도 않은 문제를 찾는다.
|
|
67
|
+
*/
|
|
68
|
+
function discovered(sites) {
|
|
69
|
+
return { sites: Array.isArray(sites) ? [...sites] : [] };
|
|
70
|
+
}
|
|
71
|
+
/** 실패를 결과로 접는다 — 사이트는 비어 있다(반쪽 목록을 성공처럼 보이게 하지 않는다). */
|
|
72
|
+
function failed(error) {
|
|
73
|
+
return { sites: [], error };
|
|
74
|
+
}
|
|
75
|
+
/** 어댑터가 던진 것에서 사람이 읽을 사유를 뽑는다 — 형태를 가정하지 않는다. */
|
|
76
|
+
function detailOf(e) {
|
|
77
|
+
if (e instanceof Error && e.message)
|
|
78
|
+
return e.message;
|
|
79
|
+
if (typeof e === 'string' && e)
|
|
80
|
+
return e;
|
|
81
|
+
/* 무엇이 왔는지 모르면 그렇게 말한다 — 빈 문장으로 "이유 없음" 처럼 보이게 하지 않는다. */
|
|
82
|
+
return 'the connector failed without a message';
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=discovery-result.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery-result.js","sourceRoot":"","sources":["../../../server/service/reference/discovery-result.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;;;AAsDH,gCAEC;AAGD,wBAEC;AAGD,4BAKC;AAlDD,2CAA2C;AACpC,MAAM,iBAAiB,GAAG,CAAC,MAAc,EAAkB,EAAE,CAAC,CAAC;IACpE,IAAI,EAAE,qBAAqB;IAC3B,MAAM,EAAE,EAAE,MAAM,EAAE;IAClB,OAAO,EAAE,cAAc,MAAM,oCAAoC;CAClE,CAAC,CAAA;AAJW,QAAA,iBAAiB,qBAI5B;AAEF,+CAA+C;AACxC,MAAM,cAAc,GAAG,CAAC,WAAmB,EAAE,MAAc,EAAkB,EAAE,CAAC,CAAC;IACtF,IAAI,EAAE,iBAAiB;IACvB,MAAM,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE;IAC/B,OAAO,EAAE,yBAAyB,WAAW,iCAAiC,MAAM,GAAG;CACxF,CAAC,CAAA;AAJW,QAAA,cAAc,kBAIzB;AAEF,0CAA0C;AACnC,MAAM,eAAe,GAAG,CAAC,MAAc,EAAE,MAAc,EAAkB,EAAE,CAAC,CAAC;IAClF,IAAI,EAAE,kBAAkB;IACxB,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE;IAC1B,OAAO,EAAE,8BAA8B,MAAM,MAAM,MAAM,EAAE;CAC5D,CAAC,CAAA;AAJW,QAAA,eAAe,mBAI1B;AAEF,wCAAwC;AACjC,MAAM,iBAAiB,GAAG,CAAC,MAAc,EAAE,OAAe,EAAkB,EAAE,CAAC,CAAC;IACrF,IAAI,EAAE,mBAAmB;IACzB,MAAM,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE;IAC3B,OAAO,EAAE,IAAI,MAAM,2BAA2B,OAAO,GAAG;CACzD,CAAC,CAAA;AAJW,QAAA,iBAAiB,qBAI5B;AAEF;;;;;;GAMG;AACH,SAAgB,UAAU,CAAI,KAAsC;IAClE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAA;AAC1D,CAAC;AAED,wDAAwD;AACxD,SAAgB,MAAM,CAAI,KAAqB;IAC7C,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,CAAA;AAC7B,CAAC;AAED,iDAAiD;AACjD,SAAgB,QAAQ,CAAC,CAAU;IACjC,IAAI,CAAC,YAAY,KAAK,IAAI,CAAC,CAAC,OAAO;QAAE,OAAO,CAAC,CAAC,OAAO,CAAA;IACrD,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC;QAAE,OAAO,CAAC,CAAA;IACxC,yDAAyD;IACzD,OAAO,wCAAwC,CAAA;AACjD,CAAC","sourcesContent":["/*\n * 사이트 발견 결과 — **\"못 찾았다\" 와 \"없다\" 를 가른다.** 순수.\n *\n * ── 왜 있나 (2026-08-06) ────────────────────────────────────────────────────\n * 실 시스템을 연결해 \"거기 있는 것\" 으로 트윈을 만드는 경로에서, 발견이 실패하면 화면이\n * **\"사이트를 찾지 못했습니다\"** 라고만 말했다. 네 가지가 그 한 문장으로 접혀 있었다.\n *\n * ① 그 레퍼런스가 등록돼 있지 않다 (`!ref → []`)\n * ② 어댑터 종류를 모른다 (`!adapter → []`)\n * ③ 연결·조회가 실패했다 (`catch { return [] }`)\n * ④ 정말로 사이트가 없다 (빈 배열)\n *\n * ①②③ 은 **고칠 수 있는 문제**이고 ④ 는 사실이다. 그런데 화면이 같은 말을 하므로 사용자는 자격이\n * 틀렸는지, 주소가 틀렸는지, 정말 공장이 없는지 알 수 없다 — 실 시스템 연결에서 가장 자주 막히는\n * 자리인데 거기서 아무 단서도 주지 않았다.\n *\n * ── 왜 문장이 아니라 코드인가 ───────────────────────────────────────────────\n * 이 프로젝트가 이미 쓰는 규약을 따른다(주의 신호·커맨드 거절·인제스트 경고와 같은 모양):\n * `code`(언어 중립) + `params`(문장에 끼울 값) + `message`(영어 canonical, 로그·폴백).\n * 화면은 `twin.connect.discover.<code>` 로 사람 말을 만든다.\n *\n * ── 원 시스템의 오류 문장을 감추지 않는다 ────────────────────────────────────\n * 어댑터가 준 사유(자격 거부·타임아웃·404)는 **그 시스템만 아는 사실**이라 우리가 다시 쓸 수 없다.\n * `detail` 로 그대로 실어 보내 화면이 보조 문장으로 보인다 — 원문이 있으면 사용자가 자기 관리자에게\n * 물어볼 수 있고, 없으면 우리 짐작만 남는다.\n */\n\n/** 발견이 왜 아무것도 주지 못했나 — 언어 중립 코드. */\nexport type DiscoveryErrorCode = 'reference-not-found' | 'adapter-unknown' | 'discovery-failed' | 'discovery-timeout'\n\nexport interface DiscoveryError {\n code: DiscoveryErrorCode\n params?: Record<string, string | number>\n /** 영어 canonical — 로그에 남고, 번역이 없으면 그대로 보인다. */\n message: string\n}\n\nexport interface DiscoveryResult<S = unknown> {\n /** 발견된 사이트들. 실패했으면 빈 배열이고 `error` 가 이유를 말한다. */\n sites: S[]\n /** 실패 사유. **없으면 성공이다** — 빈 배열 + 사유 없음 = \"연결은 됐지만 사이트가 없다\". */\n error?: DiscoveryError\n}\n\n/** 등록된 레퍼런스가 없다 — 목록이 낡았거나 다른 테넌트의 것이다. */\nexport const referenceNotFound = (source: string): DiscoveryError => ({\n code: 'reference-not-found',\n params: { source },\n message: `reference \"${source}\" is not registered in this tenant`\n})\n\n/** 어댑터 종류를 모른다 — 배포에 그 커넥터가 없다(패키지 미설치·오타). */\nexport const adapterUnknown = (adapterType: string, source: string): DiscoveryError => ({\n code: 'adapter-unknown',\n params: { adapterType, source },\n message: `no connector of type \"${adapterType}\" is available for reference \"${source}\"`\n})\n\n/** 연결·조회가 실패했다 — 원 시스템이 준 사유를 그대로 싣는다. */\nexport const discoveryFailed = (source: string, detail: string): DiscoveryError => ({\n code: 'discovery-failed',\n params: { source, detail },\n message: `could not read sites from \"${source}\": ${detail}`\n})\n\n/** 기다려도 답이 없었다 — 무한히 도는 화면보다 사유가 낫다. */\nexport const discoveryTimedOut = (source: string, seconds: number): DiscoveryError => ({\n code: 'discovery-timeout',\n params: { source, seconds },\n message: `\"${source}\" did not answer within ${seconds}s`\n})\n\n/**\n * 어댑터가 준 것을 결과로 접는다.\n *\n * **사이트가 없는 것을 실패로 만들지 않는다** — 연결이 되고 조회가 성공했는데 공장이 하나도 없는\n * 시스템은 있을 수 있다(권한이 좁거나 아직 등록되지 않은 경우). 그것은 사실이고, 화면은 \"없다\" 고\n * 말해야 한다. 실패로 뭉개면 사용자가 있지도 않은 문제를 찾는다.\n */\nexport function discovered<S>(sites: readonly S[] | undefined | null): DiscoveryResult<S> {\n return { sites: Array.isArray(sites) ? [...sites] : [] }\n}\n\n/** 실패를 결과로 접는다 — 사이트는 비어 있다(반쪽 목록을 성공처럼 보이게 하지 않는다). */\nexport function failed<S>(error: DiscoveryError): DiscoveryResult<S> {\n return { sites: [], error }\n}\n\n/** 어댑터가 던진 것에서 사람이 읽을 사유를 뽑는다 — 형태를 가정하지 않는다. */\nexport function detailOf(e: unknown): string {\n if (e instanceof Error && e.message) return e.message\n if (typeof e === 'string' && e) return e\n /* 무엇이 왔는지 모르면 그렇게 말한다 — 빈 문장으로 \"이유 없음\" 처럼 보이게 하지 않는다. */\n return 'the connector failed without a message'\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** 공간 판정 결과. */
|
|
2
|
+
export interface IngestSpaceChoice {
|
|
3
|
+
/** 실제로 쓸 공간 id. */
|
|
4
|
+
spaceId: string;
|
|
5
|
+
/** 사용자가 기존 현장에 합치기를 골랐나(마스터가 파생한 공간과 다른 곳으로 들어가나). */
|
|
6
|
+
joined: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** 거절 사유 — 호출측이 문장을 만들 때 쓴다(언어 중립 코드). */
|
|
9
|
+
export declare const INGEST_SPACE_NOT_FOUND = "space-not-found";
|
|
10
|
+
/**
|
|
11
|
+
* 요청받은 공간과 마스터가 파생한 공간 중 무엇으로 들어갈지 정한다.
|
|
12
|
+
*
|
|
13
|
+
* @param masterSpaceId 마스터에서 파생된 공간 id(요청이 없을 때의 기본).
|
|
14
|
+
* @param requested 사용자가 고른 기존 공간 id. 없으면 빈 값/undefined.
|
|
15
|
+
* @param requestedExists 그 공간이 이 테넌트에 실제로 있나(호출측이 조회해 넘긴다).
|
|
16
|
+
* @throws 요청한 공간이 없으면 — 조용히 새로 만들지 않는다.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveIngestSpace(masterSpaceId: string, requested: string | undefined | null, requestedExists: boolean): IngestSpaceChoice;
|
|
19
|
+
/** 구역·랜드마크처럼 id 로 합쳐지는 것들. */
|
|
20
|
+
interface Identified {
|
|
21
|
+
id?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 합치는 쪽과 기존 쪽이 같은 id 를 쓰는 것들을 찾아 경고 문장을 만든다.
|
|
25
|
+
*
|
|
26
|
+
* 정당한 경우(같은 현장의 같은 구역)와 사고(서로 다른 구역이 우연히 같은 이름)를 기계가 가릴 수는
|
|
27
|
+
* 없다. 가릴 수 없는 것을 고르지 않고, **무엇이 합쳐졌는지 말한다.**
|
|
28
|
+
*/
|
|
29
|
+
export declare function collidingIds(existing: readonly Identified[] | undefined | null, incoming: readonly Identified[] | undefined | null): string[];
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* 인제스트가 어느 공간으로 들어갈지 정한다 — **순수**.
|
|
4
|
+
*
|
|
5
|
+
* ── 왜 있나 (2026-08-05) ────────────────────────────────────────────────────
|
|
6
|
+
* 한 현실(공장·물류센터)을 여러 렌즈가 비춘다: 창고(WMS)·공정(MES)·야드(YMS) 가 같은 부지에 있다.
|
|
7
|
+
* 엔진은 이미 그것을 지원한다 — 같은 `spaceId` 로 들어오면 공간 content 를 병합하고 기존 값을 이긴다.
|
|
8
|
+
*
|
|
9
|
+
* 그런데 **만드는 흐름에 그 선택이 없었다.** 템플릿은 `space.spaceId` 를 정하지 않으므로 항상
|
|
10
|
+
* `<source>-site` 로 파생돼, 새 트윈은 늘 새 현장처럼 생겼다. 데이터로는 가능한 일이 저작으로는
|
|
11
|
+
* 불가능했다.
|
|
12
|
+
*
|
|
13
|
+
* ── 없는 공간에 조용히 새 공간을 만들지 않는다 ────────────────────────────────
|
|
14
|
+
* "이 현장에 더한다" 는 요청이 오타·낡은 목록으로 빗나갔을 때 새 공간을 만들어 주면, 사용자는
|
|
15
|
+
* 합쳤다고 믿고 화면에서는 따로 놀게 된다 — 나중에 두 현장을 손으로 합쳐야 하고, 그때는 이미
|
|
16
|
+
* 이벤트가 양쪽에 쌓여 있다. 그래서 **모르는 공간이면 거절한다.**
|
|
17
|
+
*
|
|
18
|
+
* ── 같은 id 를 쓰는 구역은 조용히 합쳐지므로 알린다 ──────────────────────────
|
|
19
|
+
* 공간 content 의 구역·랜드마크는 id 로 합집합을 만든다(`unionById`). 두 렌즈가 우연히 같은 구역
|
|
20
|
+
* id(`a-yard` 따위)를 쓰면 서로 다른 구역이 하나로 접힌다. 막을 일은 아니다(같은 현장의 같은 구역을
|
|
21
|
+
* 가리키는 정당한 경우가 많다) — 다만 **모르고 지나가지 않게** 경고로 돌려준다.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.INGEST_SPACE_NOT_FOUND = void 0;
|
|
25
|
+
exports.resolveIngestSpace = resolveIngestSpace;
|
|
26
|
+
exports.collidingIds = collidingIds;
|
|
27
|
+
/** 거절 사유 — 호출측이 문장을 만들 때 쓴다(언어 중립 코드). */
|
|
28
|
+
exports.INGEST_SPACE_NOT_FOUND = 'space-not-found';
|
|
29
|
+
/**
|
|
30
|
+
* 요청받은 공간과 마스터가 파생한 공간 중 무엇으로 들어갈지 정한다.
|
|
31
|
+
*
|
|
32
|
+
* @param masterSpaceId 마스터에서 파생된 공간 id(요청이 없을 때의 기본).
|
|
33
|
+
* @param requested 사용자가 고른 기존 공간 id. 없으면 빈 값/undefined.
|
|
34
|
+
* @param requestedExists 그 공간이 이 테넌트에 실제로 있나(호출측이 조회해 넘긴다).
|
|
35
|
+
* @throws 요청한 공간이 없으면 — 조용히 새로 만들지 않는다.
|
|
36
|
+
*/
|
|
37
|
+
function resolveIngestSpace(masterSpaceId, requested, requestedExists) {
|
|
38
|
+
const want = (requested ?? '').trim();
|
|
39
|
+
if (!want)
|
|
40
|
+
return { spaceId: masterSpaceId, joined: false };
|
|
41
|
+
if (!requestedExists) {
|
|
42
|
+
const e = new Error(`space "${want}" not found — cannot co-locate into a space that does not exist`);
|
|
43
|
+
e.code = exports.INGEST_SPACE_NOT_FOUND;
|
|
44
|
+
throw e;
|
|
45
|
+
}
|
|
46
|
+
/* 마스터가 이미 그 공간을 가리키면 합치기가 아니다 — 같은 공간의 재인제스트다. */
|
|
47
|
+
return { spaceId: want, joined: want !== masterSpaceId };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 합치는 쪽과 기존 쪽이 같은 id 를 쓰는 것들을 찾아 경고 문장을 만든다.
|
|
51
|
+
*
|
|
52
|
+
* 정당한 경우(같은 현장의 같은 구역)와 사고(서로 다른 구역이 우연히 같은 이름)를 기계가 가릴 수는
|
|
53
|
+
* 없다. 가릴 수 없는 것을 고르지 않고, **무엇이 합쳐졌는지 말한다.**
|
|
54
|
+
*/
|
|
55
|
+
function collidingIds(existing, incoming) {
|
|
56
|
+
const have = new Set((existing ?? []).map(x => x?.id).filter(Boolean));
|
|
57
|
+
const hits = new Set();
|
|
58
|
+
for (const x of incoming ?? [])
|
|
59
|
+
if (x?.id && have.has(x.id))
|
|
60
|
+
hits.add(x.id);
|
|
61
|
+
return [...hits].sort();
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=ingest-space.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ingest-space.js","sourceRoot":"","sources":["../../../server/service/reference/ingest-space.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;;;AAqBH,gDAUC;AAaD,oCAKC;AAvCD,0CAA0C;AAC7B,QAAA,sBAAsB,GAAG,iBAAiB,CAAA;AAEvD;;;;;;;GAOG;AACH,SAAgB,kBAAkB,CAAC,aAAqB,EAAE,SAAoC,EAAE,eAAwB;IACtH,MAAM,IAAI,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;IACrC,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,CAAA;IAC3D,IAAI,CAAC,eAAe,EAAE,CAAC;QACrB,MAAM,CAAC,GAAG,IAAI,KAAK,CAAC,UAAU,IAAI,iEAAiE,CAAC,CACnG;QAAC,CAAS,CAAC,IAAI,GAAG,8BAAsB,CAAA;QACzC,MAAM,CAAC,CAAA;IACT,CAAC;IACD,kDAAkD;IAClD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,KAAK,aAAa,EAAE,CAAA;AAC1D,CAAC;AAOD;;;;;GAKG;AACH,SAAgB,YAAY,CAAC,QAAkD,EAAE,QAAkD;IACjI,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAa,CAAC,CAAA;IAClF,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAC9B,KAAK,MAAM,CAAC,IAAI,QAAQ,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IAC3E,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,EAAE,CAAA;AACzB,CAAC","sourcesContent":["/*\n * 인제스트가 어느 공간으로 들어갈지 정한다 — **순수**.\n *\n * ── 왜 있나 (2026-08-05) ────────────────────────────────────────────────────\n * 한 현실(공장·물류센터)을 여러 렌즈가 비춘다: 창고(WMS)·공정(MES)·야드(YMS) 가 같은 부지에 있다.\n * 엔진은 이미 그것을 지원한다 — 같은 `spaceId` 로 들어오면 공간 content 를 병합하고 기존 값을 이긴다.\n *\n * 그런데 **만드는 흐름에 그 선택이 없었다.** 템플릿은 `space.spaceId` 를 정하지 않으므로 항상\n * `<source>-site` 로 파생돼, 새 트윈은 늘 새 현장처럼 생겼다. 데이터로는 가능한 일이 저작으로는\n * 불가능했다.\n *\n * ── 없는 공간에 조용히 새 공간을 만들지 않는다 ────────────────────────────────\n * \"이 현장에 더한다\" 는 요청이 오타·낡은 목록으로 빗나갔을 때 새 공간을 만들어 주면, 사용자는\n * 합쳤다고 믿고 화면에서는 따로 놀게 된다 — 나중에 두 현장을 손으로 합쳐야 하고, 그때는 이미\n * 이벤트가 양쪽에 쌓여 있다. 그래서 **모르는 공간이면 거절한다.**\n *\n * ── 같은 id 를 쓰는 구역은 조용히 합쳐지므로 알린다 ──────────────────────────\n * 공간 content 의 구역·랜드마크는 id 로 합집합을 만든다(`unionById`). 두 렌즈가 우연히 같은 구역\n * id(`a-yard` 따위)를 쓰면 서로 다른 구역이 하나로 접힌다. 막을 일은 아니다(같은 현장의 같은 구역을\n * 가리키는 정당한 경우가 많다) — 다만 **모르고 지나가지 않게** 경고로 돌려준다.\n */\n\n/** 공간 판정 결과. */\nexport interface IngestSpaceChoice {\n /** 실제로 쓸 공간 id. */\n spaceId: string\n /** 사용자가 기존 현장에 합치기를 골랐나(마스터가 파생한 공간과 다른 곳으로 들어가나). */\n joined: boolean\n}\n\n/** 거절 사유 — 호출측이 문장을 만들 때 쓴다(언어 중립 코드). */\nexport const INGEST_SPACE_NOT_FOUND = 'space-not-found'\n\n/**\n * 요청받은 공간과 마스터가 파생한 공간 중 무엇으로 들어갈지 정한다.\n *\n * @param masterSpaceId 마스터에서 파생된 공간 id(요청이 없을 때의 기본).\n * @param requested 사용자가 고른 기존 공간 id. 없으면 빈 값/undefined.\n * @param requestedExists 그 공간이 이 테넌트에 실제로 있나(호출측이 조회해 넘긴다).\n * @throws 요청한 공간이 없으면 — 조용히 새로 만들지 않는다.\n */\nexport function resolveIngestSpace(masterSpaceId: string, requested: string | undefined | null, requestedExists: boolean): IngestSpaceChoice {\n const want = (requested ?? '').trim()\n if (!want) return { spaceId: masterSpaceId, joined: false }\n if (!requestedExists) {\n const e = new Error(`space \"${want}\" not found — cannot co-locate into a space that does not exist`)\n ;(e as any).code = INGEST_SPACE_NOT_FOUND\n throw e\n }\n /* 마스터가 이미 그 공간을 가리키면 합치기가 아니다 — 같은 공간의 재인제스트다. */\n return { spaceId: want, joined: want !== masterSpaceId }\n}\n\n/** 구역·랜드마크처럼 id 로 합쳐지는 것들. */\ninterface Identified {\n id?: string\n}\n\n/**\n * 합치는 쪽과 기존 쪽이 같은 id 를 쓰는 것들을 찾아 경고 문장을 만든다.\n *\n * 정당한 경우(같은 현장의 같은 구역)와 사고(서로 다른 구역이 우연히 같은 이름)를 기계가 가릴 수는\n * 없다. 가릴 수 없는 것을 고르지 않고, **무엇이 합쳐졌는지 말한다.**\n */\nexport function collidingIds(existing: readonly Identified[] | undefined | null, incoming: readonly Identified[] | undefined | null): string[] {\n const have = new Set((existing ?? []).map(x => x?.id).filter(Boolean) as string[])\n const hits = new Set<string>()\n for (const x of incoming ?? []) if (x?.id && have.has(x.id)) hits.add(x.id)\n return [...hits].sort()\n}\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** 템플릿 노브 — 기본값 적용에 필요한 만큼만 본다. */
|
|
2
|
+
export interface KnobLike {
|
|
3
|
+
key?: string;
|
|
4
|
+
default?: unknown;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* 빠진 노브를 기본값으로 채운다.
|
|
8
|
+
*
|
|
9
|
+
* @param knobs 템플릿이 선언한 노브들(없으면 파라미터를 그대로 돌려준다).
|
|
10
|
+
* @param params 호출자가 준 파라미터.
|
|
11
|
+
* @returns 새 객체 — 입력을 바꾸지 않는다(호출자의 객체가 몰래 변하면 추적이 어렵다).
|
|
12
|
+
*/
|
|
13
|
+
export declare function applyKnobDefaults(knobs: readonly KnobLike[] | undefined | null, params: Record<string, unknown> | undefined | null): Record<string, unknown>;
|
|
14
|
+
/**
|
|
15
|
+
* 기본값으로 채운 노브 이름들 — 호출자에게 "무엇을 내가 정해 줬는지" 알릴 때 쓴다.
|
|
16
|
+
*
|
|
17
|
+
* 조용히 채우는 것도 조용히 비우는 것만큼 위험하다: 사용자가 준 적 없는 값으로 트윈이 만들어졌는데
|
|
18
|
+
* 아무 말이 없으면, 나중에 "왜 이 수치인가" 를 아무도 답할 수 없다.
|
|
19
|
+
*/
|
|
20
|
+
export declare function filledKnobKeys(knobs: readonly KnobLike[] | undefined | null, params: Record<string, unknown> | undefined | null): string[];
|