import { Command, EditorState } from '../command/types'; import { ReconcilePlan } from '../core/propagation'; import { Attribute, ClassStereotype, DbColumn, Entity, LogicalModel, ModelId, Point } from '../core/types'; /** * 한 조작이 지우는 속성의 누산기 — 인덱스 컬럼 정리를 **조작당 한 번**만 방출하기 위한 것. * * ★왜 누산해야 하나: 정리 패치(`index.update`)는 `columns` 배열을 **통째로 교체**하고, 계획은 * 구성 시점에 고정된다(결정적 redo). 그래서 같은 엔티티의 속성을 두 경로가 각각 지우면서 각자 * 정리를 내면 **뒤 패치가 앞 정리를 되돌린다** — 자기참조 관계가 정확히 그 형상이다(관계 삭제 시 * 부모 inverse nav 제거와 자식 FK reconcile 제거가 *같은 엔티티*에 떨어진다). 사이트마다 이 배열을 * 넘기고 마지막에 `indexCleanupCommands`를 한 번 부르면 그 충돌이 구조적으로 불가능해진다 * (resolver 가 `OrderAllocator`·`removedAttrDedup`을 스레딩하는 것과 같은 관용구). */ export type RemovedAttributes = { entityId: ModelId; attributeId: ModelId; }[]; /** * 속성 제거 명령 — **`cmd.removeAttribute`를 직접 부르지 않고 항상 이것을 쓴다.** 제거 사실을 * `removed`에 기록해 인덱스 정리가 빠지지 않게 한다(그 누락이 이 트랙이 고치는 결함이다). */ export declare function removeAttributeCommands(entityId: ModelId, attributeIds: readonly ModelId[], removed: RemovedAttributes): Command[]; /** * 누산된 제거가 남길 인덱스 dangling 정리 명령 — 조작의 서브명령 **맨 끝**에 한 번 붙인다. * `logical`은 제거 **전** 상태여야 한다(사라지는 속성의 dbAttr modelId를 알아야 하므로). * `survivingRefs`는 그대로 통과시킨다 — 의미·근거는 `core/indexCleanup.planIndexColumnCleanup` 소유. */ export declare function indexCleanupCommands(logical: LogicalModel, removed: RemovedAttributes, survivingRefs?: ReadonlyMap): Command[]; /** * 식별자 전파 reconcile 계획을 자식 엔터티 대상 서브명령 배열로 변환. * 1차 변경(관계 생성/삭제 등)과 함께 composite로 묶으면 전파 FK 동기화가 하나의 undo/redo * 원자 단위가 된다 — 부분 undo로 고아 FK가 남지 않는다. * 순서(remove→update→add)는 각기 다른 속성을 대상으로 해 상호 간섭이 없다. * * ★**owner 앵커 와이어링(CC-3)** — 계획이 관계의 FK를 새로 만들면 `end2.attributeRef`를 그 FK로 맞춘다. * 종전에는 속성 CRUD만 방출해 앵커가 죽은 id를 계속 가리켰고, 코드젠 `relByAttr`(scaffoldJava:454)이 * 해소에 실패해 **관계가 산출에서 사라졌다**. 두 형상을 함께 덮는다: * - **교체**: 부모 PK가 바뀌어 구 FK remove + 신 FK add가 같이 나는데 앵커가 구 FK를 가리킴 * → 모델은 완전한데 산출만 소실되는 최악 형상. * - **재획득**: 부모가 PK를 얻어 FK가 add되는데 앵커가 미설정/죽은 상태 → cascade add 경로엔 * 앵커 배선이 없었다(생성 경로 `addAssociation:1947`·임베더블 전환 `:749`에만 있었다). * 살아남는 속성을 가리키는 앵커는 건드리지 않는다(멱등 + 사용자/기존 상태 존중). * * ⚠️ **예방 전용 — 기존 dangling은 복구하지 않는다.** 이 함수는 전파 계획이 있을 때만 호출되므로, * 이미 앵커가 죽은 문서는 그 자식에 계획이 발동하는 편집(부모 식별자 변경 등)이 있어야 정정된다. * 엔티티명 변경 같은 비-식별자 편집은 계획이 비어 도달하지 않는다(실측: 실 데이터 유일 사례 * `Cart` self-assoc은 부모 rename으로 정정되지 않음). 기존 분의 복구는 로드-타임 파생 + 통합 스윕 * (`planHealBackfillOps`가 이미 `end1.attributeRef`를 diff하므로 end2 대칭 확장) 소관 — 별 결정. * * ⚠️ 순수 제거(부모가 PK를 잃음·임베더블 전환)로 **가리킬 FK가 아예 없는 경우는 해제하지 않는다** — * 그 상태는 전파 산출이 0이라 방출할 관계도 없어 무해하고, `CHK-JPA-19`도 같은 근거로 면제한다(관계가 * 아니라 부모 PK 부재가 고칠 자리 = `CHK-JPA-1`). 호스트 검출기 규칙 5(`owner-fk-attr`)에는 남으므로 * 축적 위반 정리(B축) 소관. * ※ 종전 이 자리에 적힌 "undefined가 wire에서 탈락해(CC-8) clear를 표현할 수 없다"는 **해소됐다** — * `toWirePatch`(op.ts)가 update patch의 undefined를 null로 정규화하므로 `{attributeRef: undefined}`도 * 서버에 clear로 도달한다. 즉 위 판단은 이제 **전송 제약이 아니라 도메인 판단**으로만 서 있다(해제해도 * 무해하지만 얻는 것이 없다). 해제로 전환하려면 CC-3 복구 트랙에서 함께 결정할 것. */ export declare function reconcilePlanToCommands(logical: LogicalModel, childId: ModelId, plan: ReconcilePlan, removed: RemovedAttributes): Command[]; /** logical을 얕게 복제하되 한 엔터티의 attributes만 transform 결과로 교체(읽기 전용 전파 계획 입력용). */ export declare function withEntityAttributes(logical: LogicalModel, entityId: ModelId, transform: (attrs: Attribute[]) => Attribute[]): LogicalModel; /** * 부모(parentId) 식별자 변경이 자식·후손으로 번지는 연쇄 reconcile 서브명령. * futureLogical은 변경이 이미 반영된 상태여야 한다(construction 시점 계획 고정 → 결정적 redo). * 영향 없는 변경(비식별 속성 등)이면 빈 배열을 돌려준다(planPropagation이 빈 계획만 산출). * * **임베드 슬롯 보충도 여기서 함께 방출**한다 — 편집된 엔티티가 임베더블이면 사용처(owner)의 * `EMBED_OWN` 표시 속성 슬롯을 선언 컬럼 수까지 채운다(FK 전파의 임베드 짝, `core/embedSlots`). * 이 seam을 고른 이유: 속성 추가·수정·삭제·임베더블 전환의 모든 진입점이 이미 여기를 지나므로 * 트리거를 한 곳에만 두면 되고, 서브명령이 같은 composite에 실려 **undo가 원자적**이다. */ export declare function cascadeCommands(futureLogical: LogicalModel, parentId: ModelId, removed: RemovedAttributes): Command[]; /** * FK 전파 **단독** — 임베드 슬롯 보충을 «다른 시점»에 계산해야 하는 호출자용. * * ★필요해진 이유: remove+add 합성 조작(`convertAttributeType`)은 전파를 «삭제 후» 모델로, 보충을 * «추가 후» 모델로 재야 한다. 둘을 묶어 두면 한쪽이 반드시 틀린 시점을 본다. ⚠️**둘 다 부르면 같은 * 슬롯 배열을 두 커맨드가 겨냥한다** — 최종 결과는 나중 것이 이겨 맞지만, 같은 배열을 여러 op 가 * 겨냥하는 형태는 이 리포가 호스트 500 으로 대가를 치른 자리다(`op-interpreter-pull-merge-asymmetry`). */ export declare function propagationCommands(futureLogical: LogicalModel, parentId: ModelId, removed: RemovedAttributes): Command[]; /** * 임베더블 속성 재정렬이 사용처 슬롯으로 번지는 서브명령(`core/embedSlots.planEmbedSlotReorder`). * 재정렬 커맨드와 **같은 composite**에 실어 undo가 원자적이게 한다(보충이 `cascadeCommands`에 실리는 * 것과 같은 규율). 대상이 임베더블이 아니거나 게이트에 걸리면 빈 배열 = 재정렬만 실행된다. * * 순서 산식은 `reorderedSequence` 단일 출처를 쓴다 — 커맨드가 apply에서 쓰는 그 함수라 계획과 실행이 * 어긋날 수 없다(여기서 splice를 다시 쓰면 그 지점이 드리프트 후보가 된다). */ export declare function embedSlotReorderCommands(logical: LogicalModel, entityId: ModelId, attrIds: ModelId[], toIndex: number): Command[]; /** * 임베더블 속성 **삭제**가 사용처 슬롯으로 번지는 서브명령(`core/embedSlots.planEmbedSlotRemoval`). * 위 재정렬 짝과 같은 형태다 — 삭제 명령과 **같은 composite**에 실어 undo 를 원자적으로 만든다. * * ★`cascadeCommands`(보충 축이 사는 자리)에 얹지 않고 **전용 seam**으로 둔 이유: 그쪽은 조작이 **반영된 * 미래 모델**을 받아 «무엇이 사라졌는가»를 알 수 없다. 삭제 전파는 사라지는 슬롯의 정체가 입력이라 * **삭제 전 `logical` + 지울 id 목록**이 필요하고, 그 시그니처가 정확히 재정렬 짝과 같다. * * `logical`은 삭제 **전** 상태여야 한다(인덱스 정리 `indexCleanupCommands`와 같은 계약). */ export declare function embedSlotRemovalCommands(logical: LogicalModel, entityId: ModelId, removedAttributeIds: readonly ModelId[]): Command[]; /** * 위와 같은 계획 + **적용 후 모델**. 삭제 전파 «뒤에» 다른 슬롯 계획기를 이어 붙일 때 필요하다. * * ⚠️★**투영 없이 이어 붙이면 방금 지운 슬롯이 되살아난다** — 슬롯 계획기들은 patch 가 아니라 **전체 * `dbAttrs` 배열**을 내므로, 삭제 «전» 모델로 계산한 보충 명령이 나중에 적용되면 삭제분을 포함한 * 배열로 통째 덮어쓴다. 그래서 이어 붙이는 쪽은 커맨드가 아니라 **모델**을 받아야 한다. */ export declare function embedSlotRemovalPlan(logical: LogicalModel, entityId: ModelId, removedAttributeIds: readonly ModelId[]): { commands: Command[]; projected: LogicalModel; }; /** * 임베더블 **컬럼 성장** 보충만 떼어낸 서브명령(`cascadeCommands` 와 같은 계획기·같은 계약). * * ★★**왜 따로 필요한가 — `cascadeCommands` 는 «삭제 후» 모델을 받는다.** remove+add 로 합성되는 조작 * (`convertAttributeType` = 속성 타입 전환)에서는 그 시점의 선언이 «추가 전»이라 보충이 **원리적으로 * 발동할 수 없고**(선언 컬럼 수 ≤ 사용처 슬롯 수), 결과적으로 사용처가 미정렬로 남아 **owner DDL 에서 * 그 컬럼들이 사라진다**(실측: 1→2컬럼 전환 후 `amount`·`currency` 2컬럼 소실 · 다음 임베더블 편집이 * 우연히 치유). ⇒ **계획 판정 기준은 «계획 적용 후» 상태**(`(기존−remove)∪add`)여야 한다 — 이 리포가 * FK 전파에서 두 번 대가를 치른 그 축의 세 번째 사본이다. */ export declare function embedSlotFillCommands(logical: LogicalModel, embeddableRef: ModelId): Command[]; /** * "자식의 FK 집합이 직접 바뀌는" 트리거(관계 삭제·identifying 토글)용 연쇄 서브명령. * 1) seed 자식을 직접 reconcile(FK 증감/플립), 2) 그 변화를 적용한 상태로 후손 cascade. * (부모 식별자 자체가 바뀌는 트리거는 부모 자신 reconcile이 불필요해 cascadeCommands를 직접 쓴다.) * futureLogical은 트리거 변경이 이미 반영된 상태여야 한다(construction 시점 계획 고정). */ export declare function childReconcileWithCascade(futureLogical: LogicalModel, childIds: readonly ModelId[], removed: RemovedAttributes): Command[]; /** * **컬럼 축이 사라질 때 그 축의 «모든» 필드를 정리**하는 서브명령 — 직렬화 타입 전환 전용. * * ★**왜 함수로 뽑았나**: 전환은 두 경로로 들어온다 — 일반 엔티티에서 오는 `enteringValueType` 과 * **임베더블에서 오는 `betweenValueTypes`**. 처음엔 전자에만 정리를 달았고, 그 결과 실제 마이그레이션의 * 주 경로(임베더블 → 직렬화 타입)가 **컬럼·테이블을 그대로 달고 전환**됐다(2026-08-29 프로덕션 실측: * `CartGift` 가 `table=cart_gift` · 속성 5개 전부 1컬럼을 유지한 채 `SERIALIZED_TYPE` 이 됐다). * ⇒ 「축이 사라지면 그 축의 모든 필드를 정리」 규율의 **재발이고, 이번엔 필드가 아니라 «분기»를 빠뜨린 * 형태**다. 한 함수로 모아 두 분기가 같은 것을 쓰게 한다. * * ★대상은 **컬럼 축뿐**(`isColumnlessStereotype`) — 임베더블은 owner 테이블로 평탄화돼 **실제로 컬럼을 * 가지므로** 지우면 안 된다. 값 타입 축(테이블·PK)은 둘이 공유하지만 컬럼 축은 갈리는 자리다. * * @param removedIds 이 조작이 «이미» 제거한 속성(FK reconcile 산물) — 판정은 계획 **적용 후** 상태로 * 해야 한다(memory `plan-diff-judgment-uses-post-plan-state`). 없으면 빈 집합. */ export declare function columnAxisCleanupCommands(entity: Entity, to: ClassStereotype, removedIds?: ReadonlySet): Command[]; export declare function flattenEmbeddableColumns(embeddable: Entity): DbColumn[]; /** * 임베더블 → 일반 엔터티 역전환의 부수 명령(stereotype update 명령에 합성). * 임베더블이 1급 엔터티가 되면 자체 테이블·PK를 가지므로: * 1) PK 자동 부여 — 식별자가 하나도 없으면 기본 PK(id/Long/BIGINT, addEntity 기본형과 동일)를 신설. * 2) 이 임베더블(id)을 임베드하던 EMBED 관계를 일반 관계로 전환 — owner가 보유한 EMBED_OWN 표시속성 * (embedded.embeddableRef === id)을 제거하고, 관계 ends를 owner=end1(부모)/X=end2(자식)로 정규화한다. * composition·embed attributeRef·legacyEmbedRaw를 버려야 저장 분기(assocToEmbed·legacyEmbedRaw)가 * 더는 EMBED로 환원하지 않는다. owner를 부모로 유지해 X(end2 자식)가 owner PK를 FK로 받는다(전파). * plan은 construction 시점 고정(결정적 redo) — childReconcileWithCascade와 동일 산식을 인라인한다. */ export declare function leavingEmbeddableCommands(logical: LogicalModel, id: ModelId): Command[]; /** * `end1.navigable` 토글의 동반 명령 — inverse nav(부모 `RELATION_REF` 속성) **생성/삭제**. * * ★**여기로 추출된 경위**(2026-09-03): 종전엔 `editor/association.ts` 의 `updateAssociationEnd` 에 * 인라인돼 있었고, 그래서 AI(op) 경로가 `navigable-toggle-unsupported` 로 **통째 거부**됐다. 조립을 * 순수 함수로 내리면 GUI 는 `Command` 로 실행하고 AI 는 `planOpsFromCommands` 로 op 로 펼친다 * (`enteringValueTypeCommands` 와 같은 처방 · 사용자 목표: *「사람이 조작 안 하는 게 목표」*). * * ★**불변식**: `navigable=true ⟺ 부모에 RELATION_REF nav 속성 존재`(단일 진실). 이것이 깨지면 로드 * 자가치유가 클라마다 비결정 실체화를 반복하거나(true) 실체화된 nav 가 고아로 남는다(false) — * 그래서 「patch 만 흘리기」가 금지였고, 동반 명령을 함께 내는 이 함수가 그 답이다. * * ⚠️**end2 는 대상이 아니다** — JPA 소유측 참조는 관계 존재와 동치라 고정 true 이고, 그쪽은 **불변식이라 * 닫힌 채가 정답**이다(참조 없는 FK 는 관계가 아니라 일반 컬럼으로 모델링). * ⚠️EMBED 관계는 제외 — 표시 속성 모델이 별도다. * * 반환은 **동반 명령만**이다(`updateAssociationEnd` 자체는 호출부가 낸다 — `leavingEmbeddableCommands` * 와 같은 계약). ⚠️단 end patch 에 실어야 할 **`attributeRef`·`navFieldNameHint`** 가 이 계획에서 * 결정되므로 그 조각을 함께 돌려준다(호출부가 자기 patch 에 머지). */ export declare function navigableToggleCommands(logical: LogicalModel, assocId: ModelId, next: boolean): { cmds: Command[]; endPatch: Record; } | null; /** * 값 타입(임베더블·직렬화 타입)으로 **진입**할 때의 동반 명령 — `updateEntity(stereotype)` 앞뒤에 합성한다. * * ★**여기로 추출된 경위**(2026-09-03): 종전엔 `editor/entity.ts` 의 `updateEntity` 분기에 인라인돼 있었고, * 그래서 **AI(op) 경로가 이 전환을 아예 표현할 수 없었다**(`stereotype-embeddable-transition-unsupported`). * 조립을 순수 함수로 내리면 GUI 는 `Command` 로 실행하고 AI 는 `planOpsFromCommands` 로 op 로 펼친다 — * **같은 계획을 두 소비처가 나눠 쓴다**(사용자 목표: *「궁극적으로 사람이 조작 안 하는 게 목표」*). * * 불변식 강제: 값 타입은 `@Id` 를 가질 수 없고(PK 해제) 자체 테이블이 없어 FK 컬럼도 보유 불가 * (보유 derived FK 제거 + 식별자 소멸을 자식으로 cascade). * * ★**직렬화 타입은 EMBED 복원을 타지 않는다** — 관계로 임베드되지 않고 소비 속성 `type` 으로 지목된다 * (결정 3-①). ★**컬럼 축 정리도 직렬화 타입에만** 건다(`isColumnlessStereotype`) — 임베더블은 owner * 테이블로 평탄화돼 **실제로 컬럼을 가지므로** 지우면 안 된다. * ★★**판정은 계획 «적용 후» 상태로 한다** — `childReconcileWithCascade` 가 보유 FK 를 **제거**하므로 * 전환 «전» 속성 목록으로 명령을 만들면 이미 사라진 속성을 겨냥해 composite 이 중간에 실패한다 * (memory `plan-diff-judgment-uses-post-plan-state` 의 세 번째 재발이었다). `embRemoved` 가 그 제거분이다. * * ⚠️게이트(임베드되고 있는 임베더블을 직렬화 타입으로 전환 · 관계를 가진 엔티티의 직렬화 전환)는 **호출부 * 소유**다 — 차단은 *안내 emit* 을 수반하고 그건 사용자 intent 층의 일이다. */ export declare function enteringValueTypeCommands(logical: LogicalModel, id: ModelId, to: ClassStereotype): Command[]; export declare function enteringEmbeddableCommands(logical: LogicalModel, id: ModelId): Command[]; /** * 그룹 이동 명령 집합 — 그룹 박스 + 멤버 절대좌표 + 내부 관계 waypoint를 같은 delta로. * 드래그(`./group` dragGroup)·키보드(`./selection` nudgeNodes) 양쪽에서 재사용; 호출부가 * composite로 감싸 한 명령으로 실행한다. * * ★**이 파일에서 유일하게 `LogicalModel`이 아니라 `EditorState`를 받는다**(위 헤더 계약의 예외). * 그룹 이동은 본질적으로 레이아웃 연산이라 `groupLayouts`·`associationLayouts`·`notes`를 읽어야 * 하고, 그러면서 멤버십 판정(`logical.groups`)·내부 관계 판정(`logical.associations`)도 필요해 * 양 뎁스를 함께 본다. 순수성(같은 입력 → 같은 `Command[]`)은 그대로다. * ★**여기로 내려온 경위**(VE-6 결정 ②): 팩토리 클로저 안에 있으면서 `./selection`에 주입되고 * `dragGroup`(컨트롤러)도 쓰던 함수다. S8이 그룹 축을 분리하면서 **소비자 둘 다 외부 모듈**이 * 되어, 팩토리가 자기는 쓰지 않는 헬퍼를 들고 두 모듈에 배관만 하는 형태가 됐다 ⇒ 중립 위치인 * 여기로 내리고 양쪽이 직접 import 한다(`SelectionContext`의 주입 항목이 하나 줄었다). * `selection.ts` 헤더가 *"세 번째 소비자가 생길 때"*로 적어 둔 트리거의 실제 발동 사유는 * 개수가 아니라 **팩토리가 소비자에서 빠진 것**이었다. */ export declare function groupMoveCommands(state: EditorState, groupId: ModelId, location: Point, members: { id: ModelId; location: Point; }[]): Command[];