# Pygmalion

[English](./README.md) | [한국어](./README.ko.md)

Pygmalion은 React 애플리케이션을 위한 코드 기반 비주얼 에디터입니다. 실제
DOM과 등록된 React 컴포넌트를 캔버스에 렌더링하고 공통 컴포넌트 인스턴스를
동기화합니다. 화면에서 한 편집은 검토 가능한 소스 코드 변경으로 이어집니다.

> **왜 Pygmalion인가요?** 조각가가 만든 작품이 생명을 얻은 피그말리온
> 신화에서 이름을 가져왔습니다.
>
> **디자인을 이루는 것은 실제 React 코드입니다.**

## 주요 기능

- 등록된 화면, 컴포넌트, 에셋, 디자인 토큰을 탐색합니다.
- 사이드바나 캔버스에서 많은 화면을 빠르게 이동합니다.
- 레이아웃, 스타일, 텍스트, props, 지원하는 컴포넌트 구조를 편집합니다.
- 공통 소스 수정과 단일 화면 오버라이드 중 편집 범위를 선택합니다.
- 공통 요소를 수정하면 영향을 받는 모든 프레임에 즉시 반영합니다.
- 변경을 적용하기 전에 소스 diff와 영향받는 화면을 확인합니다.
- 기준 화면과 변경 화면을 DOM 및 픽셀 단위로 비교합니다.
- 캔버스 편집을 실행 취소하거나 다시 실행합니다. 적용하지 않은 변경은 한 번에
  초기화합니다.

## 설치

Pygmalion은 공개 npm 패키지인 `@pygmalionjs/pygmalion`으로 배포합니다.
별도의 레지스트리 설정이나 인증 토큰은 필요하지 않습니다.

```bash
npm install --save-dev @pygmalionjs/pygmalion
```

## 호스트 프로젝트 설정

호스트 애플리케이션에 `pygmalion.config.ts`를 만드세요.

```ts
import path from "node:path";
import { definePygmalionProject } from "@pygmalionjs/pygmalion/vite";

const appRoot = import.meta.dirname;
const projectRoot = path.resolve(appRoot, "..");

export default definePygmalionProject({
  configRoot: appRoot,
  projectRoot,
  appRoot,
  appDirectory: "frontend",
  sourceDirectory: "src",
  source: { remote: "origin", branch: "dev" },
  mirrorRoot: path.resolve(projectRoot, "..", "app-dev-view"),
  inventory: {
    script: path.join(appRoot, "scripts", "generate-pygmalion-inventory.mjs"),
    outputRoot: appRoot,
    outputs: [
      "src/generated/design-registry.ts",
      "src/generated/design-inventory.ts",
    ],
    runtimeInputs: [".env.local"],
  },
  inspect: {
    normalizeValue(property, value) {
      return value;
    },
  },
});
```

`inventory.outputs`에는 애플리케이션 루트 기준으로 생성기가 소유하는 경로만
등록합니다. Pygmalion은 전용 미러의 리비전을 전환하기 전에 이 경로를 복구하고
생성이 끝난 뒤 다시 확인합니다. 선언한 출력 밖에서 변경이 발견되면 동기화를
중단합니다. Git은 무시된 파일을 보고하지 않습니다. 그래서 생성기가 건드릴 수
있는 무시된 런타임 파일이나 설정 파일은 `inventory.runtimeInputs`에 적어
두어야 합니다. 각 항목은 애플리케이션 루트 기준 파일 하나입니다. 최대 64개
파일, 합계 16 MiB까지 허용합니다. Pygmalion은 생성 전후로 이 파일들의 내용을
직접 비교합니다. `.git`, `node_modules` 같은 무관한 디렉터리는 재귀적으로 훑지
않습니다.

호스트의 Vite 설정에 통합 플러그인을 추가하세요.

```ts
import { createPygmalionVitePlugins } from "@pygmalionjs/pygmalion/vite";
import pygmalionConfig from "./pygmalion.config";

export default defineConfig({
  plugins: [react(), ...createPygmalionVitePlugins(pygmalionConfig)],
});
```

JavaScript 전용 파이프라인으로 webpack 로더를 노출하는 프레임워크에서는 inspect
로더에 소스 계측 뒤 TypeScript 제거를 요청합니다. Turbopack 규칙에서는
`transpile: true`를 켜고 원래 파일 확장자는 그대로 두세요. 그래야 상대 경로
TSX 모듈 해석이 호스트의 소스 식별자를 유지합니다.

```ts
{
  './src/**/*.tsx': {
    loaders: [{
      loader: require.resolve('@pygmalionjs/pygmalion/webpack-loader'),
      options: { root: process.cwd(), sourceDirectory: 'src', transpile: true },
    }],
  },
}
```

미러 프리뷰가 호스트의 Vite 설정을 다시 불러올 때
`PYGMALION_PREVIEW_MODE=1`을 사용하면 통합 플러그인이 재귀적으로 실행되지
않습니다.

미러 프리뷰와 세션 프리뷰는 각자 별도의 Vite 프로세스로 실행됩니다. 그래서
에디터에 넘긴 mode는 그쪽까지 전달되지 않습니다. 특정 env 파일에서만 존재하는
화면이 있다면 `preview.mode`를 지정하세요. 꺼져 있어야 하는 기능 플래그나
레시피가 의존하는 목 프로필이 여기에 해당합니다.

```ts
export default definePygmalionProject({
  // ...
  preview: { mode: "e2e" },
});
```

## 미러가 렌더링할 대상 바꾸기

미러는 `source.ref`로 고정하지 않는 한 `source.branch`를 따라갑니다. 둘 다
런타임에 다시 가리킬 수 있습니다. 두 리비전을 비교하려고 dev 서버를 재시작할
필요가 없습니다.

```ts
const { switchSource, mirror } = usePygmalionProject();

await switchSource("origin/dev");
await switchSource(PYGMALION_WORKTREE_SOURCE_REF); // 커밋하지 않은 작업
mirror.sourceRef; // 지금 따라가고 있는 대상
```

`PYGMALION_WORKTREE_SOURCE_REF`는 현재 작업 트리를 일회용 커밋으로 풀어서
렌더링합니다. 브랜치, HEAD, 인덱스는 건드리지 않습니다. 추적 중인 변경만
포함합니다. 추적되지 않은 파일이 있으면 반환된 상태에 경고를 남깁니다.

### 화면 선언을 선택한 리비전에 묶기

에디터 호스트는 보통 한 번 빌드되지만 미러는 여러 체크아웃 사이를 오갑니다.
라우팅된 페이지를 호스트에 번들하면 새 프리뷰가 옛 화면 카탈로그 아래에서
실행되는 일이 생기죠. 미러 체크아웃마다 JSON 카탈로그를 생성하고 exact-source
엔드포인트가 읽기 리스 아래에서 그 카탈로그를 제공하게 하세요.

```ts
export default definePygmalionProject({
  // ...
  inventory: {
    script: './scripts/generate-pygmalion-inventory.mjs',
    outputs: ['artifacts/pygmalion-revision-catalog.json'],
  },
  preview: {
    catalog: { file: 'artifacts/pygmalion-revision-catalog.json' },
  },
});
```

기본 `inventory.script` 호출에서 Pygmalion은
`--source-root <path> --out-root <path> --commit <sha>`를 넘깁니다. 생성기의
CLI 형태가 다르면 `inventory.args(context)`를 직접 정의하세요. 이때도 같은
`context.commit`을 받습니다.

생성된 파일은 `PygmalionRevisionCatalog`로 export되는 버전 있는 JSON 전용
계약을 따릅니다. 인벤토리 생성기의 `commit` 컨텍스트로 `sourceRevision`을
기록하세요. 라우팅된 `screens.pages`와 `screens.assets`, `tokens`, 그리고
리비전마다 달라질 수 있는 모든 화면 선언을 포함합니다. 직렬화 가능한
레지스트리 메타데이터는 `componentRegistry`에 넣습니다. 에디터는 이
메타데이터를 이름으로 호스트가 이미 번들한 React 구현에 연결합니다. 구현이 없는
카탈로그는 거부합니다. 이렇게 하면 JSON에 함수를 넣지 않고도 props, 기본값,
채택 정보, 코드 생성 메타데이터가 리비전에 묶입니다. 저장소가 소유하는 생성
코드 의도는 `codegenProject`에 두세요. 그러면 프레임 계약과 프롬프트 출처가 같은
카탈로그와 함께 전환됩니다. 그런 다음 에디터의 transport에서 정확한 프리뷰
서비스를 켭니다.

```tsx
// Once, for the editor's lifetime. The catalog endpoint derives from the
// artifact endpoint; pass `previewCatalog: null` to run artifacts alone.
const transport = createViteTransport({ endpoints: { previewArtifact: true } });
const runtime = createEditorRuntime({ transport });

function Editor() {
  const { appOrigin, previewRevision } = usePygmalionProject({ transport });
  return (
    <PygmalionEditor
      runtime={runtime}
      previewCacheNamespace="my-product"
      previewRevision={previewRevision}
      appOrigin={appOrigin ?? undefined}
    />
  );
}
```

에디터와 프로젝트 훅이 보내는 모든 요청은 이 transport를 거칩니다. 둘이 같은
서비스와 대화하는 셈이죠. 연산과 경로와 오류 목록은
[The editor transport](./docs/transport.md)에 정리했습니다. 훅이 돌려주는
`previewRevision`을 전체 그대로 넘기세요. 에디터는 카탈로그 요청에 쓸 소스
SHA를 여기서 뽑아냅니다. 화면 캐시 무효화에 쓰는 세션 변경 카운터는 그대로
유지합니다.

에디터는 설치 전에 네임스페이스와 소스 리비전을 모두 검증합니다. 다른
체크아웃이 준비되는 동안에는 마지막으로 완성된 카탈로그, 프리뷰 origin, 상호작용
선언을 한 묶음으로 유지합니다. dev → qa → dev 순서로 요청했을 때 앞선 요청의
늦은 응답이 현재 선택을 덮어쓰지 못합니다. 잘못된 카탈로그, 영구히 사용할 수
없는 카탈로그, 설치 가능한 페이지가 하나도 없는 카탈로그는 전용 오류 화면을
띄우고 설치를 중단합니다. 이전 캔버스를 요청한 리비전인 것처럼 표시하는 일은
없습니다. 라우팅된 화면이 없는 카탈로그라도 호스트가 화면이 아닌 라이브러리
페이지를 제공하면 설치됩니다.

`onDesignChange`가 연결되어 있으면 각 payload에 편집 배치를 소유한 정확한
`sourceRevision`이 실립니다. 열거되지 않는(non-enumerable) `signal` 속성은
해당 카탈로그가 퇴역할 때 중단됩니다. 프로세스 안에서 실행되는 호스트 어댑터는
기존 JSON 요청 본문을 바꾸지 않고도 이 신호로 작업을 취소합니다.

ref 문자열을 물어보는 대신 선택지를 보여 주고 싶다면 리비전 목록을 가져와
제공되는 컨트롤을 툴바에 렌더링하세요.

```tsx
const { sourceRefs, switchSource, mirror } = usePygmalionProject();

<SourceRefControl
  value={mirror.sourceRef}
  currentCommit={mirror.shortCommit}
  refs={sourceRefs.refs}
  defaultRef={sourceRefs.defaultRef}
  worktreeRef={sourceRefs.worktreeRef}
  busy={mirror.state === 'syncing'}
  refreshLabel="Refresh this branch"
  onChange={(ref) => void switchSource(ref)}
>
  {/* 선택: 툴바의 일부처럼 보이도록 직접 만든 트리거 */}
  <span>{mirror.sourceRef ?? 'dev'}@{mirror.shortCommit}</span>
</SourceRefControl>;
```

메뉴는 그 트리거 위에 겹쳐진 네이티브 select입니다. 키보드 접근성, 모바일 동작,
배치는 플랫폼이 맡습니다. 그려진 트리거는 호스트 코드가 그대로 소유합니다. 첫
항목은 선택한 리비전을 명시적으로 다시 불러옵니다. 같은 브랜치를 선택한 채로
브랜치의 최신 커밋이 바뀐 경우도 포함됩니다. `currentCommit`을 넘기면 목록에 더
새로운 ref가 있을 때 컨트롤에 `[data-update-available]`과
`[data-latest-commit]`이 표시됩니다. 스타일링에는 `[data-pygmalion-source-ref]`와
`[data-busy]`도 쓸 수 있습니다.

이 목록은 `GET /__pygmalion-dev-control/refs`가 뒷받침합니다. 로컬 브랜치, 원격
추적 브랜치, 기본 리비전, 워크트리 ref를 돌려주죠. 브랜치가 바뀐 뒤에는
`loadSourceRefs()`로 다시 불러옵니다.

프리뷰 산출물은 소스 SHA별로 캐시됩니다. 전환하면 그 캐시는 무효화되고 다음
캡처는 처음부터 다시 실행됩니다. 이전 리비전을 위해 이미 실행 중이던 캡처는
발행되지 않고 버려집니다. 그래서 에디터가 옮겨 간 산출물을 덮어쓰지 못합니다.

디자인 세션의 워크트리는 소스 SHA 하나에서 분기됩니다. 적용하지 않은 편집이
남아 있으면 `switchSource`는 거부합니다. 그 편집이 작성된 적 없는 리비전 위에
캔버스를 두지 않기 위해서입니다. 먼저 적용하거나 되돌리세요. 아니면
`{ discardEdits: true }`를 넘기면 됩니다.

## 지원하는 폭에서 프레임 확인하기

프레임은 모서리를 끌거나 W/H 입력으로 크기를 바꿉니다. 여기에 애플리케이션이
실제로 보장하는 폭에 이름을 붙여 두면 그 입력 옆에 버튼으로 놓입니다. 숫자를
외우지 않아도 폭 사이를 오갈 수 있습니다.

```tsx
<PygmalionEditor
  viewportPresets={[
    { label: '480', width: 480, height: 800, description: '지원 최소 폭' },
    { label: '기본', width: 1280, height: 800 },
  ]}
/>
```

`height`를 빼면 폭만 바꾸고 높이는 그대로 둡니다. 프레임이 그 크기에 있는 동안
버튼은 눌린 상태로 보입니다. **Reset to default size**는 여전히 매니페스트가
선언한 뷰포트로 되돌립니다.

첫 번째로 선언된 캔버스가 큰 에셋 카탈로그라면 작업 캔버스를 바로 여세요. 시작
시점에 그 카탈로그를 렌더링하지 않습니다.

```tsx
<PygmalionEditor initialCanvas="Screens" />
```

### 멈추지 않는 화면

캡처는 문서가 가만히 멈추길 기다립니다. 경과 시간 시계, 레벨 미터, 마키가 있는
화면은 절대 멈추지 않으므로 아무리 기다려도 `stabilize` 단계에서 실패하죠. 그런
영역을 선언해 두면 그 안의 변화는 더 이상 변경으로 집계되지 않습니다.

```ts
captureStoryboardCase({
  // ...
  stability: {
    volatileSelectors: ['[data-testid="elapsed-time"]', '[data-live-meter]'],
  },
});
```

대기는 100ms마다 표본을 뽑아 같은 표본이 세 번 연속 나오면 끝납니다. 상한은
4초입니다. 1초 간격으로 다시 그려지는 화면에도 충분한 폭입니다. 그보다 느린
화면은 `attempts`, `requiredStableSamples`, `intervalMs`, `minimumWaitMs`로
조정합니다.

## 화면과 시나리오 등록

애플리케이션별 라우트, fixture, 인증, 목 데이터, 화면 이름은 호스트가
관리합니다. Pygmalion은 프리뷰 스케줄링, DOM 캡처, 편집, 소스 세션, QA를
관리합니다.

`createDesignScreenCollection`을 사용해 발견한 모든 시나리오를 다음 중 하나로
분류하세요.

- 라우트나 등록된 컴포넌트 fixture가 있는 시각 화면
- 명시적인 사유가 있는 비시각 동작

하나의 시나리오는 반드시 한 분류에만 속해야 합니다. 잘못된 참조, 누락된
시나리오, 중복 분류가 있으면 에디터를 열기 전에 검증이 실패합니다.

모든 소스 라우트가 표현되었는지는 별개의 질문이며 에디터 없이 답합니다.
`auditRouteCoverage`는 호스트가 발견한 라우트를 호스트가 선언한 화면(구체적인
프리뷰, 명시적인 `coverageRoutes` 매핑, 등록된 컴포넌트 fixture)과 대조합니다.
`validateSurfaceDemands`는 나머지 라우트에 내린 판정이 실제로 선언된 대상을
가리키는지 확인합니다. 요약 함수는 제외 항목과 todo 항목을 커버리지와 따로
셉니다. 식별자, 진단, 호스트에 남는 책임은
[Coverage contracts](./docs/coverage-contracts.md)가 정의합니다.

화면 레시피는 `click`, `focus`, `hover`, `focus-visible`, `active`, `fill`,
`check`, `press`, `scroll`, `wait`, `storage` 단계를 지원합니다. 각 프레임이 항상
같은 애플리케이션 상태를 재현하도록 결정적인 레시피와 assertion을 사용하세요.
어떤 변형이 프레임인지, 상호작용 상태인지, 조건 상태인지, 편집 가능한
매개변수인지, 모션 프리뷰인지, 동작 전용 QA인지는
[화면 상태 계약](./docs/screen-state-contract.md)이 정의합니다. 렌더링된 CSS와
Web Animations 대상은 캡처를 위해 자동으로 찾아 정착시킵니다. 라이브 프레임은
제품의 모션을 그대로 재생합니다.

### 제품 라우트와 프리뷰 진입 라우트 분리하기

인증과 fixture 게이트웨이는 호스트 애플리케이션이 소유합니다. 화면에 게이트웨이가
필요하면 `route`는 정식 제품 주소로 두고 라우팅된 게이트웨이는 `previewRoute`로
선언하세요.

```ts
{
  id: 'screen:account-security',
  name: 'Account security',
  section: 'Account',
  route: '/account/security',
  previewRoute: '/__preview__/account/security',
}
```

Pygmalion은 `previewRoute`로 부팅, 캡처, 캐시, 상호작용을 수행합니다. 인벤토리,
커버리지, 소스 식별자에는 계속 `route`를 사용합니다. 게이트웨이는 개발 전용이어야
합니다. 자격 증명을 URL이나 클라이언트 매니페스트에 넣지 마세요. 별도로 조립한
컴포넌트 fixture 대신 실제 라우팅된 화면을 렌더링하거나 그쪽으로 리디렉션해야
합니다. 게이트웨이를 바꾸면 그 프레임의 프리뷰 캐시는 자동으로 무효화됩니다.

선언한 `screenFlows`도 같은 분리를 지원합니다. 정식 시작 주소는 `route`에, 호스트
게이트웨이는 `previewRoute`에 두세요. 플로는 게이트웨이로 부팅하되 캡처 준비에는
정식 라우트를 보고합니다.

소스 내비게이션 추론에만 의존하지 말고 사용자 여정을 명시적으로 유지하세요. 경로는
이웃한 그래프 간선으로 확장됩니다. 소스에서 발견된 간선은 그 아래에 계속
병합됩니다.

```tsx
const storyboard = {
  startScreenIds: ['screen:entry'],
  paths: [
    {
      id: 'checkout',
      label: 'Checkout',
      screenIds: ['screen:entry', 'screen:cart', 'screen:confirmation'],
    },
  ],
};

<PygmalionEditor storyboard={storyboard} />
```

모든 id는 해석된 화면 카탈로그와 대조합니다. 알 수 없는 끝점과 도달할 수 없는
화면은 사이드바에서 사라지는 대신 그래프 진단으로 남습니다. 코드 전용
리디렉션이나 fixture는 `createStoryboardRouteScenarioId(route)`로 만든 id를
`nonVisualScenarioIds`에 넣어 분류합니다.

에디터 셸과 프레임 배치는 스토리보드 탐색이 끝나기 전에 마운트됩니다. 캐시된
라우트 콘텐츠와 라이브 라우트 콘텐츠는 기준 환경과 소스 다이제스트가 확정될
때까지 막혀 있습니다. 그래서 먼저 그려진 캔버스가 임시 프리뷰 식별자를 쓰는 일은
없습니다. 이미 보이는 프레임을 사이드바에서 선택하면 카메라는 그대로입니다. 화면
밖에 있는 프레임은 최소한의 이동으로 보이게 하고 사용자의 줌은 바꾸지 않습니다.

## 캡처 산출물 생명주기

재사용 가능한 캡처 기능은 `@pygmalionjs/pygmalion/storyboard`에서 가져옵니다.
Pygmalion은 격리된 브라우저 컨텍스트, 제한된 병렬 스케줄링, 비활성 DOM 스냅샷,
스크린샷, QA 진단, 압축된 버전 산출물, 정확한 리비전 검증, 원자적 산출물 발행을
관리합니다. Vite 통합은 현재 에디터의 네임스페이스와 소스 리비전에 정확히
일치하는 산출물만 제공합니다.

호스트 애플리케이션은 라우트, 환경 preset, 목 어댑터, 상호작용 레시피,
assertion을 제공합니다. 도메인별 네트워크 fixture는 호스트 hook에서 설치할 수
있지만 브라우저나 산출물 생명주기를 별도로 구현하면 안 됩니다.

산출물 v3는 등록한 모든 프레임을 `ready`, `rendered-with-qa-failure`,
`capture-error` 중 하나의 명시적 결과로 보존합니다. QA 실패는 CI를 실패 상태로
유지하면서 마지막으로 안정화된 DOM과 스크린샷을 남깁니다. 따라서 엄격 캡처가
실패해도 이전 산출물이 조용히 계속 활성화되지 않습니다.

transport에 프리뷰 산출물 엔드포인트가 설정되어 있으면 네임스페이스나 소스
리비전이 다른 내장 산출물은 눈에 띄게 오래된 마지막 프리뷰로만 취급됩니다. 이 산출물은
`data-preview-warm`에 포함되지 않습니다. 각 프레임은 정확한 해석의 생명주기를
`data-artifact-resolution`으로 드러냅니다. 값은 `checking`, `outdated`,
`generating`, `error`, `exact` 중 하나입니다. 전송, 검증, 생성 실패는 프레임에
재시도 동작과 함께 남습니다. 최신 라이브 스냅샷이나 정확한 엔드포인트 응답이
오면 오래된 상태가 해소됩니다.

## 편집 흐름

**Icons**, **Atoms**, **Components**에서 항목을 선택하면 해당 카탈로그
프레임으로 이동해 소스 구현 레이어를 자동으로 선택합니다. 행 오른쪽의 `+`는
삽입 전용이므로 에셋을 열기만 해서는 다른 프레임을 변경하지 않습니다.

1. **Screens**를 엽니다. 프레임을 클릭하지 않아도 프리뷰 준비를 시작합니다.
2. 사이드바에서 프레임을 선택해 캔버스 중앙으로 이동합니다.
3. DOM 레이어나 등록된 컴포넌트를 선택합니다.
4. 편집 범위로 **Shared component** 또는 **This screen**을 선택합니다.
5. 레이아웃, 콘텐츠, props, 지원하는 구조를 변경합니다.
6. 영향받는 프레임 목록과 생성된 소스 diff를 검토합니다.
7. 시각 QA를 실행한 뒤 격리된 디자인 세션에 변경을 적용합니다.
8. **Reset changes**로 적용하지 않은 편집과 에디터 기록을 모두 버립니다.

공통 요소를 편집하면 세부 렌더링 중인 활성 프레임과 경량 렌더링 중인 비활성
프레임이 같은 렌더 주기 안에서 갱신됩니다. 공통 변경을 확인하기 위해 프레임을
먼저 활성화할 필요가 없습니다.

## 로컬 개발

```bash
npm install
npm run dev
```

검증 명령:

```bash
npm run lint
npm test
npm run typecheck
npm run build:lib
npm run lint:language
```

## 프로젝트 문서

- [Architecture](./ARCHITECTURE.md): 패키지와 호스트의 경계를 정의합니다.
- [Design compiler and component connections](./docs/design-compiler.md): 버전
  있는 IR, 대상 어댑터, 진단, 외부 디자인 매핑을 정의합니다.
- [Screen state contract](./docs/screen-state-contract.md): 프레임과 같은
  프레임 안의 배치를 구분합니다. 유지된 pseudo 상태 캡처도 여기서 정의합니다.
- [Editor usability](./EDITOR-USABILITY.md): 에디터 상호작용의 지원 범위를
  정의합니다.
- [Product readiness](./PRODUCTIZATION.md): 품질 기준과 남은 작업을 정의합니다.
- [The editor transport](./docs/transport.md): 모든 요청이 거치는 하나의
  객체를 정의합니다. 엔드포인트 표와 오류 모델도 여기에 있습니다.
- [Coverage contracts](./docs/coverage-contracts.md): 호스트의 소스 라우트
  게이트를 정의합니다. 라우트를 식별하는 기준과 프리뷰·명시적 매핑·fixture로
  표현하는 방법, 시나리오를 정확히 한 번 분류하는 규칙, 진단을 소스로 연결하는
  방식이 여기에 있습니다.
- [Upgrading to 0.7](./docs/migration-0.7.md): 사라진 모듈 수준 export를
  에디터 런타임으로, 사라진 엔드포인트 prop을 transport로 대응시킵니다.
- [Upgrading to 0.8](./docs/migration-0.8.md): 런타임으로 옮겨간 것들, 커버리지
  계약, 라이브 캔버스, 그리고 Next.js 호스트를 위한 0.6 → 0.8 체크리스트를
  정리합니다.
- [Contributing](./CONTRIBUTING.md): 필수 커밋 형식과 릴리스 절차를 정의합니다.
- [Changelog](./CHANGELOG.md): 발행된 각 버전이 무엇을 바꿨는지 기록합니다.

## 라이선스

Pygmalion은 [Apache License 2.0](./LICENSE)으로 배포합니다.
