diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index a81404a..f578bc3 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,7 +6,7 @@ "name": "lodado", "url": "https://github.com/lodado" }, - "version": "0.17.6", + "version": "0.18.4", "plugins": [ { "name": "vibe-coding-helper", @@ -23,7 +23,7 @@ { "name": "frontend-oracle-design", "description": "Medium/high-risk Oracle-driven frontend contracts, TDD, evidence gates, and review.", - "version": "0.17.6", + "version": "0.18.4", "author": { "name": "lodado" }, diff --git a/packages/frontend-oracle-design/.claude-plugin/plugin.json b/packages/frontend-oracle-design/.claude-plugin/plugin.json index 1d2607a..991a831 100644 --- a/packages/frontend-oracle-design/.claude-plugin/plugin.json +++ b/packages/frontend-oracle-design/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "frontend-oracle-design", - "version": "0.17.6", + "version": "0.18.4", "description": "Medium/high-risk Oracle-driven frontend contracts, TDD, evidence gates, and review.", "author": { "name": "lodado", diff --git a/packages/frontend-oracle-design/.codex-plugin/plugin.json b/packages/frontend-oracle-design/.codex-plugin/plugin.json index a58f863..5a06987 100644 --- a/packages/frontend-oracle-design/.codex-plugin/plugin.json +++ b/packages/frontend-oracle-design/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "frontend-oracle-design", - "version": "0.17.6", + "version": "0.18.4", "description": "Medium/high-risk Oracle-driven frontend contracts, TDD, evidence gates, and review.", "author": { "name": "lodado", diff --git a/packages/frontend-oracle-design/EVALUATION.md b/packages/frontend-oracle-design/EVALUATION.md index 4e57634..477051d 100644 --- a/packages/frontend-oracle-design/EVALUATION.md +++ b/packages/frontend-oracle-design/EVALUATION.md @@ -24,10 +24,10 @@ 코드를 함께 읽었다. - skills/SKILL.md [L1] -- skills/references/oracle-card.md [L2] +- skills/references/card/ (분할: policy-sources·risk-grill·card-format·confirmation-lock) [L2] - skills/references/bva.md [L3] - skills/references/visual-design.md [L4] -- skills/references/implementation-loop.md [L5] +- skills/references/delivery/ (분할: ledger·red·implementation-decision·green-review) [L5] - skills/references/frontend-implementation.md [L6] - skills/references/architecture-contract.md [L7] - skills/references/fsd.md, backend.md, @@ -783,10 +783,10 @@ Nielsen/Norman식 사용자 학습, 최신 agent harness의 환경 검증, Arena ### 로컬 구현 [L1]: ./skills/SKILL.md 'frontend-oracle-design SKILL' -[L2]: ./skills/references/oracle-card.md 'Oracle Card' +[L2]: ./skills/references/card/policy-sources.md 'Oracle Card (card/*)' [L3]: ./skills/references/bva.md 'Boundary Value Analysis' [L4]: ./skills/references/visual-design.md 'Visual Design Contract' -[L5]: ./skills/references/implementation-loop.md 'Implementation Loop' +[L5]: ./skills/references/delivery/ledger.md 'Implementation Loop (delivery/*)' [L6]: ./skills/references/frontend-implementation.md 'Frontend Implementation' [L7]: ./skills/references/architecture-contract.md 'Architecture Contract' [L8]: ./skills/references/fsd.md 'FSD Guidance' diff --git a/packages/frontend-oracle-design/package.json b/packages/frontend-oracle-design/package.json index c06be81..bafb5f6 100644 --- a/packages/frontend-oracle-design/package.json +++ b/packages/frontend-oracle-design/package.json @@ -1,6 +1,6 @@ { "name": "@lodado/frontend-oracle-design-plugin", - "version": "0.17.6", + "version": "0.18.4", "description": "Claude Code / Codex plugin for medium/high-risk Oracle-driven frontend delivery.", "private": true, "scripts": { diff --git a/packages/frontend-oracle-design/skills/SKILL.md b/packages/frontend-oracle-design/skills/SKILL.md index 2e12d76..005e741 100644 --- a/packages/frontend-oracle-design/skills/SKILL.md +++ b/packages/frontend-oracle-design/skills/SKILL.md @@ -18,9 +18,10 @@ description: Use when the user explicitly requests an Oracle contract or graph-o 미결이면 구현에 맞추지 말고 `NEEDS_DECISION`으로 멈춘다. - 일반 architecture나 FSD(Feature-Sliced Design) 폴더 조언만 필요한 요청에는 이 스킬을 단독으로 자동 호출하지 않는다. -- 진입 시 risk부터 짧게 판정한다. **Low fast path는 reference를 로드하지 않고** - 카드·lock·run artifact 없이 기존 레포 검증만 수행한다. 명시적 Oracle 요청 또는 - Medium/High만 카드 절차로 들어간다. +- 진입 시 risk부터 짧게 판정해 lane을 라우팅한다. **Low fast path는 + [`lanes/low-fast-path.md`](references/lanes/low-fast-path.md) 하나만 로드하고 다른 + reference 노드는 로드하지 않으며**, 카드·lock·run artifact 없이 기존 레포 검증만 + 수행한다. 명시적 Oracle 요청 또는 Medium/High만 카드 절차로 들어간다. ## 불변 규칙 @@ -87,7 +88,7 @@ description: Use when the user explicitly requests an Oracle contract or graph-o - 카드에 async·순서·중복 제출·다단계 상태 행이 있거나 client state·exported Props· shared/package API·trust boundary 타입 형태를 만들거나 바꾸면 구현 전에 - `references/type-constraints.md`를 읽는다. 상태·이벤트·전이표는 카드 `O*` 행에서 + `references/types/state-ladder.md`를 읽는다. 상태·이벤트·전이표는 카드 `O*` 행에서 도출하고 그 문서의 상태 설계 사다리를 따른다. - 기존 query·router·form이 상태를 소유하면 새 `status` union을 만들지 않는다. discriminated union은 기존 소유자가 표현 못 하는 진짜 client state에만. 카드에 없는 @@ -123,8 +124,10 @@ description: Use when the user explicitly requests an Oracle contract or graph-o ## 위험도와 두 개의 Lane - **Low fast path:** 새 정책·카드·architecture 결정 없는, 기존 승인 계약 안의 되돌리기 - 쉬운 copy·token·고립 CSS·명확한 회귀 수정. 스킬 reference·Oracle artifact 없이 관련 - 테스트와 레포 필수 검증만 수행. + 쉬운 copy·token·고립 CSS·명확한 회귀 수정. 진입 조건·절차·승격(실격) 규칙은 + [`lanes/low-fast-path.md`](references/lanes/low-fast-path.md) lane 노드가 소유한다 — + Oracle artifact 없이 관련 테스트와 레포 필수 검증만 수행하고, 작업 중 정책 질문이 + 생기면 즉시 실격·승격한다. - **Medium:** 새 상태·form·responsive 구조 등 계약이 필요한 변경. Oracle + `VALID_RED` + 필수 GREEN run + 단일 독립 리뷰. - **High:** 결제·권한·파괴적 작업·데이터 손실·복잡한 concurrency. full Oracle + 다중 @@ -133,21 +136,32 @@ description: Use when the user explicitly requests an Oracle contract or graph-o 정책도 baseline도 아니다. - **Delivery Lane:** 사용자가 확인한 한 revision만 lock하고 TDD와 리뷰 수행. -## Reference 로딩 +## Reference 로딩 — 그래프 -조건 충족 시에만 지정 파일을 **전부 읽고**, 무관한 reference는 로드하지 않는다. +reference는 [`reference-graph.json`](references/reference-graph.json)에 선언된 +노드다. 각 노드는 로드 조건(`when`)과 함께 읽어야 하는 의존 노드(`requires` 엣지)를 +선언한다. 조건 충족 시에만 지정 노드 파일을 **전부 읽고** `requires` 엣지로 연결된 +노드를 함께 로드하며, 무관한 reference는 로드하지 않는다. 카드 절차에 진입하면 공통 +노드 [`common.md`](references/common.md)를 다른 노드보다 먼저 읽는다 — 권위 우선순위· +정책 출처·피드백 라우팅의 canonical 정의가 거기 있다. +- 진입 risk 판정이 Low → [`lanes/low-fast-path.md`](references/lanes/low-fast-path.md)만 로드 (exclusive lane — 다른 노드 로드 금지, 실격 조건이 나오면 아래 카드 절차로 승격) - graph-orchestrated delivery loop를 명시적으로 요청받은 경우에만 → 설치된 `$agent-graph-engineering`을 이름으로 명시적으로 로드·호출하고 [`graph-orchestration.md`](references/graph-orchestration.md)를 전부 읽은 뒤 bundled workflow 실행 -- 명시적 Oracle 요청 또는 Medium/High 판정 뒤 카드 작성 시작 → [`bva.md`](references/bva.md), [`oracle-card.md`](references/oracle-card.md) +- 명시적 Oracle 요청 또는 Medium/High 판정 뒤 카드 작성 시작 → [`card/policy-sources.md`](references/card/policy-sources.md), [`card/risk-grill.md`](references/card/risk-grill.md) +- 계약 행(매트릭스) 작성 → [`bva.md`](references/bva.md), [`card/card-format.md`](references/card/card-format.md) +- Draft 사용자 확인·lock·run artifact 초기화 직전 → [`card/confirmation-lock.md`](references/card/confirmation-lock.md) - 새 UI·redesign 또는 보이는 layout·palette·type·copy·motion·responsive·identity 변경 전 → [`visual-design.md`](references/visual-design.md) - screenshot 비교·직접 브라우저 QA 명시 요청 → 별도 `$frontend-visual-qa` 호출, 이 스킬은 실행을 소유하지 않음 -- Delivery 진입 직후 → 설치된 `$test` 스킬을 이름으로 명시적으로 로드·호출, 못 찾으면 `FAIL`; [`implementation-loop.md`](references/implementation-loop.md), [`changeability.md`](references/changeability.md), [`frontend-implementation.md`](references/frontend-implementation.md), [`architecture-contract.md`](references/architecture-contract.md) -- 카드에 async·순서·중복 제출·retry·다단계 상태 `O*` 행, 또는 client state·exported Props·shared/package API·trust boundary 타입 변경 전 → [`type-constraints.md`](references/type-constraints.md) +- Delivery 진입 직후 → 설치된 `$test` 스킬을 이름으로 명시적으로 로드·호출, 못 찾으면 `FAIL`; [`delivery/ledger.md`](references/delivery/ledger.md), [`delivery/red.md`](references/delivery/red.md) +- `VALID_RED` 뒤 production 수정 직전 → [`delivery/implementation-decision.md`](references/delivery/implementation-decision.md), [`changeability.md`](references/changeability.md), [`frontend-implementation.md`](references/frontend-implementation.md); React architecture 경계·state ownership·public API 변경이면 [`architecture-contract.md`](references/architecture-contract.md) +- 셀프피드백·GREEN 게이트·review 전이 → [`delivery/green-review.md`](references/delivery/green-review.md) +- 카드에 async·순서·중복 제출·retry·다단계 상태 `O*` 행, 또는 client state·exported Props·shared/package API·trust boundary 타입 변경 전 → [`types/state-ladder.md`](references/types/state-ladder.md), [`types/authoring.md`](references/types/authoring.md) +- exported shared/package API·Props 표면 변경 전 → [`types/api-surface.md`](references/types/api-surface.md) - 레포당 1회 — 타입 계약 첫 작성 전, 또는 diff가 tsconfig·TS 버전을 바꿈 → [`type-environment.md`](references/type-environment.md), 결과를 Source Registry에 기록, 카드마다 반복하지 않음 - Delivery 활성 + FSD 레포(또는 도입 승인) + FSD 채택·폴더 구조를 제안·설계·리뷰하기 전 → [`fsd.md`](references/fsd.md) - backend·full-stack·DB·data-access 경계를 만들거나 바꾸기 전 → [`backend.md`](references/backend.md) - 성능 요구·개선 claim이 있는 카드 작성 또는 production 수정 전 → [`performance.md`](references/performance.md) -- 구현·테스트 검증 후 → [`subagent-review.md`](references/subagent-review.md); Design Intent 있으면 [`visual-design.md`](references/visual-design.md) 재독 +- 구현·테스트 검증 후 → [`subagent-review.md`](references/subagent-review.md); 타입 계약을 만든 변경이면 [`types/review-criteria.md`](references/types/review-criteria.md); Design Intent 있으면 [`visual-design.md`](references/visual-design.md) 재독. 리뷰 기준은 프롬프트에 복붙하지 않고 diff에 해당하는 reference 파일만 `review-packet --review-point`의 파일 링크로 전달한다 ## 모드 선택 @@ -165,7 +179,7 @@ description: Use when the user explicitly requests an Oracle contract or graph-o 범위 기록. `local`·`identity-shaping`은 Design Change Confirmation을 받고 카드에 기록. 미확인·미결이면 `NEEDS_DECISION`. 6. Risk 판정 + 정책 출처 조사. -7. 필요한 Grill 질문과 BVA로 **Draft Oracle** 작성. Grill은 `oracle-card.md`의 phase +7. 필요한 Grill 질문과 BVA로 **Draft Oracle** 작성. Grill은 `card/risk-grill.md`의 phase 순서(결과→위험→데이터·아키텍처→API→경합·비동기→상태→시각→성능·운영)를 따르고, 사용자가 명시적으로 1문1답 인터뷰를 요청한 경우에만 라운드 상한 없이 진행한다. 8. 기존 revision은 semantic delta, 새 카드는 전체 정책·미결 질문을 보여주고 명시적으로 @@ -198,7 +212,7 @@ description: Use when the user explicitly requests an Oracle contract or graph-o reporter의 실패 test name을 카드 행에 매핑하고 `oracle-verify.mjs red` 통과 run만 `VALID_RED`로 전이. network·mock·테스트 배치는 불변 규칙과 승인된 architecture source, FSD면 `references/fsd.md` 규칙을 따른다. -7. production 수정 전 `implementation-loop.md`·`frontend-implementation.md`로 구현 결정 +7. production 수정 전 `delivery/implementation-decision.md`·`frontend-implementation.md`로 구현 결정 기록 후 최소 구현→GREEN. 8. High risk는 sibling `test` skill의 mutation kill·원복·재-GREEN 먼저. 9. `oracle-run.mjs review-packet`으로 원시 리뷰 입력 생성 → `subagent-review.md`로 독립 @@ -206,15 +220,9 @@ description: Use when the user explicitly requests an Oracle contract or graph-o ## 피드백 라우팅 -테스트·리뷰의 새 관찰마다 주원인 하나를 기록하고 아래 경로만 사용한다. - -- `POLICY_GAP` → 카드 현재본과 질문을 출력하고 `NEEDS_DECISION` -- `EVIDENCE_GAP` → 잠긴 카드 범위 안에서 누락된 테스트·reviewer 매핑만 추가 -- `HARNESS_DEFECT` → locator·fixture·barrier 등 허용 항목만 공용 2회 예산으로 보정 -- `PRODUCT_DEFECT` → 결정론 테스트의 `VALID_RED` 뒤 production 개선 예산 사용 -- `ENVIRONMENT_DEFECT` → production을 건드리지 않고 실제 원인과 함께 `FAIL` -- `NON_ORACLE_OPINION` → 근거와 함께 기록하고 완료 차단이나 정책 변경에 사용하지 않음 - +테스트·리뷰의 새 관찰마다 주원인 하나를 `POLICY_GAP`·`EVIDENCE_GAP`· +`HARNESS_DEFECT`·`PRODUCT_DEFECT`·`ENVIRONMENT_DEFECT`·`NON_ORACLE_OPINION` 중 +하나로 기록하고, [`common.md`](references/common.md)의 canonical 라우팅 표만 따른다. 현재 구현·test 관찰·reviewer 선호는 분류 증거일 뿐 정책 출처가 아니다. 승인된 Design Intent 불일치는 단순 선호가 아니며 `visual-design.md` 기준으로 분류한다. diff --git a/packages/frontend-oracle-design/skills/references/card/card-format.md b/packages/frontend-oracle-design/skills/references/card/card-format.md new file mode 100644 index 0000000..21c1a1e --- /dev/null +++ b/packages/frontend-oracle-design/skills/references/card/card-format.md @@ -0,0 +1,105 @@ +# Oracle Card — 카드 형식과 self-review + +## 카드 형식 + +[`bva.md`](../bva.md)의 네 축과 자동 추가 TC 7종을 적용한다. 모든 새 카드는 +`Outcome Brief → Source Registry → User Confirmation → 결정된 정책 → 계약 행` 순서. +`oracle-verify.mjs card`가 Outcome Brief 필수 값과 Source Registry `Kind`를 lock 전에 +검사한다. + +Design Intent가 있으면 [`visual-design.md`](../visual-design.md) 형식을 행동 매트릭스 +바로 앞에 둔다. Design Intent·`D*` 행·`O*` 행·확인 근거는 모두 같은 Oracle bytes로 +잠근다. 비-N/A `D*` 행은 `HARD → test`, `RELATIONAL → visual | pending`, +`JUDGMENT → designer reviewer`로 매핑. `local`·`identity-shaping`이면 Design Change +Confirmation의 사용자 답변 위치를 같은 Design Intent에 기록. + +```markdown +## User Confirmation + +- Status: draft | approved +- Source: 사용자 승인 응답의 메시지·issue·문서 위치 +- Delta: new card 또는 이전 revision 대비 의미 변경 요약 +- Visual QA authorization: approved | declined # RELATIONAL 행이 있을 때 +``` + +Draft 단계는 `Status: draft` 유지. 사용자가 카드 전문·delta를 확인하고 명시적으로 +승인한 뒤에만 `approved`와 실제 응답 위치를 기록한다. 에이전트 추천이나 "사용자가 +원할 것"이라는 추론을 Source로 쓰지 않는다. + +| ID | 정책 | Given | When | Then | Never | 부작용(종류×횟수) | BVA | +| --- | ---- | ----- | ---- | ---- | ----- | ----------------- | --- | + +| 열 | 의미 | +| -------- | ------------------------------------------ | +| `Given` | 행동 직전 상태와 전제 | +| `When` | 사용자 행동, 응답, 시간 또는 순서 변화 | +| `Then` | 반드시 관찰되어야 하는 결과 | +| `Never` | 절대 발생하면 안 되는 반대 결과 | +| `부작용` | 요청·저장·이동·이벤트의 정확한 종류와 횟수 | +| `BVA` | 값·상태·시간/순서·횟수 중 검토한 경계 | + +규칙: + +- `Never`와 부작용 횟수가 빈 행은 미완성이다. +- 각 결정에 stable 정책 ID(`P*`)와 적용 행, 각 `O*`·`D*` 행의 `정책` 열에 같은 ID. + 정책 ID와 행 ID의 양방향 참조가 정확히 일치해야 한다. +- UI 상태와 실제 부작용 횟수를 각각 검증. +- 전제가 있는 자동 추가 TC는 행으로 만들고, 없으면 N/A와 사유 기록. +- 존재하지 않는 retry·cancel·race를 테스트 목적으로 발명하지 않는다. +- 오류는 기능에 해당하는 subtype별 메시지·복구·부작용을 구분. + +축약 예시: + +```markdown +| ID | 정책 | Given | When | Then | Never | 부작용 | BVA | +| --- | ---- | --------- | ---------- | -------------- | ------------------ | ----------- | ------------- | +| O1 | P1 | 유효 입력 | 저장 클릭 | pending 표시 | 응답 전 성공 UI | POST×1 | 상태: pending | +| O2 | P1 | pending | 클릭+Enter | pending 유지 | 두 번째 POST | POST×1(총) | 횟수: 1/2 | +| O3 | P2 | pending | 서버 5xx | 오류+입력 유지 | 성공 UI, 입력 유실 | 성공 저장×0 | 상태: error | +``` + +## State Model — 선택 사항 + +기본값은 생략이다. async 행이 있어도 `O*` 행 자체가 계약이며, 섹션이 없다고 +lint가 막지 않는다. 전이 정책이 행 나열만으로 읽히지 않을 만큼 얽힌 카드 +(다단계 제출·낙관적 롤백·결제류)에만 계약 행 뒤에 `## State Model`을 추가한다. +추가했다면 `oracle-verify.mjs card`가 구조를 검증한다: 비어 있지 않은 +`States`·`Events`, 모든 전이가 실제 `O*` 행을 인용하는 전이표 없이는 +`CARD_LINT_FAILED`로 lock이 막힌다. + +```markdown +## State Model + +- States: editing, submitting, success, failure +- Events: SUBMIT, RESPONSE_OK, RESPONSE_ERROR + +| From | Event | To | 행 | +| ---------- | -------------- | ---------- | --- | +| editing | SUBMIT | submitting | O1 | +| submitting | SUBMIT | submitting | O2 | +| submitting | RESPONSE_OK | success | O4 | +| submitting | RESPONSE_ERROR | failure | O3 | +``` + +- 상태·이벤트는 `O*` 행의 `Given`·`When`·`Then`에서만 도출하고 표의 모든 전이는 행 + ID를 참조한다. 참조 없는 전이는 발명된 정책이다. +- `상태 × 이벤트`의 빈 조합은 불가능(타입으로 표현 불가)인지 미결 정책인지 구분한다. + 미결이면 `NEEDS_DECISION`이며 "무시"를 기본값으로 채우지 않는다. +- 이 섹션은 카드 bytes에 포함되어 함께 잠긴다. discriminated union 번역은 + [`types/state-ladder.md`](../types/state-ladder.md) 담당. + +## Adversarial self-review + +각 행에 네 질문을 적용하고 반례가 나오면 행을 보강한다. + +1. 이 행을 통과하면서 요구사항을 위반하는 가장 단순한 구현은? +2. 정상적인 다른 구현인데 이 행 때문에 실패할 수 있는가? +3. UI만 흉내 내고 실제 부작용 없이 통과할 수 있는가? +4. loading, error, retry, 연속 입력, 순서 역전 중 관련 있지만 빠진 것은? + +예: "저장 중 버튼 disabled"만으로는 disabled 적용 전 POST 두 번을 잡지 못한다. 같은 +행에 `POST×1(총)`과 "두 번째 POST 없음"을 병기한다. + +Design Intent가 있으면 [`visual-design.md`](../visual-design.md)의 genericity·restraint +비평도 수행한다. 출처 있는 미적 요구를 자동화하기 어렵다는 이유로 +`NON_ORACLE_OPINION`이나 N/A로 내리지 않는다. diff --git a/packages/frontend-oracle-design/skills/references/card/confirmation-lock.md b/packages/frontend-oracle-design/skills/references/card/confirmation-lock.md new file mode 100644 index 0000000..a761b36 --- /dev/null +++ b/packages/frontend-oracle-design/skills/references/card/confirmation-lock.md @@ -0,0 +1,147 @@ +# Oracle Card — 사용자 확인·revision lock·run artifact + +## Draft Oracle과 사용자 확인 + +새 카드나 의미가 바뀐 revision은 다음 직렬 gate를 반드시 거쳐 사용자에게 확인받는다. + +1. production 수정 없이 외부 기준과 기존 revision 조사. +2. **Draft Oracle**과 semantic delta, 미결 질문 작성. +3. 전체 카드와 delta를 사용자에게 보여주고 재확인. +4. 승인되면 `User Confirmation`을 `approved`로 바꾸고 실제 응답 위치 기록. +5. 수정 요청이면 Draft를 고쳐 재확인. +6. 무응답·정책 충돌이면 `NEEDS_DECISION`. + +기존 locked Oracle 범위 안의 구현·테스트 보정에는 새 카드를 만들지 않는다. 그러나 +`Then`·`Never`·부작용·BVA·Design Intent·정책 출처 중 하나라도 의미가 바뀌면 잠긴 +파일을 제자리에서 고치지 않고 새 경로에 Draft revision을 만든다. 새 revision도 사용자 +확인 전에는 lint·lock·테스트·production 수정으로 넘어가지 않는다. + +## 결정적 revision lock + +사용자가 확인한 카드는 self-review 뒤 exact bytes를 파일로 저장하고 bundled script로 +잠근다. Low fast path처럼 새 정책·카드가 없는 작업은 이 절차에 들어오지 않는다. + +Design-only로 잠근 revision을 나중에 Delivery로 확장하며 architecture·backend 등 새 +local source가 필요해지면 기존 lock에 덧붙이지 않는다. source delta를 사용자에게 +보여주고 새 revision 경로에서 카드와 전체 source 집합을 한 번에 잠근다. 처음부터 +Delivery가 요청됐으면 모든 source 승인을 마칠 때까지 lock을 미룬다. + +대상 레포가 agent artifact 위치를 정하지 않았다면: + +```text +/.ai/oracles//oracle.md +/.ai/oracles//oracle.lock.json +/.ai/oracles//run-state.json +/.ai/oracles//runs.jsonl +/.ai/oracles//.run-ids/ # 병렬 exec의 원자적 runId reservation +/.ai/oracles//evidence.json +``` + +### 카드 구조 lint + +lock 전에 `oracle-verify.mjs card`로 구조적 최소선을 기계 확인한다. lint는 token·표 +구조 검사일 뿐 semantic approval이 아니다 — 의미 심사는 +[`card-format.md`](card-format.md)의 adversarial self-review 담당. + +```bash +node /scripts/oracle-verify.mjs card \ + --oracle .ai/oracles//oracle.md +``` + +검사 항목: 완전한 Outcome Brief, `Kind` 있는 Source Registry, 승인된 User Confirmation +존재, 모든 정책 줄의 stable ID·`(출처: …)`·적용 행, 정책 ID와 행 ID의 양방향 참조, +중복 없는 행 ID, `O*` 행의 `Then`·`Never`·부작용, `D*` 행의 계약·출처·증거 계층과 +Source Registry 참조, 모호어 부재, 자동 추가 TC 7종의 +실제 계약 행 또는 출처 있는 N/A 표기. `CARD_LINT_FAILED`는 lock 전에 카드를 고치라는 +뜻이며, 검사를 우회하려고 문구만 바꾸지 않는다. + +에이전트가 직접 실행하며 사용자에게 명령 실행을 요청하지 않는다. 승인된 로컬 명세 +파일은 `--source`를 반복해 함께 잠근다. URL·Figma 같은 원격 기준은 정확한 version을 +카드 bytes에 기록하고 외부 기준 게이트에서 다시 확인한다. + +```bash +node /scripts/oracle-lock.mjs create \ + --oracle .ai/oracles//oracle.md \ + --lock .ai/oracles//oracle.lock.json \ + --source + +node /scripts/oracle-lock.mjs verify \ + --lock .ai/oracles//oracle.lock.json +``` + +- ``는 현재 host가 실제로 로드한 이 스킬의 디렉터리. home 경로 hardcode + 금지. +- 출력된 `sha256:`가 Oracle revision이다. +- `create`는 동일 bytes의 기존 lock에 idempotent, 변경된 카드·source의 기존 lock은 + 덮어쓰지 않는다. 승인된 새 revision은 이전 artifact를 보존하고 새 경로에 생성. +- 모든 새 카드·revision은 카드 전문·delta를 확인받는다. digest는 확인된 + bytes의 식별자일 뿐 사용자 확인을 대신하지 않는다. +- 테스트 작성, production 수정, 독립 리뷰, 완료 상태 발급 직전 `verify` 재실행. + `oracle-run.mjs`의 `exec`·`transition`은 매 호출 같은 검증을 자동 수행. +- `ORACLE_CHANGED`·`SOURCE_CHANGED`면 기존 RED·GREEN·리뷰 증거를 폐기하고 변경 diff와 + 카드 현재본을 제시해 `NEEDS_DECISION`으로 복귀. +- `LOCK_INVALID`·도구 부재·실행 불가는 결정론 판정 실패 → `FAIL`. +- mismatch 제거용 자동 재생성 금지. 재잠금은 source gate, Draft delta, 사용자 재확인, + self-review를 다시 거친 뒤에만. +- Low risk로 카드를 생략했으면 lock N/A 사유를 남긴다. Medium/High에서 파일시스템· + Node가 없으면 Design-only와 Delivery 모두 LLM 판정으로 대체하지 않고 `FAIL`. + +SHA-256은 drift 검출 장치일 뿐 lockfile을 다시 쓸 수 있는 actor의 승인 권한을 +보장하지 않는다. 강한 통제가 필요하면 CI human approval·CODEOWNERS·외부 서명을 +추가한다. run ledger·상태 파일도 같은 한계. + +### Run artifact 초기화 + +Delivery 진입 시 lock 직후 run ledger와 상태 파일을 만든다. Design-only로 끝나면 +생성하지 않는다. `journal.md`는 예외다 — Grill부터 같은 디렉터리에 쌓이며 ledger와 +별개로 단계 근거만 담는다. + +```bash +node /scripts/oracle-run.mjs init \ + --dir .ai/oracles/ \ + --lock .ai/oracles//oracle.lock.json \ + --risk low|medium|high \ + --required-label behavior \ + --required-label lint \ + --harness-path vitest.config.ts \ + --milestone list:O1,O2 \ + --milestone detail:O3,O4 +``` + +- `--required-label`: 대상 레포에서 실제 적용되는 targeted test, lint, typecheck, + build label을 반복 선언. 최소 하나 필요, GREEN·리뷰 후 재검증에서 모두 재확인. +- `init`은 lock을 검증하고 현재 worktree digest를 `ORACLE_READY` 기준선으로 저장 — + 이후 TDD 순서 판정의 근거. +- `--scan-root` 기본값은 현재 작업 디렉터리. monorepo에서 범위를 좁힐 때만 명시. +- RED 전에 바꿔야 하는 config·setup·mock 배선은 `--harness-path`로 scan root 기준의 + 정확한 상대 파일 경로를 반복 선언. glob·디렉터리·root 밖 경로 불허, 실제로 존재하고 + worktree snapshot에 포함되는 파일만. +- 큰 카드는 `--milestone :O1,O2`를 반복해 겹치지 않는 test-owned 행을 묶는다. + 행은 Oracle에 존재해야 하고 두 milestone이 같은 행을 소유하지 않는다. 작은 카드에는 + 선언하지 않는다. +- 상태 파일이 이미 있으면 `init`은 실패한다. 예산·기준선 초기화 목적 재실행 금지. 새 + revision은 새 `` 디렉터리. + +## 설계 종료 상태 + +### `ORACLE_READY` + +- Outcome Brief 완성, Source Registry에 Kind·관할·위치·version·승인 상태 또는 N/A 사유 +- 카드가 외부 기준의 상태·문구·interaction·부작용을 누락·왜곡하지 않음 +- 모든 정책에 인정되는 출처 +- `User Confirmation`이 `approved`이고 새 카드 또는 semantic delta를 승인한 실제 + 사용자 응답 위치가 있음 +- UI 시각 범위 기록, `local`·`identity-shaping`이면 승인된 Design Intent와 모든 `D*` + 행의 `Never`·출처·증거 계층 완성 +- `local`·`identity-shaping`이면 Design Change Confirmation의 명시적 사용자 답변 위치 +- `identity-shaping`이면 두 번의 설계 pass를 완료한 제안으로 확인받음 +- 모든 행의 `Never`와 부작용 횟수 완성 +- 자동 추가 TC 7종 추가 또는 N/A 사유 +- adversarial self-review 통과 +- `oracle-verify.mjs card` lint와 revision lock 검증이 통과함 + +### `NEEDS_DECISION` + +미결 질문, 질문별 추천안과 근거, 카드 현재본을 출력한다. 이 상태에서는 테스트·구현을 +진행하지 않는다. 잠긴 적이 있으면 마지막 SHA-256과 mismatch를 함께 출력한다. 카드 +현재본은 다음 세션의 재개 자료다. diff --git a/packages/frontend-oracle-design/skills/references/card/policy-sources.md b/packages/frontend-oracle-design/skills/references/card/policy-sources.md new file mode 100644 index 0000000..3576a08 --- /dev/null +++ b/packages/frontend-oracle-design/skills/references/card/policy-sources.md @@ -0,0 +1,87 @@ +# Oracle Card — 외부 기준과 정책 출처 + +## 외부 기준 게이트 + +Risk·Grill 전에 사용자가 제공했거나 레포가 승인된 기준으로 지정한 자료를 찾아 전부 +읽는다. 우선순위와 관할 규칙은 [`common.md`](../common.md)의 권위 우선순위를 따른다 — +production 코드·기존 테스트·브라우저 관찰은 조사 증거일 뿐 정책 출처가 아니다. + +카드 상단에 이번 변경의 제품 결과와 범위를 기록한다. KPI가 없으면 수치를 발명하지 +않고 사용자가 관찰할 수 있는 성공 결과를 쓴다. + +```markdown +## Outcome Brief + +- Actor and context: 누가 어떤 상황에서 사용하는가 +- Observable success: 관찰 가능한 성공 결과 +- Non-goals: 이번 변경에서 하지 않을 일 +- Worst regression: false GREEN의 가장 큰 피해 +- Reversibility: 되돌리는 방법 또는 N/A 사유 +- Sources: S1, S2 +``` + +### Requested mechanism check — 수단과 결과 분리 + +사용자가 구체적 수단(화면·필드·버튼·자동화·조건)을 요청했지만 의도한 결과나 +사용자가 불명확하면 Outcome Brief에 다음을 함께 기록한다. 수단과 결과가 이미 +일치하면 이 소절 없이 그대로 진행한다. + +- Requested mechanism: 사용자가 요청한 구체적 수단 +- Intended outcome: 실제로 해결하려는 사용자·비즈니스 문제 +- Smallest reversible scope: 그 결과를 확인할 수 있는 최소 가역 범위 +- Deferred scope: 검증 전에는 만들지 않을 범위 — Non-goals에 사유와 함께 기록 + +규칙: + +- 더 작은 대안은 Draft Oracle에 제시만 한다. scope 축소는 사용자의 명시적 + 승인으로만 확정하며 에이전트가 임의로 줄이지 않는다. +- 이 검토를 `mandatory-constraint`(보안·개인정보·법·접근성·데이터 정합성) 생략 + 근거로 쓰지 않는다. + +## Source Registry + +```markdown +## Source Registry + +| ID | Kind | 관할 | 기준 | 위치·version | 승인 상태 | +| --- | -------------------- | ------------------------ | ------------- | ----------------------------------- | --------- | +| S1 | product-policy | 비즈니스 결과 | PRD | docs/profile.md#save-flow, revision | approved | +| S2 | product-policy | UI·문구·interaction | Figma | file/page/frame/version | approved | +| S3 | project-constraint | payload·오류·idempotency | API 계약 | endpoint/version | approved | +| S4 | mandatory-constraint | 접근성·토큰 | 디자인 시스템 | 문서 위치/version | approved | +``` + +허용 `Kind` 4종: + +- `product-policy`: 사용자 답변과 승인된 PRD·Figma처럼 제품 결과를 정하는 자료 +- `mandatory-constraint`: 보안·개인정보·법·접근성·데이터 정합성처럼 제품 선호로 낮출 + 수 없는 제약 +- `project-constraint`: 저장소의 공개 API·architecture·테스트·호환성 계약 +- `implementation-reference`: 실제 설치 버전의 공식 문서·구현 휴리스틱. 제품 결과를 + 정하지 못한다. + +규칙: + +- Figma는 원본 파일의 정확한 page·frame·variant를 직접 확인. 열 수 없으면 기억·유사 + 스크린샷으로 대체하지 않는다. +- 외부 기준이 없으면 `N/A — 제공되거나 승인된 외부 기준 없음` 기록. +- 외부 기준끼리 또는 사용자 답변과 충돌, 필수 기준 접근 불가 → 충돌 위치·영향 정책 + 제시 후 `NEEDS_DECISION`. +- 카드는 외부 기준의 실행 가능한 번역이다. 작성 후 외부 기준의 상태·문구·interaction· + 부작용 요구가 누락·왜곡되지 않았는지 대조한다. +- 기준은 자신의 관할 안에서만 우선한다([`common.md`](../common.md) 관할 규칙). + `mandatory-constraint` 충돌 처리도 같은 문서를 따른다. +- 기준의 revision/version이 바뀌면 기존 `ORACLE_READY`를 무효화하고 다시 대조. + +## 정책 출처 + +인정·불인정 목록은 [`common.md`](../common.md)의 정책 출처 절이 canonical이다. +결정된 정책마다 출처를 붙인다 — 출처 없는 정책이 하나라도 있으면 `ORACLE_READY`가 +아니다. + +```markdown +### 결정된 정책 + +- P1: 저장 중 추가 제출은 무시한다. (출처: 유저 Q1=A) (행: O1, O2) +- P2: 5xx 실패 시 입력을 유지한다. (출처: docs/save.md#failure-policy) (행: O3) +``` diff --git a/packages/frontend-oracle-design/skills/references/card/risk-grill.md b/packages/frontend-oracle-design/skills/references/card/risk-grill.md new file mode 100644 index 0000000..3cbc34e --- /dev/null +++ b/packages/frontend-oracle-design/skills/references/card/risk-grill.md @@ -0,0 +1,105 @@ +# Oracle Card — Risk 판정과 정책 Grill + +## UI 디자인 의도 게이트 + +새 UI·redesign 또는 보이는 layout·palette·typography·copy·motion·responsive behavior· +visual identity 변경이면 카드 작성 전 [`visual-design.md`](../visual-design.md)를 전부 +읽는다 — 시각 범위 3종, Design Proposal 규칙, Design Change Confirmation 게이트, +`HARD`·`RELATIONAL`·`JUDGMENT` 증거 계층은 그 문서가 소유한다. 기존 시각 결과 유지 +작업은 `behavior-only`와 N/A 사유만 기록. `local`·`identity-shaping`이면 승인된 +Design Intent와 `D*` 행을 같은 카드에 포함하고, Design Change Confirmation의 명시적 +확인 전에는 잠그지 않는다(`NEEDS_DECISION`). + +시각 범위는 기능 Risk를 대신하지 않는다. 두 판정은 별도로 기록. + +## Risk 판정 + +코드 복잡도가 아니라 **false GREEN의 최악 피해**로 판정한다. UI가 단순해도 부작용이 +위험하면 High다. + +- Low (정적 표시, 순수 동기 helper) → 카드 생략 가능 — risk와 사유 한 줄 기록 +- Medium (조회, 검색, 폼, 캐시) → 카드 작성 +- High (결제, 주문, 저장, 삭제, 권한, 외부 mutation) → 카드 작성 + 사용자 카드 확인 필수 + +## 정책 Grill — 시스템 디자인 인터뷰 + +**답에 따라 예상 결과나 테스트가 달라지는 질문만** 한다. + +- 라운드당 3~5개, 최대 2라운드 +- 각 질문에 추천안과 근거 동봉 +- 레포 문서·승인된 명세에 답이 있으면 질문하지 않음 +- 추천안은 결정이 아니며, 답이 없으면 default로 적용하지 않음 +- 2라운드 후에도 결과를 바꾸는 질문이 남으면 `NEEDS_DECISION` + +### Phase 순서 + +질문은 **앞 답이 뒤 가지를 죽이는 순서**로 한다. 질문 전에 레포·PRD·Figma·API +문서를 먼저 탐색해 답이 있는 질문을 제거한다. 코드 관찰로 얻은 답은 +`project-constraint` 후보일 뿐 제품 정책 출처가 아니다. + +- P1 결과: actor·상황, 관찰 가능한 성공, 비목표, 최악 회귀·가역성, 플랫폼·디바이스·offline·다국어 → Outcome Brief +- P2 부작용·위험: 서버 상태 변경 여부, 돈·데이터·권한 피해 → Risk lane +- P3 데이터·아키텍처: source of truth, stale 허용, 기존 상태 소유자(query·router·form), 핵심 entity와 소유 컴포넌트 → architecture intake, State ownership +- P4 API 계약: 스펙 소스 위치·version, error code별 UI 결과·재시도, idempotency key 주체, pagination 끝 판정 → Source Registry, `API contract` 절 +- P5 경합·비동기: 아래 "자주 필요한 질문" → 카드 `O*` 행 +- P6 상태 모델: 상태 수·불가능한 전이 → State Model(opt-in) +- P7 시각: visual scope, 로딩·빈·에러 표시, 접근성 확인 → Design Intent·`D*` 행 +- P8 성능·운영: 성능 목표 수치·측정법, rollout·flag → performance 게이트 + +가지치기: + +- P1에서 Low 판정이면 grill을 끝내고 [`lanes/low-fast-path.md`](../lanes/low-fast-path.md) + lane으로 라우팅한다. +- endpoint가 없으면 P4, mutation·async가 없으면 P5, `behavior-only`면 P7, 성능 + claim이 없으면 P8을 통째로 건너뛴다. +- 기능이 설치된 `frontend-system-design` reference와 매칭되면 그 문서의 결정 + 포인트를 P4·P5 질문으로 변환해 일반 질문을 대체한다. +- API 스펙 소스가 없으면 P4를 추측으로 채우지 않는다. 대신 카드 행에서 draft + schema를 도출해 Draft Oracle과 함께 제시하고, 명시 승인 시 `project-constraint` + source로 등록해 함께 잠근다. 승인이 없으면 `NEEDS_DECISION`. + +라운드 구성: Round 1 = P1~P3 생존 질문, Round 2 = P4~P7 생존 질문. 사용자가 +명시적으로 1문1답 인터뷰를 요청하면(예: "grill me") Design-only 조사에 한해 +라운드 상한 없이 phase 순서로 진행한다. Delivery 중 정책 질문은 그대로 +`oracle-run.mjs budget` 2라운드를 따른다. + +각 라운드가 끝나면 질문·답·추천안 채택 여부와 가지치기 사유를 +`.ai/oracles//journal.md`에 append한다. 답을 대화에만 남기지 않는다 — +컨텍스트가 요약돼도 다음 단계는 journal과 카드에서 이어진다. + +문답 항목은 한 줄 규격으로 쓴다 — 질문·답·채택·매핑 행이 빠지면 미완성이다: + +```markdown +## Grill Round 1 (P1~P3) — 2026-08-21 + +- Q1(P1): 성공 판정 기준? → 답: 완료 화면+주문번호 → 채택: 추천 수용 → 행: P1, O1 +- Q2(P4): 409의 UI 결과? → 답: 기존 주문 화면 이동 → 채택: 수정 → 행: P3, O5 +- 가지치기: P7 스킵 — behavior-only +``` + +자주 필요한 질문(P5): + +- pending 중 중복 제출을 무시할지, 큐잉할지, 오류로 볼지 +- 실패 후 입력·기존 데이터를 유지할지 +- 오류 subtype별 재시도 허용 여부 +- A 후 B 요청, B 후 A 응답에서 어떤 결과가 이길지 +- 이탈·취소 후 늦은 응답을 어떻게 처리할지 +- outcome-unknown timeout에서 재시도와 idempotency를 어떻게 보장할지 +- 요청된 수단이 의도한 결과를 얻는 최소 수단인지, 더 작은 대안을 먼저 검증할지 + +방법 근거: phase 순서는 +[RADIO framework](https://www.greatfrontend.com/front-end-system-design-playbook/framework)의 +R→A→D→I→O 순서를, 질문·정책·예시 분리는 +[Example Mapping](https://cucumber.io/blog/bdd/example-mapping-introduction/)의 +rule(=`P*`)·example(=`O*`)·question(=red card) 대응을 따른다. `Then`이 불명확한 +예시는 질문이다 — 행을 만들지 않고 red card로 기록한다. red card가 쌓이면 +`NEEDS_DECISION`, rule이 쌓이면 Smallest reversible scope 분할을 제안한다. + +RADIO 각 요소의 처리 위치 — grill이 전부 소유하지 않는다: + +- R Requirements: Grill P1·P2 +- A Architecture: Delivery architecture 게이트 — grill에서 구현 구조를 질문하지 않는다 +- D Data model: Grill P3 +- I Interface (server): Grill P4 → 스펙 없으면 카드 도출 draft → 승인 → `## API contract` +- I Interface (component): [`types/state-ladder.md`](../types/state-ladder.md) — 카드 `O*` 행에서 도출 +- O Optimizations: P5 경합·P7 접근성·P8 성능 + Delivery 증거 행 diff --git a/packages/frontend-oracle-design/skills/references/changeability.md b/packages/frontend-oracle-design/skills/references/changeability.md index eb08dbe..3db50b9 100644 --- a/packages/frontend-oracle-design/skills/references/changeability.md +++ b/packages/frontend-oracle-design/skills/references/changeability.md @@ -6,10 +6,11 @@ 휴리스틱이다. 결과·문구·상태·부작용을 새로 정하거나 승인된 Oracle을 고치는 데 쓰지 않는다. -권위 순서: 1) 보안·개인정보·법적·접근성·정합성 제약과 승인된 Oracle, 2) 대상 레포의 -`AGENTS.md`·`CLAUDE.md`·architecture·API·테스트 계약, 3) 실제 설치 버전과 기존 구현 -관례, 4) 이 문서의 구현 휴리스틱과 외부 사례. 충돌하면 상위 기준을 따른다. Toss -자료는 구현 후보를 찾는 근거일 뿐 다른 레포에 강제하는 권위가 아니다. +권위 순서는 [`common.md`](common.md)의 공통 우선순위가 canonical이다 — 강제 제약과 +승인된 Oracle, 대상 레포의 `AGENTS.md`·`CLAUDE.md`·architecture·API·테스트 계약, +실제 설치 버전과 기존 구현 관례, 마지막으로 이 문서의 구현 휴리스틱과 외부 사례. +충돌하면 상위 기준을 따른다. Toss 자료는 구현 후보를 찾는 근거일 뿐 다른 레포에 +강제하는 권위가 아니다. ## 읽는 방법 @@ -240,7 +241,7 @@ memoization·cache·lazy loading과 단일 request를 위한 global state다. - React runtime 기준은 [`frontend-implementation.md`](frontend-implementation.md)가 소유한다. - Implementation Decision의 경로·필드·작성 시점은 - [`implementation-loop.md`](implementation-loop.md)가 소유한다. + [`delivery/implementation-decision.md`](delivery/implementation-decision.md)가 소유한다. - `PASS | FINDING | N/A`, finding router와 최소 수정 절차는 [`subagent-review.md`](subagent-review.md)가 소유한다. diff --git a/packages/frontend-oracle-design/skills/references/common.md b/packages/frontend-oracle-design/skills/references/common.md new file mode 100644 index 0000000..f1d8639 --- /dev/null +++ b/packages/frontend-oracle-design/skills/references/common.md @@ -0,0 +1,78 @@ +# 공통 계약 — 권위·정책 출처·피드백 라우팅 + +카드 절차(명시적 Oracle 요청 또는 Medium/High 판정)에 진입하면 다른 reference 노드보다 +먼저 읽는다. 여러 reference가 공유하던 중복 정의는 이 문서가 canonical이다 — 각 +reference는 자기 단계의 특칙만 더하고, 이 문서와 어긋나면 이 문서가 이긴다. + +## 권위 우선순위 + +사용자가 제공했거나 레포가 승인된 기준으로 지정한 자료의 우선순위. 하위 출처는 상위 +출처를 덮어쓰지 않는다. + +1. 보안·개인정보·법적·접근성·금융 및 데이터 정합성의 강제 제약(`mandatory-constraint`) +2. 사용자의 명시적 행동 계약과 공개 호환성 +3. 대상 레포의 필수 아키텍처·API·테스트 계약(`AGENTS.md`·`CLAUDE.md` 포함) +4. 승인된 기획서·PRD·수용 기준·디자인 시스템·Figma 원본의 해당 관할 +5. 위 기준을 실행 가능한 계약으로 옮긴 Oracle Card +6. 실제 설치 버전의 공식 문서, framework maintainer·커뮤니티 휴리스틱 — 구현 + 선택지일 뿐 제품 정책 출처가 아님 +7. production 코드·기존 테스트·브라우저 관찰 — 조사 증거일 뿐 정답 권위가 아님 + +`mandatory-constraint`와 다른 source가 충돌하면 보안·접근성·정합성을 제품·시각 선호로 +낮춰 통과하지 않는다. 충돌과 안전한 대안을 제시하고 `NEEDS_DECISION`. + +## 관할 규칙 + +- 기준은 자신의 관할 안에서만 우선한다. Figma는 layout·copy·interaction을 정하지만 + API idempotency를 정하지 못하고, API 계약은 그 반대다. +- 관할이 겹치거나 불명확하면 임의로 절충하지 않고 `NEEDS_DECISION`. +- 기준의 revision/version이 바뀌면 그 기준을 인용한 기존 판정을 무효화하고 다시 + 대조한다. + +## 정책 출처 + +인정: 1) 사용자의 명시적 답변, 2) 승인된 기획서·PRD·수용 기준·디자인 시스템·Figma의 +정확한 위치·version, 3) 적용되는 보안·개인정보·법적·접근성·데이터 정합성 제약, 4) +레포가 공개 계약으로 지정한 API·architecture·호환성 문서. + +인정 안 함: 에이전트 추천안, production 코드, 기존 테스트, 브라우저에서 관찰한 현재 +동작, `implementation-reference`로 분류한 framework 문서·구현 휴리스틱, 테스트·subagent +의 증거·비평. + +결정된 정책마다 출처를 붙인다. 출처 없는 정책이 하나라도 있으면 `ORACLE_READY`가 +아니다. + +## 피드백 라우팅 — canonical 분류 + +테스트·리뷰·구현의 새 관찰마다 주원인 하나를 기록하고 아래 경로만 사용한다. 현재 +구현·test 관찰·reviewer 선호는 분류 증거일 뿐 정책 출처가 아니다. + +| 분류 | 뜻 | 라우팅 | +| -------------------- | ------------------------------------------- | ------------------------------------------------------------ | +| `POLICY_GAP` | 결과를 바꾸는 정책이 카드에 없거나 미결 | 카드 현재본과 질문을 출력하고 `NEEDS_DECISION` | +| `EVIDENCE_GAP` | 잠긴 카드 범위 안의 테스트·매핑 누락 | 누락된 테스트·reviewer 매핑만 추가 | +| `HARNESS_DEFECT` | locator·fixture·barrier 등 테스트 기계 결함 | 허용 항목만 공용 2회 예산(`budget --spend harness`)으로 보정 | +| `PRODUCT_DEFECT` | 잠긴 계약과 실제 구현의 불일치 | 결정론 테스트의 `VALID_RED` 뒤 production 개선 예산 사용 | +| `ENVIRONMENT_DEFECT` | 도구·환경 문제로 판정 불가 | production을 건드리지 않고 실제 원인과 함께 `FAIL` | +| `NON_ORACLE_OPINION` | 출처 없는 선호·취향 | 근거와 함께 기록, 완료 차단이나 정책 변경에 사용하지 않음 | + +- revision mismatch는 피드백 분류 대상이 아니다. 기존 증거를 즉시 폐기하고 lock + 규칙대로 `NEEDS_DECISION` 또는 `FAIL`로 이동한다. +- 예산은 서로 대체하지 않는다. `BUDGET_EXHAUSTED`면 마지막 실제 실패와 함께 `FAIL`로 + 보고하고 다른 예산으로 우회하지 않는다. + +## 공통 상태 의미 + +- `NEEDS_DECISION` — 결과를 바꾸는 정책이 미결. 카드 현재본, 미결 질문, 질문별 + 추천안과 근거를 출력하고 테스트·구현을 진행하지 않는다. 잠긴 적이 있으면 마지막 + SHA-256과 mismatch를 함께 출력한다. +- `FAIL` — 환경·하네스·도구 문제 또는 예산 소진으로 계약 판정 불가. LLM 판정으로 + 대체하지 않는다. + +## 공통 금지 + +- ledger를 거치지 않은 실행을 증거로 보고 +- mismatch 통과용 자동 재잠금·lock 우회 +- assertion 약화, `test.skip` 전환, 임의 sleep으로 GREEN 만들기 +- 브라우저의 현재 동작을 기대값으로 채택 +- 카드에 없는 상태·전이·정책 발명 — `POLICY_GAP`으로 `NEEDS_DECISION` diff --git a/packages/frontend-oracle-design/skills/references/delivery/green-review.md b/packages/frontend-oracle-design/skills/references/delivery/green-review.md new file mode 100644 index 0000000..e15bf9b --- /dev/null +++ b/packages/frontend-oracle-design/skills/references/delivery/green-review.md @@ -0,0 +1,189 @@ +# Delivery — 최소 구현·GREEN 게이트·review 전이 + +## 최소 구현·셀프피드백 + +최대 3라운드. 한 라운드: + +1. 실패한 카드 행 하나 또는 같은 root cause의 행 묶음 선택. +2. 관련 호출 경로를 끝까지 추적해 모든 호출자가 공유하는 원인 파악. +3. 해당 계약만 만족하는 최소 production 변경 작성. +4. 실패했던 targeted test 재실행. +5. 영향 범위 테스트 실행. +6. 결과를 분류하고 다음 행동 결정. + +분류·라우팅의 canonical 정의는 [`common.md`](../common.md)의 피드백 라우팅이다. +이 단계의 특칙: + +- `POLICY_GAP` → 카드 현재본과 질문을 출력하고 `NEEDS_DECISION` +- `EVIDENCE_GAP` → 잠긴 카드 범위 안에서 누락 테스트·증거만 추가 +- `HARNESS_DEFECT` → sibling `test` skill 허용 항목과 공용 2회 예산 안에서만 보정 +- `PRODUCT_DEFECT` → 같은 카드 행을 유지하고 최소 수정 후 재실행 +- `ENVIRONMENT_DEFECT` → production을 건드리지 않고 `FAIL` +- `NON_ORACLE_OPINION` → 기록하되 정책·assertion·완료 상태를 바꾸지 않음 + +revision mismatch는 피드백 분류 대상이 아니다. 기존 증거를 즉시 폐기하고 +[`card/confirmation-lock.md`](../card/confirmation-lock.md)의 lock 규칙대로 +`NEEDS_DECISION` 또는 `FAIL`로 이동. + +매 라운드 기록: + +| 라운드 | 카드 행 | 실패 가설 | 최소 변경 | 실제 실행 결과 | 다음 판단 | +| ------ | ------- | --------- | --------- | -------------- | --------- | + +## GREEN 게이트 + +카드 테스트 통과 후 init에서 `--required-label`로 고정한 레포 검증을 각 label의 +`exec`로 실제 실행한다. + +1. targeted test +2. 영향 범위 test +3. typecheck와 lint +4. Oracle source lock verify 및 레포에 존재하는 구조 검증 명령 +5. 루트 또는 패키지 필수 test/build + +성능 요구·개선 claim이 있으면 동일 조건 baseline/after를 검사하는 기존 repo 명령을 +`performance` 필수 label로 추가. exported shared/package API가 바뀌면 레포가 이미 +제공하는 type test, runtime test, pack/export·changeset 검증만 필수 label로 추가. +해당 없는 작업에 이 명령이나 새 dependency를 만들지 않는다. + +레포 규칙에 정의된 명령이 우선이다. 실행하지 않은 검증을 통과로 보고하지 않는다. +문서화된 명령이 없으면 package scripts를 읽어 targeted + 가장 가까운 package 검증을 +실행한다. 필수 root 명령이 없거나 무관한 기존 실패가 있으면 원문과 영향을 분리해 +보고하고 GREEN으로 숨기지 않는다. + +그다음 `--to IMPLEMENTED_GREEN` 전이를 시도한다. 기계 검사: + +- `ORACLE_CHANGED` — 카드·source bytes가 잠긴 값과 다름 → 증거를 폐기하고 `NEEDS_DECISION` +- `RUN_NOT_GREEN` — 인용한 run이 통과하지 않음 → 실제 통과 run을 만들고 인용 +- `EVIDENCE_REQUIRED` — evidence manifest 없이 상태 전이를 시도함 → 잠긴 카드 전 행을 매핑하고 `--evidence`로 인용 +- `REQUIRED_RUN_MISSING` — 선언한 필수 label의 최신 통과가 없음 → 해당 repo 명령을 `exec --label`로 다시 실행 +- `FLAKINESS_GATE` — 같은 명령의 연속 통과가 risk 필요 횟수에 못 미침 → 같은 명령을 그대로 다시 실행해 연속 통과를 확보 +- `TEST_WEAKENED` — RED 기준선 대비 assertion 감소·금지 토큰·삭제 → 테스트를 원래 강도로 되돌린다 +- `ENV_DRIFT`(경고) — RED와 GREEN의 실행 환경이 다름 → 환경 차이가 결과를 바꿨는지 확인하고 보고에 남긴다 + +flakiness 필요 횟수는 Low 1회, Medium 2회, High 3회. 재실행으로 통과를 뽑는 게 아니라 +**같은 명령이 반복해도 결정론적으로 통과함**을 보이는 절차다. 실패가 섞이면 +`HARNESS_DEFECT`로 분류하고 조용히 다시 굴리지 않는다. + +`TEST_WEAKENED` 금지 토큰: `test.skip`·`it.skip`·`describe.skip`·`.only(`· +`waitForTimeout(`·`toBeTruthy(`·`toBeFalsy(`·`.first()`·`.nth(`·`setTimeout(`과 +screenshot 허용치(`maxDiffPixels`·`maxDiffPixelRatio`·`threshold`) 상향. + +선택한 GREEN run은 parsed reporter가 있는 카드 test run이어야 한다. 별도 lint· +typecheck·build는 각각 선언한 label로 기록. 전이는 모든 필수 label과 evidence +manifest를 직접 검사하며, 전이가 통과해야 `IMPLEMENTED_GREEN`이다. + +### Evidence manifest + +증거 매핑은 산문이 아니라 `.ai/oracles//evidence.json`으로 관리하고 기계로 +검증한다. + +```json +{ + "schemaVersion": 1, + "rows": { + "O1": { "kind": "test", "name": "저장 > pending 표시와 POST 1회" }, + "O2": { "kind": "reviewer", "finding": "f-3", "role": "code-reviewer" }, + "O3": { "kind": "na", "reason": "이 기능에 취소 경로가 없다", "source": "S1" }, + "D1": { "kind": "visual", "artifact": "visual-qa/v-001/evidence.json" }, + "D2": { "kind": "reviewer", "finding": "d-1", "role": "designer" } + } +} +``` + +```bash +node /scripts/oracle-verify.mjs evidence \ + --oracle .ai/oracles//oracle.md \ + --map .ai/oracles//evidence.json \ + --ledger .ai/oracles//runs.jsonl \ + --run r-007 \ + --phase green +``` + +`D*` 행 owner: `HARD → test`, `RELATIONAL → visual | pending`, `JUDGMENT → designer +reviewer`. visual `pending`은 GREEN 증거 검증에서 미검증 항목으로 보고되지만 review +증거 검증에서는 `EVIDENCE_PENDING`으로 완료를 차단한다. + +GREEN 전이는 같은 manifest를 필수 입력으로 받는다. + +```bash +node /scripts/oracle-run.mjs transition \ + --dir .ai/oracles/ \ + --to IMPLEMENTED_GREEN \ + --run r-007 \ + --evidence .ai/oracles//evidence.json +``` + +`kind: test`는 인용한 run의 reporter 결과에 같은 이름이 통과로 존재해야 한다. +`EVIDENCE_NOT_IN_RUN` = 매핑이 실제 실행과 어긋남, `EVIDENCE_UNVERIFIABLE` = run이 +`exit-only`라 이름 확인 불가. 둘 다 이름을 지어내지 말고 reporter를 붙여 재실행. + +최종 보고는 Outcome Brief의 사용자·성공 결과·비목표, 선택한 최소 경계, path별 변화, +검증, 남은 위험과 가역성을 먼저 쓴다. 증거 부록에 Oracle SHA-256·source hashes·마지막 +verify command/exit, 인용 runId와 실제 검증 command/PASS·FAIL 수, +`oracle-verify.mjs evidence` 출력 기록. 결과에 영향을 주는 commit·runtime/browser +version·locale/timezone·viewport/theme·role·clock/seed·데이터 초기화만 함께 기록. +비-N/A 행 미매핑 또는 revision 불일치면 GREEN을 발급하지 않는다. + +production diff의 비결정 소스는 `oracle-verify.mjs scan`을 변경 파일에 실행해 확인. +검출된 `Date.now`·`Math.random`·`crypto.randomUUID`·`toLocale`·`new Intl.`은 주입 +seam으로 바꾸거나 `oracle:nondeterminism <사유>` 주석으로 면제를 기록. + +### 최종 review 전이 + +`IMPLEMENTED_GREEN` 뒤 reviewer finding을 반영하면 init에서 선언한 필수 label을 전부 +다시 실행한다. 선택한 test run은 GREEN 때와 같은 command여야 하며, review artifact에 +blocking finding이 없어야 한다. + +```bash +node /scripts/oracle-verify.mjs review \ + --oracle .ai/oracles//oracle.md \ + --file .ai/oracles//findings-code-reviewer.json + +node /scripts/oracle-run.mjs transition \ + --dir .ai/oracles/ \ + --to REVIEW_VERIFIED \ + --run r-010 \ + --evidence .ai/oracles//evidence.json \ + --findings .ai/oracles//findings-code-reviewer.json +``` + +High risk는 GREEN 뒤 guard를 제거한 reported failing run과 영향받은 카드 행을 +`--mutation-run`·`--mutation-row`로 넘기고, guard 복구 뒤 같은 GREEN command를 review +run으로 다시 통과시킨다. runner는 GREEN 대비 production digest가 mutation에서 바뀌고 +review 전에 정확히 돌아왔는지도 검사한다. 둘 중 하나가 없으면 +`MUTATION_EVIDENCE_REQUIRED`, 순서·실패·reporter·digest 조건이 어긋나면 +`MUTATION_EVIDENCE_INVALID`. 두 번째 reviewer 파일도 `--intersect`로 함께 넘긴다. +critical/high finding은 한쪽에만 있어도 review를 막는다. + +```bash +node /scripts/oracle-run.mjs transition \ + --dir .ai/oracles/ \ + --to REVIEW_VERIFIED \ + --run r-012 \ + --evidence .ai/oracles//evidence.json \ + --findings .ai/oracles//findings-code-reviewer.json \ + --intersect .ai/oracles//findings-second-reviewer.json \ + --mutation-run r-011 \ + --mutation-row O3 +``` + +## 금지 + +[`common.md`](../common.md)의 공통 금지에 더해: + +- 카드의 정책·`Then`·`Never`·부작용 횟수 변경 +- ledger를 거치지 않은 실행을 증거로 보고 +- 거부된 전이를 우회하거나 `run-state.json`·`runs.jsonl`을 직접 편집 +- 예산을 계수하지 않고 보정·개선 라운드를 반복 +- assertion 약화, `test.skip`, `first()`/`nth()`로 오류 은폐 +- fixture에 기대 결과 인코딩 +- 임의 sleep 또는 단정 대상을 기다려 race 직렬화 +- 브라우저의 현재 동작을 기대값으로 채택 +- 유효하지 않은 RED를 근거로 production 수정 +- revision mismatch를 자동 재잠금해 기존 증거 재사용 +- 승인 없이 architecture 문서 생성·수정 또는 lock 갱신 +- 승인된 architecture 문서가 아닌 구현에 맞춰 문서·경계를 사후 변경 + +3라운드 후에도 GREEN이 아니면 남은 카드 위반과 실제 출력을 포함해 `FAIL`로 보고한다. +무한 자가개선은 하지 않는다. diff --git a/packages/frontend-oracle-design/skills/references/delivery/implementation-decision.md b/packages/frontend-oracle-design/skills/references/delivery/implementation-decision.md new file mode 100644 index 0000000..6e76dcc --- /dev/null +++ b/packages/frontend-oracle-design/skills/references/delivery/implementation-decision.md @@ -0,0 +1,45 @@ +# Delivery — Frontend 구현 결정 기록 + +`VALID_RED` 뒤 frontend production 수정 전, 실제 package version과 레포 규칙을 근거로 +아래 기록을 남긴다. 외부 best practice는 제품 정책을 정하거나 레포 계약을 덮어쓰지 +않는다. 해당 없는 항목은 이유와 함께 N/A. + +먼저 [`changeability.md`](../changeability.md)를 전부 읽고, 이번 diff에 material한 +근거만 Decision에 옮긴다. 원칙 본문 복제나 다섯 축 완주 선언으로 대신하지 않는다. + +기록 위치는 `.ai/oracles//implementation-decision.md`. 제품 정책 source가 +아니라 reviewer가 diff와 대조할 구현 reasoning 원문이다. 모든 축을 의례적으로 채우는 +boilerplate 대신 material한 trade-off만 기록한다. + +```markdown +### Implementation Decision + +- Target: React/Next.js/TanStack Query version과 router/runtime +- State ownership: server state, URL state, client state, derived state의 소유자 +- Server/Client boundary: server에 남길 것과 최소 client leaf +- Async boundary: initial loading, refetch, error, retry, mutation pending 처리 +- Hook boundary: 분리할 interaction/query 책임과 분리하지 않을 trivial logic +- Type contract: material한 입력·성공·실패·상태 전이와 불가능 상태, 또는 N/A 사유 +- Architecture: 영향 unit, 승인된 architecture 문서, 기존 관례·data/effect 경계와 + Oracle source hash +- Changeability: material한 Readability·Predictability·Cohesion·Coupling 판단, + 우선한 축과 희생한 축의 trade-off +- Side effects: request·navigation·storage·analytics·logging의 종류와 owner/boundary +- Simplicity: 기존 구현→platform/framework 기본 기능→설치 dependency→최소 local + code 중 처음 요구를 만족한 단계 +- Dependency: 새로 도입·교체한 framework/library가 있으면 해결하는 실제 문제, + 실제로 사용할 기능, 검토한 대안, 비용과 제거 경로; 없으면 N/A +- Design: Design Intent가 있으면 visual scope, component·token 재사용, typography, + responsive, motion·reduced motion, copy, signature와 버린 generic 선택; 없으면 N/A +- Accessibility: interactive UI의 semantic name·keyboard·focus·상태 전달 증거, 또는 N/A +- Performance: claim이 있으면 metric·budget·동일 환경 baseline/after runId, 없으면 N/A +- Public API: exported shared/package surface가 바뀌면 consumer·호환성·type/runtime·pack· + migration 계약, 아니면 N/A +- Sources: 적용한 레포 계약·공식 문서·휴리스틱 +- Rejected: 실제 검토했지만 적용하지 않은 대안, 관련 품질 축과 구체 이유 +``` + +선택이 카드의 관찰 결과를 바꾸거나 승인 기준과 충돌하면 구현하지 말고 +`NEEDS_DECISION` 복귀. 기술적으로 동등한 선택이면 +[`frontend-implementation.md`](../frontend-implementation.md)의 runtime 기준과 +[`changeability.md`](../changeability.md)의 변경 비용 기준으로 결정하고 계속한다. diff --git a/packages/frontend-oracle-design/skills/references/delivery/ledger.md b/packages/frontend-oracle-design/skills/references/delivery/ledger.md new file mode 100644 index 0000000..9648747 --- /dev/null +++ b/packages/frontend-oracle-design/skills/references/delivery/ledger.md @@ -0,0 +1,84 @@ +# Delivery — 권위·스케줄·판정 명령 ledger + +## 권위와 진입 조건 + +테스트 파일을 작성하기 직전에 설치된 `$test` 스킬을 이름으로 명시적으로 로드·호출해 +SKILL.md 전문과 판정 계약을 활성화한다. 파일을 참고만 하는 것으로 대체하지 않으며, +못 찾으면 `FAIL`. `$test`의 Oracle 게이트·테스트 작성·실행·`VALID_RED` 판정·보정 +예산을 그대로 따른다. Delivery 노드들은 production 구현과 자가피드백만 추가한다. +frontend production 수정 시 +[`frontend-implementation.md`](../frontend-implementation.md)도 전부 읽는다. + +**TDD 우선.** `ORACLE_READY` 뒤 테스트 먼저 작성·실행, `VALID_RED` 확보 전 production +작성·수정 금지. + +- Medium/High risk는 `ORACLE_READY` 카드가 필수다. Low fast path는 새 정책·카드가 + 없고 기존 승인 계약 안의 되돌리기 쉬운 수정에만 쓴다 — lane 계약은 + [`lanes/low-fast-path.md`](../lanes/low-fast-path.md). +- 새 카드와 의미가 바뀐 revision은 risk와 무관하게 Draft와 delta를 사용자에게 다시 + 확인받은 뒤 lock한다. +- 대상 레포의 `AGENTS.md`, `CLAUDE.md`, 테스트 스크립트, 인접 테스트, 필수 아키텍처 + 문서를 production 수정 전에 읽는다. +- React architecture 경계·state ownership·public API가 바뀔 때만 + [`architecture-contract.md`](../architecture-contract.md)의 명시적 문서 승인과 Oracle + local-source lock을 완료한다. 기존 승인 문서가 변경을 정확히 허용하면 경로와 source + hash만 기록. +- 기존 worktree 변경을 보존하고 관련 없는 파일을 수정하지 않는다. + +## 압축 스케줄 + +`policy`, `architecture`, `evidence`, `naming`, `review` 질문을 한 intake에 묶는다. +lock 전에는 독립적인 read-only 조사를 병렬 실행할 수 있지만, 모든 결과 변경 결정이 +끝난 뒤 final lock을 1회 만든다. Draft Oracle 사용자 승인은 직렬 gate다. screenshot· +direct-browser 실행은 사용자가 명시적으로 요청한 별도 `$frontend-visual-qa` 소유. + +`VALID_RED` 전에는 production을 수정하지 않는다. 이후 구현을 현재 agent가 직접 +수행할지, 위임할지, 병렬화할지는 이 계약이 강제하지 않는다. 선택한 실행 방식과 +무관하게 합친 production 기준으로 targeted GREEN을 1회 실행한다. + +targeted GREEN 뒤에는 root test·lint·format과 독립 review를 병렬 실행한다. 각 `exec`가 +runId reservation을 원자적으로 만들어 병렬에도 runId 충돌이 없다. 모든 결과가 +합류하고 유효 finding이 반영된 뒤 final verify를 직렬 1회 실행한다. 어느 한 결과만으로 +완료 처리 금지. + +## 판정 명령은 ledger로 실행한다 + +모든 판정용 실행은 bundled `oracle-run.mjs exec` 경유. `exec`는 실행 직전 lock을 +검증하고 runId·exit code·reporter 결과·env fingerprint를 append-only ledger에 남긴다. +ledger에 없는 실행은 증거가 아니다. + +```bash +node /scripts/oracle-run.mjs exec \ + --dir .ai/oracles/ --label red-1 \ + --report \ + -- <레포의 실제 테스트 명령> +``` + +- reporter 경로를 넘기면 테스트 이름·상태까지 기록되어 grade가 `reported`가 된다. + vitest·jest `--reporter=json --outputFile`, Playwright `--reporter=json`, + `node --test --test-reporter=json` 지원. +- reporter 없거나 형식 미상이면 `exit-only`로 격하 — 카드 행의 테스트 이름을 증거로 + 확정할 수 없으므로 가능하면 reporter를 붙인다. +- node:test 레포는 번들 `scripts/oracle-node-reporter.mjs` 사용. `--test-reporter`는 + module specifier라 `./` 또는 절대 경로로 넘긴다. +- 상태 전이는 `oracle-run.mjs transition`으로만 기록. 스크립트가 TDD 순서, 행별 + RED/GREEN evidence, `--required-label` 실행, 연속 통과 횟수, 테스트 약화, review + artifact와 lock을 검사하고 거부 사유를 코드로 출력한다. +- TDD 순서 판정 기준선 = `init` 시점 worktree. 에디터 캐시·agent runtime 파일이 계속 + 바뀌는 레포는 `init` 전에 worktree를 정리하거나 `--scan-root`로 범위를 대상 package로 + 좁힌다. 무관한 변경이 `PRODUCTION_TOUCHED_BEFORE_RED`를 만들면 범위를 좁히고 다시 + 시작하며, 검사를 끄지 않는다. +- 판정 범위: git 레포는 `git ls-files -c -o --exclude-standard`, 아니면 `node_modules`· + 빌드 산출물 제외 목록. **gitignore된 경로는 production 변경으로 세지 않는다.** 실제 + production인데 gitignore돼 있으면 `--scan-root`나 ignore 설정을 먼저 정리한다. + +### 이 하네스가 판정하지 못하는 것 + +- `evidence verify`는 인용 테스트 이름이 그 run에서 **실제로 통과했는지**만 본다. + 행↔테스트 대응의 타당성은 독립 reviewer 체크리스트 담당. +- `run-state.json`·`runs.jsonl`을 지울 수 있는 actor는 기준선·예산을 재시작할 수 있다. + `init`의 거부는 drift 검출이지 권한 통제가 아니다. 강한 통제는 `.ai/oracles/**`를 + CODEOWNERS·CI human approval로 보호. +- 비결정 소스 scan은 알려진 토큰 목록 기반 — 검출 실패를 무결성 증거로 쓰지 않는다. +- 예산 사용마다 `oracle-run.mjs budget --spend policy|harness|product --reason ...` 호출. + `BUDGET_EXHAUSTED`면 다른 예산으로 우회하지 않고 `FAIL`로 보고. diff --git a/packages/frontend-oracle-design/skills/references/delivery/red.md b/packages/frontend-oracle-design/skills/references/delivery/red.md new file mode 100644 index 0000000..eed7cfd --- /dev/null +++ b/packages/frontend-oracle-design/skills/references/delivery/red.md @@ -0,0 +1,64 @@ +# Delivery — 테스트로 계약 상태 확인 (`VALID_RED`) + +1. bundled `oracle-lock.mjs verify` 실행, revision·exit code 기록. `exec`·`transition`은 + 매 호출 같은 검증 자동 수행. +2. 카드의 모든 비-N/A 행을 관찰 가능한 테스트로 번역하고 test name을 `evidence.json` + 해당 행에 먼저 매핑. +3. network 경계는 레포가 이미 쓰는 test boundary를 우선한다. MSW가 설치됐거나 도입이 + 승인됐으면 MSW handler, 아니면 기존 transport seam. 테스트만 위해 + dependency를 조용히 추가하지 않는다. handler·예시 데이터는 경계를 소유한 가장 + 가까운 곳, FSD 배치는 [`fsd.md`](../fsd.md)의 `__mocks__/` 규칙. +4. 각 행의 `Then`, `Never`, 부작용 종류·횟수를 함께 assert. 요청 횟수·순서는 handler에서 + 관찰. +5. 테스트를 `exec`로 실제 실행. +6. 실패가 `$test`의 `VALID_RED` 술어를 만족하면 `oracle-verify.mjs red`로 지정 행의 + reported test 실패를 확인. 그 runId·행으로 전이를 기록하고, 전이 통과 뒤에만 + production 수정. + +카드가 커서 init에 milestone을 선언했다면 각 묶음 작성 즉시 `red:` label로 +reported RED를 실행한다. 모든 묶음이 실제로 실패한 후 마지막 milestone run을 `--run`으로 +인용해 전역 `VALID_RED`로 전이한다. 하나라도 없으면 `MILESTONE_RED_MISSING`이며 독립 +lock·상태를 만들지 않는다. milestone은 초기 RED 피드백만 앞당기고 GREEN·review는 기존 +전역 gate 그대로. + +```bash +node /scripts/oracle-run.mjs exec \ + --dir .ai/oracles/ \ + --label red:list \ + --report .ai/oracles//red-list.json \ + -- +``` + +```bash +node /scripts/oracle-verify.mjs red \ + --oracle .ai/oracles//oracle.md \ + --map .ai/oracles//evidence.json \ + --ledger .ai/oracles//runs.jsonl \ + --run r-001 \ + --row O1 +``` + +```bash +node /scripts/oracle-run.mjs transition \ + --dir .ai/oracles/ \ + --to VALID_RED \ + --run r-001 \ + --evidence .ai/oracles//evidence.json \ + --row O1 +``` + +`RED_EVIDENCE_UNVERIFIABLE`·`RED_EVIDENCE_MISSING`은 무관한 compile/setup 실패나 +exit-only run을 RED로 쓰지 못하게 한다. `PRODUCTION_TOUCHED_BEFORE_RED`는 테스트보다 +production을 먼저 건드렸다는 기계 증거 — 변경 파일을 되돌려 순서를 지키고 우회하지 +않는다. 전이는 이 시점의 테스트 파일 digest·assertion 수를 GREEN 게이트 기준선으로 +저장한다. + +`--harness-path` 등록 파일은 해당 bytes로 reported RED를 기록하기 전까지 변경 가능. +`VALID_RED` 후 다시 바꾸면 harness 예산 미사용 시 `HARNESS_BUDGET_REQUIRED`, 변경된 +bytes로 새 reported RED→GREEN 미실행 시 `HARNESS_RED_REQUIRED`로 완료 차단. production +파일을 harness로 등록해 순서 게이트를 우회하지 않는다. + +요청된 동작이 이미 GREEN이면 production을 억지로 바꾸거나 RED를 만들지 않는다. 기존 +구현이 카드를 충족한다는 증거를 기록하고 `--to IMPLEMENTED_GREEN --reason ...`으로 +전이한다. 이 경로는 `ORACLE_READY` 이후 production 변경이 없을 때만 통과. High risk는 +`$test`의 mutation 단계로 테스트 민감도를 별도 확인. diff --git a/packages/frontend-oracle-design/skills/references/frontend-implementation.md b/packages/frontend-oracle-design/skills/references/frontend-implementation.md index 5e082b2..069a780 100644 --- a/packages/frontend-oracle-design/skills/references/frontend-implementation.md +++ b/packages/frontend-oracle-design/skills/references/frontend-implementation.md @@ -7,14 +7,13 @@ Oracle Card의 관찰 가능한 계약을 React·Next.js·TanStack Query 코드 `NEEDS_DECISION`으로 복귀. 구현 전에 대상 package의 `package.json`, router 구조, framework config, 레포의 -`AGENTS.md`·`CLAUDE.md`·필수 아키텍처 문서를 읽는다. 권위 순서: +`AGENTS.md`·`CLAUDE.md`·필수 아키텍처 문서를 읽는다. 권위 순서는 +[`common.md`](common.md)의 공통 우선순위가 canonical이며, 이 문서 관할의 하위 출처만 +추가한다: -1. 보안·개인정보·법적·접근성·데이터 정합성의 강제 제약 -2. 승인된 Oracle Card와 기획·디자인·API 계약 -3. 대상 레포의 아키텍처·테스트·호환성 규칙 -4. **실제 설치 버전**의 React·Next.js·TanStack Query 공식 문서 -5. Vercel Engineering 같은 framework maintainer의 적용 가능한 휴리스틱 -6. 커뮤니티 전문가의 반례와 보완 의견 +- **실제 설치 버전**의 React·Next.js·TanStack Query 공식 문서 +- Vercel Engineering 같은 framework maintainer의 적용 가능한 휴리스틱 +- 커뮤니티 전문가의 반례와 보완 의견 하위 출처가 상위 출처를 덮어쓰지 않는다. 예: bundle guide가 barrel import를 피하라 해도 레포가 FSD slice public API import를 요구하면 레포 규칙을 따른다. 가용한 @@ -41,7 +40,7 @@ React production 변경이면 [`changeability.md`](changeability.md)도 전부 추론되는 타입은 반복하지 않는다. - 카드에 async·순서 역전·중복 제출·retry·다단계 상태 행이 있거나 client state· exported Props·shared/package API·trust boundary 타입 형태를 만들거나 바꾸면 - [`type-constraints.md`](type-constraints.md)를 전부 읽는다. 상태·이벤트는 카드 `O*` + [`types/state-ladder.md`](types/state-ladder.md)를 전부 읽는다. 상태·이벤트는 카드 `O*` 행에서 도출, 상태 설계 사다리와 discriminated union 계약을 따르고 카드에 없는 전이는 발명하지 않는다. 단순 toggle·독립 boolean 하나는 state machine으로 바꾸지 않는다. - `any`, 광범위한 assertion, 의미 없는 optional로 카드의 오류·상태 계약을 숨기지 않는다. @@ -67,7 +66,7 @@ query `select` 또는 render 중 파생으로. 데이터를 `useState`+`useEffect`+`useRef`로 직접 관리하면 freshness·중복 요청·취소를 전부 재구현하게 된다 — 기존 경계에 없는 데이터일 때만 직접 관리하고 사유를 Implementation Decision에 적는다. 직접 관리해도 상태 값에는 데이터만 담고 `retry` -같은 함수는 넣지 않는다 — [`type-constraints.md`](type-constraints.md)의 「상태는 +같은 함수는 넣지 않는다 — [`types/state-ladder.md`](types/state-ladder.md)의 「상태는 데이터, action은 형제」를 따른다. ## 2. 실행 위치를 고른다 @@ -143,7 +142,7 @@ shell을 막지 말고 느린 부분 가까이에 Suspense boundary. 컴포넌트는 현재 상태의 UI와 사용자 intent를 선언하고 DOM을 명령식으로 조작하지 않는다. 독립 boolean 여러 개로 불가능한 조합을 만들기보다 실제 UI 상태를 표현하는 최소 상태를 둔다. async·다단계 흐름의 상태 도출·exhaustiveness는 -[`type-constraints.md`](type-constraints.md)를 따르고, 새 state-machine dependency는 +[`types/state-ladder.md`](types/state-ladder.md)를 따르고, 새 state-machine dependency는 필요가 입증될 때만. micro-hook은 **짧은 코드**가 아니라 **작은 소유권 경계**다. UI와 비즈니스 로직 책임: diff --git a/packages/frontend-oracle-design/skills/references/implementation-loop.md b/packages/frontend-oracle-design/skills/references/implementation-loop.md deleted file mode 100644 index 40e94c1..0000000 --- a/packages/frontend-oracle-design/skills/references/implementation-loop.md +++ /dev/null @@ -1,375 +0,0 @@ -# Oracle 기반 구현·자가검증 루프 - -## 권위와 진입 조건 - -테스트 파일을 작성하기 직전에 설치된 `$test` 스킬을 이름으로 명시적으로 로드·호출해 -SKILL.md 전문과 판정 계약을 활성화한다. 파일을 참고만 하는 것으로 대체하지 않으며, -못 찾으면 `FAIL`. `$test`의 Oracle 게이트·테스트 작성·실행·`VALID_RED` 판정·보정 -예산을 그대로 따른다. 이 문서는 production 구현과 자가피드백만 추가한다. frontend -production 수정 시 [`frontend-implementation.md`](frontend-implementation.md)도 전부 -읽는다. - -**TDD 우선.** `ORACLE_READY` 뒤 테스트 먼저 작성·실행, `VALID_RED` 확보 전 production -작성·수정 금지. - -- Medium/High risk는 `ORACLE_READY` 카드가 필수다. Low fast path는 새 정책·카드가 - 없고 기존 승인 계약 안의 되돌리기 쉬운 수정에만 쓴다. -- 새 카드와 의미가 바뀐 revision은 risk와 무관하게 Draft와 delta를 사용자에게 다시 - 확인받은 뒤 lock한다. -- 대상 레포의 `AGENTS.md`, `CLAUDE.md`, 테스트 스크립트, 인접 테스트, 필수 아키텍처 - 문서를 production 수정 전에 읽는다. -- React architecture 경계·state ownership·public API가 바뀔 때만 - [`architecture-contract.md`](architecture-contract.md)의 명시적 문서 승인과 Oracle - local-source lock을 완료한다. 기존 승인 문서가 변경을 정확히 허용하면 경로와 source - hash만 기록. -- 기존 worktree 변경을 보존하고 관련 없는 파일을 수정하지 않는다. - -## 압축 스케줄 - -`policy`, `architecture`, `evidence`, `naming`, `review` 질문을 한 intake에 묶는다. -lock 전에는 독립적인 read-only 조사를 병렬 실행할 수 있지만, 모든 결과 변경 결정이 -끝난 뒤 final lock을 1회 만든다. Draft Oracle 사용자 승인은 직렬 gate다. screenshot· -direct-browser 실행은 사용자가 명시적으로 요청한 별도 `$frontend-visual-qa` 소유. - -`VALID_RED` 전에는 production을 수정하지 않는다. 이후 구현을 현재 agent가 직접 -수행할지, 위임할지, 병렬화할지는 이 계약이 강제하지 않는다. 선택한 실행 방식과 -무관하게 합친 production 기준으로 targeted GREEN을 1회 실행한다. - -targeted GREEN 뒤에는 root test·lint·format과 독립 review를 병렬 실행한다. 각 `exec`가 -runId reservation을 원자적으로 만들어 병렬에도 runId 충돌이 없다. 모든 결과가 -합류하고 유효 finding이 반영된 뒤 final verify를 직렬 1회 실행한다. 어느 한 결과만으로 -완료 처리 금지. - -## 0. 판정 명령은 ledger로 실행한다 - -모든 판정용 실행은 bundled `oracle-run.mjs exec` 경유. `exec`는 실행 직전 lock을 -검증하고 runId·exit code·reporter 결과·env fingerprint를 append-only ledger에 남긴다. -ledger에 없는 실행은 증거가 아니다. - -```bash -node /scripts/oracle-run.mjs exec \ - --dir .ai/oracles/ --label red-1 \ - --report \ - -- <레포의 실제 테스트 명령> -``` - -- reporter 경로를 넘기면 테스트 이름·상태까지 기록되어 grade가 `reported`가 된다. - vitest·jest `--reporter=json --outputFile`, Playwright `--reporter=json`, - `node --test --test-reporter=json` 지원. -- reporter 없거나 형식 미상이면 `exit-only`로 격하 — 카드 행의 테스트 이름을 증거로 - 확정할 수 없으므로 가능하면 reporter를 붙인다. -- node:test 레포는 번들 `scripts/oracle-node-reporter.mjs` 사용. `--test-reporter`는 - module specifier라 `./` 또는 절대 경로로 넘긴다. -- 상태 전이는 `oracle-run.mjs transition`으로만 기록. 스크립트가 TDD 순서, 행별 - RED/GREEN evidence, `--required-label` 실행, 연속 통과 횟수, 테스트 약화, review - artifact와 lock을 검사하고 거부 사유를 코드로 출력한다. -- TDD 순서 판정 기준선 = `init` 시점 worktree. 에디터 캐시·agent runtime 파일이 계속 - 바뀌는 레포는 `init` 전에 worktree를 정리하거나 `--scan-root`로 범위를 대상 package로 - 좁힌다. 무관한 변경이 `PRODUCTION_TOUCHED_BEFORE_RED`를 만들면 범위를 좁히고 다시 - 시작하며, 검사를 끄지 않는다. -- 판정 범위: git 레포는 `git ls-files -c -o --exclude-standard`, 아니면 `node_modules`· - 빌드 산출물 제외 목록. **gitignore된 경로는 production 변경으로 세지 않는다.** 실제 - production인데 gitignore돼 있으면 `--scan-root`나 ignore 설정을 먼저 정리한다. - -### 이 하네스가 판정하지 못하는 것 - -- `evidence verify`는 인용 테스트 이름이 그 run에서 **실제로 통과했는지**만 본다. - 행↔테스트 대응의 타당성은 독립 reviewer 체크리스트 담당. -- `run-state.json`·`runs.jsonl`을 지울 수 있는 actor는 기준선·예산을 재시작할 수 있다. - `init`의 거부는 drift 검출이지 권한 통제가 아니다. 강한 통제는 `.ai/oracles/**`를 - CODEOWNERS·CI human approval로 보호. -- 비결정 소스 scan은 알려진 토큰 목록 기반 — 검출 실패를 무결성 증거로 쓰지 않는다. -- 예산 사용마다 `oracle-run.mjs budget --spend policy|harness|product --reason ...` 호출. - `BUDGET_EXHAUSTED`면 다른 예산으로 우회하지 않고 `FAIL`로 보고. - -## 1. 테스트로 계약 상태 확인 - -1. bundled `oracle-lock.mjs verify` 실행, revision·exit code 기록. `exec`·`transition`은 - 매 호출 같은 검증 자동 수행. -2. 카드의 모든 비-N/A 행을 관찰 가능한 테스트로 번역하고 test name을 `evidence.json` - 해당 행에 먼저 매핑. -3. network 경계는 레포가 이미 쓰는 test boundary를 우선한다. MSW가 설치됐거나 도입이 - 승인됐으면 MSW handler, 아니면 기존 transport seam. 테스트만 위해 - dependency를 조용히 추가하지 않는다. handler·예시 데이터는 경계를 소유한 가장 - 가까운 곳, FSD 배치는 [`fsd.md`](fsd.md)의 `__mocks__/` 규칙. -4. 각 행의 `Then`, `Never`, 부작용 종류·횟수를 함께 assert. 요청 횟수·순서는 handler에서 - 관찰. -5. 테스트를 `exec`로 실제 실행. -6. 실패가 `$test`의 `VALID_RED` 술어를 만족하면 `oracle-verify.mjs red`로 지정 행의 - reported test 실패를 확인. 그 runId·행으로 전이를 기록하고, 전이 통과 뒤에만 - production 수정. - -카드가 커서 init에 milestone을 선언했다면 각 묶음 작성 즉시 `red:` label로 -reported RED를 실행한다. 모든 묶음이 실제로 실패한 후 마지막 milestone run을 `--run`으로 -인용해 전역 `VALID_RED`로 전이한다. 하나라도 없으면 `MILESTONE_RED_MISSING`이며 독립 -lock·상태를 만들지 않는다. milestone은 초기 RED 피드백만 앞당기고 GREEN·review는 기존 -전역 gate 그대로. - -```bash -node /scripts/oracle-run.mjs exec \ - --dir .ai/oracles/ \ - --label red:list \ - --report .ai/oracles//red-list.json \ - -- -``` - -```bash -node /scripts/oracle-verify.mjs red \ - --oracle .ai/oracles//oracle.md \ - --map .ai/oracles//evidence.json \ - --ledger .ai/oracles//runs.jsonl \ - --run r-001 \ - --row O1 -``` - -```bash -node /scripts/oracle-run.mjs transition \ - --dir .ai/oracles/ \ - --to VALID_RED \ - --run r-001 \ - --evidence .ai/oracles//evidence.json \ - --row O1 -``` - -`RED_EVIDENCE_UNVERIFIABLE`·`RED_EVIDENCE_MISSING`은 무관한 compile/setup 실패나 -exit-only run을 RED로 쓰지 못하게 한다. `PRODUCTION_TOUCHED_BEFORE_RED`는 테스트보다 -production을 먼저 건드렸다는 기계 증거 — 변경 파일을 되돌려 순서를 지키고 우회하지 -않는다. 전이는 이 시점의 테스트 파일 digest·assertion 수를 GREEN 게이트 기준선으로 -저장한다. - -`--harness-path` 등록 파일은 해당 bytes로 reported RED를 기록하기 전까지 변경 가능. -`VALID_RED` 후 다시 바꾸면 harness 예산 미사용 시 `HARNESS_BUDGET_REQUIRED`, 변경된 -bytes로 새 reported RED→GREEN 미실행 시 `HARNESS_RED_REQUIRED`로 완료 차단. production -파일을 harness로 등록해 순서 게이트를 우회하지 않는다. - -요청된 동작이 이미 GREEN이면 production을 억지로 바꾸거나 RED를 만들지 않는다. 기존 -구현이 카드를 충족한다는 증거를 기록하고 `--to IMPLEMENTED_GREEN --reason ...`으로 -전이한다. 이 경로는 `ORACLE_READY` 이후 production 변경이 없을 때만 통과. High risk는 -`$test`의 mutation 단계로 테스트 민감도를 별도 확인. - -## 2. Frontend 구현 결정 - -`VALID_RED` 뒤 frontend production 수정 전, 실제 package version과 레포 규칙을 근거로 -아래 기록을 남긴다. 외부 best practice는 제품 정책을 정하거나 레포 계약을 덮어쓰지 -않는다. 해당 없는 항목은 이유와 함께 N/A. - -먼저 [`changeability.md`](changeability.md)를 전부 읽고, 이번 diff에 material한 근거만 -Decision에 옮긴다. 원칙 본문 복제나 다섯 축 완주 선언으로 대신하지 않는다. - -기록 위치는 `.ai/oracles//implementation-decision.md`. 제품 정책 source가 -아니라 reviewer가 diff와 대조할 구현 reasoning 원문이다. 모든 축을 의례적으로 채우는 -boilerplate 대신 material한 trade-off만 기록한다. - -```markdown -### Implementation Decision - -- Target: React/Next.js/TanStack Query version과 router/runtime -- State ownership: server state, URL state, client state, derived state의 소유자 -- Server/Client boundary: server에 남길 것과 최소 client leaf -- Async boundary: initial loading, refetch, error, retry, mutation pending 처리 -- Hook boundary: 분리할 interaction/query 책임과 분리하지 않을 trivial logic -- Type contract: material한 입력·성공·실패·상태 전이와 불가능 상태, 또는 N/A 사유 -- Architecture: 영향 unit, 승인된 architecture 문서, 기존 관례·data/effect 경계와 - Oracle source hash -- Changeability: material한 Readability·Predictability·Cohesion·Coupling 판단, - 우선한 축과 희생한 축의 trade-off -- Side effects: request·navigation·storage·analytics·logging의 종류와 owner/boundary -- Simplicity: 기존 구현→platform/framework 기본 기능→설치 dependency→최소 local - code 중 처음 요구를 만족한 단계 -- Dependency: 새로 도입·교체한 framework/library가 있으면 해결하는 실제 문제, - 실제로 사용할 기능, 검토한 대안, 비용과 제거 경로; 없으면 N/A -- Design: Design Intent가 있으면 visual scope, component·token 재사용, typography, - responsive, motion·reduced motion, copy, signature와 버린 generic 선택; 없으면 N/A -- Accessibility: interactive UI의 semantic name·keyboard·focus·상태 전달 증거, 또는 N/A -- Performance: claim이 있으면 metric·budget·동일 환경 baseline/after runId, 없으면 N/A -- Public API: exported shared/package surface가 바뀌면 consumer·호환성·type/runtime·pack· - migration 계약, 아니면 N/A -- Sources: 적용한 레포 계약·공식 문서·휴리스틱 -- Rejected: 실제 검토했지만 적용하지 않은 대안, 관련 품질 축과 구체 이유 -``` - -선택이 카드의 관찰 결과를 바꾸거나 승인 기준과 충돌하면 구현하지 말고 -`NEEDS_DECISION` 복귀. 기술적으로 동등한 선택이면 `frontend-implementation.md`의 -runtime 기준과 `changeability.md`의 변경 비용 기준으로 결정하고 계속한다. - -## 3. 최소 구현·셀프피드백 - -최대 3라운드. 한 라운드: - -1. 실패한 카드 행 하나 또는 같은 root cause의 행 묶음 선택. -2. 관련 호출 경로를 끝까지 추적해 모든 호출자가 공유하는 원인 파악. -3. 해당 계약만 만족하는 최소 production 변경 작성. -4. 실패했던 targeted test 재실행. -5. 영향 범위 테스트 실행. -6. 결과를 분류하고 다음 행동 결정. - -- `POLICY_GAP` → 카드 현재본과 질문을 출력하고 `NEEDS_DECISION` -- `EVIDENCE_GAP` → 잠긴 카드 범위 안에서 누락 테스트·증거만 추가 -- `HARNESS_DEFECT` → sibling `test` skill 허용 항목과 공용 2회 예산 안에서만 보정 -- `PRODUCT_DEFECT` → 같은 카드 행을 유지하고 최소 수정 후 재실행 -- `ENVIRONMENT_DEFECT` → production을 건드리지 않고 `FAIL` -- `NON_ORACLE_OPINION` → 기록하되 정책·assertion·완료 상태를 바꾸지 않음 - -revision mismatch는 피드백 분류 대상이 아니다. 기존 증거를 즉시 폐기하고 -`oracle-card.md`의 lock 규칙대로 `NEEDS_DECISION` 또는 `FAIL`로 이동. - -매 라운드 기록: - -| 라운드 | 카드 행 | 실패 가설 | 최소 변경 | 실제 실행 결과 | 다음 판단 | -| ------ | ------- | --------- | --------- | -------------- | --------- | - -## 4. GREEN 게이트 - -카드 테스트 통과 후 init에서 `--required-label`로 고정한 레포 검증을 각 label의 -`exec`로 실제 실행한다. - -1. targeted test -2. 영향 범위 test -3. typecheck와 lint -4. Oracle source lock verify 및 레포에 존재하는 구조 검증 명령 -5. 루트 또는 패키지 필수 test/build - -성능 요구·개선 claim이 있으면 동일 조건 baseline/after를 검사하는 기존 repo 명령을 -`performance` 필수 label로 추가. exported shared/package API가 바뀌면 레포가 이미 -제공하는 type test, runtime test, pack/export·changeset 검증만 필수 label로 추가. -해당 없는 작업에 이 명령이나 새 dependency를 만들지 않는다. - -레포 규칙에 정의된 명령이 우선이다. 실행하지 않은 검증을 통과로 보고하지 않는다. -문서화된 명령이 없으면 package scripts를 읽어 targeted + 가장 가까운 package 검증을 -실행한다. 필수 root 명령이 없거나 무관한 기존 실패가 있으면 원문과 영향을 분리해 -보고하고 GREEN으로 숨기지 않는다. - -그다음 `--to IMPLEMENTED_GREEN` 전이를 시도한다. 기계 검사: - -- `ORACLE_CHANGED` — 카드·source bytes가 잠긴 값과 다름 → 증거를 폐기하고 `NEEDS_DECISION` -- `RUN_NOT_GREEN` — 인용한 run이 통과하지 않음 → 실제 통과 run을 만들고 인용 -- `EVIDENCE_REQUIRED` — evidence manifest 없이 상태 전이를 시도함 → 잠긴 카드 전 행을 매핑하고 `--evidence`로 인용 -- `REQUIRED_RUN_MISSING` — 선언한 필수 label의 최신 통과가 없음 → 해당 repo 명령을 `exec --label`로 다시 실행 -- `FLAKINESS_GATE` — 같은 명령의 연속 통과가 risk 필요 횟수에 못 미침 → 같은 명령을 그대로 다시 실행해 연속 통과를 확보 -- `TEST_WEAKENED` — RED 기준선 대비 assertion 감소·금지 토큰·삭제 → 테스트를 원래 강도로 되돌린다 -- `ENV_DRIFT`(경고) — RED와 GREEN의 실행 환경이 다름 → 환경 차이가 결과를 바꿨는지 확인하고 보고에 남긴다 - -flakiness 필요 횟수는 Low 1회, Medium 2회, High 3회. 재실행으로 통과를 뽑는 게 아니라 -**같은 명령이 반복해도 결정론적으로 통과함**을 보이는 절차다. 실패가 섞이면 -`HARNESS_DEFECT`로 분류하고 조용히 다시 굴리지 않는다. - -`TEST_WEAKENED` 금지 토큰: `test.skip`·`it.skip`·`describe.skip`·`.only(`· -`waitForTimeout(`·`toBeTruthy(`·`toBeFalsy(`·`.first()`·`.nth(`·`setTimeout(`과 -screenshot 허용치(`maxDiffPixels`·`maxDiffPixelRatio`·`threshold`) 상향. - -선택한 GREEN run은 parsed reporter가 있는 카드 test run이어야 한다. 별도 lint· -typecheck·build는 각각 선언한 label로 기록. 전이는 모든 필수 label과 evidence -manifest를 직접 검사하며, 전이가 통과해야 `IMPLEMENTED_GREEN`이다. - -### Evidence manifest - -증거 매핑은 산문이 아니라 `.ai/oracles//evidence.json`으로 관리하고 기계로 -검증한다. - -```json -{ - "schemaVersion": 1, - "rows": { - "O1": { "kind": "test", "name": "저장 > pending 표시와 POST 1회" }, - "O2": { "kind": "reviewer", "finding": "f-3", "role": "code-reviewer" }, - "O3": { "kind": "na", "reason": "이 기능에 취소 경로가 없다", "source": "S1" }, - "D1": { "kind": "visual", "artifact": "visual-qa/v-001/evidence.json" }, - "D2": { "kind": "reviewer", "finding": "d-1", "role": "designer" } - } -} -``` - -```bash -node /scripts/oracle-verify.mjs evidence \ - --oracle .ai/oracles//oracle.md \ - --map .ai/oracles//evidence.json \ - --ledger .ai/oracles//runs.jsonl \ - --run r-007 \ - --phase green -``` - -`D*` 행 owner: `HARD → test`, `RELATIONAL → visual | pending`, `JUDGMENT → designer -reviewer`. visual `pending`은 GREEN 증거 검증에서 미검증 항목으로 보고되지만 review -증거 검증에서는 `EVIDENCE_PENDING`으로 완료를 차단한다. - -GREEN 전이는 같은 manifest를 필수 입력으로 받는다. - -```bash -node /scripts/oracle-run.mjs transition \ - --dir .ai/oracles/ \ - --to IMPLEMENTED_GREEN \ - --run r-007 \ - --evidence .ai/oracles//evidence.json -``` - -`kind: test`는 인용한 run의 reporter 결과에 같은 이름이 통과로 존재해야 한다. -`EVIDENCE_NOT_IN_RUN` = 매핑이 실제 실행과 어긋남, `EVIDENCE_UNVERIFIABLE` = run이 -`exit-only`라 이름 확인 불가. 둘 다 이름을 지어내지 말고 reporter를 붙여 재실행. - -최종 보고는 Outcome Brief의 사용자·성공 결과·비목표, 선택한 최소 경계, path별 변화, -검증, 남은 위험과 가역성을 먼저 쓴다. 증거 부록에 Oracle SHA-256·source hashes·마지막 -verify command/exit, 인용 runId와 실제 검증 command/PASS·FAIL 수, -`oracle-verify.mjs evidence` 출력 기록. 결과에 영향을 주는 commit·runtime/browser -version·locale/timezone·viewport/theme·role·clock/seed·데이터 초기화만 함께 기록. -비-N/A 행 미매핑 또는 revision 불일치면 GREEN을 발급하지 않는다. - -production diff의 비결정 소스는 `oracle-verify.mjs scan`을 변경 파일에 실행해 확인. -검출된 `Date.now`·`Math.random`·`crypto.randomUUID`·`toLocale`·`new Intl.`은 주입 -seam으로 바꾸거나 `oracle:nondeterminism <사유>` 주석으로 면제를 기록. - -### 최종 review 전이 - -`IMPLEMENTED_GREEN` 뒤 reviewer finding을 반영하면 init에서 선언한 필수 label을 전부 -다시 실행한다. 선택한 test run은 GREEN 때와 같은 command여야 하며, review artifact에 -blocking finding이 없어야 한다. - -```bash -node /scripts/oracle-verify.mjs review \ - --oracle .ai/oracles//oracle.md \ - --file .ai/oracles//findings-code-reviewer.json - -node /scripts/oracle-run.mjs transition \ - --dir .ai/oracles/ \ - --to REVIEW_VERIFIED \ - --run r-010 \ - --evidence .ai/oracles//evidence.json \ - --findings .ai/oracles//findings-code-reviewer.json -``` - -High risk는 GREEN 뒤 guard를 제거한 reported failing run과 영향받은 카드 행을 -`--mutation-run`·`--mutation-row`로 넘기고, guard 복구 뒤 같은 GREEN command를 review -run으로 다시 통과시킨다. runner는 GREEN 대비 production digest가 mutation에서 바뀌고 -review 전에 정확히 돌아왔는지도 검사한다. 둘 중 하나가 없으면 -`MUTATION_EVIDENCE_REQUIRED`, 순서·실패·reporter·digest 조건이 어긋나면 -`MUTATION_EVIDENCE_INVALID`. 두 번째 reviewer 파일도 `--intersect`로 함께 넘긴다. -critical/high finding은 한쪽에만 있어도 review를 막는다. - -```bash -node /scripts/oracle-run.mjs transition \ - --dir .ai/oracles/ \ - --to REVIEW_VERIFIED \ - --run r-012 \ - --evidence .ai/oracles//evidence.json \ - --findings .ai/oracles//findings-code-reviewer.json \ - --intersect .ai/oracles//findings-second-reviewer.json \ - --mutation-run r-011 \ - --mutation-row O3 -``` - -## 금지 - -- 카드의 정책·`Then`·`Never`·부작용 횟수 변경 -- ledger를 거치지 않은 실행을 증거로 보고 -- 거부된 전이를 우회하거나 `run-state.json`·`runs.jsonl`을 직접 편집 -- 예산을 계수하지 않고 보정·개선 라운드를 반복 -- assertion 약화, `test.skip`, `first()`/`nth()`로 오류 은폐 -- fixture에 기대 결과 인코딩 -- 임의 sleep 또는 단정 대상을 기다려 race 직렬화 -- 브라우저의 현재 동작을 기대값으로 채택 -- 유효하지 않은 RED를 근거로 production 수정 -- revision mismatch를 자동 재잠금해 기존 증거 재사용 -- 승인 없이 architecture 문서 생성·수정 또는 lock 갱신 -- 승인된 architecture 문서가 아닌 구현에 맞춰 문서·경계를 사후 변경 - -3라운드 후에도 GREEN이 아니면 남은 카드 위반과 실제 출력을 포함해 `FAIL`로 보고한다. -무한 자가개선은 하지 않는다. diff --git a/packages/frontend-oracle-design/skills/references/lanes/low-fast-path.md b/packages/frontend-oracle-design/skills/references/lanes/low-fast-path.md new file mode 100644 index 0000000..a3a155b --- /dev/null +++ b/packages/frontend-oracle-design/skills/references/lanes/low-fast-path.md @@ -0,0 +1,42 @@ +# Low fast path — lane 계약 + +진입 시 risk 판정이 Low인 작업의 유일한 로드 노드다. 이 lane에서는 이 파일 **하나만** +읽고 다른 reference 노드는 로드하지 않는다 — 그래프의 Oracle lane(`common` 이하)은 +명시적 Oracle 요청 또는 Medium/High 판정에서만 연다. + +## 진입 조건 — 전부 만족할 때만 + +- 새 정책·카드·architecture 결정이 없다. +- 기존 승인 계약 안의 변경이다: 되돌리기 쉬운 copy·token·고립 CSS·명확한 회귀 수정. +- false GREEN의 최악 피해가 작다(정적 표시, 순수 동기 helper 수준). 부작용이 위험하면 + UI가 단순해도 Low가 아니다. + +## 절차 + +1. risk 판정과 사유를 한 줄 기록한다 (예: `risk: low — 승인된 문구 계약 안의 copy 수정`). +2. 변경을 수행하고 관련 테스트와 레포 필수 검증(lint·typecheck·targeted test)만 + 실행한다. +3. 결과를 보고한다: 변경 path, 실행한 검증 명령과 실제 결과, risk 사유. + +## 하지 않는 것 + +- Oracle Card·revision lock·run ledger·상태 파일·evidence manifest 생성 +- Grill·사용자 카드 확인·독립 subagent 리뷰 +- 다른 reference 노드 로드 + +절차를 생략하는 lane이지 검증을 생략하는 lane이 아니다 — 레포 필수 검증은 그대로 +실행하고, 실행하지 않은 검증을 통과로 보고하지 않는다. + +## 승격 — Low 실격 조건 + +작업 중 아래 중 하나라도 나타나면 그 즉시 Low 실격이다. fast path를 계속 타지 말고 +지금까지의 변경 내용을 보고한 뒤 Oracle lane으로 승격한다 +(`common.md` → `card/` 노드, Medium 절차). + +- 결과를 바꾸는 정책 질문이 생겼다 (`Then`·`Never`·부작용 횟수를 정해야 한다) +- 새 상태·form·async 흐름·responsive 구조가 필요해졌다 +- architecture 경계·state ownership·public API를 바꾸게 됐다 +- mutation·권한·데이터 정합성 등 부작용 위험이 드러났다 + +승격 후에는 이미 만든 변경을 기정사실로 두지 않는다 — 카드 절차가 정한 정책과 +어긋나면 되돌린다. diff --git a/packages/frontend-oracle-design/skills/references/oracle-card.md b/packages/frontend-oracle-design/skills/references/oracle-card.md deleted file mode 100644 index 9383921..0000000 --- a/packages/frontend-oracle-design/skills/references/oracle-card.md +++ /dev/null @@ -1,458 +0,0 @@ -# Oracle Card 설계 계약 - -## 0. 외부 기준 게이트 - -Risk·Grill 전에 사용자가 제공했거나 레포가 승인된 기준으로 지정한 자료를 찾아 전부 -읽는다. 우선순위: - -1. 보안·개인정보·법적 제약·접근성·금융 및 데이터 정합성 -2. 사용자의 명시적 행동 계약과 공개 호환성 -3. 레포의 필수 아키텍처·API·테스트 계약 -4. 승인된 기획서·PRD·수용 기준·디자인 시스템·Figma 원본의 해당 관할 -5. production 코드·기존 테스트·브라우저 관찰은 조사 증거일 뿐 정책 출처가 아님 - -카드 상단에 이번 변경의 제품 결과와 범위를 기록한다. KPI가 없으면 수치를 발명하지 -않고 사용자가 관찰할 수 있는 성공 결과를 쓴다. - -```markdown -## Outcome Brief - -- Actor and context: 누가 어떤 상황에서 사용하는가 -- Observable success: 관찰 가능한 성공 결과 -- Non-goals: 이번 변경에서 하지 않을 일 -- Worst regression: false GREEN의 가장 큰 피해 -- Reversibility: 되돌리는 방법 또는 N/A 사유 -- Sources: S1, S2 -``` - -### Requested mechanism check — 수단과 결과 분리 - -사용자가 구체적 수단(화면·필드·버튼·자동화·조건)을 요청했지만 의도한 결과나 -사용자가 불명확하면 Outcome Brief에 다음을 함께 기록한다. 수단과 결과가 이미 -일치하면 이 소절 없이 그대로 진행한다. - -- Requested mechanism: 사용자가 요청한 구체적 수단 -- Intended outcome: 실제로 해결하려는 사용자·비즈니스 문제 -- Smallest reversible scope: 그 결과를 확인할 수 있는 최소 가역 범위 -- Deferred scope: 검증 전에는 만들지 않을 범위 — Non-goals에 사유와 함께 기록 - -규칙: - -- 더 작은 대안은 Draft Oracle에 제시만 한다. scope 축소는 사용자의 명시적 - 승인으로만 확정하며 에이전트가 임의로 줄이지 않는다. -- 이 검토를 `mandatory-constraint`(보안·개인정보·법·접근성·데이터 정합성) 생략 - 근거로 쓰지 않는다. - -```markdown -## Source Registry - -| ID | Kind | 관할 | 기준 | 위치·version | 승인 상태 | -| --- | -------------------- | ------------------------ | ------------- | ----------------------------------- | --------- | -| S1 | product-policy | 비즈니스 결과 | PRD | docs/profile.md#save-flow, revision | approved | -| S2 | product-policy | UI·문구·interaction | Figma | file/page/frame/version | approved | -| S3 | project-constraint | payload·오류·idempotency | API 계약 | endpoint/version | approved | -| S4 | mandatory-constraint | 접근성·토큰 | 디자인 시스템 | 문서 위치/version | approved | -``` - -허용 `Kind` 4종: - -- `product-policy`: 사용자 답변과 승인된 PRD·Figma처럼 제품 결과를 정하는 자료 -- `mandatory-constraint`: 보안·개인정보·법·접근성·데이터 정합성처럼 제품 선호로 낮출 - 수 없는 제약 -- `project-constraint`: 저장소의 공개 API·architecture·테스트·호환성 계약 -- `implementation-reference`: 실제 설치 버전의 공식 문서·구현 휴리스틱. 제품 결과를 - 정하지 못한다. - -규칙: - -- Figma는 원본 파일의 정확한 page·frame·variant를 직접 확인. 열 수 없으면 기억·유사 - 스크린샷으로 대체하지 않는다. -- 외부 기준이 없으면 `N/A — 제공되거나 승인된 외부 기준 없음` 기록. -- 외부 기준끼리 또는 사용자 답변과 충돌, 필수 기준 접근 불가 → 충돌 위치·영향 정책 - 제시 후 `NEEDS_DECISION`. -- 카드는 외부 기준의 실행 가능한 번역이다. 작성 후 외부 기준의 상태·문구·interaction· - 부작용 요구가 누락·왜곡되지 않았는지 대조한다. -- 기준은 자신의 관할 안에서만 우선한다. Figma로 idempotency, API 계약으로 시각 - 레이아웃을 정하지 않는다. 관할이 겹치거나 불명확하면 `NEEDS_DECISION`. -- `mandatory-constraint`와 다른 source 충돌 시 보안·접근성·정합성을 낮춰 통과하지 - 않는다. 충돌과 안전한 대안 제시 후 `NEEDS_DECISION`. -- 기준의 revision/version이 바뀌면 기존 `ORACLE_READY`를 무효화하고 다시 대조. - -## 1. UI 디자인 의도 게이트 - -새 UI·redesign 또는 보이는 layout·palette·typography·copy·motion·responsive behavior· -visual identity 변경이면 카드 작성 전 [`visual-design.md`](visual-design.md)를 전부 -읽는다. 기존 시각 결과 유지 작업은 `behavior-only`와 N/A 사유만 기록. - -- `local`·`identity-shaping`이면 승인된 시각 기준을 Design Intent와 `D*` Visual - Contract 행으로 같은 카드에 포함. -- AI·디자인 skill의 Design Proposal은 사용자 승인 전 정책 출처가 아니다. -- 출처 있는 시각 요구마다 `HARD`·`RELATIONAL`·`JUDGMENT` 증거 계층을 정한다. -- **Design Change Confirmation 필수.** `local`·`identity-shaping`은 변경 축과 전체 - Design Intent를 보여주고 명시적 확인 전 잠그지 않는다. 승인된 디자인 source도 확인을 - 대신하지 않으며, 미확인이면 `NEEDS_DECISION`. -- `identity-shaping`은 두 번의 설계 pass까지 마친 제안으로 확인받는다. -- 승인된 로컬 디자인 자료는 `--source`로 함께 잠그고, 원격 자료는 정확한 version을 - Design Intent와 Source Registry에 기록. - -시각 범위는 기능 Risk를 대신하지 않는다. 두 판정은 별도로 기록. - -## 2. Risk 판정 - -코드 복잡도가 아니라 **false GREEN의 최악 피해**로 판정한다. UI가 단순해도 부작용이 -위험하면 High다. - -- Low (정적 표시, 순수 동기 helper) → 카드 생략 가능 — risk와 사유 한 줄 기록 -- Medium (조회, 검색, 폼, 캐시) → 카드 작성 -- High (결제, 주문, 저장, 삭제, 권한, 외부 mutation) → 카드 작성 + 사용자 카드 확인 필수 - -## 3. 정책 Grill — 시스템 디자인 인터뷰 - -**답에 따라 예상 결과나 테스트가 달라지는 질문만** 한다. - -- 라운드당 3~5개, 최대 2라운드 -- 각 질문에 추천안과 근거 동봉 -- 레포 문서·승인된 명세에 답이 있으면 질문하지 않음 -- 추천안은 결정이 아니며, 답이 없으면 default로 적용하지 않음 -- 2라운드 후에도 결과를 바꾸는 질문이 남으면 `NEEDS_DECISION` - -### Phase 순서 - -질문은 **앞 답이 뒤 가지를 죽이는 순서**로 한다. 질문 전에 레포·PRD·Figma·API -문서를 먼저 탐색해 답이 있는 질문을 제거한다. 코드 관찰로 얻은 답은 -`project-constraint` 후보일 뿐 제품 정책 출처가 아니다. - -- P1 결과: actor·상황, 관찰 가능한 성공, 비목표, 최악 회귀·가역성, 플랫폼·디바이스·offline·다국어 → Outcome Brief -- P2 부작용·위험: 서버 상태 변경 여부, 돈·데이터·권한 피해 → Risk lane -- P3 데이터·아키텍처: source of truth, stale 허용, 기존 상태 소유자(query·router·form), 핵심 entity와 소유 컴포넌트 → architecture intake, State ownership -- P4 API 계약: 스펙 소스 위치·version, error code별 UI 결과·재시도, idempotency key 주체, pagination 끝 판정 → Source Registry, `API contract` 절 -- P5 경합·비동기: 아래 "자주 필요한 질문" → 카드 `O*` 행 -- P6 상태 모델: 상태 수·불가능한 전이 → State Model(opt-in) -- P7 시각: visual scope, 로딩·빈·에러 표시, 접근성 확인 → Design Intent·`D*` 행 -- P8 성능·운영: 성능 목표 수치·측정법, rollout·flag → performance 게이트 - -가지치기: - -- P1에서 Low 판정이면 grill을 끝내고 fast path로 간다. -- endpoint가 없으면 P4, mutation·async가 없으면 P5, `behavior-only`면 P7, 성능 - claim이 없으면 P8을 통째로 건너뛴다. -- 기능이 설치된 `frontend-system-design` reference와 매칭되면 그 문서의 결정 - 포인트를 P4·P5 질문으로 변환해 일반 질문을 대체한다. -- API 스펙 소스가 없으면 P4를 추측으로 채우지 않는다. 대신 카드 행에서 draft - schema를 도출해 Draft Oracle과 함께 제시하고, 명시 승인 시 `project-constraint` - source로 등록해 함께 잠근다. 승인이 없으면 `NEEDS_DECISION`. - -라운드 구성: Round 1 = P1~P3 생존 질문, Round 2 = P4~P7 생존 질문. 사용자가 -명시적으로 1문1답 인터뷰를 요청하면(예: "grill me") Design-only 조사에 한해 -라운드 상한 없이 phase 순서로 진행한다. Delivery 중 정책 질문은 그대로 -`oracle-run.mjs budget` 2라운드를 따른다. - -각 라운드가 끝나면 질문·답·추천안 채택 여부와 가지치기 사유를 -`.ai/oracles//journal.md`에 append한다. 답을 대화에만 남기지 않는다 — -컨텍스트가 요약돼도 다음 단계는 journal과 카드에서 이어진다. - -문답 항목은 한 줄 규격으로 쓴다 — 질문·답·채택·매핑 행이 빠지면 미완성이다: - -```markdown -## Grill Round 1 (P1~P3) — 2026-08-21 - -- Q1(P1): 성공 판정 기준? → 답: 완료 화면+주문번호 → 채택: 추천 수용 → 행: P1, O1 -- Q2(P4): 409의 UI 결과? → 답: 기존 주문 화면 이동 → 채택: 수정 → 행: P3, O5 -- 가지치기: P7 스킵 — behavior-only -``` - -자주 필요한 질문(P5): - -- pending 중 중복 제출을 무시할지, 큐잉할지, 오류로 볼지 -- 실패 후 입력·기존 데이터를 유지할지 -- 오류 subtype별 재시도 허용 여부 -- A 후 B 요청, B 후 A 응답에서 어떤 결과가 이길지 -- 이탈·취소 후 늦은 응답을 어떻게 처리할지 - -방법 근거: phase 순서는 -[RADIO framework](https://www.greatfrontend.com/front-end-system-design-playbook/framework)의 -R→A→D→I→O 순서를, 질문·정책·예시 분리는 -[Example Mapping](https://cucumber.io/blog/bdd/example-mapping-introduction/)의 -rule(=`P*`)·example(=`O*`)·question(=red card) 대응을 따른다. `Then`이 불명확한 -예시는 질문이다 — 행을 만들지 않고 red card로 기록한다. red card가 쌓이면 -`NEEDS_DECISION`, rule이 쌓이면 Smallest reversible scope 분할을 제안한다. - -RADIO 각 요소의 처리 위치 — grill이 전부 소유하지 않는다: - -- R Requirements: Grill P1·P2 -- A Architecture: Delivery architecture 게이트 — grill에서 구현 구조를 질문하지 않는다 -- D Data model: Grill P3 -- I Interface (server): Grill P4 → 스펙 없으면 카드 도출 draft → 승인 → `## API contract` -- I Interface (component): `type-constraints.md` — 카드 `O*` 행에서 도출 -- O Optimizations: P5 경합·P7 접근성·P8 성능 + Delivery 증거 행 - -- outcome-unknown timeout에서 재시도와 idempotency를 어떻게 보장할지 -- 요청된 수단이 의도한 결과를 얻는 최소 수단인지, 더 작은 대안을 먼저 검증할지 - -## 4. 정책 출처 - -인정: 1) 사용자의 명시적 답변, 2) 승인된 기획서·PRD·수용 기준·디자인 시스템·Figma의 -정확한 위치·version, 3) 적용되는 보안·개인정보·법적·접근성·데이터 정합성 제약, 4) -레포가 공개 계약으로 지정한 API·architecture·호환성 문서. - -인정 안 함: 에이전트 추천안, production 코드, 기존 테스트, 브라우저에서 관찰한 현재 -동작, `implementation-reference`로 분류한 framework 문서·구현 휴리스틱. - -결정된 정책마다 출처를 붙인다. 출처 없는 정책이 하나라도 있으면 `ORACLE_READY`가 -아니다. - -```markdown -### 결정된 정책 - -- P1: 저장 중 추가 제출은 무시한다. (출처: 유저 Q1=A) (행: O1, O2) -- P2: 5xx 실패 시 입력을 유지한다. (출처: docs/save.md#failure-policy) (행: O3) -``` - -## 5. 카드 형식 - -`references/bva.md`의 네 축과 자동 추가 TC 7종을 적용한다. 모든 새 카드는 -`Outcome Brief → Source Registry → User Confirmation → 결정된 정책 → 계약 행` 순서. -`oracle-verify.mjs card`가 Outcome Brief 필수 값과 Source Registry `Kind`를 lock 전에 -검사한다. - -Design Intent가 있으면 `visual-design.md` 형식을 행동 매트릭스 바로 앞에 둔다. -Design Intent·`D*` 행·`O*` 행·확인 근거는 모두 같은 Oracle bytes로 잠근다. 비-N/A -`D*` 행은 `HARD → test`, `RELATIONAL → visual | pending`, `JUDGMENT → designer -reviewer`로 매핑. `local`·`identity-shaping`이면 Design Change Confirmation의 사용자 -답변 위치를 같은 Design Intent에 기록. - -```markdown -## User Confirmation - -- Status: draft | approved -- Source: 사용자 승인 응답의 메시지·issue·문서 위치 -- Delta: new card 또는 이전 revision 대비 의미 변경 요약 -- Visual QA authorization: approved | declined # RELATIONAL 행이 있을 때 -``` - -Draft 단계는 `Status: draft` 유지. 사용자가 카드 전문·delta를 확인하고 명시적으로 -승인한 뒤에만 `approved`와 실제 응답 위치를 기록한다. 에이전트 추천이나 "사용자가 -원할 것"이라는 추론을 Source로 쓰지 않는다. - -| ID | 정책 | Given | When | Then | Never | 부작용(종류×횟수) | BVA | -| --- | ---- | ----- | ---- | ---- | ----- | ----------------- | --- | - -| 열 | 의미 | -| -------- | ------------------------------------------ | -| `Given` | 행동 직전 상태와 전제 | -| `When` | 사용자 행동, 응답, 시간 또는 순서 변화 | -| `Then` | 반드시 관찰되어야 하는 결과 | -| `Never` | 절대 발생하면 안 되는 반대 결과 | -| `부작용` | 요청·저장·이동·이벤트의 정확한 종류와 횟수 | -| `BVA` | 값·상태·시간/순서·횟수 중 검토한 경계 | - -규칙: - -- `Never`와 부작용 횟수가 빈 행은 미완성이다. -- 각 결정에 stable 정책 ID(`P*`)와 적용 행, 각 `O*`·`D*` 행의 `정책` 열에 같은 ID. - 정책 ID와 행 ID의 양방향 참조가 정확히 일치해야 한다. -- UI 상태와 실제 부작용 횟수를 각각 검증. -- 전제가 있는 자동 추가 TC는 행으로 만들고, 없으면 N/A와 사유 기록. -- 존재하지 않는 retry·cancel·race를 테스트 목적으로 발명하지 않는다. -- 오류는 기능에 해당하는 subtype별 메시지·복구·부작용을 구분. - -축약 예시: - -```markdown -| ID | 정책 | Given | When | Then | Never | 부작용 | BVA | -| --- | ---- | --------- | ---------- | -------------- | ------------------ | ----------- | ------------- | -| O1 | P1 | 유효 입력 | 저장 클릭 | pending 표시 | 응답 전 성공 UI | POST×1 | 상태: pending | -| O2 | P1 | pending | 클릭+Enter | pending 유지 | 두 번째 POST | POST×1(총) | 횟수: 1/2 | -| O3 | P2 | pending | 서버 5xx | 오류+입력 유지 | 성공 UI, 입력 유실 | 성공 저장×0 | 상태: error | -``` - -### State Model — 선택 사항 - -기본값은 생략이다. async 행이 있어도 `O*` 행 자체가 계약이며, 섹션이 없다고 -lint가 막지 않는다. 전이 정책이 행 나열만으로 읽히지 않을 만큼 얽힌 카드 -(다단계 제출·낙관적 롤백·결제류)에만 계약 행 뒤에 `## State Model`을 추가한다. -추가했다면 `oracle-verify.mjs card`가 구조를 검증한다: 비어 있지 않은 -`States`·`Events`, 모든 전이가 실제 `O*` 행을 인용하는 전이표 없이는 -`CARD_LINT_FAILED`로 lock이 막힌다. - -```markdown -## State Model - -- States: editing, submitting, success, failure -- Events: SUBMIT, RESPONSE_OK, RESPONSE_ERROR - -| From | Event | To | 행 | -| ---------- | -------------- | ---------- | --- | -| editing | SUBMIT | submitting | O1 | -| submitting | SUBMIT | submitting | O2 | -| submitting | RESPONSE_OK | success | O4 | -| submitting | RESPONSE_ERROR | failure | O3 | -``` - -- 상태·이벤트는 `O*` 행의 `Given`·`When`·`Then`에서만 도출하고 표의 모든 전이는 행 - ID를 참조한다. 참조 없는 전이는 발명된 정책이다. -- `상태 × 이벤트`의 빈 조합은 불가능(타입으로 표현 불가)인지 미결 정책인지 구분한다. - 미결이면 `NEEDS_DECISION`이며 "무시"를 기본값으로 채우지 않는다. -- 이 섹션은 카드 bytes에 포함되어 함께 잠긴다. discriminated union 번역은 - [`type-constraints.md`](type-constraints.md) 담당. - -## 6. Adversarial self-review - -각 행에 네 질문을 적용하고 반례가 나오면 행을 보강한다. - -1. 이 행을 통과하면서 요구사항을 위반하는 가장 단순한 구현은? -2. 정상적인 다른 구현인데 이 행 때문에 실패할 수 있는가? -3. UI만 흉내 내고 실제 부작용 없이 통과할 수 있는가? -4. loading, error, retry, 연속 입력, 순서 역전 중 관련 있지만 빠진 것은? - -예: "저장 중 버튼 disabled"만으로는 disabled 적용 전 POST 두 번을 잡지 못한다. 같은 -행에 `POST×1(총)`과 "두 번째 POST 없음"을 병기한다. - -Design Intent가 있으면 `visual-design.md`의 genericity·restraint 비평도 수행한다. 출처 -있는 미적 요구를 자동화하기 어렵다는 이유로 `NON_ORACLE_OPINION`이나 N/A로 내리지 -않는다. - -## 7. Draft Oracle과 사용자 확인 - -새 카드나 의미가 바뀐 revision은 다음 직렬 gate를 반드시 거쳐 사용자에게 확인받는다. - -1. production 수정 없이 외부 기준과 기존 revision 조사. -2. **Draft Oracle**과 semantic delta, 미결 질문 작성. -3. 전체 카드와 delta를 사용자에게 보여주고 재확인. -4. 승인되면 `User Confirmation`을 `approved`로 바꾸고 실제 응답 위치 기록. -5. 수정 요청이면 Draft를 고쳐 재확인. -6. 무응답·정책 충돌이면 `NEEDS_DECISION`. - -기존 locked Oracle 범위 안의 구현·테스트 보정에는 새 카드를 만들지 않는다. 그러나 -`Then`·`Never`·부작용·BVA·Design Intent·정책 출처 중 하나라도 의미가 바뀌면 잠긴 -파일을 제자리에서 고치지 않고 새 경로에 Draft revision을 만든다. 새 revision도 사용자 -확인 전에는 lint·lock·테스트·production 수정으로 넘어가지 않는다. - -## 8. 결정적 revision lock - -사용자가 확인한 카드는 self-review 뒤 exact bytes를 파일로 저장하고 bundled script로 -잠근다. Low fast path처럼 새 정책·카드가 없는 작업은 이 절차에 들어오지 않는다. - -Design-only로 잠근 revision을 나중에 Delivery로 확장하며 architecture·backend 등 새 -local source가 필요해지면 기존 lock에 덧붙이지 않는다. source delta를 사용자에게 -보여주고 새 revision 경로에서 카드와 전체 source 집합을 한 번에 잠근다. 처음부터 -Delivery가 요청됐으면 모든 source 승인을 마칠 때까지 lock을 미룬다. - -대상 레포가 agent artifact 위치를 정하지 않았다면: - -```text -/.ai/oracles//oracle.md -/.ai/oracles//oracle.lock.json -/.ai/oracles//run-state.json -/.ai/oracles//runs.jsonl -/.ai/oracles//.run-ids/ # 병렬 exec의 원자적 runId reservation -/.ai/oracles//evidence.json -``` - -### 카드 구조 lint - -lock 전에 `oracle-verify.mjs card`로 구조적 최소선을 기계 확인한다. lint는 token·표 -구조 검사일 뿐 semantic approval이 아니다 — 의미 심사는 6절 담당. - -```bash -node /scripts/oracle-verify.mjs card \ - --oracle .ai/oracles//oracle.md -``` - -검사 항목: 완전한 Outcome Brief, `Kind` 있는 Source Registry, 승인된 User Confirmation -존재, 모든 정책 줄의 stable ID·`(출처: …)`·적용 행, 정책 ID와 행 ID의 양방향 참조, -중복 없는 행 ID, `O*` 행의 `Then`·`Never`·부작용, `D*` 행의 계약·출처·증거 계층과 -Source Registry 참조, 모호어 부재, 자동 추가 TC 7종의 -실제 계약 행 또는 출처 있는 N/A 표기. `CARD_LINT_FAILED`는 lock 전에 카드를 고치라는 -뜻이며, 검사를 우회하려고 문구만 바꾸지 않는다. - -에이전트가 직접 실행하며 사용자에게 명령 실행을 요청하지 않는다. 승인된 로컬 명세 -파일은 `--source`를 반복해 함께 잠근다. URL·Figma 같은 원격 기준은 정확한 version을 -카드 bytes에 기록하고 외부 기준 게이트에서 다시 확인한다. - -```bash -node /scripts/oracle-lock.mjs create \ - --oracle .ai/oracles//oracle.md \ - --lock .ai/oracles//oracle.lock.json \ - --source - -node /scripts/oracle-lock.mjs verify \ - --lock .ai/oracles//oracle.lock.json -``` - -- ``는 현재 host가 실제로 로드한 이 스킬의 디렉터리. home 경로 hardcode - 금지. -- 출력된 `sha256:`가 Oracle revision이다. -- `create`는 동일 bytes의 기존 lock에 idempotent, 변경된 카드·source의 기존 lock은 - 덮어쓰지 않는다. 승인된 새 revision은 이전 artifact를 보존하고 새 경로에 생성. -- 모든 새 카드·revision은 7절에서 카드 전문·delta를 확인받는다. digest는 확인된 - bytes의 식별자일 뿐 사용자 확인을 대신하지 않는다. -- 테스트 작성, production 수정, 독립 리뷰, 완료 상태 발급 직전 `verify` 재실행. - `oracle-run.mjs`의 `exec`·`transition`은 매 호출 같은 검증을 자동 수행. -- `ORACLE_CHANGED`·`SOURCE_CHANGED`면 기존 RED·GREEN·리뷰 증거를 폐기하고 변경 diff와 - 카드 현재본을 제시해 `NEEDS_DECISION`으로 복귀. -- `LOCK_INVALID`·도구 부재·실행 불가는 결정론 판정 실패 → `FAIL`. -- mismatch 제거용 자동 재생성 금지. 재잠금은 source gate, Draft delta, 사용자 재확인, - self-review를 다시 거친 뒤에만. -- Low risk로 카드를 생략했으면 lock N/A 사유를 남긴다. Medium/High에서 파일시스템· - Node가 없으면 Design-only와 Delivery 모두 LLM 판정으로 대체하지 않고 `FAIL`. - -SHA-256은 drift 검출 장치일 뿐 lockfile을 다시 쓸 수 있는 actor의 승인 권한을 -보장하지 않는다. 강한 통제가 필요하면 CI human approval·CODEOWNERS·외부 서명을 -추가한다. run ledger·상태 파일도 같은 한계. - -### Run artifact 초기화 - -Delivery 진입 시 lock 직후 run ledger와 상태 파일을 만든다. Design-only로 끝나면 -생성하지 않는다. `journal.md`는 예외다 — Grill부터 같은 디렉터리에 쌓이며 ledger와 -별개로 단계 근거만 담는다. - -```bash -node /scripts/oracle-run.mjs init \ - --dir .ai/oracles/ \ - --lock .ai/oracles//oracle.lock.json \ - --risk low|medium|high \ - --required-label behavior \ - --required-label lint \ - --harness-path vitest.config.ts \ - --milestone list:O1,O2 \ - --milestone detail:O3,O4 -``` - -- `--required-label`: 대상 레포에서 실제 적용되는 targeted test, lint, typecheck, - build label을 반복 선언. 최소 하나 필요, GREEN·리뷰 후 재검증에서 모두 재확인. -- `init`은 lock을 검증하고 현재 worktree digest를 `ORACLE_READY` 기준선으로 저장 — - 이후 TDD 순서 판정의 근거. -- `--scan-root` 기본값은 현재 작업 디렉터리. monorepo에서 범위를 좁힐 때만 명시. -- RED 전에 바꿔야 하는 config·setup·mock 배선은 `--harness-path`로 scan root 기준의 - 정확한 상대 파일 경로를 반복 선언. glob·디렉터리·root 밖 경로 불허, 실제로 존재하고 - worktree snapshot에 포함되는 파일만. -- 큰 카드는 `--milestone :O1,O2`를 반복해 겹치지 않는 test-owned 행을 묶는다. - 행은 Oracle에 존재해야 하고 두 milestone이 같은 행을 소유하지 않는다. 작은 카드에는 - 선언하지 않는다. -- 상태 파일이 이미 있으면 `init`은 실패한다. 예산·기준선 초기화 목적 재실행 금지. 새 - revision은 새 `` 디렉터리. - -## 9. 설계 종료 상태 - -### `ORACLE_READY` - -- Outcome Brief 완성, Source Registry에 Kind·관할·위치·version·승인 상태 또는 N/A 사유 -- 카드가 외부 기준의 상태·문구·interaction·부작용을 누락·왜곡하지 않음 -- 모든 정책에 인정되는 출처 -- `User Confirmation`이 `approved`이고 새 카드 또는 semantic delta를 승인한 실제 - 사용자 응답 위치가 있음 -- UI 시각 범위 기록, `local`·`identity-shaping`이면 승인된 Design Intent와 모든 `D*` - 행의 `Never`·출처·증거 계층 완성 -- `local`·`identity-shaping`이면 Design Change Confirmation의 명시적 사용자 답변 위치 -- `identity-shaping`이면 두 번의 설계 pass를 완료한 제안으로 확인받음 -- 모든 행의 `Never`와 부작용 횟수 완성 -- 자동 추가 TC 7종 추가 또는 N/A 사유 -- adversarial self-review 통과 -- `oracle-verify.mjs card` lint와 revision lock 검증이 통과함 - -### `NEEDS_DECISION` - -미결 질문, 질문별 추천안과 근거, 카드 현재본을 출력한다. 이 상태에서는 테스트·구현을 -진행하지 않는다. 잠긴 적이 있으면 마지막 SHA-256과 mismatch를 함께 출력한다. 카드 -현재본은 다음 세션의 재개 자료다. diff --git a/packages/frontend-oracle-design/skills/references/oracle-workflow.graph.json b/packages/frontend-oracle-design/skills/references/oracle-workflow.graph.json index 7636858..440bde6 100644 --- a/packages/frontend-oracle-design/skills/references/oracle-workflow.graph.json +++ b/packages/frontend-oracle-design/skills/references/oracle-workflow.graph.json @@ -178,425 +178,140 @@ { "from": "draft-oracle", "to": "user-confirmation", - "when": { - "field": "classification", - "equals": "CONFIRMATION_REQUIRED" - } - }, - { - "from": "draft-oracle", - "to": "failed", - "when": { - "field": "classification", - "equals": "FAIL" - } - }, - { - "from": "user-confirmation", - "to": "lock-oracle", - "when": { - "field": "decision", - "equals": "APPROVE_DESIGN" - } - }, - { - "from": "user-confirmation", - "to": "lock-oracle", - "when": { - "field": "decision", - "equals": "APPROVE_DELIVERY" - } - }, - { - "from": "user-confirmation", - "to": "draft-oracle", - "when": { - "field": "decision", - "equals": "REVISE" - } - }, - { - "from": "user-confirmation", - "to": "cancelled", - "when": { - "field": "decision", - "equals": "CANCEL" - } - }, - { - "from": "lock-oracle", - "to": "oracle-ready", - "when": { - "field": "route", - "equals": "DESIGN_READY" - } - }, - { - "from": "lock-oracle", - "to": "delivery-init", - "when": { - "field": "route", - "equals": "DELIVERY_READY" - } - }, - { - "from": "lock-oracle", - "to": "draft-oracle", - "when": { - "field": "route", - "equals": "POLICY_GAP" - } - }, - { - "from": "lock-oracle", - "to": "failed", - "when": { - "field": "route", - "equals": "FAIL" - } - }, - { - "from": "delivery-init", - "to": "valid-red", - "when": { - "field": "classification", - "equals": "READY" - } - }, - { - "from": "delivery-init", - "to": "draft-oracle", - "when": { - "field": "classification", - "equals": "POLICY_GAP" - } - }, - { - "from": "delivery-init", - "to": "failed", - "when": { - "field": "classification", - "equals": "ENVIRONMENT_DEFECT" - } - }, - { - "from": "delivery-init", - "to": "failed", - "when": { - "field": "classification", - "equals": "FAIL" - } - }, - { - "from": "valid-red", - "to": "implement-green", - "when": { - "field": "classification", - "equals": "VALID_RED" - } - }, - { - "from": "valid-red", - "to": "valid-red", - "when": { - "field": "classification", - "equals": "HARNESS_DEFECT" - } - }, - { - "from": "valid-red", - "to": "draft-oracle", - "when": { - "field": "classification", - "equals": "POLICY_GAP" - } - }, - { - "from": "valid-red", - "to": "failed", - "when": { - "field": "classification", - "equals": "ENVIRONMENT_DEFECT" - } - }, - { - "from": "valid-red", - "to": "failed", - "when": { - "field": "classification", - "equals": "FAIL" - } - }, + "when": { "field": "classification", "equals": "CONFIRMATION_REQUIRED" } + }, + { "from": "draft-oracle", "to": "failed", "when": { "field": "classification", "equals": "FAIL" } }, + { "from": "user-confirmation", "to": "lock-oracle", "when": { "field": "decision", "equals": "APPROVE_DESIGN" } }, + { "from": "user-confirmation", "to": "lock-oracle", "when": { "field": "decision", "equals": "APPROVE_DELIVERY" } }, + { "from": "user-confirmation", "to": "draft-oracle", "when": { "field": "decision", "equals": "REVISE" } }, + { "from": "user-confirmation", "to": "cancelled", "when": { "field": "decision", "equals": "CANCEL" } }, + { "from": "lock-oracle", "to": "oracle-ready", "when": { "field": "route", "equals": "DESIGN_READY" } }, + { "from": "lock-oracle", "to": "delivery-init", "when": { "field": "route", "equals": "DELIVERY_READY" } }, + { "from": "lock-oracle", "to": "draft-oracle", "when": { "field": "route", "equals": "POLICY_GAP" } }, + { "from": "lock-oracle", "to": "failed", "when": { "field": "route", "equals": "FAIL" } }, + { "from": "delivery-init", "to": "valid-red", "when": { "field": "classification", "equals": "READY" } }, + { "from": "delivery-init", "to": "draft-oracle", "when": { "field": "classification", "equals": "POLICY_GAP" } }, + { "from": "delivery-init", "to": "failed", "when": { "field": "classification", "equals": "ENVIRONMENT_DEFECT" } }, + { "from": "delivery-init", "to": "failed", "when": { "field": "classification", "equals": "FAIL" } }, + { "from": "valid-red", "to": "implement-green", "when": { "field": "classification", "equals": "VALID_RED" } }, + { "from": "valid-red", "to": "valid-red", "when": { "field": "classification", "equals": "HARNESS_DEFECT" } }, + { "from": "valid-red", "to": "draft-oracle", "when": { "field": "classification", "equals": "POLICY_GAP" } }, + { "from": "valid-red", "to": "failed", "when": { "field": "classification", "equals": "ENVIRONMENT_DEFECT" } }, + { "from": "valid-red", "to": "failed", "when": { "field": "classification", "equals": "FAIL" } }, { "from": "implement-green", "to": "standard-review", - "when": { - "field": "classification", - "equals": "IMPLEMENTED_GREEN_STANDARD" - } + "when": { "field": "classification", "equals": "IMPLEMENTED_GREEN_STANDARD" } }, { "from": "implement-green", "to": "high-review-fanout", - "when": { - "field": "classification", - "equals": "IMPLEMENTED_GREEN_HIGH" - } + "when": { "field": "classification", "equals": "IMPLEMENTED_GREEN_HIGH" } }, { "from": "implement-green", "to": "implement-green", - "when": { - "field": "classification", - "equals": "PRODUCT_DEFECT" - } + "when": { "field": "classification", "equals": "PRODUCT_DEFECT" } }, { "from": "implement-green", "to": "evidence-repair", - "when": { - "field": "classification", - "equals": "EVIDENCE_GAP" - } + "when": { "field": "classification", "equals": "EVIDENCE_GAP" } }, { "from": "implement-green", "to": "evidence-repair", - "when": { - "field": "classification", - "equals": "HARNESS_DEFECT" - } - }, - { - "from": "implement-green", - "to": "draft-oracle", - "when": { - "field": "classification", - "equals": "POLICY_GAP" - } + "when": { "field": "classification", "equals": "HARNESS_DEFECT" } }, + { "from": "implement-green", "to": "draft-oracle", "when": { "field": "classification", "equals": "POLICY_GAP" } }, { "from": "implement-green", "to": "failed", - "when": { - "field": "classification", - "equals": "ENVIRONMENT_DEFECT" - } - }, - { - "from": "implement-green", - "to": "failed", - "when": { - "field": "classification", - "equals": "FAIL" - } + "when": { "field": "classification", "equals": "ENVIRONMENT_DEFECT" } }, + { "from": "implement-green", "to": "failed", "when": { "field": "classification", "equals": "FAIL" } }, { "from": "evidence-repair", "to": "implement-green", - "when": { - "field": "classification", - "equals": "EVIDENCE_READY" - } + "when": { "field": "classification", "equals": "EVIDENCE_READY" } }, { "from": "evidence-repair", "to": "evidence-repair", - "when": { - "field": "classification", - "equals": "HARNESS_DEFECT" - } + "when": { "field": "classification", "equals": "HARNESS_DEFECT" } }, { "from": "evidence-repair", "to": "implement-green", - "when": { - "field": "classification", - "equals": "PRODUCT_DEFECT" - } - }, - { - "from": "evidence-repair", - "to": "draft-oracle", - "when": { - "field": "classification", - "equals": "POLICY_GAP" - } - }, - { - "from": "evidence-repair", - "to": "failed", - "when": { - "field": "classification", - "equals": "ENVIRONMENT_DEFECT" - } + "when": { "field": "classification", "equals": "PRODUCT_DEFECT" } }, + { "from": "evidence-repair", "to": "draft-oracle", "when": { "field": "classification", "equals": "POLICY_GAP" } }, { "from": "evidence-repair", "to": "failed", - "when": { - "field": "classification", - "equals": "FAIL" - } - }, - { - "from": "standard-review", - "to": "review-decision", - "when": "always" - }, - { - "from": "high-review-fanout", - "to": "high-review-a", - "when": { - "field": "status", - "equals": "READY" - } - }, - { - "from": "high-review-fanout", - "to": "high-review-b", - "when": { - "field": "status", - "equals": "READY" - } - }, - { - "from": "high-review-a", - "to": "high-review-join", - "when": "always" - }, - { - "from": "high-review-b", - "to": "high-review-join", - "when": "always" - }, - { - "from": "high-review-join", - "to": "review-decision", - "when": { - "field": "status", - "equals": "READY" - } + "when": { "field": "classification", "equals": "ENVIRONMENT_DEFECT" } }, + { "from": "evidence-repair", "to": "failed", "when": { "field": "classification", "equals": "FAIL" } }, + { "from": "standard-review", "to": "review-decision", "when": "always" }, + { "from": "high-review-fanout", "to": "high-review-a", "when": { "field": "status", "equals": "READY" } }, + { "from": "high-review-fanout", "to": "high-review-b", "when": { "field": "status", "equals": "READY" } }, + { "from": "high-review-a", "to": "high-review-join", "when": "always" }, + { "from": "high-review-b", "to": "high-review-join", "when": "always" }, + { "from": "high-review-join", "to": "review-decision", "when": { "field": "status", "equals": "READY" } }, { "from": "review-decision", "to": "final-verify", - "when": { - "field": "classification", - "equals": "REVIEW_ACCEPTED" - } + "when": { "field": "classification", "equals": "REVIEW_ACCEPTED" } }, { "from": "review-decision", "to": "final-verify", - "when": { - "field": "classification", - "equals": "NON_ORACLE_OPINION" - } + "when": { "field": "classification", "equals": "NON_ORACLE_OPINION" } }, { "from": "review-decision", "to": "implement-green", - "when": { - "field": "classification", - "equals": "PRODUCT_DEFECT" - } + "when": { "field": "classification", "equals": "PRODUCT_DEFECT" } }, { "from": "review-decision", "to": "evidence-repair", - "when": { - "field": "classification", - "equals": "EVIDENCE_GAP" - } + "when": { "field": "classification", "equals": "EVIDENCE_GAP" } }, { "from": "review-decision", "to": "evidence-repair", - "when": { - "field": "classification", - "equals": "HARNESS_DEFECT" - } - }, - { - "from": "review-decision", - "to": "draft-oracle", - "when": { - "field": "classification", - "equals": "POLICY_GAP" - } - }, - { - "from": "review-decision", - "to": "failed", - "when": { - "field": "classification", - "equals": "ENVIRONMENT_DEFECT" - } + "when": { "field": "classification", "equals": "HARNESS_DEFECT" } }, + { "from": "review-decision", "to": "draft-oracle", "when": { "field": "classification", "equals": "POLICY_GAP" } }, { "from": "review-decision", "to": "failed", - "when": { - "field": "classification", - "equals": "FAIL" - } + "when": { "field": "classification", "equals": "ENVIRONMENT_DEFECT" } }, + { "from": "review-decision", "to": "failed", "when": { "field": "classification", "equals": "FAIL" } }, { "from": "final-verify", "to": "review-verified", - "when": { - "field": "classification", - "equals": "REVIEW_VERIFIED" - } + "when": { "field": "classification", "equals": "REVIEW_VERIFIED" } }, { "from": "final-verify", "to": "implement-green", - "when": { - "field": "classification", - "equals": "PRODUCT_DEFECT" - } + "when": { "field": "classification", "equals": "PRODUCT_DEFECT" } }, { "from": "final-verify", "to": "evidence-repair", - "when": { - "field": "classification", - "equals": "EVIDENCE_GAP" - } + "when": { "field": "classification", "equals": "EVIDENCE_GAP" } }, { "from": "final-verify", "to": "evidence-repair", - "when": { - "field": "classification", - "equals": "HARNESS_DEFECT" - } + "when": { "field": "classification", "equals": "HARNESS_DEFECT" } }, - { - "from": "final-verify", - "to": "draft-oracle", - "when": { - "field": "classification", - "equals": "POLICY_GAP" - } - }, - { - "from": "final-verify", - "to": "failed", - "when": { - "field": "classification", - "equals": "ENVIRONMENT_DEFECT" - } - }, - { - "from": "final-verify", - "to": "failed", - "when": { - "field": "classification", - "equals": "FAIL" - } - } + { "from": "final-verify", "to": "draft-oracle", "when": { "field": "classification", "equals": "POLICY_GAP" } }, + { "from": "final-verify", "to": "failed", "when": { "field": "classification", "equals": "ENVIRONMENT_DEFECT" } }, + { "from": "final-verify", "to": "failed", "when": { "field": "classification", "equals": "FAIL" } } ] } diff --git a/packages/frontend-oracle-design/skills/references/performance.md b/packages/frontend-oracle-design/skills/references/performance.md index 390608c..aa4e56c 100644 --- a/packages/frontend-oracle-design/skills/references/performance.md +++ b/packages/frontend-oracle-design/skills/references/performance.md @@ -8,7 +8,7 @@ reference다. metric·threshold는 승인된 성능 계약이나 사용자 답 측정 명령·baseline/after run·`performance` 필수 label은 [`frontend-implementation.md`](frontend-implementation.md) 7절과 -[`implementation-loop.md`](implementation-loop.md)의 GREEN 게이트가 소유한다. 이 +[`delivery/green-review.md`](delivery/green-review.md)의 GREEN 게이트가 소유한다. 이 문서는 문제 분류·원인 확인·trade-off 판단만 소유한다. ## 1. 문제를 세 축으로 분류한다 diff --git a/packages/frontend-oracle-design/skills/references/reference-graph.json b/packages/frontend-oracle-design/skills/references/reference-graph.json new file mode 100644 index 0000000..511bb0a --- /dev/null +++ b/packages/frontend-oracle-design/skills/references/reference-graph.json @@ -0,0 +1,183 @@ +{ + "id": "frontend-oracle-design-reference-graph", + "version": 1, + "description": "조건 로드 reference 노드 그래프. 진입 risk 판정으로 lane 선택: Low는 low-fast-path 단일 노드 exclusive lane, 명시적 Oracle 요청·Medium/High는 common entry의 oracle lane. 노드 when 충족 시 path 전문과 requires 엣지를 함께 로드, 무관 노드 로드 금지. 로드 조건의 전체 서술은 SKILL.md 「Reference 로딩」이 소유한다.", + "entry": "common", + "lanes": [ + { + "id": "low-fast-path", + "when": "risk=Low", + "nodes": ["low-fast-path"], + "exclusive": true, + "escalation": "정책 질문·새 계약·architecture 결정 발생 시 즉시 실격 — oracle lane으로 승격" + }, + { "id": "oracle", "when": "명시적 Oracle 요청 또는 Medium/High", "entry": "common" } + ], + "reviewPoints": [ + { "when": "항상", "nodes": ["changeability"] }, + { "when": "frontend production 변경", "nodes": ["frontend-implementation"] }, + { "when": "타입·상태 계약 생성·변경", "nodes": ["types-review-criteria"] }, + { "when": "FSD 레포", "nodes": ["fsd"] }, + { "when": "Design Intent 포함", "nodes": ["visual-design"] }, + { "when": "backend·DB·data-access 변경", "nodes": ["backend"] }, + { "when": "성능 요구·개선 claim", "nodes": ["performance"] } + ], + "nodes": [ + { + "id": "low-fast-path", + "path": "references/lanes/low-fast-path.md", + "when": "risk=Low — 이 노드만 로드", + "requires": [] + }, + { + "id": "common", + "path": "references/common.md", + "when": "카드 절차 진입 시 최우선", + "requires": [] + }, + { + "id": "bva", + "path": "references/bva.md", + "when": "계약 행 작성·테스트 번역 ($test와 byte-동일 공유)", + "requires": [] + }, + { + "id": "card-policy-sources", + "path": "references/card/policy-sources.md", + "when": "카드 작성 시작", + "requires": ["common"] + }, + { + "id": "card-risk-grill", + "path": "references/card/risk-grill.md", + "when": "카드 작성 시작", + "requires": ["common", "card-policy-sources"] + }, + { + "id": "card-format", + "path": "references/card/card-format.md", + "when": "계약 행·State Model 작성", + "requires": ["common", "bva"] + }, + { + "id": "card-confirmation-lock", + "path": "references/card/confirmation-lock.md", + "when": "사용자 확인·lock·init 직전", + "requires": ["common"] + }, + { + "id": "visual-design", + "path": "references/visual-design.md", + "when": "보이는 UI 변경 전; Design Intent 있으면 리뷰 재독", + "requires": ["common"] + }, + { + "id": "delivery-ledger", + "path": "references/delivery/ledger.md", + "when": "Delivery 진입 직후 ($test 명시 호출 뒤)", + "requires": ["common"] + }, + { + "id": "delivery-red", + "path": "references/delivery/red.md", + "when": "테스트 작성·VALID_RED 전이", + "requires": ["delivery-ledger", "bva"] + }, + { + "id": "delivery-implementation-decision", + "path": "references/delivery/implementation-decision.md", + "when": "VALID_RED 뒤 production 수정 직전", + "requires": ["changeability", "frontend-implementation"] + }, + { + "id": "delivery-green-review", + "path": "references/delivery/green-review.md", + "when": "셀프피드백·GREEN·review 전이", + "requires": ["delivery-ledger"] + }, + { + "id": "changeability", + "path": "references/changeability.md", + "when": "production 수정 전·독립 리뷰", + "requires": ["common"] + }, + { + "id": "frontend-implementation", + "path": "references/frontend-implementation.md", + "when": "frontend production 수정 전", + "requires": ["common"] + }, + { + "id": "architecture-contract", + "path": "references/architecture-contract.md", + "when": "architecture 경계·state ownership·public API 변경", + "requires": ["common"] + }, + { + "id": "types-state-ladder", + "path": "references/types/state-ladder.md", + "when": "async·중복 제출 O* 행 또는 client state·boundary 타입 변경 전", + "requires": ["common"] + }, + { + "id": "types-authoring", + "path": "references/types/authoring.md", + "when": "state-ladder와 함께", + "requires": ["types-state-ladder"] + }, + { + "id": "types-api-surface", + "path": "references/types/api-surface.md", + "when": "exported shared/package API·Props 변경", + "requires": ["types-state-ladder", "types-authoring"] + }, + { + "id": "types-review-criteria", + "path": "references/types/review-criteria.md", + "when": "타입·상태 계약 변경의 독립 리뷰", + "requires": ["subagent-review"] + }, + { + "id": "type-environment", + "path": "references/type-environment.md", + "when": "레포당 1회 — 첫 타입 계약 또는 tsconfig·TS 버전 변경", + "requires": [] + }, + { + "id": "fsd", + "path": "references/fsd.md", + "when": "Delivery + FSD 레포(또는 도입 승인)", + "requires": [] + }, + { + "id": "backend", + "path": "references/backend.md", + "when": "backend·DB·data-access 경계 변경 전", + "requires": [] + }, + { + "id": "performance", + "path": "references/performance.md", + "when": "성능 요구·개선 claim", + "requires": [] + }, + { + "id": "subagent-review", + "path": "references/subagent-review.md", + "when": "구현·테스트 검증 후 독립 리뷰", + "requires": ["common", "changeability"] + }, + { + "id": "graph-orchestration", + "path": "references/graph-orchestration.md", + "when": "graph-orchestrated delivery loop 명시 요청만", + "requires": ["oracle-workflow-graph"] + }, + { + "id": "oracle-workflow-graph", + "path": "references/oracle-workflow.graph.json", + "when": "graph-orchestration과 함께", + "requires": [] + } + ] +} diff --git a/packages/frontend-oracle-design/skills/references/subagent-review.md b/packages/frontend-oracle-design/skills/references/subagent-review.md index ba4546a..01441a0 100644 --- a/packages/frontend-oracle-design/skills/references/subagent-review.md +++ b/packages/frontend-oracle-design/skills/references/subagent-review.md @@ -29,14 +29,9 @@ reviewer를 호출하지 않고 기존 증거를 폐기한다. ## 리뷰 기준 우선순위 -사용자가 제공했거나 승인된 기준으로 지정된 자료의 우선순위: - -1. 보안·개인정보·법적·접근성·금융 및 데이터 정합성의 강제 제약 -2. 사용자의 명시적 행동 계약과 공개 호환성 -3. 대상 레포의 필수 아키텍처·API·테스트 계약 -4. 승인된 기획서·PRD·수용 기준·디자인 시스템·Figma 원본의 해당 관할 -5. 위 기준을 실행 가능한 계약으로 옮긴 Oracle Card -6. production 코드·기존 headless test 관찰은 증거일 뿐 정답 권위가 아님 +리뷰 기준의 우선순위는 [`common.md`](common.md)의 권위 우선순위가 canonical이다 — +강제 제약부터 Oracle Card까지 같은 순서로 대조하고, production 코드·기존 headless +test 관찰은 증거일 뿐 정답 권위가 아니다. Figma가 기준이면 정확한 파일·페이지·프레임·버전을 확인하고, 접근 불가면 추측· 스크린샷 기억으로 대체하지 말고 미검증으로 보고한다. 외부 기준과 Oracle Card가 @@ -57,6 +52,29 @@ review와 별도로 설치된 `designer` 역할을 명시해 시각 계약을 reviewer도 정책을 새로 정하지 않는다. deterministic comparison이 그대로 통과하고 `JUDGMENT` 행과 baseline 변경이 모두 없으면 추가 designer 검수는 N/A와 사유를 기록. +## 리뷰 포인트 — 파일 링크로 전달 + +리뷰 기준은 reviewer 프롬프트에 본문을 복붙하지 않고 **reference 파일 링크로 +전달한다.** primary agent가 diff가 실제로 건드린 영역에 해당하는 기준 파일만 골라 +`--review-point`로 packet에 등록하면, packet에는 경로와 SHA-256 digest만 기록된다 — +reviewer는 링크된 파일을 직접 **전부** 읽고, digest로 어떤 revision의 기준을 읽었는지 +고정된다. 기준 본문을 요약·발췌해 프롬프트에 넣는 것은 입력 고정 원칙 위반이다. + +| 조건 (diff 기준) | 리뷰 포인트 파일 | +| --------------------------- | ---------------------------------------------------------------- | +| 항상 | [`changeability.md`](changeability.md) — 다섯 축 판정 기준 | +| frontend production 변경 | [`frontend-implementation.md`](frontend-implementation.md) | +| 타입·상태 계약 생성·변경 | [`types/review-criteria.md`](types/review-criteria.md) | +| FSD 레포 | [`fsd.md`](fsd.md) — 「자주 나오는 위반」 표 | +| Design Intent 포함 | [`visual-design.md`](visual-design.md) — 증거 계층·Delivery 책임 | +| backend·DB·data-access 변경 | [`backend.md`](backend.md) — 경계·검증 절 | +| 성능 요구·개선 claim | [`performance.md`](performance.md) | + +조건에 해당하지 않는 기준 파일은 등록하지 않는다 — reviewer도 그래프 로딩 규칙을 +따르며 무관한 기준으로 finding을 만들지 않는다. 조건→노드 매핑은 +[`reference-graph.json`](reference-graph.json)의 `reviewPoints`에도 기계 판독 가능하게 +선언되어 있다. + ## Reviewer 입력 리뷰 직전 기계 생성한 입력으로 고정한다. @@ -65,12 +83,15 @@ reviewer도 정책을 새로 정하지 않는다. deterministic comparison이 node /scripts/oracle-run.mjs review-packet \ --dir .ai/oracles/ \ --decision .ai/oracles//implementation-decision.md \ + --review-point /references/changeability.md \ + --review-point /references/types/review-criteria.md \ --output .ai/oracles//review-input.json ``` 패킷은 마지막 lock verify command·exit, lock manifest, Oracle 전문, 잠긴 local source 전문, run state, ledger, evidence mapping, init 이후 변경 파일 digest, git diff, visual -pending과 Implementation Decision의 path·sha256·content를 원시 필드로 담는다. +pending, Implementation Decision의 path·sha256·content와 등록한 리뷰 포인트의 +path·sha256(본문 없이 링크만)을 원시 필드로 담는다. reviewer는 `implementation-decision.md`의 주장과 실제 diff를 대조한다. 결론·의도한 해결책·유리한 요약을 추가하지 않는다. 패킷을 손으로 고치지 말고 입력이 바뀌면 다시 생성한다. URL·Figma처럼 lock에 담을 수 없는 외부 기준만 Oracle Registry의 정확한 @@ -199,20 +220,13 @@ reviewer는 아래 질문으로 사용자를 재인터뷰하거나 새 정책을 - loading, retry, race, out-of-order가 결정론적으로 통제되는가? - 구현이 카드 밖의 정책이나 동작을 임의로 추가하지 않았는가? - 실제 package version과 레포 계약을 확인하고 외부 best practice보다 우선했는가? -- material한 입력·성공·실패·상태가 TypeScript로 표현되고 `any`나 광범위한 assertion으로 - 불가능 상태를 숨기지 않았는가? 단순 상태에 불필요한 state machine도 만들지 않았는가? -- state에 저장된 action(`retry`·`submit`)이나 no-op action이 있는가? 상태는 데이터만 - 담고 action은 hook 반환의 형제여야 하며, 서버 상태면 기존 `refetch`를 재사용해야 - 한다. 위반이면 `FINDING`이다. -- 기존 query API로 표현되는 서버 상태를 `useState`+`useEffect`로 다시 구현했는가? - unmount·route 변경 뒤 늦은 응답이 상태를 덮을 수 있는가? +- 타입·상태 계약 — state union과 action 배치, 불가능 상태 은폐, 서버 상태 재구현, + Suspense/Error Boundary 분기, 늦은 응답 방어 — 는 리뷰 포인트로 받은 + [`types/review-criteria.md`](types/review-criteria.md)와 + [`frontend-implementation.md`](frontend-implementation.md) 기준으로만 판정한다. + 같은 기준을 이 목록에 반복하지 않는다. - server state를 query cache와 local/global state가 중복 소유하지 않는가? - Server Component로 충분한 일을 Client Component·TanStack Query로 옮기지 않았는가? -- Suspense/Error Boundary가 필요한 subtree에만 있고 initial load·background refetch· - mutation pending을 같은 상태로 취급하지 않았는가? -- 무조건 실행되는 첫 조회의 loading·error를 경계로 올리지 않고 컴포넌트 안에서 - 분기했는가? 조건부 query·placeholder·취소 제약 같은 실격 사유가 Implementation - Decision에 없으면 `FINDING`이다. - retry가 실패한 query/boundary 범위만 복구하고 전체 cache를 무차별 reset하지 않는가? - micro-hook이 UI와 비즈니스 로직의 책임을 정확히 분리하는가? UI component는 semantic JSX·접근성·시각 상태·사용자 intent 연결만 소유하고, domain 판정·DTO 변환·query/cache· diff --git a/packages/frontend-oracle-design/skills/references/type-constraints.md b/packages/frontend-oracle-design/skills/references/type-constraints.md deleted file mode 100644 index dbbe8b7..0000000 --- a/packages/frontend-oracle-design/skills/references/type-constraints.md +++ /dev/null @@ -1,434 +0,0 @@ -# 타입 제약 설계 — AI 비결정성을 컴파일 계약으로 줄인다 - -## 목적과 권위 - -카드에 async·순서 역전·중복 제출·retry·다단계 상태 `O*` 행이 있거나, client 상태· -Props·boundary 타입의 형태를 새로 만들거나 바꿀 때 사용한다. 이 문서는 제품 정책을 -만들지 않는다. 상태·전이·오류 분류는 전부 카드의 `O*` 행에서 도출하며, 카드에 없는 -상태나 전이가 필요해지면 발명하지 말고 `POLICY_GAP`으로 `NEEDS_DECISION`에 돌아간다. - -권위 순서는 [`frontend-implementation.md`](frontend-implementation.md)와 같다. -이 문서의 도구·라이브러리 선택은 구현 휴리스틱이며 정책 출처가 아니다. - -모든 설계는 다음 질문으로 판정한다. - -> AI가 생성할 수 있었던 잘못된 코드 중 **무엇이 이제 컴파일되지 않는가?** - -이 질문에 구체적으로 답할 수 없는 타입 복잡성은 추가하지 않는다. AI 생성 자체는 계속 -비결정적이다. 이 문서의 목표는 동일한 source·TypeScript·tsconfig에서 후보를 같은 -결과로 통과·거절하는 **수용 판정 결정성**이다. - -컴파일 통과는 건전성 증명이 아니라 결정적 고효율 필터다. TypeScript는 의도적으로 -불건전하고(bivariance, 리터럴에만 적용되는 excess property check), 필터 강도는 -tsconfig·컴파일러 버전의 함수다. 전제 환경 검증은 -[`type-environment.md`](type-environment.md)가 소유한다 — 레포당 1회 검증하고 -여기서는 반복하지 않는다. - -## 제약 소유권 - -- 값·Props·상태 조합·입출력 관계 → 타입 -- API·storage·URL·message 같은 외부 입력 → `unknown`에서 runtime parser -- 관찰 가능한 제품 행동 → `$test` -- 순서 역전·중복 제출·retry·unmount 후 도착 → abort signal·pending guard·멱등키·서버 검증 -- 같은 prompt의 생성 재현성 → 모델·provider — 이 문서가 보장하지 않음 - -시간축은 타입으로 증명되지 않는다. union을 만들었다고 순서 문제가 "해결됨"이라고 -선언하면 `FINDING`이다. 남은 시간축 비결정성과 그 런타임 방어는 Implementation -Decision에 반드시 기록한다. type-valid를 behavior-correct로 보고해도 `FINDING`이다. - -설계 전에 변경 대상에서 아래 여섯 지점을 찾고, **컴파일되지 않아야 할 잘못된 -사용을 최소 세 개 먼저 적는다** — exported API면 그대로 `.test-d.ts`의 -`@ts-expect-error` 케이스가 된다. - -- 값: 넓은 `string`·`number`·`Date` → 브랜드·의미 타입 -- 조합: 관련 boolean 여러 개, 배타적인 optional Props → discriminated union, union + `never` -- 관계: mode가 값·반환 타입을 결정하는데 타입에 없음 → generic lookup map, 별도 컴포넌트 -- 경로·키: route·query key·field path 자유 문자열 → factory·`keyof`·파생 union -- 결과: 성공·실패·부재·유지·삭제가 `undefined` 하나에 → `Result`·연산 union -- 확장: 소비자가 확장할 key가 `string`으로 열림 → typed registry·module augmentation - -## 상태 설계 사다리 - -union 작성은 3단이다. 1·2단에서 끝나는 문제에 3단을 쓰지 않는다. - -1. **파생 가능하면 저장하지 않는다.** 원본에서 계산한다(`itemCount = items.length`). - 타입이 강해도 중복 저장된 상태는 한쪽만 갱신된다. -2. **라이브러리가 이미 union을 소유하면 그대로 소비한다.** TanStack Query의 - `status`·`fetchStatus`, mutation의 `isPending`/`isSuccess`/`isError`는 이미 - discriminated contract고 최신 호출 기준 시간축 처리까지 포함한다. 같은 상태를 - `useState` 기계로 복사하지 않는다. 필수 파라미터가 없으면 non-null assertion - 대신 `skipToken` 또는 API 부재로 표현한다. - **로딩·로드 실패의 기본은 컴포넌트 분기가 아니라 경계다.** 무조건 실행되는 첫 - 조회는 `useSuspenseQuery`를 기본으로 두고 loading·error 분기를 국소 - ``와 Error Boundary로 올려 컴포넌트 본문에서 제거한다. 상황별 판정 - 표는 [`frontend-implementation.md`](frontend-implementation.md) 3절이 소유하며 - 이 문서가 그 기본값을 덮지 않는다. 조건부 query·placeholder·취소 제약처럼 - 경계로 올릴 수 없는 나머지에만 분기를 남기고, 그때도 자작 union 없이 라이브러리 - union에 ts-pattern을 직접 물린다 - (`match(mutation).with({ status: 'error', error: { code: 'CONFLICT' } }, …)`). - 같은 이유로 **기존 query·framework 상태를 최우선으로 재사용한다.** 이미 있는 - query API·router state·form state로 표현되는 서버 상태를 직접 관리하는 hook으로 - 다시 만들지 않는다. 카드가 요구하는 데이터가 기존 경계에 없을 때만 3단으로 - 내려간다. -3. **그래도 남는 진짜 client 상태만 `useState` + 의도 함수 hook으로 만든다.** - raw `setState`·setter를 hook 밖으로 노출하지 않고, 도메인 의도를 표현하는 함수 - (`pick`, `submit`, `reset`)만 반환한다. 잘못된 상태에서 온 호출은 카드가 정한 - 대로 무시·오류 처리하고, 카드에 없으면 `POLICY_GAP`으로 `NEEDS_DECISION`이다. - -reducer·전이표·상태 기계는 기본값이 아니다. 순서 위반 자체가 카드의 도메인 오류인 -흐름(결제·다단계 제출·낙관적 롤백)에서만, 새 state-machine dependency는 필요가 -입증될 때만 쓴다. XState는 계층·병렬 상태나 actor 조율이 카드에 실제로 있을 때만 -후보이며, 설치돼 있거나 도입이 승인된 경우만 쓴다. - -**카드에 `## State Model`이 있다는 사실만으로** Event union·전이 함수·transition -command 같은 런타임 기계를 만들지 않는다. 카드의 State Model은 정책을 빠짐없이 -적어 두는 표기이고, 그 정책을 무엇으로 구현할지는 이 사다리가 정한다. 단순 조회 -하나의 로딩·성공·실패는 2단에서 끝나며, 3단에서도 필요한 것은 상태 union 하나와 -의도 함수 몇 개다. - -### 상태 소유권은 하나다 - -하나의 async 흐름에는 canonical state owner를 하나만 둔다. TanStack Query처럼 -framework가 상태를 소유하면 그 결과를 `NextPageState` 같은 새 `status` union으로 -재포장하거나 같은 의미의 application 타입으로 복제하지 않는다. 공통 UI가 전체 -lifecycle이 아니라 다음 행동 가능 여부만 필요하면 `onLoadMore?: () => void`처럼 -callback 존재 자체가 capability가 되게 한다. loading·error·retry 표시는 원래 query를 -소유한 feature가 렌더링한다. 카드의 State Model은 정책 명세이지 구현마다 union을 -생성하라는 명령이 아니다. - -## 상태는 데이터, action은 형제 - -**상태는 데이터만 담는다.** union 멤버의 필드는 그 상태에서 참인 값이고, action은 -그 값으로 다음에 할 수 있는 일이다. 둘은 수명이 다르므로 한 값에 섞지 않는다. - -- `retry`·`submit`·`reset` 같은 함수를 state 값에 저장하지 않는다. 저장한 함수는 - 그것을 만든 render의 closure에 고정되므로, 이후 props·param이 바뀌어도 낡은 값을 - 계속 캡처한다. `@lodado/eslint-config/local-rules`를 쓰는 레포에서는 - `no-action-in-state`가 타입과 값 양쪽에서 이 형태를 잡는다. -- action은 **hook 반환 객체의 sibling**으로 준다 (`{ state, retry }`). 서버 상태면 - 새 action을 만들지 말고 query의 `refetch`를 그대로 다시 노출한다. -- 어떤 상태에서 쓸 수 없는 action은 만들지 않는다. `retry: () => undefined`처럼 - 타입을 맞추려고 넣는 no-op action은 UI에 "재시도할 수 있다"는 거짓 정보를 준다. - action이 상태별로 달라지면 상태를 좁힌 자식에 좁힌 action을 넘긴다. -- 잘못된 입력(파싱 실패한 ID, 없는 route param)은 **UI 동작·문구가 실제로 다를 때만 - 별도 상태로 나눈다.** 화면과 복구 경로가 같으면 기존 실패 상태에 합치고, 나누면 - 그 상태만의 필드와 action을 각각 채운다. 카드에 구분이 없으면 발명하지 말고 - `POLICY_GAP`으로 `NEEDS_DECISION`이다. - -```typescript -// 금지 — 상태에 action이 들어가 stale closure와 가짜 retry가 동시에 생긴다 -type DetailState = { status: 'loading' } | { status: 'failure'; retry: () => void } - -// 허용 — 상태는 데이터, action은 형제 -type DetailState = { status: 'loading' } | { status: 'failure'; reason: LoadFailure } -function useDetail(id: DetailId): { state: DetailState; retry: () => void } -``` - -## 카드에서 상태를 도출한다 - -카드에 `## State Model` 섹션이 있으면 그것이 상태·이벤트·전이의 유일한 출처다. -섹션은 선택이라 대부분의 카드에는 없다 — 없으면 `O*` 행의 `Given`·`When`·`Then`에서 -직접 도출하며, 섹션이 없다는 사실은 전이표·상태 기계를 만들 이유가 아니라 만들지 -않을 이유다. - -| 카드 열 | 타입 대응 | -| -------- | -------------------------------------------------------- | -| `Given` | 출발 상태와 그 상태에서만 유효한 필드 | -| `When` | 이벤트 (사용자 행동, 응답, 시간·순서 변화) | -| `Then` | 도착 상태와 관찰 결과 | -| `Never` | 금지 상태 또는 금지 전이 — 타입으로 표현 불가하게 만든다 | -| `부작용` | 전이에 결합된 외부 write의 종류와 횟수 | - -`상태 × 이벤트`에서 빈 조합은 불가능(타입으로 표현 불가)인지 미결 정책인지 -구분한다. 미결이면 `NEEDS_DECISION`이며 "아마 무시"를 기본값으로 채우지 않는다. -카드 행 ID를 참조하지 않는 전이는 발명된 정책이다. - -## 제약 선택 순서 - -먼저 문제가 어느 종류인지 분류한다. 소유권·상태 공간·API 관계는 서로 다른 축이라 -하나의 전역 순서로 섞지 않는다 — `keyof`가 discriminated union보다 항상 뒤라는 -식의 전역 순서는 없다. 각 사다리 안에서만 **앞 단부터** 검토하고, 앞 단으로 실제 -오용이 닫히면 뒷 단을 쓰지 않는다. 생성 결과를 같게 만들려는 규칙이 아니라, -불필요하게 복잡한 뒷 단 메커니즘을 일관되게 탈락시키는 우선순위다. - -```text -A. 소유권·boundary - 기존 owner 재사용 → 저장하지 않고 파생 → API 부재로 불가능하게 - → 외부 값은 unknown에서 runtime parse → schema·config·상수에서 타입 파생 - -B. 상태 공간 - framework union 소비 → capability·API 분리 → union + never - → discriminated union → exhaustive lookup·assertNever - → 순서 위반 자체가 도메인 오류일 때만 transition machine - -C. API 관계 - typeof·as const·satisfies → keyof·indexed access - → 내장 utility (Pick·Omit·Extract·Exclude·Parameters·ReturnType·Awaited) - → 관계형 generic·lookup map → tagged type → const type parameter·NoInfer - → 이산 입출력 관계 2~3개면 overload → 합성 가능한 관계면 mapped·conditional - → 중첩 구조 자체가 계약일 때만 recursive -``` - -trust boundary의 parse는 선택 사항이 아니다. 서로 다른 종류의 문제는 각 사다리에서 -독립적으로 판정하고, 같은 사다리 안에서 앞 단으로 닫히는 문제에 뒷 단을 쓰면 -`FINDING`이다. - -내장 utility로 표현되는 관계(`Awaited`·`ReturnType`·`Parameters`·`Extract`· -`Exclude`·`NonNullable`·`NoInfer`·`Readonly`·`Record`·`Pick`·`Omit`)를 custom -conditional type으로 재구현하지 않는다. 뒷 단 세 개(overload·mapped/conditional· -recursive)는 「Props와 API 표면」의 격리 조건을 만족할 때만 쓴다. - -## 타입 작성 규칙 - -**선언보다 파생.** 수기 선언은 타입 자체가 정책의 유일한 출처인 닫힌 계약 — -상태·이벤트·실패·연산 union, tagged/branded type, 공개 API의 capability·상호 -배타 Props — 에만 쓴다. schema·config·상수·entity에서 계산 가능한 projection은 -수기로 복제하지 않는다: entity·ID는 `z.output`, 부분집합은 `Extract`·`Exclude`, -유한 문자열 union은 `as const` 상수, 객체 key는 `keyof typeof`, 함수 관계는 -`Parameters`·`ReturnType`·`Awaited`에서 파생한다. 판정 기준은 수기 선언의 개수가 -아니라 **동일한 사실을 둘 이상의 위치가 소유하는지**다. 하나의 정책 사실을 -schema와 interface, 상수와 union이 동시에 소유하면 두 권위가 어긋난다. - -```typescript -type PaymentState = - | { status: 'editing'; amount: number; fieldErrors: FieldErrors } - | { status: 'submitting'; amount: number; requestId: string } - | { status: 'success'; paymentId: string } - | { status: 'failure'; amount: number; reason: PaymentFailure } -``` - -- 단일 `status` 문자열 literal discriminant를 쓴다. boolean 병렬 flag - (`isLoading`·`isError`·`isSuccess`)로 같은 흐름을 표현하지 않는다. -- 각 상태의 필드는 **그 상태에서만 의미 있는 값**만 담는다. 전 상태 공통 optional - 필드로 합치지 않는다. variant 수 자체는 variant record 도입 근거가 아니다 — - 명시 union이 더 읽기 쉬우면 상태가 많아도 유지한다. 같은 record가 상태 union과 - variant별 runtime lookup(config·renderer·메시지·권한) 중 둘 이상을 실제로 - 파생하는 단일 권위일 때만 record에서 union을 파생한다: - ```typescript - type Steps = { - editing: { amount: number; fieldErrors: FieldErrors } - submitting: { amount: number; requestId: string } - } - type State = { [K in keyof Steps]: { status: K } & Steps[K] }[keyof Steps] - const stepLabel = { editing: '입력 중', submitting: '처리 중' } satisfies Record - ``` -- 실패는 카드가 subtype을 구분하면 (`network`·`validation`·`5xx`) `reason`도 - discriminated union으로 만든다. 문자열 하나로 뭉개지 않는다. 예상 가능한 실패를 - 반환값으로 처리해야 하면 `Result` 형태의 닫힌 union을 쓰고, - `throw`는 결함(깨진 invariant) 전용으로 남긴다. -- trust boundary(API 응답, storage, URL, message)의 값은 `unknown`에서 시작해 - **파싱**으로 도메인 타입을 획득한다. 레포에 zod가 있으면 `z.discriminatedUnion()` - 을 쓰고 타입은 `z.output`으로 파생한다 — 스키마와 interface를 중복 선언하지 - 않는다. 응답에 `as DomainType` 단언을 쓰지 않는다. -- mutation payload는 entity의 `Partial`이 아니라 실제 연산 union으로 모델링한다 - (`rename`·`clear-description`처럼). `undefined`가 "유지"인지 "삭제"인지 모호한 - patch 타입을 만들지 않는다. 유지·설정·삭제가 모두 가능하면 연산을 분리한다. - -## 런타임보다 강하게 말하지 않는다 - -타입은 구현이 실제로 보장하는 범위까지만 약속한다. 아래는 컴파일은 되지만 -런타임보다 강한 거짓 계약이다. - -- **`Record`는 totality 계약이다** — 모든 `K`가 결과에 존재한다는 뜻이다. - 구현이 관찰된 key만 채우는 sparse lookup(`groupBy` 결과 등)이면 - `Partial>` 또는 `Map`를 쓴다. 전체 key를 사전 순회로 - 초기화하거나 누락 key에 기본값을 채울 때만 `Record`다. 이것은 - `Partial` mutation 금지와 다른 문제다 — 전자는 연산 의미를 잃는 - patch고, 후자는 일부 key만 런타임에 존재한다는 결과 표현이다. -- **type predicate는 검사 의무가 있다.** `value is T`는 본문이 `T`의 필수 - invariant를 실제로 검사할 때만 쓴다. 항상 `true`를 반환하거나, `as`를 감싸거나, - 일부 필드만 검사하고 전체 도메인 타입을 약속하는 predicate를 만들지 않는다. - boundary의 복잡한 도메인 타입은 predicate 수기 조립 대신 schema parser가 - 우선이고, `isNotNil` 수준의 단순·정확한 narrowing만 predicate로 남긴다. -- **wrapper의 반환 계약은 실행 시점을 따른다.** `Parameters`로 호출 계약은 - 보존하되, `ReturnType`는 wrapper가 같은 호출에서 실제 값을 반환할 때만 - 보존한다. debounce·schedule처럼 실행이 지연되면 즉시 반환형은 `void`, 캐시 - 반환이면 `ReturnType | undefined`, async wrapper면 - `Promise>>`처럼 런타임 의미를 그대로 쓴다. -- **excess property check는 sanitizer가 아니다.** object literal 대입에만 - 적용되며, `const user: PublicUser = source` 같은 annotation은 `source`의 민감 - 필드를 런타임에서 제거하지 않는다. 민감 필드 제거·exact object 보장은 runtime - projection이나 parser가 소유한다. -- **key remapping 반환형은 런타임과 동형이어야 한다.** 함수가 실제로 key를 - 변환하지 않는데 `ToCamelCaseKeys` 같은 key 변환 반환 타입만 붙이면 거짓 - 계약이다. - -## Props와 API 표면 - -### 공용 API 승격 델타 - -exported shared/package API를 만들거나 바꿀 때는 구현 타입보다 **호출부 먼저** 쓴다. -대표 제품 호출부에서 컴파일러가 추론할 수 있는 component generic을 명시하지 않는 것을 -목표로 하고, config 정의처럼 값만으로 Row를 추론할 수 없는 경계에서만 generic을 한 번 -고정한다. 이후 아래 델타만 설계한다. - -1. 대표 정상 호출부를 명시적 component type argument 없이 작성한다. -2. 변경 전 허용되던 넓은 값·optional 조합·끊어진 관계 중 실제 오용을 적는다. -3. 값·조합·관계·경로·결과·확장 중 이번 API가 닫아야 할 항목만 고른다. -4. schema·config·`as const` 값에서 key와 union을 파생하고 수기 권위를 늘리지 않는다. -5. controlled surface와 현재 제품이 쓰는 mode만 공개하고 나머지는 API 부재로 둔다. -6. type test에 generic 명시 없는 대표 정상 호출 1개와 컴파일되지 않아야 할 사용 최소 - 3개를 함께 둔다. JSX를 쓰면 파일은 `.test-d.tsx`로 만든다. - -정상 호출도 추론되지 않으면 부정 테스트가 통과해도 좋은 공개 API가 아니다. helper의 -추론을 보존하거나 generic을 단순화하고, 호출부가 같은 type argument를 반복하게 두지 -않는다. - -새로 설계하는 exported shared/package API에서 generic 자체는 목표가 아니다. 둘 이상의 -public 위치 사이 관계를 만들고, 일반 제품 호출부에서 자동 추론되며, 추론 권위가 하나이고, -구체적 오답을 컴파일 실패시키는 경우에만 쓴다. config·schema 정의 경계의 1회 고정은 -허용한다. 하나라도 아니면 concrete type·파생 union·API 분리를 우선한다. - -- 상호 배타 Props는 union + `never`로 표현한다 (`href` 있는 link와 `onClick` 있는 - action, controlled `value`와 uncontrolled `defaultValue`). 전부 optional인 한 - 객체로 만들지 않는다. -- union 상태를 자식에 통째로 내리지 않는다. `Extract` - 로 좁힌 variant만 전달하고, 자식 안에서 재분기하지 않는다. -- 유한한 문자열 집합은 `as const` 상수 하나에서 union·schema·registry를 파생한다. - route·query key·analytics event 이름은 factory를 경유하고 호출부에서 문자열을 - 조립하지 않는다. -- 같은 원시 타입인데 혼동 시 실제 장애가 나는 값(서로 다른 ID, 단위, 검증 완료 - 값)만 tagged/branded type을 쓴다. 생성은 검증 함수 한 곳에 격리한다. 모든 - 문자열을 브랜드화하지 않는다. -- helper는 타입 추론을 보존한다. 반환 타입을 넓게 annotation하거나 호출부에 - generic 반복을 요구하는 helper는 만들지 않는다 (`queryOptions` 패턴). -- literal factory는 `const` type parameter로 호출부의 `as const` 반복 없이 - key·tuple literal 추론을 보존한다. 여러 인자 중 하나만 추론 권위면 나머지 - 인자에 `NoInfer`를 붙여 추론 지점을 하나로 고정한다: - ```typescript - function defineRoutes(paths: T): T - function pick(options: readonly T[], fallback: NoInfer): T - ``` -- 입력을 변경하지 않는 함수는 `readonly T[]`를 받는다. -- mode가 값·반환 타입을 기계적으로 결정하면 generic lookup map으로 관계를 - 보존한다 (`{ single: Id | null; multiple: ReadonlySet }[M]`). mode별 - hook·lifecycle·사용 의미가 다르면 generic 대신 별도 컴포넌트로 나눈다 - (`Calendar.Single`·`Calendar.Range`). 제품이 일부 mode만 쓰면 그 mode만 - 구현하고 mode prop 자체를 만들지 않는다. -- 제품 컴포넌트는 controlled-first — 한 값의 권위를 하나로 유지한다. - uncontrolled 병행 지원은 범용 라이브러리를 만들 때만 한다. -- 닫힌 union(도메인 상태·오류·이벤트)과 소비자가 확장하는 열린 집합(플러그인 - key, 앱 query key)을 구분한다. 열린 집합은 넓은 `string`이 아니라 typed - registry나 module augmentation으로 연다. -- 타입 오류 메시지도 공개 API 품질이다. 소비자가 볼 오류가 "X is not assignable - to CalendarDate | null" 수준으로 읽히지 않으면 generic을 단순화하거나 API를 - 나눈다. -- 소비 루프에 단언이 필요하면 API 형태가 틀린 것이다. union·mapped 타입 config를 - 소비자가 `map`으로 펼치는 순간 key↔value 관계가 끊긴다. 관계는 값 생성 시점에 - 묶고(`accessor(key, { cell })`이 `render(row)`를 반환), 남는 단언은 그 생성 - 함수 안 한 줄로 격리한다. 정의 지점만 닫고 소비 지점에 `as`를 남기는 설계는 - 공용 API 승격 실격이다. -- 공개 variant가 2~3개고 입·출력 타입 관계만 다르면 overload를 검토한다. - variant마다 동작이 다르면 overload 대신 API를 분리한다. -- mapped·conditional·recursive 타입 계산은 공용 라이브러리의 `types/internal`에 - 격리하고 type test를 함께 둔다. feature 컴포넌트 안에 자작 고급 utility를 - 작성하지 않는다. Props에 generic이 4개 이상 노출되면 공개 API 분리를 검토한다. - -## Exhaustiveness 강제 - -dependency 없는 수단부터 쓰고, 라이브러리는 조건이 맞을 때만 도입한다. - -- 기본 (항상, dependency 불필요): 상태별 early return·guard chain 뒤 공용 `assertNever` -- 선언적 매핑 (상태별 결과가 정적 값·render 함수일 때): lookup 객체 + `satisfies Record` -- 라이브러리 (**설치돼 있거나 도입이 승인된 경우만**): `ts-pattern`의 `.exhaustive()` - -variant별 설정(라벨·메시지·핸들러·권한)도 `satisfies Record`로 -전체 union 커버를 강제한다. 새 variant 추가 시 모든 필수 소비 지점이 컴파일 -오류로 드러나야 하며, catch-all 기본 분기로 누락을 숨기지 않는다. - -타입 **형태** 일부는 기계로 잡는다. `@lodado/eslint-config/local-rules`를 쓰는 -레포는 다음 규칙이 이미 켜져 있다. - -- `no-response-type-assertion`: boundary payload를 파싱 대신 `as`로 단언 -- `require-discriminated-state`: `status` literal union 옆의 optional 형제 필드 -- `no-boolean-state-flags`: 한 흐름을 병렬 boolean flag나 boolean `useState` 2개로 표현 -- `no-action-in-state`: state union·state 값 안에 저장된 `retry` 같은 action - -`assertNever`는 레포에 이미 있으면 재사용하고, 없으면 공용 위치 하나에만 만든다. - -## 단언과 `any` 정책 - -제품 코드에서 `value as DomainType`, `as unknown as`, non-null assertion, -`@ts-ignore`, `any`로 타입 오류를 숨기지 않는다. 허용은 네 가지뿐이다: -literal 보존용 `as const`, 검증 함수 내부에 격리된 브랜드 생성자, 라이브러리 -한계를 잇는 adapter 내부 단언, 공용 generic helper 내부 한 지점의 construction -assertion. 마지막은 TypeScript가 점진적 객체 구성을 증명하지 못하는 -`const result = {} as Pick` 같은 경우로, 공개 반환 타입이 입력 generic에서 -기계적으로 도출되고 구현이 그 invariant를 실제로 만들며 소비자 호출부에 `as`가 -전파되지 않을 때만이다 — boundary 값을 도메인 타입으로 바꾸는 데는 쓰지 않는다. -격리된 단언에는 런타임 invariant 근거를 남긴다. 외부 패키지가 `any`를 반환하면 -즉시 `unknown`으로 받아 좁힌다. - -`any` 금지는 application 값 기준이다. `(...args: any[]) => unknown` 같은 callable -constraint처럼 `any`가 generic 연결에만 쓰이고 값으로 읽히거나 공개 반환형·Props로 -누출되지 않으면 `types/internal`·adapter 안에서만 허용한다. `unknown`으로 되는 -자리는 `unknown`을 쓴다. - -타입 오류가 나면 구현이 계약을 위반했는지, 계약이 실제 요구사항과 다른지 먼저 -판정한다. 근거 없이 필수 필드를 optional로 바꾸거나 union을 `string`으로 넓혀서 -오류를 없애지 않는다. - -## 검증 매핑 - -- 카드 행 → 실패 테스트 매핑은 `$test` 계약대로 유지한다. 별도 "타입 테스트 - layer"를 전 상태에 만들지 않는다. -- exported shared/package API로 상태·Props 타입이 노출될 때만 불가능 사용이 - 컴파일되지 않음을 `@ts-expect-error` type test(`.test-d.ts`, JSX면 `.test-d.tsx`, - 또는 vitest `expectTypeOf`)로 증명한다. generic API면 명시적 type argument가 없는 - 대표 정상 호출도 같은 typecheck에서 증명한다. 해당되면 readonly·`as const` - tuple 입력 수용, type predicate narrowing, literal의 `string` widening 미발생도 - 같이 검증한다. 각 `@ts-expect-error`에는 어떤 오용을 차단하는지 한 줄 이유를 - 적는다. 로컬 상태에는 추가하지 않는다. -- Implementation Decision에는 (1) 도출한 상태·이벤트 집합과 카드 행 매핑, - (2) 선택한 사다리 단과 exhaustiveness 계층, (3) **이제 컴파일되지 않는 잘못된 - 사용 목록과 실패 증거**, (4) 타입으로 못 잡아 런타임으로 방어한 행동·시간축 항목을 - 기록한다. - -## Reviewer 판정 기준 - -- boolean 조합이 카드 `Never` 행을 타입상 허용하는데 union으로 만들지 않았으면 - `FINDING`이다. 카드에 없는 전이가 구현에 있으면 `FINDING`이다. -- boundary 값을 파싱 없이 단언했거나, `any`가 application 계층으로 새거나, - `Partial` mutation을 도입했으면 `FINDING`이다. -- 파생 가능한 값을 별도 상태로 저장했거나, query·mutation 상태를 로컬 기계로 - 복사했거나, raw setter를 hook 밖으로 노출했거나, 스키마·연산 union에서 파생 - 가능한 타입을 수기로 복제 선언했으면 `FINDING`이다. -- 기존 query·router·form 상태를 이름만 바꾼 새 `status` union으로 재포장했거나, - 단일 capability만 필요한 공통 UI에 전체 lifecycle 타입을 만들었으면 `FINDING`이다. -- state union이나 state 값에 action을 저장했거나, 쓸 수 없는 상태에 no-op action을 - 채웠거나, 기존 `refetch`가 있는데 같은 일을 하는 action을 새로 만들었으면 - `FINDING`이다. -- 카드의 State Model을 근거로 단순 조회에 Event union·전이 함수·transition command를 - 도입했으면 `FINDING`이다. 사다리 단 선택 사유가 Implementation Decision에 있으면 - 아니다. -- 무조건 실행되는 첫 조회의 loading·error를 경계로 올리지 않고 컴포넌트 안에서 - 분기했으면 `FINDING`이다. 조건부 query·placeholder·취소 제약 같은 실제 실격 - 사유를 Implementation Decision에 적었으면 아니다. -- 사다리 1·2단으로 끝나는 문제에 union·기계를 도입했거나, 상태 분기에 - exhaustiveness 강제(기본 계층 이상)가 없으면 Decision의 예외 사유 없이는 - `FINDING`이다. -- 앞 단 메커니즘으로 닫히는 문제에 뒷 단 타입을 썼거나, feature 코드에 자작 - mapped·conditional·recursive utility가 있거나, 내장 utility 재구현이나 type - test 없는 고급 utility가 있으면 `FINDING`이다. -- 구현이 모든 key를 채우지 않는데 `Record`로 total map을 약속했거나, 실행이 - 지연·캐시되는 wrapper가 즉시 `ReturnType`를 반환한다고 선언했으면 - `FINDING`이다. sparse lookup 결과의 `Partial>`는 - `Partial` mutation 금지의 대상이 아니다 — 둘을 같은 규칙으로 - 금지하면 오적용이다. -- 필수 invariant를 검사하지 않는 type predicate, runtime key 변환 없는 - key-remapping 반환형, `satisfies`·`as const`·annotation·excess property check를 - runtime 검증이나 sanitization으로 보고한 것, 단일 권위 없이 선언 줄 수만 줄이는 - variant record는 `FINDING`이다. -- 이번 변경에서 새로 설계한 exported shared/package API의 generic이 둘 이상의 public - 위치 사이 관계를 만들지 않거나 일반 제품 호출부가 type argument를 반복해야 하면 - `FINDING`이다. config·schema 정의 경계의 1회 고정과 기존 library generic 사용은 - 대상이 아니다. -- 시간축 비결정성을 타입만으로 "해결됨" 처리했으면 `FINDING`이다. -- 생성 후 typecheck를 실행했을 뿐인데 생성 자체를 결정론화했다고 보고하면 - `FINDING`이다. -- 구현 diff가 `.test-d.*`의 `@ts-expect-error` 케이스를 삭제·약화했거나, 계약 - 타입·스키마를 넓혀(필수 필드→optional, union→`string`) 타입 오류를 없앴는데 - 카드 행 인용이 없으면 `FINDING`이다. 계약 파일은 검수의 신뢰 뿌리다 — 완화는 - 구현 결정이 아니라 정책 변경이며 `POLICY_GAP`으로 `NEEDS_DECISION`이다. -- 상태 이름 취향, reducer 대 개별 handler 문법 선호, 패턴 매칭 라이브러리 선호만 - 다르면 `NON_ORACLE_OPINION`이다. diff --git a/packages/frontend-oracle-design/skills/references/type-environment.md b/packages/frontend-oracle-design/skills/references/type-environment.md index 6b1f761..29c96c3 100644 --- a/packages/frontend-oracle-design/skills/references/type-environment.md +++ b/packages/frontend-oracle-design/skills/references/type-environment.md @@ -2,7 +2,7 @@ ## 언제 읽나 -대상 레포에서 이 스킬로 타입 계약([`type-constraints.md`](type-constraints.md))을 +대상 레포에서 이 스킬로 타입 계약([`types/state-ladder.md`](types/state-ladder.md))을 처음 만들기 전 **레포당 1회**, 또는 diff가 tsconfig·TypeScript 버전을 바꿀 때만 읽는다. 매 카드마다 다시 읽지 않는다. diff --git a/packages/frontend-oracle-design/skills/references/types/api-surface.md b/packages/frontend-oracle-design/skills/references/types/api-surface.md new file mode 100644 index 0000000..77094e8 --- /dev/null +++ b/packages/frontend-oracle-design/skills/references/types/api-surface.md @@ -0,0 +1,89 @@ +# 타입 제약 — Props와 공용 API 표면 + +## 공용 API 승격 델타 + +exported shared/package API를 만들거나 바꿀 때는 구현 타입보다 **호출부 먼저** 쓴다. +대표 제품 호출부에서 컴파일러가 추론할 수 있는 component generic을 명시하지 않는 것을 +목표로 하고, config 정의처럼 값만으로 Row를 추론할 수 없는 경계에서만 generic을 한 번 +고정한다. 이후 아래 델타만 설계한다. + +1. 대표 정상 호출부를 명시적 component type argument 없이 작성한다. +2. 변경 전 허용되던 넓은 값·optional 조합·끊어진 관계 중 실제 오용을 적는다. +3. 값·조합·관계·경로·결과·확장([`state-ladder.md`](state-ladder.md)의 여섯 지점) 중 + 이번 API가 닫아야 할 항목만 고른다. +4. schema·config·`as const` 값에서 key와 union을 파생하고 수기 권위를 늘리지 않는다. +5. controlled surface와 현재 제품이 쓰는 mode만 공개하고 나머지는 API 부재로 둔다. +6. type test에 generic 명시 없는 대표 정상 호출 1개와 컴파일되지 않아야 할 사용 최소 + 3개를 함께 둔다. JSX를 쓰면 파일은 `.test-d.tsx`로 만든다. + +정상 호출도 추론되지 않으면 부정 테스트가 통과해도 좋은 공개 API가 아니다. helper의 +추론을 보존하거나 generic을 단순화하고, 호출부가 같은 type argument를 반복하게 두지 +않는다. + +새로 설계하는 exported shared/package API에서 generic 자체는 목표가 아니다. 둘 이상의 +public 위치 사이 관계를 만들고, 일반 제품 호출부에서 자동 추론되며, 추론 권위가 하나이고, +구체적 오답을 컴파일 실패시키는 경우에만 쓴다. config·schema 정의 경계의 1회 고정은 +허용한다. 하나라도 아니면 concrete type·파생 union·API 분리를 우선한다. + +## Props와 API 표면 규칙 + +- 상호 배타 Props는 union + `never`로 표현한다 (`href` 있는 link와 `onClick` 있는 + action, controlled `value`와 uncontrolled `defaultValue`). 전부 optional인 한 + 객체로 만들지 않는다. +- union 상태를 자식에 통째로 내리지 않는다. `Extract` + 로 좁힌 variant만 전달하고, 자식 안에서 재분기하지 않는다. +- 유한한 문자열 집합은 `as const` 상수 하나에서 union·schema·registry를 파생한다. + route·query key·analytics event 이름은 factory를 경유하고 호출부에서 문자열을 + 조립하지 않는다. +- 같은 원시 타입인데 혼동 시 실제 장애가 나는 값(서로 다른 ID, 단위, 검증 완료 + 값)만 tagged/branded type을 쓴다. 생성은 검증 함수 한 곳에 격리한다. 모든 + 문자열을 브랜드화하지 않는다. +- helper는 타입 추론을 보존한다. 반환 타입을 넓게 annotation하거나 호출부에 + generic 반복을 요구하는 helper는 만들지 않는다 (`queryOptions` 패턴). +- literal factory는 `const` type parameter로 호출부의 `as const` 반복 없이 + key·tuple literal 추론을 보존한다. 여러 인자 중 하나만 추론 권위면 나머지 + 인자에 `NoInfer`를 붙여 추론 지점을 하나로 고정한다: + ```typescript + function defineRoutes(paths: T): T + function pick(options: readonly T[], fallback: NoInfer): T + ``` +- 입력을 변경하지 않는 함수는 `readonly T[]`를 받는다. +- mode가 값·반환 타입을 기계적으로 결정하면 generic lookup map으로 관계를 + 보존한다 (`{ single: Id | null; multiple: ReadonlySet }[M]`). mode별 + hook·lifecycle·사용 의미가 다르면 generic 대신 별도 컴포넌트로 나눈다 + (`Calendar.Single`·`Calendar.Range`). 제품이 일부 mode만 쓰면 그 mode만 + 구현하고 mode prop 자체를 만들지 않는다. +- 제품 컴포넌트는 controlled-first — 한 값의 권위를 하나로 유지한다. + uncontrolled 병행 지원은 범용 라이브러리를 만들 때만 한다. +- 닫힌 union(도메인 상태·오류·이벤트)과 소비자가 확장하는 열린 집합(플러그인 + key, 앱 query key)을 구분한다. 열린 집합은 넓은 `string`이 아니라 typed + registry나 module augmentation으로 연다. +- 타입 오류 메시지도 공개 API 품질이다. 소비자가 볼 오류가 "X is not assignable + to CalendarDate | null" 수준으로 읽히지 않으면 generic을 단순화하거나 API를 + 나눈다. +- 소비 루프에 단언이 필요하면 API 형태가 틀린 것이다. union·mapped 타입 config를 + 소비자가 `map`으로 펼치는 순간 key↔value 관계가 끊긴다. 관계는 값 생성 시점에 + 묶고(`accessor(key, { cell })`이 `render(row)`를 반환), 남는 단언은 그 생성 + 함수 안 한 줄로 격리한다. 정의 지점만 닫고 소비 지점에 `as`를 남기는 설계는 + 공용 API 승격 실격이다. +- 공개 variant가 2~3개고 입·출력 타입 관계만 다르면 overload를 검토한다. + variant마다 동작이 다르면 overload 대신 API를 분리한다. +- mapped·conditional·recursive 타입 계산은 공용 라이브러리의 `types/internal`에 + 격리하고 type test를 함께 둔다. feature 컴포넌트 안에 자작 고급 utility를 + 작성하지 않는다. Props에 generic이 4개 이상 노출되면 공개 API 분리를 검토한다. + +## 검증 매핑 + +- 카드 행 → 실패 테스트 매핑은 `$test` 계약대로 유지한다. 별도 "타입 테스트 + layer"를 전 상태에 만들지 않는다. +- exported shared/package API로 상태·Props 타입이 노출될 때만 불가능 사용이 + 컴파일되지 않음을 `@ts-expect-error` type test(`.test-d.ts`, JSX면 `.test-d.tsx`, + 또는 vitest `expectTypeOf`)로 증명한다. generic API면 명시적 type argument가 없는 + 대표 정상 호출도 같은 typecheck에서 증명한다. 해당되면 readonly·`as const` + tuple 입력 수용, type predicate narrowing, literal의 `string` widening 미발생도 + 같이 검증한다. 각 `@ts-expect-error`에는 어떤 오용을 차단하는지 한 줄 이유를 + 적는다. 로컬 상태에는 추가하지 않는다. +- Implementation Decision에는 (1) 도출한 상태·이벤트 집합과 카드 행 매핑, + (2) 선택한 사다리 단과 exhaustiveness 계층, (3) **이제 컴파일되지 않는 잘못된 + 사용 목록과 실패 증거**, (4) 타입으로 못 잡아 런타임으로 방어한 행동·시간축 항목을 + 기록한다. diff --git a/packages/frontend-oracle-design/skills/references/types/authoring.md b/packages/frontend-oracle-design/skills/references/types/authoring.md new file mode 100644 index 0000000..7afa585 --- /dev/null +++ b/packages/frontend-oracle-design/skills/references/types/authoring.md @@ -0,0 +1,155 @@ +# 타입 제약 — 제약 선택 순서와 작성 규칙 + +## 제약 선택 순서 + +먼저 문제가 어느 종류인지 분류한다. 소유권·상태 공간·API 관계는 서로 다른 축이라 +하나의 전역 순서로 섞지 않는다 — `keyof`가 discriminated union보다 항상 뒤라는 +식의 전역 순서는 없다. 각 사다리 안에서만 **앞 단부터** 검토하고, 앞 단으로 실제 +오용이 닫히면 뒷 단을 쓰지 않는다. 생성 결과를 같게 만들려는 규칙이 아니라, +불필요하게 복잡한 뒷 단 메커니즘을 일관되게 탈락시키는 우선순위다. + +```text +A. 소유권·boundary + 기존 owner 재사용 → 저장하지 않고 파생 → API 부재로 불가능하게 + → 외부 값은 unknown에서 runtime parse → schema·config·상수에서 타입 파생 + +B. 상태 공간 + framework union 소비 → capability·API 분리 → union + never + → discriminated union → exhaustive lookup·assertNever + → 순서 위반 자체가 도메인 오류일 때만 transition machine + +C. API 관계 + typeof·as const·satisfies → keyof·indexed access + → 내장 utility (Pick·Omit·Extract·Exclude·Parameters·ReturnType·Awaited) + → 관계형 generic·lookup map → tagged type → const type parameter·NoInfer + → 이산 입출력 관계 2~3개면 overload → 합성 가능한 관계면 mapped·conditional + → 중첩 구조 자체가 계약일 때만 recursive +``` + +trust boundary의 parse는 선택 사항이 아니다. 서로 다른 종류의 문제는 각 사다리에서 +독립적으로 판정하고, 같은 사다리 안에서 앞 단으로 닫히는 문제에 뒷 단을 쓰면 +`FINDING`이다. + +내장 utility로 표현되는 관계(`Awaited`·`ReturnType`·`Parameters`·`Extract`· +`Exclude`·`NonNullable`·`NoInfer`·`Readonly`·`Record`·`Pick`·`Omit`)를 custom +conditional type으로 재구현하지 않는다. 뒷 단 세 개(overload·mapped/conditional· +recursive)는 [`api-surface.md`](api-surface.md)의 격리 조건을 만족할 때만 쓴다. + +## 타입 작성 규칙 + +**선언보다 파생.** 수기 선언은 타입 자체가 정책의 유일한 출처인 닫힌 계약 — +상태·이벤트·실패·연산 union, tagged/branded type, 공개 API의 capability·상호 +배타 Props — 에만 쓴다. schema·config·상수·entity에서 계산 가능한 projection은 +수기로 복제하지 않는다: entity·ID는 `z.output`, 부분집합은 `Extract`·`Exclude`, +유한 문자열 union은 `as const` 상수, 객체 key는 `keyof typeof`, 함수 관계는 +`Parameters`·`ReturnType`·`Awaited`에서 파생한다. 판정 기준은 수기 선언의 개수가 +아니라 **동일한 사실을 둘 이상의 위치가 소유하는지**다. 하나의 정책 사실을 +schema와 interface, 상수와 union이 동시에 소유하면 두 권위가 어긋난다. + +```typescript +type PaymentState = + | { status: 'editing'; amount: number; fieldErrors: FieldErrors } + | { status: 'submitting'; amount: number; requestId: string } + | { status: 'success'; paymentId: string } + | { status: 'failure'; amount: number; reason: PaymentFailure } +``` + +- 단일 `status` 문자열 literal discriminant를 쓴다. boolean 병렬 flag + (`isLoading`·`isError`·`isSuccess`)로 같은 흐름을 표현하지 않는다. +- 각 상태의 필드는 **그 상태에서만 의미 있는 값**만 담는다. 전 상태 공통 optional + 필드로 합치지 않는다. variant 수 자체는 variant record 도입 근거가 아니다 — + 명시 union이 더 읽기 쉬우면 상태가 많아도 유지한다. 같은 record가 상태 union과 + variant별 runtime lookup(config·renderer·메시지·권한) 중 둘 이상을 실제로 + 파생하는 단일 권위일 때만 record에서 union을 파생한다: + ```typescript + type Steps = { + editing: { amount: number; fieldErrors: FieldErrors } + submitting: { amount: number; requestId: string } + } + type State = { [K in keyof Steps]: { status: K } & Steps[K] }[keyof Steps] + const stepLabel = { editing: '입력 중', submitting: '처리 중' } satisfies Record + ``` +- 실패는 카드가 subtype을 구분하면 (`network`·`validation`·`5xx`) `reason`도 + discriminated union으로 만든다. 문자열 하나로 뭉개지 않는다. 예상 가능한 실패를 + 반환값으로 처리해야 하면 `Result` 형태의 닫힌 union을 쓰고, + `throw`는 결함(깨진 invariant) 전용으로 남긴다. +- trust boundary(API 응답, storage, URL, message)의 값은 `unknown`에서 시작해 + **파싱**으로 도메인 타입을 획득한다. 레포에 zod가 있으면 `z.discriminatedUnion()` + 을 쓰고 타입은 `z.output`으로 파생한다 — 스키마와 interface를 중복 선언하지 + 않는다. 응답에 `as DomainType` 단언을 쓰지 않는다. +- mutation payload는 entity의 `Partial`이 아니라 실제 연산 union으로 모델링한다 + (`rename`·`clear-description`처럼). `undefined`가 "유지"인지 "삭제"인지 모호한 + patch 타입을 만들지 않는다. 유지·설정·삭제가 모두 가능하면 연산을 분리한다. + +## 런타임보다 강하게 말하지 않는다 + +타입은 구현이 실제로 보장하는 범위까지만 약속한다. 아래는 컴파일은 되지만 +런타임보다 강한 거짓 계약이다. + +- **`Record`는 totality 계약이다** — 모든 `K`가 결과에 존재한다는 뜻이다. + 구현이 관찰된 key만 채우는 sparse lookup(`groupBy` 결과 등)이면 + `Partial>` 또는 `Map`를 쓴다. 전체 key를 사전 순회로 + 초기화하거나 누락 key에 기본값을 채울 때만 `Record`다. 이것은 + `Partial` mutation 금지와 다른 문제다 — 전자는 연산 의미를 잃는 + patch고, 후자는 일부 key만 런타임에 존재한다는 결과 표현이다. +- **type predicate는 검사 의무가 있다.** `value is T`는 본문이 `T`의 필수 + invariant를 실제로 검사할 때만 쓴다. 항상 `true`를 반환하거나, `as`를 감싸거나, + 일부 필드만 검사하고 전체 도메인 타입을 약속하는 predicate를 만들지 않는다. + boundary의 복잡한 도메인 타입은 predicate 수기 조립 대신 schema parser가 + 우선이고, `isNotNil` 수준의 단순·정확한 narrowing만 predicate로 남긴다. +- **wrapper의 반환 계약은 실행 시점을 따른다.** `Parameters`로 호출 계약은 + 보존하되, `ReturnType`는 wrapper가 같은 호출에서 실제 값을 반환할 때만 + 보존한다. debounce·schedule처럼 실행이 지연되면 즉시 반환형은 `void`, 캐시 + 반환이면 `ReturnType | undefined`, async wrapper면 + `Promise>>`처럼 런타임 의미를 그대로 쓴다. +- **excess property check는 sanitizer가 아니다.** object literal 대입에만 + 적용되며, `const user: PublicUser = source` 같은 annotation은 `source`의 민감 + 필드를 런타임에서 제거하지 않는다. 민감 필드 제거·exact object 보장은 runtime + projection이나 parser가 소유한다. +- **key remapping 반환형은 런타임과 동형이어야 한다.** 함수가 실제로 key를 + 변환하지 않는데 `ToCamelCaseKeys` 같은 key 변환 반환 타입만 붙이면 거짓 + 계약이다. + +## Exhaustiveness 강제 + +dependency 없는 수단부터 쓰고, 라이브러리는 조건이 맞을 때만 도입한다. + +- 기본 (항상, dependency 불필요): 상태별 early return·guard chain 뒤 공용 `assertNever` +- 선언적 매핑 (상태별 결과가 정적 값·render 함수일 때): lookup 객체 + `satisfies Record` +- 라이브러리 (**설치돼 있거나 도입이 승인된 경우만**): `ts-pattern`의 `.exhaustive()` + +variant별 설정(라벨·메시지·핸들러·권한)도 `satisfies Record`로 +전체 union 커버를 강제한다. 새 variant 추가 시 모든 필수 소비 지점이 컴파일 +오류로 드러나야 하며, catch-all 기본 분기로 누락을 숨기지 않는다. + +타입 **형태** 일부는 기계로 잡는다. `@lodado/eslint-config/local-rules`를 쓰는 +레포는 다음 규칙이 이미 켜져 있다. + +- `no-response-type-assertion`: boundary payload를 파싱 대신 `as`로 단언 +- `require-discriminated-state`: `status` literal union 옆의 optional 형제 필드 +- `no-boolean-state-flags`: 한 흐름을 병렬 boolean flag나 boolean `useState` 2개로 표현 +- `no-action-in-state`: state union·state 값 안에 저장된 `retry` 같은 action + +`assertNever`는 레포에 이미 있으면 재사용하고, 없으면 공용 위치 하나에만 만든다. + +## 단언과 `any` 정책 + +제품 코드에서 `value as DomainType`, `as unknown as`, non-null assertion, +`@ts-ignore`, `any`로 타입 오류를 숨기지 않는다. 허용은 네 가지뿐이다: +literal 보존용 `as const`, 검증 함수 내부에 격리된 브랜드 생성자, 라이브러리 +한계를 잇는 adapter 내부 단언, 공용 generic helper 내부 한 지점의 construction +assertion. 마지막은 TypeScript가 점진적 객체 구성을 증명하지 못하는 +`const result = {} as Pick` 같은 경우로, 공개 반환 타입이 입력 generic에서 +기계적으로 도출되고 구현이 그 invariant를 실제로 만들며 소비자 호출부에 `as`가 +전파되지 않을 때만이다 — boundary 값을 도메인 타입으로 바꾸는 데는 쓰지 않는다. +격리된 단언에는 런타임 invariant 근거를 남긴다. 외부 패키지가 `any`를 반환하면 +즉시 `unknown`으로 받아 좁힌다. + +`any` 금지는 application 값 기준이다. `(...args: any[]) => unknown` 같은 callable +constraint처럼 `any`가 generic 연결에만 쓰이고 값으로 읽히거나 공개 반환형·Props로 +누출되지 않으면 `types/internal`·adapter 안에서만 허용한다. `unknown`으로 되는 +자리는 `unknown`을 쓴다. + +타입 오류가 나면 구현이 계약을 위반했는지, 계약이 실제 요구사항과 다른지 먼저 +판정한다. 근거 없이 필수 필드를 optional로 바꾸거나 union을 `string`으로 넓혀서 +오류를 없애지 않는다. diff --git a/packages/frontend-oracle-design/skills/references/types/review-criteria.md b/packages/frontend-oracle-design/skills/references/types/review-criteria.md new file mode 100644 index 0000000..e495acc --- /dev/null +++ b/packages/frontend-oracle-design/skills/references/types/review-criteria.md @@ -0,0 +1,52 @@ +# 타입 제약 — Reviewer 판정 기준 + +타입·상태 계약을 만든 변경을 리뷰할 때 [`state-ladder.md`](state-ladder.md)· +[`authoring.md`](authoring.md)·[`api-surface.md`](api-surface.md)와 같은 기준으로 +판정한다. + +- boolean 조합이 카드 `Never` 행을 타입상 허용하는데 union으로 만들지 않았으면 + `FINDING`이다. 카드에 없는 전이가 구현에 있으면 `FINDING`이다. +- boundary 값을 파싱 없이 단언했거나, `any`가 application 계층으로 새거나, + `Partial` mutation을 도입했으면 `FINDING`이다. +- 파생 가능한 값을 별도 상태로 저장했거나, query·mutation 상태를 로컬 기계로 + 복사했거나, raw setter를 hook 밖으로 노출했거나, 스키마·연산 union에서 파생 + 가능한 타입을 수기로 복제 선언했으면 `FINDING`이다. +- 기존 query·router·form 상태를 이름만 바꾼 새 `status` union으로 재포장했거나, + 단일 capability만 필요한 공통 UI에 전체 lifecycle 타입을 만들었으면 `FINDING`이다. +- state union이나 state 값에 action을 저장했거나, 쓸 수 없는 상태에 no-op action을 + 채웠거나, 기존 `refetch`가 있는데 같은 일을 하는 action을 새로 만들었으면 + `FINDING`이다. +- 카드의 State Model을 근거로 단순 조회에 Event union·전이 함수·transition command를 + 도입했으면 `FINDING`이다. 사다리 단 선택 사유가 Implementation Decision에 있으면 + 아니다. +- 무조건 실행되는 첫 조회의 loading·error를 경계로 올리지 않고 컴포넌트 안에서 + 분기했으면 `FINDING`이다. 조건부 query·placeholder·취소 제약 같은 실제 실격 + 사유를 Implementation Decision에 적었으면 아니다. +- 사다리 1·2단으로 끝나는 문제에 union·기계를 도입했거나, 상태 분기에 + exhaustiveness 강제(기본 계층 이상)가 없으면 Decision의 예외 사유 없이는 + `FINDING`이다. +- 앞 단 메커니즘으로 닫히는 문제에 뒷 단 타입을 썼거나, feature 코드에 자작 + mapped·conditional·recursive utility가 있거나, 내장 utility 재구현이나 type + test 없는 고급 utility가 있으면 `FINDING`이다. +- 구현이 모든 key를 채우지 않는데 `Record`로 total map을 약속했거나, 실행이 + 지연·캐시되는 wrapper가 즉시 `ReturnType`를 반환한다고 선언했으면 + `FINDING`이다. sparse lookup 결과의 `Partial>`는 + `Partial` mutation 금지의 대상이 아니다 — 둘을 같은 규칙으로 + 금지하면 오적용이다. +- 필수 invariant를 검사하지 않는 type predicate, runtime key 변환 없는 + key-remapping 반환형, `satisfies`·`as const`·annotation·excess property check를 + runtime 검증이나 sanitization으로 보고한 것, 단일 권위 없이 선언 줄 수만 줄이는 + variant record는 `FINDING`이다. +- 이번 변경에서 새로 설계한 exported shared/package API의 generic이 둘 이상의 public + 위치 사이 관계를 만들지 않거나 일반 제품 호출부가 type argument를 반복해야 하면 + `FINDING`이다. config·schema 정의 경계의 1회 고정과 기존 library generic 사용은 + 대상이 아니다. +- 시간축 비결정성을 타입만으로 "해결됨" 처리했으면 `FINDING`이다. +- 생성 후 typecheck를 실행했을 뿐인데 생성 자체를 결정론화했다고 보고하면 + `FINDING`이다. +- 구현 diff가 `.test-d.*`의 `@ts-expect-error` 케이스를 삭제·약화했거나, 계약 + 타입·스키마를 넓혀(필수 필드→optional, union→`string`) 타입 오류를 없앴는데 + 카드 행 인용이 없으면 `FINDING`이다. 계약 파일은 검수의 신뢰 뿌리다 — 완화는 + 구현 결정이 아니라 정책 변경이며 `POLICY_GAP`으로 `NEEDS_DECISION`이다. +- 상태 이름 취향, reducer 대 개별 handler 문법 선호, 패턴 매칭 라이브러리 선호만 + 다르면 `NON_ORACLE_OPINION`이다. diff --git a/packages/frontend-oracle-design/skills/references/types/state-ladder.md b/packages/frontend-oracle-design/skills/references/types/state-ladder.md new file mode 100644 index 0000000..28ddca2 --- /dev/null +++ b/packages/frontend-oracle-design/skills/references/types/state-ladder.md @@ -0,0 +1,145 @@ +# 타입 제약 — 목적·소유권·상태 설계 사다리 + +## 목적과 권위 + +카드에 async·순서 역전·중복 제출·retry·다단계 상태 `O*` 행이 있거나, client 상태· +Props·boundary 타입의 형태를 새로 만들거나 바꿀 때 사용한다. 이 문서는 제품 정책을 +만들지 않는다. 상태·전이·오류 분류는 전부 카드의 `O*` 행에서 도출하며, 카드에 없는 +상태나 전이가 필요해지면 발명하지 말고 `POLICY_GAP`으로 `NEEDS_DECISION`에 돌아간다. + +권위 순서는 [`common.md`](../common.md)의 공통 우선순위와 +[`frontend-implementation.md`](../frontend-implementation.md)를 따른다. +이 문서의 도구·라이브러리 선택은 구현 휴리스틱이며 정책 출처가 아니다. + +모든 설계는 다음 질문으로 판정한다. + +> AI가 생성할 수 있었던 잘못된 코드 중 **무엇이 이제 컴파일되지 않는가?** + +이 질문에 구체적으로 답할 수 없는 타입 복잡성은 추가하지 않는다. AI 생성 자체는 계속 +비결정적이다. 이 문서의 목표는 동일한 source·TypeScript·tsconfig에서 후보를 같은 +결과로 통과·거절하는 **수용 판정 결정성**이다. + +컴파일 통과는 건전성 증명이 아니라 결정적 고효율 필터다. TypeScript는 의도적으로 +불건전하고(bivariance, 리터럴에만 적용되는 excess property check), 필터 강도는 +tsconfig·컴파일러 버전의 함수다. 전제 환경 검증은 +[`type-environment.md`](../type-environment.md)가 소유한다 — 레포당 1회 검증하고 +여기서는 반복하지 않는다. + +## 제약 소유권 + +- 값·Props·상태 조합·입출력 관계 → 타입 +- API·storage·URL·message 같은 외부 입력 → `unknown`에서 runtime parser +- 관찰 가능한 제품 행동 → `$test` +- 순서 역전·중복 제출·retry·unmount 후 도착 → abort signal·pending guard·멱등키·서버 검증 +- 같은 prompt의 생성 재현성 → 모델·provider — 이 문서가 보장하지 않음 + +시간축은 타입으로 증명되지 않는다. union을 만들었다고 순서 문제가 "해결됨"이라고 +선언하면 `FINDING`이다. 남은 시간축 비결정성과 그 런타임 방어는 Implementation +Decision에 반드시 기록한다. type-valid를 behavior-correct로 보고해도 `FINDING`이다. + +설계 전에 변경 대상에서 아래 여섯 지점을 찾고, **컴파일되지 않아야 할 잘못된 +사용을 최소 세 개 먼저 적는다** — exported API면 그대로 `.test-d.ts`의 +`@ts-expect-error` 케이스가 된다. + +- 값: 넓은 `string`·`number`·`Date` → 브랜드·의미 타입 +- 조합: 관련 boolean 여러 개, 배타적인 optional Props → discriminated union, union + `never` +- 관계: mode가 값·반환 타입을 결정하는데 타입에 없음 → generic lookup map, 별도 컴포넌트 +- 경로·키: route·query key·field path 자유 문자열 → factory·`keyof`·파생 union +- 결과: 성공·실패·부재·유지·삭제가 `undefined` 하나에 → `Result`·연산 union +- 확장: 소비자가 확장할 key가 `string`으로 열림 → typed registry·module augmentation + +## 상태 설계 사다리 + +union 작성은 3단이다. 1·2단에서 끝나는 문제에 3단을 쓰지 않는다. + +1. **파생 가능하면 저장하지 않는다.** 원본에서 계산한다(`itemCount = items.length`). + 타입이 강해도 중복 저장된 상태는 한쪽만 갱신된다. +2. **라이브러리가 이미 union을 소유하면 그대로 소비한다.** TanStack Query의 + `status`·`fetchStatus`, mutation의 `isPending`/`isSuccess`/`isError`는 이미 + discriminated contract고 최신 호출 기준 시간축 처리까지 포함한다. 같은 상태를 + `useState` 기계로 복사하지 않는다. 필수 파라미터가 없으면 non-null assertion + 대신 `skipToken` 또는 API 부재로 표현한다. + **로딩·로드 실패의 기본은 컴포넌트 분기가 아니라 경계다.** 무조건 실행되는 첫 + 조회는 `useSuspenseQuery`를 기본으로 두고 loading·error 분기를 국소 + ``와 Error Boundary로 올려 컴포넌트 본문에서 제거한다. 상황별 판정 + 표는 [`frontend-implementation.md`](../frontend-implementation.md) 3절이 소유하며 + 이 문서가 그 기본값을 덮지 않는다. 조건부 query·placeholder·취소 제약처럼 + 경계로 올릴 수 없는 나머지에만 분기를 남기고, 그때도 자작 union 없이 라이브러리 + union에 ts-pattern을 직접 물린다 + (`match(mutation).with({ status: 'error', error: { code: 'CONFLICT' } }, …)`). + 같은 이유로 **기존 query·framework 상태를 최우선으로 재사용한다.** 이미 있는 + query API·router state·form state로 표현되는 서버 상태를 직접 관리하는 hook으로 + 다시 만들지 않는다. 카드가 요구하는 데이터가 기존 경계에 없을 때만 3단으로 + 내려간다. +3. **그래도 남는 진짜 client 상태만 `useState` + 의도 함수 hook으로 만든다.** + raw `setState`·setter를 hook 밖으로 노출하지 않고, 도메인 의도를 표현하는 함수 + (`pick`, `submit`, `reset`)만 반환한다. 잘못된 상태에서 온 호출은 카드가 정한 + 대로 무시·오류 처리하고, 카드에 없으면 `POLICY_GAP`으로 `NEEDS_DECISION`이다. + +reducer·전이표·상태 기계는 기본값이 아니다. 순서 위반 자체가 카드의 도메인 오류인 +흐름(결제·다단계 제출·낙관적 롤백)에서만, 새 state-machine dependency는 필요가 +입증될 때만 쓴다. XState는 계층·병렬 상태나 actor 조율이 카드에 실제로 있을 때만 +후보이며, 설치돼 있거나 도입이 승인된 경우만 쓴다. + +**카드에 `## State Model`이 있다는 사실만으로** Event union·전이 함수·transition +command 같은 런타임 기계를 만들지 않는다. 카드의 State Model은 정책을 빠짐없이 +적어 두는 표기이고, 그 정책을 무엇으로 구현할지는 이 사다리가 정한다. 단순 조회 +하나의 로딩·성공·실패는 2단에서 끝나며, 3단에서도 필요한 것은 상태 union 하나와 +의도 함수 몇 개다. + +### 상태 소유권은 하나다 + +하나의 async 흐름에는 canonical state owner를 하나만 둔다. TanStack Query처럼 +framework가 상태를 소유하면 그 결과를 `NextPageState` 같은 새 `status` union으로 +재포장하거나 같은 의미의 application 타입으로 복제하지 않는다. 공통 UI가 전체 +lifecycle이 아니라 다음 행동 가능 여부만 필요하면 `onLoadMore?: () => void`처럼 +callback 존재 자체가 capability가 되게 한다. loading·error·retry 표시는 원래 query를 +소유한 feature가 렌더링한다. 카드의 State Model은 정책 명세이지 구현마다 union을 +생성하라는 명령이 아니다. + +## 상태는 데이터, action은 형제 + +**상태는 데이터만 담는다.** union 멤버의 필드는 그 상태에서 참인 값이고, action은 +그 값으로 다음에 할 수 있는 일이다. 둘은 수명이 다르므로 한 값에 섞지 않는다. + +- `retry`·`submit`·`reset` 같은 함수를 state 값에 저장하지 않는다. 저장한 함수는 + 그것을 만든 render의 closure에 고정되므로, 이후 props·param이 바뀌어도 낡은 값을 + 계속 캡처한다. `@lodado/eslint-config/local-rules`를 쓰는 레포에서는 + `no-action-in-state`가 타입과 값 양쪽에서 이 형태를 잡는다. +- action은 **hook 반환 객체의 sibling**으로 준다 (`{ state, retry }`). 서버 상태면 + 새 action을 만들지 말고 query의 `refetch`를 그대로 다시 노출한다. +- 어떤 상태에서 쓸 수 없는 action은 만들지 않는다. `retry: () => undefined`처럼 + 타입을 맞추려고 넣는 no-op action은 UI에 "재시도할 수 있다"는 거짓 정보를 준다. + action이 상태별로 달라지면 상태를 좁힌 자식에 좁힌 action을 넘긴다. +- 잘못된 입력(파싱 실패한 ID, 없는 route param)은 **UI 동작·문구가 실제로 다를 때만 + 별도 상태로 나눈다.** 화면과 복구 경로가 같으면 기존 실패 상태에 합치고, 나누면 + 그 상태만의 필드와 action을 각각 채운다. 카드에 구분이 없으면 발명하지 말고 + `POLICY_GAP`으로 `NEEDS_DECISION`이다. + +```typescript +// 금지 — 상태에 action이 들어가 stale closure와 가짜 retry가 동시에 생긴다 +type DetailState = { status: 'loading' } | { status: 'failure'; retry: () => void } + +// 허용 — 상태는 데이터, action은 형제 +type DetailState = { status: 'loading' } | { status: 'failure'; reason: LoadFailure } +function useDetail(id: DetailId): { state: DetailState; retry: () => void } +``` + +## 카드에서 상태를 도출한다 + +카드에 `## State Model` 섹션이 있으면 그것이 상태·이벤트·전이의 유일한 출처다. +섹션은 선택이라 대부분의 카드에는 없다 — 없으면 `O*` 행의 `Given`·`When`·`Then`에서 +직접 도출하며, 섹션이 없다는 사실은 전이표·상태 기계를 만들 이유가 아니라 만들지 +않을 이유다. + +| 카드 열 | 타입 대응 | +| -------- | -------------------------------------------------------- | +| `Given` | 출발 상태와 그 상태에서만 유효한 필드 | +| `When` | 이벤트 (사용자 행동, 응답, 시간·순서 변화) | +| `Then` | 도착 상태와 관찰 결과 | +| `Never` | 금지 상태 또는 금지 전이 — 타입으로 표현 불가하게 만든다 | +| `부작용` | 전이에 결합된 외부 write의 종류와 횟수 | + +`상태 × 이벤트`에서 빈 조합은 불가능(타입으로 표현 불가)인지 미결 정책인지 +구분한다. 미결이면 `NEEDS_DECISION`이며 "아마 무시"를 기본값으로 채우지 않는다. +카드 행 ID를 참조하지 않는 전이는 발명된 정책이다. diff --git a/packages/frontend-oracle-design/skills/references/visual-design.md b/packages/frontend-oracle-design/skills/references/visual-design.md index 929268f..5abad67 100644 --- a/packages/frontend-oracle-design/skills/references/visual-design.md +++ b/packages/frontend-oracle-design/skills/references/visual-design.md @@ -179,7 +179,7 @@ owner의 `pending`으로 남긴다. `$frontend-visual-qa`는 다음만 반환한 있는 `N/A`는 어느 계층에나 가능. - 외부 visual QA 결과를 위해 이 스킬의 상태를 추가하지 않는다. -피드백은 기존 라우터를 그대로 사용: +피드백은 [`common.md`](common.md)의 canonical 라우터를 그대로 사용 — 시각 관할 매핑: - 승인 Figma·Design Intent와 실제 UI 불일치 → `PRODUCT_DEFECT` - source의 시각 요구가 카드에 누락되거나 source끼리 충돌 → `POLICY_GAP` diff --git a/packages/frontend-oracle-design/skills/scripts/oracle-run.mjs b/packages/frontend-oracle-design/skills/scripts/oracle-run.mjs index 77664fa..feaed43 100644 --- a/packages/frontend-oracle-design/skills/scripts/oracle-run.mjs +++ b/packages/frontend-oracle-design/skills/scripts/oracle-run.mjs @@ -33,6 +33,7 @@ const FLAG_NAMES = [ 'spend', 'output', 'decision', + 'review-point', ] const REQUIRED_CONSECUTIVE_PASSES = { low: 1, medium: 2, high: 3 } @@ -85,7 +86,7 @@ class CliError extends Error { } function parseOptions(args) { - const options = { command: null, requiredLabels: [], harnessPaths: [], milestones: [] } + const options = { command: null, requiredLabels: [], harnessPaths: [], milestones: [], reviewPoints: [] } for (let index = 0; index < args.length; index += 1) { const flag = args[index] @@ -105,6 +106,7 @@ function parseOptions(args) { if (name === 'required-label') options.requiredLabels.push(value) else if (name === 'harness-path') options.harnessPaths.push(value) else if (name === 'milestone') options.milestones.push(value) + else if (name === 'review-point') options.reviewPoints.push(value) else options[name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())] = value index += 1 } @@ -1199,6 +1201,34 @@ async function reviewPacket(options) { } } + const reviewPoints = [] + const seenReviewPoints = new Set() + for (const point of options.reviewPoints) { + const pointPath = resolve(point) + if (seenReviewPoints.has(pointPath)) { + throw new CliError('REVIEW_POINT_INVALID', `Duplicate review point: ${point}`) + } + seenReviewPoints.add(pointPath) + + let metadata + try { + metadata = await lstat(pointPath) + } catch (error) { + throw new CliError('REVIEW_POINT_INVALID', `Cannot read review point: ${error.message}`) + } + if (!metadata.isFile() || metadata.isSymbolicLink()) { + throw new CliError('REVIEW_POINT_INVALID', '--review-point must be a regular file') + } + + const content = await readFile(pointPath, 'utf8') + if (!content.trim()) { + throw new CliError('REVIEW_POINT_INVALID', '--review-point cannot be empty') + } + // 링크만 전달한다 — reviewer가 경로의 파일을 직접 전부 읽고, digest로 어떤 + // revision의 기준을 읽었는지 고정한다. 본문을 packet에 복제하지 않는다. + reviewPoints.push({ path: point, sha256: sha256(content) }) + } + const state = await readState(directory) const revision = verifyLock(directory, state) const lock = resolve(directory, state.lock) @@ -1265,6 +1295,7 @@ async function reviewPacket(options) { ledger: await readLedger(directory), evidence, ...(implementationDecision ? { implementationDecision } : {}), + ...(reviewPoints.length ? { reviewPoints } : {}), changedFiles, diff: gitDiff(scanRoot, changed, state.snapshot, current), pending, diff --git a/packages/frontend-oracle-design/skills/scripts/oracle-run.test.mjs b/packages/frontend-oracle-design/skills/scripts/oracle-run.test.mjs index 5659290..f4fe2e1 100644 --- a/packages/frontend-oracle-design/skills/scripts/oracle-run.test.mjs +++ b/packages/frontend-oracle-design/skills/scripts/oracle-run.test.mjs @@ -1336,3 +1336,72 @@ test('O17: High risk REVIEW_VERIFIED는 GREEN 이후 mutation kill 증거를 요 assert.equal(current.history.at(-1).mutationRunId, 'r-007') assert.equal(current.history.at(-1).mutationRow, 'O1') }) + +test('review-packet은 리뷰 포인트를 본문 없이 path·digest 링크로만 기록한다', async (t) => { + const { root, oracleDirectory } = await workspace(t) + const criteria = join(root, 'criteria.md') + const content = '# Review criteria\n\n- 다섯 축 판정\n' + await writeFile(criteria, content) + + const output = join(oracleDirectory, 'review-input-points.json') + const generated = run([ + 'review-packet', + '--dir', + oracleDirectory, + '--review-point', + criteria, + '--output', + output, + ]) + + assert.equal(generated.status, 0, generated.stderr) + const packet = JSON.parse(await readFile(output, 'utf8')) + assert.deepEqual(packet.reviewPoints, [ + { path: criteria, sha256: createHash('sha256').update(content).digest('hex') }, + ]) + + const missing = run([ + 'review-packet', + '--dir', + oracleDirectory, + '--review-point', + join(root, 'absent.md'), + '--output', + join(oracleDirectory, 'missing.json'), + ]) + assert.equal(missing.status, 1) + assert.match(missing.stderr, /^REVIEW_POINT_INVALID: /) + + const empty = join(root, 'empty-criteria.md') + await writeFile(empty, ' \n') + const emptyResult = run([ + 'review-packet', + '--dir', + oracleDirectory, + '--review-point', + empty, + '--output', + join(oracleDirectory, 'empty-points.json'), + ]) + assert.equal(emptyResult.status, 1) + assert.match(emptyResult.stderr, /^REVIEW_POINT_INVALID: /) + + const duplicate = run([ + 'review-packet', + '--dir', + oracleDirectory, + '--review-point', + criteria, + '--review-point', + criteria, + '--output', + join(oracleDirectory, 'duplicate-points.json'), + ]) + assert.equal(duplicate.status, 1) + assert.match(duplicate.stderr, /^REVIEW_POINT_INVALID: /) + + const without = run(['review-packet', '--dir', oracleDirectory, '--output', join(oracleDirectory, 'no-points.json')]) + assert.equal(without.status, 0, without.stderr) + const bare = JSON.parse(await readFile(join(oracleDirectory, 'no-points.json'), 'utf8')) + assert.equal('reviewPoints' in bare, false) +}) diff --git a/packages/frontend-oracle-design/skills/scripts/skill-contract.test.mjs b/packages/frontend-oracle-design/skills/scripts/skill-contract.test.mjs index 85d9699..f8bd193 100644 --- a/packages/frontend-oracle-design/skills/scripts/skill-contract.test.mjs +++ b/packages/frontend-oracle-design/skills/scripts/skill-contract.test.mjs @@ -11,6 +11,34 @@ async function read(relativePath) { return readFile(join(skillDirectory, relativePath), 'utf8') } +const CARD_NODE_FILES = [ + 'references/card/policy-sources.md', + 'references/card/risk-grill.md', + 'references/card/card-format.md', + 'references/card/confirmation-lock.md', +] +const DELIVERY_NODE_FILES = [ + 'references/delivery/ledger.md', + 'references/delivery/red.md', + 'references/delivery/implementation-decision.md', + 'references/delivery/green-review.md', +] +const TYPES_NODE_FILES = [ + 'references/types/state-ladder.md', + 'references/types/authoring.md', + 'references/types/api-surface.md', + 'references/types/review-criteria.md', +] + +async function readAll(relativePaths) { + const parts = await Promise.all(relativePaths.map(read)) + return parts.join('\n') +} + +const readCard = () => readAll(CARD_NODE_FILES) +const readDelivery = () => readAll(DELIVERY_NODE_FILES) +const readTypes = () => readAll(TYPES_NODE_FILES) + test('runs the Oracle contract through the bundled deterministic workflow graph', async () => { const [skill, graphOrchestration, graphSource] = await Promise.all([ read('SKILL.md'), @@ -54,7 +82,7 @@ test('O26: backs reported verification with a run ledger, machine transitions an }) test('O27: lints the card structure and initializes run artifacts around the lock', async () => { - const oracleCard = await read('references/oracle-card.md') + const oracleCard = await readCard() assert.match(oracleCard, /oracle-verify\.mjs card/) assert.match(oracleCard, /CARD_LINT_FAILED/) @@ -75,7 +103,7 @@ test('O27: lints the card structure and initializes run artifacts around the loc }) test('O28: routes delivery runs through exec and gates GREEN on flakiness and test strength', async () => { - const implementationLoop = await read('references/implementation-loop.md') + const implementationLoop = await readDelivery() assert.match(implementationLoop, /oracle-run\.mjs exec/) assert.match(implementationLoop, /oracle-run\.mjs transition/) @@ -141,7 +169,7 @@ test('O30: delegates screenshot and direct-browser execution to a separate skill }) test('requires automatic deterministic locking at delivery boundaries', async () => { - const [skill, oracleCard] = await Promise.all([read('SKILL.md'), read('references/oracle-card.md')]) + const [skill, oracleCard] = await Promise.all([read('SKILL.md'), readCard()]) assert.match(skill, /scripts\/oracle-lock\.mjs/) assert.match(skill, /각 단계 직전 revision lock을 자동 검증/) @@ -155,8 +183,8 @@ test('requires automatic deterministic locking at delivery boundaries', async () test('locks all approved Delivery sources once instead of extending an existing lock', async () => { const [skill, oracleCard, implementationLoop] = await Promise.all([ read('SKILL.md'), - read('references/oracle-card.md'), - read('references/implementation-loop.md'), + readCard(), + readDelivery(), ]) assert.match(skill, /Delivery.*lock.*미룬다/s) @@ -167,7 +195,7 @@ test('locks all approved Delivery sources once instead of extending an existing }) test('keeps feedback routing and evidence tied to the locked revision', async () => { - const [skill, implementationLoop] = await Promise.all([read('SKILL.md'), read('references/implementation-loop.md')]) + const [skill, implementationLoop] = await Promise.all([read('SKILL.md'), readDelivery()]) for (const classification of [ 'POLICY_GAP', @@ -188,7 +216,7 @@ test('keeps feedback routing and evidence tied to the locked revision', async () test('carries the locked revision through tests and review without owning visual QA', async () => { const [skill, implementationLoop, subagentReview] = await Promise.all([ read('SKILL.md'), - read('references/implementation-loop.md'), + readDelivery(), read('references/subagent-review.md'), ]) @@ -225,15 +253,15 @@ test('keeps Oracle plugin release metadata versions aligned', async () => { const marketplace = JSON.parse(marketplaceJson) const marketplaceVersion = marketplace.plugins.find(({ name }) => name === 'frontend-oracle-design')?.version - assert.equal(version, '0.17.6') + assert.equal(version, '0.18.4') assert.equal(JSON.parse(claudePluginJson).version, version) assert.equal(JSON.parse(codexPluginJson).version, version) assert.equal(marketplaceVersion, version) - assert.equal(marketplace.version, '0.17.6') + assert.equal(marketplace.version, '0.18.4') }) test('separates requested mechanism from intended outcome without letting the agent shrink scope', async () => { - const oracleCard = await read('references/oracle-card.md') + const oracleCard = await readCard() assert.match(oracleCard, /Requested mechanism check — 수단과 결과 분리/) assert.match(oracleCard, /Intended outcome/) @@ -269,7 +297,7 @@ test('loads the performance reference only for measured performance claims', asy test('records new dependency decisions and reviews them against real problems and context', async () => { const [implementationLoop, subagentReview] = await Promise.all([ - read('references/implementation-loop.md'), + readDelivery(), read('references/subagent-review.md'), ]) @@ -284,7 +312,7 @@ test('records new dependency decisions and reviews them against real problems an test('reuses the repository network boundary and colocates approved MSW handlers', async () => { const [skill, implementationLoop, fsd] = await Promise.all([ read('SKILL.md'), - read('references/implementation-loop.md'), + readDelivery(), read('references/fsd.md'), ]) @@ -299,7 +327,7 @@ test('reuses the repository network boundary and colocates approved MSW handlers }) test('explicitly invokes $test before writing frontend tests', async () => { - const [skill, implementationLoop] = await Promise.all([read('SKILL.md'), read('references/implementation-loop.md')]) + const [skill, implementationLoop] = await Promise.all([read('SKILL.md'), readDelivery()]) assert.match(skill, /테스트 파일을 작성하기 직전에 `\$test` 스킬을 이름으로 명시적으로 로드·호출/) assert.match(skill, /테스트 파일 작성 직전에 `\$test` 스킬을 명시적으로 호출/) @@ -309,7 +337,7 @@ test('explicitly invokes $test before writing frontend tests', async () => { test('batches delivery decisions without prescribing an implementation topology', async () => { const [implementationLoop, graphSource] = await Promise.all([ - read('references/implementation-loop.md'), + readDelivery(), read('references/oracle-workflow.graph.json'), ]) const graph = JSON.parse(graphSource) @@ -408,7 +436,7 @@ test('loads visual design guidance only for UI-shaping work and carries its cont const [skill, visualDesign, oracleCard, frontendImplementation, subagentReview] = await Promise.all([ read('SKILL.md'), read('references/visual-design.md'), - read('references/oracle-card.md'), + readCard(), read('references/frontend-implementation.md'), read('references/subagent-review.md'), ]) @@ -460,7 +488,7 @@ test('O1-O7: loads one detailed changeability reference before implementation de read('SKILL.md'), read('references/changeability.md'), read('references/frontend-implementation.md'), - read('references/implementation-loop.md'), + readDelivery(), ]) for (const term of ['Readability', 'Predictability', 'Cohesion', 'Coupling', 'Simplicity']) { @@ -488,7 +516,7 @@ test('O1-O7: loads one detailed changeability reference before implementation de assert.doesNotMatch(changeability, /^## Implementation Decision 형식$/m) assert.doesNotMatch(changeability, /^## Reviewer 판정과 최소 수정$/m) assert.match(changeability, /frontend-implementation\.md/) - assert.match(changeability, /implementation-loop\.md/) + assert.match(changeability, /delivery\/implementation-decision\.md/) assert.match(changeability, /subagent-review\.md/) assert.match(skill, /references\/changeability\.md/) @@ -547,8 +575,8 @@ test('O8-O10: reviews with the same changeability reference without turning tast test('O2: 기존 Oracle Delivery gate를 유지한다', async () => { const [skill, oracleCard, implementationLoop] = await Promise.all([ read('SKILL.md'), - read('references/oracle-card.md'), - read('references/implementation-loop.md'), + readCard(), + readDelivery(), ]) for (const contract of ['VALID_RED', 'oracle-lock.mjs', 'oracle-run.mjs', 'evidence.json']) { @@ -557,7 +585,7 @@ test('O2: 기존 Oracle Delivery gate를 유지한다', async () => { }) test('pins document-driven stage journal and disk recall', async () => { - const [skill, oracleCard] = await Promise.all([read('SKILL.md'), read('references/oracle-card.md')]) + const [skill, oracleCard] = await Promise.all([read('SKILL.md'), readCard()]) assert.match(skill, /### 문서 기준 진행/) assert.match(skill, /대화 기억이 아니라 disk를 재독/) @@ -574,7 +602,7 @@ test('pins document-driven stage journal and disk recall', async () => { test('pins the system-design grill phases and the conditional API contract format', async () => { const [skill, oracleCard, architectureContract] = await Promise.all([ read('SKILL.md'), - read('references/oracle-card.md'), + readCard(), read('references/architecture-contract.md'), ]) @@ -608,9 +636,9 @@ test('pins the system-design grill phases and the conditional API contract forma test('O7: 조건부 품질 계약과 human-first 보고를 안내한다', async () => { const [skill, oracleCard, frontendImplementation, implementationLoop, architecture, review] = await Promise.all([ read('SKILL.md'), - read('references/oracle-card.md'), + readCard(), read('references/frontend-implementation.md'), - read('references/implementation-loop.md'), + readDelivery(), read('references/architecture-contract.md'), read('references/subagent-review.md'), ]) @@ -630,7 +658,7 @@ test('O7: 조건부 품질 계약과 human-first 보고를 안내한다', async test('O8: 카드 schema version 분기와 migration을 추가하지 않는다', async () => { const [oracleCard, verifier] = await Promise.all([ - read('references/oracle-card.md'), + readCard(), read('scripts/oracle-verify.mjs'), ]) @@ -641,9 +669,9 @@ test('O8: 카드 schema version 분기와 migration을 추가하지 않는다', test('type-constraints: derives state contracts from card rows and narrows AI choice space', async () => { const [skill, oracleCard, frontendImplementation, typeConstraints, verifier] = await Promise.all([ read('SKILL.md'), - read('references/oracle-card.md'), + readCard(), read('references/frontend-implementation.md'), - read('references/type-constraints.md'), + readTypes(), read('scripts/oracle-verify.mjs'), ]) @@ -653,7 +681,7 @@ test('type-constraints: derives state contracts from card rows and narrows AI ch assert.match(verifier, /state-model-row-unlinked/) assert.match(verifier, /state-model-row-unknown/) - assert.match(skill, /references\/type-constraints\.md/) + assert.match(skill, /references\/types\/state-ladder\.md/) assert.match(skill, /client state·exported Props/) assert.match(skill, /shared\/package API·trust boundary/) assert.doesNotMatch(skill, /state-modeling\.md/) @@ -662,7 +690,7 @@ test('type-constraints: derives state contracts from card rows and narrows AI ch assert.match(oracleCard, /State Model — 선택 사항/) assert.match(oracleCard, /섹션이 없다고\s*\n?\s*lint가 막지 않는다/) assert.match(oracleCard, /참조 없는 전이는 발명된 정책이다/) - assert.match(frontendImplementation, /type-constraints\.md/) + assert.match(frontendImplementation, /types\/state-ladder\.md/) assert.match(frontendImplementation, /client state·/) assert.match(frontendImplementation, /exported Props·shared\/package API·trust boundary/) assert.doesNotMatch(frontendImplementation, /state-modeling\.md/) @@ -693,7 +721,7 @@ test('type-constraints: derives state contracts from card rows and narrows AI ch test('type-environment: pins compiler environment once per repo and protects contract files', async () => { const [skill, typeConstraints, typeEnvironment] = await Promise.all([ read('SKILL.md'), - read('references/type-constraints.md'), + readTypes(), read('references/type-environment.md'), ]) @@ -725,7 +753,7 @@ test('type-environment: pins compiler environment once per repo and protects con test('prefers Suspense and Error Boundary over in-component loading branches', async () => { const [frontendImplementation, typeConstraints, subagentReview] = await Promise.all([ read('references/frontend-implementation.md'), - read('references/type-constraints.md'), + readTypes(), read('references/subagent-review.md'), ]) @@ -738,12 +766,12 @@ test('prefers Suspense and Error Boundary over in-component loading branches', a assert.match(frontendImplementation, /startTransition/) assert.match(frontendImplementation, /throwOnError.*data 부재 조건으로 좁혀/s) - assert.match(subagentReview, /실격 사유가 Implementation\n {2}Decision에 없으면 `FINDING`이다/) + assert.match(subagentReview, /types\/review-criteria\.md/) }) test('keeps client state data-only and hands actions back beside it', async () => { const [typeConstraints, frontendImplementation, subagentReview] = await Promise.all([ - read('references/type-constraints.md'), + readTypes(), read('references/frontend-implementation.md'), read('references/subagent-review.md'), ]) @@ -773,5 +801,135 @@ test('keeps client state data-only and hands actions back beside it', async () = assert.match(frontendImplementation, /state와 action을 형제로 반환한다/) // 리뷰는 같은 계약으로 판정한다 - assert.match(subagentReview, /state에 저장된 action/) + assert.match(subagentReview, /state union과 action 배치/) +}) + +test('declares every reference as a loadable graph node with resolvable edges', async () => { + const { readdir } = await import('node:fs/promises') + const [skill, graphSource] = await Promise.all([read('SKILL.md'), read('references/reference-graph.json')]) + const graph = JSON.parse(graphSource) + const ids = graph.nodes.map((node) => node.id) + + assert.match(skill, /references\/reference-graph\.json/) + assert.equal(graph.entry, 'common') + assert.equal(new Set(ids).size, ids.length) + + for (const node of graph.nodes) { + assert.ok(node.when, `node ${node.id} must declare a load condition`) + assert.ok(Array.isArray(node.requires), `node ${node.id} must declare requires edges`) + for (const dependency of node.requires) { + assert.ok(ids.includes(dependency), `node ${node.id} requires unknown node ${dependency}`) + } + await read(node.path.replace(/^references\//, 'references/')) + } + + const entries = await readdir(join(skillDirectory, 'references'), { recursive: true, withFileTypes: true }) + const files = entries + .filter((entry) => entry.isFile()) + .map((entry) => join(entry.parentPath ?? entry.path, entry.name).slice(join(skillDirectory, 'references').length + 1)) + .map((relative) => `references/${relative}`) + const nodePaths = new Set(graph.nodes.map((node) => node.path)) + for (const file of files) { + if (file === 'references/reference-graph.json') continue + assert.ok(nodePaths.has(file), `${file} is not declared in reference-graph.json`) + } +}) + +test('collects shared authority, policy sources, and feedback routing into one common file', async () => { + const [skill, common, card, delivery, changeability, subagentReview] = await Promise.all([ + read('SKILL.md'), + read('references/common.md'), + readCard(), + readDelivery(), + read('references/changeability.md'), + read('references/subagent-review.md'), + ]) + + assert.match(common, /## 권위 우선순위/) + assert.match(common, /## 정책 출처/) + assert.match(common, /## 피드백 라우팅/) + assert.match(common, /mandatory-constraint/) + for (const classification of [ + 'POLICY_GAP', + 'EVIDENCE_GAP', + 'HARNESS_DEFECT', + 'PRODUCT_DEFECT', + 'ENVIRONMENT_DEFECT', + 'NON_ORACLE_OPINION', + ]) { + assert.match(common, new RegExp(classification)) + } + + // canonical 정의는 common.md 하나가 소유하고, 나머지는 pointer와 단계 특칙만 남긴다 + assert.match(skill, /common\.md/) + assert.match(card, /common\.md/) + assert.match(delivery, /common\.md/) + assert.match(changeability, /common\.md/) + assert.match(subagentReview, /common\.md/) + assert.doesNotMatch(changeability, /권위 순서: 1\)/) + assert.doesNotMatch(subagentReview, /^1\. 보안·개인정보·법적·접근성/m) +}) + +test('passes review criteria to reviewers as file links, not pasted text', async () => { + const [skill, subagentReview, graphSource, runner] = await Promise.all([ + read('SKILL.md'), + read('references/subagent-review.md'), + read('references/reference-graph.json'), + read('scripts/oracle-run.mjs'), + ]) + const graph = JSON.parse(graphSource) + const ids = new Set(graph.nodes.map((node) => node.id)) + + assert.match(skill, /--review-point/) + assert.match(subagentReview, /## 리뷰 포인트 — 파일 링크로 전달/) + assert.match(subagentReview, /--review-point/) + assert.match(subagentReview, /본문을 복붙하지 않고/) + assert.match(subagentReview, /경로와 SHA-256 digest만 기록/) + assert.match(subagentReview, /types\/review-criteria\.md/) + assert.match(subagentReview, /무관한 기준으로 finding을 만들지 않는다/) + assert.match(runner, /review-point/) + assert.match(runner, /REVIEW_POINT_INVALID/) + + assert.ok(Array.isArray(graph.reviewPoints), 'reference-graph.json must declare reviewPoints routing') + for (const entry of graph.reviewPoints) { + assert.ok(entry.when, 'each review point routing needs a condition') + for (const node of entry.nodes) { + assert.ok(ids.has(node), `review point routing references unknown node ${node}`) + } + } +}) + +test('routes the low fast path as an explicit exclusive lane in the graph', async () => { + const [skill, lane, card, delivery, graphSource] = await Promise.all([ + read('SKILL.md'), + read('references/lanes/low-fast-path.md'), + readCard(), + readDelivery(), + read('references/reference-graph.json'), + ]) + const graph = JSON.parse(graphSource) + const ids = new Set(graph.nodes.map((node) => node.id)) + + // lane 노드가 진입 조건·절차·승격 규칙을 소유한다 + assert.match(lane, /## 진입 조건/) + assert.match(lane, /## 하지 않는 것/) + assert.match(lane, /## 승격 — Low 실격 조건/) + assert.match(lane, /다른 reference 노드는 로드하지 않는다/) + assert.match(lane, /검증을 생략하는 lane이 아니다/) + assert.match(lane, /즉시 Low 실격/) + + // 그래프가 lane 분기를 기계 판독 가능하게 선언한다 + assert.ok(Array.isArray(graph.lanes), 'reference-graph.json must declare lanes') + const lowLane = graph.lanes.find((entry) => entry.id === 'low-fast-path') + const oracleLane = graph.lanes.find((entry) => entry.id === 'oracle') + assert.ok(lowLane, 'low-fast-path lane must exist') + assert.equal(lowLane.exclusive, true) + for (const node of lowLane.nodes) assert.ok(ids.has(node), `lane references unknown node ${node}`) + assert.ok(lowLane.escalation, 'low lane must declare an escalation rule') + assert.equal(oracleLane?.entry, 'common') + + // SKILL과 각 단계 문서가 lane 노드로 라우팅한다 + assert.match(skill, /lanes\/low-fast-path\.md/) + assert.match(card, /lanes\/low-fast-path\.md/) + assert.match(delivery, /lanes\/low-fast-path\.md/) })