@g1cloud/entity-modeler-next 5.0.0-alpha.1 → 5.0.0-alpha.2

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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # entity-modeler-next
2
2
 
3
3
  Vue 3 기반 비주얼 엔티티 모델링 / 클래스 다이어그램 에디터.
4
- 레거시 GWT/Vaadin 7 모듈(`bluework-entitymodeler`) 후속이며, `bluework4-tool`(Nuxt/Vue/Mongo)에 임베드된다.
4
+ 라이브러리 패키지(`@g1cloud/entity-modeler-next`) 배포되며, `bluework4-tool`(Nuxt/Vue/Mongo)에 임베드된다.
5
5
 
6
6
  ## 스택
7
7
 
@@ -15,7 +15,7 @@ src/
15
15
  command/ Command(do/undo)·CommandStack·op/opSync(시맨틱 op emit + CAS 동시성)
16
16
  editor/ reactive controller (CommandStack 래핑 컴포저블, EDITOR inject 키)
17
17
  view/ Vue Flow 컴포넌트 (DiagramCanvas·EntityNode·GroupNode·AssociationEdge·PropertyPanel·ValidationPanel 등)
18
- adapter/ 레거시 PDiagram 매핑 (persisted v1/v2·fromPersisted/toPersisted/toPersistedV2 라운드트립)
18
+ adapter/ 저장 스키마 매핑 (persisted v1/v2·fromPersisted/toPersisted/toPersistedV2 라운드트립)
19
19
  agent/ 자연어→op 트랙 (symbolicOp·resolver·schema·buildAgentBatch)
20
20
  dev/ 데모 하니스 (라이브러리 빌드에서 제외)
21
21
  ```
@@ -26,17 +26,8 @@ src/
26
26
  ## 스크립트
27
27
 
28
28
  ```bash
29
- pnpm dev # 스파이크/데모 dev 서버
30
- pnpm test # Vitest (core 단위 테스트)
31
- pnpm typecheck # vue-tsc
29
+ pnpm dev # 데모 하니스 dev 서버 (src/dev)
30
+ pnpm test # Vitest
31
+ pnpm typecheck # vue-tsc --noEmit
32
32
  pnpm build # 라이브러리 빌드 (dist/)
33
33
  ```
34
-
35
- ## M0 스파이크 검증 포인트 (브라우저)
36
-
37
- `pnpm dev` 후 다음을 확인:
38
- - 엔티티 노드가 속성 행·PK 마커와 함께 렌더되는가
39
- - 관계선이 **직각 라우팅**으로 그려지는가
40
- - 파란 **waypoint**를 드래그하면 경로가 갱신되는가
41
- - 세그먼트 중점(회색 원)을 **더블클릭**하면 waypoint가 추가되는가
42
- - 관계선 끝이 특정 **컬럼 행(앵커)**에 붙는가 (Order.id ↔ OrderLine.orderId)
@@ -35,6 +35,75 @@ export type ResolveError = {
35
35
  code: 'self-identifying';
36
36
  opIndex: number;
37
37
  handle: AssocHandle;
38
+ }
39
+ /** `type:'GroupCodeEnum'` 인데 `groupCode` 미동반 — groupCode 바인딩 없는 깨진 파생 타입 방지(B3). */
40
+ | {
41
+ code: 'groupcode-required';
42
+ opIndex: number;
43
+ entity: Handle;
44
+ handle: Handle;
45
+ }
46
+ /** index.add 컬럼 핸들이 엔티티 내 dbAttr와 0개 매칭. */
47
+ | {
48
+ code: 'column-not-found';
49
+ opIndex: number;
50
+ entity: Handle;
51
+ handle: Handle;
52
+ }
53
+ /** index.add 컬럼 핸들이 복수 dbAttr와 매칭(예: 속성명 지정인데 다중 컬럼 임베드/Money). */
54
+ | {
55
+ code: 'column-ambiguous';
56
+ opIndex: number;
57
+ entity: Handle;
58
+ handle: Handle;
59
+ matches: ModelId[];
60
+ }
61
+ /** index.update/remove 핸들이 엔티티 내 인덱스와 0개/복수 매칭. */
62
+ | {
63
+ code: 'index-not-found';
64
+ opIndex: number;
65
+ entity: Handle;
66
+ handle: Handle;
67
+ } | {
68
+ code: 'index-ambiguous';
69
+ opIndex: number;
70
+ entity: Handle;
71
+ handle: Handle;
72
+ matches: ModelId[];
73
+ }
74
+ /** operation.update/remove 핸들이 엔티티 내 operation과 0개/복수 매칭. */
75
+ | {
76
+ code: 'operation-not-found';
77
+ opIndex: number;
78
+ entity: Handle;
79
+ handle: Handle;
80
+ } | {
81
+ code: 'operation-ambiguous';
82
+ opIndex: number;
83
+ entity: Handle;
84
+ handle: Handle;
85
+ matches: ModelId[];
86
+ }
87
+ /**
88
+ * index.update patch에 `columns` 키 — columnRef(dbAttr modelId)는 사람 핸들로 표현 불가라 패스스루 시
89
+ * 깨진 인덱스가 된다. 컬럼 변경은 index.remove + index.add(컬럼 핸들 해소 구현)로 유도(거부 권고안).
90
+ */
91
+ | {
92
+ code: 'index-columns-patch-unsupported';
93
+ opIndex: number;
94
+ entity: Handle;
95
+ handle: Handle;
96
+ }
97
+ /**
98
+ * 같은 배치에서 `entity.add`로 추가되는 엔티티를 자식/관계 op가 핸들로 참조(A 백스톱 가드).
99
+ * mongo arrayFilter는 update 선이미지에 평가되므로 갓 push된 엔티티에 닿는 child push가 silent no-op이 된다.
100
+ * 신규 엔티티의 자식은 `attribute.add`/`index.add`/`operation.add` 분리가 아니라 `entity.add` spec에 inline fold할 것
101
+ * (관계는 inline 불가 → 엔티티 배치 확정 후 별도 배치). entity-not-found 대신 의도를 드러내는 명시 에러.
102
+ */
103
+ | {
104
+ code: 'pending-entity-ref';
105
+ opIndex: number;
106
+ handle: Handle;
38
107
  };
39
108
  export type ResolveResult = {
40
109
  ok: true;
@@ -1,4 +1,4 @@
1
- import { ClassStereotype, Multiplicity } from '../core/types';
1
+ import { ClassStereotype, MultiLangText, Multiplicity, OperationVisibility } from '../core/types';
2
2
  /**
3
3
  * 요소 지정 핸들 — modelId 대신 이름/물리명.
4
4
  * 엔티티: 물리명(`table.physicalName`) 우선, 논리명(`name`) 보조 — 논리명은 모델 내 중복 허용(CHK-NAME-3)이라 모호 가능.
@@ -19,18 +19,36 @@ export interface AssocHandle {
19
19
  from: Handle;
20
20
  to: Handle;
21
21
  }
22
- /** entity.add 페이로드 — resolver가 modelId 발급 + 빈 골격 조립. */
22
+ /**
23
+ * entity.add 페이로드 — resolver가 modelId 발급 + 골격 조립.
24
+ *
25
+ * **inline fold**: 신규 엔티티의 자식(속성·인덱스·operation)은 별도 `attribute.add` 등으로 보내지 않고
26
+ * 여기에 inline 배열로 담는다. resolver가 완성형 엔티티를 한 `entity.add`로 조립해 단일 `$push`로 들어간다.
27
+ * (별도 child op는 mongo arrayFilter 선이미지 한계로 *같은 배치 신규 엔티티*에 닿지 못해 silent no-op이 된다 —
28
+ * 그래서 신규 엔티티 자식은 op 분리가 아니라 inline이 정공법. 분리 emit은 resolver가 `pending-entity-ref`로 거부.)
29
+ * `attribute.add`/`index.add`/`operation.add` op은 *기존* 엔티티에 자식을 추가하는 용도로 남는다.
30
+ */
23
31
  export interface EntitySpec {
24
32
  name: string;
25
33
  /** 기본 'JPA_ENTITY'. */
26
34
  stereotype?: ClassStereotype;
27
35
  /** 설정 시 `table.physicalName`. */
28
36
  physicalName?: string;
37
+ /** inline 속성 — 배열 순서가 곧 `order`. GroupCodeEnum+groupCode 규칙은 `attribute.add`와 동일하게 강제. */
38
+ attributes?: AttrSpec[];
39
+ /** inline 인덱스 — 컬럼 핸들은 *이 엔티티가 inline으로 만드는* dbAttr 내에서 해소된다(속성 inline 동반 전제). */
40
+ indexes?: IndexSpec[];
41
+ /** inline 도메인 메서드 — 배열 순서가 곧 `order`. */
42
+ operations?: OperationSpec[];
29
43
  }
30
44
  /** attribute.add 페이로드 — resolver가 modelId 발급 + dbAttr 골격 조립. */
31
45
  export interface AttrSpec {
32
46
  name: string;
33
- /** Java 타입(예: 'Long', 'String'). */
47
+ /**
48
+ * Java 타입(예: 'Long', 'String'). 카탈로그 저장값.
49
+ * `'GroupCodeEnum'`은 파생 타입이라 **`groupCode` 동반 필수** — 없으면 resolver가 거부(groupCode 바인딩
50
+ * 없는 깨진 파생 타입 방지). groupCode 지정 시 코드성 속성으로 해소된다.
51
+ */
34
52
  type: string;
35
53
  identifier?: boolean;
36
54
  notNull?: boolean;
@@ -42,6 +60,26 @@ export interface AttrSpec {
42
60
  length?: number;
43
61
  /** dbAttr 소수 자릿수(DECIMAL/NUMERIC scale). length 동반이 일반적. 0/미설정은 미지정. */
44
62
  scale?: number;
63
+ /**
64
+ * 코드성 속성의 group code 바인딩(레거시 AttributeModel.groupCode). `type:'GroupCodeEnum'`이면 필수.
65
+ * 모델→모델 복사·코드 동기화에서 GroupCodeEnum 속성을 온전히 재현하는 핵심 필드(없으면 깨진 파생 타입).
66
+ */
67
+ groupCode?: string;
68
+ /** 속성 기본값(레거시 AttributeModel.defaultValue, 문자열 패스스루). */
69
+ defaultValue?: string;
70
+ /** 속성 분류 그룹(레거시 attributeGroup, 예: 'code'·'amt'). 표시·분류 전용 패스스루. */
71
+ attributeGroup?: string;
72
+ /** 속성 설명(다국어, 레거시 AttributeModel.description). */
73
+ description?: MultiLangText;
74
+ /**
75
+ * dbAttr 컬럼 논리명(다국어, 예: {ko:'체크아웃유형'}). 한글 업무명이 모델→모델 복사에서 보존되는 위치 —
76
+ * 속성 자체는 단일 식별자 `name`만 갖고, 다국어 업무명은 컬럼에 붙는다. 컬럼 생성 시에만 의미.
77
+ */
78
+ columnLogicalName?: MultiLangText;
79
+ /** dbAttr UNIQUE 제약. */
80
+ unique?: boolean;
81
+ /** dbAttr 갱신 가능 여부(JPA @Column(updatable=)). */
82
+ updatable?: boolean;
45
83
  }
46
84
  /** association.add 페이로드 — from/to 엔티티는 resolver가 해소, modelId 발급 + end 조립. */
47
85
  export interface AssocSpec {
@@ -58,6 +96,66 @@ export interface AssocSpec {
58
96
  /** end2 composition(부모가 자식 생명주기 소유). */
59
97
  composition?: boolean;
60
98
  }
99
+ /**
100
+ * group.add 페이로드 — 엔티티 멤버를 묶는 논리 그룹(레거시 LogicalGroup). resolver가 modelId 발급 +
101
+ * 멤버 핸들을 entity modelId(`memberEntityRefs`)로 해소. 그룹 멤버십은 값 포함이 아닌 **id 참조**(엔티티는
102
+ * 그룹과 독립 존재). 멤버는 *기존* 엔티티여야 한다 — 같은 배치 신규 엔티티(entity.add)는 닿지 못하므로
103
+ * (top-level 그룹은 inline fold 불가) resolver가 `pending-entity-ref`로 거부, 엔티티 배치 확정 후 별도 배치로.
104
+ * 그룹 박스 좌표(groupLayout)는 호스트가 멤버 엔티티 레이아웃을 감싸 incidental로 첨부(C1 동형).
105
+ */
106
+ export interface GroupSpec {
107
+ /** 그룹 논리명(다국어, 예: {ko:'주문'}). 패키지 기반 그룹이면 보통 패키지 leaf의 업무명. */
108
+ name?: MultiLangText;
109
+ /** 자바 패키지명 등 그룹의 물리 식별(레거시 LogicalGroup.packageName). */
110
+ packageName?: string;
111
+ /** 그룹 설명(자유 텍스트 — 모듈/도메인 경계 의도 메모). */
112
+ description?: string;
113
+ /** DDL 생성 제외 여부(기본 false). */
114
+ excludeDDLGeneration?: boolean;
115
+ /** 멤버 엔티티 핸들(물리명 우선/논리명 보조). resolver가 entity modelId로 해소. */
116
+ members: Handle[];
117
+ }
118
+ /**
119
+ * index.add 컬럼 지정 — 인덱스 컬럼은 dbAttr(컬럼) modelId(`IndexColumn.columnRef`)를 참조하나,
120
+ * 핸들은 사람이 지정 가능한 물리명/속성명이다. resolver가 소속 엔티티 내에서 dbAttr modelId로 해소한다.
121
+ */
122
+ export interface IndexColumnSpec {
123
+ /**
124
+ * 컬럼 핸들 — 컬럼 물리명(`dbAttrs[].physicalName`) 우선, 속성명(`name`) 보조.
125
+ * 속성명으로 지정 시 그 속성이 단일 컬럼이어야 한다(다중 dbAttr=임베드/Money는 모호 → resolver 거부).
126
+ */
127
+ column: Handle;
128
+ /** 내림차순 정렬 컬럼(기본 false=오름차순). */
129
+ descending?: boolean;
130
+ }
131
+ /** index.add 페이로드 — resolver가 modelId 발급 + 컬럼 핸들을 dbAttr modelId로 해소. */
132
+ export interface IndexSpec {
133
+ name: string;
134
+ /** UNIQUE 인덱스 여부(기본 false). */
135
+ unique?: boolean;
136
+ /** 인덱스 컬럼(순서 의미 있음). 비어 있으면 컬럼 없는 인덱스(허용 — 값 품질은 검증 엔진). */
137
+ columns: IndexColumnSpec[];
138
+ /** 인덱스 설명(레거시 IndexModel.description, 평문). */
139
+ description?: string;
140
+ /** 인덱스 파라미터(레거시 IndexModel.parameters, 패스스루). */
141
+ parameters?: string;
142
+ }
143
+ /**
144
+ * operation.add 페이로드 — 도메인 메서드(레거시 OperationModel). resolver가 modelId 발급 + order 누적.
145
+ * 파라미터·반환 타입은 별도 모델 필드가 아니라 `sourceCode`(Java 메서드 본문 텍스트)에 담긴다.
146
+ * legacyRaw(소스 엔티티 parent back-ref·properties)는 복사 시 stale이 되므로 op로 다루지 않는다.
147
+ */
148
+ export interface OperationSpec {
149
+ name: string;
150
+ /** 가시성(기본 미설정 → 호스트/렌더 기본). */
151
+ visibility?: OperationVisibility;
152
+ /** Java 메서드 본문(멀티라인, 파라미터·반환 시그니처 포함). */
153
+ sourceCode?: string;
154
+ /** 메서드 설명(다국어). 레거시 직렬화는 평문이라 어댑터가 평문↔{ko} 변환(types.ts Operation 주석). */
155
+ description?: MultiLangText;
156
+ /** 개인정보 속성 유형(레거시 personalInfoAttributeType 패스스루). */
157
+ personalInfoAttributeType?: string;
158
+ }
61
159
  /**
62
160
  * LLM이 산출하는 단일 심볼릭 op. `kind`는 op.ts 어휘와 1:1(로케이터만 심볼릭).
63
161
  * `patch`는 OpShape.patch와 동형(부분 patch, 그대로 전달 — 값 품질은 검증 엔진이 사후 경고).
@@ -100,10 +198,39 @@ export type SymbolicOp = {
100
198
  association: AssocHandle;
101
199
  end: 'from' | 'to';
102
200
  patch: Record<string, unknown>;
201
+ } | {
202
+ kind: 'index.add';
203
+ entity: Handle;
204
+ spec: IndexSpec;
205
+ } | {
206
+ kind: 'index.update';
207
+ entity: Handle;
208
+ index: Handle;
209
+ patch: Record<string, unknown>;
210
+ } | {
211
+ kind: 'index.remove';
212
+ entity: Handle;
213
+ index: Handle;
214
+ } | {
215
+ kind: 'operation.add';
216
+ entity: Handle;
217
+ spec: OperationSpec;
218
+ } | {
219
+ kind: 'operation.update';
220
+ entity: Handle;
221
+ operation: Handle;
222
+ patch: Record<string, unknown>;
223
+ } | {
224
+ kind: 'operation.remove';
225
+ entity: Handle;
226
+ operation: Handle;
227
+ } | {
228
+ kind: 'group.add';
229
+ spec: GroupSpec;
103
230
  };
104
231
  /**
105
232
  * v1이 다루는 심볼릭 op 종류의 닫힌 집합(단일 출처). resolver·JSON Schema(`schema.ts`)가 공유한다.
106
233
  * 아래 컴파일타임 단언이 이 튜플과 `SymbolicOp['kind']`의 일치를 강제 — 한쪽만 늘리면 타입 에러.
107
234
  */
108
- export declare const SYMBOLIC_OP_KINDS: readonly ["entity.add", "entity.update", "entity.remove", "attribute.add", "attribute.update", "attribute.remove", "association.add", "association.remove", "association.update", "associationEnd.update"];
235
+ export declare const SYMBOLIC_OP_KINDS: readonly ["entity.add", "entity.update", "entity.remove", "attribute.add", "attribute.update", "attribute.remove", "association.add", "association.remove", "association.update", "associationEnd.update", "index.add", "index.update", "index.remove", "operation.add", "operation.update", "operation.remove", "group.add"];
109
236
  export type SymbolicOpKind = (typeof SYMBOLIC_OP_KINDS)[number];
@@ -40,6 +40,8 @@ export declare function resizeGroup(groupId: ModelId, size: {
40
40
  width: number;
41
41
  height: number;
42
42
  }): Command;
43
+ /** 그룹 색상 변경 (레이아웃만 변경 — setEntitySwatch와 동형) */
44
+ export declare function setGroupSwatch(groupId: ModelId, token: string | null): Command;
43
45
  export declare function addAttribute(entityId: ModelId, attribute: Attribute): Command;
44
46
  /** 속성 필드 부분 수정 (name·type·dataType·dbAttrs·identifier·notNull 등). 얕은 patch. */
45
47
  export declare function updateAttribute(entityId: ModelId, attributeId: ModelId, patch: Partial<Attribute>): Command;
@@ -89,6 +91,8 @@ export declare function resizeNote(noteId: ModelId, size: {
89
91
  height: number;
90
92
  }): Command;
91
93
  export declare function setNoteMemo(noteId: ModelId, memo: string): Command;
94
+ /** 노트 색상 변경 (레이아웃만 변경 — setEntitySwatch와 동형) */
95
+ export declare function setNoteSwatch(noteId: ModelId, token: string | null): Command;
92
96
  /**
93
97
  * 노트를 그룹 박스 멤버로 편입(레이아웃 GroupLayout.memberNoteRefs). 이미 멤버면 no-op.
94
98
  * addEntityToGroup과 거울이나 대상이 논리(memberEntityRefs)가 아닌 레이아웃이다 — 노트가 순수
@@ -10,3 +10,48 @@ export declare function computeAutoLayout(logical: LogicalModel, layout: Diagram
10
10
  id: ModelId;
11
11
  location: Point;
12
12
  }[];
13
+ /**
14
+ * 높이 인지(height-aware) 박스 패킹 — 신규 엔티티 다수를 겹침 없이 격자 배치 (순수 함수, 엔진 무관).
15
+ *
16
+ * `computeAutoLayout`(고정 셀 격자)는 모든 엔티티가 같은 높이라는 가정이라, 속성 수가 많은 박스가
17
+ * 아랫줄을 침범한다. 이 함수는 박스마다 **속성 수에 비례한 높이를 추정**하고, 열별로 누적 Y(`colBottom`)를
18
+ * 추적해 다음 박스를 그 아래에 둔다 → 어떤 박스 높이 분포에서도 겹침이 구조적으로 발생하지 않는다.
19
+ *
20
+ * 배치 순서(좌→우, 위→아래 라운드로빈)는 입력 순서를 보존한다. 높이 추정은 의도적으로 넉넉히
21
+ * (간격이 뜨는 건 무해하나 겹침은 깨져 보임) — EntityNode가 `min-height: min-content`로 콘텐츠 구동이라
22
+ * 픽셀 정확이 아닌 선형 추정으로 충분하다.
23
+ *
24
+ * 좌표는 절대좌표. 기존 레이아웃 하단 아래에서 시작하려면 `baseY`를 넘긴다(호출자가 계산).
25
+ */
26
+ export interface EntityBoxSpec {
27
+ id: ModelId;
28
+ /** 엔티티의 속성(attribute) 개수 — 높이 추정 입력. */
29
+ attributeCount: number;
30
+ }
31
+ export interface PackedEntityBox {
32
+ id: ModelId;
33
+ location: Point;
34
+ size: {
35
+ width: number;
36
+ height: number;
37
+ };
38
+ }
39
+ export interface PackBoxesOptions {
40
+ cols: number;
41
+ boxWidth: number;
42
+ /** 박스 간(가로·세로) 여백. */
43
+ gap: number;
44
+ baseX: number;
45
+ baseY: number;
46
+ /** 헤더(스테레오타입+이름) 추정 높이. */
47
+ headerHeight: number;
48
+ /** 속성 1행 추정 높이. */
49
+ rowHeight: number;
50
+ /** 헤더·행 외 하단 여유(연산/인덱스 섹션 등) 추정 높이. */
51
+ footerHeight: number;
52
+ /** 박스 최소 높이(속성 0개여도 이 높이는 확보). */
53
+ minHeight: number;
54
+ }
55
+ /** 속성 수 → 박스 추정 높이 (넉넉히 over-reserve). */
56
+ export declare function estimateEntityHeight(attributeCount: number, opts?: Partial<PackBoxesOptions>): number;
57
+ export declare function packEntityBoxes(boxes: EntityBoxSpec[], options?: Partial<PackBoxesOptions>): PackedEntityBox[];
@@ -0,0 +1,41 @@
1
+ import { ModelId } from './types';
2
+ import { FieldAuditRecord } from './projectFieldAudit';
3
+ import { EntityAuditSubjectKind } from './projectEntityAudit';
4
+ export type ActionSubjectKind = EntityAuditSubjectKind | 'association' | 'associationEnd' | 'group';
5
+ /** 무엇이 바뀐 대상인가. entityRef는 자식(속성/연산/인덱스) subject의 부모(이름 조회·복원 스코프). */
6
+ export interface ActionSubject {
7
+ kind: ActionSubjectKind;
8
+ ref: ModelId;
9
+ entityRef?: ModelId;
10
+ end?: 'end1' | 'end2';
11
+ }
12
+ /** 한 대상의 한 필드 변경(old→new / tombstone). */
13
+ export interface ActionFieldChange {
14
+ field: string;
15
+ value: unknown;
16
+ oldValue?: unknown;
17
+ removed?: boolean;
18
+ }
19
+ /** 한 seq에서 한 대상에 일어난 액션 — verb + 변경 필드들(born=추가 요약, update=필드별). */
20
+ export interface SubjectAction {
21
+ subject: ActionSubject;
22
+ verb: 'add' | 'update' | 'remove';
23
+ fields: ActionFieldChange[];
24
+ }
25
+ /** 한 seq(= 한 op 배치 = 한 사용자 액션) 그룹. */
26
+ export interface ActionGroup {
27
+ seq: number;
28
+ ts?: string | number | Date;
29
+ origin?: string;
30
+ author?: string;
31
+ kind?: 'edit' | 'restore';
32
+ restoredFromSeq?: number;
33
+ actions: SubjectAction[];
34
+ }
35
+ /**
36
+ * op 이력 스트림을 seq(액션) 단위 그룹으로 투영한다(diagram-wide).
37
+ *
38
+ * @param records seq 정렬 이력 레코드.
39
+ * @returns seq **내림차순**(최신 먼저) 액션 그룹. 각 그룹은 subject별 액션(verb+필드)으로 구성.
40
+ */
41
+ export declare function projectActionLog(records: readonly FieldAuditRecord[]): ActionGroup[];
@@ -0,0 +1,22 @@
1
+ import { ModelId } from './types';
2
+ import { FieldAuditRecord, FieldAuditEntry } from './projectFieldAudit';
3
+ /** 감사 대상 종류 — 엔티티 자신 또는 자식 컬렉션. */
4
+ export type EntityAuditSubjectKind = 'entity' | 'attribute' | 'operation' | 'index';
5
+ /** 무엇이 바뀐 대상인가 — 피드에서 필드 값과 함께 표시. */
6
+ export interface EntityAuditSubject {
7
+ kind: EntityAuditSubjectKind;
8
+ /** 대상 ref. kind='entity'면 엔티티 자신(entityRef와 동일). */
9
+ ref: ModelId;
10
+ }
11
+ /** 엔티티 감사 피드 한 엔트리 — projectFieldAudit 엔트리 + 어느 대상인지(subject). */
12
+ export interface EntityAuditEntry extends FieldAuditEntry {
13
+ subject: EntityAuditSubject;
14
+ }
15
+ /**
16
+ * 한 엔티티의 감사 피드(엔티티 자신 + 모든 자식 속성/연산/인덱스의 필드 변경)를 seq 순으로 반환한다.
17
+ *
18
+ * @param records seq 정렬 이력 레코드(projectFieldAudit와 동일 입력). 필요 시 방어적 재정렬은 하위가 수행.
19
+ * @param entityRef 대상 엔티티.
20
+ * @returns seq 오름차순 병합 피드(각 엔트리에 subject 태그). 동일 seq는 원 삽입 순서 유지(안정 정렬).
21
+ */
22
+ export declare function projectEntityAudit(records: readonly FieldAuditRecord[], entityRef: ModelId): EntityAuditEntry[];
@@ -0,0 +1,65 @@
1
+ import { ModelId } from './types';
2
+ import { OpShape } from '../command/op';
3
+ /**
4
+ * 투영기 입력 — 이력 레코드의 lib-로컬 최소 형태(호스트 `OpHistoryRecord`의 부분집합).
5
+ * 호스트가 `{diagramId, seq}` 정렬로 조회해 넘긴다. lib는 호스트 타입에 의존하지 않으려 여기서 재선언한다.
6
+ */
7
+ export interface FieldAuditRecord {
8
+ /** diagram 내 단조 증가 좌표 — 타임라인 정렬 키(갭 허용, 연속성 가정 금지 §5.4). */
9
+ seq: number;
10
+ /** 서버 시각(감사 표시용 패스스루). */
11
+ ts?: string | number | Date;
12
+ /** op 출처(gui/agent/rest). 감사 "누가"의 현 해상도(경로 수준, §4 결정 2). */
13
+ origin?: string;
14
+ /**
15
+ * 개별 사용자 식별자(감사 "누가"의 per-user 해상도, §4 결정 2-(나)).
16
+ * 슬라이스 1/3c 레코드에는 없음(forward-only) — 있으면 그대로 타임라인에 패스스루한다(author-aware).
17
+ */
18
+ author?: string;
19
+ /** 편집 vs 복원 축(감사 표시). */
20
+ kind?: 'edit' | 'restore';
21
+ /** kind='restore'일 때 복원 대상 seq(패스스루). */
22
+ restoredFromSeq?: number;
23
+ /** ★필드 변경의 원천. edit=배치 그대로 / restore=합성 diff. */
24
+ ops: OpShape[];
25
+ }
26
+ /**
27
+ * 감사 대상 로케이터. 설정된 ref로 대상 종류(엔티티/속성/연산/인덱스/연관/연관end/그룹)가 결정된다.
28
+ * `field`를 주면 그 필드 단일 타임라인, 생략하면 대상의 모든 변경 필드.
29
+ */
30
+ export interface FieldAuditTarget {
31
+ entityRef?: ModelId;
32
+ attributeRef?: ModelId;
33
+ operationRef?: ModelId;
34
+ indexRef?: ModelId;
35
+ associationRef?: ModelId;
36
+ groupRef?: ModelId;
37
+ end?: 'end1' | 'end2';
38
+ /** 단일 필드로 좁힘(생략 시 전 필드). */
39
+ field?: string;
40
+ }
41
+ /** 타임라인 한 엔트리 — 한 필드가 한 seq에서 얻은 값(또는 tombstone). */
42
+ export interface FieldAuditEntry {
43
+ seq: number;
44
+ ts?: string | number | Date;
45
+ origin?: string;
46
+ author?: string;
47
+ kind?: 'edit' | 'restore';
48
+ restoredFromSeq?: number;
49
+ /** 바뀐 필드명. 멤버십은 'memberEntityRefs'. */
50
+ field: string;
51
+ /** 신규 값. null=필드 비움(clear). removed=true면 undefined. */
52
+ value: unknown;
53
+ /** 직전 엔트리에서 파생한 이전 값(§3.3-(가)). 최초 born·재-add는 없음. */
54
+ oldValue?: unknown;
55
+ /** 대상/필드가 이 seq에 소멸(remove/entity.remove/group.removeMember). */
56
+ removed?: boolean;
57
+ }
58
+ /**
59
+ * op 이력 스트림을 특정 대상의 per-field 변경 타임라인으로 투영한다(순수).
60
+ *
61
+ * @param records seq 오름차순 이력 레코드(방어적으로 seq 재정렬). 각 `ops`가 필드 변경 원천.
62
+ * @param target 대상 로케이터(+ 선택적 field 필터).
63
+ * @returns seq 순 필드 감사 엔트리 배열(edit·restore 혼합, born·update·tombstone·멤버십).
64
+ */
65
+ export declare function projectFieldAudit(records: readonly FieldAuditRecord[], target: FieldAuditTarget): FieldAuditEntry[];
@@ -0,0 +1,21 @@
1
+ import { DiagramLayout, LogicalModel } from './types';
2
+ import { OpShape } from '../command/op';
3
+ /** 합성기 입력 — 정합한 완전 상태(logical + layout). 호스트는 v2 doc 서브트리를 그대로 넘긴다(rev/version은 무시). */
4
+ export interface DiagramSnapshotState {
5
+ logical: LogicalModel;
6
+ layout: DiagramLayout;
7
+ }
8
+ /**
9
+ * null≡undefined≡absent 정규화 deep-equal. 스칼라/배열(인덱스별)/객체(키 합집합)를 재귀 비교한다.
10
+ * (mongo가 optional 필드 clear를 null로 저장하는 것과 target의 absent를 동치로 본다.)
11
+ */
12
+ export declare function deepEqual(a: unknown, b: unknown): boolean;
13
+ /**
14
+ * current → target 복원 diff를 forward op으로 합성한다(순수, 감사 전용 — 실행 안 됨).
15
+ *
16
+ * 산출 순서(결정성): logical(entities → associations → groups; 각 remove → add → update/child) →
17
+ * layout(entityLayouts → associationLayouts → groupLayouts → notes). 각 컬렉션은 key 정렬.
18
+ *
19
+ * @returns OpShape[] — CAS 메타(rev 숫자·layout.version) 없는 어휘 형태(호스트 이력 레코드 `ops`에 그대로 기록).
20
+ */
21
+ export declare function synthesizeRestoreOps(current: DiagramSnapshotState, target: DiagramSnapshotState): OpShape[];
@@ -3,6 +3,7 @@ import { EditorState } from '../command/types';
3
3
  import { DiagramMeta } from '../adapter/diagramAdapter';
4
4
  import { OpConflict } from '../command/op';
5
5
  import { OpSyncTransport } from '../command/opSync';
6
+ import { FieldAuditRecord } from '../core/projectFieldAudit';
6
7
  import { ResolvedDiagram } from '../core/resolve';
7
8
  import { RoutingMode } from '../core/routing';
8
9
  import { ValidationIssue } from '../core/validation';
@@ -36,6 +37,22 @@ export type Selection = {
36
37
  ids: ModelId[];
37
38
  anchor: ModelId;
38
39
  } | null;
40
+ /**
41
+ * (나) 필드 감사 조회 transport — 호스트가 주입하는 이력 조회 함수. diagram의 op 이력 스트림을
42
+ * seq 순 `FieldAuditRecord[]`로 반환한다. `sinceSeq`를 주면 그 이후만(증분 fetch, 기본 전량).
43
+ * op emit의 `OpSyncTransport`와 대칭 — 쓰기는 op 배치, 읽기는 감사 레코드.
44
+ */
45
+ export type HistoryTransport = (sinceSeq?: number) => Promise<FieldAuditRecord[]>;
46
+ /** 복원 결과 — 호스트 restore 라우트 응답의 lib-로컬 최소 형태(ok + 실패 사유). */
47
+ export interface RestoreOutcome {
48
+ ok: boolean;
49
+ reason?: string;
50
+ }
51
+ /**
52
+ * 시점 복원 transport — 호스트가 주입하는 복원 함수(`restoreEntityDiagram` 라우트). toSeq 시점 상태로
53
+ * forward 보상 쓰기(diagram-wide). 성공 시 라이브 doc이 바뀌므로 controller가 reloadRequest로 재적재를 요청한다.
54
+ */
55
+ export type RestoreTransport = (toSeq: number) => Promise<RestoreOutcome>;
39
56
  export interface EditorController {
40
57
  state: EditorState;
41
58
  resolved: ComputedRef<ResolvedDiagram>;
@@ -57,6 +74,8 @@ export interface EditorController {
57
74
  canUndo: Ref<boolean>;
58
75
  canRedo: Ref<boolean>;
59
76
  dirty: Ref<boolean>;
77
+ /** 모델 변경 신호 — 명령 적용마다 단조 증가. 외부 관찰자(감사 패널 등)가 재조회 트리거로 watch(디바운스 권장). */
78
+ revision: Ref<number>;
60
79
  /**
61
80
  * 편집 가능 여부 — false(조회 모드)면 모든 모델 변경(추가·삭제·이동·편집·재정렬·연결·붙여넣기·
62
81
  * undo/redo·저장)이 차단된다. 선택·팬/줌·hover 강조·복사·export 같은 읽기성 동작은 유지.
@@ -87,6 +106,28 @@ export interface EditorController {
87
106
  * `loadDiagram`으로 재적재하면 해소된다(B-5). op-mode가 아니면 항상 null.
88
107
  */
89
108
  opConflict: Ref<OpConflict | null>;
109
+ /**
110
+ * (나) 필드 감사 조회 transport — 호스트가 이력 조회 라우트(`entityDiagramHistory.get`) fetch 래퍼를
111
+ * 주입(opTransport 미러). 감사 패널이 호출해 이력 레코드를 얻고 `projectEntityAudit`로 투영한다.
112
+ * 미주입(dev/test·비-op doc) 시 undefined → 감사 패널 비활성.
113
+ */
114
+ historyTransport?: HistoryTransport;
115
+ /** 감사 패널 사용 가능 여부 — op-mode + historyTransport 주입 양쪽 충족. */
116
+ historyAvailable: ComputedRef<boolean>;
117
+ /** 시점 복원 transport(호스트 주입) — 감사 패널 '이 시점으로 복원'이 호출. */
118
+ restoreTransport?: RestoreTransport;
119
+ /** 복원 사용 가능 여부 — op-mode + editable + restoreTransport 주입(조회 모드·비-op은 불가). */
120
+ restoreAvailable: ComputedRef<boolean>;
121
+ /**
122
+ * 특정 seq 시점으로 복원 요청 — restoreTransport 위임. 성공(ok) 시 reloadRequest를 올려 호스트 재적재를 유도한다.
123
+ * transport 미주입/실패 시 { ok:false } 반환(패널이 사유 표시). editable=false면 no-op({ ok:false }).
124
+ */
125
+ restoreToSeq(toSeq: number): Promise<RestoreOutcome>;
126
+ /**
127
+ * 재적재 요청 신호(단조 증가) — 복원처럼 라이브 doc이 서버에서 바뀐 뒤, 호스트가 이 값을 watch해
128
+ * 최신 doc을 refetch → loadDiagram으로 갈아끼운다(focusRequest nonce와 같은 신호 패턴).
129
+ */
130
+ reloadRequest: Ref<number>;
90
131
  /**
91
132
  * 프리즈 후(또는 외부 변경 감지 시) 재적재 경로 — 호스트가 `fromPersisted`로 파싱한 state/meta를 주입한다
92
133
  * (생성자와 대칭 시그니처). state를 in-place 교체하고 스택을 리셋하며, 마커(schemaVersion/opWriteEnabled/
@@ -183,6 +224,8 @@ export interface EditorController {
183
224
  }): void;
184
225
  /** 그룹 필드 부분 수정 (name·description·packageName·excludeDDLGeneration) */
185
226
  updateGroup(groupId: ModelId, patch: Partial<LogicalGroup>): void;
227
+ /** 그룹 박스 색상 토큰 지정 (null이면 해제 → 기본 그룹 배경) */
228
+ setGroupSwatch(groupId: ModelId, token: string | null): void;
186
229
  /** 선택 엔티티들로 새 그룹 생성 (멤버 bbox에 박스 배치) */
187
230
  createGroup(entityIds: ModelId[], name?: string): ModelId | null;
188
231
  /** 빈 그룹 박스 생성 (이후 드래그로 엔티티 편입) */
@@ -326,6 +369,8 @@ export interface EditorController {
326
369
  height: number;
327
370
  }): void;
328
371
  setNoteMemo(noteId: ModelId, memo: string): void;
372
+ /** 노트 색상 토큰 지정 (null이면 해제 → 기본 노트 배경) */
373
+ setNoteSwatch(noteId: ModelId, token: string | null): void;
329
374
  /** 노트 → 대상(엔티티) 연결선 추가/삭제 + 꺾은점 편집 */
330
375
  addNoteConnection(noteId: ModelId, targetRef: ModelId): void;
331
376
  removeNoteConnection(noteId: ModelId, targetRef: ModelId): void;
@@ -358,6 +403,13 @@ export declare function createEditorController(initial?: EditorState, options?:
358
403
  * 어댑터가 활성(미주입 = dev/test → 어댑터 비활성, full-replace 저장 경로 유지).
359
404
  */
360
405
  opTransport?: OpSyncTransport;
406
+ /**
407
+ * (나) 필드 감사 조회 transport(`entityDiagramHistory.get` fetch 래퍼). 주입 + op-mode 시에만 감사
408
+ * 패널 활성(미주입 = dev/test·비-op doc → 패널 비활성). opTransport(쓰기)와 대칭인 읽기 채널.
409
+ */
410
+ historyTransport?: HistoryTransport;
411
+ /** 시점 복원 transport(`restoreEntityDiagram` 라우트). 주입 + op-mode + editable 시 감사 패널 '복원' 활성. */
412
+ restoreTransport?: RestoreTransport;
361
413
  /**
362
414
  * 검증 컨텍스트 — 카탈로그/정책 의존 규칙을 깨우는 호스트 주입(미주입 시 해당 규칙 skip).
363
415
  * selectableDataTypes(CHK-TYPE-1)는 코어 상수(SELECTABLE_DATA_TYPES)라 dev/호스트가 즉시 주입 가능.