# contacts — 파괴성 축

연락처 디렉터리. 이름·이메일·팩스 번호를 담은 표 하나이고, 시드는 **잃을 것이 있게**
만들어져 있다: 팩스 번호는 세 행 전부에 값이 있고 **그중 둘은 같은 번호를 공유한다**
(한 사무실의 공용 팩스).

## 왜 이 전시물이 있나

도메인을 하나 더 늘리려고 세운 것이 **아니다**. 갤러리가 지금까지 시험한 스키마
연산은 **`field.add` 하나**, 데이터 연산은 **`update` 하나**였다(실측). 스펙이 가진
스키마 어휘 9종 중 제거·타입 변경·이름 변경·제약은 **0건**이었다. 즉 이 패밀리의 중심
주장 — *"변경은 검토 가능하고 되돌릴 수 있다"* — 이 **가장 안전한 연산에서만** 증명돼
있었다. 여기서는 **지우는 변경**이 무대에 선다.

시드가 두 성질을 갖는 것이 설계의 본체다. 값이 없으면 필드를 지워도 잃을 것이 없고,
중복이 없으면 unique 제약이 기존 데이터를 위반하지 못한다 — 둘 중 하나라도 빠지면
파괴적 턴은 파괴할 것이 없어 **아무것도 시험하지 않으면서 초록**이 된다.

## 성공의 형태가 턴마다 다르다

이것이 이 축의 핵심이고, 게이트(`host/smoke-destructive.ts`)가 세 어법을 나눠 쓰는
이유다.

| # | 무엇을 판정하는가 | 성격 |
| --- | --- | --- |
| 1 | 시드에 지울 값이 실재하고 중복이 하나 있다 | **전제** — 사라지면 그 자체로 실패 |
| 2 | 팩스 폐기 턴이 스키마·데이터·UI **세 곳에서 함께 지운다** | ① 적용되어야 통과 |
| 3 | 롤백이 **행마다 잃은 값을 되돌린다** | ① **완주 기준** |
| 4 | 롤백 공통 게이트(설계 §3) | 모든 시나리오의 완주 조건 |
| 5 | 부재 엔티티를 지목한 제거 → 422 `AdapterRefused`, 이유가 표적을 이름으로 | ② 거부되어야 통과 |
| 6 | 부재 필드를 지목한 타입 변경 → **500 구조적 크래시** | ② **게이트가 결함을 고정** |
| 7 | 부재 엔티티 제거가 아무 일 없이 `Applied` 로 남는다 | ③ **통과가 곧 결함** |
| 8 | 기존 데이터를 위반하는 제약이 그대로 착지한다 | ③ **미선언** — 결함이 아니다 |
| 9 | 개명 run 의 산출물에서 **이름 칸 3/3 이 비었음**을 자리 선언이 잡는다 (시드는 통과) | **대조군 쌍** — 아래 |

### ① 되돌릴 수 있는 파괴 (2·3·4)

"팩스 번호를 폐기해 줘"는 **3-facet 변경**이다. 스키마에서 필드를 지우는 것만으로는
행이 값을 그대로 갖고 있으므로, 승인된 문서가 *"이 번호들은 사라진다"* 고 말하려면
데이터 패치가 그것을 함께 말해야 한다. 화면의 열도 마찬가지다 — 열 목록은 아티팩트
소유다.

**완주 기준은 "롤백이 200 을 줬는가"가 아니다.** 물어야 할 것은 *"잃은 값이 행마다
돌아왔는가"* 이고, 단언 3 이 그것을 직접 읽는다. 이 단언은 **롤백 직후의 라이브**를
읽는다 — 공통 게이트는 마지막에 재적용을 하므로 그것을 돌린 뒤 읽으면 파괴된 상태를
보게 되고, 읽기 전에 시드를 다시 밀어 넣으면 **롤백이 아니라 시드를 판정하게 된다**.
첫 판이 정확히 그렇게 쓰였고 롤백을 전혀 시험하지 않으면서 초록이었다. 반증으로
확인했다: 롤백 호출을 지우면 단언 3 이 즉시 빨개진다.

### ② 거부가 정답 (5·6) — 그리고 **두 개의 문**

부재 표적을 지목하는 턴은 **에이전트가 저작할 수 없다**. 저작 시점 표적 검사가 실재
하지 않는 엔티티·필드를 먼저 거부하기 때문이다. 그래서 게이트는 그 턴들을 **손으로
저작해** 첫 번째 문을 지나쳐 **두 번째 문(백엔드 어댑터)** 에 닿는다.

이것은 지름길이 아니라 요점이다: **같은 규칙이 이제 두 층에서 집행되고, 그중 하나만
소비 중인 게시본에 있다.** 어느 문이 무엇을 잡는지 모르면, 한 문이 열려 있어도 다른
문 덕분에 초록이 유지되어 **아무도 눈치채지 못한다.**

단언 6 은 **결함이 있어서 초록**이다. 어댑터 계약은 이 실패 모드를 명시적으로 금지
한다 — *"부재 멤버를 역참조하면 null 참조 결함이 나고, **결함은 이유가 아니다**"*.
그런데 소비 중인 게시본은 정확히 그것을 한다. 그 사실을 판정되는 자리에 적어 두는
것이 단언 6 의 일이고, 의존이 그 구멍을 닫으면 빨개진다. **실패 메시지가 뒤집는
방법을 말한다.**

> 단언 5 와 6 이 갈려 보이는 것 자체가 최근에 생긴 능력이다. 어댑터 층 거부가
> 크래시와 같은 코드로 나오던 동안에는 둘 다 500 이었고, 그때는 *"거부가 정답인 턴"*
> 을 게이트로 쓸 수 없었다.

### 단언 9 — 총량이 아니라 **자리**

`render-check` 의 텍스트 하한은 화면 **전체**를 합산한다. 그래서 표의 열 하나가 통째로
비어도 나머지 열이 하한을 넘기면 통과한다 — 위 run 의 개명 턴이 정확히 그 상태를 만들었고
(이름 칸 3/3 이 빈 칸 표기), **모든 게이트가 초록이었다**. 총량으로는 잡을 수 없으므로
전시물이 *비어서는 안 되는 자리*를 선언하고(`expectFilled`) 게이트가 그것을 읽는다 —
상호작용 기대와 같은 어법이다.

**픽스처를 손으로 쓰지 않은 것이 요점이다.** 이 판정의 대상은 아카이브된 실모델 run 의
산출물 그대로이고, 손으로 쓴 픽스처는 이 사각을 만들어 내지 못했다. 그리고 **대조군이
쌍으로 붙는다**: 시드가 통과하지 않으면 실패는 "이 판정이 무엇에나 빨간불을 켠다"는 뜻일
뿐 아무것도 구별하지 않는다. 단언은 마운트 총량이 **여전히 멀쩡함**(children≥1·text≥20)도
함께 확인한다 — 그것이 깨지면 자리 선언은 애초에 필요 없다.

### ③ 통과가 곧 결함 vs 미선언 (7·8)

둘 다 "거부 없이 통과한다"지만 **같은 것이 아니고, 그 구분이 이 축이 남기는 값**이다.

- **7 은 결함이다.** 아무 일도 하지 않은 것을 했다고 말하고 원장에 완료로 남긴다.
  제거는 대상이 없어도 결과가 같아 보여 조용한 성공이 가장 그럴듯한 자리다.
  의존이 이것을 고치면 **뒤집힌다**.
- **8 은 결함이 아니다.** 라이브러리는 데이터를 제약에 대고 검사하겠다고 **선언한 적이
  없다** — 스펙은 어댑터에 *"표현할 수 없는 것을 거부하라"* 만 요구하고, 제약 위반은
  표현 불가가 아니다. 거부는 **표현 가능성**에 대한 것이지 **안전성**에 대한 것이
  아니며, 안전망은 **되돌림**이다. 그래서 8 은 **뒤집히지 않는다.**

## 증거

데이터 facet 이 실제로 움직였다는 증거는 **화면 픽셀이 아니다** — `contacts.list`
capability 는 정적 mock 이다(규율 2: 전시물은 실백엔드를 알지 못한다). 증거는 월드
상태(`GET /stage/targets/contacts/artifacts` 의 `data`·`schema`·`fingerprints`)와
원장이다.

## 결정적 회귀

```bash
# stage-host (8891) + host 서버를 contacts 전시물로 기동한 뒤
node host/smoke-destructive.ts   # 기대: smoke-destructive: 8/8 PASS
```

또는 `node host/tools/run-gates.ts --gate smoke-destructive`.

## 실모델이 이 무대에서 한 일 (run 1건, 미완주 1/2)

정본은 [`runs/20260804-claude-opus-4-8/RUN.md`](runs/20260804-claude-opus-4-8/RUN.md).
요지 둘:

**폐기 턴은 저작되지 않았다 — 거부해서가 아니라 형태에서 떨어져서.** 모델은 지우기를
시도했고 데이터 facet 의 **패치 래퍼**를 빠뜨렸으며, 스펙에 없는 데이터 연산
(`remove`)을 발명했다. 데이터 어휘에는 *"이 필드의 값을 비운다"* 를 직접 말하는 연산이
없고 우회는 `update … set { field: null }` 하나다. 재시도는 세 번 모두 **다른** 틀린
형태를 냈다 — 수렴하지 않았다. 이것은 소비 중인 게시본의 상태이며, 떨어진 두 지점 모두
의존의 다음 버전이 이미 다루고 있다.

**개명 턴은 통과했고 화면은 비었다.** 스키마는 `fullName`, 행은 여전히 `name`, UI 는
`fullName` 을 읽는다 — 세 행의 이름 칸이 전부 `—` 가 된다. 그런데 검증·apply·드리프트
게이트·`render-check`·롤백 게이트가 **전부 초록**이다. 결함이 아니라 **선언된 대가**이고
(라이브러리는 facet 정합성을 집행하지 않는다), 파괴성 축이 그것을 날카롭게 만든다:
더하는 축에서 비정합은 빈 칸이지만 개명에서는 **보이던 것이 사라진다.**

> **그 사각은 이제 닫혔다** — 단언 9 를 보라. `render-check` 가 *비어서는 안 되는
> 자리*(`expectFilled`)를 받고, 그 판정의 픽스처는 **이 run 의 산출물 그대로**다.

## 이 전시물이 아직 시험하지 않는 것

- **부재 표적을 지목한 개명·제약 제거.** 어휘는 더 있고 이 게이트는 그중 넷을 태운다.
  나머지는 같은 문을 지나므로 값이 낮다고 보고 넣지 않았다 — 넣을 이유가 생기면
  단언 5~7 과 같은 어법을 그대로 쓸 것.
- ~~**열이 통째로 비는 것**~~ — 단언 9 가 닫았다. 남은 것은 **다른 전시물의 자리
  선언**이다: `expectFilled` 는 전시물이 선언해야 하고 지금 선언한 것은 이 전시물뿐이다.
- **의존을 범프한 뒤의 재측정.** 폐기 턴이 떨어진 두 지점은 의존의 다음 버전이 이미
  다루고 있으므로, 범프 후 같은 spec 을 같은 저울로 다시 돌리는 것이 완료 판정이다.
