@tienne/gestalt 0.87.0 → 0.89.0
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/CLAUDE.md +12 -4
- package/README.ko.md +2 -1
- package/README.md +3 -1
- package/dist/bin/gestalt-hook.d.ts +5 -0
- package/dist/bin/gestalt-hook.d.ts.map +1 -0
- package/dist/bin/gestalt-hook.js +51 -0
- package/dist/bin/gestalt-hook.js.map +1 -0
- package/dist/package.json +5 -3
- package/dist/plugin/skills/_shared/proactive-routing.md +1 -0
- package/dist/plugin/skills/architecture/SKILL.md +913 -0
- package/dist/plugin/skills/blast-radius/SKILL.md +11 -1
- package/dist/plugin/skills/diff-radius/SKILL.md +11 -0
- package/dist/schemas/architecture-ir.schema.json +596 -0
- package/dist/schemas/gestalt.schema.json +19 -0
- package/dist/src/architecture/canvas-geometry.d.ts +40 -0
- package/dist/src/architecture/canvas-geometry.d.ts.map +1 -0
- package/dist/src/architecture/canvas-geometry.js +212 -0
- package/dist/src/architecture/canvas-geometry.js.map +1 -0
- package/dist/src/architecture/codeowners.d.ts +16 -0
- package/dist/src/architecture/codeowners.d.ts.map +1 -0
- package/dist/src/architecture/codeowners.js +58 -0
- package/dist/src/architecture/codeowners.js.map +1 -0
- package/dist/src/architecture/compare-layout.d.ts +27 -0
- package/dist/src/architecture/compare-layout.d.ts.map +1 -0
- package/dist/src/architecture/compare-layout.js +72 -0
- package/dist/src/architecture/compare-layout.js.map +1 -0
- package/dist/src/architecture/dataflow-layout.d.ts +29 -0
- package/dist/src/architecture/dataflow-layout.d.ts.map +1 -0
- package/dist/src/architecture/dataflow-layout.js +229 -0
- package/dist/src/architecture/dataflow-layout.js.map +1 -0
- package/dist/src/architecture/doc-fresh.d.ts +31 -0
- package/dist/src/architecture/doc-fresh.d.ts.map +1 -0
- package/dist/src/architecture/doc-fresh.js +84 -0
- package/dist/src/architecture/doc-fresh.js.map +1 -0
- package/dist/src/architecture/doc-link.d.ts +84 -0
- package/dist/src/architecture/doc-link.d.ts.map +1 -0
- package/dist/src/architecture/doc-link.js +396 -0
- package/dist/src/architecture/doc-link.js.map +1 -0
- package/dist/src/architecture/doc-routes.d.ts +39 -0
- package/dist/src/architecture/doc-routes.d.ts.map +1 -0
- package/dist/src/architecture/doc-routes.js +123 -0
- package/dist/src/architecture/doc-routes.js.map +1 -0
- package/dist/src/architecture/doc-scan.d.ts +107 -0
- package/dist/src/architecture/doc-scan.d.ts.map +1 -0
- package/dist/src/architecture/doc-scan.js +617 -0
- package/dist/src/architecture/doc-scan.js.map +1 -0
- package/dist/src/architecture/doc-stale.d.ts +32 -0
- package/dist/src/architecture/doc-stale.d.ts.map +1 -0
- package/dist/src/architecture/doc-stale.js +60 -0
- package/dist/src/architecture/doc-stale.js.map +1 -0
- package/dist/src/architecture/drilldown.d.ts +51 -0
- package/dist/src/architecture/drilldown.d.ts.map +1 -0
- package/dist/src/architecture/drilldown.js +1160 -0
- package/dist/src/architecture/drilldown.js.map +1 -0
- package/dist/src/architecture/endpoint-match.d.ts +43 -0
- package/dist/src/architecture/endpoint-match.d.ts.map +1 -0
- package/dist/src/architecture/endpoint-match.js +133 -0
- package/dist/src/architecture/endpoint-match.js.map +1 -0
- package/dist/src/architecture/flow-layout.d.ts +79 -0
- package/dist/src/architecture/flow-layout.d.ts.map +1 -0
- package/dist/src/architecture/flow-layout.js +519 -0
- package/dist/src/architecture/flow-layout.js.map +1 -0
- package/dist/src/architecture/focus.d.ts +14 -0
- package/dist/src/architecture/focus.d.ts.map +1 -0
- package/dist/src/architecture/focus.js +107 -0
- package/dist/src/architecture/focus.js.map +1 -0
- package/dist/src/architecture/global-context.d.ts +23 -0
- package/dist/src/architecture/global-context.d.ts.map +1 -0
- package/dist/src/architecture/global-context.js +67 -0
- package/dist/src/architecture/global-context.js.map +1 -0
- package/dist/src/architecture/html-client.d.ts +35 -0
- package/dist/src/architecture/html-client.d.ts.map +1 -0
- package/dist/src/architecture/html-client.js +1852 -0
- package/dist/src/architecture/html-client.js.map +1 -0
- package/dist/src/architecture/html-renderer.d.ts +22 -0
- package/dist/src/architecture/html-renderer.d.ts.map +1 -0
- package/dist/src/architecture/html-renderer.js +1290 -0
- package/dist/src/architecture/html-renderer.js.map +1 -0
- package/dist/src/architecture/html-theme.d.ts +25 -0
- package/dist/src/architecture/html-theme.d.ts.map +1 -0
- package/dist/src/architecture/html-theme.js +681 -0
- package/dist/src/architecture/html-theme.js.map +1 -0
- package/dist/src/architecture/index.d.ts +9 -0
- package/dist/src/architecture/index.d.ts.map +1 -0
- package/dist/src/architecture/index.js +9 -0
- package/dist/src/architecture/index.js.map +1 -0
- package/dist/src/architecture/ir-schema.d.ts +1832 -0
- package/dist/src/architecture/ir-schema.d.ts.map +1 -0
- package/dist/src/architecture/ir-schema.js +343 -0
- package/dist/src/architecture/ir-schema.js.map +1 -0
- package/dist/src/architecture/kind-text.d.ts +21 -0
- package/dist/src/architecture/kind-text.d.ts.map +1 -0
- package/dist/src/architecture/kind-text.js +42 -0
- package/dist/src/architecture/kind-text.js.map +1 -0
- package/dist/src/architecture/layout.d.ts +227 -0
- package/dist/src/architecture/layout.d.ts.map +1 -0
- package/dist/src/architecture/layout.js +627 -0
- package/dist/src/architecture/layout.js.map +1 -0
- package/dist/src/architecture/mcp-tool-match.d.ts +49 -0
- package/dist/src/architecture/mcp-tool-match.d.ts.map +1 -0
- package/dist/src/architecture/mcp-tool-match.js +53 -0
- package/dist/src/architecture/mcp-tool-match.js.map +1 -0
- package/dist/src/architecture/merge.d.ts +54 -0
- package/dist/src/architecture/merge.d.ts.map +1 -0
- package/dist/src/architecture/merge.js +681 -0
- package/dist/src/architecture/merge.js.map +1 -0
- package/dist/src/architecture/micro-app.d.ts +20 -0
- package/dist/src/architecture/micro-app.d.ts.map +1 -0
- package/dist/src/architecture/micro-app.js +41 -0
- package/dist/src/architecture/micro-app.js.map +1 -0
- package/dist/src/architecture/name-ref-match.d.ts +43 -0
- package/dist/src/architecture/name-ref-match.d.ts.map +1 -0
- package/dist/src/architecture/name-ref-match.js +127 -0
- package/dist/src/architecture/name-ref-match.js.map +1 -0
- package/dist/src/architecture/packs/data.d.ts +109 -0
- package/dist/src/architecture/packs/data.d.ts.map +1 -0
- package/dist/src/architecture/packs/data.js +64 -0
- package/dist/src/architecture/packs/data.js.map +1 -0
- package/dist/src/architecture/packs/generic.d.ts +47 -0
- package/dist/src/architecture/packs/generic.d.ts.map +1 -0
- package/dist/src/architecture/packs/generic.js +35 -0
- package/dist/src/architecture/packs/generic.js.map +1 -0
- package/dist/src/architecture/packs/harness.d.ts +96 -0
- package/dist/src/architecture/packs/harness.d.ts.map +1 -0
- package/dist/src/architecture/packs/harness.js +66 -0
- package/dist/src/architecture/packs/harness.js.map +1 -0
- package/dist/src/architecture/packs/index.d.ts +840 -0
- package/dist/src/architecture/packs/index.d.ts.map +1 -0
- package/dist/src/architecture/packs/index.js +87 -0
- package/dist/src/architecture/packs/index.js.map +1 -0
- package/dist/src/architecture/packs/infra.d.ts +91 -0
- package/dist/src/architecture/packs/infra.d.ts.map +1 -0
- package/dist/src/architecture/packs/infra.js +57 -0
- package/dist/src/architecture/packs/infra.js.map +1 -0
- package/dist/src/architecture/packs/knowledge.d.ts +72 -0
- package/dist/src/architecture/packs/knowledge.d.ts.map +1 -0
- package/dist/src/architecture/packs/knowledge.js +58 -0
- package/dist/src/architecture/packs/knowledge.js.map +1 -0
- package/dist/src/architecture/packs/process.d.ts +87 -0
- package/dist/src/architecture/packs/process.d.ts.map +1 -0
- package/dist/src/architecture/packs/process.js +52 -0
- package/dist/src/architecture/packs/process.js.map +1 -0
- package/dist/src/architecture/packs/render-classes.d.ts +11 -0
- package/dist/src/architecture/packs/render-classes.d.ts.map +1 -0
- package/dist/src/architecture/packs/render-classes.js +92 -0
- package/dist/src/architecture/packs/render-classes.js.map +1 -0
- package/dist/src/architecture/packs/types.d.ts +66 -0
- package/dist/src/architecture/packs/types.d.ts.map +1 -0
- package/dist/src/architecture/packs/types.js +18 -0
- package/dist/src/architecture/packs/types.js.map +1 -0
- package/dist/src/architecture/packs/web-product.d.ts +348 -0
- package/dist/src/architecture/packs/web-product.d.ts.map +1 -0
- package/dist/src/architecture/packs/web-product.js +226 -0
- package/dist/src/architecture/packs/web-product.js.map +1 -0
- package/dist/src/architecture/sequence-layout.d.ts +50 -0
- package/dist/src/architecture/sequence-layout.d.ts.map +1 -0
- package/dist/src/architecture/sequence-layout.js +140 -0
- package/dist/src/architecture/sequence-layout.js.map +1 -0
- package/dist/src/architecture/service-facts.d.ts +30 -0
- package/dist/src/architecture/service-facts.d.ts.map +1 -0
- package/dist/src/architecture/service-facts.js +88 -0
- package/dist/src/architecture/service-facts.js.map +1 -0
- package/dist/src/architecture/store.d.ts +50 -0
- package/dist/src/architecture/store.d.ts.map +1 -0
- package/dist/src/architecture/store.js +320 -0
- package/dist/src/architecture/store.js.map +1 -0
- package/dist/src/architecture/types.d.ts +400 -0
- package/dist/src/architecture/types.d.ts.map +1 -0
- package/dist/src/architecture/types.js +75 -0
- package/dist/src/architecture/types.js.map +1 -0
- package/dist/src/architecture/validator.d.ts +65 -0
- package/dist/src/architecture/validator.d.ts.map +1 -0
- package/dist/src/architecture/validator.js +1249 -0
- package/dist/src/architecture/validator.js.map +1 -0
- package/dist/src/code-graph/cochange.d.ts +40 -0
- package/dist/src/code-graph/cochange.d.ts.map +1 -1
- package/dist/src/code-graph/cochange.js +88 -3
- package/dist/src/code-graph/cochange.js.map +1 -1
- package/dist/src/code-graph/engine.d.ts +35 -1
- package/dist/src/code-graph/engine.d.ts.map +1 -1
- package/dist/src/code-graph/engine.js +300 -55
- package/dist/src/code-graph/engine.js.map +1 -1
- package/dist/src/code-graph/freshness.d.ts +39 -0
- package/dist/src/code-graph/freshness.d.ts.map +1 -0
- package/dist/src/code-graph/freshness.js +93 -0
- package/dist/src/code-graph/freshness.js.map +1 -0
- package/dist/src/code-graph/hooks/background.d.ts +18 -0
- package/dist/src/code-graph/hooks/background.d.ts.map +1 -0
- package/dist/src/code-graph/hooks/background.js +55 -0
- package/dist/src/code-graph/hooks/background.js.map +1 -0
- package/dist/src/code-graph/hooks/pointers.d.ts +66 -0
- package/dist/src/code-graph/hooks/pointers.d.ts.map +1 -0
- package/dist/src/code-graph/hooks/pointers.js +292 -0
- package/dist/src/code-graph/hooks/pointers.js.map +1 -0
- package/dist/src/code-graph/hooks/rank.d.ts +36 -0
- package/dist/src/code-graph/hooks/rank.d.ts.map +1 -0
- package/dist/src/code-graph/hooks/rank.js +212 -0
- package/dist/src/code-graph/hooks/rank.js.map +1 -0
- package/dist/src/code-graph/hooks/run.d.ts +37 -0
- package/dist/src/code-graph/hooks/run.d.ts.map +1 -0
- package/dist/src/code-graph/hooks/run.js +324 -0
- package/dist/src/code-graph/hooks/run.js.map +1 -0
- package/dist/src/code-graph/hooks/settings.d.ts +11 -0
- package/dist/src/code-graph/hooks/settings.d.ts.map +1 -0
- package/dist/src/code-graph/hooks/settings.js +25 -0
- package/dist/src/code-graph/hooks/settings.js.map +1 -0
- package/dist/src/code-graph/hooks/state.d.ts +20 -0
- package/dist/src/code-graph/hooks/state.d.ts.map +1 -0
- package/dist/src/code-graph/hooks/state.js +73 -0
- package/dist/src/code-graph/hooks/state.js.map +1 -0
- package/dist/src/code-graph/index.d.ts +6 -2
- package/dist/src/code-graph/index.d.ts.map +1 -1
- package/dist/src/code-graph/index.js +3 -1
- package/dist/src/code-graph/index.js.map +1 -1
- package/dist/src/code-graph/ko-text.d.ts +39 -0
- package/dist/src/code-graph/ko-text.d.ts.map +1 -0
- package/dist/src/code-graph/ko-text.js +223 -0
- package/dist/src/code-graph/ko-text.js.map +1 -0
- package/dist/src/code-graph/lock.d.ts +30 -0
- package/dist/src/code-graph/lock.d.ts.map +1 -0
- package/dist/src/code-graph/lock.js +140 -0
- package/dist/src/code-graph/lock.js.map +1 -0
- package/dist/src/code-graph/plugins/typescript-skeleton.d.ts +7 -0
- package/dist/src/code-graph/plugins/typescript-skeleton.d.ts.map +1 -0
- package/dist/src/code-graph/plugins/typescript-skeleton.js +157 -0
- package/dist/src/code-graph/plugins/typescript-skeleton.js.map +1 -0
- package/dist/src/code-graph/plugins/typescript.d.ts.map +1 -1
- package/dist/src/code-graph/plugins/typescript.js +58 -0
- package/dist/src/code-graph/plugins/typescript.js.map +1 -1
- package/dist/src/code-graph/storage.d.ts +85 -0
- package/dist/src/code-graph/storage.d.ts.map +1 -1
- package/dist/src/code-graph/storage.js +318 -3
- package/dist/src/code-graph/storage.js.map +1 -1
- package/dist/src/code-graph/types.d.ts +66 -0
- package/dist/src/code-graph/types.d.ts.map +1 -1
- package/dist/src/core/config.d.ts +33 -0
- package/dist/src/core/config.d.ts.map +1 -1
- package/dist/src/core/config.js +14 -0
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/errors.d.ts +4 -0
- package/dist/src/core/errors.d.ts.map +1 -1
- package/dist/src/core/errors.js +8 -0
- package/dist/src/core/errors.js.map +1 -1
- package/dist/src/core/hooks-env.d.ts +9 -0
- package/dist/src/core/hooks-env.d.ts.map +1 -0
- package/dist/src/core/hooks-env.js +16 -0
- package/dist/src/core/hooks-env.js.map +1 -0
- package/dist/src/mcp/schemas.d.ts +326 -3
- package/dist/src/mcp/schemas.d.ts.map +1 -1
- package/dist/src/mcp/schemas.js +170 -2
- package/dist/src/mcp/schemas.js.map +1 -1
- package/dist/src/mcp/server.d.ts.map +1 -1
- package/dist/src/mcp/server.js +28 -4
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/mcp/tools/architecture-passthrough.d.ts +9 -0
- package/dist/src/mcp/tools/architecture-passthrough.d.ts.map +1 -0
- package/dist/src/mcp/tools/architecture-passthrough.js +587 -0
- package/dist/src/mcp/tools/architecture-passthrough.js.map +1 -0
- package/dist/src/mcp/tools/code-graph-passthrough.d.ts +6 -1
- package/dist/src/mcp/tools/code-graph-passthrough.d.ts.map +1 -1
- package/dist/src/mcp/tools/code-graph-passthrough.js +95 -6
- package/dist/src/mcp/tools/code-graph-passthrough.js.map +1 -1
- package/dist/src/utils/claude-projects.d.ts +19 -0
- package/dist/src/utils/claude-projects.d.ts.map +1 -0
- package/dist/src/utils/claude-projects.js +157 -0
- package/dist/src/utils/claude-projects.js.map +1 -0
- package/dist/src/utils/read-only-tools.d.ts +19 -0
- package/dist/src/utils/read-only-tools.d.ts.map +1 -0
- package/dist/src/utils/read-only-tools.js +132 -0
- package/dist/src/utils/read-only-tools.js.map +1 -0
- package/package.json +5 -3
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +1 -1
- package/plugin/mcp.json +1 -1
- package/plugin/skills/_shared/proactive-routing.md +1 -0
- package/plugin/skills/architecture/SKILL.md +913 -0
- package/plugin/skills/blast-radius/SKILL.md +11 -1
- package/plugin/skills/diff-radius/SKILL.md +11 -0
- package/schemas/architecture-ir.schema.json +596 -0
- package/schemas/gestalt.schema.json +19 -0
|
@@ -0,0 +1,1160 @@
|
|
|
1
|
+
import { assignStages, computeGraphLayout, environmentRank, laneOfNode, PARTITION_RANK, pinToColumnTop, stackTree, alignColumnsTo, sinkToColumnBottom, stackBands, bandGroupsOf, stackFrames, frameRects, } from './layout.js';
|
|
2
|
+
import { computeFlowLevels } from './flow-layout.js';
|
|
3
|
+
import { indexMicroApps, orderedApps } from './micro-app.js';
|
|
4
|
+
import { irVocabulary } from './packs/index.js';
|
|
5
|
+
import { computeServiceFacts } from './service-facts.js';
|
|
6
|
+
import { chipTextOverride, displayKindOf } from './types.js';
|
|
7
|
+
export const ROOT_LEVEL_ID = 'root';
|
|
8
|
+
// 클라이언트가 받는 쪽 모듈은 보내는 모듈보다 오른쪽에 서야 화살표가 거꾸로 꺾이지 않는다.
|
|
9
|
+
// 사슬 단수만큼 더해 가므로 테이블 순위와 겹치지 않게 넉넉히 띄운다
|
|
10
|
+
const RECEIVER_RANK = 10;
|
|
11
|
+
const TABLE_RANK_AFTER_RECEIVERS = 100;
|
|
12
|
+
// 전체 레벨에서 서버를 거쳐서만 닿는 게이트웨이와 모듈. 같은 레인 안에서도 게이트웨이가 왼쪽에 선다
|
|
13
|
+
// tree 드릴다운에서 바깥 카드 한 열에 세우는 최대 장수
|
|
14
|
+
const OUTSIDE_PER_COLUMN = 4;
|
|
15
|
+
const EXTERNAL_GATEWAY_RANK = PARTITION_RANK.app_module + 1;
|
|
16
|
+
const EXTERNAL_MODULE_RANK = PARTITION_RANK.app_module + 2;
|
|
17
|
+
// 저장소는 외부 서비스 레인 오른쪽 끝에 선다. 외부 서버도 같은 저장소를 쓰므로 그보다 뒤여야 선이 거꾸로 안 간다
|
|
18
|
+
const DATASTORE_ROOT_RANK = EXTERNAL_MODULE_RANK + 1;
|
|
19
|
+
function compareStr(a, b) {
|
|
20
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
21
|
+
}
|
|
22
|
+
function byId(a, b) {
|
|
23
|
+
return compareStr(a.id, b.id);
|
|
24
|
+
}
|
|
25
|
+
function buildGraph(validated) {
|
|
26
|
+
const { ir, drawableNodeIds, drawableEdgeIds } = validated;
|
|
27
|
+
const nodes = ir.nodes.filter((n) => drawableNodeIds.has(n.id));
|
|
28
|
+
const nodeById = new Map(nodes.map((n) => [n.id, n]));
|
|
29
|
+
const edges = ir.edges.filter((e) => drawableEdgeIds.has(e.id)).sort(byId);
|
|
30
|
+
const edgeById = new Map(edges.map((e) => [e.id, e]));
|
|
31
|
+
// 부모가 그려지지 않으면 자식은 부모 없는 노드로 친다. 안 그린 노드 아래로 들어가는 길을 만들지 않기 위해서다
|
|
32
|
+
const parentOf = new Map();
|
|
33
|
+
for (const n of nodes) {
|
|
34
|
+
if (n.parent !== undefined && nodeById.has(n.parent))
|
|
35
|
+
parentOf.set(n.id, n.parent);
|
|
36
|
+
}
|
|
37
|
+
return { nodeById, edgeById, edges, parentOf };
|
|
38
|
+
}
|
|
39
|
+
function kindOf(g, id) {
|
|
40
|
+
return g.nodeById.get(id)?.kind;
|
|
41
|
+
}
|
|
42
|
+
/** 가장 가까운 micro_app이나 service 조상. 화면과 기능 영역이 어느 레벨 칸에 서는지 이걸로 정한다 */
|
|
43
|
+
function ownerOf(g, id) {
|
|
44
|
+
let cursor = g.parentOf.get(id);
|
|
45
|
+
const seen = new Set([id]);
|
|
46
|
+
while (cursor !== undefined && !seen.has(cursor)) {
|
|
47
|
+
const kind = kindOf(g, cursor);
|
|
48
|
+
if (kind === 'micro_app' || kind === 'service')
|
|
49
|
+
return cursor;
|
|
50
|
+
seen.add(cursor);
|
|
51
|
+
cursor = g.parentOf.get(cursor);
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
function featureOf(g, screenId) {
|
|
56
|
+
const parent = g.parentOf.get(screenId);
|
|
57
|
+
return parent !== undefined && kindOf(g, parent) === 'feature' ? parent : undefined;
|
|
58
|
+
}
|
|
59
|
+
/** 서비스나 앱 레벨에서 화면이 속하는 칸. feature 없이 서비스나 앱에 바로 달린 화면은 자기 자신이 칸이다 */
|
|
60
|
+
function unitOf(g, screenId) {
|
|
61
|
+
const feature = featureOf(g, screenId);
|
|
62
|
+
if (feature !== undefined)
|
|
63
|
+
return feature;
|
|
64
|
+
const parentKind = kindOf(g, g.parentOf.get(screenId) ?? '');
|
|
65
|
+
return parentKind === 'service' || parentKind === 'micro_app' ? screenId : undefined;
|
|
66
|
+
}
|
|
67
|
+
function edgesOf(g, kind, fromKind, toKind) {
|
|
68
|
+
return g.edges.filter((e) => e.kind === kind && kindOf(g, e.from) === fromKind && kindOf(g, e.to) === toKind);
|
|
69
|
+
}
|
|
70
|
+
/** 화면 자리에 서는 카드. 하네스의 스킬은 사용자가 고르는 입구라 웹의 화면과 같은 자리에 둔다 */
|
|
71
|
+
function isSurface(g, id) {
|
|
72
|
+
const kind = kindOf(g, id);
|
|
73
|
+
return kind === 'screen' || kind === 'skill';
|
|
74
|
+
}
|
|
75
|
+
/** 엔드포인트를 부르는 선. 화면과 스킬, 에이전트가 부른다 */
|
|
76
|
+
function screenCalls(g) {
|
|
77
|
+
return g.edges.filter((e) => e.kind === 'calls' &&
|
|
78
|
+
kindOf(g, e.to) === 'endpoint' &&
|
|
79
|
+
(isSurface(g, e.from) || kindOf(g, e.from) === 'agent'));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 에이전트를 띄운 스킬까지 spawns를 거슬러 올라간다. 에이전트는 소유자가 없어서 묶을 때 띄운 스킬의 소유자를 빌린다.
|
|
83
|
+
* 아무도 안 띄운 에이전트나 화면, 스킬은 자기 자신을 낸다
|
|
84
|
+
*/
|
|
85
|
+
function callerRoots(g, id) {
|
|
86
|
+
if (kindOf(g, id) !== 'agent')
|
|
87
|
+
return [{ id, via: [] }];
|
|
88
|
+
const out = [];
|
|
89
|
+
const seen = new Set([id]);
|
|
90
|
+
const queue = [{ id, via: [] }];
|
|
91
|
+
while (queue.length > 0) {
|
|
92
|
+
const at = queue.shift();
|
|
93
|
+
const ups = g.edges.filter((e) => e.kind === 'spawns' && e.to === at.id);
|
|
94
|
+
if (ups.length === 0 && at.id !== id)
|
|
95
|
+
out.push(at);
|
|
96
|
+
for (const e of ups) {
|
|
97
|
+
const via = [e.id, ...at.via];
|
|
98
|
+
if (kindOf(g, e.from) === 'skill')
|
|
99
|
+
out.push({ id: e.from, via });
|
|
100
|
+
else if (!seen.has(e.from)) {
|
|
101
|
+
seen.add(e.from);
|
|
102
|
+
queue.push({ id: e.from, via });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return out.length > 0 ? out : [{ id, via: [] }];
|
|
107
|
+
}
|
|
108
|
+
/** 띄운 쪽에서 spawns를 따라 내려가 닿는 에이전트와 그 선 */
|
|
109
|
+
function spawnedFrom(g, starts) {
|
|
110
|
+
const out = [];
|
|
111
|
+
const seen = new Set(starts);
|
|
112
|
+
const queue = [...seen];
|
|
113
|
+
while (queue.length > 0) {
|
|
114
|
+
const at = queue.shift();
|
|
115
|
+
for (const e of g.edges.filter((x) => x.kind === 'spawns' && x.from === at)) {
|
|
116
|
+
out.push(e);
|
|
117
|
+
if (seen.has(e.to))
|
|
118
|
+
continue;
|
|
119
|
+
seen.add(e.to);
|
|
120
|
+
queue.push(e.to);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
function routesInto(g, endpointId) {
|
|
126
|
+
return g.edges.filter((e) => e.kind === 'routes' && e.to === endpointId && kindOf(g, e.from) === 'gateway');
|
|
127
|
+
}
|
|
128
|
+
function handlersOf(g, endpointId) {
|
|
129
|
+
return g.edges.filter((e) => e.kind === 'handles' && e.from === endpointId && kindOf(g, e.to) === 'app_module');
|
|
130
|
+
}
|
|
131
|
+
function gatewayHops(g) {
|
|
132
|
+
return edgesOf(g, 'routes', 'gateway', 'gateway');
|
|
133
|
+
}
|
|
134
|
+
/** 게이트웨이에서 gateway → gateway routes를 거꾸로 따라 올라가 맨 앞 게이트웨이까지의 사슬을 모두 낸다 */
|
|
135
|
+
function chainsUp(g, gatewayId) {
|
|
136
|
+
const out = [];
|
|
137
|
+
const hops = gatewayHops(g);
|
|
138
|
+
const walk = (at, path, seen) => {
|
|
139
|
+
const incoming = hops.filter((e) => e.to === at && !seen.has(e.from));
|
|
140
|
+
if (incoming.length === 0) {
|
|
141
|
+
out.push({ head: at, hops: path });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
for (const e of incoming)
|
|
145
|
+
walk(e.from, [...path, e], new Set([...seen, e.from]));
|
|
146
|
+
};
|
|
147
|
+
walk(gatewayId, [], new Set([gatewayId]));
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
function ids(edges) {
|
|
151
|
+
return edges.map((e) => e.id);
|
|
152
|
+
}
|
|
153
|
+
class Bundler {
|
|
154
|
+
bundles = new Map();
|
|
155
|
+
/** key는 묶음 안에서 건수로 셀 단위다. 같은 key가 여러 번 들어와도 한 건이다 */
|
|
156
|
+
add(from, to, key, members) {
|
|
157
|
+
if (from === to)
|
|
158
|
+
return;
|
|
159
|
+
const id = `bundle:${from}->${to}`;
|
|
160
|
+
let b = this.bundles.get(id);
|
|
161
|
+
if (!b) {
|
|
162
|
+
b = { from, to, keys: new Set(), members: new Set() };
|
|
163
|
+
this.bundles.set(id, b);
|
|
164
|
+
}
|
|
165
|
+
b.keys.add(key);
|
|
166
|
+
for (const m of members)
|
|
167
|
+
b.members.add(m);
|
|
168
|
+
}
|
|
169
|
+
toEdges(g) {
|
|
170
|
+
return [...this.bundles.entries()]
|
|
171
|
+
.map(([id, b]) => {
|
|
172
|
+
const memberEdgeIds = [...b.members].sort(compareStr);
|
|
173
|
+
// 코드로 확인된 고리가 하나라도 있으면 실선이다. 문서나 사용자 근거만 섞였다는 건 inferred 건수로 따로 알린다
|
|
174
|
+
const inferred = memberEdgeIds.filter((m) => g.edgeById.get(m)?.lineStyle === 'dashed').length;
|
|
175
|
+
const allInferred = inferred === memberEdgeIds.length;
|
|
176
|
+
return {
|
|
177
|
+
id,
|
|
178
|
+
from: b.from,
|
|
179
|
+
to: b.to,
|
|
180
|
+
kind: 'bundle',
|
|
181
|
+
count: b.keys.size,
|
|
182
|
+
memberEdgeIds,
|
|
183
|
+
lineStyle: allInferred ? 'dashed' : 'solid',
|
|
184
|
+
...(inferred > 0 && !allInferred ? { inferred } : {}),
|
|
185
|
+
};
|
|
186
|
+
})
|
|
187
|
+
.sort(byId);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function asDetail(edge) {
|
|
191
|
+
return {
|
|
192
|
+
id: edge.id,
|
|
193
|
+
from: edge.from,
|
|
194
|
+
to: edge.to,
|
|
195
|
+
kind: edge.kind,
|
|
196
|
+
count: 1,
|
|
197
|
+
memberEdgeIds: [edge.id],
|
|
198
|
+
lineStyle: edge.lineStyle,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* 화면 호출을 소유자 단위로 묶는다. 게이트웨이를 거치면 소유자 → 게이트웨이, 아니면 소유자 → 모듈이다.
|
|
203
|
+
* withGatewayHops면 거친 게이트웨이 → 모듈 묶음도 이 호출들로만 만든다.
|
|
204
|
+
*/
|
|
205
|
+
/**
|
|
206
|
+
* 화면 호출을 소유자 단위로 묶는다. 게이트웨이를 거치면 소유자 → 사슬 맨 앞 게이트웨이, 아니면 소유자 → 모듈이다.
|
|
207
|
+
* withGatewayHops면 사슬 안 게이트웨이끼리와 맨 뒤 게이트웨이 → 모듈 묶음도 이 호출들로만 만든다.
|
|
208
|
+
*/
|
|
209
|
+
function bundleCalls(g, b, calls, ownerOf, withGatewayHops, liftAgents = true) {
|
|
210
|
+
for (const call of calls) {
|
|
211
|
+
const roots = liftAgents ? callerRoots(g, call.from) : [{ id: call.from, via: [] }];
|
|
212
|
+
for (const root of roots) {
|
|
213
|
+
const owner = ownerOf(root.id);
|
|
214
|
+
if (owner === undefined)
|
|
215
|
+
continue;
|
|
216
|
+
bundleCall(g, b, call, owner, root.via, withGatewayHops);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function bundleCall(g, b, call, owner, via, withGatewayHops) {
|
|
221
|
+
const routes = routesInto(g, call.to);
|
|
222
|
+
const handlers = handlersOf(g, call.to);
|
|
223
|
+
if (routes.length === 0) {
|
|
224
|
+
for (const h of handlers)
|
|
225
|
+
b.add(owner, h.to, call.id, [...via, call.id, h.id]);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
for (const r of routes) {
|
|
229
|
+
for (const chain of chainsUp(g, r.from)) {
|
|
230
|
+
b.add(owner, chain.head, call.id, [...via, call.id, r.id, ...ids(chain.hops)]);
|
|
231
|
+
if (!withGatewayHops)
|
|
232
|
+
continue;
|
|
233
|
+
for (const hop of chain.hops)
|
|
234
|
+
b.add(hop.from, hop.to, hop.id, [hop.id]);
|
|
235
|
+
}
|
|
236
|
+
if (!withGatewayHops)
|
|
237
|
+
continue;
|
|
238
|
+
for (const h of handlers)
|
|
239
|
+
b.add(r.from, h.to, h.id, [r.id, h.id]);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/** 게이트웨이가 넘기는 곳을 묶는다. 사슬 안 다음 게이트웨이, 엔드포인트를 받는 모듈, 펼치지 않은 모듈 셋이다 */
|
|
243
|
+
function bundleGatewayRoutes(g, b) {
|
|
244
|
+
for (const hop of gatewayHops(g))
|
|
245
|
+
b.add(hop.from, hop.to, hop.id, [hop.id]);
|
|
246
|
+
for (const r of edgesOf(g, 'routes', 'gateway', 'endpoint')) {
|
|
247
|
+
for (const h of handlersOf(g, r.to))
|
|
248
|
+
b.add(r.from, h.to, h.id, [r.id, h.id]);
|
|
249
|
+
}
|
|
250
|
+
for (const r of edgesOf(g, 'routes', 'gateway', 'app_module'))
|
|
251
|
+
b.add(r.from, r.to, r.id, [r.id]);
|
|
252
|
+
}
|
|
253
|
+
function bundleModuleToModule(g, b) {
|
|
254
|
+
for (const use of edgesOf(g, 'uses', 'app_module', 'external_service')) {
|
|
255
|
+
for (const call of g.edges.filter((e) => e.kind === 'calls' && e.from === use.to)) {
|
|
256
|
+
const targetKind = kindOf(g, call.to);
|
|
257
|
+
if (targetKind === 'app_module') {
|
|
258
|
+
b.add(use.from, call.to, call.id, [use.id, call.id]);
|
|
259
|
+
}
|
|
260
|
+
else if (targetKind === 'endpoint') {
|
|
261
|
+
for (const h of handlersOf(g, call.to)) {
|
|
262
|
+
b.add(use.from, h.to, call.id, [use.id, call.id, h.id]);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
else if (targetKind === 'gateway') {
|
|
266
|
+
// 게이트웨이 뒤쪽 사슬은 bundleGatewayRoutes가 따로 묶는다
|
|
267
|
+
b.add(use.from, call.to, call.id, [use.id, call.id]);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/** 클라이언트가 부른 게이트웨이에서 routes를 앞으로 따라간다. 받는 쪽 노드마다 단계 수를 같이 낸다 */
|
|
273
|
+
function routesDown(g, gatewayId) {
|
|
274
|
+
const edges = [];
|
|
275
|
+
const depth = new Map([[gatewayId, 0]]);
|
|
276
|
+
const queue = [gatewayId];
|
|
277
|
+
while (queue.length > 0) {
|
|
278
|
+
const at = queue.shift();
|
|
279
|
+
const d = depth.get(at);
|
|
280
|
+
for (const r of g.edges.filter((e) => e.kind === 'routes' && e.from === at)) {
|
|
281
|
+
edges.push(r);
|
|
282
|
+
const targetKind = kindOf(g, r.to);
|
|
283
|
+
if (!depth.has(r.to))
|
|
284
|
+
depth.set(r.to, d + 1);
|
|
285
|
+
if (targetKind === 'gateway' && depth.get(r.to) === d + 1)
|
|
286
|
+
queue.push(r.to);
|
|
287
|
+
if (targetKind === 'endpoint') {
|
|
288
|
+
for (const h of handlersOf(g, r.to)) {
|
|
289
|
+
edges.push(h);
|
|
290
|
+
if (!depth.has(h.to))
|
|
291
|
+
depth.set(h.to, d + 2);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return { edges, depth };
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* 서비스에서 출발해 게이트웨이를 거쳐 닿는 노드. 서버에서 나가는 선은 따라가지 않는다.
|
|
300
|
+
* 다만 inProcess 선은 한 프로세스 안에서 핸들러가 엔진을 부르는 것이라 외부로 치지 않고 따라간다
|
|
301
|
+
*/
|
|
302
|
+
function frontReach(g, edges, inProcess = new Set()) {
|
|
303
|
+
const front = new Set([...g.nodeById.values()].filter((n) => n.kind === 'service').map((n) => n.id));
|
|
304
|
+
// 스킬이 안 부르고 세션이 바로 부르는 도구의 핸들러는 서비스에서 안 닿는다. 그 핸들러가 부르는 엔진도 같은 프로세스라 출발점에 넣는다
|
|
305
|
+
for (const e of edges)
|
|
306
|
+
if (inProcess.has(e.id))
|
|
307
|
+
front.add(e.from);
|
|
308
|
+
const queue = [...front];
|
|
309
|
+
while (queue.length > 0) {
|
|
310
|
+
const at = queue.shift();
|
|
311
|
+
const kind = kindOf(g, at);
|
|
312
|
+
const open = kind === 'service' || kind === 'gateway';
|
|
313
|
+
for (const e of edges) {
|
|
314
|
+
if (e.from !== at || front.has(e.to))
|
|
315
|
+
continue;
|
|
316
|
+
if (!open && !inProcess.has(e.id))
|
|
317
|
+
continue;
|
|
318
|
+
front.add(e.to);
|
|
319
|
+
queue.push(e.to);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return front;
|
|
323
|
+
}
|
|
324
|
+
/** 서버에서 나가는 선으로만 닿는 노드. 서비스 쪽에서 닿는 노드는 뺀다 */
|
|
325
|
+
function externalReach(g, edges, front) {
|
|
326
|
+
const ext = new Set();
|
|
327
|
+
const queue = [];
|
|
328
|
+
for (const e of edges) {
|
|
329
|
+
if (kindOf(g, e.from) !== 'app_module' || front.has(e.to) || ext.has(e.to))
|
|
330
|
+
continue;
|
|
331
|
+
ext.add(e.to);
|
|
332
|
+
queue.push(e.to);
|
|
333
|
+
}
|
|
334
|
+
while (queue.length > 0) {
|
|
335
|
+
const at = queue.shift();
|
|
336
|
+
for (const e of edges) {
|
|
337
|
+
if (e.from !== at || front.has(e.to) || ext.has(e.to))
|
|
338
|
+
continue;
|
|
339
|
+
ext.add(e.to);
|
|
340
|
+
queue.push(e.to);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return ext;
|
|
344
|
+
}
|
|
345
|
+
const ROOT_KINDS = new Set([
|
|
346
|
+
'client',
|
|
347
|
+
'service',
|
|
348
|
+
'micro_app',
|
|
349
|
+
'gateway',
|
|
350
|
+
'app_module',
|
|
351
|
+
'datastore',
|
|
352
|
+
]);
|
|
353
|
+
/** 환경이 prod이거나 비어 있는 노드. 환경을 모르는 노드는 prod 쪽으로 친다 */
|
|
354
|
+
function isProdOrUnknown(n) {
|
|
355
|
+
return n.environment === undefined || n.environment === 'prod';
|
|
356
|
+
}
|
|
357
|
+
/** prod가 아닌 노드와 거기 걸린 엣지를 뺀 그래프. 전체보기는 이걸로 묶어 dev 경로가 묶음 선 건수에 안 섞이게 한다 */
|
|
358
|
+
function prodGraph(g) {
|
|
359
|
+
const nodeById = new Map([...g.nodeById].filter(([, n]) => isProdOrUnknown(n)));
|
|
360
|
+
const edges = g.edges.filter((e) => nodeById.has(e.from) && nodeById.has(e.to));
|
|
361
|
+
const edgeById = new Map(edges.map((e) => [e.id, e]));
|
|
362
|
+
const parentOf = new Map([...g.parentOf].filter(([c, p]) => nodeById.has(c) && nodeById.has(p)));
|
|
363
|
+
return { nodeById, edgeById, edges, parentOf };
|
|
364
|
+
}
|
|
365
|
+
/** 서버가 읽고 쓰는 저장소. 테이블에 걸린 선은 그 테이블이 속한 저장소로 올려 묶는다 */
|
|
366
|
+
/** 핸들러 모듈이 같은 프로세스의 엔진 모듈을 쓰는 선. 하네스 MCP 서버에서 도구 핸들러가 엔진을 부르는 자리다 */
|
|
367
|
+
function moduleUses(g) {
|
|
368
|
+
return edgesOf(g, 'uses', 'app_module', 'app_module');
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* 아무도 calls로 부르지 않는 MCP 도구를 parent 서비스에서 핸들러로 잇는다. 스킬 없는 MCP 서버 레포는 클라이언트가 도구를 바로 불러
|
|
372
|
+
* 서비스에서 서버로 가는 선이 하나도 안 생기기 때문이다. 스킬이 부르는 도구는 그 호출로 이미 묶이니 건수가 두 번 세지지 않게 뺀다
|
|
373
|
+
*/
|
|
374
|
+
function bundleDirectTools(g, b) {
|
|
375
|
+
const called = new Set(g.edges.filter((e) => e.kind === 'calls').map((e) => e.to));
|
|
376
|
+
for (const n of g.nodeById.values()) {
|
|
377
|
+
if (n.kind !== 'endpoint' || n.protocol !== 'mcp' || called.has(n.id))
|
|
378
|
+
continue;
|
|
379
|
+
const owner = g.parentOf.get(n.id);
|
|
380
|
+
if (owner === undefined || kindOf(g, owner) !== 'service')
|
|
381
|
+
continue;
|
|
382
|
+
for (const h of handlersOf(g, n.id))
|
|
383
|
+
b.add(owner, h.to, n.id, [h.id]);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* MCP 도구 핸들러가 같은 프로세스에서 부르는 엔진 모듈. 전체 그림에서 핸들러와 엔진을 한 열에 두면 elk가 나가는 선 없는 핸들러를
|
|
388
|
+
* 엔진 층으로 밀어 버려 엔진에 따로 한 열을 준다. 핸들러를 겸하는 모듈은 핸들러 열에 남긴다
|
|
389
|
+
*/
|
|
390
|
+
function mcpEngines(g) {
|
|
391
|
+
const handlers = new Set(g.edges
|
|
392
|
+
.filter((e) => e.kind === 'handles' && g.nodeById.get(e.from)?.protocol === 'mcp')
|
|
393
|
+
.filter((e) => kindOf(g, e.to) === 'app_module')
|
|
394
|
+
.map((e) => e.to));
|
|
395
|
+
const engines = new Set();
|
|
396
|
+
const queue = [...handlers];
|
|
397
|
+
const uses = moduleUses(g);
|
|
398
|
+
while (queue.length > 0) {
|
|
399
|
+
const at = queue.shift();
|
|
400
|
+
for (const u of uses) {
|
|
401
|
+
if (u.from !== at || handlers.has(u.to) || engines.has(u.to))
|
|
402
|
+
continue;
|
|
403
|
+
engines.add(u.to);
|
|
404
|
+
queue.push(u.to);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return engines;
|
|
408
|
+
}
|
|
409
|
+
function bundleDatastores(g, b) {
|
|
410
|
+
for (const e of g.edges) {
|
|
411
|
+
if (e.kind !== 'reads_writes' || kindOf(g, e.from) !== 'app_module')
|
|
412
|
+
continue;
|
|
413
|
+
const toKind = kindOf(g, e.to);
|
|
414
|
+
const store = toKind === 'datastore' ? e.to : toKind === 'db_table' ? g.parentOf.get(e.to) : undefined;
|
|
415
|
+
if (store === undefined || kindOf(g, store) !== 'datastore')
|
|
416
|
+
continue;
|
|
417
|
+
b.add(e.from, store, e.id, [e.id]);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function rootLevel(full, apps) {
|
|
421
|
+
const g = prodGraph(full);
|
|
422
|
+
const nodeIds = new Set([...g.nodeById.values()].filter((n) => ROOT_KINDS.has(n.kind)).map((n) => n.id));
|
|
423
|
+
// 범용 component는 묶을 위계가 없다. 부모 없는 것만 전체에 세우고 그 사이 선은 그대로 긋는다
|
|
424
|
+
const components = new Set([...g.nodeById.values()]
|
|
425
|
+
.filter((n) => n.kind === 'component' && !g.parentOf.has(n.id))
|
|
426
|
+
.map((n) => n.id));
|
|
427
|
+
for (const id of components)
|
|
428
|
+
nodeIds.add(id);
|
|
429
|
+
const b = new Bundler();
|
|
430
|
+
bundleCalls(g, b, screenCalls(g), (s) => ownerOf(g, s), false);
|
|
431
|
+
bundleGatewayRoutes(g, b);
|
|
432
|
+
bundleModuleToModule(g, b);
|
|
433
|
+
bundleDirectTools(g, b);
|
|
434
|
+
const inProcessUses = new Set();
|
|
435
|
+
for (const u of moduleUses(g)) {
|
|
436
|
+
b.add(u.from, u.to, u.id, [u.id]);
|
|
437
|
+
inProcessUses.add(`bundle:${u.from}->${u.to}`);
|
|
438
|
+
}
|
|
439
|
+
bundleDatastores(g, b);
|
|
440
|
+
// 클라이언트가 플러그인을 싣는 선은 테두리를 넘는 일이 없어 그대로 그린다
|
|
441
|
+
const clientLoads = g.edges
|
|
442
|
+
.filter((e) => e.kind === 'loads' && kindOf(g, e.from) === 'client' && nodeIds.has(e.to))
|
|
443
|
+
.map(asDetail);
|
|
444
|
+
// 같은 서비스 안 로드는 테두리가 이미 말해준다. 테두리를 넘는 로드(여러 호스트가 같이 쓰는 리모트)만 선으로 남긴다
|
|
445
|
+
const crossing = apps.loads
|
|
446
|
+
.filter((e) => nodeIds.has(e.from) && nodeIds.has(e.to))
|
|
447
|
+
.filter((e) => g.parentOf.get(e.from) !== g.parentOf.get(e.to))
|
|
448
|
+
.map(asDetail);
|
|
449
|
+
const componentEdges = g.edges
|
|
450
|
+
.filter((e) => components.has(e.from) && components.has(e.to))
|
|
451
|
+
.map(asDetail);
|
|
452
|
+
const edges = [...b.toEdges(g), ...crossing, ...clientLoads, ...componentEdges].sort(byId);
|
|
453
|
+
const frames = [...apps.appsOf.keys()]
|
|
454
|
+
.filter((svc) => nodeIds.has(svc))
|
|
455
|
+
.sort(compareStr)
|
|
456
|
+
.map((svc) => ({ head: svc, members: orderedApps(apps, svc).filter((a) => nodeIds.has(a)) }));
|
|
457
|
+
const ext = externalReach(g, edges, frontReach(g, edges, inProcessUses));
|
|
458
|
+
const rankOverride = new Map();
|
|
459
|
+
const laneOverride = new Map();
|
|
460
|
+
const engines = mcpEngines(g);
|
|
461
|
+
// 엔진에 한 열을 내주면 그 뒤 열이 한 칸씩 밀린다. MCP 핸들러가 없는 그림은 예전 열 번호 그대로다
|
|
462
|
+
const shift = engines.size > 0 ? 1 : 0;
|
|
463
|
+
for (const id of engines)
|
|
464
|
+
if (!ext.has(id))
|
|
465
|
+
rankOverride.set(id, PARTITION_RANK.app_module + 1);
|
|
466
|
+
for (const id of ext) {
|
|
467
|
+
const kind = kindOf(g, id);
|
|
468
|
+
if (kind === 'datastore') {
|
|
469
|
+
rankOverride.set(id, DATASTORE_ROOT_RANK + shift);
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (kind !== 'gateway' && kind !== 'app_module')
|
|
473
|
+
continue;
|
|
474
|
+
rankOverride.set(id, (kind === 'gateway' ? EXTERNAL_GATEWAY_RANK : EXTERNAL_MODULE_RANK) + shift);
|
|
475
|
+
laneOverride.set(id, 'external');
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
id: ROOT_LEVEL_ID,
|
|
479
|
+
kind: 'root',
|
|
480
|
+
title: '전체',
|
|
481
|
+
trail: [ROOT_LEVEL_ID],
|
|
482
|
+
nodeIds,
|
|
483
|
+
edges,
|
|
484
|
+
rankOverride,
|
|
485
|
+
laneOverride,
|
|
486
|
+
laneOfKind: { service: 'service', micro_app: 'service' },
|
|
487
|
+
...(frames.length > 0 ? { frames } : {}),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* 진입 앱을 못 정한 마이크로 프론트엔드 서비스 레벨. 앱 카드를 늘어놓고 하나씩 들어가게 한다.
|
|
492
|
+
* 어느 앱이 위계 맨 위인지 모르니 treeLevel처럼 한 나무로 세우지 못한다
|
|
493
|
+
*/
|
|
494
|
+
function ownerLevel(g, owner, kind, trail, extra = {}) {
|
|
495
|
+
const id = trail[trail.length - 1];
|
|
496
|
+
const children = extra.children ?? [];
|
|
497
|
+
const nodeIds = new Set();
|
|
498
|
+
for (const n of g.nodeById.values()) {
|
|
499
|
+
if (n.kind === 'feature' && g.parentOf.get(n.id) === owner.id)
|
|
500
|
+
nodeIds.add(n.id);
|
|
501
|
+
if (isSurface(g, n.id) && g.parentOf.get(n.id) === owner.id)
|
|
502
|
+
nodeIds.add(n.id);
|
|
503
|
+
}
|
|
504
|
+
const b = new Bundler();
|
|
505
|
+
const mine = (screenId) => {
|
|
506
|
+
const unit = unitOf(g, screenId);
|
|
507
|
+
return unit !== undefined && ownerOf(g, unit) === owner.id;
|
|
508
|
+
};
|
|
509
|
+
const calls = screenCalls(g).filter((c) => mine(c.from));
|
|
510
|
+
bundleCalls(g, b, calls, (s) => unitOf(g, s), true);
|
|
511
|
+
for (const nav of edgesOf(g, 'navigates', 'screen', 'screen')) {
|
|
512
|
+
if (!mine(nav.from) || !mine(nav.to))
|
|
513
|
+
continue;
|
|
514
|
+
const from = unitOf(g, nav.from);
|
|
515
|
+
const to = unitOf(g, nav.to);
|
|
516
|
+
if (from !== undefined && to !== undefined)
|
|
517
|
+
b.add(from, to, nav.id, [nav.id]);
|
|
518
|
+
}
|
|
519
|
+
const edges = b.toEdges(g);
|
|
520
|
+
for (const e of edges) {
|
|
521
|
+
nodeIds.add(e.from);
|
|
522
|
+
nodeIds.add(e.to);
|
|
523
|
+
}
|
|
524
|
+
const infra = infraChainOf(g, owner.id);
|
|
525
|
+
const base = {
|
|
526
|
+
id,
|
|
527
|
+
kind,
|
|
528
|
+
focusId: owner.id,
|
|
529
|
+
title: owner.displayName ?? owner.label,
|
|
530
|
+
trail,
|
|
531
|
+
nodeIds,
|
|
532
|
+
edges,
|
|
533
|
+
laneOfKind: { feature: 'unit', screen: 'unit', skill: 'unit' },
|
|
534
|
+
};
|
|
535
|
+
if (infra.length === 0 && children.length === 0)
|
|
536
|
+
return base;
|
|
537
|
+
// 서빙 인프라가 붙으면 도메인부터 서버까지 한 줄로 읽히게 소유자 카드를 기능 영역 레인 맨 위에 세우고
|
|
538
|
+
// 포함 선으로 기능 영역에 잇는다. 포커스가 이 선을 타고 도메인에서 서버까지 이어진다
|
|
539
|
+
const units = [...nodeIds].filter((n) => g.parentOf.get(n) === owner.id).sort(compareStr);
|
|
540
|
+
const contains = units.map((u) => ({
|
|
541
|
+
id: `contains:${owner.id}->${u}`,
|
|
542
|
+
from: owner.id,
|
|
543
|
+
to: u,
|
|
544
|
+
kind: 'contains',
|
|
545
|
+
count: 1,
|
|
546
|
+
memberEdgeIds: [],
|
|
547
|
+
lineStyle: 'solid',
|
|
548
|
+
}));
|
|
549
|
+
const rankOverride = new Map();
|
|
550
|
+
for (const n of nodeIds)
|
|
551
|
+
rankOverride.set(n, PARTITION_RANK[kindOf(g, n)] + INFRA_RANK_SHIFT);
|
|
552
|
+
rankOverride.set(owner.id, PARTITION_RANK.service + INFRA_RANK_SHIFT);
|
|
553
|
+
nodeIds.add(owner.id);
|
|
554
|
+
// 앱 카드는 기능 영역과 같은 열에 세운다. 소유자 아래 한 단계라는 점이 기능 영역과 같아서다
|
|
555
|
+
for (const c of children) {
|
|
556
|
+
nodeIds.add(c.to);
|
|
557
|
+
rankOverride.set(c.to, PARTITION_RANK.feature + INFRA_RANK_SHIFT);
|
|
558
|
+
}
|
|
559
|
+
for (const e of infra) {
|
|
560
|
+
for (const n of [e.from, e.to]) {
|
|
561
|
+
nodeIds.add(n);
|
|
562
|
+
const r = SERVICE_INFRA_RANK[kindOf(g, n)];
|
|
563
|
+
if (r !== undefined)
|
|
564
|
+
rankOverride.set(n, r);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return {
|
|
568
|
+
...base,
|
|
569
|
+
edges: [...edges, ...contains, ...children, ...dedupe(infra).map(asDetail)].sort(byId),
|
|
570
|
+
rankOverride,
|
|
571
|
+
laneOfKind: {
|
|
572
|
+
feature: 'unit',
|
|
573
|
+
screen: 'unit',
|
|
574
|
+
skill: 'unit',
|
|
575
|
+
service: 'unit',
|
|
576
|
+
micro_app: 'unit',
|
|
577
|
+
},
|
|
578
|
+
pinTop: owner.id,
|
|
579
|
+
alignToPin: dedupe(infra)
|
|
580
|
+
.flatMap((e) => [e.from, e.to])
|
|
581
|
+
.filter((n) => n !== owner.id),
|
|
582
|
+
...(children.length > 0 ? { sinkBottom: children.map((c) => c.to) } : {}),
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
// 나무 레벨의 열 순서. 서빙 사슬(0~2) 뒤로 소유자, 불러온 앱, 기능 영역, 화면을 한 열씩 세우고 게이트웨이부터는 그 뒤로 민다
|
|
586
|
+
const TREE_RANK = { owner: 3, loaded: 4, feature: 5, screen: 6 };
|
|
587
|
+
const TREE_RANK_SHIFT = TREE_RANK.screen;
|
|
588
|
+
/**
|
|
589
|
+
* 소유자 하나를 펼친 레벨. 소유자 → 불러온 앱 → 기능 영역 → 화면을 열로 나눠 위계가 왼쪽에서 오른쪽으로 읽히게 하고
|
|
590
|
+
* 앞에는 소유자 서빙 사슬, 뒤에는 화면이 부르는 게이트웨이와 서버를 잇는다.
|
|
591
|
+
* 마이크로 프론트엔드든 아니든 같은 열 구성이라야 서비스끼리 오가며 봐도 자리 감각이 안 바뀐다
|
|
592
|
+
*/
|
|
593
|
+
function treeLevel(g, spec) {
|
|
594
|
+
const { id, root: entry, loads } = spec;
|
|
595
|
+
const remotes = new Set(loads.map((e) => e.to));
|
|
596
|
+
const owners = new Set([entry.id, ...remotes]);
|
|
597
|
+
const nodeIds = new Set(owners);
|
|
598
|
+
const rankOverride = new Map([[entry.id, TREE_RANK.owner]]);
|
|
599
|
+
const laneOverride = new Map([[entry.id, spec.rootLane]]);
|
|
600
|
+
for (const r of remotes) {
|
|
601
|
+
rankOverride.set(r, TREE_RANK.loaded);
|
|
602
|
+
laneOverride.set(r, 'remote');
|
|
603
|
+
}
|
|
604
|
+
const tree = [];
|
|
605
|
+
const children = new Map([[entry.id, [...remotes]]]);
|
|
606
|
+
// 리모트 블록을 먼저 쌓고 호스트 자기 기능 영역, 호스트에 바로 단 화면 순으로 잇는다. 첫 줄이 호스트 → 리모트 → 기능 → 화면으로 읽힌다
|
|
607
|
+
const sortKey = new Map();
|
|
608
|
+
for (const r of remotes)
|
|
609
|
+
sortKey.set(r, 0);
|
|
610
|
+
for (const n of [...g.nodeById.values()].sort(byId)) {
|
|
611
|
+
if (n.kind !== 'feature' && !isSurface(g, n.id))
|
|
612
|
+
continue;
|
|
613
|
+
const owner = ownerOf(g, n.id);
|
|
614
|
+
const parent = g.parentOf.get(n.id);
|
|
615
|
+
if (owner === undefined || !owners.has(owner) || parent === undefined)
|
|
616
|
+
continue;
|
|
617
|
+
nodeIds.add(n.id);
|
|
618
|
+
rankOverride.set(n.id, n.kind === 'feature' ? TREE_RANK.feature : TREE_RANK.screen);
|
|
619
|
+
children.set(parent, [...(children.get(parent) ?? []), n.id]);
|
|
620
|
+
sortKey.set(n.id, n.kind === 'feature' ? 1 : 2);
|
|
621
|
+
tree.push({
|
|
622
|
+
id: `contains:${parent}->${n.id}`,
|
|
623
|
+
from: parent,
|
|
624
|
+
to: n.id,
|
|
625
|
+
kind: 'contains',
|
|
626
|
+
count: 1,
|
|
627
|
+
memberEdgeIds: [],
|
|
628
|
+
lineStyle: 'solid',
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
// 스킬이 띄우는 에이전트는 스킬 바로 뒤 열에 세우고 띄운 선을 그대로 그린다
|
|
632
|
+
const spawns = spawnedFrom(g, [...nodeIds].filter((n) => kindOf(g, n) === 'skill'));
|
|
633
|
+
for (const e of spawns) {
|
|
634
|
+
if (nodeIds.has(e.to))
|
|
635
|
+
continue;
|
|
636
|
+
nodeIds.add(e.to);
|
|
637
|
+
rankOverride.set(e.to, PARTITION_RANK.agent + TREE_RANK_SHIFT);
|
|
638
|
+
}
|
|
639
|
+
const invokes = g.edges.filter((e) => e.kind === 'invokes' && nodeIds.has(e.from) && nodeIds.has(e.to));
|
|
640
|
+
const b = new Bundler();
|
|
641
|
+
const calls = screenCalls(g).filter((c) => nodeIds.has(c.from));
|
|
642
|
+
// MCP 도구는 하네스 그림의 중심이라 묶어 접지 않고 카드로 세운다. 웹 API처럼 수백 개가 되는 일이 없다
|
|
643
|
+
const toolCalls = calls.filter((c) => g.nodeById.get(c.to)?.protocol === 'mcp');
|
|
644
|
+
const toolEdges = dedupe([...toolCalls, ...toolCalls.flatMap((c) => handlersOf(g, c.to))]);
|
|
645
|
+
for (const e of toolEdges) {
|
|
646
|
+
for (const n of [e.from, e.to]) {
|
|
647
|
+
if (nodeIds.has(n))
|
|
648
|
+
continue;
|
|
649
|
+
nodeIds.add(n);
|
|
650
|
+
rankOverride.set(n, PARTITION_RANK[kindOf(g, n)] + TREE_RANK_SHIFT);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
bundleCalls(g, b, calls.filter((c) => !toolCalls.includes(c)), (s) => s, true, false);
|
|
654
|
+
const bundles = b.toEdges(g);
|
|
655
|
+
for (const e of bundles) {
|
|
656
|
+
for (const n of [e.from, e.to]) {
|
|
657
|
+
if (nodeIds.has(n))
|
|
658
|
+
continue;
|
|
659
|
+
nodeIds.add(n);
|
|
660
|
+
rankOverride.set(n, PARTITION_RANK[kindOf(g, n)] + TREE_RANK_SHIFT);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
const infra = dedupe(infraChainOf(g, entry.id));
|
|
664
|
+
for (const e of infra) {
|
|
665
|
+
for (const n of [e.from, e.to]) {
|
|
666
|
+
nodeIds.add(n);
|
|
667
|
+
const r = SERVICE_INFRA_RANK[kindOf(g, n)];
|
|
668
|
+
if (r !== undefined)
|
|
669
|
+
rankOverride.set(n, r);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return {
|
|
673
|
+
id,
|
|
674
|
+
kind: spec.kind,
|
|
675
|
+
focusId: entry.id,
|
|
676
|
+
title: spec.title,
|
|
677
|
+
trail: spec.trail,
|
|
678
|
+
nodeIds,
|
|
679
|
+
edges: [
|
|
680
|
+
...bundles,
|
|
681
|
+
...tree,
|
|
682
|
+
...loads.map(asDetail),
|
|
683
|
+
...infra.map(asDetail),
|
|
684
|
+
...dedupe([...spawns, ...invokes, ...toolEdges]).map(asDetail),
|
|
685
|
+
].sort(byId),
|
|
686
|
+
rankOverride,
|
|
687
|
+
laneOverride,
|
|
688
|
+
laneOfKind: { feature: 'unit', screen: 'screen' },
|
|
689
|
+
pinTop: entry.id,
|
|
690
|
+
tree: { root: entry.id, children, sortKey },
|
|
691
|
+
alignToPin: infra.flatMap((e) => [e.from, e.to]).filter((n) => n !== entry.id),
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
// 서비스 레벨 왼쪽 인프라 레인. 요청이 들어오는 순서(도메인 → CDN → 버킷이나 SSR 서버)대로 놓고 나머지는 그 뒤로 민다
|
|
695
|
+
const SERVICE_INFRA_RANK = {
|
|
696
|
+
domain: 0,
|
|
697
|
+
cdn: 1,
|
|
698
|
+
bucket: 2,
|
|
699
|
+
deploy_target: 2,
|
|
700
|
+
};
|
|
701
|
+
const INFRA_RANK_SHIFT = 3;
|
|
702
|
+
/** 서비스를 서빙하는 버킷이나 SSR 서버에서 CDN, 도메인까지 거슬러 올라간 엣지 */
|
|
703
|
+
function infraChainOf(g, serviceId) {
|
|
704
|
+
const into = (kind, to, fromKind) => g.edges.filter((e) => e.kind === kind && e.to === to && kindOf(g, e.from) === fromKind);
|
|
705
|
+
const serves = [
|
|
706
|
+
...into('serves', serviceId, 'bucket'),
|
|
707
|
+
...into('serves', serviceId, 'deploy_target'),
|
|
708
|
+
];
|
|
709
|
+
const origins = serves.flatMap((s) => into('origin', s.from, 'cdn'));
|
|
710
|
+
const resolves = [
|
|
711
|
+
...origins,
|
|
712
|
+
...serves.filter((s) => kindOf(g, s.from) === 'deploy_target'),
|
|
713
|
+
].flatMap((o) => into('resolves_to', o.from, 'domain'));
|
|
714
|
+
return [...serves, ...origins, ...resolves];
|
|
715
|
+
}
|
|
716
|
+
function serverLevel(g, server) {
|
|
717
|
+
const id = `server:${server.id}`;
|
|
718
|
+
const picked = [];
|
|
719
|
+
const rankOverride = new Map();
|
|
720
|
+
if (server.kind === 'gateway') {
|
|
721
|
+
picked.push(...g.edges.filter((e) => e.kind === 'routes' && e.from === server.id));
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
picked.push(...g.edges.filter((e) => e.kind === 'handles' && e.to === server.id && kindOf(g, e.from) === 'endpoint'), ...g.edges.filter((e) => e.kind === 'reads_writes' &&
|
|
725
|
+
e.from === server.id &&
|
|
726
|
+
(kindOf(g, e.to) === 'db_table' || kindOf(g, e.to) === 'datastore')));
|
|
727
|
+
for (const use of g.edges.filter((e) => e.kind === 'uses' && e.from === server.id && kindOf(g, e.to) === 'external_service')) {
|
|
728
|
+
picked.push(use);
|
|
729
|
+
for (const call of g.edges.filter((e) => e.kind === 'calls' && e.from === use.to)) {
|
|
730
|
+
const targetKind = kindOf(g, call.to);
|
|
731
|
+
if (call.to === server.id)
|
|
732
|
+
continue;
|
|
733
|
+
if (targetKind === 'app_module') {
|
|
734
|
+
picked.push(call);
|
|
735
|
+
rankOverride.set(call.to, RECEIVER_RANK);
|
|
736
|
+
}
|
|
737
|
+
else if (targetKind === 'endpoint') {
|
|
738
|
+
picked.push(call);
|
|
739
|
+
rankOverride.set(call.to, RECEIVER_RANK);
|
|
740
|
+
for (const h of handlersOf(g, call.to)) {
|
|
741
|
+
if (h.to === server.id)
|
|
742
|
+
continue;
|
|
743
|
+
picked.push(h);
|
|
744
|
+
rankOverride.set(h.to, RECEIVER_RANK + 1);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
else if (targetKind === 'gateway') {
|
|
748
|
+
picked.push(call);
|
|
749
|
+
const down = routesDown(g, call.to);
|
|
750
|
+
for (const e of down.edges)
|
|
751
|
+
if (e.to !== server.id)
|
|
752
|
+
picked.push(e);
|
|
753
|
+
for (const [nodeId, d] of down.depth) {
|
|
754
|
+
if (nodeId !== server.id)
|
|
755
|
+
rankOverride.set(nodeId, RECEIVER_RANK + d);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
const nodeIds = new Set([server.id]);
|
|
762
|
+
for (const e of picked) {
|
|
763
|
+
nodeIds.add(e.from);
|
|
764
|
+
nodeIds.add(e.to);
|
|
765
|
+
}
|
|
766
|
+
const laneOverride = new Map();
|
|
767
|
+
for (const id of rankOverride.keys())
|
|
768
|
+
laneOverride.set(id, 'external');
|
|
769
|
+
// 같은 프로세스 안 모듈은 외부가 아니다. 부르는 핸들러는 엔드포인트 열에, 불리는 엔진은 받는 쪽 열에 둔다
|
|
770
|
+
if (server.kind === 'app_module') {
|
|
771
|
+
for (const u of moduleUses(g)) {
|
|
772
|
+
if (u.from !== server.id && u.to !== server.id)
|
|
773
|
+
continue;
|
|
774
|
+
picked.push(u);
|
|
775
|
+
const other = u.from === server.id ? u.to : u.from;
|
|
776
|
+
nodeIds.add(other);
|
|
777
|
+
rankOverride.set(other, u.from === server.id ? RECEIVER_RANK : PARTITION_RANK.endpoint);
|
|
778
|
+
laneOverride.set(other, 'app_module');
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
// 레지스트리가 읽어 들이는 에이전트는 받는 쪽 열에 세운다. 스킬이 안 띄우는 에이전트가 드릴다운에 서는 유일한 자리다
|
|
782
|
+
if (server.kind === 'app_module') {
|
|
783
|
+
for (const l of edgesOf(g, 'loads', 'app_module', 'agent')) {
|
|
784
|
+
if (l.from !== server.id)
|
|
785
|
+
continue;
|
|
786
|
+
picked.push(l);
|
|
787
|
+
nodeIds.add(l.to);
|
|
788
|
+
rankOverride.set(l.to, RECEIVER_RANK);
|
|
789
|
+
laneOverride.set(l.to, 'agent');
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
// 테이블은 받는 쪽 모듈들 뒤 맨 오른쪽 레인에 모은다
|
|
793
|
+
for (const id of nodeIds) {
|
|
794
|
+
const kind = kindOf(g, id);
|
|
795
|
+
if (kind === 'db_table' || kind === 'datastore')
|
|
796
|
+
rankOverride.set(id, TABLE_RANK_AFTER_RECEIVERS);
|
|
797
|
+
}
|
|
798
|
+
const edges = dedupe(picked).map(asDetail);
|
|
799
|
+
return {
|
|
800
|
+
id,
|
|
801
|
+
kind: 'server',
|
|
802
|
+
focusId: server.id,
|
|
803
|
+
title: server.displayName ?? server.label,
|
|
804
|
+
trail: [ROOT_LEVEL_ID, id],
|
|
805
|
+
nodeIds,
|
|
806
|
+
edges,
|
|
807
|
+
rankOverride,
|
|
808
|
+
laneOverride,
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
function featureLevel(g, feature, ownerTrail) {
|
|
812
|
+
const id = `feature:${feature.id}`;
|
|
813
|
+
const screens = new Set([...g.nodeById.values()]
|
|
814
|
+
.filter((n) => isSurface(g, n.id) && featureOf(g, n.id) === feature.id)
|
|
815
|
+
.map((n) => n.id));
|
|
816
|
+
const picked = [];
|
|
817
|
+
const spawns = spawnedFrom(g, screens);
|
|
818
|
+
const callers = new Set([...screens, ...spawns.map((e) => e.to)]);
|
|
819
|
+
picked.push(...spawns, ...g.edges.filter((e) => e.kind === 'invokes' && screens.has(e.from)));
|
|
820
|
+
for (const call of screenCalls(g).filter((c) => callers.has(c.from))) {
|
|
821
|
+
const routes = routesInto(g, call.to);
|
|
822
|
+
picked.push(call, ...routes, ...handlersOf(g, call.to));
|
|
823
|
+
for (const r of routes) {
|
|
824
|
+
for (const chain of chainsUp(g, r.from))
|
|
825
|
+
picked.push(...chain.hops);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
picked.push(...edgesOf(g, 'navigates', 'screen', 'screen').filter((e) => screens.has(e.from) && screens.has(e.to)));
|
|
829
|
+
const nodeIds = new Set(screens);
|
|
830
|
+
for (const e of picked) {
|
|
831
|
+
nodeIds.add(e.from);
|
|
832
|
+
nodeIds.add(e.to);
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
id,
|
|
836
|
+
kind: 'feature',
|
|
837
|
+
focusId: feature.id,
|
|
838
|
+
title: feature.displayName ?? feature.label,
|
|
839
|
+
trail: [...ownerTrail, id],
|
|
840
|
+
nodeIds,
|
|
841
|
+
edges: dedupe(picked).map(asDetail),
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
function dedupe(edges) {
|
|
845
|
+
const seen = new Map();
|
|
846
|
+
for (const e of edges)
|
|
847
|
+
seen.set(e.id, e);
|
|
848
|
+
return [...seen.values()].sort(byId);
|
|
849
|
+
}
|
|
850
|
+
/** 자식을 순서 키, elk가 정한 높이, id 순으로 줄 세운다. 같은 블록 안에서는 elk가 줄인 교차를 그대로 살린다 */
|
|
851
|
+
function treeOrder(layout, tree) {
|
|
852
|
+
const yOf = new Map(layout.nodes.map((n) => [n.id, n.y]));
|
|
853
|
+
const children = new Map();
|
|
854
|
+
for (const [parent, list] of tree.children) {
|
|
855
|
+
children.set(parent, [...list].sort((a, b) => (tree.sortKey.get(a) ?? 0) - (tree.sortKey.get(b) ?? 0) ||
|
|
856
|
+
(yOf.get(a) ?? 0) - (yOf.get(b) ?? 0) ||
|
|
857
|
+
compareStr(a, b)));
|
|
858
|
+
}
|
|
859
|
+
return { root: tree.root, children };
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* tree 전략의 레벨. 전체는 parent 없는 노드를, 자식이 있는 노드마다 그 직속 자식을 세운다.
|
|
863
|
+
* 자손끼리의 선은 이 레벨에 선 카드까지 끌어올려 묶는다. 밖으로 나가는 선은 상대를 이 레벨의 형제 높이까지 올려 같이 세운다
|
|
864
|
+
*/
|
|
865
|
+
function groupDrafts(full) {
|
|
866
|
+
const g = prodGraph(full);
|
|
867
|
+
const children = new Map();
|
|
868
|
+
for (const [c, p] of [...g.parentOf].sort((a, b) => compareStr(a[0], b[0]))) {
|
|
869
|
+
children.set(p, [...(children.get(p) ?? []), c]);
|
|
870
|
+
}
|
|
871
|
+
const chainOf = (id) => {
|
|
872
|
+
const out = [id];
|
|
873
|
+
for (let p = g.parentOf.get(id); p !== undefined; p = g.parentOf.get(p))
|
|
874
|
+
out.push(p);
|
|
875
|
+
return out;
|
|
876
|
+
};
|
|
877
|
+
const levelIdOf = (id) => `group:${id}`;
|
|
878
|
+
const enter = {};
|
|
879
|
+
for (const id of children.keys())
|
|
880
|
+
enter[id] = levelIdOf(id);
|
|
881
|
+
const draftFor = (focus) => {
|
|
882
|
+
const members = new Set(focus === undefined
|
|
883
|
+
? [...g.nodeById.keys()].filter((id) => !g.parentOf.has(id))
|
|
884
|
+
: children.get(focus));
|
|
885
|
+
// 이 레벨에 선 카드 중 id를 품은 것. 없으면 이 레벨 밖이다
|
|
886
|
+
const lift = (id) => chainOf(id).find((x) => members.has(x));
|
|
887
|
+
const around = new Set(focus === undefined ? [] : chainOf(focus));
|
|
888
|
+
// 밖의 끝점은 이 레벨을 품은 조상 바로 아래까지만 올린다. 그래야 옆 서브넷이나 옆 조직처럼 한 단계 위 형제로 보인다
|
|
889
|
+
const liftOutside = (id) => {
|
|
890
|
+
let x = id;
|
|
891
|
+
for (let p = g.parentOf.get(x); p !== undefined && !around.has(p); p = g.parentOf.get(x))
|
|
892
|
+
x = p;
|
|
893
|
+
return x;
|
|
894
|
+
};
|
|
895
|
+
const nodeIds = new Set(members);
|
|
896
|
+
const sendsOut = new Set();
|
|
897
|
+
const receives = new Set();
|
|
898
|
+
const b = new Bundler();
|
|
899
|
+
const detail = [];
|
|
900
|
+
const pairs = new Map();
|
|
901
|
+
for (const e of g.edges) {
|
|
902
|
+
let from = lift(e.from);
|
|
903
|
+
let to = lift(e.to);
|
|
904
|
+
if (from === undefined && to === undefined)
|
|
905
|
+
continue;
|
|
906
|
+
if (focus !== undefined && from === undefined)
|
|
907
|
+
from = liftOutside(e.from);
|
|
908
|
+
if (focus !== undefined && to === undefined)
|
|
909
|
+
to = liftOutside(e.to);
|
|
910
|
+
if (from === undefined || to === undefined || from === to)
|
|
911
|
+
continue;
|
|
912
|
+
nodeIds.add(from);
|
|
913
|
+
nodeIds.add(to);
|
|
914
|
+
if (!members.has(from))
|
|
915
|
+
sendsOut.add(from);
|
|
916
|
+
if (!members.has(to))
|
|
917
|
+
receives.add(to);
|
|
918
|
+
const k = `${from}\u0000${to}`;
|
|
919
|
+
pairs.set(k, [...(pairs.get(k) ?? []), e]);
|
|
920
|
+
}
|
|
921
|
+
for (const [k, es] of pairs) {
|
|
922
|
+
const [from, to] = k.split('\u0000');
|
|
923
|
+
// 끌어올리지 않은 선 하나는 원래 kind 그대로 그려야 범례 글자가 맞는다
|
|
924
|
+
if (es.length === 1 && es[0].from === from && es[0].to === to)
|
|
925
|
+
detail.push(asDetail(es[0]));
|
|
926
|
+
else
|
|
927
|
+
for (const e of es)
|
|
928
|
+
b.add(from, to, e.id, [e.id]);
|
|
929
|
+
}
|
|
930
|
+
// 바깥 카드를 kind 순위대로 두면 안쪽 카드 사이에 끼어 선이 크게 돈다. 받는 쪽은 오른쪽 끝, 보내기만 하는 쪽은 왼쪽 끝에 모은다.
|
|
931
|
+
// 한 열에 다 쌓으면 바깥 카드가 많을 때 그림이 세로로만 길어진다. 그래서 kind 순위마다 열을 나누고 한 열이 넘치면 옆 열로 넘긴다
|
|
932
|
+
const ranks = [...members].map((id) => PARTITION_RANK[g.nodeById.get(id).kind]);
|
|
933
|
+
const rankOverride = new Map();
|
|
934
|
+
const laneOverride = new Map();
|
|
935
|
+
const outsideColumns = (ids) => {
|
|
936
|
+
const byRank = new Map();
|
|
937
|
+
for (const id of [...ids].sort(compareStr)) {
|
|
938
|
+
const r = PARTITION_RANK[g.nodeById.get(id).kind];
|
|
939
|
+
byRank.set(r, [...(byRank.get(r) ?? []), id]);
|
|
940
|
+
}
|
|
941
|
+
return [...byRank.keys()]
|
|
942
|
+
.sort((x, y) => x - y)
|
|
943
|
+
.flatMap((r) => {
|
|
944
|
+
const list = byRank.get(r);
|
|
945
|
+
return Array.from({ length: Math.ceil(list.length / OUTSIDE_PER_COLUMN) }, (_, i) => list.slice(i * OUTSIDE_PER_COLUMN, (i + 1) * OUTSIDE_PER_COLUMN));
|
|
946
|
+
});
|
|
947
|
+
};
|
|
948
|
+
const right = outsideColumns([...receives]);
|
|
949
|
+
right.forEach((col, i) => {
|
|
950
|
+
for (const id of col) {
|
|
951
|
+
rankOverride.set(id, Math.max(...ranks) + 1 + i);
|
|
952
|
+
laneOverride.set(id, 'outside_to');
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
const left = outsideColumns([...sendsOut].filter((id) => !receives.has(id)));
|
|
956
|
+
left.forEach((col, i) => {
|
|
957
|
+
for (const id of col) {
|
|
958
|
+
rankOverride.set(id, Math.min(...ranks) - left.length + i);
|
|
959
|
+
laneOverride.set(id, 'outside_from');
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
const node = focus === undefined ? undefined : g.nodeById.get(focus);
|
|
963
|
+
const trail = [
|
|
964
|
+
ROOT_LEVEL_ID,
|
|
965
|
+
...(focus === undefined ? [] : chainOf(focus).reverse().map(levelIdOf)),
|
|
966
|
+
];
|
|
967
|
+
return {
|
|
968
|
+
id: focus === undefined ? ROOT_LEVEL_ID : levelIdOf(focus),
|
|
969
|
+
kind: focus === undefined ? 'root' : 'group',
|
|
970
|
+
...(focus !== undefined ? { focusId: focus } : {}),
|
|
971
|
+
title: node === undefined ? '전체' : (node.displayName ?? node.label),
|
|
972
|
+
trail,
|
|
973
|
+
nodeIds,
|
|
974
|
+
edges: [...b.toEdges(g), ...detail].sort(byId),
|
|
975
|
+
...(rankOverride.size > 0 ? { rankOverride, laneOverride } : {}),
|
|
976
|
+
};
|
|
977
|
+
};
|
|
978
|
+
const drafts = [draftFor(undefined), ...[...children.keys()].sort(compareStr).map(draftFor)];
|
|
979
|
+
return { drafts, enter };
|
|
980
|
+
}
|
|
981
|
+
/** service 노드가 하나라도 그려지면 드릴다운으로 그린다 */
|
|
982
|
+
export function shouldDrillDown(validated) {
|
|
983
|
+
return (validated.ir.nodes.some((n) => n.kind === 'service' && validated.drawableNodeIds.has(n.id)) ||
|
|
984
|
+
// 독립 흐름은 레벨로만 그려지므로 서비스가 없어도 드릴다운 HTML이 필요하다
|
|
985
|
+
(validated.ir.flows ?? []).some((f) => f.service === undefined) ||
|
|
986
|
+
// 질문별 그림도 레벨로만 붙는다
|
|
987
|
+
(validated.drawableProjectionIds?.size ?? 0) > 0 ||
|
|
988
|
+
// tree 팩은 포함 관계가 하나라도 있으면 한 단계씩 들어가 본다
|
|
989
|
+
(irVocabulary(validated.ir).drilldown === 'tree' &&
|
|
990
|
+
validated.ir.nodes.some((n) => n.parent !== undefined &&
|
|
991
|
+
validated.drawableNodeIds.has(n.id) &&
|
|
992
|
+
validated.drawableNodeIds.has(n.parent))));
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* 검증된 IR에서 드릴다운 레벨과 묶음 엣지를 계산하고 레벨마다 elkjs 좌표를 미리 낸다.
|
|
996
|
+
* 정렬은 전부 id 기준이라 같은 입력이면 같은 결과가 나온다.
|
|
997
|
+
*/
|
|
998
|
+
/** 서비스 id → 그린 흐름 수. 카드 배지 폭과 글자가 같은 수를 써야 한다 */
|
|
999
|
+
export function flowCountByService(flows) {
|
|
1000
|
+
const out = {};
|
|
1001
|
+
for (const f of flows)
|
|
1002
|
+
if (f.service !== undefined)
|
|
1003
|
+
out[f.service] = (out[f.service] ?? 0) + 1;
|
|
1004
|
+
return out;
|
|
1005
|
+
}
|
|
1006
|
+
export async function computeDrilldown(validated) {
|
|
1007
|
+
const g = buildGraph(validated);
|
|
1008
|
+
const facts = computeServiceFacts([...g.nodeById.values()], g.edges);
|
|
1009
|
+
const sorted = [...g.nodeById.values()].sort(byId);
|
|
1010
|
+
const bands = bandGroupsOf(validated.ir);
|
|
1011
|
+
const apps = indexMicroApps(sorted, g.edges);
|
|
1012
|
+
const tree = irVocabulary(validated.ir).drilldown === 'tree' ? groupDrafts(g) : undefined;
|
|
1013
|
+
const drafts = tree?.drafts ?? [rootLevel(g, apps)];
|
|
1014
|
+
const enter = tree?.enter ?? {};
|
|
1015
|
+
const entryApps = new Set(apps.entryOf.values());
|
|
1016
|
+
const trailOf = (ownerId) => {
|
|
1017
|
+
const owner = g.nodeById.get(ownerId);
|
|
1018
|
+
if (owner?.kind === 'service')
|
|
1019
|
+
return [ROOT_LEVEL_ID, `service:${owner.id}`];
|
|
1020
|
+
if (owner?.kind !== 'micro_app')
|
|
1021
|
+
return undefined;
|
|
1022
|
+
const parent = g.parentOf.get(owner.id);
|
|
1023
|
+
// 진입 앱은 서비스 레벨이 곧 그 앱 레벨이다. 사용자가 서비스로 들어가면 처음 받는 게 이 앱이라서다
|
|
1024
|
+
if (entryApps.has(owner.id))
|
|
1025
|
+
return [ROOT_LEVEL_ID, `service:${parent}`];
|
|
1026
|
+
return [
|
|
1027
|
+
ROOT_LEVEL_ID,
|
|
1028
|
+
...(parent !== undefined && kindOf(g, parent) === 'service' ? [`service:${parent}`] : []),
|
|
1029
|
+
`app:${owner.id}`,
|
|
1030
|
+
];
|
|
1031
|
+
};
|
|
1032
|
+
const loadsFrom = (appId) => apps.loads.filter((e) => e.from === appId && e.to !== appId);
|
|
1033
|
+
// 진입 앱을 못 정한 서비스는 앱 카드를 서비스 아래에 포함 선으로 늘어놓고 하나씩 들어가게 한다
|
|
1034
|
+
const appCardsOf = (serviceId) => orderedApps(apps, serviceId).map((a) => ({
|
|
1035
|
+
id: `contains:${serviceId}->${a}`,
|
|
1036
|
+
from: serviceId,
|
|
1037
|
+
to: a,
|
|
1038
|
+
kind: 'contains',
|
|
1039
|
+
count: 1,
|
|
1040
|
+
memberEdgeIds: [],
|
|
1041
|
+
lineStyle: 'solid',
|
|
1042
|
+
}));
|
|
1043
|
+
for (const n of tree === undefined ? sorted : []) {
|
|
1044
|
+
if (n.kind === 'service') {
|
|
1045
|
+
const entry = g.nodeById.get(apps.entryOf.get(n.id) ?? '');
|
|
1046
|
+
const trail = trailOf(n.id);
|
|
1047
|
+
const id = trail[trail.length - 1];
|
|
1048
|
+
const title = n.displayName ?? n.label;
|
|
1049
|
+
const level = entry !== undefined
|
|
1050
|
+
? treeLevel(g, {
|
|
1051
|
+
id,
|
|
1052
|
+
kind: 'service',
|
|
1053
|
+
title,
|
|
1054
|
+
trail,
|
|
1055
|
+
root: entry,
|
|
1056
|
+
rootLane: 'host',
|
|
1057
|
+
loads: loadsFrom(entry.id),
|
|
1058
|
+
})
|
|
1059
|
+
: apps.appsOf.has(n.id)
|
|
1060
|
+
? ownerLevel(g, n, 'service', trail, { children: appCardsOf(n.id) })
|
|
1061
|
+
: treeLevel(g, {
|
|
1062
|
+
id,
|
|
1063
|
+
kind: 'service',
|
|
1064
|
+
title,
|
|
1065
|
+
trail,
|
|
1066
|
+
root: n,
|
|
1067
|
+
rootLane: 'service',
|
|
1068
|
+
loads: [],
|
|
1069
|
+
});
|
|
1070
|
+
drafts.push(level);
|
|
1071
|
+
enter[n.id] = level.id;
|
|
1072
|
+
if (entry !== undefined)
|
|
1073
|
+
enter[entry.id] = level.id;
|
|
1074
|
+
}
|
|
1075
|
+
else if (n.kind === 'micro_app') {
|
|
1076
|
+
if (entryApps.has(n.id))
|
|
1077
|
+
continue;
|
|
1078
|
+
const trail = trailOf(n.id);
|
|
1079
|
+
const level = treeLevel(g, {
|
|
1080
|
+
id: trail[trail.length - 1],
|
|
1081
|
+
kind: 'app',
|
|
1082
|
+
title: n.displayName ?? n.label,
|
|
1083
|
+
trail,
|
|
1084
|
+
root: n,
|
|
1085
|
+
rootLane: 'remote',
|
|
1086
|
+
loads: loadsFrom(n.id),
|
|
1087
|
+
});
|
|
1088
|
+
drafts.push(level);
|
|
1089
|
+
enter[n.id] = level.id;
|
|
1090
|
+
}
|
|
1091
|
+
else if (n.kind === 'gateway' || n.kind === 'app_module') {
|
|
1092
|
+
const level = serverLevel(g, n);
|
|
1093
|
+
drafts.push(level);
|
|
1094
|
+
enter[n.id] = level.id;
|
|
1095
|
+
}
|
|
1096
|
+
else if (n.kind === 'feature') {
|
|
1097
|
+
const ownerTrail = trailOf(g.parentOf.get(n.id) ?? '');
|
|
1098
|
+
if (ownerTrail === undefined)
|
|
1099
|
+
continue;
|
|
1100
|
+
const level = featureLevel(g, n, ownerTrail);
|
|
1101
|
+
drafts.push(level);
|
|
1102
|
+
enter[n.id] = level.id;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
const [root, ...rest] = drafts;
|
|
1106
|
+
const ordered = [root, ...rest.sort(byId)];
|
|
1107
|
+
const levels = [];
|
|
1108
|
+
const staged = assignStages(validated.ir);
|
|
1109
|
+
const flowLevels = computeFlowLevels(validated);
|
|
1110
|
+
const flowCount = flowCountByService(flowLevels);
|
|
1111
|
+
for (const d of ordered) {
|
|
1112
|
+
const nodeIds = [...d.nodeIds].filter((id) => g.nodeById.has(id)).sort(compareStr);
|
|
1113
|
+
const layoutNodes = nodeIds.map((id) => {
|
|
1114
|
+
const node = g.nodeById.get(id);
|
|
1115
|
+
const f = facts.get(id);
|
|
1116
|
+
return {
|
|
1117
|
+
id,
|
|
1118
|
+
label: node.label,
|
|
1119
|
+
...(node.displayName !== undefined ? { displayName: node.displayName } : {}),
|
|
1120
|
+
...(node.displayNameInferred ? { displayNameInferred: true } : {}),
|
|
1121
|
+
rank: d.rankOverride?.get(id) ?? PARTITION_RANK[node.kind],
|
|
1122
|
+
lane: d.laneOverride?.get(id) ?? d.laneOfKind?.[node.kind] ?? laneOfNode(node),
|
|
1123
|
+
...(staged !== undefined ? { stage: staged.indexOf.get(id) } : {}),
|
|
1124
|
+
kind: displayKindOf(node),
|
|
1125
|
+
...(chipTextOverride(node) !== undefined ? { chip: chipTextOverride(node) } : {}),
|
|
1126
|
+
...(d.orderOverride?.has(id)
|
|
1127
|
+
? { order: d.orderOverride.get(id) }
|
|
1128
|
+
: node.environment !== undefined
|
|
1129
|
+
? { order: environmentRank(node.environment) }
|
|
1130
|
+
: {}),
|
|
1131
|
+
...(f !== undefined && f.platforms.length > 0 ? { platforms: f.platforms } : {}),
|
|
1132
|
+
...(f?.webHosting !== undefined ? { webHosting: f.webHosting } : {}),
|
|
1133
|
+
...(f?.prodDomain !== undefined ? { secondLine: f.prodDomain } : {}),
|
|
1134
|
+
...(flowCount[id] !== undefined ? { flows: flowCount[id] } : {}),
|
|
1135
|
+
};
|
|
1136
|
+
});
|
|
1137
|
+
const laid = await computeGraphLayout(layoutNodes, d.edges, staged?.labels);
|
|
1138
|
+
const pinned = d.pinTop !== undefined ? pinToColumnTop(laid, d.pinTop) : laid;
|
|
1139
|
+
const treed = d.tree !== undefined ? stackTree(pinned, treeOrder(pinned, d.tree)) : pinned;
|
|
1140
|
+
const aligned = d.pinTop !== undefined && d.alignToPin !== undefined
|
|
1141
|
+
? alignColumnsTo(treed, d.pinTop, d.alignToPin)
|
|
1142
|
+
: treed;
|
|
1143
|
+
const sunk = d.sinkBottom !== undefined ? sinkToColumnBottom(aligned, d.sinkBottom) : aligned;
|
|
1144
|
+
const framed = d.frames !== undefined ? stackFrames(sunk, d.frames) : sunk;
|
|
1145
|
+
const banded = stackBands(framed, bands, validated.ir.edges);
|
|
1146
|
+
const layout = d.frames !== undefined ? frameRects(banded, d.frames) : banded;
|
|
1147
|
+
levels.push({
|
|
1148
|
+
id: d.id,
|
|
1149
|
+
kind: d.kind,
|
|
1150
|
+
...(d.focusId !== undefined ? { focusId: d.focusId } : {}),
|
|
1151
|
+
title: d.title,
|
|
1152
|
+
trail: d.trail,
|
|
1153
|
+
nodeIds,
|
|
1154
|
+
edges: d.edges,
|
|
1155
|
+
layout,
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
return { levels, enter, flows: flowLevels };
|
|
1159
|
+
}
|
|
1160
|
+
//# sourceMappingURL=drilldown.js.map
|