From b77287dc9c0a5b3046fb6dbc2885dcad44d30bc3 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Wed, 25 Mar 2026 12:17:57 +0900 Subject: [PATCH 1/8] docs(patchmap): add reference guides --- docs/memory.md | 7 + .../2026-03-25-patchmap-feature-guides.md | 67 ++ docs/reference/patchmap/coverage-checklist.md | 105 +++ docs/reference/patchmap/data-model.md | 394 ++++++++++++ .../patchmap/history-and-transformer.md | 258 ++++++++ docs/reference/patchmap/interactions.md | 202 ++++++ docs/reference/patchmap/overview.md | 304 +++++++++ docs/reference/patchmap/public-api.md | 603 ++++++++++++++++++ .../03-25/patchmap-feature-guides/BRIEF.md | 28 + .../03-25/patchmap-feature-guides/STATUS.md | 6 + .../patchmap-feature-guides/logs/DECISIONS.md | 11 + .../patchmap-feature-guides/logs/WORKLOG.md | 9 + 12 files changed, 1994 insertions(+) create mode 100644 docs/memory.md create mode 100644 docs/plans/2026-03-25-patchmap-feature-guides.md create mode 100644 docs/reference/patchmap/coverage-checklist.md create mode 100644 docs/reference/patchmap/data-model.md create mode 100644 docs/reference/patchmap/history-and-transformer.md create mode 100644 docs/reference/patchmap/interactions.md create mode 100644 docs/reference/patchmap/overview.md create mode 100644 docs/reference/patchmap/public-api.md create mode 100644 docs/tasks/2026/03-25/patchmap-feature-guides/BRIEF.md create mode 100644 docs/tasks/2026/03-25/patchmap-feature-guides/STATUS.md create mode 100644 docs/tasks/2026/03-25/patchmap-feature-guides/logs/DECISIONS.md create mode 100644 docs/tasks/2026/03-25/patchmap-feature-guides/logs/WORKLOG.md diff --git a/docs/memory.md b/docs/memory.md new file mode 100644 index 00000000..902d83f1 --- /dev/null +++ b/docs/memory.md @@ -0,0 +1,7 @@ +- Current phase: documenting the existing `patch-map` feature set against the current JS/TS implementation. +- Canonical reusable documentation belongs under `docs/reference/patchmap/`. +- Active task workspace: `docs/tasks/2026/03-25/patchmap-feature-guides/`. +- Documentation should map code-level behavior to user-facing usage with links to the relevant source files. +- Feature coverage must include public exports, map data schema, rendering elements/components, interaction APIs, history, and transformer behavior. +- Use subagents with narrow read/write scopes and keep their outputs reviewed before merge. +- Final completion requires a fresh coverage check against current exports, README API sections, and relevant source modules. diff --git a/docs/plans/2026-03-25-patchmap-feature-guides.md b/docs/plans/2026-03-25-patchmap-feature-guides.md new file mode 100644 index 00000000..298aa45d --- /dev/null +++ b/docs/plans/2026-03-25-patchmap-feature-guides.md @@ -0,0 +1,67 @@ +# Patchmap Feature Guides Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Produce repository-local feature guides for the current `patch-map` implementation, with project-context artifacts and explicit coverage verification. + +**Architecture:** Keep durable task context in `docs/tasks/...`, write canonical guides in `docs/reference/patchmap/`, and split drafting by independent feature domains. Review each drafted guide against current source behavior before finalizing the coverage matrix. + +**Tech Stack:** Markdown, repository source (`README.md`, `src/**/*.js`, `src/**/*.ts`), project-context task files, subagents for drafting/review. + +--- + +### Task 1: Bootstrap Project Context + +**Files:** +- Create: `docs/memory.md` +- Create: `docs/tasks/2026/03-25/patchmap-feature-guides/BRIEF.md` +- Create: `docs/tasks/2026/03-25/patchmap-feature-guides/STATUS.md` +- Create: `docs/tasks/2026/03-25/patchmap-feature-guides/logs/DECISIONS.md` +- Create: `docs/tasks/2026/03-25/patchmap-feature-guides/logs/WORKLOG.md` + +**Step 1:** Create the task workspace and global memory. + +**Step 2:** Record the documentation scope, assumptions, and validation strategy. + +### Task 2: Inventory Public Features + +**Files:** +- Modify: `docs/tasks/2026/03-25/patchmap-feature-guides/BRIEF.md` +- Modify: `docs/tasks/2026/03-25/patchmap-feature-guides/STATUS.md` +- Append: `docs/tasks/2026/03-25/patchmap-feature-guides/logs/WORKLOG.md` + +**Step 1:** Read exported APIs and README feature sections. + +**Step 2:** Group features into independent documentation domains. + +### Task 3: Draft Domain Guides in Parallel + +**Files:** +- Create: `docs/reference/patchmap/*.md` +- Append: `docs/tasks/2026/03-25/patchmap-feature-guides/logs/WORKLOG.md` + +**Step 1:** Dispatch subagents with disjoint write scopes. + +**Step 2:** Draft guides for each feature domain with source-backed usage notes. + +### Task 4: Review and Correct Guides + +**Files:** +- Modify: `docs/reference/patchmap/*.md` +- Append: `docs/tasks/2026/03-25/patchmap-feature-guides/logs/DECISIONS.md` +- Append: `docs/tasks/2026/03-25/patchmap-feature-guides/logs/WORKLOG.md` + +**Step 1:** Review each guide for feature coverage and source accuracy. + +**Step 2:** Fix gaps, inconsistencies, or unclear examples. + +### Task 5: Verify Coverage + +**Files:** +- Create: `docs/reference/patchmap/coverage-checklist.md` +- Modify: `docs/tasks/2026/03-25/patchmap-feature-guides/STATUS.md` +- Append: `docs/tasks/2026/03-25/patchmap-feature-guides/logs/WORKLOG.md` + +**Step 1:** Cross-check guides against public exports and README/API sections. + +**Step 2:** Run project-context guardrails and repository tests relevant to documentation confidence. diff --git a/docs/reference/patchmap/coverage-checklist.md b/docs/reference/patchmap/coverage-checklist.md new file mode 100644 index 00000000..37fba7c2 --- /dev/null +++ b/docs/reference/patchmap/coverage-checklist.md @@ -0,0 +1,105 @@ +# Patchmap 문서 커버리지 체크리스트 + +이 문서는 현재 저장소 기준으로 어떤 기능이 어느 가이드에 설명되었는지 확인하는 최종 점검표다. + +## 1. Patchmap 공개 API 커버리지 + +| API | 설명 위치 | 상태 | +| --- | --- | --- | +| `new Patchmap()` | `public-api.md`, `overview.md` | covered | +| `init(element, opts)` | `public-api.md`, `overview.md` | covered | +| `draw(data)` | `public-api.md`, `overview.md`, `data-model.md` | covered | +| `update(opts)` | `public-api.md`, `overview.md`, `data-model.md`, `history-and-transformer.md` | covered | +| `destroy()` | `public-api.md`, `overview.md` | covered | +| `focus(ids, opts)` | `public-api.md`, `overview.md`, `interactions.md` | covered | +| `fit(ids, opts)` | `public-api.md`, `overview.md`, `interactions.md` | covered | +| `selector(path, opts)` | `public-api.md`, `overview.md`, `interactions.md` | covered | +| `rotation` | `public-api.md`, `overview.md` | covered | +| `flip` | `public-api.md`, `overview.md` | covered | +| `event` facade | `public-api.md`, `overview.md`, `interactions.md` | covered | +| `app` / `viewport` / `world` / `theme` | `public-api.md`, `overview.md` | covered | +| `undoRedoManager` | `public-api.md`, `overview.md`, `history-and-transformer.md` | covered | +| `stateManager` | `public-api.md`, `overview.md`, `interactions.md` | covered | +| `transformer` | `public-api.md`, `overview.md`, `history-and-transformer.md` | covered | +| `animationContext` | `public-api.md` | covered | + +## 2. 공개 export 커버리지 + +| Export | 설명 위치 | 상태 | +| --- | --- | --- | +| `Patchmap` | `overview.md` | covered | +| `Command` | `history-and-transformer.md` | covered | +| `UndoRedoManager` | `history-and-transformer.md` | covered | +| `State` | `interactions.md`, `overview.md` | covered | +| `PROPAGATE_EVENT` | `interactions.md` | covered | +| `Transformer` | `history-and-transformer.md`, `overview.md` | covered | +| `selector` | `overview.md`, `interactions.md` | covered | +| `convertLegacyData` | `data-model.md` | covered | +| `findIntersectObject` | `interactions.md` | covered | +| `isMoved` | `interactions.md` | covered | +| `intersectPoint` | `interactions.md` | covered | +| `uid` | `interactions.md` | covered | + +## 3. README API 섹션 커버리지 + +| README 항목 | 설명 위치 | 상태 | +| --- | --- | --- | +| `init(el, options)` | `public-api.md`, `overview.md` | covered | +| `destroy()` | `public-api.md`, `overview.md` | covered | +| `draw(data)` | `public-api.md`, `overview.md`, `data-model.md` | covered | +| `update(options)` | `public-api.md`, `overview.md`, `data-model.md`, `history-and-transformer.md` | covered | +| `event` | `public-api.md`, `overview.md`, `interactions.md` | covered | +| `viewport` | `public-api.md`, `overview.md`, `interactions.md` | covered | +| `asset` | `public-api.md`, `overview.md` | covered | +| `focus(ids, opts)` | `public-api.md`, `overview.md`, `interactions.md` | covered | +| `fit(ids, options)` | `public-api.md`, `overview.md`, `interactions.md` | covered | +| `rotation` | `public-api.md`, `overview.md` | covered | +| `flip` | `public-api.md`, `overview.md` | covered | +| `selector(path)` | `public-api.md`, `overview.md`, `interactions.md` | covered | +| `stateManager` | `public-api.md`, `overview.md`, `interactions.md` | covered | +| `SelectionState` | `interactions.md` | covered | +| `Transformer` | `public-api.md`, `overview.md`, `history-and-transformer.md` | covered | +| `undoRedoManager` | `history-and-transformer.md` | covered | +| 이벤트 목록 | `overview.md`, `interactions.md`, `history-and-transformer.md` | covered | + +## 4. 데이터 스키마 커버리지 + +| 영역 | 설명 위치 | 상태 | +| --- | --- | --- | +| 최상위 `MapData` 구조 | `data-model.md` | covered | +| 요소 타입 `group`, `grid`, `item`, `relations`, `image`, `text`, `rect` | `data-model.md` | covered | +| 컴포넌트 타입 `background`, `bar`, `icon`, `text` | `data-model.md` | covered | +| primitive 타입 `size`, `gap`, `margin`, `padding`, `placement`, `color`, `radius`, `calc()` | `data-model.md` | covered | +| 정규화 규칙 | `data-model.md` | covered | +| 레거시 변환 | `data-model.md` | covered | + +## 5. 상호작용/상태 커버리지 + +| 영역 | 설명 위치 | 상태 | +| --- | --- | --- | +| `patchmap.event` | `interactions.md` | covered | +| viewport plugin 제어 | `interactions.md` | covered | +| `selector(path)` 규칙 | `interactions.md` | covered | +| `focus()` / `fit()` 타깃 해석 | `interactions.md` | covered | +| `StateManager` | `interactions.md` | covered | +| `SelectionState` | `interactions.md` | covered | +| 충돌 판정 / 선택 헬퍼 | `interactions.md` | covered | + +## 6. 히스토리/트랜스포머 커버리지 + +| 영역 | 설명 위치 | 상태 | +| --- | --- | --- | +| `UndoRedoManager` 메서드와 이벤트 | `history-and-transformer.md` | covered | +| `historyId` 번들링 | `history-and-transformer.md` | covered | +| `Command` / `UpdateCommand` / `BundleCommand` | `history-and-transformer.md` | covered | +| `Transformer` 옵션과 bounds 모드 | `history-and-transformer.md` | covered | +| resize handle / resize history | `history-and-transformer.md` | covered | +| `patchmap.transformer` setter 동작 | `history-and-transformer.md` | covered | +| `update_elements` 이벤트 | `history-and-transformer.md` | covered | + +## 7. 남은 확인 항목 + +- 리뷰 에이전트 피드백이 모두 반영되었는지 확인한다. +- `project-context` runtime shape checker를 다시 실행한다. +- 문서 파일들이 모두 `docs/reference/patchmap/` 아래에 존재하는지 확인한다. +- `public-api.md`만 읽어도 공개 메서드 옵션 표면이 추적 가능한지 다시 점검한다. diff --git a/docs/reference/patchmap/data-model.md b/docs/reference/patchmap/data-model.md new file mode 100644 index 00000000..15fc4a30 --- /dev/null +++ b/docs/reference/patchmap/data-model.md @@ -0,0 +1,394 @@ +# Patch Map 데이터 모델 가이드 + +이 문서는 현재 `patch-map` 구현이 실제로 받아들이는 데이터 형식만 정리한다. +기준 소스는 `README.md`의 `draw(data)` 설명, `src/display/data-schema/*.js`, `src/display/normalize.js`, `src/utils/spacing.js`, `src/utils/convert.js`, `src/utils/validator.js`다. + +## 1) 검증 흐름 + +- `patchmap.draw(data)`는 입력을 `JSON.parse(JSON.stringify(data))`로 한 번 복제한 뒤 처리한다. +- 따라서 `draw()` 입력은 plain JSON 객체/배열 형태가 가장 안전하다. 스키마가 일부 비JSON 값도 허용하더라도 복제 과정에서 보존되지 않을 수 있다. +- 입력이 객체이고 `grids` 키를 가지면 `convertLegacyData()`로 구형 데이터 형식을 새 형식으로 바꾼다. +- 이후 `validateMapData()`가 `mapDataSchema`로 검증한다. +- 검증에 성공하면 내부적으로 `canvas` 루트에 감싸서 렌더링하지만, 외부에서 작성하는 최상위 데이터는 배열이다. +- `update({ changes })`는 기본적으로 `normalizeChanges()`를 거친 뒤 병합한다. `normalize` 옵션을 끌 수 있다. +- 스키마는 대부분 `.strict()`이므로 알 수 없는 속성은 실패한다. + +## 2) 최상위 구조 + +### MapData + +- 최상위 데이터는 `Element[]`다. +- 허용되는 요소 타입은 `group`, `grid`, `item`, `relations`, `image`, `text`, `rect`다. +- `id`는 생략 가능하며 기본값은 `uid()`로 생성된다. +- `show`는 기본값 `true`다. +- 요소 타입(`group`, `grid`, `item`, `relations`, `image`, `text`, `rect`)은 `locked`를 사용할 수 있고 기본값은 `false`다. +- `mapDataSchema`는 루트 배열과 `group.children`만 재귀적으로 검사한다. 즉, 중복 ID 검사는 최상위 요소와 group 하위 요소에 적용된다. + +```js +const data = [ + { type: 'item', id: 'node-a', size: 80 }, + { + type: 'group', + id: 'cluster-1', + children: [ + { type: 'item', id: 'node-b', size: { width: 120, height: 80 } }, + ], + }, +]; +``` + +## 3) 요소 타입 + +### group + +- 필수: `children` +- `children`은 `Element[]`다. +- `attrs`는 그룹 전체의 위치나 원시 속성을 담는 용도로 쓴다. +- 빈 `children: []`는 허용된다. + +```js +{ + type: 'group', + id: 'group-api', + attrs: { x: 100, y: 50 }, + children: [ + { type: 'item', id: 'api-1', size: 64 }, + { type: 'item', id: 'api-2', size: 64, locked: true }, + ], +} +``` + +### grid + +- 필수: `cells`, `item` +- `cells`는 2차원 배열이며 각 칸은 `0`, `1`, 문자열 중 하나여야 한다. +- 허용값 안에서의 활성 규칙은 다음처럼 이해하면 된다. + - `0`은 비활성이다. + - `1`과 비어 있지 않은 문자열은 활성이다. + - 빈 문자열 `''`도 스키마상 문자열이므로 입력 자체는 가능하지만, 런타임에서는 비활성으로 처리된다. +- `inactiveCellStrategy` 기본값은 `destroy`다. `hide`를 쓰면 비활성 셀도 유지되지만 `show: false`가 된다. +- `gap` 기본값은 `0`이다. +- `item.size`는 필수이며 숫자 하나를 쓰면 정사각형으로 확장된다. +- `item.components` 기본값은 `[]`, `item.padding` 기본값은 `0`, `item.contentOrientation` 기본값은 `upright`다. +- 각 활성 셀은 런타임에 `${grid.id}.${rowIndex}.${colIndex}` 형식의 item ID로 생성된다. 예를 들어 `rack` grid의 첫 행 첫 열은 `rack.0.0`이다. +- 생성된 셀 item의 `label`은 `String(cell)` 값이 된다. + +```js +{ + type: 'grid', + id: 'rack', + cells: [ + [1, 0, 1], + ['db', 1, 1], + ], + gap: 4, + inactiveCellStrategy: 'hide', + item: { + size: 40, + components: [ + { type: 'background', source: { type: 'rect', fill: 'white', radius: 6 } }, + { type: 'icon', source: 'server', size: 16 }, + ], + }, +} +``` + +### item + +- 필수: `size` +- `components` 기본값은 `[]` +- `padding` 기본값은 `0` +- `contentOrientation` 기본값은 `upright` +- `item`은 내부 컴포넌트를 담는 기본 요소다. + +```js +{ + type: 'item', + id: 'server-1', + size: { width: 120, height: 80 }, + padding: { top: 8, x: 12 }, + components: [ + { type: 'background', source: { type: 'rect', fill: '#fff', borderWidth: 1 } }, + { type: 'text', text: 'Server', placement: 'top' }, + ], +} +``` + +### relations + +- 필수: `links` +- `links`는 `{ source: string; target: string }[]`다. +- `style`은 선택이며, 생략하면 `color`는 `black` 기본값을 가진다. +- `update()`에서 `mergeStrategy: 'merge'`일 때는 이미 존재하는 source/target 쌍의 중복 링크를 건너뛴다. + +```js +{ + type: 'relations', + id: 'server-links', + links: [ + { source: 'server-1', target: 'server-2' }, + { source: 'server-2', target: 'server-3' }, + ], + style: { width: 2, cap: 'round', join: 'round' }, +} +``` + +### image + +- 필수: `source` +- `size`는 선택이다. +- `source`는 URL 또는 asset key 문자열이다. + +```js +{ + type: 'image', + id: 'logo', + source: 'https://example.com/logo.png', + size: { width: 160, height: 48 }, +} +``` + +### text + +- `text`는 선택이며 기본값은 빈 문자열이다. +- `style`은 `ElementTextStyle`를 사용한다. +- `size`는 선택이다. +- `style`을 생략하면 기본 텍스트 스타일이 채워진다. + +```js +{ + type: 'text', + id: 'title', + text: 'Hello Patch Map', + style: { fontSize: 20, fontWeight: 'bold' }, + size: { width: 220, height: 40 }, +} +``` + +### rect + +- 필수: `size` +- `fill`과 `stroke`는 선택이다. +- `radius` 기본값은 `0`이다. +- `radius`는 숫자 하나 또는 `EachRadius` 객체를 받을 수 있다. + +```js +{ + type: 'rect', + id: 'panel', + size: { width: 200, height: 120 }, + fill: '#ffffff', + stroke: { color: '#999', width: 1 }, + radius: { topLeft: 8, topRight: 8, bottomRight: 0, bottomLeft: 0 }, +} +``` + +## 4) 컴포넌트 타입 + +컴포넌트는 `item.components` 또는 `grid.item.components` 안에서만 쓴다. +컴포넌트도 `Base`를 따르므로 `id`, `label`, `show`, `attrs`를 가진다. `locked`는 없다. + +### background + +- 필수: `source` +- `source`는 문자열 또는 `TextureStyle` 객체다. +- `size`는 입력을 받아도 실제로는 항상 부모를 가득 채우는 `100% x 100%`로 정규화된다. +- `tint`는 선택이며 생략 시 흰색 계열 기본값을 쓴다. + +```js +{ + type: 'background', + source: { type: 'rect', fill: 'white', borderWidth: 2, borderColor: 'primary.dark', radius: 6 }, +} +``` + +### bar + +- 필수: `source`, `size` +- `source`는 `TextureStyle` 객체다. +- `size`는 `PxOrPercentSize`다. +- `placement` 기본값은 `bottom` +- `margin` 기본값은 `0` +- `animation` 기본값은 `true` +- `animationDuration` 기본값은 `200` + +```js +{ + type: 'bar', + source: { type: 'rect', fill: 'white', radius: 3 }, + size: '100%', + placement: 'bottom', +} +``` + +### icon + +- 필수: `source`, `size` +- `source`는 문자열이다. +- `placement` 기본값은 `center` +- `margin` 기본값은 `0` + +```js +{ + type: 'icon', + source: 'loading', + size: 16, + placement: 'right-bottom', +} +``` + +### text + +- `text` 기본값은 빈 문자열이다. +- `placement` 기본값은 `center` +- `margin` 기본값은 `0` +- `split` 기본값은 `0` +- `style`은 `LabelTextStyle`다. + +```js +{ + type: 'text', + text: 'CPU', + split: 0, + style: { fontSize: 'auto', autoFont: { min: 10, max: 18 }, overflow: 'ellipsis' }, +} +``` + +## 5) primitive 타입과 축약 표기 + +### size + +- `Size`는 숫자 또는 `{ width, height }` 객체다. +- 숫자 하나를 넣으면 `width`와 `height`가 같은 정사각형으로 확장된다. +- 숫자는 음수일 수 없다. + +### gap + +- `Gap`은 숫자 또는 `{ x, y }` 객체다. +- 숫자 하나를 넣으면 `x`와 `y`가 같은 값으로 확장된다. +- `x`, `y`는 기본값 `0`을 가진다. +- 음수는 허용되지 않는다. + +### margin / padding + +- `Margin`은 숫자, `{ x, y }`, 또는 `{ top, right, bottom, left }` 객체다. +- 숫자 하나를 넣으면 네 변 모두 같은 값이 된다. +- `{ x, y }`는 `x -> left/right`, `y -> top/bottom`으로 확장된다. +- edge 키가 axis 키보다 우선한다. 예: `{ top: 10, x: 5 }` -> `{ top: 10, right: 5, bottom: 0, left: 5 }` +- `padding`도 같은 정규화 규칙을 쓴다. +- `margin`은 음수도 허용한다. + +### placement + +- 현재 스키마가 허용하는 값은 `left`, `left-top`, `left-bottom`, `top`, `right`, `right-top`, `right-bottom`, `bottom`, `center`다. +- 현재 구현의 스키마는 `none`을 허용하지 않는다. + +### colors + +- `Color`는 문자열, 숫자, 숫자 배열, `Float32Array`, `Uint8Array`, `Uint8ClampedArray`, RGB/HSL/HSV 계열 객체, `PixiColor` 인스턴스를 허용한다. +- 문자열 색상은 theme key로도 해석된다. theme에 없으면 원래 문자열을 그대로 사용한다. +- `Color` 스키마 자체의 기본값은 `0xffffff`다. +- `rect.fill`은 선택이며, `rect.stroke.color`와 `relations.style.color`는 각각 다른 기본값을 가진다. + +### texture style / text style + +- `TextureStyle` 객체는 partial이다. `{ fill: 'red' }`처럼 `type` 없이 써도 되고, `type`을 쓰면 `rect`여야 한다. +- `background.source`는 문자열 또는 `TextureStyle` 객체를 받는다. +- `bar.source`는 `TextureStyle` 객체만 받는다. +- `LabelTextStyle`에서 확실한 기본값은 `fontFamily: 'FiraCode'`, `fontWeight: 400`, `fill: 'black'`, `autoFont: { min: 1, max: 100 }`, `overflow: 'visible'`다. +- `LabelTextStyle.fontSize`는 현재 스키마에서 optional override라서, `TextStyle`의 기본 `16`이 항상 그대로 적용된다고 단정하지 않는 편이 안전하다. +- `ElementTextStyle` 기본값은 `wordWrap: true`, `letterSpacing: 0`이다. + +### radius + +- `rect.radius`와 `TextureStyle.radius`는 숫자 또는 `EachRadius` 객체다. +- 숫자 하나를 넣으면 모든 모서리에 같은 반경이 적용된다. +- `EachRadius`는 `topLeft`, `topRight`, `bottomRight`, `bottomLeft`를 가진다. +- 각 모서리 값은 기본값 `0`이다. + +### px / percent / calc() + +- `PxOrPercent`는 숫자, `"%"` + 문자열, 또는 `{ value, unit }` 객체다. +- 숫자는 `{ value, unit: 'px' }`로 확장된다. +- `"75%"` 같은 문자열은 퍼센트로 파싱된다. +- `calc(...)` 문자열도 허용되지만, 연산자는 공백으로 분리되어야 한다. 예: `calc(100% - 20px)`. +- `PxOrPercentSize`는 위 값을 width/height에 각각 적용한다. +- `bar.size`와 `icon.size`가 이 형식을 쓴다. + +## 6) 정규화와 레거시 변환 + +### `normalizeChanges()` + +- `update()` 경로에서 기본적으로 적용된다. +- plain object만 정규화한다. 클래스 인스턴스 같은 non-plain payload는 그대로 둔다. +- `size: 80` -> `{ width: 80, height: 80 }` +- `gap: 4` -> `{ x: 4, y: 4 }` +- `padding: 3` -> `{ top: 3, right: 3, bottom: 3, left: 3 }` +- `margin: { top: 10, x: 5 }` 같은 혼합 입력은 edge 값이 우선한다. +- `background.size`는 무조건 `{ width: 100%, height: 100% }`로 바뀐다. +- `bar.size`와 `icon.size`는 숫자/문자열 축약을 `{ width, height }` 객체로 바꾼다. +- `components`, `children`, `item` 내부도 재귀적으로 정규화한다. + +### `convertLegacyData()` + +- `draw()`는 루트 객체가 legacy 형식이면 자동 변환한다. +- `metadata` 키는 무시한다. +- `grids`는 `grid` 요소 배열로 바뀐다. +- `strings`는 `relations` 요소 배열로 바뀐다. +- 그 외 키는 `item` 요소 배열로 바뀐다. +- 변환 결과에는 현재 구현용 기본 컴포넌트와 `attrs`가 채워진다. + +```js +// legacy 예시 +{ + grids: [...], + strings: [...], + combines: [...], + metadata: {...}, +} +``` + +## 7) 작성 규칙 + +- 최상위는 항상 배열로 쓴다. +- `group.children` 안에는 요소만 넣는다. +- `grid.item.components`와 `item.components` 안에는 컴포넌트만 넣는다. +- `rect`는 `size` 없이는 그릴 수 없다. +- `bar`와 `icon`은 `size`가 필수다. +- `background`는 `size`를 따로 제어하는 요소가 아니므로, 채우기 용도로만 생각하는 편이 맞다. +- `relations.links`는 ID 문자열로 연결한다. +- `grid.cells`는 스키마상 `0 | 1 | string`만 넣을 수 있고, 그 안에서는 `0`과 `''`가 비활성, `1`과 비어 있지 않은 문자열이 활성으로 동작한다고 이해하는 편이 안전하다. +- `contentOrientation`의 기본값은 `upright`다. `item`과 `grid.item`의 내부 `text`, `icon`, `bar`에 영향을 준다. +- `relations.links`로 grid 셀을 연결할 때는 런타임이 생성한 셀 ID 규칙(`${grid.id}.${row}.${col}`)을 써야 한다. + +## 8) 짧은 권장 예시 + +```js +const data = [ + { + type: 'group', + id: 'service-cluster', + attrs: { x: 120, y: 80 }, + children: [ + { + type: 'grid', + id: 'rack', + cells: [[1, 0, 1]], + item: { + size: 40, + components: [ + { type: 'background', source: { type: 'rect', fill: '#fff', radius: 6 } }, + { type: 'icon', source: 'server', size: 16 }, + ], + }, + }, + { + type: 'relations', + id: 'links', + links: [{ source: 'rack.0.0', target: 'rack.0.2' }], + }, + ], + }, +]; +``` diff --git a/docs/reference/patchmap/history-and-transformer.md b/docs/reference/patchmap/history-and-transformer.md new file mode 100644 index 00000000..b82907fb --- /dev/null +++ b/docs/reference/patchmap/history-and-transformer.md @@ -0,0 +1,258 @@ +# Patchmap history / transformer 가이드 + +이 문서는 현재 구현 기준으로 `history`와 `transformer`의 동작만 정리한다. 설명 근거는 `src/command/**`, `src/transformer/**`, `src/patchmap.js`, `src/utils/get.js`, `src/utils/bounds.js`, `src/utils/viewport.js`, 그리고 README의 해당 섹션이다. + +## 1. UndoRedoManager 모델 + +`UndoRedoManager`는 명령 스택과 현재 위치를 따로 관리한다. + +- 스택은 내부 배열 `commands`와 현재 인덱스 `index`로 구성된다. +- `commands` getter는 내부 배열의 복사본을 돌려준다. +- 기본 최대 저장 개수는 50개다. +- 새 명령을 실행하면 현재 인덱스 뒤의 redo 스택은 잘린다. + +### `execute(command, options)` + +`execute()`는 다음 순서로 동작한다. + +1. `command.execute()`를 먼저 호출한다. +2. 현재 redo 스택을 제거한다. +3. `options.historyId`가 있으면 같은 `historyId`를 가진 직전 명령과 묶을 수 있는지 검사한다. +4. 같은 `historyId`가 연속으로 들어오면 기존 `BundleCommand`에 추가하거나, 단일 명령을 `BundleCommand`로 바꾼다. +5. 같은 `historyId`가 아니면 새 항목으로 스택에 넣는다. + +중요한 점은 `historyId`가 같아도 **연속된 명령**일 때만 하나의 undo/redo 단계로 묶인다는 것이다. 중간에 다른 history가 끼면 새 번들이 시작된다. + +### `undo()`, `redo()` + +- `undo()`는 현재 인덱스의 명령에 대해 `undo()`를 호출한 뒤 인덱스를 하나 내린다. +- `redo()`는 인덱스를 먼저 올린 뒤 해당 명령의 `execute()`를 다시 호출한다. +- 둘 다 가능 여부는 `canUndo()`, `canRedo()`로 확인한다. + +### `clear()`, `destroy()` + +- `clear()`는 명령 스택을 비우고 인덱스를 `-1`로 되돌린다. +- `destroy()`는 키보드 단축키 리스너를 제거하고 `clear()`를 호출한 뒤, 모든 리스너를 해제한다. +- `destroy()`는 `history:destroyed`를 emit한다. + +### 핫키 + +핫키는 `Patchmap.init()`에서 `undoRedoManager._setHotkeys()`를 호출할 때만 등록된다. 즉, `UndoRedoManager`를 단독으로 만든다고 자동 연결되지는 않는다. + +현재 단축키는 다음과 같다. + +- `Ctrl/Cmd + Z`: undo +- `Ctrl/Cmd + Shift + Z`: redo +- `Ctrl/Cmd + Y`: redo + +편집 가능한 요소 필터링은 넓지 않다. 현재 구현의 `isInput()`은 `element.type === 'text'`, `element.tagName === 'INPUT'`, `element.type === 'textarea'`, `element.type === 'SELECT'`만 검사한다. 즉 일반적인 모든 editable target을 포괄한다고 보면 안 된다. + +## 2. Command / UpdateCommand / BundleCommand + +### `Command` + +`Command`는 추상 베이스다. + +- 생성 시 `id`를 받는다. +- `execute()`와 `undo()`는 서브클래스가 구현해야 한다. +- 구현하지 않으면 에러를 던진다. + +### `UpdateCommand` + +`UpdateCommand`는 공개 API로 직접 생성하는 타입이 아니라, `element.apply(..., { historyId })` 경로에서 내부적으로 만들어진다. + +- 생성 시 대상 element, changes, options를 저장한다. +- `previousProps`를 미리 캡처한다. +- `execute()`는 현재 element에 변경사항을 적용한다. +- `undo()`는 캡처해 둔 이전 상태를 `mergeStrategy: 'replace'`로 복원한다. +- undo 중에는 `historyId: false`로 다시 history 기록이 쌓이지 않게 막는다. + +부분 `attrs` 업데이트를 undo할 때는, 원래 props에 없던 attr 값도 element의 raw 값에서 채워 넣는다. 그래서 `attrs.x`만 바꾼 경우에도 나머지 `attrs`와 원래 인스턴스 값이 유지된다. + +### `BundleCommand` + +`BundleCommand`는 여러 명령을 하나로 묶는 래퍼다. + +- `execute()`는 안에 든 명령을 앞에서부터 순서대로 실행한다. +- `undo()`는 뒤에서부터 역순으로 되돌린다. +- `addCommand()`는 새 명령을 배열 끝에 추가한다. + +실사용 관점에서는 사용자가 직접 다루기보다 `UndoRedoManager`가 같은 `historyId`를 묶기 위해 내부적으로 만든다고 보는 편이 맞다. + +### 공개 사용 패턴 + +현재 공개적으로 history를 여는 대표 경로는 `patchmap.update({ history })`다. + +- `history: true`는 호출마다 새 `historyId`를 만든다. +- `history: 'same-id'`처럼 문자열을 넘기면 같은 문자열이 같은 히스토리 단위가 된다. + +즉, 여러 `update()` 호출을 한 단계로 묶고 싶다면 `true`가 아니라 **같은 문자열**을 직접 넘겨야 한다. + +## 3. Transformer 목적과 옵션 + +`Transformer`는 선택된 요소의 bounds를 시각화하고, 현재 구현에서는 리사이즈를 제공하는 시각적 도구다. 현재 코드 기준으로는 회전 핸들은 없다. + +생성 시 옵션은 아래와 같다. + +- `elements`: 초기 선택 요소 배열 +- `wireframeStyle`: 윤곽선 스타일 +- `boundsDisplayMode`: bounds 표시 모드 +- `resizeHandles`: group bounds 리사이즈 핸들 표시 여부 +- `resizeHistory`: 리사이즈 변경을 history에 기록할지 여부 + +기본값은 다음과 같다. + +- `wireframeStyle.thickness = 1.5` +- `wireframeStyle.color = '#1099FF'` +- `boundsDisplayMode = 'all'` +- `resizeHandles = false` +- `resizeHistory = false` + +설정 객체는 생성 시 검증된다. 잘못된 타입이 들어오면 생성 단계에서 예외가 난다. + +### SelectionModel 표면 + +`Transformer`는 내부에 `SelectionModel`을 가진다. + +- `transformer.selection`은 선택 모델 인스턴스를 돌려준다. +- `transformer.elements`는 `selection.elements`의 편의 getter다. +- `transformer.elements = value`는 `selection.set(value)`로 이어진다. +- `selection`은 `add()`, `remove()`, `set()`, `destroy()`를 가진다. + +`SelectionModel.elements` getter는 복사본을 반환하므로, 반환 배열을 직접 수정해도 선택 상태는 바뀌지 않는다. + +선택이 바뀌면 `SelectionModel`이 `update`를 emit하고, `Transformer`는 이를 받아 redraw를 예약한 뒤 `update_elements`를 emit한다. + +또한 `Transformer`는 viewport에 추가된 뒤 `zoomed`, `zoomed-end` 이벤트를 구독한다. 그래서 확대/축소나 flip처럼 viewport scale이 바뀌면 redraw를 예약하고, 선 두께와 핸들 크기를 현재 절대 scale 기준으로 다시 계산한다. 음수 scale(`flip`)에서도 `getSafeViewportScale()`로 절대값을 써서 시각 두께를 안정적으로 유지한다. + +## 4. Bounds 표시 모드와 리사이즈 핸들 + +`boundsDisplayMode`는 다음 네 가지다. + +- `all`: 개별 element bounds와 group bounds를 함께 보여준다. +- `groupOnly`: group bounds만 보여준다. +- `elementOnly`: 개별 element bounds를 보여준다. +- `none`: bounds를 숨긴다. + +주의할 점은 화면에 보이는 "group bounds"가 두 종류라는 점이다. + +- wireframe의 group bounds는 **현재 선택된 모든 요소**를 합쳐 axis-aligned bounds로 그린다. +- resize frame과 handle은 **resizable + unlocked 요소만 추린 subset**으로 viewport 좌표계에서 따로 계산한다. + +그래서 회전된 선택이나 잠긴 요소가 섞인 선택에서는, 보이는 외곽선과 실제 리사이즈 핸들 프레임이 완전히 같은 집합/기하를 쓰지 않을 수 있다. `resizeHandles`가 `true`일 때는 이 resize frame을 위해 group bounds가 추가로 그려질 수 있고, `boundsDisplayMode = 'none'`일 때만 핸들도 함께 완전히 숨는다. + +리사이즈 UI는 두 층으로 나뉜다. + +- 실제로 보이는 corner handle 4개 +- 클릭 가능한 edge hit target 4개 + +모두 `ResizeHandleLayer`가 lazy 생성하며, corner는 핸들로 보이고 edge는 거의 투명한 hit area로만 쓰인다. + +리사이즈는 다음 규칙을 따른다. + +- 선택 요소 중 리사이즈 가능한 요소만 대상이 된다. +- locked이거나 리사이즈 불가인 요소는 제외된다. +- 드래그 중 `Shift`를 누르면 aspect ratio가 유지된다. +- `resizeHandles = false`로 바꾸면 현재 핸들을 지우고 진행 중인 resize gesture도 종료한다. + +`resizeHistory = true`이면 한 번의 drag gesture에 대해 하나의 `historyId`를 만들어, 그 제스처에서 적용된 모든 element 변경이 같은 undo 단계로 묶인다. + +## 5. `patchmap.transformer` setter + +`Patchmap`의 `transformer` setter는 현재 인스턴스를 교체하는 역할을 한다. + +- 기존 transformer가 살아 있으면 먼저 `viewport`의 `object_transformed` 리스너를 제거한다. +- 이어서 기존 transformer를 `destroy(true)`로 정리한다. +- 새 값이 `Transformer` 인스턴스가 아니면 console error를 남기고 내부 참조를 `null`로 둔다. +- 유효한 인스턴스면 `viewport.addChild()`로 붙이고 `object_transformed`를 `transformer.update`에 연결한다. + +즉, setter는 단순 참조 교체가 아니라 **기존 인스턴스의 teardown까지 포함**한다. + +`Patchmap.init()`에서 `opts.transformer`를 넘기면 이 setter를 통해 연결된다. + +## 6. 관련 이벤트 + +history/transformer 사용자 입장에서 직접 볼 수 있는 이벤트는 아래가 핵심이다. + +### `UndoRedoManager` + +- `history:executed` +- `history:undone` +- `history:redone` +- `history:cleared` +- `history:destroyed` +- `history:*` + +각 history 이벤트는 일반 이벤트와 함께 namespace wildcard에도 걸린다. `history:*`를 구독하면 `namespace`와 `type`이 보강된 payload를 받을 수 있다. + +### `Transformer` + +- `update_elements` + +이 이벤트는 `selection.elements`가 바뀌었을 때와 resize drag 중에 emit된다. payload는 `target`, `current`, `added`, `removed`를 포함한다. + +### 주변 이벤트 + +- `patchmap:initialized` +- `patchmap:destroyed` + +이 둘은 transformer를 붙이거나 떼는 생명주기와 직접 맞닿아 있다. + +중요한 caveat 하나는, resize drag는 `element.apply()`를 직접 호출하므로 `patchmap.update()` 경로를 타지 않는다. 따라서 resize가 곧바로 `patchmap:updated`로 이어지지는 않는다. + +## 7. 실무 예시 + +### 7-1. history가 있는 update + +```js +patchmap.update({ + path: '$..[?(@.id=="item-1")]', + changes: { attrs: { x: 200 } }, + history: true, +}); + +patchmap.undoRedoManager.undo(); +patchmap.undoRedoManager.redo(); +``` + +`history: true`는 호출마다 고유 id를 새로 만든다. 여러 호출을 한 단계로 묶고 싶으면 같은 문자열을 직접 넘겨야 한다. + +### 7-2. transformer 연결 + +```js +import { Patchmap, Transformer } from '@conalog/patch-map'; + +const patchmap = new Patchmap(); +await patchmap.init(document.body); +patchmap.draw([ + { + type: 'item', + id: 'group-1', + size: 80, + }, +]); + +patchmap.transformer = new Transformer({ + elements: [patchmap.selector('$..[?(@.id=="group-1")]')[0]], + resizeHandles: true, + resizeHistory: true, +}); +``` + +### 7-3. selection 변경 감지 + +```js +patchmap.transformer.on('update_elements', ({ current, added, removed }) => { + console.log({ current, added, removed }); +}); +``` + +## 8. 주의할 점 + +- `UndoRedoManager.commands`는 복사본이라 직접 수정해도 내부 히스토리는 바뀌지 않는다. +- `patchmap.draw()`는 렌더링 전에 `undoRedoManager.clear()`를 호출하므로, 새 데이터를 다시 그리면 기존 history는 사라진다. +- `Transformer`는 `boundsDisplayMode = 'none'`일 때 핸들도 숨긴다. +- 리사이즈 대상은 항상 resizable candidate만 남는다. +- `patchmap.transformer`를 다시 대입하면 기존 transformer는 destroy된다. +- resize history는 `resizeHistory`를 켜야만 붙는다. +- `update_elements`는 selection/resize 갱신을 알릴 뿐, `patchmap:updated`와는 별개다. diff --git a/docs/reference/patchmap/interactions.md b/docs/reference/patchmap/interactions.md new file mode 100644 index 00000000..35e2695b --- /dev/null +++ b/docs/reference/patchmap/interactions.md @@ -0,0 +1,202 @@ +# Patchmap Interactions + +이 문서는 현재 JS/TS 구현 기준의 상호작용 동작만 정리한다. + +## `patchmap.event` + +`patchmap.event`는 `viewport.events` 레지스트리를 감싸는 facade다. `add()`는 이벤트 정의를 저장한 뒤 즉시 활성화까지 수행하고, `remove()`와 `removeAll()`은 등록된 리스너를 먼저 떼고 레지스트리에서도 제거한다. `get()`과 `getAll()`은 저장된 이벤트를 조회한다. + +- `add({ id, path, elements, action, fn, options })` + - `id`를 생략하면 `uid()`로 생성된다. + - `path`는 기본값이 `'$'`이며, `'$'`는 패치맵 캔버스 표면, 즉 `viewport` 자체를 뜻한다. + - `path`가 `'$'`가 아니면 `world`를 기준으로 selector가 동작한다. + - `elements`를 직접 넘기면 selector를 거치지 않는다. + - `path`와 `elements`를 함께 주면 둘 다 이벤트 대상이 된다. + - 같은 `id`가 이미 있으면 경고만 남기고 덮어쓰지 않는다. +- `on(id)` / `off(id)` / `remove(id)` + - 공백으로 구분한 여러 `id`를 한 번에 처리할 수 있다. + - `on()`은 각 대상 객체에 `addEventListener`를 붙이고, `off()`는 같은 조합으로 `removeEventListener`를 호출한다. + - `remove()`는 `off()` 후 레지스트리에서 항목을 삭제한다. +- `removeAll()` + - 현재 등록된 모든 이벤트를 제거한다. +- `get(id)` + - 존재하지 않는 `id`, 상속된 프로퍼티 이름, prototype 키는 `null`을 반환한다. +- `getAll()` + - 현재 `viewport.events` 원본을 그대로 돌려준다. + +## `viewport.plugin` + +초기화 시 `initViewport()`는 다음 플러그인을 기본으로 켠다. + +- `clampZoom: { minScale: 0.5, maxScale: 30 }` +- `drag: {}` +- `wheel: {}` +- `pinch: {}` +- `decelerate: {}` +- `passiveWheel: false` + +플러그인 제어는 `viewport.plugin` 래퍼로 한다. + +- `add(plugins)` + - `disabled: true`인 항목은 건너뛴다. + - 같은 키가 있으면 먼저 `viewport.plugins.remove(key)`를 호출한 뒤 `viewport[key](options)`로 다시 붙인다. +- `start(keys)` + - `viewport.plugins.resume(key)`를 호출한다. +- `stop(keys)` + - `viewport.plugins.pause(key)`를 호출한다. +- `remove(keys)` + - `viewport.plugins.remove(key)`를 호출한다. + +`SelectionState`는 드래그/페인트 선택이 시작될 때 `mouse-edges` 플러그인을 시작하고, `pointerup`에서 중지한다. + +## `selector(path)` + +`patchmap.selector(path, opts)`는 `world`를 루트로 하는 JSONPath 스타일 탐색기다. 내부적으로 `JSONPath-Plus`를 사용하며, 탐색 가능한 키는 `children`으로 제한된다. + +- 루트 `'$'`는 `world`를 가리킨다. +- `flatten: true`가 적용되어 결과가 평탄화된다. +- `path`가 `null`/`undefined`면 빈 문자열로 처리된다. +- 예시 형태의 쿼리인 `$..[?(@.label=="group-label-1")]` 같은 패턴을 그대로 사용할 수 있다. + +주의할 점은, `patchmap.event`에서 `'$'`는 `viewport` 표면을 뜻하지만 `patchmap.selector('$')`는 `world` 루트를 뜻한다는 점이다. + +## `focus()` / `fit()` + +두 함수 모두 `ids`를 `string`, `string[]`, `null`, `undefined` 중 하나로 받는다. 객체를 첫 번째 인자로 넘기면 옵션이 아니라 `ids`로 해석되어 검증에 실패한다. 옵션만 전달하려면 `null` 또는 `undefined`를 넣어야 한다. + +- 공통 규칙 + - `ids`를 생략하면 기본 타깃은 `world.children` 중 `relations`를 제외한 최상위 관리 요소다. + - `filter`는 탐색 과정에서 적용되며, 거짓값을 반환한 노드와 그 하위 트리는 제외된다. + - 명시적 `ids`는 `$..children[?(@.type != null)]`에서 먼저 찾는다. + - 주소 가능한 노드는 `constructor.isElement`가 참인 항목만이다. + - `relations`는 명시적으로 찾으면 `props.links`의 `source`/`target` 엔드포인트로 해석된다. 링크 엔드포인트가 없으면 `relations` 자신을 사용한다. + - 컨테이너는 기본적으로 관리 자식이 있으면 자식 쪽으로 내려간다. 단, `grid`는 하위로 내려가지 않고 자체가 바운스 기여자로 취급된다. + - `filter`가 컨테이너에서 거짓이면 그 하위 트리는 내려가지 않는다. + - 결과가 비면 `null`을 반환하고 뷰포트를 움직이지 않는다. + +- `focus(ids, opts)` + - 계산된 바운스의 중심으로만 이동한다. + - `fit()`과 달리 크기 조정은 하지 않는다. + +- `fit(ids, opts)` + - `padding`은 기본값 `{ x: 16, y: 16 }`에서 시작한다. + - 숫자를 넣으면 모든 변에 같은 패딩이 적용된다. + - `{ x, y }`만 허용한다. `{ top, right, bottom, left }` 같은 edge 키는 거부된다. + - 바운스를 중심으로 이동한 뒤 `viewport.fit(true, width, height)`를 호출한다. + - 현재 뷰포트 scale을 기준으로 fit 크기를 계산한다. + +## `StateManager` / `SelectionState` + +`StateManager`는 스택 기반 상태 머신이다. `patchmap.init()` 시 `selection` 상태가 등록되며, 활성화는 `patchmap.stateManager.setState('selection', options)`로 한다. + +- `register(name, StateClassOrObject, isSingleton = true)` + - 클래스나 싱글톤 인스턴스를 등록한다. + - `handledEvents`에 적힌 이벤트들을 자동으로 바인딩한다. +- `setState(name, ...args)` + - 현재 스택을 비우고 새 상태를 푸시한다. +- `pushState(name, ...args)` + - 기존 활성 상태에 `pause()`를 호출한 뒤 새 상태의 `enter()`를 호출한다. +- `popState(payload)` + - 현재 상태의 `exit()` 후 아래 상태의 `resume(payload)`를 호출한다. +- `resetState()` + - 모든 상태에 `exit()`를 호출하고 스택을 비운다. +- `activateModifier(name, ...args)` / `deactivateModifier()` + - modifier 상태는 메인 스택과 별도로 동작하고, 활성화되면 모든 이벤트를 먼저 받는다. + +modifier 상태가 활성화되어 있으면 해당 modifier가 이벤트를 독점하고 메인 스택은 이벤트를 받지 않는다. modifier가 없을 때만 `stateStack`의 top부터 아래로 내려가며, 상태 핸들러가 `PROPAGATE_EVENT`를 반환하면 다음 상태로 이벤트가 전달되고, 다른 값이면 전파가 멈춘다. 키 이벤트는 `window`, 나머지는 `viewport`에 붙는다. + +`SelectionState`는 현재 구현의 기본 선택/드래그 상태다. 설정은 `deepMerge(defaultConfig, config)`로 합쳐진다. + +중요한 현재 구현 전제 조건이 하나 있다. `SelectionState`의 hit-test 경로는 내부에서 `this.store.transformer.elements`를 직접 읽기 때문에, 실사용에서는 `patchmap.transformer`가 연결되어 있고 `elements` 배열을 제공한다고 가정하는 편이 안전하다. transformer를 붙이지 않은 상태에서 선택 로직을 바로 쓰면 런타임 오류가 날 수 있다. + +- 기본값 + - `draggable: false` + - `paintSelection: false` + - `filter: () => true` + - `selectUnit: 'entity'` + - `drillDown: false` + - `deepSelect: false` + - 콜백들은 기본적으로 no-op + - `selectionBoxStyle.fill = { color: '#9FD6FF', alpha: 0.2 }` + - `selectionBoxStyle.stroke = { width: 2, color: '#1099FF' }` +- 선택 단위 + - `entity`: 대상 자체 + - `closestGroup`: 가장 가까운 `group`, 없으면 `grid` + - `highestGroup`: 가장 바깥쪽 `group`, 없으면 가장 가까운 `grid` + - `grid`: 가장 가까운 `grid` + - 알 수 없는 값은 대상 자체로 되돌아간다 +- 입력 처리 + - `pointerdown`에서 시작점과 viewport 상태 스냅샷을 저장하고 `onDown(target, event)`를 호출한다. + - 오른쪽 버튼(`button === 2`)이면 즉시 상태/선택 박스/제스처를 초기화한다. + - `draggable`이 꺼져 있으면 `pointermove`는 사실상 무시된다. + - 이동 임계값은 `4 / viewport.scale` 기준이다. + - 임계값을 넘으면 드래그 또는 페인트 모드로 전환되고 `mouse-edges`가 시작된다. + - `pointerup`에서 드래그/페인트 중이면 `onDragEnd()`를 호출하고 `mouse-edges`를 중지한다. + - `onUp()`은 `PRESSING` 상태에서의 `pointerup`마다 호출된다. 즉 뷰포트 이동 여부와 무관하게 drag 모드로 넘어가지 않았다면 실행될 수 있다. + - `pointerover`는 idle 상태에서만 `onOver()`를 호출한다. + - `ontap`은 `onclick`으로 라우팅된다. + - `click`과 `rightclick`은 `pointerdown` 이후 뷰포트 위치/scale이 바뀌지 않았고 실제 이동이 임계값을 넘지 않았을 때만 처리된다. + - `click`의 `detail === 2`일 때만 `onDoubleClick()`이 호출되고, 이 경우 `onClick()`은 호출되지 않는다. + - `rightclick`은 `onRightClick()`으로 라우팅된다. + - `pointerleave`는 상태와 selection box, gesture 데이터를 정리하지만 `onDragEnd()`를 대신 호출하지는 않는다. +- 추가 동작 + - `drillDown`이 켜져 있고 `detail >= 2`이면 같은 위치에서 더 깊은 대상이 있는지 반복적으로 다시 찾는다. + - `deepSelect`가 켜져 있고 `Ctrl` 또는 `Meta`를 누른 상태면 `selectUnit`을 강제로 `grid`로 바꿔 찾는다. + - 현재 transformer의 `elements`로부터 조상 집합을 만들어, `closestGroup` / `highestGroup` / `grid` 선택 시 그 조상은 제외한다. + - wireframe 위를 클릭하면, 가능하면 같은 지점의 아래 대상로 한 번 더 찾아 내려간다. + +## 찾기 / 충돌 판정 + +선택과 드래그 판정은 다음 헬퍼에 의존한다. + +- `findIntersectObject(parent, point, config)` + - 점 충돌 기준의 단일 대상 탐색이다. + - `zIndex`가 높은 순, 그리고 표시 순서를 반영해 먼저 맞는 대상을 찾는다. + - `hitScope === 'children'`인 후보는 자기 자신이 아니라 자식들을 검사한다. +- `findIntersectObjects(parent, polygon, config)` + - 사각형/다각형 선택에 쓰인다. + - 결과는 중복 제거된 배열이다. +- `findIntersectObjectsBySegment(parent, p1, p2, config)` + - 페인트 선택에 쓰인다. + - 교차 진입 `t`가 빠른 순으로 정렬된다. + +공통으로 다음 규칙을 따른다. + +- 검색 루트가 잠겨 있으면 결과는 비거나 `null`이다. +- 잠긴 오브젝트와 잠긴 조상 아래의 오브젝트는 후보에서 제외된다. +- 후보 선택 가능 여부는 `constructor.isSelectable`와 잠금 상태로 결정된다. +- `isResizableCandidate()`는 `constructor.isResizable`와 잠금 상태를 함께 본다. + +## 상호작용 관련 유틸리티와 이벤트 + +`src/utils/index.js`에서 상호작용에 직접 쓰이는 공개 export는 다음이다. + +- `findIntersectObject` +- `isMoved` +- `intersectPoint` +- `uid` + +`isMoved()`는 기본 이동 임계값 4px를 쓰고, viewport scale에 따라 임계값을 보정한다. `uid()`는 이벤트 id를 자동 생성할 때도 쓰인다. + +패치맵과 상태 시스템이 실제로 내보내는 주요 이벤트는 다음이다. + +- `patchmap:initialized` +- `patchmap:draw` +- `patchmap:draw`는 `draw()` 호출 직후 동기적으로 emit되지 않고, `scheduler.postTask(..., { priority: 'user-visible' })` 또는 `setTimeout(..., 0)`을 통해 늦게 전달된다. +- `patchmap:updated` +- `patchmap:rotated` +- `patchmap:flipped` +- `patchmap:destroyed` +- `state:pushed` +- `state:popped` +- `state:set` +- `state:reset` +- `state:destroyed` +- `modifier:activated` +- `modifier:deactivated` + +추가 주의사항: + +- `patchmap.draw()`는 관계선(`relations`)을 한 번 더 갱신하도록 다음 틱에 `update({ path: '$..[?(@.type=="relations")]', refresh: true, emit: false })`를 예약한다. +- 캔버스 래퍼는 `contextmenu`를 막는다. +- `patchmap.destroy()`는 이벤트 레지스트리, 상태 관리, viewport, 애니메이션 컨텍스트를 정리한다. diff --git a/docs/reference/patchmap/overview.md b/docs/reference/patchmap/overview.md new file mode 100644 index 00000000..e1bd8ca0 --- /dev/null +++ b/docs/reference/patchmap/overview.md @@ -0,0 +1,304 @@ +# Patchmap 개요 및 라이프사이클 + +PATCH MAP은 `pixi.js`와 `pixi-viewport` 위에서 동작하는 캔버스 지도 라이브러리다. 현재 구현 기준으로는 `Patchmap` 인스턴스를 만들고 `init()`으로 캔버스와 씬을 준비한 뒤 `draw()`로 데이터를 렌더링하고, 이후 `update()`, `focus()`, `fit()`, `rotation`, `flip`, `event`, `stateManager`, `transformer`를 통해 상호작용을 제어한다. + +공개 메서드와 프로퍼티를 옵션 단위로 빠르게 훑고 싶다면 먼저 `public-api.md`를 보고, 세부 데이터/상호작용/히스토리 동작은 각 주제 문서로 내려가는 방식이 가장 효율적이다. + +## 런타임 의존성과 전제 조건 + +- 필수 peer dependency는 `pixi.js >= 8`이다. +- 런타임 의존성은 `pixi-viewport`, `gsap`, `zod`, `zod-validation-error`, `jsonpath-plus`, `@pixi-essentials/bounds`, `is-plain-object`, `nanoid`다. +- `package.json` 기준 개발/빌드용 Node.js 요구사항은 `>=20`이다. +- `init()`은 DOM 엘리먼트가 필요하고, 내부적으로 `ResizeObserver`, `window`, `document`를 사용한다. +- `init()`은 기본 아이콘 번들과 Fira Code 폰트 번들을 미리 적재한다. `opts.assets`로 추가 자산을 병합할 수 있다. +- CDN 사용 시에는 README처럼 `pixi.js`를 먼저 로드하고, 그 다음 `@conalog/patch-map` 번들을 로드해야 한다. +- npm 설치는 `npm install @conalog/patch-map`을 사용한다. 호스트 앱에는 `pixi.js`를 함께 설치해야 한다. + +### `init()`의 기본 옵션 + +`src/init.js` 기준 기본값은 다음과 같다. + +- `app` + - `background: '#FAFAFA'` + - `antialias: true` + - `autoStart: true` + - `autoDensity: true` + - `useContextAlpha: true` + - `resolution: 2` +- `viewport` + - `passiveWheel: false` + - `clampZoom: { minScale: 0.5, maxScale: 30 }` + - `drag`, `wheel`, `pinch`, `decelerate` 플러그인 활성화 +- `theme` + - `primary`, `gray`, `white`, `black` 팔레트가 기본으로 들어 있다. + +## `src/patch-map.ts` 공개 export + +이 모듈은 아래를 외부로 노출한다. + +- `Patchmap` +- `Command` +- `UndoRedoManager` +- `State` +- `PROPAGATE_EVENT` +- `Transformer` +- `selector` +- `convertLegacyData` +- `./utils` 전체 재export + +## Patchmap 라이프사이클 + +### 생성 + +`new Patchmap()`은 아직 캔버스를 만들지 않는다. 이 시점에 내부적으로는 이벤트 emitter, theme store, `UndoRedoManager`, `gsap.context()`, `ViewTransform`이 준비되지만 `app`, `viewport`, `world`는 모두 `null`이다. + +### `init(element, opts)` + +`init()`은 한 번만 실제 초기화를 수행한다. 이미 초기화된 인스턴스라면 바로 반환한다. + +현재 구현 흐름은 다음과 같다. + +1. `UndoRedoManager`의 단축키를 등록한다. +2. `theme`를 병합한다. +3. `PIXI.Application`을 만들고 `initApp()`로 초기화한다. +4. `store`를 만든다. 이 store에는 `app`, `viewport`, `world`, `view`, `undoRedoManager`, `theme`, `animationContext`가 들어간다. +5. `initViewport()`로 `Viewport`를 만들고 `app.stage`에 붙인다. +6. `World`를 생성해 `viewport`의 자식으로 추가한다. +7. `ViewTransform`을 `viewport`와 `world`에 연결한다. +8. `initAsset()`로 자산을 로드한다. +9. `initCanvas()`로 `app.canvas`를 전달받은 엘리먼트 안에 넣는다. +10. `ResizeObserver`를 붙여 리사이즈 시 `app.resize()`, `viewport.resize()`, `ViewTransform.applyWorldTransform()`를 수행한다. +11. `StateManager`를 만들고 `selection` 상태를 기본 상태로 등록한다. +12. `opts.transformer`가 있으면 `transformer` setter로 연결한다. +13. `isInit = true`가 된 뒤 `patchmap:initialized`를 emit한다. + +`init()` 직후 사용자에게 보이는 관계는 다음과 같다. + +- `patchmap.app`는 `PIXI.Application`이다. +- `patchmap.viewport`는 `app.stage` 아래에 있다. +- `patchmap.world`는 `viewport`의 첫 번째 자식이다. +- `patchmap.theme`는 병합된 테마 객체의 얕은 복사본이다. 최상위 객체는 새로 만들어지지만, 내부 팔레트 객체는 공유 참조일 수 있다. + +### `draw(data)` + +`draw()`는 들어온 데이터를 먼저 `JSON.parse(JSON.stringify(data))`로 복제한 뒤 처리한다. 객체에 `grids` 키가 있으면 레거시 데이터로 보고 `convertLegacyData()`를 거친다. 이후 `validateMapData()`로 검증하고, 검증 에러면 그대로 throw한다. + +렌더링 직전에는 다음을 수행한다. + +- `app.stop()` +- `undoRedoManager.clear()` +- `animationContext.revert()` +- `event.removeAllEvent(viewport)` + +그 다음 `draw(store, validatedData)`를 호출한다. 초기 draw 이후에는 `relations` 요소를 한 번 더 갱신하기 위해, 다음 ticker tick에서 `update({ path: '$..[?(@.type=="relations")]', refresh: true, emit: false })`를 실행한다. draw가 끝나면 `app.start()`를 다시 호출하고, 사용자에게 보이는 시점에 `patchmap:draw`를 emit한다. `scheduler.postTask()`가 있으면 `user-visible` 우선순위로, 없으면 `setTimeout()`으로 지연 실행한다. + +`draw()`의 반환값은 검증을 통과한 데이터다. + +### `update(opts)` + +`update()`는 내부 `world`에 대해 `update(this.world, opts)`를 호출하고, 기본적으로 `patchmap:updated`를 emit한다. 단, `opts.emit === false`이면 이벤트를 내보내지 않는다. 반환값은 업데이트된 요소 목록이다. + +### `destroy()` + +초기화되지 않은 상태라면 아무 일도 하지 않는다. 초기화된 상태에서는 다음 순서로 정리한다. + +- `undoRedoManager.destroy()` +- `animationContext.revert()` +- `stateManager.resetState()` 후 `stateManager.destroy()` +- `event.removeAllEvent(viewport)` +- `viewport.destroy({ children: true, context: true, style: true })` +- `app.destroy(true)` 후 `app.canvas.parentElement.remove()` +- `ResizeObserver.disconnect()` + +그 뒤 내부 참조를 전부 초기화하고 `isInit = false`로 되돌린 다음 `patchmap:destroyed`를 emit한다. 마지막에 `removeAllListeners()`까지 호출한다. + +## 사용자에게 보이는 상태와 관계 + +### `app`, `viewport`, `world`, `theme` + +- `app`는 Pixi 애플리케이션이다. +- `viewport`는 줌/드래그/핀치/휠 플러그인을 붙인 카메라 레이어다. +- `world`는 실제 도형과 컴포넌트가 렌더링되는 루트 컨테이너다. +- `theme`는 `themeStore()` 기반 병합 객체이며 getter는 얕은 복사본을 반환한다. +- `selector()`와 `focus()/fit()`는 모두 `world`를 기준으로 동작한다. + +`StateManager`가 state를 `enter(store, ...)`로 넘길 때 받는 값은 내부 draw store가 아니라 `Patchmap` 인스턴스 자신이다. 그래서 커스텀 state에서는 `store.app`, `store.viewport`, `store.world`, `store.undoRedoManager`, `store.transformer`, `store.stateManager` 같은 `Patchmap` 공개 표면을 사용할 수 있지만, `_createStoreContext()`의 `view` 같은 내부 전용 필드를 직접 기대하면 안 된다. + +### `event` facade + +`patchmap.event`는 `src/utils/event/canvas.js`의 함수를 감싼 얇은 facade다. + +- `add(opts)`는 이벤트를 등록하고 즉시 활성화한다. 반환값은 이벤트 id다. +- `remove(id)`는 하나 또는 공백으로 구분된 여러 id를 제거한다. +- `removeAll()`은 등록된 이벤트를 전부 제거한다. +- `on(id)`과 `off(id)`는 활성화/비활성화만 제어한다. +- `get(id)`과 `getAll()`은 현재 등록 상태를 읽는다. + +`path: '$'`는 `viewport` 자체를 대상으로 한다. 그 외 경로는 `world`를 루트로 `selector()`가 해석한다. `elements`를 직접 넘기면 그 객체들에만 이벤트가 연결된다. + +### `stateManager` + +`StateManager`는 스택 기반 상태 머신이다. `init()` 시점에 기본 `selection` 상태가 이미 등록된다. 사용자는 `register(name, StateClassOrObject, isSingleton)`로 상태를 추가하고 `setState()`, `pushState()`, `popState()`, `activateModifier()`, `deactivateModifier()`, `resetState()`를 사용할 수 있다. + +현재 구현에서 알아둘 이벤트는 다음과 같다. + +- `state:pushed` +- `state:popped` +- `state:set` +- `state:reset` +- `state:destroyed` +- `modifier:activated` +- `modifier:deactivated` + +### `undoRedoManager` + +`patchmap.undoRedoManager`는 `Patchmap` 인스턴스가 기본으로 들고 있는 history 관리자다. 상세 계약은 `history-and-transformer.md`를 보는 편이 맞지만, 개요 수준에서는 다음 정도를 알아두면 충분하다. + +- `execute(command, { historyId })`로 명령을 기록한다. +- `undo()`, `redo()`, `canUndo()`, `canRedo()`, `clear()`를 제공한다. +- `init()` 시점에 기본 단축키를 등록한다. + - `Ctrl/Cmd + Z`: undo + - `Ctrl/Cmd + Shift + Z`: redo + - `Ctrl/Cmd + Y`: redo +- `draw()`는 새 데이터를 다시 그리기 전에 기존 history를 비운다. +- 관련 이벤트는 `history:executed`, `history:undone`, `history:redone`, `history:cleared`, `history:destroyed`, `history:*`다. + +### `transformer` + +`patchmap.transformer`는 `Transformer` 인스턴스만 허용한다. 새 값을 넣으면 기존 transformer가 살아 있을 때 먼저 `object_transformed` 리스너를 해제하고 `destroy(true)`로 정리한 뒤, 새 transformer를 `viewport`에 추가한다. 이후 `viewport`의 `object_transformed` 이벤트가 `transformer.update`와 연결된다. + +## focus / fit / rotation / flip / selector + +### `focus(ids, opts)`와 `fit(ids, opts)` + +- `ids`는 `string`, `string[]`, `null`, `undefined`를 받을 수 있다. +- `ids`가 비어 있으면 최상위 `world` 자식들에 대해 동작한다. +- `filter` 옵션을 줄 수 있다. +- `fit()`은 `padding`을 추가로 받는다. 현재 구현에서 `padding`은 `number` 또는 `{ x, y }`만 허용하고, 기본값은 좌우/상하 각각 `16`이다. +- `relations`를 직접 가리키면 즉시 안정적인 bounds가 없을 수 있어서, 가능한 경우 연결된 endpoint로 해석한다. + +### `rotation` / `flip` + +`patchmap.rotation`은 회전 컨트롤러다. + +- `value` +- `set(value)` +- `rotateBy(delta)` +- `reset()` + +`patchmap.flip`은 뒤집기 컨트롤러다. + +- `x`, `y` getter/setter +- `set({ x, y })` +- `toggleX()` +- `toggleY()` +- `reset()` + +회전 또는 flip이 바뀌면 각각 `patchmap:rotated`, `patchmap:flipped`가 emit된다. 내부적으로는 world의 pivot, position, angle, scale을 다시 맞춘다. + +### `selector(path, opts)` + +`selector()`는 `JSONSearch`를 사용해 `world`를 기준으로 검색한다. `searchableKeys`는 `children`이고 결과는 `flatten: true`로 펼쳐진다. 예를 들어 `patchmap.selector('$')[0]`는 `patchmap.world`다. + +## 자주 쓰는 이벤트 + +### Patchmap + +- `patchmap:initialized` +- `patchmap:draw` +- `patchmap:updated` +- `patchmap:rotated` +- `patchmap:flipped` +- `patchmap:destroyed` + +### Transformer + +- `update_elements` + +### StateManager + +- `state:pushed` +- `state:popped` +- `state:set` +- `state:reset` +- `state:destroyed` +- `modifier:activated` +- `modifier:deactivated` + +## 짧은 사용 예시 + +### 초기화와 렌더링 + +```js +import { Patchmap } from '@conalog/patch-map'; + +const patchmap = new Patchmap(); + +await patchmap.init(document.body); + +patchmap.draw([ + { + type: 'group', + id: 'group-1', + children: [ + { + type: 'item', + id: 'item-1', + attrs: { x: 100, y: 120 }, + }, + ], + }, +]); +``` + +### 갱신, 포커스, 회전 + +```js +patchmap.update({ + path: '$..[?(@.id=="item-1")]', + changes: { attrs: { x: 240 } }, +}); + +patchmap.focus('item-1'); +patchmap.fit('item-1', { padding: { x: 12, y: 8 } }); + +patchmap.rotation.set(90); +patchmap.flip.toggleX(); +``` + +### 이벤트, 선택기, transformer + +```js +import { Transformer } from '@conalog/patch-map'; + +const id = patchmap.event.add({ + path: '$..[?(@.id=="item-1")]', + action: 'pointerdown', + fn: (event) => { + console.log(event); + }, +}); + +const item = patchmap.selector('$..[?(@.id=="item-1")]')[0]; +patchmap.event.off(id); +patchmap.event.remove(id); + +patchmap.transformer = new Transformer(); +patchmap.transformer.elements = item ? [item] : []; +``` + +### 사용자 상태 등록 + +```js +import { State } from '@conalog/patch-map'; + +class CustomState extends State { + static handledEvents = ['onpointerdown']; + onpointerdown(event) { + console.log(event); + } +} + +patchmap.stateManager.register('custom', CustomState); +patchmap.stateManager.setState('custom'); +``` diff --git a/docs/reference/patchmap/public-api.md b/docs/reference/patchmap/public-api.md new file mode 100644 index 00000000..8bd42e39 --- /dev/null +++ b/docs/reference/patchmap/public-api.md @@ -0,0 +1,603 @@ +# Patchmap Public API 가이드 + +이 문서는 `Patchmap` 클래스의 공개 메서드와 주요 프로퍼티를 한 곳에서 설명한다. 목표는 `docs/reference/patchmap/`만 읽어도 README 없이 현재 구현 기준 사용법을 파악할 수 있게 하는 것이다. + +상세 데이터 스키마는 `data-model.md`, 상호작용 계층은 `interactions.md`, history/transformer는 `history-and-transformer.md`를 함께 보면 된다. 이 문서는 그중에서도 `Patchmap` 인스턴스가 직접 노출하는 표면에 집중한다. + +## 1. 공개 표면 요약 + +`src/patchmap.js` 기준으로 실사용자가 직접 만나는 `Patchmap` 표면은 아래와 같다. + +- getter + - `app` + - `viewport` + - `world` + - `theme` + - `isInit` + - `undoRedoManager` + - `transformer` + - `stateManager` + - `animationContext` + - `event` +- 메서드 + - `init(element, opts)` + - `destroy()` + - `draw(data)` + - `update(opts)` + - `focus(ids, opts)` + - `fit(ids, opts)` + - `selector(path, opts)` +- 컨트롤러 getter + - `rotation` + - `flip` + +## 2. 생성과 초기화 + +### `new Patchmap()` + +인스턴스 생성만으로는 Pixi canvas가 만들어지지 않는다. + +- 생성 직후 기본 상태 + - `app`, `viewport`, `world`, `stateManager`, `transformer`는 `null` + - `isInit`은 `false` + - `undoRedoManager`는 새 인스턴스로 준비됨 + - `theme`는 기본 테마를 가진 store 기반 getter + - `rotation`, `flip`은 아직 world가 없어도 호출 가능한 컨트롤러 + +### `await init(element, opts)` + +`Patchmap` 런타임을 실제 DOM에 붙이는 진입점이다. + +- 인자 + - `element` + - canvas 래퍼를 붙일 DOM 요소 + - `ResizeObserver`가 이 요소를 관찰한다 + - `opts` + - 선택 객체, 생략 가능 + +#### `init()` 특징 + +- `async` 메서드다. +- 이미 초기화된 인스턴스면 바로 반환한다. +- `patchmap:initialized` 이벤트를 emit한다. +- `selection` 상태를 `stateManager`에 기본 등록한다. +- `opts.transformer`가 있으면 초기화 마지막 단계에서 자동 attach한다. + +#### `init()` 옵션 + +##### `opts.app` + +`PIXI.Application.init()`으로 전달되는 옵션이다. 내부적으로 `resizeTo: element`가 강제로 합쳐진다. + +기본값: + +```js +{ + background: '#FAFAFA', + antialias: true, + autoStart: true, + autoDensity: true, + useContextAlpha: true, + resolution: 2, +} +``` + +##### `opts.viewport` + +`pixi-viewport` 생성 옵션이다. 내부적으로 아래 필드가 자동 주입된다. + +- `screenWidth: app.screen.width` +- `screenHeight: app.screen.height` +- `events: app.renderer.events` + +기본값: + +```js +{ + passiveWheel: false, + plugins: { + clampZoom: { minScale: 0.5, maxScale: 30 }, + drag: {}, + wheel: {}, + pinch: {}, + decelerate: {}, + }, +} +``` + +주의사항: + +- `plugins`는 단순 옵션 객체가 아니라 초기화 시 자동 등록될 viewport plugin 집합이다. +- `{ disabled: true }`가 붙은 플러그인은 등록되지 않는다. + +##### `opts.theme` + +기본 테마에 deep merge된다. + +기본 구조: + +```js +{ + primary: { + default: '#0C73BF', + dark: '#083967', + accent: '#EF4444', + }, + gray: { + light: '#9EB3C3', + default: '#D9D9D9', + dark: '#71717A', + }, + white: '#FFFFFF', + black: '#1A1A1A', +} +``` + +##### `opts.assets` + +Pixi `Assets`에 추가로 등록/로드할 asset 정의 배열이다. 현재 구현은 두 형식을 모두 받는다. + +- bundle 형식 + +```js +{ + name: 'icons', + items: [ + { alias: 'custom-icon', src: '/icons/custom.svg' }, + ], +} +``` + +- 단일 asset 형식 + +```js +{ + alias: 'logo', + src: '/images/logo.png', +} +``` + +현재 구현 규칙: + +- 기본 아이콘 번들과 Fira Code 폰트 번들이 항상 먼저 merge 대상에 포함된다. +- bundle은 `name`, 단일 asset은 `alias` 기준으로 중복 등록을 피한다. +- 이미 Pixi resolver에 등록된 항목은 다시 추가하지 않는다. +- bundle과 단일 asset은 각각 `Assets.loadBundle()` / `Assets.load()`로 함께 로드된다. + +##### `opts.transformer` + +`Transformer` 인스턴스를 넘기면 `init()` 마지막 단계에서 `patchmap.transformer = transformer`가 실행된다. + +예시: + +```js +import { Patchmap, Transformer } from '@conalog/patch-map'; + +const patchmap = new Patchmap(); + +await patchmap.init(document.body, { + app: { background: '#eceff3' }, + viewport: { + plugins: { + decelerate: { disabled: true }, + }, + }, + theme: { + primary: { default: '#c2410c' }, + }, + assets: [ + { alias: 'logo', src: '/logo.png' }, + ], + transformer: new Transformer({ resizeHandles: true }), +}); +``` + +## 3. 렌더링 메서드 + +### `draw(data)` + +맵 데이터를 새로 렌더링한다. + +#### 입력 + +- `data` + - 일반적으로 `Element[]` + - 레거시 형식이면 `{ grids, strings, ... }` 객체도 허용 + +#### 현재 구현 흐름 + +1. `JSON.parse(JSON.stringify(data))`로 입력을 복제한다. +2. 복제 결과가 레거시 형식이면 `convertLegacyData()`로 변환한다. +3. `validateMapData()`로 검증한다. +4. 기존 실행 상태를 정리한다. + - `app.stop()` + - `undoRedoManager.clear()` + - `animationContext.revert()` + - `event.removeAllEvent(viewport)` +5. `world`를 새 데이터로 다시 그린다. +6. 다음 ticker tick에서 `relations`만 `refresh: true`로 한 번 더 갱신한다. +7. `app.start()`를 호출한다. +8. 비동기적으로 `patchmap:draw`를 emit한다. + +#### 반환값 + +- 성공 시 검증된 데이터 +- 레거시 입력이면 변환 후 데이터 +- `null`처럼 clone 이후 falsy가 되는 입력은 조용히 `undefined` +- 검증 실패 시 예외 throw + +#### `draw()`를 쓸 때 알아둘 점 + +- 새 draw는 이전 history를 지운다. +- 등록된 canvas 이벤트도 모두 제거된다. +- `patchmap:draw`는 동기 이벤트가 아니다. `scheduler.postTask()` 또는 `setTimeout()`으로 늦게 실행된다. +- 입력은 JSON 복제를 거치므로 함수, 클래스 인스턴스, 순환 참조는 안전하지 않다. + +예시: + +```js +const rendered = patchmap.draw([ + { + type: 'item', + id: 'item-1', + size: 80, + }, +]); + +console.log(rendered[0].id); // item-1 +``` + +### `update(opts = {})` + +이미 렌더링된 객체를 선택해서 변경한다. + +`Patchmap.update()`는 내부적으로 `src/display/update.js`를 그대로 감싼다. 즉 README에 없는 옵션도 현재 구현상은 전달된다. + +#### 반환값 + +- 실제로 처리 대상으로 잡힌 요소 배열 +- 대상이 없으면 빈 배열 + +#### `update()` 대상 선택 규칙 + +- `elements` + - 직접 참조를 넘기는 방식 + - 단일 객체 또는 배열 허용 +- `path` + - `selector()`로 찾는 방식 +- 두 옵션을 함께 쓰면 합쳐진다. +- 중복 제거는 하지 않으므로 같은 요소가 `elements`와 `path` 양쪽에서 잡히면 두 번 적용될 수 있다. +- falsy 대상은 순회 중 건너뛴다. + +#### `update()` 옵션 + +##### `path?: string` + +`world`를 루트로 하는 JSONPath 스타일 selector다. + +```js +path: '$..[?(@.id=="item-1")]' +``` + +##### `elements?: object | object[]` + +이미 잡아둔 요소 참조를 직접 넘길 수 있다. + +```js +const item = patchmap.selector('$..[?(@.id=="item-1")]')[0]; +patchmap.update({ elements: item, changes: { show: false } }); +``` + +##### `changes?: object | null` + +적용할 변경사항이다. + +- 기본적으로 patch merge 대상으로 해석된다. +- `refresh: true`일 때는 생략 가능하다. +- `refresh: false`인데 `changes`도 없으면 대상 탐색만 하고 실질 변경은 일어나지 않는다. +- `relativeTransform: true`면 내부적으로 복제 후 `attrs.x`, `attrs.y`, `attrs.rotation`, `attrs.angle`만 상대값 처리한다. + +##### `history?: boolean | string` + +history 기록 여부와 묶음 단위를 정한다. + +- `false` 또는 생략 + - history 기록 안 함 +- `true` + - 호출마다 새 `historyId`를 자동 생성 +- `'some-id'` + - 같은 문자열을 쓰는 **연속 update**를 하나의 undo/redo 단계로 묶음 + - 중간에 다른 history가 끼면 새 묶음이 시작된다 + +##### `relativeTransform?: boolean` + +`changes.attrs` 안의 아래 숫자 필드만 절대값이 아니라 상대 델타로 처리한다. + +- `x` +- `y` +- `rotation` +- `angle` + +예시: + +```js +patchmap.update({ + path: '$..[?(@.id=="item-1")]', + changes: { attrs: { x: 20, y: -10 } }, + relativeTransform: true, +}); +``` + +이 경우 현재 좌표에 `(+20, -10)`이 더해진다. + +##### `mergeStrategy?: 'merge' | 'replace'` + +기본값은 `'merge'`다. + +- `'merge'` + - deep merge + - 일부 필드만 갱신할 때 사용 +- `'replace'` + - 지정한 top-level 필드를 통째로 교체 + - undo 복원이나 style/component 전체 교체에 적합 + +##### `refresh?: boolean` + +기본값은 `false`다. + +- `true`면 실제 diff가 없어도 핸들러를 강제로 다시 실행한다. +- 부모/관계선처럼 계산 기반 자식을 다시 만들고 싶을 때 유용하다. + +##### `emit?: boolean` + +이 옵션은 `Patchmap.update()` 레벨에서만 해석된다. + +- 기본값은 `true` +- `false`면 `patchmap:updated`를 emit하지 않는다. +- 요소 업데이트 자체는 계속 수행된다. + +##### `validateSchema?: boolean` + +이 옵션은 내부 `element.apply()`로 전달된다. + +- 기본값은 `true` +- `false`면 schema 검증 없이 적용한다. +- 정상 public usage에서는 끄지 않는 편이 안전하다. + +##### `normalize?: boolean` + +이 옵션도 내부 `element.apply()`로 전달된다. + +- 기본값은 `true` +- `false`면 `size: 80`, `gap: 4`, `margin: { x, y }` 같은 shorthand 정규화를 건너뛴다. + +#### `update()` 예시 + +기본 patch update: + +```js +patchmap.update({ + path: '$..[?(@.id=="item-1")]', + changes: { + attrs: { x: 240 }, + show: true, + }, +}); +``` + +history를 묶어서 여러 번 호출: + +```js +patchmap.update({ + path: '$..[?(@.id=="item-1")]', + changes: { attrs: { x: 10 } }, + relativeTransform: true, + history: 'drag-item-1', +}); + +patchmap.update({ + path: '$..[?(@.id=="item-1")]', + changes: { attrs: { y: 12 } }, + relativeTransform: true, + history: 'drag-item-1', +}); +``` + +강제 refresh: + +```js +patchmap.update({ + path: '$..[?(@.type=="relations")]', + refresh: true, + emit: false, +}); +``` + +정규화/검증을 끄는 내부 지향 호출: + +```js +patchmap.update({ + elements: patchmap.selector('$..[?(@.id=="item-1")]'), + changes: { + size: { width: 100, height: 80 }, + }, + validateSchema: false, + normalize: false, +}); +``` + +#### `draw()`와 `update()`의 역할 차이 + +- `draw()` + - 전체 scene을 새로 렌더링 + - history/event/animation 초기화 + - 입력 검증 후 canvas 루트 교체 +- `update()` + - 기존 scene 일부만 갱신 + - 선택된 요소에만 patch 적용 + - history, refresh, relative transform 제어 가능 + +## 4. 탐색과 뷰포트 제어 + +### `focus(ids?, opts?)` + +현재 뷰포트 중심을 특정 대상 쪽으로 이동시킨다. 확대/축소는 하지 않는다. + +- `ids` + - `string | string[] | null | undefined` +- `opts.filter` + - `(obj) => unknown` + +핵심 규칙: + +- `ids`가 없으면 top-level 관리 요소(기본적으로 `relations` 제외)를 대상으로 한다. +- `relations` id를 직접 넘기면 가능한 경우 연결 endpoint를 사용한다. +- 결과가 없으면 `null`을 반환한다. + +```js +patchmap.focus('item-1'); +patchmap.focus(['item-1', 'item-2']); +patchmap.focus(null, { + filter: (obj) => obj.id !== 'background-image', +}); +``` + +### `fit(ids?, opts?)` + +대상을 화면 안에 맞게 center + zoom을 동시에 수행한다. + +- `ids` + - `string | string[] | null | undefined` +- `opts.filter` + - `(obj) => unknown` +- `opts.padding` + - `number | { x?: number, y?: number }` + - 기본 패딩은 `{ x: 16, y: 16 }` + +```js +patchmap.fit('group-1', { padding: 24 }); +patchmap.fit(['item-1', 'item-2'], { padding: { x: 8, y: 12 } }); +``` + +### `selector(path, opts?)` + +`world`를 루트로 하는 object 탐색기다. + +- `path` + - JSONPath 스타일 문자열 + - `'$'`는 `world`를 가리킨다 +- `opts` + - 내부 `JSONSearch` 옵션으로 pass-through된다 + - 기본값은 `searchableKeys: ['children']`, `flatten: true` + - 필요하면 override 가능하다 + +```js +const allGroups = patchmap.selector('$..[?(@.type=="group")]'); +const raw = patchmap.selector('$', { flatten: false }); +``` + +### `rotation` + +world view 회전 컨트롤러다. 단위는 degree다. + +- `rotation.value` +- `rotation.set(value)` +- `rotation.rotateBy(delta)` +- `rotation.reset()` + +### `flip` + +world view 반전 컨트롤러다. + +- `flip.x` +- `flip.y` +- `flip.set({ x, y })` +- `flip.toggleX()` +- `flip.toggleY()` +- `flip.reset()` + +## 5. 인스턴스 프로퍼티 + +### `app` + +초기화 후 `PIXI.Application` 인스턴스. + +### `viewport` + +초기화 후 `pixi-viewport` 인스턴스. + +- plugin 제어는 `interactions.md` 참고 + +### `world` + +실제 element tree가 붙는 루트 컨테이너. + +### `theme` + +현재 테마 getter. 얕은 복사본을 반환한다. + +### `undoRedoManager` + +현재 인스턴스가 사용하는 history 관리자. + +- 상세는 `history-and-transformer.md` + +### `stateManager` + +현재 인스턴스의 상태 머신 관리자. + +- 상세는 `interactions.md` + +### `transformer` + +현재 연결된 `Transformer` 인스턴스. + +- setter에 `Transformer`가 아닌 값을 넣으면 `console.error` 후 `null` 처리 +- 기존 transformer가 있으면 교체 전에 destroy + +### `animationContext` + +GSAP context getter. draw/destroy 시 `revert()` 대상이다. + +### `event` + +canvas event facade다. + +- `add(opts)` +- `remove(id)` +- `removeAll()` +- `on(id)` +- `off(id)` +- `get(id)` +- `getAll()` + +상세 규약은 `interactions.md` 참고. + +## 6. 종료와 재초기화 + +### `destroy()` + +현재 인스턴스를 정리한다. + +- history listener 제거 +- animation context revert +- state reset/destroy +- canvas event 제거 +- viewport destroy +- app destroy +- canvas wrapper DOM 제거 +- resize observer disconnect +- 내부 참조 초기화 +- `patchmap:destroyed` emit +- listener 전체 제거 + +현재 구현 기준으로는 `destroy()` 후에도 같은 인스턴스를 다시 `init()`할 수 있다. 내부 상태를 새로 만들어 두기 때문이다. + +## 7. README의 `asset` 섹션에 대한 보정 + +현재 구현에는 `patchmap.asset` 같은 공개 메서드/프로퍼티가 없다. README의 `asset` 항목은 Pixi `Assets` 일반 문서를 가리키는 참고 링크에 가깝다. + +실제 `Patchmap` 공개 표면에서 asset과 직접 연결되는 부분은 다음 둘이다. + +- `init({ assets })` +- component / element의 `source` 필드가 Pixi asset alias를 참조할 수 있다는 점 diff --git a/docs/tasks/2026/03-25/patchmap-feature-guides/BRIEF.md b/docs/tasks/2026/03-25/patchmap-feature-guides/BRIEF.md new file mode 100644 index 00000000..a3e86400 --- /dev/null +++ b/docs/tasks/2026/03-25/patchmap-feature-guides/BRIEF.md @@ -0,0 +1,28 @@ +Goal +- Create source-backed feature guides for the current `patch-map` implementation. + +Scope +- Public entry points from `src/patch-map.ts`. +- `Patchmap` lifecycle and interaction APIs from `src/patchmap.js`. +- Map data schema, renderable elements, and components from `src/display/data-schema/data.d.ts` and related schema files. +- Event, selection, focus/fit, history, transformer, and utility surfaces that affect usage. + +Current understanding +- The repository has no existing `docs/` project-context structure. +- `README.md` documents major APIs but does not yet provide subsystem-level guides. +- Documentation can target the current JS/TS implementation; implementation-independent abstraction is not required in this phase. + +Current output snapshot +- Completed reference docs under `docs/reference/patchmap/`: + - `public-api.md` + - `overview.md` + - `data-model.md` + - `interactions.md` + - `history-and-transformer.md` + - `coverage-checklist.md` +- Review loop completed: + - overview reviewer findings reflected in `overview.md` + - interactions reviewer findings reflected in `interactions.md` + - history/transformer reviewer findings reflected in `history-and-transformer.md` + - data-model reviewer findings reflected in `data-model.md` + - public API completeness gaps reflected in `public-api.md` and `coverage-checklist.md` diff --git a/docs/tasks/2026/03-25/patchmap-feature-guides/STATUS.md b/docs/tasks/2026/03-25/patchmap-feature-guides/STATUS.md new file mode 100644 index 00000000..0f4c5ad9 --- /dev/null +++ b/docs/tasks/2026/03-25/patchmap-feature-guides/STATUS.md @@ -0,0 +1,6 @@ +- Status: feature guides plus dedicated public API guide drafted, and 공개 메서드 옵션 커버리지까지 보강 완료. +- Next step: no immediate next implementation step; future sessions can extend or translate the guides if needed. +- Blockers: none. +- Declared read scope: `README.md`, `package.json`, `src/patch-map.ts`, `src/patchmap.js`, `src/init.js`, `src/display/data-schema/**`, `src/events/**`, `src/command/**`, `src/transformer/**`, `src/utils/**`. +- Declared write scope: `docs/memory.md`, `docs/plans/2026-03-25-patchmap-feature-guides.md`, `docs/reference/patchmap/**`, `docs/tasks/2026/03-25/patchmap-feature-guides/**`. +- Latest validation: `find docs/reference/patchmap -maxdepth 1 -type f | sort` confirmed 6 guide files; `python3 /Users/minholim/.agents/skills/project-context/scripts/check_runtime_shape.py` returned `[OK]`. diff --git a/docs/tasks/2026/03-25/patchmap-feature-guides/logs/DECISIONS.md b/docs/tasks/2026/03-25/patchmap-feature-guides/logs/DECISIONS.md new file mode 100644 index 00000000..09f394a5 --- /dev/null +++ b/docs/tasks/2026/03-25/patchmap-feature-guides/logs/DECISIONS.md @@ -0,0 +1,11 @@ +**2026-03-25** +- 배경: `patch-map`의 현재 기능을 재사용 가능한 저장소 문서로 남겨야 했고, 이후 세션에서도 작업 상태를 복원할 수 있어야 했다. +- 선택지: README만 확장, 임시 메모만 남김, `project-context` 구조를 부트스트랩하고 기준 문서를 분리. +- 결정: `project-context`를 부트스트랩하고 `docs/reference/patchmap/`에 기능 영역별 문서를 작성한다. +- 영향: 이후 문서 작성, 리뷰, 커버리지 검증이 모두 동일한 작업 트리와 로그를 기준으로 이어진다. + +**2026-03-25** +- 배경: README 설명과 현재 구현 사이에 차이가 있어 문서가 소스 기준으로 재검토되어야 했다. +- 선택지: README 서술을 우선, 소스 구현을 우선, 차이점을 모두 병기. +- 결정: 현재 문서는 README보다 소스 구현을 우선하고, 리뷰 에이전트가 잡은 구현 차이를 직접 반영한다. +- 영향: `overview.md`, `interactions.md`, `history-and-transformer.md`, `data-model.md`가 현재 런타임 동작에 더 가깝게 유지된다. diff --git a/docs/tasks/2026/03-25/patchmap-feature-guides/logs/WORKLOG.md b/docs/tasks/2026/03-25/patchmap-feature-guides/logs/WORKLOG.md new file mode 100644 index 00000000..6d71eb79 --- /dev/null +++ b/docs/tasks/2026/03-25/patchmap-feature-guides/logs/WORKLOG.md @@ -0,0 +1,9 @@ +**2026-03-25** +- `README.md`, `package.json`, `src/patch-map.ts`, `src/patchmap.js`, `src/init.js`를 읽어 초기 public API와 런타임 초기화 흐름을 파악했다. +- `project-context` 구조와 이번 문서화 작업용 계획/상태/로그 파일을 생성했다. +- 서브 에이전트를 기능 영역별(`overview`, `data-model`, `interactions`, `history-and-transformer`) 문서 작성 범위로 분리해 병렬 작성했다. +- 작성 직후 별도 리뷰 에이전트를 붙여 `overview.md`, `interactions.md`, `history-and-transformer.md`, `data-model.md`의 구현 불일치를 점검했다. +- 리뷰 결과를 반영해 `stateManager` store 설명, `SelectionState` 전제조건, `focus/fit` 탐색 경로, transformer 예시 순서, grid cell 규칙, style 기본값 설명을 수정했다. +- `coverage-checklist.md`를 추가하고 `find docs/reference/patchmap -maxdepth 1 -type f | sort`, `python3 /Users/minholim/.agents/skills/project-context/scripts/check_runtime_shape.py`로 최종 검증했다. +- reference만으로 공개 메서드 사용법을 파악 가능한지 다시 검토한 뒤, `draw()`/`update()`/`init()`/`focus()`/`fit()`/getter 표면을 한 곳에 모은 `public-api.md`를 추가했다. +- `public-api.md`에는 `update()`의 `emit`, `validateSchema`, `normalize`처럼 README에 없지만 현재 구현상 노출되는 옵션까지 기록했고, `coverage-checklist.md`를 그 구조에 맞게 갱신했다. From 304bf55716b5b3425cfee517efb9a5ba9c35e56f Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Wed, 25 Mar 2026 12:28:38 +0900 Subject: [PATCH 2/8] docs(patchmap): add developer context guide --- docs/memory.md | 1 + docs/reference/patchmap/coverage-checklist.md | 10 + docs/reference/patchmap/developer-context.md | 249 ++++++++++++++++++ docs/reference/patchmap/overview.md | 2 +- .../03-25/patchmap-feature-guides/BRIEF.md | 9 +- .../03-25/patchmap-feature-guides/STATUS.md | 8 +- .../patchmap-feature-guides/logs/DECISIONS.md | 6 + .../patchmap-feature-guides/logs/WORKLOG.md | 5 + 8 files changed, 282 insertions(+), 8 deletions(-) create mode 100644 docs/reference/patchmap/developer-context.md diff --git a/docs/memory.md b/docs/memory.md index 902d83f1..0f716ddc 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -3,5 +3,6 @@ - Active task workspace: `docs/tasks/2026/03-25/patchmap-feature-guides/`. - Documentation should map code-level behavior to user-facing usage with links to the relevant source files. - Feature coverage must include public exports, map data schema, rendering elements/components, interaction APIs, history, and transformer behavior. +- Agent handoff is only complete when reference docs also state source ownership and validation commands for common change types. - Use subagents with narrow read/write scopes and keep their outputs reviewed before merge. - Final completion requires a fresh coverage check against current exports, README API sections, and relevant source modules. diff --git a/docs/reference/patchmap/coverage-checklist.md b/docs/reference/patchmap/coverage-checklist.md index 37fba7c2..293544e7 100644 --- a/docs/reference/patchmap/coverage-checklist.md +++ b/docs/reference/patchmap/coverage-checklist.md @@ -103,3 +103,13 @@ - `project-context` runtime shape checker를 다시 실행한다. - 문서 파일들이 모두 `docs/reference/patchmap/` 아래에 존재하는지 확인한다. - `public-api.md`만 읽어도 공개 메서드 옵션 표면이 추적 가능한지 다시 점검한다. + +## 8. 개발자 컨텍스트 커버리지 + +| 영역 | 설명 위치 | 상태 | +| --- | --- | --- | +| 배포/소스 진입점(`package.json`, `src/patch-map.ts`) | `developer-context.md` | covered | +| 런타임 본체와 책임 경계(`src/patchmap.js`, `src/init.js`) | `developer-context.md` | covered | +| draw/update 구현 진입점(`src/display/draw.js`, `src/display/update.js`) | `developer-context.md`, `public-api.md` | covered | +| 변경 유형별 우선 읽기 파일 | `developer-context.md` | covered | +| 빌드/테스트/문서 검증 명령 | `developer-context.md` | covered | diff --git a/docs/reference/patchmap/developer-context.md b/docs/reference/patchmap/developer-context.md new file mode 100644 index 00000000..9f9e0801 --- /dev/null +++ b/docs/reference/patchmap/developer-context.md @@ -0,0 +1,249 @@ +# Patchmap 개발자 컨텍스트 + +이 문서는 `docs/reference/patchmap/`만 읽고도 개발 에이전트가 현재 `patch-map` 저장소의 구현 진입점, 수정 지점, 검증 루틴을 파악할 수 있도록 정리한 개발자용 안내서다. + +현재 reference 세트는 기능 사용법만이 아니라, "어떤 변경을 하려면 어디를 읽고 어디를 고쳐야 하는가"까지 문서에서 바로 찾을 수 있어야 한다. 이 문서는 그 목적을 위해 `package.json`, `src/patch-map.ts`, `src/patchmap.js`, `src/init.js`, `src/display/draw.js`, `src/display/update.js`, 그리고 기존 기능 가이드들의 근거 소스를 한 번 더 압축해 놓는다. + +## 1. 에이전트 관점 결론 + +현재 `overview.md`, `public-api.md`, `data-model.md`, `interactions.md`, `history-and-transformer.md`만으로는 "라이브러리를 어떻게 쓰는가"는 충분히 파악할 수 있다. + +하지만 그 조합만으로는 아래 개발 컨텍스트가 부족했다. + +- 어떤 파일이 공개 진입점이고 어떤 파일이 실제 런타임 본체인지 +- `draw()`와 `update()`가 실제로 어느 모듈을 호출하는지 +- 기능 수정 시 어떤 디렉터리를 우선 읽어야 하는지 +- 어떤 검증 명령을 어느 상황에서 실행해야 하는지 + +즉, 기존 문서만으로는 "코드를 전혀 안 보고 구현 변경을 설계"하기에는 정보가 모자랐고, 이 문서가 그 공백을 메운다. + +## 2. 저장소 진입점 + +### 배포 진입점 + +- 패키지 이름은 `@conalog/patch-map`이다. +- 패키지 export는 `package.json` 기준 아래 세 산출물을 가리킨다. + - ESM: `dist/index.esm.js` + - CJS: `dist/index.cjs.js` + - 타입: `dist/types/src/patchmap.d.ts` +- 소스 수준 공개 진입점은 `src/patch-map.ts`다. + +### 소스 수준 공개 export + +`src/patch-map.ts`는 아래만 외부로 내보낸다. + +- `Patchmap` +- `Command` +- `UndoRedoManager` +- `State` +- `PROPAGATE_EVENT` +- `Transformer` +- `selector` +- `convertLegacyData` +- `src/utils/index.js`의 재export 4개 + - `findIntersectObject` + - `isMoved` + - `intersectPoint` + - `uid` + +따라서 공개 API를 넓히거나 줄이는 작업은 대부분 `src/patch-map.ts`와 대응 문서(`public-api.md`, `coverage-checklist.md`)를 함께 수정해야 한다. + +## 3. 구현 책임 맵 + +현재 구현은 아래 책임 경계로 이해하면 가장 빠르다. + +### `src/patchmap.js` + +- 런타임 퍼사드이자 실제 `Patchmap` 클래스 본체다. +- 공개 getter, `init()`, `draw()`, `update()`, `focus()`, `fit()`, `selector()`, `rotation`, `flip`, `transformer` setter가 여기에 모여 있다. +- 공개 동작이 바뀌면 이 파일과 `public-api.md`를 같이 보는 것이 기본이다. + +### `src/init.js` + +- 초기화 전용 모듈이다. +- `initApp()`, `initViewport()`, `initAsset()`, `initResizeObserver()`, `initCanvas()`를 제공한다. +- 기본 app/viewport 옵션, 기본 asset 번들, viewport plugin 등록 규칙을 바꾸려면 여기서 시작한다. + +### `src/display/draw.js` + +- 새 데이터를 통째로 다시 그리는 경로다. +- `store.world.apply({ type: 'canvas', children: data }, { mergeStrategy: 'replace', validateSchema: false })`를 수행한다. +- draw 관련 버그를 추적할 때는 실제 렌더링 세부 구현보다 먼저 이 파일과 `patchmap.draw()` 호출부를 보면 된다. + +### `src/display/update.js` + +- 기존 요소 부분 갱신 경로다. +- `path`, `elements`, `changes`, `history`, `relativeTransform`, `mergeStrategy`, `refresh`를 해석한다. +- README에 없는 `validateSchema`, `normalize`도 그대로 `element.apply()`까지 전달된다. +- update 계약을 바꾸는 수정은 이 파일과 `public-api.md`, `history-and-transformer.md`, `data-model.md`가 함께 영향받는다. + +### `src/display/data-schema/**` + +- draw/update 입력 스키마의 기준 정의다. +- 새 요소 타입, 새 컴포넌트 타입, primitive 규칙, strict validation 변경은 여기서 시작한다. +- 관련 문서는 `data-model.md`다. + +### `src/events/**` + +- selection, focus/fit, 상태 머신, hit-test 규칙을 담는다. +- `StateManager`, `SelectionState`, 충돌 판정 헬퍼, focus/fit 규칙 변경은 이 폴더가 기준이다. +- 관련 문서는 `interactions.md`다. + +### `src/command/**` + +- undo/redo와 command abstraction을 담는다. +- 히스토리 묶음, command stack, hotkey 규칙 변경은 여기서 본다. +- 관련 문서는 `history-and-transformer.md`다. + +### `src/transformer/**` + +- transformer selection model, wireframe, resize handle/gesture를 담는다. +- 선택 bounds와 resize 동작을 바꾸려면 이 폴더를 읽는다. +- 관련 문서는 `history-and-transformer.md`다. + +### `src/utils/**` + +- 공개 util은 적지만 내부 공용 헬퍼가 넓게 퍼져 있다. +- selector, viewport helper, bounds, convert, uuid, theme, event helper가 여기에 있다. +- 공개 export 여부는 `src/utils/index.js`와 `src/patch-map.ts`를 함께 봐야 한다. + +## 4. 런타임 흐름을 문서만으로 따라가는 법 + +개발 에이전트가 코드를 열지 않고도 현재 동작을 추적하려면 아래 순서가 가장 효율적이다. + +1. 공개 표면과 옵션: `public-api.md` +2. 생성부터 destroy까지 생명주기: `overview.md` +3. 입력 데이터와 정규화: `data-model.md` +4. 선택, 이벤트, viewport, focus/fit: `interactions.md` +5. history, command, transformer: `history-and-transformer.md` +6. 수정 파일 위치와 검증 명령: 이 문서 + +실제 호출 체인은 아래처럼 압축할 수 있다. + +- `new Patchmap()` + - 내부 store/manager 껍데기만 준비 +- `init(element, opts)` + - app, viewport, world, asset, stateManager, optional transformer 준비 +- `draw(data)` + - clone -> legacy convert -> validate -> world replace draw -> 다음 tick relations refresh +- `update(opts)` + - selector/elements 해석 -> element.apply() 반복 -> optional history emit +- `destroy()` + - history/state/event/viewport/app teardown + +## 5. 변경 유형별 시작 지점 + +### 공개 메서드나 getter를 바꾸는 경우 + +- 먼저 읽을 파일 + - `src/patchmap.js` + - `src/patch-map.ts` +- 같이 갱신할 문서 + - `public-api.md` + - `overview.md` + - `coverage-checklist.md` + +### init 기본 옵션이나 asset 정책을 바꾸는 경우 + +- 먼저 읽을 파일 + - `src/init.js` + - `src/patchmap.js` +- 같이 갱신할 문서 + - `overview.md` + - `public-api.md` + +### draw/update 입력 형식이나 요소 타입을 바꾸는 경우 + +- 먼저 읽을 파일 + - `src/display/data-schema/**` + - `src/display/update.js` + - `src/display/draw.js` + - `src/utils/convert.js` + - `src/utils/validator.js` +- 같이 갱신할 문서 + - `data-model.md` + - `public-api.md` + - `coverage-checklist.md` + +### selection, focus/fit, canvas 이벤트를 바꾸는 경우 + +- 먼저 읽을 파일 + - `src/events/**` + - `src/utils/event/**` + - `src/utils/selector/**` +- 같이 갱신할 문서 + - `interactions.md` + - 필요 시 `overview.md` + +### undo/redo 또는 transformer를 바꾸는 경우 + +- 먼저 읽을 파일 + - `src/command/**` + - `src/transformer/**` + - `src/patchmap.js` +- 같이 갱신할 문서 + - `history-and-transformer.md` + - 필요 시 `public-api.md` + +## 6. 개발/검증 명령 + +현재 저장소의 표준 스크립트는 `package.json` 기준 아래와 같다. + +- `npm run build` + - `tsc && rollup -c` + - dist 산출물과 타입 출력까지 확인할 때 사용한다. +- `npm run test:unit` + - vitest unit 프로젝트 실행 +- `npm run test:browser` + - chromium 브라우저로 vitest browser 테스트 실행 +- `npm run test:headless` + - headless browser 테스트 실행 +- `npm run format` + - `biome format` +- `npm run lint` + - staged 파일 기준 `biome check` +- `npm run lint:fix` + - staged 파일 기준 `biome check --write` + +문서 작업만 할 때는 최소한 아래 정도가 현실적인 검증 루틴이다. + +- `python3 /Users/minholim/.agents/skills/project-context/scripts/check_runtime_shape.py` +- `find docs/reference/patchmap -maxdepth 1 -type f | sort` + +코드 변경까지 들어가면 변경 유형에 맞는 테스트를 추가해야 한다. + +- schema/update/history 성격 변경: `npm run test:unit` +- Pixi 렌더링/selection/transformer 변경: `npm run test:headless` 또는 `npm run test:browser` +- 배포 진입점/타입 영향 변경: `npm run build` + +## 7. 현재 reference 세트의 한계와 보완 상태 + +이 문서 추가 전 상태를 냉정하게 말하면, reference 세트는 "사용 설명서"로는 충분했지만 "개발 핸드오프 패키지"로는 불완전했다. + +부족했던 점은 아래 두 축이었다. + +- 구현 맥락 + - 어떤 파일이 어떤 책임을 가지는지 한 번에 보이지 않았다. +- 작업 절차 + - 어떤 변경에 어떤 검증을 연결해야 하는지 reference에 없었다. + +이 문서 추가 후에는 적어도 아래 질문에 대해 코드를 먼저 열지 않고 답할 수 있다. + +- 공개 표면은 어디서 export되는가 +- 런타임 본체는 어느 파일인가 +- draw/update/focus/transformer 변경은 어느 디렉터리부터 읽어야 하는가 +- 문서 변경과 코드 변경에 각각 어떤 검증을 돌려야 하는가 + +## 8. 실무용 최소 읽기 세트 + +새 세션의 개발 에이전트가 빠르게 복귀하려면 보통 아래 핵심 세트면 충분하다. + +- `docs/memory.md` +- `docs/reference/patchmap/overview.md` +- `docs/reference/patchmap/public-api.md` +- `docs/reference/patchmap/data-model.md` +- `docs/reference/patchmap/interactions.md` +- `docs/reference/patchmap/history-and-transformer.md` +- 구현 맥락과 수정 지점이 필요하면 이 문서 `developer-context.md`까지 읽는다. + +즉, 앞으로는 "reference 문서만 읽고 개발 시작"이라는 목표를 달성하려면 `developer-context.md`를 포함한 세트를 기준으로 삼는 편이 맞다. diff --git a/docs/reference/patchmap/overview.md b/docs/reference/patchmap/overview.md index e1bd8ca0..eac5e2b4 100644 --- a/docs/reference/patchmap/overview.md +++ b/docs/reference/patchmap/overview.md @@ -2,7 +2,7 @@ PATCH MAP은 `pixi.js`와 `pixi-viewport` 위에서 동작하는 캔버스 지도 라이브러리다. 현재 구현 기준으로는 `Patchmap` 인스턴스를 만들고 `init()`으로 캔버스와 씬을 준비한 뒤 `draw()`로 데이터를 렌더링하고, 이후 `update()`, `focus()`, `fit()`, `rotation`, `flip`, `event`, `stateManager`, `transformer`를 통해 상호작용을 제어한다. -공개 메서드와 프로퍼티를 옵션 단위로 빠르게 훑고 싶다면 먼저 `public-api.md`를 보고, 세부 데이터/상호작용/히스토리 동작은 각 주제 문서로 내려가는 방식이 가장 효율적이다. +공개 메서드와 프로퍼티를 옵션 단위로 빠르게 훑고 싶다면 먼저 `public-api.md`를 보고, 세부 데이터/상호작용/히스토리 동작은 각 주제 문서로 내려가는 방식이 가장 효율적이다. 구현 수정에 필요한 파일 책임과 검증 루틴은 `developer-context.md`를 보면 된다. ## 런타임 의존성과 전제 조건 diff --git a/docs/tasks/2026/03-25/patchmap-feature-guides/BRIEF.md b/docs/tasks/2026/03-25/patchmap-feature-guides/BRIEF.md index a3e86400..f2d09ab7 100644 --- a/docs/tasks/2026/03-25/patchmap-feature-guides/BRIEF.md +++ b/docs/tasks/2026/03-25/patchmap-feature-guides/BRIEF.md @@ -1,5 +1,6 @@ Goal - Create source-backed feature guides for the current `patch-map` implementation. +- Make the reference set sufficient for later development agents to start from docs alone, without rebuilding source-file context first. Scope - Public entry points from `src/patch-map.ts`. @@ -8,9 +9,9 @@ Scope - Event, selection, focus/fit, history, transformer, and utility surfaces that affect usage. Current understanding -- The repository has no existing `docs/` project-context structure. -- `README.md` documents major APIs but does not yet provide subsystem-level guides. -- Documentation can target the current JS/TS implementation; implementation-independent abstraction is not required in this phase. +- The repository now has a `project-context` task workspace for the patchmap guide set. +- The existing reference set explains runtime usage well, but developer-facing source ownership and validation workflow were under-documented. +- Documentation should continue to target the current JS/TS implementation; implementation-independent abstraction is not required in this phase. Current output snapshot - Completed reference docs under `docs/reference/patchmap/`: @@ -19,6 +20,7 @@ Current output snapshot - `data-model.md` - `interactions.md` - `history-and-transformer.md` + - `developer-context.md` - `coverage-checklist.md` - Review loop completed: - overview reviewer findings reflected in `overview.md` @@ -26,3 +28,4 @@ Current output snapshot - history/transformer reviewer findings reflected in `history-and-transformer.md` - data-model reviewer findings reflected in `data-model.md` - public API completeness gaps reflected in `public-api.md` and `coverage-checklist.md` + - developer handoff gaps reflected in `developer-context.md`, `overview.md`, `coverage-checklist.md`, and `docs/memory.md` diff --git a/docs/tasks/2026/03-25/patchmap-feature-guides/STATUS.md b/docs/tasks/2026/03-25/patchmap-feature-guides/STATUS.md index 0f4c5ad9..ebec6382 100644 --- a/docs/tasks/2026/03-25/patchmap-feature-guides/STATUS.md +++ b/docs/tasks/2026/03-25/patchmap-feature-guides/STATUS.md @@ -1,6 +1,6 @@ -- Status: feature guides plus dedicated public API guide drafted, and 공개 메서드 옵션 커버리지까지 보강 완료. -- Next step: no immediate next implementation step; future sessions can extend or translate the guides if needed. +- Status: feature guides에 더해 개발 에이전트용 구현 책임/검증 흐름 문서까지 보강 완료. +- Next step: no immediate next implementation step; future sessions can extend the guides or validate new code changes against the documented source map. - Blockers: none. -- Declared read scope: `README.md`, `package.json`, `src/patch-map.ts`, `src/patchmap.js`, `src/init.js`, `src/display/data-schema/**`, `src/events/**`, `src/command/**`, `src/transformer/**`, `src/utils/**`. +- Declared read scope: `README.md`, `package.json`, `src/patch-map.ts`, `src/patchmap.js`, `src/init.js`, `src/display/data-schema/**`, `src/display/draw.js`, `src/display/update.js`, `src/events/**`, `src/command/**`, `src/transformer/**`, `src/utils/**`. - Declared write scope: `docs/memory.md`, `docs/plans/2026-03-25-patchmap-feature-guides.md`, `docs/reference/patchmap/**`, `docs/tasks/2026/03-25/patchmap-feature-guides/**`. -- Latest validation: `find docs/reference/patchmap -maxdepth 1 -type f | sort` confirmed 6 guide files; `python3 /Users/minholim/.agents/skills/project-context/scripts/check_runtime_shape.py` returned `[OK]`. +- Latest validation: `find docs/reference/patchmap -maxdepth 1 -type f | sort` confirmed 7 guide files; `python3 /Users/minholim/.agents/skills/project-context/scripts/check_runtime_shape.py` returned `[OK]`. diff --git a/docs/tasks/2026/03-25/patchmap-feature-guides/logs/DECISIONS.md b/docs/tasks/2026/03-25/patchmap-feature-guides/logs/DECISIONS.md index 09f394a5..506d10ca 100644 --- a/docs/tasks/2026/03-25/patchmap-feature-guides/logs/DECISIONS.md +++ b/docs/tasks/2026/03-25/patchmap-feature-guides/logs/DECISIONS.md @@ -9,3 +9,9 @@ - 선택지: README 서술을 우선, 소스 구현을 우선, 차이점을 모두 병기. - 결정: 현재 문서는 README보다 소스 구현을 우선하고, 리뷰 에이전트가 잡은 구현 차이를 직접 반영한다. - 영향: `overview.md`, `interactions.md`, `history-and-transformer.md`, `data-model.md`가 현재 런타임 동작에 더 가깝게 유지된다. + +**2026-03-25** +- 배경: 기존 reference 세트가 기능 사용법은 설명했지만, 후속 개발 에이전트가 코드 수정 지점을 문서만으로 찾기에는 구현 책임과 검증 흐름 정보가 부족했다. +- 선택지: 기능 문서만 유지, 각 문서에 구현 맥락을 흩어 넣기, 개발자용 reference를 별도로 추가. +- 결정: 구현 책임 맵, 변경 유형별 읽기 시작점, 검증 명령을 모은 `developer-context.md`를 추가하고 overview/checklist/memory/task 스냅샷에 반영한다. +- 영향: 이후 세션은 code-first 탐색 없이도 reference 문서만 읽고 공개 표면, 수정 파일 후보, 검증 루틴을 빠르게 재구성할 수 있다. diff --git a/docs/tasks/2026/03-25/patchmap-feature-guides/logs/WORKLOG.md b/docs/tasks/2026/03-25/patchmap-feature-guides/logs/WORKLOG.md index 6d71eb79..688ab4bb 100644 --- a/docs/tasks/2026/03-25/patchmap-feature-guides/logs/WORKLOG.md +++ b/docs/tasks/2026/03-25/patchmap-feature-guides/logs/WORKLOG.md @@ -7,3 +7,8 @@ - `coverage-checklist.md`를 추가하고 `find docs/reference/patchmap -maxdepth 1 -type f | sort`, `python3 /Users/minholim/.agents/skills/project-context/scripts/check_runtime_shape.py`로 최종 검증했다. - reference만으로 공개 메서드 사용법을 파악 가능한지 다시 검토한 뒤, `draw()`/`update()`/`init()`/`focus()`/`fit()`/getter 표면을 한 곳에 모은 `public-api.md`를 추가했다. - `public-api.md`에는 `update()`의 `emit`, `validateSchema`, `normalize`처럼 README에 없지만 현재 구현상 노출되는 옵션까지 기록했고, `coverage-checklist.md`를 그 구조에 맞게 갱신했다. + +**2026-03-25** +- `docs/memory.md`, 기존 patchmap reference 5종, task snapshot을 읽고 현재 문서가 기능 사용법 중심이라는 점을 확인했다. +- `package.json`, `src/patch-map.ts`, `src/patchmap.js`, `src/init.js`, `src/display/draw.js`, `src/display/update.js`, `src/utils/index.js`를 대조해 개발 에이전트 관점의 누락이 구현 책임 맵과 검증 루틴이라는 점을 정리했다. +- `docs/reference/patchmap/developer-context.md`를 추가하고 `overview.md`, `coverage-checklist.md`, `docs/memory.md`, task `BRIEF.md`/`STATUS.md`를 이번 보강 내용에 맞게 다시 썼다. From abd88e2b1076a43b0d19727410e7e4ee5a039b09 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Wed, 25 Mar 2026 14:06:40 +0900 Subject: [PATCH 3/8] docs(patchmap-spec): finalize compatibility specification --- .agents/skills/speckit-analyze/SKILL.md | 190 ++++ .agents/skills/speckit-checklist/SKILL.md | 301 +++++++ .agents/skills/speckit-clarify/SKILL.md | 183 ++++ .agents/skills/speckit-constitution/SKILL.md | 86 ++ .agents/skills/speckit-implement/SKILL.md | 204 +++++ .agents/skills/speckit-plan/SKILL.md | 151 ++++ .agents/skills/speckit-specify/SKILL.md | 304 +++++++ .agents/skills/speckit-tasks/SKILL.md | 197 +++++ .agents/skills/speckit-taskstoissues/SKILL.md | 35 + .specify/init-options.json | 11 + .specify/memory/constitution.md | 50 ++ .specify/scripts/bash/check-prerequisites.sh | 190 ++++ .specify/scripts/bash/common.sh | 289 ++++++ .specify/scripts/bash/create-new-feature.sh | 353 ++++++++ .specify/scripts/bash/setup-plan.sh | 73 ++ .specify/scripts/bash/update-agent-context.sh | 837 ++++++++++++++++++ .specify/templates/agent-file-template.md | 28 + .specify/templates/checklist-template.md | 40 + .specify/templates/constitution-template.md | 50 ++ .specify/templates/plan-template.md | 104 +++ .specify/templates/spec-template.md | 128 +++ .specify/templates/tasks-template.md | 251 ++++++ docs/memory.md | 10 +- .../2026/03-25/patchmap-library-spec/BRIEF.md | 20 + .../03-25/patchmap-library-spec/STATUS.md | 6 + .../patchmap-library-spec/logs/DECISIONS.md | 5 + .../patchmap-library-spec/logs/WORKLOG.md | 9 + .../checklists/requirements.md | 35 + specs/001-patchmap-spec/spec.md | 329 +++++++ 29 files changed, 4464 insertions(+), 5 deletions(-) create mode 100644 .agents/skills/speckit-analyze/SKILL.md create mode 100644 .agents/skills/speckit-checklist/SKILL.md create mode 100644 .agents/skills/speckit-clarify/SKILL.md create mode 100644 .agents/skills/speckit-constitution/SKILL.md create mode 100644 .agents/skills/speckit-implement/SKILL.md create mode 100644 .agents/skills/speckit-plan/SKILL.md create mode 100644 .agents/skills/speckit-specify/SKILL.md create mode 100644 .agents/skills/speckit-tasks/SKILL.md create mode 100644 .agents/skills/speckit-taskstoissues/SKILL.md create mode 100644 .specify/init-options.json create mode 100644 .specify/memory/constitution.md create mode 100755 .specify/scripts/bash/check-prerequisites.sh create mode 100755 .specify/scripts/bash/common.sh create mode 100755 .specify/scripts/bash/create-new-feature.sh create mode 100755 .specify/scripts/bash/setup-plan.sh create mode 100755 .specify/scripts/bash/update-agent-context.sh create mode 100644 .specify/templates/agent-file-template.md create mode 100644 .specify/templates/checklist-template.md create mode 100644 .specify/templates/constitution-template.md create mode 100644 .specify/templates/plan-template.md create mode 100644 .specify/templates/spec-template.md create mode 100644 .specify/templates/tasks-template.md create mode 100644 docs/tasks/2026/03-25/patchmap-library-spec/BRIEF.md create mode 100644 docs/tasks/2026/03-25/patchmap-library-spec/STATUS.md create mode 100644 docs/tasks/2026/03-25/patchmap-library-spec/logs/DECISIONS.md create mode 100644 docs/tasks/2026/03-25/patchmap-library-spec/logs/WORKLOG.md create mode 100644 specs/001-patchmap-spec/checklists/requirements.md create mode 100644 specs/001-patchmap-spec/spec.md diff --git a/.agents/skills/speckit-analyze/SKILL.md b/.agents/skills/speckit-analyze/SKILL.md new file mode 100644 index 00000000..6f962ea7 --- /dev/null +++ b/.agents/skills/speckit-analyze/SKILL.md @@ -0,0 +1,190 @@ +--- +name: "speckit-analyze" +description: "Perform a non-destructive cross-artifact consistency and quality analysis across spec.md, plan.md, and tasks.md after task generation." +compatibility: "Requires spec-kit project structure with .specify/ directory" +metadata: + author: "github-spec-kit" + source: "templates/commands/analyze.md" +--- + + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Goal + +Identify inconsistencies, duplications, ambiguities, and underspecified items across the three core artifacts (`spec.md`, `plan.md`, `tasks.md`) before implementation. This command MUST run only after `/speckit.tasks` has successfully produced a complete `tasks.md`. + +## Operating Constraints + +**STRICTLY READ-ONLY**: Do **not** modify any files. Output a structured analysis report. Offer an optional remediation plan (user must explicitly approve before any follow-up editing commands would be invoked manually). + +**Constitution Authority**: The project constitution (`.specify/memory/constitution.md`) is **non-negotiable** within this analysis scope. Constitution conflicts are automatically CRITICAL and require adjustment of the spec, plan, or tasks—not dilution, reinterpretation, or silent ignoring of the principle. If a principle itself needs to change, that must occur in a separate, explicit constitution update outside `/speckit.analyze`. + +## Execution Steps + +### 1. Initialize Analysis Context + +Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` once from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS. Derive absolute paths: + +- SPEC = FEATURE_DIR/spec.md +- PLAN = FEATURE_DIR/plan.md +- TASKS = FEATURE_DIR/tasks.md + +Abort with an error message if any required file is missing (instruct the user to run missing prerequisite command). +For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). + +### 2. Load Artifacts (Progressive Disclosure) + +Load only the minimal necessary context from each artifact: + +**From spec.md:** + +- Overview/Context +- Functional Requirements +- Non-Functional Requirements +- User Stories +- Edge Cases (if present) + +**From plan.md:** + +- Architecture/stack choices +- Data Model references +- Phases +- Technical constraints + +**From tasks.md:** + +- Task IDs +- Descriptions +- Phase grouping +- Parallel markers [P] +- Referenced file paths + +**From constitution:** + +- Load `.specify/memory/constitution.md` for principle validation + +### 3. Build Semantic Models + +Create internal representations (do not include raw artifacts in output): + +- **Requirements inventory**: Each functional + non-functional requirement with a stable key (derive slug based on imperative phrase; e.g., "User can upload file" → `user-can-upload-file`) +- **User story/action inventory**: Discrete user actions with acceptance criteria +- **Task coverage mapping**: Map each task to one or more requirements or stories (inference by keyword / explicit reference patterns like IDs or key phrases) +- **Constitution rule set**: Extract principle names and MUST/SHOULD normative statements + +### 4. Detection Passes (Token-Efficient Analysis) + +Focus on high-signal findings. Limit to 50 findings total; aggregate remainder in overflow summary. + +#### A. Duplication Detection + +- Identify near-duplicate requirements +- Mark lower-quality phrasing for consolidation + +#### B. Ambiguity Detection + +- Flag vague adjectives (fast, scalable, secure, intuitive, robust) lacking measurable criteria +- Flag unresolved placeholders (TODO, TKTK, ???, ``, etc.) + +#### C. Underspecification + +- Requirements with verbs but missing object or measurable outcome +- User stories missing acceptance criteria alignment +- Tasks referencing files or components not defined in spec/plan + +#### D. Constitution Alignment + +- Any requirement or plan element conflicting with a MUST principle +- Missing mandated sections or quality gates from constitution + +#### E. Coverage Gaps + +- Requirements with zero associated tasks +- Tasks with no mapped requirement/story +- Non-functional requirements not reflected in tasks (e.g., performance, security) + +#### F. Inconsistency + +- Terminology drift (same concept named differently across files) +- Data entities referenced in plan but absent in spec (or vice versa) +- Task ordering contradictions (e.g., integration tasks before foundational setup tasks without dependency note) +- Conflicting requirements (e.g., one requires Next.js while other specifies Vue) + +### 5. Severity Assignment + +Use this heuristic to prioritize findings: + +- **CRITICAL**: Violates constitution MUST, missing core spec artifact, or requirement with zero coverage that blocks baseline functionality +- **HIGH**: Duplicate or conflicting requirement, ambiguous security/performance attribute, untestable acceptance criterion +- **MEDIUM**: Terminology drift, missing non-functional task coverage, underspecified edge case +- **LOW**: Style/wording improvements, minor redundancy not affecting execution order + +### 6. Produce Compact Analysis Report + +Output a Markdown report (no file writes) with the following structure: + +## Specification Analysis Report + +| ID | Category | Severity | Location(s) | Summary | Recommendation | +|----|----------|----------|-------------|---------|----------------| +| A1 | Duplication | HIGH | spec.md:L120-134 | Two similar requirements ... | Merge phrasing; keep clearer version | + +(Add one row per finding; generate stable IDs prefixed by category initial.) + +**Coverage Summary Table:** + +| Requirement Key | Has Task? | Task IDs | Notes | +|-----------------|-----------|----------|-------| + +**Constitution Alignment Issues:** (if any) + +**Unmapped Tasks:** (if any) + +**Metrics:** + +- Total Requirements +- Total Tasks +- Coverage % (requirements with >=1 task) +- Ambiguity Count +- Duplication Count +- Critical Issues Count + +### 7. Provide Next Actions + +At end of report, output a concise Next Actions block: + +- If CRITICAL issues exist: Recommend resolving before `/speckit.implement` +- If only LOW/MEDIUM: User may proceed, but provide improvement suggestions +- Provide explicit command suggestions: e.g., "Run /speckit.specify with refinement", "Run /speckit.plan to adjust architecture", "Manually edit tasks.md to add coverage for 'performance-metrics'" + +### 8. Offer Remediation + +Ask the user: "Would you like me to suggest concrete remediation edits for the top N issues?" (Do NOT apply them automatically.) + +## Operating Principles + +### Context Efficiency + +- **Minimal high-signal tokens**: Focus on actionable findings, not exhaustive documentation +- **Progressive disclosure**: Load artifacts incrementally; don't dump all content into analysis +- **Token-efficient output**: Limit findings table to 50 rows; summarize overflow +- **Deterministic results**: Rerunning without changes should produce consistent IDs and counts + +### Analysis Guidelines + +- **NEVER modify files** (this is read-only analysis) +- **NEVER hallucinate missing sections** (if absent, report them accurately) +- **Prioritize constitution violations** (these are always CRITICAL) +- **Use examples over exhaustive rules** (cite specific instances, not generic patterns) +- **Report zero issues gracefully** (emit success report with coverage statistics) + +## Context + +$ARGUMENTS diff --git a/.agents/skills/speckit-checklist/SKILL.md b/.agents/skills/speckit-checklist/SKILL.md new file mode 100644 index 00000000..53bc73dc --- /dev/null +++ b/.agents/skills/speckit-checklist/SKILL.md @@ -0,0 +1,301 @@ +--- +name: "speckit-checklist" +description: "Generate a custom checklist for the current feature based on user requirements." +compatibility: "Requires spec-kit project structure with .specify/ directory" +metadata: + author: "github-spec-kit" + source: "templates/commands/checklist.md" +--- + + +## Checklist Purpose: "Unit Tests for English" + +**CRITICAL CONCEPT**: Checklists are **UNIT TESTS FOR REQUIREMENTS WRITING** - they validate the quality, clarity, and completeness of requirements in a given domain. + +**NOT for verification/testing**: + +- ❌ NOT "Verify the button clicks correctly" +- ❌ NOT "Test error handling works" +- ❌ NOT "Confirm the API returns 200" +- ❌ NOT checking if code/implementation matches the spec + +**FOR requirements quality validation**: + +- ✅ "Are visual hierarchy requirements defined for all card types?" (completeness) +- ✅ "Is 'prominent display' quantified with specific sizing/positioning?" (clarity) +- ✅ "Are hover state requirements consistent across all interactive elements?" (consistency) +- ✅ "Are accessibility requirements defined for keyboard navigation?" (coverage) +- ✅ "Does the spec define what happens when logo image fails to load?" (edge cases) + +**Metaphor**: If your spec is code written in English, the checklist is its unit test suite. You're testing whether the requirements are well-written, complete, unambiguous, and ready for implementation - NOT whether the implementation works. + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Execution Steps + +1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS list. + - All file paths must be absolute. + - For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). + +2. **Clarify intent (dynamic)**: Derive up to THREE initial contextual clarifying questions (no pre-baked catalog). They MUST: + - Be generated from the user's phrasing + extracted signals from spec/plan/tasks + - Only ask about information that materially changes checklist content + - Be skipped individually if already unambiguous in `$ARGUMENTS` + - Prefer precision over breadth + + Generation algorithm: + 1. Extract signals: feature domain keywords (e.g., auth, latency, UX, API), risk indicators ("critical", "must", "compliance"), stakeholder hints ("QA", "review", "security team"), and explicit deliverables ("a11y", "rollback", "contracts"). + 2. Cluster signals into candidate focus areas (max 4) ranked by relevance. + 3. Identify probable audience & timing (author, reviewer, QA, release) if not explicit. + 4. Detect missing dimensions: scope breadth, depth/rigor, risk emphasis, exclusion boundaries, measurable acceptance criteria. + 5. Formulate questions chosen from these archetypes: + - Scope refinement (e.g., "Should this include integration touchpoints with X and Y or stay limited to local module correctness?") + - Risk prioritization (e.g., "Which of these potential risk areas should receive mandatory gating checks?") + - Depth calibration (e.g., "Is this a lightweight pre-commit sanity list or a formal release gate?") + - Audience framing (e.g., "Will this be used by the author only or peers during PR review?") + - Boundary exclusion (e.g., "Should we explicitly exclude performance tuning items this round?") + - Scenario class gap (e.g., "No recovery flows detected—are rollback / partial failure paths in scope?") + + Question formatting rules: + - If presenting options, generate a compact table with columns: Option | Candidate | Why It Matters + - Limit to A–E options maximum; omit table if a free-form answer is clearer + - Never ask the user to restate what they already said + - Avoid speculative categories (no hallucination). If uncertain, ask explicitly: "Confirm whether X belongs in scope." + + Defaults when interaction impossible: + - Depth: Standard + - Audience: Reviewer (PR) if code-related; Author otherwise + - Focus: Top 2 relevance clusters + + Output the questions (label Q1/Q2/Q3). After answers: if ≥2 scenario classes (Alternate / Exception / Recovery / Non-Functional domain) remain unclear, you MAY ask up to TWO more targeted follow‑ups (Q4/Q5) with a one-line justification each (e.g., "Unresolved recovery path risk"). Do not exceed five total questions. Skip escalation if user explicitly declines more. + +3. **Understand user request**: Combine `$ARGUMENTS` + clarifying answers: + - Derive checklist theme (e.g., security, review, deploy, ux) + - Consolidate explicit must-have items mentioned by user + - Map focus selections to category scaffolding + - Infer any missing context from spec/plan/tasks (do NOT hallucinate) + +4. **Load feature context**: Read from FEATURE_DIR: + - spec.md: Feature requirements and scope + - plan.md (if exists): Technical details, dependencies + - tasks.md (if exists): Implementation tasks + + **Context Loading Strategy**: + - Load only necessary portions relevant to active focus areas (avoid full-file dumping) + - Prefer summarizing long sections into concise scenario/requirement bullets + - Use progressive disclosure: add follow-on retrieval only if gaps detected + - If source docs are large, generate interim summary items instead of embedding raw text + +5. **Generate checklist** - Create "Unit Tests for Requirements": + - Create `FEATURE_DIR/checklists/` directory if it doesn't exist + - Generate unique checklist filename: + - Use short, descriptive name based on domain (e.g., `ux.md`, `api.md`, `security.md`) + - Format: `[domain].md` + - File handling behavior: + - If file does NOT exist: Create new file and number items starting from CHK001 + - If file exists: Append new items to existing file, continuing from the last CHK ID (e.g., if last item is CHK015, start new items at CHK016) + - Never delete or replace existing checklist content - always preserve and append + + **CORE PRINCIPLE - Test the Requirements, Not the Implementation**: + Every checklist item MUST evaluate the REQUIREMENTS THEMSELVES for: + - **Completeness**: Are all necessary requirements present? + - **Clarity**: Are requirements unambiguous and specific? + - **Consistency**: Do requirements align with each other? + - **Measurability**: Can requirements be objectively verified? + - **Coverage**: Are all scenarios/edge cases addressed? + + **Category Structure** - Group items by requirement quality dimensions: + - **Requirement Completeness** (Are all necessary requirements documented?) + - **Requirement Clarity** (Are requirements specific and unambiguous?) + - **Requirement Consistency** (Do requirements align without conflicts?) + - **Acceptance Criteria Quality** (Are success criteria measurable?) + - **Scenario Coverage** (Are all flows/cases addressed?) + - **Edge Case Coverage** (Are boundary conditions defined?) + - **Non-Functional Requirements** (Performance, Security, Accessibility, etc. - are they specified?) + - **Dependencies & Assumptions** (Are they documented and validated?) + - **Ambiguities & Conflicts** (What needs clarification?) + + **HOW TO WRITE CHECKLIST ITEMS - "Unit Tests for English"**: + + ❌ **WRONG** (Testing implementation): + - "Verify landing page displays 3 episode cards" + - "Test hover states work on desktop" + - "Confirm logo click navigates home" + + ✅ **CORRECT** (Testing requirements quality): + - "Are the exact number and layout of featured episodes specified?" [Completeness] + - "Is 'prominent display' quantified with specific sizing/positioning?" [Clarity] + - "Are hover state requirements consistent across all interactive elements?" [Consistency] + - "Are keyboard navigation requirements defined for all interactive UI?" [Coverage] + - "Is the fallback behavior specified when logo image fails to load?" [Edge Cases] + - "Are loading states defined for asynchronous episode data?" [Completeness] + - "Does the spec define visual hierarchy for competing UI elements?" [Clarity] + + **ITEM STRUCTURE**: + Each item should follow this pattern: + - Question format asking about requirement quality + - Focus on what's WRITTEN (or not written) in the spec/plan + - Include quality dimension in brackets [Completeness/Clarity/Consistency/etc.] + - Reference spec section `[Spec §X.Y]` when checking existing requirements + - Use `[Gap]` marker when checking for missing requirements + + **EXAMPLES BY QUALITY DIMENSION**: + + Completeness: + - "Are error handling requirements defined for all API failure modes? [Gap]" + - "Are accessibility requirements specified for all interactive elements? [Completeness]" + - "Are mobile breakpoint requirements defined for responsive layouts? [Gap]" + + Clarity: + - "Is 'fast loading' quantified with specific timing thresholds? [Clarity, Spec §NFR-2]" + - "Are 'related episodes' selection criteria explicitly defined? [Clarity, Spec §FR-5]" + - "Is 'prominent' defined with measurable visual properties? [Ambiguity, Spec §FR-4]" + + Consistency: + - "Do navigation requirements align across all pages? [Consistency, Spec §FR-10]" + - "Are card component requirements consistent between landing and detail pages? [Consistency]" + + Coverage: + - "Are requirements defined for zero-state scenarios (no episodes)? [Coverage, Edge Case]" + - "Are concurrent user interaction scenarios addressed? [Coverage, Gap]" + - "Are requirements specified for partial data loading failures? [Coverage, Exception Flow]" + + Measurability: + - "Are visual hierarchy requirements measurable/testable? [Acceptance Criteria, Spec §FR-1]" + - "Can 'balanced visual weight' be objectively verified? [Measurability, Spec §FR-2]" + + **Scenario Classification & Coverage** (Requirements Quality Focus): + - Check if requirements exist for: Primary, Alternate, Exception/Error, Recovery, Non-Functional scenarios + - For each scenario class, ask: "Are [scenario type] requirements complete, clear, and consistent?" + - If scenario class missing: "Are [scenario type] requirements intentionally excluded or missing? [Gap]" + - Include resilience/rollback when state mutation occurs: "Are rollback requirements defined for migration failures? [Gap]" + + **Traceability Requirements**: + - MINIMUM: ≥80% of items MUST include at least one traceability reference + - Each item should reference: spec section `[Spec §X.Y]`, or use markers: `[Gap]`, `[Ambiguity]`, `[Conflict]`, `[Assumption]` + - If no ID system exists: "Is a requirement & acceptance criteria ID scheme established? [Traceability]" + + **Surface & Resolve Issues** (Requirements Quality Problems): + Ask questions about the requirements themselves: + - Ambiguities: "Is the term 'fast' quantified with specific metrics? [Ambiguity, Spec §NFR-1]" + - Conflicts: "Do navigation requirements conflict between §FR-10 and §FR-10a? [Conflict]" + - Assumptions: "Is the assumption of 'always available podcast API' validated? [Assumption]" + - Dependencies: "Are external podcast API requirements documented? [Dependency, Gap]" + - Missing definitions: "Is 'visual hierarchy' defined with measurable criteria? [Gap]" + + **Content Consolidation**: + - Soft cap: If raw candidate items > 40, prioritize by risk/impact + - Merge near-duplicates checking the same requirement aspect + - If >5 low-impact edge cases, create one item: "Are edge cases X, Y, Z addressed in requirements? [Coverage]" + + **🚫 ABSOLUTELY PROHIBITED** - These make it an implementation test, not a requirements test: + - ❌ Any item starting with "Verify", "Test", "Confirm", "Check" + implementation behavior + - ❌ References to code execution, user actions, system behavior + - ❌ "Displays correctly", "works properly", "functions as expected" + - ❌ "Click", "navigate", "render", "load", "execute" + - ❌ Test cases, test plans, QA procedures + - ❌ Implementation details (frameworks, APIs, algorithms) + + **✅ REQUIRED PATTERNS** - These test requirements quality: + - ✅ "Are [requirement type] defined/specified/documented for [scenario]?" + - ✅ "Is [vague term] quantified/clarified with specific criteria?" + - ✅ "Are requirements consistent between [section A] and [section B]?" + - ✅ "Can [requirement] be objectively measured/verified?" + - ✅ "Are [edge cases/scenarios] addressed in requirements?" + - ✅ "Does the spec define [missing aspect]?" + +6. **Structure Reference**: Generate the checklist following the canonical template in `.specify/templates/checklist-template.md` for title, meta section, category headings, and ID formatting. If template is unavailable, use: H1 title, purpose/created meta lines, `##` category sections containing `- [ ] CHK### ` lines with globally incrementing IDs starting at CHK001. + +7. **Report**: Output full path to checklist file, item count, and summarize whether the run created a new file or appended to an existing one. Summarize: + - Focus areas selected + - Depth level + - Actor/timing + - Any explicit user-specified must-have items incorporated + +**Important**: Each `/speckit.checklist` command invocation uses a short, descriptive checklist filename and either creates a new file or appends to an existing one. This allows: + +- Multiple checklists of different types (e.g., `ux.md`, `test.md`, `security.md`) +- Simple, memorable filenames that indicate checklist purpose +- Easy identification and navigation in the `checklists/` folder + +To avoid clutter, use descriptive types and clean up obsolete checklists when done. + +## Example Checklist Types & Sample Items + +**UX Requirements Quality:** `ux.md` + +Sample items (testing the requirements, NOT the implementation): + +- "Are visual hierarchy requirements defined with measurable criteria? [Clarity, Spec §FR-1]" +- "Is the number and positioning of UI elements explicitly specified? [Completeness, Spec §FR-1]" +- "Are interaction state requirements (hover, focus, active) consistently defined? [Consistency]" +- "Are accessibility requirements specified for all interactive elements? [Coverage, Gap]" +- "Is fallback behavior defined when images fail to load? [Edge Case, Gap]" +- "Can 'prominent display' be objectively measured? [Measurability, Spec §FR-4]" + +**API Requirements Quality:** `api.md` + +Sample items: + +- "Are error response formats specified for all failure scenarios? [Completeness]" +- "Are rate limiting requirements quantified with specific thresholds? [Clarity]" +- "Are authentication requirements consistent across all endpoints? [Consistency]" +- "Are retry/timeout requirements defined for external dependencies? [Coverage, Gap]" +- "Is versioning strategy documented in requirements? [Gap]" + +**Performance Requirements Quality:** `performance.md` + +Sample items: + +- "Are performance requirements quantified with specific metrics? [Clarity]" +- "Are performance targets defined for all critical user journeys? [Coverage]" +- "Are performance requirements under different load conditions specified? [Completeness]" +- "Can performance requirements be objectively measured? [Measurability]" +- "Are degradation requirements defined for high-load scenarios? [Edge Case, Gap]" + +**Security Requirements Quality:** `security.md` + +Sample items: + +- "Are authentication requirements specified for all protected resources? [Coverage]" +- "Are data protection requirements defined for sensitive information? [Completeness]" +- "Is the threat model documented and requirements aligned to it? [Traceability]" +- "Are security requirements consistent with compliance obligations? [Consistency]" +- "Are security failure/breach response requirements defined? [Gap, Exception Flow]" + +## Anti-Examples: What NOT To Do + +**❌ WRONG - These test implementation, not requirements:** + +```markdown +- [ ] CHK001 - Verify landing page displays 3 episode cards [Spec §FR-001] +- [ ] CHK002 - Test hover states work correctly on desktop [Spec §FR-003] +- [ ] CHK003 - Confirm logo click navigates to home page [Spec §FR-010] +- [ ] CHK004 - Check that related episodes section shows 3-5 items [Spec §FR-005] +``` + +**✅ CORRECT - These test requirements quality:** + +```markdown +- [ ] CHK001 - Are the number and layout of featured episodes explicitly specified? [Completeness, Spec §FR-001] +- [ ] CHK002 - Are hover state requirements consistently defined for all interactive elements? [Consistency, Spec §FR-003] +- [ ] CHK003 - Are navigation requirements clear for all clickable brand elements? [Clarity, Spec §FR-010] +- [ ] CHK004 - Is the selection criteria for related episodes documented? [Gap, Spec §FR-005] +- [ ] CHK005 - Are loading state requirements defined for asynchronous episode data? [Gap] +- [ ] CHK006 - Can "visual hierarchy" requirements be objectively measured? [Measurability, Spec §FR-001] +``` + +**Key Differences:** + +- Wrong: Tests if the system works correctly +- Correct: Tests if the requirements are written correctly +- Wrong: Verification of behavior +- Correct: Validation of requirement quality +- Wrong: "Does it do X?" +- Correct: "Is X clearly specified?" diff --git a/.agents/skills/speckit-clarify/SKILL.md b/.agents/skills/speckit-clarify/SKILL.md new file mode 100644 index 00000000..45ad3121 --- /dev/null +++ b/.agents/skills/speckit-clarify/SKILL.md @@ -0,0 +1,183 @@ +--- +name: "speckit-clarify" +description: "Identify underspecified areas in the current feature spec by asking up to 5 highly targeted clarification questions and encoding answers back into the spec." +compatibility: "Requires spec-kit project structure with .specify/ directory" +metadata: + author: "github-spec-kit" + source: "templates/commands/clarify.md" +--- + + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Outline + +Goal: Detect and reduce ambiguity or missing decision points in the active feature specification and record the clarifications directly in the spec file. + +Note: This clarification workflow is expected to run (and be completed) BEFORE invoking `/speckit.plan`. If the user explicitly states they are skipping clarification (e.g., exploratory spike), you may proceed, but must warn that downstream rework risk increases. + +Execution steps: + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json --paths-only` from repo root **once** (combined `--json --paths-only` mode / `-Json -PathsOnly`). Parse minimal JSON payload fields: + - `FEATURE_DIR` + - `FEATURE_SPEC` + - (Optionally capture `IMPL_PLAN`, `TASKS` for future chained flows.) + - If JSON parsing fails, abort and instruct user to re-run `/speckit.specify` or verify feature branch environment. + - For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). + +2. Load the current spec file. Perform a structured ambiguity & coverage scan using this taxonomy. For each category, mark status: Clear / Partial / Missing. Produce an internal coverage map used for prioritization (do not output raw map unless no questions will be asked). + + Functional Scope & Behavior: + - Core user goals & success criteria + - Explicit out-of-scope declarations + - User roles / personas differentiation + + Domain & Data Model: + - Entities, attributes, relationships + - Identity & uniqueness rules + - Lifecycle/state transitions + - Data volume / scale assumptions + + Interaction & UX Flow: + - Critical user journeys / sequences + - Error/empty/loading states + - Accessibility or localization notes + + Non-Functional Quality Attributes: + - Performance (latency, throughput targets) + - Scalability (horizontal/vertical, limits) + - Reliability & availability (uptime, recovery expectations) + - Observability (logging, metrics, tracing signals) + - Security & privacy (authN/Z, data protection, threat assumptions) + - Compliance / regulatory constraints (if any) + + Integration & External Dependencies: + - External services/APIs and failure modes + - Data import/export formats + - Protocol/versioning assumptions + + Edge Cases & Failure Handling: + - Negative scenarios + - Rate limiting / throttling + - Conflict resolution (e.g., concurrent edits) + + Constraints & Tradeoffs: + - Technical constraints (language, storage, hosting) + - Explicit tradeoffs or rejected alternatives + + Terminology & Consistency: + - Canonical glossary terms + - Avoided synonyms / deprecated terms + + Completion Signals: + - Acceptance criteria testability + - Measurable Definition of Done style indicators + + Misc / Placeholders: + - TODO markers / unresolved decisions + - Ambiguous adjectives ("robust", "intuitive") lacking quantification + + For each category with Partial or Missing status, add a candidate question opportunity unless: + - Clarification would not materially change implementation or validation strategy + - Information is better deferred to planning phase (note internally) + +3. Generate (internally) a prioritized queue of candidate clarification questions (maximum 5). Do NOT output them all at once. Apply these constraints: + - Maximum of 5 total questions across the whole session. + - Each question must be answerable with EITHER: + - A short multiple‑choice selection (2–5 distinct, mutually exclusive options), OR + - A one-word / short‑phrase answer (explicitly constrain: "Answer in <=5 words"). + - Only include questions whose answers materially impact architecture, data modeling, task decomposition, test design, UX behavior, operational readiness, or compliance validation. + - Ensure category coverage balance: attempt to cover the highest impact unresolved categories first; avoid asking two low-impact questions when a single high-impact area (e.g., security posture) is unresolved. + - Exclude questions already answered, trivial stylistic preferences, or plan-level execution details (unless blocking correctness). + - Favor clarifications that reduce downstream rework risk or prevent misaligned acceptance tests. + - If more than 5 categories remain unresolved, select the top 5 by (Impact * Uncertainty) heuristic. + +4. Sequential questioning loop (interactive): + - Present EXACTLY ONE question at a time. + - For multiple‑choice questions: + - **Analyze all options** and determine the **most suitable option** based on: + - Best practices for the project type + - Common patterns in similar implementations + - Risk reduction (security, performance, maintainability) + - Alignment with any explicit project goals or constraints visible in the spec + - Present your **recommended option prominently** at the top with clear reasoning (1-2 sentences explaining why this is the best choice). + - Format as: `**Recommended:** Option [X] - ` + - Then render all options as a Markdown table: + + | Option | Description | + |--------|-------------| + | A |