설계 결정 기록 (decisions)
설계서 §16.1-12: 구현 도중 설계 변경이 필요하면 결정 이유·영향·변경된 인수 기준을 이 문서에 기록한다. 설계서 §1.2: 화면·상태 코드·승인 절차·내부 API·데이터 모델·구현 기술·성능 목표는 제안이며, 확정 정책이 아니다.
D-001. 구현 기술 스택 — Next.js + Prisma 대신 사내 표준(Vite + Express + tRPC + Drizzle) 채택
- 일자: 2026-09-19
- 상태: 채택
- 설계서 원안: §13.1 — 웹·API
Next.js + TypeScript, DBPostgreSQL, 데이터 접근Prisma, 입력 검증Zod, 비동기 worker, 검색 PostgreSQL.
결정 이유
- 설계서 자신의 우선 규칙: §13.1 첫 문장 — "기존 사내 표준이 제공되면 그 표준을 우선한다. 별도 표준이 없다면 다음 구성을 기본 구현안으로 사용한다."
- 사내 표준이 실제로 존재한다: 이 생태계의 포털 3개가 동일 스택을 쓴다.
ProcessCenter— Vite + Express + tRPC + Drizzle + React 19,client/·server/·shared/,vendor/*.tgz로@hohyun94/*설치PortalConsole— 동일 스택UiPlatform— 동일 스택
- 연계 대상과의 정합: Agent Center의 원본 시스템은
ProcessCenter(프로세스 노드 관리 포탈)다. 같은 스택·같은@hohyun94계약·같은 배포 방식을 쓰면 어댑터·인증·검증 도구를 공유할 수 있다. - CI·운영 규약 정합: PR 전용 검증,
agent-commit도우미,vendor방식, Coolify/Railway 배포 관례가 이미 정착되어 있다.
영향
| 영역 | 원안 | 채택안 | 비고 |
|---|---|---|---|
| 앱 프레임워크 | Next.js App Router | Vite(클라이언트) + Express(서버) | 화면·API를 같은 저장소에서 관리하는 §13.1 목표는 유지 |
| 데이터 접근 | Prisma | Drizzle ORM | 스키마·마이그레이션·타입 목표 동일. 부분 고유 인덱스는 마이그레이션 SQL로 보완 |
| DB | PostgreSQL | PostgreSQL | 변경 없음(§13.1 유지) |
| 입력 검증 | Zod | Zod | 변경 없음 |
| 비동기 처리 | Node worker + outbox | Node worker + outbox | 변경 없음(§13.2 구성도 유지) |
| 인증 | 사내 OIDC 어댑터 | 동일(현재 Mock) | §13.1 유지 |
디렉터리 대응 (§13.3 → 이 저장소)
| 설계서 §13.3 | 이 저장소 | 비고 |
|---|---|---|
src/app/ (화면 + API) | client/src/pages/ + server/routers.ts | 클라이언트·서버 분리 |
src/components/ | client/src/components/ | portal-foundation 조합 |
src/domain/ | server/domain/ | 계층·책임 유지 |
src/adapters/ | server/adapters/ | 동일 |
src/application/ | server/application/ | 동일 |
src/infrastructure/ | server/infrastructure/ | 동일 |
src/contracts/ | shared/ | 요청·응답·상태·오류 계약 |
src/workers/ | server/workers/ | 동일 |
tests/fixtures/demo/ | shared/demo/ | D-002 참고 |
변경된 인수 기준
- §16.2 "실행 명령의 제공 기준"(의존성 설치·DB 마이그레이션·데모 데이터·웹·Worker·테스트)은 Vite/Express 기준 명령으로 제공한다(README).
- §15.2 MVP 완료 조건의 "타입 검사·정적 검사·핵심 통합 테스트·대표 E2E"는
pnpm check·pnpm test· (후속) Playwright로 검증한다. - 그 외 §15 인수 시나리오(AT-01~AT-36)는 스택과 무관하게 그대로 유효하다.
D-002. 합성 데모 데이터 위치 — tests/fixtures/demo/ → shared/demo/
- 일자: 2026-09-19
- 상태: 채택
- 설계서 원안: §13.3 — 합성 fixture 를
tests/fixtures/demo/에 둔다.
결정 이유
§8.5 는 "Mock 환경은 화면 상단에 '데모 데이터'를 지속 표시한다" 고 요구한다. 즉 데모 데이터가 런타임에 서버에서 서빙되어야 한다. tests/ 아래에 두면 프로덕션 코드(서버 어댑터)가 테스트 디렉터리를 import 하게 되어 의존 방향이 뒤집힌다.
영향
shared/demo/(processTree · accounts · agents)에 단일 원천을 두고, Mock 어댑터와 테스트가 함께 import 한다.@shared별칭(Vite/vitest)과 상대 경로(서버 esbuild 번들) 모두로 참조 가능하다.- 테스트 전용 fixture 가 필요해지면
tests/fixtures/를 별도로 둔다.
D-003. AgentCenterMark 는 portal-foundation에 추가한다
- 일자: 2026-09-19
D-006. 데이터 모델 스키마를 구현된 도메인에 맞춰 다시 세운다
일자: 2026-09-19
상태: 채택
내용:
drizzle/schema.ts는 "준비됨"으로 기록되어 있었지만 컴파일조차 되지 않는 상태였다(agentMembers정의가 중간에서 끊기고 나머지가 파일 끝에 떨어져 있어 TS1005 12건,tsconfig의include에drizzle/이 없어 아무도 눈치채지 못했다). PostgreSQL 저장소를 붙이면서 세 가지를 함께 정리했다.- 문법 복구 — 끊긴
agentMembers를 제자리로 되돌리고 파일 끝의 고아 블록을 제거했다. - id 타입 — 모든 PK·FK 를
uuid에서varchar(128)로 바꿨다. 구현된 도메인은demo-agent-edu-domain·agent-000001같은 읽기 쉬운 문자열 id 를 쓰고(시드 fixture 포함), id 생성은IdGenerator포트가 소유한다 —uuid컬럼은 이 값들을 거부한다.- §7.1 "내부 PK는 UUID" 의 의도는 저장소별
IdGenerator로 지킨다: PostgreSQL 경로는randomUUID()를 쓴다(운영에서 새로 만들어지는 레코드는 UUID). 컬럼은 문자열이라 시드·이관 데이터도 그대로 들어간다.
- §7.1 "내부 PK는 UUID" 의 의도는 저장소별
- 매핑의 노드 식별자 —
process_mappings는process_node_refs에 대한uuidFK 만 갖고 있었지만, 애플리케이션은processNodeRefId에 외부 노드 id(node-context등)를 넣고nodeKey(복합 식별자)를 함께 다닌다.process_node_refs를 채우는 동기화 Worker 가 아직 없어 FK 를 만족시킬 수 없다. 그래서process_node_ref_id를varchar(128)(불투명 외부 참조)로 바꾸고,node_key jsonb컬럼을 추가해 매핑이 자기 복합 식별자를 직접 갖게 했다(§7.1).
- 문법 복구 — 끊긴
이유: "스키마는 준비되어 있다"는 기록만 믿고 저장소를 붙이면 첫 마이그레이션에서 전부 실패한다. 스키마는 코드가 실제로 넣는 값을 받아야 하고, 그 차이는 컴파일과 실 DB 검증에서만 드러난다.
검증:
pnpm db:generate로 마이그레이션 생성 → 실제 PostgreSQL 16 컨테이너에 적용 → 포트 계약 8개 시나리오(마이그레이션·jsonb 왕복·낙관적 잠금·중복 매핑·dedupe·트랜잭션 롤백/커밋) 통과. 앱을 DB 로 기동해 시드 적재 후 재시작해도 데이터가 유지되고 중복 삽입이 없음을 확인했다.상태: 보류(후속 PR)
내용:
packages/ui-react/src/productMarks.tsx에는ProcessCenterMark·GuideCenterMark·DocumentLibraryMark·FileStoreMark만 있다. Agent Center 셸의 제품 마크를 위해AgentCenterMark를 같은 SVG 마크 패턴으로 portal-foundation에 추가하는 것이 맞다(포털별 로컬 마크를 만들면 브랜딩 규칙이 갈라진다).이유: 공통 UI 변경은
portal-foundation의 릴리스·검증 절차를 거쳐야 하므로 이 저장소 작업과 분리한다.
D-007. Ant Design 시드 토큰에 CSS 변수를 넘기지 않는다
- 일자: 2026-09-20
- 상태: 채택
- 내용:
PortalConfigProvider의portalTheme(antd 시드)에var(--pf-surface)같은 CSS 변수를 넘기지 않는다. 모드별 실제 색을 넘긴다(client/src/App.tsx의ANTD_SEED_TOKENS). - 이유: antd v6 는 시드에서 파생 색을 합성해 만든다 —
new FastColor(colorFillAlter).onBackground(colorBgContainer).toHexString()(antd/es/table/style/index.js). 시드가 CSS 변수 문자열이면 색 파싱이 실패해 파생 토큰이 #000000 이 된다. 실측된 피해(라이트 모드,--ant-*1065개 중 12개):--ant-table-header-bg,--ant-table-row-hover-bg,--ant-table-body-sort-bg,--ant-table-header-sort-{hover,active}-bg,--ant-table-footer-bg,--ant-table-fixed-header-sort-active-bg→ 전부#000000--ant-btn-border-color,--ant-btn-text-color,--ant-select-border-color→#000- 증상: 라이트 모드에서 Agent 카탈로그 표 헤더가 검정 배경 + 어두운 글자로 거의 보이지 않음
- 영향: 값의 출처는
design-tokens/tokens.css의--pf-*이므로 토큰을 바꾸면ANTD_SEED_TOKENS도 함께 맞춘다(다크--pf-surface=color-mix(--pf-rail 82%, white)=#3b4453). - 검증: 헤드리스 브라우저에서
--ant-table-header-bg가 라이트#fafafa(글자#263b4f) · 다크#434b5a(글자#f7f9ff) 로 나오는지 확인. 수정 전에는 두 모드 모두#000000이었다. - 남은 공통 과제: 같은 이유로
portal-foundation을 쓰는 다른 포털(ProcessCenter 등)도 CSS 변수 시드를 넘기면 같은 증상이 난다. 공통 패키지에서 시드를 해석하거나 합성 파생 토큰을--pf-*로 명시하는 보완이 필요하다(§17 후속).
D-004. 검토 요청·매핑 승인의 세부 정책은 미확정으로 둔다
- 일자: 2026-09-19
- 상태: 미확정(설계서 §17)
- 내용: 검토자 배정 방식(동적 검토 대기함 vs 개인 배정), 노드별 대리 검토자, 자기 승인 예외, 유형·레벨 불일치 예외의 실제 허용 여부는 사내 확정 정책이 아니다. Mock 구현은 §4.1·§6.4의 제안을 그대로 따르고, 운영 값은 확정 전까지 설정 지점으로만 둔다.
D-005. 화면 전환은 경로 기반으로 통일하고 라우터는 wouter 로 고정한다
- 일자: 2026-09-19
- 상태: 채택 (이 저장소 파일럿 적용)
- 내용: 화면 전환의 원천을
viewIdstate 에서 URL 경로로 바꾸고, 라우터 구현은wouter로 통일한다. 규약 전문은 portal-foundationdocs/ROUTING_CONVENTION.md이다. - 결정 이유:
- 플랫폼이 이미 경로를 계약으로 요구한다 — 메뉴 검증이
LINK항목에/로 시작하는 경로를 필수로 보고(INVALID_PATH), 셸은href가 있는 항목을<a href>로 렌더하며, 활성 메뉴는activeMenuKey(경로)로 파생한다. state 만으로 화면을 바꾸면 이 계약과 어긋난다. - 이 저장소의
MENU_TREE에는 이미 모든 항목의href가 있었지만buildNavigationItems가 이를 버리고onSelect로 state 를 바꾸고 있었다 — 경로가 죽은 데이터였다. - 딥링크·뒤로가기·새로고침 유지가 없으면 운영에서 화면을 공유하거나 새로고침으로 상태가 보존되지 않는다.
- 플랫폼이 이미 경로를 계약으로 요구한다 — 메뉴 검증이
- 영향:
client/src/routing.ts가MENU_TREE의href에서 경로 ↔ 화면 표를 파생한다. 경로를 두 곳에 적지 않으므로 메뉴와 라우터가 어긋날 수 없다.App.tsx는 경로 → 화면 해석을 단일<Switch>에 모으고,activeNavigationId를 현재 경로에서 파생한다._core/useInternalLinkNavigation이 같은 출처 내부 링크의 일반 클릭을 SPA 이동으로 바꾼다(앱이 아는 경로만). 링크 시맨틱은 유지된다.vitest.config.ts의environmentMatchGlobs는 이 저장소에서 적용되지 않아 제거했고, 클라이언트 테스트는 DOM 없이(순수 함수 + 클릭 핸들러 팩토리) 작성했다 — jsdom + React 렌더가 이 저장소에서 실패하기 때문이다(§3-11 에 재현 조건 기록).
D-008. 메뉴 소유와 관리자 메뉴 진입 — 사용자 메뉴는 포털, 관리자 메뉴는 공통(중앙)/포털로 분리
- 일자: 2026-09-20
- 상태: 채택
- 내용: 사용자 메뉴는 포털이 소유하고 항목별
roles로 노출을 거른다. 관리자 메뉴는PortalAdminShell로 표시하며owner로 공급 주체를 나눈다 —FOUNDATION(중앙 콘솔이 공급할 플랫폼 기능: 사용자·역할)과PORTAL(포털이 소유하는 기능 제어 화면: 분류 정책·연계 동기화·감사 이력). 진입은 사이드바roleSwitch로 통일한다(관리자 판정 =roles.includes("ADMIN")). - 이유: AgentCenter 는 소비 포털이면서 ProcessCenter 원본 데이터를 소비하고 Agent 메타데이터를 공급하는 3중 성격이라 메뉴 소유가 모호했다. 앞서 테마·아이콘·관리자 항목 노출이 공통 계약과 어긋난 것도 같은 뿌리다. 공통 규약(portal-foundation
docs/ADMIN_MENU_CONVENTION.md)으로 소유·진입을 고정한다. - 영향:
MENU_TREE에 항목별roles게이트 추가,admin그룹을 관리자 메뉴로 분리(ADMIN_MENU_COMMON/ADMIN_MENU_PORTAL).- 라우팅 표를
ROUTES(사용자) +ADMIN_ROUTES(관리자)로 나누고isAdminPath로 셸을 결정한다. - 관리자 화면은
PortalAdminShell(commonAdminItems, portalAdminItems)로 렌더한다(화면 내용은 다음 단계 — 현재Placeholder). - 중앙 공급(L3)은 설계만 둔다(portal-foundation
docs/MENU_GOVERNANCE_DESIGN.md). 지금은 공통 구간을 폴백 시드로 로컬에 두고, 전환 시 공급처만 교체한다(렌더러 불변).
- 검증: 역할별 헤드리스 캡처 — VIEWER 는 소유자·검토 메뉴가 사라짐, ADMIN 은 전체 항목 + 사이드바 "관리자" 전환, 관리자 셸은 공통/포털 4개 항목과 "포털" 복귀 항목. 자동 테스트 26 파일 / 207 테스트.
D-009. 공통 관리자 메뉴 공급을 중앙(콘솔)에서 받는다 — 로컬 시드는 폴백으로 격하
- 일자: 2026-09-20
- 상태: 채택 (공급 경로 구현 · 저장·저작은 L3b)
- 내용: 관리자 셸의 공통(FOUNDATION) 구간을 포털 로컬 배열에서 중앙 공급으로 바꾼다. 소비 경로는
GET {PORTAL_CONSOLE_ORIGIN}/api/directory/v1/menu?portalId=agent-center(+Authorization: Bearer ${PORTAL_CONSOLE_API_KEY}, scopedirectory:read)이고, 서버가 받아 캐시한 뒤trpc.system.adminMenu로 클라이언트에 전달한다. 포털(PORTAL) 구간은 그대로 포털이 소유한다 — 렌더러(PortalAdminShell)는 바뀌지 않는다. - 결정 이유:
- 공통 관리자 항목(사용자·역할 등 플랫폼 기능)은 포털마다 복사·수정되면 플랫폼 개정이 모든 포털 배포에 묶인다. 소유자를 중앙으로 고정하면 항목 개정이 콘솔 한 곳에서 끝난다(
ADMIN_MENU_CONVENTION.md§1,MENU_GOVERNANCE_DESIGN.md§2). - 폴백 규칙을 서버 한 곳에 모은다. 클라이언트가 콘솔을 직접 호출하지 않으므로 인증키가 브라우저에 내려가지 않고, 장애 시 동작(캐시 → 시드)을 서버에서 판단한다.
- D-008 의 "전환 시 공급처만 교체" 예고를 이행한 것으로,
buildAdminMenu시그니처 외에 화면 코드는 손대지 않는다.
- 공통 관리자 항목(사용자·역할 등 플랫폼 기능)은 포털마다 복사·수정되면 플랫폼 개정이 모든 포털 배포에 묶인다. 소유자를 중앙으로 고정하면 항목 개정이 콘솔 한 곳에서 끝난다(
- 영향:
- 신규:
shared/adminMenu.ts(계약·시드·중앙 응답 검증),server/adapters/identity/consoleAdminMenu.ts(조회·캐시·폴백),tests/unit/consoleAdminMenu.test.ts. - 변경:
server/routers.ts(system.adminMenu— 인증 필요),server/_core/container.ts(공급원 조립),server/_core/env.ts(+PORTAL_CONSOLE_API_KEY,PORTAL_CONSOLE_MENU_TTL_MS기본 15분),client/src/navigation.tsx(buildAdminMenu(consoleCommonItems | null)),client/src/App.tsx(조회 결과 전달). - 폴백 규칙: 중앙 응답 → 캐시(마지막 정상) → 로컬 시드.
null(아직 못 받음·중앙 미설정)에서만 로컬 시드를 쓰고, 중앙이 빈 배열을 주면 그대로 반영한다(공급 주체가 중앙이므로 포털이 항목을 되살리지 않는다). - 중앙 응답 중 셸에 넣을 수 없는 항목은 버린다 —
visibility ≠ ADMIN,enabled=false,owner ≠ FOUNDATION,type ≠ LINK,/로 시작하지 않는 경로. - 콘솔 장애·미연결은 포털 장애가 아니다: 경고 로그만 남기고 관리자 셸은 캐시/시드로 정상 표시한다.
- 신규:
- 검증:
tests/unit/consoleAdminMenu.test.ts(10) — 캐시 채택·TTL 만료·동시 조회 1회 호출·장애 시 캐시/시드 폴백·portalId불일치 거부·부적합 항목 필터·invalidate.tests/integration/router.test.ts(+2) — 인증 요구·시드 보장.client/src/navigation.test.tsx(+4) — 중앙 항목 렌더·빈 목록 반영·null시 시드·두 구간 순서 연속.tests/unit/env.test.ts(+1) — 새 환경 변수와 기본값. 전체 27 파일 / 224 테스트(215 통과 · 9 skip). - 남은 것(L3b): 콘솔의 메뉴 저장소·저작 화면·감사, 포털 캐시의 영속화(현재 인메모리), 관리자 수동 새로고침 UI — portal-foundation
MENU_GOVERNANCE_DESIGN.md§7 참조.
D-010. 관리자 화면 4종을 실제 데이터로 채운다 — 없는 것은 없는 대로 표시한다
- 일자: 2026-09-20
- 상태: 채택
- 내용: D-008 에서
Placeholder로 남겨둔 관리자 화면 4종을 실제 내용으로 채운다. 서버에adminProcedure(ADMIN 전용)와admin라우터(roleMapping·classificationPolicy·integrationStatus)를 추가하고, 감사 이력은 이미 있던workflow.auditEvents(ADMIN·AUDITOR)를 쓴다./admin/users(UI-11 사용자·역할): 현재 사용자 최소 정보(§2.1) · 역할 부여 규칙(중앙 관리자/기본 역할 + §17 브리지 개별 부여) · 현재 역할의 메뉴 노출./admin/policy(UI-06 분류 정책): 정책 버전 · 유형별 허용 레벨 범위 · 예외 승인 허용 여부./admin/sync(UI-10 연계·동기화): 어댑터 종류와 구성 여부 · 동기화 주기 설정값 · 동기화 Worker 미구현 표시 · 중앙 메뉴 공급 구성(규약 §1)./admin/audit(UI-11 감사 이력): 최근 이벤트 표 + 대상 ID 필터, 권한 제한(VIEWER) 안내와 빈 결과 안내.
- 결정 이유:
- 화면을
Placeholder로 두면 "관리자 기능이 있다"는 인상만 남고 실제로는 아무것도 확인할 수 없다. 있는 것(정책·역할 매핑·연계 구성·감사 기록)은 실제 값으로 보여주고, 없는 것(동기화 Worker·정책 편집·사용자 원장 편집)은 미구현이라고 적는다(§16.1-11). - 중앙 사용자 원장은 콘솔 소유이므로 포털은 편집 화면을 만들지 않는다(§2.1). "편집 버튼은 있는데 저장이 안 되는" 화면을 만들지 않는다.
- 화면 숨김은 권한이 아니므로 새 조회도 서버에서 ADMIN 을 강제한다(
adminProcedure) — 감사 이력만 별도 판정(ADMIN·AUDITOR)을 유지한다. - 연계 상태는 값이 아니라 구성 여부만 내보낸다(§14.1) — 세션 키·API 키·서비스 토큰은 어떤 형태로도 화면에 내려가지 않는다.
- 화면을
- 영향:
- 서버:
server/_core/trpc.ts(+adminProcedure),server/_core/container.ts(+runtime— 비밀 제외),server/routers.ts(+admin라우터). - 클라이언트:
client/src/pages/admin/{AdminUsers,AdminPolicy,AdminSync,AdminAudit}.tsx,client/src/pages/admin/presenters.ts(표시 변환 — 순수 함수),App.tsx(AdminRoutes),const.ts(레지스트리implemented: true).
- 서버:
- 검증:
client/src/pages/admin/presenters.test.ts(7) — 레벨 범위 문구·상한 없음 처리·주기 단위·mock/contract 상태·감사 행 변환.client/src/pages/admin/pages.test.tsx(4) — 4개 화면이 로딩 상태를 표시.client/src/const.test.ts(3) — 구현 완료 표시와 라우팅 표의 일치.tests/integration/adminScreens.test.ts(6) — 정책·역할 매핑(정렬)·연계 상태(비밀 미노출)·ADMIN 전용 강제·감사 권한. 전체 31 파일 / 244 테스트(235 통과 · 9 skip). - 남은 것: 정책 편집(사내 값 확정 §17), 사용자 원장 편집(콘솔 소유), 감사 보존 기간(사내 정책), 동기화 Worker(§9.2·§9.3).
D-011. AX Agent 포탈 통합 설계서 v0.2 채택 — v0.1 대비 변경을 단계별로 반영한다
- 일자: 2026-09-22
- 상태: 채택 (0단계 반영 완료 · 1~10단계 예정)
- 설계서 원안:
design-v0.2.md— 「AX Agent 포탈 통합 설계서 0.2」(2026-09-22). 기준 문서는 기존 v0.1(1~18장) + 신규 가이드 G1(8-1 Agent 정의 및 유형)·G2(8-2 Agent Life Cycle 관리). - 설계서 인용: §0.1 — "이 문서의 상태 코드, 테이블, API, 제출 해시, 작업 버전 제약 및 구현 순서는 개발을 위한 설계 제안이다." §16.1-12 — 설계 변경 시 이유·영향·변경된 인수 기준을 이 문서에 기록한다.
결정 이유
- v0.1 의 안전장치는 유지하고 신규 가이드가 요구하는 관리 범위를 더한다. §0.1 은 유지 항목(원본 프로세스 소유권·다대다 매핑·버전 불변성·권한 제한·동시성·감사·Mock 격리)을 명시하고, 변경·추가 항목은 CH-01~CH-16 으로 추적한다.
- 가이드 값을 회사 확정 정책으로 승격하지 않는다. §0.1·§21.3 — 가이드는 정책 템플릿이며 효력은 적용 조직이 승인한 정책 버전으로 부여한다. 그래서 미확정 14항목을 값이 아니라
shared/openQuestions.ts목록으로 두고, 확정 전에는 해당 게이트를 차단한다. - 가장 큰 구조 변경은 "게시 ≠ 운영 적용"과 유형·레벨 정책이다. v0.1 은 첫 게시에서
ACTIVE로 전환했고 유형별 레벨 범위(§6.4)를 고정 상수로 두었다. v0.2 는DRAFT → READY → ACTIVE분리(CH-10)와domain_allowed_levels/task_allowed_levels정책(CH-02, §4.3)을 요구한다. 후자는 현행server/domain/classification/policy.ts와 직접 충돌하므로 단계 1 에서 교체한다. - 단계를 건너뛰지 않는다. §21.1 이 0~10단계와 완료 근거(AT 묶음)를 지정했으므로 그 순서를 그대로 따른다.
영향 (v0.1 → v0.2)
| 영역 | v0.1 (현행 코드) | v0.2 대상 |
|---|---|---|
| Agent 생애주기 | DRAFT/ACTIVE/SUSPENDED/RETIRED | +READY·RETIRING (6개) |
| Agent 유형 | DOMAIN/TASK/PERSONAL/UNCLASSIFIED | PERSONAL/ENTERPRISE_TASK/ENTERPRISE_DOMAIN/UNCLASSIFIED |
| 유형별 레벨 | 고정 상수(DOMAIN=L4~5, TASK=L6, PERSONAL=L7~) | domain_allowed_levels·task_allowed_levels 정책 목록(숫자 비교 금지) |
| 검토 흐름 | workflow.* 단일 라우터, 검토 요청 RELEASE/RETIREMENT | 케이스(INITIAL/CHANGE/RETIREMENT/RESUME) + 게이트 11종 + 결재 슬롯 |
| 검토 결정 | APPROVE/REQUEST_CHANGES | +REJECT (요청 상태 +REJECTED·INVALIDATED) |
| 위험·권한 | 없음(행동 정책 AUTO_ALLOWED/APPROVAL_REQUIRED/DENIED 만) | 실행 권한(READ/CREATE_UPDATE/EXECUTE)·자율성·3×3 Risk·데이터·환경 적재 |
| 식별자 | Agent.code 단일 | registration_code(기획) + operational_agent_id(운영 승인 후) |
| 배포 | Deployment.runtimeStatus 만 | 배포 결과(REQUESTED~ROLLED_BACK)와 런타임 관측 분리 |
| 역할 | 7종 | 17종(+DEVELOPER·EDITOR·PRE_REVIEWER·VALIDATION_REVIEWER·SECURITY_REVIEWER·DATA_REVIEWER·OPERATION_APPROVER·RETIREMENT_APPROVER·GOVERNANCE_MANAGER·POLICY_APPROVER) |
| 데이터 | 엔터티 12종 | 기존 5종 확장 + 신규 약 28종(§15.3) |
| API | workflow.*·읽기 라우터 | 신규 약 30개 경로(§16.3) + If-Match·Idempotency-Key |
| 화면 | UI-01~11 | +UI-12~18, 메뉴 9그룹 재구성 |
0단계 산출물 (이번 작업)
| 산출물 | 내용 |
|---|---|
docs/design-v0.2.md | 설계서 전문(§0~§22)을 저장소 참조 원천으로 옮겼다. 표·문단의 빈 줄만 markdown 으로 정리했고 문구는 바꾸지 않았다. |
shared/status.ts | v0.2 대상 상태 코드·축 추가(생애주기 6·유형 4·실행 권한·자율성·Risk·적용성·환경·케이스·게이트 11·게이트 상태·검증 결과·배포 결과·폐기 태스크·사건·책임 위치·역할 17). 기존 v0.1 상수는 지우지 않았다 — 전환은 단계별로 한다. |
shared/policyTemplate.ts | 가이드 기준 템플릿(유형별 레벨 허용 목록·3×3 Risk 행렬·데이터/Region 표·운영·폐기 결재 템플릿·성과·Risk 점검 주기). 데모 값이며 회사 확정 정책이 아님을 문구로 남긴다. |
shared/openQuestions.ts | 미확정 14항목(Q-01~Q-14)과 차단 게이트, 그리고 설계서가 코드를 주지 않은 항목(보안 검토 결과·공식 ID 상태 등)을 값 대신 목록으로 둔다. |
shared/integrationPorts.ts | 외부 연계 6포트(조직·결재·런타임·IAM·모니터링·증빙) 계약. 포탈이 하지 않는 일(실행·발급·회수)을 노출하지 않고 요청·관측만 노출한다(§2). |
server/adapters/integration/index.ts | 6포트를 미연계로 조립. 호출하면 NOT_CONFIGURED 로 드러나며 조용히 성공하지 않는다(§19.3·AT-32 정신). describeIntegrationPorts 는 모드·구성 여부만 내보낸다(§14.1). |
server/_core/container.ts | 조립 지점에 integrations 를 추가 — 단계 9 에서 실제 연계를 붙일 자리(§13.3). |
이번 단계에서 하지 않은 것: 상태 흐름 배선, 정책 엔진, 케이스·게이트 생성, 스키마·마이그레이션, API·화면 변경. 위 상태 코드와 포트는 선언만 되어 있고 어떤 흐름에도 연결되지 않았다.
변경된 인수 기준
§20.3 의 MVP 완료 조건은 그대로 유효하다 — "타입·정적 검사, 핵심 단위·통합·대표 E2E를 통과하고 실제 실행한 테스트·미연계·미확정 정책을 README와 검증 보고에 기록한다. 화면만 있고 게이트·권한 검증이 없는 상태는 완료가 아니다." 0단계에서는 §21.1 단계 0 의 완료 근거(설계 결정·계약 테스트·합성 fixture)만 주장한다.
검증
pnpm check(tsc --noEmit) — 통과.pnpm test— 34 파일 / 267 통과 · 9 skip(기존 31 파일 / 235 통과 · 9 skip 대비 +3 파일 … 신규 테스트 30건).shared/status.test.ts(5) — v0.2 상태가 v0.1 값을 지우지 않고 더하는지, 유형 값 집합이 바뀌었는지, 역할·게이트 수가 맞는지.shared/policyTemplate.test.ts(15) — Risk 행렬 9개 조합이 §5.2 표와 정확히 일치(AT-41), Task L5~L7, Personal 비고정, 데이터·Region 표, 결재 템플릿 3유형·기준 조직(개발/폐기), 점검 주기.shared/openQuestions.test.ts(5) — Q-01~Q-14 완결성, 차단 게이트가 선언된 게이트 종류인지, 코드 미부여 항목 기록 여부.tests/unit/integrationPorts.test.ts(5) — 6포트 조립, 설명은 모드·구성 여부만, 미연계는NOT_CONFIGURED로 실패(재시도 대상 아님).
남은 것
- 단계 1~10 (§21.1). 특히 단계 1 의 유형·레벨 정책 교체는
server/domain/classification/policy.ts와server/domain/releases/publishReadiness.ts를 다시 쓴다. - Q-01~Q-14 확정 — 확정 전에는 해당 게이트(운영 승인·분류·검증·보안·데이터·폐기 완료)를 차단한다.
- §19.1 의
src/app·prisma/migrations구성은 채택하지 않는다. 같은 절 첫 문장의 "사내 표준 우선"과 D-001 의 사내 표준(Vite+Express+tRPC+Drizzle)이 일치하므로 기술 스택은 유지하고, 디렉터리 대응만 §13.3 → 이 저장소 표(D-001)를 따른다. README.md·docs/implementation-status.md의 설계서 참조를 0.2 기준으로 갱신한다.
D-012. 매핑 적용성(미적용 검토 경로)을 도입한다 — 유형 판정은 대표 매핑으로, 오류 코드는 재사용
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 1 · 적용성·분류 정책)
- 설계서 원안: §4.5 매핑 적용성 4종(
REQUIRED/PERSONAL_NOT_APPLICABLE/UNSTRUCTURED_TASK/PROCESS_DEFINITION_PENDING)과 경로별 제출·운영 조건. §14.1 "미적용 승인 경로에서는 대표 매핑을 null 로 허용한다". §4.3 "유형의 책임 범위를 대표하는 매핑과 세부 수행 매핑을 구분하여 판정한다".
결정 이유
- 미적용은 "검토 결과"로만 인정한다. §4.5 는 매핑 미적용을 자동 승인하거나 임의의 공통·가상 노드로 대체하지 말라고 한다. 그래서 경로를 택하려면 사유가 필수이고, 유형 제한(Personal·Task)을 서버가 강제한다. 사유 없는 미적용은
REQUIRED_FIELDS_MISSING으로 막는다. - 새 필드(
classification_role)를 만들지 않고 대표 매핑을 대표 책임으로 쓴다. §4.3 의 "대표 책임 매핑과 세부 수행 매핑 구분"을 §7.6 의 기존primaryMappingId로 충족했다. §14.1 의PRIMARY_SCOPE/SUPPORTING_EXECUTION속성은 제안 단계라 지금 도입하면 스키마·API·마이그레이션이 함께 늘어난다 — 한 버전에 대표 책임 매핑이 여러 개여야 한다는 요구가 생기면 그때 추가한다(§14.1 은 "주된 관계 하나를 선택한다"고도 말한다). - Q-03 미확정을 정책 한 곳에서 다룬다. §21.3 의 "설정으로 비활성 또는 미확정 표시"를 만족하도록
applicabilityPolicy.notApplicablePathsEnabled를 두고, 끄면 네 경로가 모두 기존 "직접 매핑 필수"로 되돌아간다(§4.5 마지막 문장). 정책은AgentCenterDeps로 주입되므로 저장소·배포 설정에서 바꿀 수 있다. - 오류 코드를 새로 발명하지 않는다. §16.5 의 "주요 오류" 목록에 미적용 전용 코드가 없다. 사유 누락은
REQUIRED_FIELDS_MISSING(422), 유형 제한 위반은CLASSIFICATION_CONFLICT(422), 원본 정의 대기는INVALID_STATE_TRANSITION(409)로 매핑해 기존 계약을 재사용한다. - 승인 게이트도 같은 규칙을 본다. v0.1 의
decide는 "직접 수행 매핑이 없으면 버전 승인 불가"를 무조건 적용했다. 이제effectiveApplicability가REQUIRED일 때만 적용한다 — 승인과 게시가 서로 다른 규칙을 쓰면 미적용 경로가 승인은 되고 게시는 막히는 모순이 생긴다.
영향
| 영역 | 변경 |
|---|---|
| 계약 | AgentRelease +mappingApplicability·applicabilityReason·applicabilitySubstitute. MAPPING_APPLICABILITIES 를 v0.2 "선언만" 구역에서 본문으로 옮김(이제 배선됨) |
| 도메인 | 신규 server/domain/mappings/applicability.ts — 경로별 조건과 정책. publishReadiness 의 "직접 매핑 필수" 규칙을 적용성 판정으로 대체 |
| 저장소 | agent_releases +3 컬럼(mapping_applicability enum·applicability_reason·applicability_substitute) + 마이그레이션 0001_certain_apocalypse.sql(기존 행은 REQUIRED 로 채워짐) |
| 유스케이스 | createAgent 가 기획 단계에서 적용성을 받는다(§17.2 등록 흐름). 기본값은 REQUIRED |
| API | workflow.createAgent 입력에 3개 필드 추가(모두 선택) |
| 정책 | AgentCenterDeps.applicabilityPolicy 신규 — classificationPolicy 와 같은 방식으로 주입 |
| 문서 | .env.example·README 에 TEST_DATABASE_URL 안내 추가(없으면 PostgreSQL 테스트가 skip 된다) |
검증
- 로컬
pnpm verify:full통과. - PostgreSQL 포함
TEST_DATABASE_URL=… pnpm test— 36 파일 / 300 테스트 통과(skip 0). 마이그레이션0001이 실제 PostgreSQL 18 에 적용되고, 새 enum·컬럼이 왕복하는 것을 확인했다(tests/integration/postgresStores.test.ts"매핑 적용성과 사유를 왕복한다"). - 신규
tests/unit/applicability.test.ts(10) — 4경로 조건, 사유 필수, 유형 제한, 정책 비활성 시REQUIRED회귀. tests/integration/agentPublishGate.test.ts(+2) — AT-40(원본 정의 대기 → 게시 차단), AT-02(검토된 개인 업무 미적용 → 매핑 없이 게시).tests/unit/publishReadiness.test.ts(+3) — 미적용 경로 게시 허용·사유 누락 차단·AT-40 구분.
남은 것
- 미적용 경로의 사유·대체 업무를 등록 이후에 바꾸는 경로(UI-04 등록 마법사·초안 편집)는 화면 단계에서 추가한다.
- §18 집계의 "미적용 Agent 는 프로세스 적용률 분자에 넣지 않는다"는 집계 단계(단계 5)에서 반영한다.
classification_role속성 도입 여부는 위 이유 2의 조건이 확인될 때 결정한다.
D-013. 기본 위험등급은 서버가 계산하고, 평가 스냅샷 저장은 정책 모델과 함께 단계 2 에서 도입한다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 1 · Risk 계산)
- 설계서 원안: §5.1 실행 권한·자율성, §5.2 3×3 Risk 행렬과 "평가 결과에는 입력 스냅샷·계산 근거·정책 버전·시각을 저장한다", §15.2
agent_release의highest_action_level·autonomy_scope, §15.3risk_assessment엔터티.
결정 이유
- 계산은 지금 확정한다. AT-41(9개 조합)·AT-42(승인 필요 EXECUTE 유지·DENIED 제외)·AT-43(
INCOMPLETE, LOW 자동 설정 금지)은 모두 계산 결과를 요구한다.server/domain/risk/assessment.ts가 그 셋을 만족한다. - 저장은 정책 모델과 함께 간다. §5.2 의 저장 대상은 "정책 버전"이 붙은 스냅샷이고 §15.3 의
risk_assessment는policy_snapshot_id를 갖는다. 정책 엔터티(governance_policy·policy_rule·policy_snapshot)는 §21.1 단계 2 의 산출물이다. 지금 저장하면 템플릿 버전 문자열만 남은 스냅샷이 되어 단계 2 에서 버려야 할 데이터가 된다 — §0.1("효력은 적용 조직의 승인된 정책 버전으로 부여한다")과 어긋난다. - 그래서 입력을 저장하고 결과는 계산해 돌려준다.
AgentRelease.autonomyScope(선언)와AgentActionPolicy.actionLevel(행위별 실행 권한 수준)를 저장하고, 최고 허용 수준과 등급은 서버가 계산한다(§5.2 "서버가 … 계산한다"). §15.2 의 비정규화 필드highest_action_level도 계산 결과를 저장하는 시점에 함께 도입한다. - Risk 가 게시를 막지 않는다. §10.1 은 Risk 평가를 운영 승인 제출 요건으로 둔다(게이트 모델·운영 승인은 단계 2·4). 지금 게시를 막으면 입력이 미확정인 데모 표본이 전부 게시 불가가 되어 §20.3 의 MVP 흐름이 깨진다.
- 미확정을 낮게 추정하지 않는다. 허용 행위의 수준이 하나라도 미확정이면 최고 수준을
null로 두고 등급을 만들지 않는다 — 확정된 행위만으로 최고값을 만들면 실제보다 낮은 등급이 나온다(§5.2 "정보 부족을 LOW 로 대체하지 않는다").
영향
| 영역 | 변경 |
|---|---|
| 도메인 | 신규 server/domain/risk/assessment.ts — assessRisk(3×3), highestAllowedActionLevel(§5.1 최고 수준), DEFAULT_RISK_POLICY(템플릿 파생) |
| 계약 | AgentActionPolicy +actionLevel(실행 권한 수준 — Human 승인 조건과 다른 축), AgentRelease +autonomyScope. ACTION_LEVELS·AUTONOMY_SCOPES·RISK_LEVELS·RISK_ASSESSMENT_STATUSES 를 v0.2 "선언만" 구역에서 본문으로 옮김(이제 배선됨) |
| 저장소 | agent_releases +autonomy_scope 컬럼 — 마이그레이션 0002_short_wraith.sql(nullable: 미확정을 표현한다) |
| 유스케이스 | createAgent 가 autonomyScope·actionPolicies 를 받고, 신규 evaluateReleaseRisk 가 등급을 계산해 돌려준다 |
| 정책 | AgentCenterDeps.riskPolicy 신규(행렬 + 버전) — 분류·적용성 정책과 같은 주입 방식 |
| 데모 | 데모 표본에는 Risk 입력을 채우지 않는다 — 값이 없으면 INCOMPLETE 로 표시된다(§5.2). Risk 표본은 위험 평가 화면 단계에서 추가한다 |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 38 파일 / 317 테스트 통과. 마이그레이션0002가 실제 PostgreSQL 에 적용되고autonomy_scope·actionPolicies(jsonbactionLevel포함)가 왕복한다.- 신규
tests/unit/riskAssessment.test.ts(13) — AT-41 9개 조합 정확 일치, AT-42, AT-43, 정책이 바뀌면 결과가 바뀌는지. - 신규
tests/integration/agentRisk.test.ts(3) — 서버 유스케이스 경로(COMPLETE · DENIED 제외 · INCOMPLETE). tests/integration/postgresStores.test.ts(+1) — 행위 정책(실행 권한 수준 포함) 왕복.
남은 것
- 평가 스냅샷 저장(
risk_assessment테이블 —input_hash·policy_snapshot_id·assessed_at)은 단계 2 에서 정책 모델과 함께 도입한다. - Risk 입력을 화면에서 받는 UI 와, Risk 평가를 요구하는 운영 승인 게이트는 단계 2·4.
- 데이터·환경의 금지·조건부 통제(§5.3)는 이 등급에 가산하지 않고 별도 평가로 다룬다(다음 조각).
D-014. 데이터·환경 적합성을 기본 Risk 와 분리해 판정하고, 모르는 것은 허용으로 추정하지 않는다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 1 · 환경 판정)
- 설계서 원안: §5.3 데이터 등급 × 처리 Region 표("데이터·환경 조건을 Risk 기본 등급에 임의 합산하지 않는다"), §5.4 구현·운영환경 유형, §0.3("임의 가산점으로 기본 Risk를 바꾸지 않는다").
결정 이유
- 등급에 더하지 않는다. §0.3 은 데이터 등급·업무 범위·환경을 추가 통제 조건으로 평가하라고 한다. 그래서 별도 판정(
assessEnvironment)을 두고 기본 Risk(§5.2)와 값을 섞지 않는다 — AT-44 는 기본 Risk 가 LOW 여도 기밀·외부 AI 를 별도 규칙으로 차단하는 것을 요구한다. - 최고 등급 하나로 소거하지 않는다. §5.3 은 "국가핵심기술·개인정보·중요 보호 데이터 등의 별도 태그는 최고 등급 하나로 소거하지 않는다. 데이터 정책은 모든 관련 등급·태그에 대해 검사한다"고 한다. 판정은 항목별 결과를 모두 남기고(
items) 결합 결과만 가장 엄격한 값으로 만든다 — AT-45. - 모르는 것을 허용으로 추정하지 않는다. 표에 없는 등급·태그 →
UNDETERMINED, 처리 위치 미확정 →UNDETERMINED, 사내 실행환경 → §5.3 표로 추정하지 않고UNDETERMINED. §5.3 의 "금지·미확정 또는 필수 조건 미충족은 운영 승인과 적용을 차단한다"에 따라 셋 다 차단한다. CONDITIONAL을 조건 충족으로 넘기지 않는다. 조건 자체가 미확정(Q-08)이므로DATA_REVIEW_PENDING으로 보류한다(§21.3 Q-08 "환경 적합성 미확정, 운영 보류").- 처리 위치 코드는 제안이다. §5.3 표의 열은 산문("국외 Region 외부 AI"/"국내 Region 외부 AI"/"사내 실행환경")으로만 주어져 영문 코드가 없다. 발명한 값 집합을
shared/openQuestions.ts의 "설계서가 코드를 주지 않은 항목"에 기록한다 — 임의 코드를 조용히 굳히지 않는다.
영향
| 영역 | 변경 |
|---|---|
| 도메인 | 신규 server/domain/environment/assessment.ts — assessEnvironment(항목별 판정 + 가장 엄격한 결합), DEFAULT_ENVIRONMENT_POLICY(§5.3 표 파생) |
| 계약 | AgentRelease +dataTags(최고 등급과 별개 보호 태그)·processingLocation·implementationEnvironment·environmentProfile(§5.4 기록 항목). ENVIRONMENT_RESULTS·IMPLEMENTATION_ENVIRONMENTS 를 v0.2 "선언만" 구역에서 본문으로 옮김(배선됨) + DATA_PROCESSING_LOCATIONS 신규 |
| 저장소 | agent_releases +4 컬럼 — 마이그레이션 0003_strong_tomorrow_man.sql(enum 2종 + data_tags jsonb + environment_profile jsonb, 모두 기존 행 보존) |
| 유스케이스 | createAgent 가 데이터 등급/태그/처리 위치/환경 유형/기록 항목을 받고, 신규 evaluateReleaseEnvironment 가 판정한다 |
| 정책 | AgentCenterDeps.environmentPolicy 신규(§5.3 표 + 버전) |
| 문서 | shared/openQuestions.ts 에 처리 위치 코드 미부여 항목 기록 |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 40 파일 / 332 테스트 통과. 마이그레이션0003이 실제 PostgreSQL 에 적용되고 새 enum·jsonb 가 왕복한다.- 신규
tests/unit/environmentAssessment.test.ts(11) — AT-44(기밀 차단·Risk 와 분리), AT-45(항목 보존·미부여 태그UNDETERMINED), 사내 실행환경·미확정 처리 위치 보류, 정책이 바뀌면 결과가 바뀜. - 신규
tests/integration/agentEnvironment.test.ts(3) — 서버 유스케이스 경로. tests/integration/postgresStores.test.ts(+1) — 데이터·환경 입력 왕복(태그 소실 없음).
남은 것
- 환경 판정 결과 저장(§15.3
environment_assessment—data_tags·processing_locations·conditions·reviewer)은 Risk 와 같은 이유(D-013)로 정책 모델과 함께 단계 2. - 판정 결과를 운영 승인 게이트에 연결하는 일은 단계 2·4.
- 개인정보·보호 데이터·서비스별 조건(Q-08) 확정 시 정책 표(
DATA_REGION_TEMPLATE) 한 곳만 고치면 된다.
D-015. 적용 대상(Agent 여부) 판정 — 후보와 확정을 분리하고, 비Agent 만 게시를 막는다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 1 · 대상 판정)
- 설계서 원안: §4.1 적용 대상 판정(근거 6항목, 상태 4종, "자동 규칙은 누락·모순을 탐지하고 AX 검토자가 정책에 따라 확정한다", "NON_AGENT 는 등록 시도·판정 이력을 유지하되 Agent 운영 승인과 Agent 집계 대상에서 제외한다"), §15.2
agent_release.qualification, §15.3qualification_assessment, §16.5NON_AGENT_NOT_ELIGIBLE.
결정 이유
- 자동 규칙은 후보까지만 말한다. §4.1 은 누락·모순을 자동으로 탐지하되 확정은 AX 검토자가 하라고 한다. 그래서
assessQualification은candidate(후보)와missing(누락)을 돌려주고,NON_AGENT를 자동으로 단정하지 않는다 — 근거 문구가 "정해진 절차만 따른다"처럼 보여도 판단은 근거를 읽는 검토자의 몫이다. - 이름 충돌을 정리했다. §4.1 의 "적용 대상"과 §4.5 의 "매핑 적용성"은 다른 축인데 둘 다
APPLICABILITY_*로 읽힐 수 있었다. 0단계에서 선언만 해 둔APPLICABILITY_STATUSES를 §15.3 의 엔터티 이름을 따라QUALIFICATION_STATUSES로 바꿨다(배선 전이라 안전하게 개명). - 모순 규칙은 만들지 않는다. §4.1 은 "누락·모순"을 탐지하라고 하지만 모순의 기준을 정하지 않는다. 임의 규칙을 만들면 검토자 판단을 코드가 앞질러 굳힌다 — 그래서
contradictions는 항상 비어 있고, 규칙 확정 시 채운다(누락 탐지는 지금 구현). - 게이트는
NON_AGENT만 막는다. 미확정(UNASSESSED)·보완(NEEDS_CLARIFICATION) 상태의 운영 승인 차단은 게이트 모델(§21.1 단계 2) 의 몫이다. 지금 미확정을 막으면 등록 직후 모든 버전이 게시 불가가 되어 §20.3 의 MVP 흐름이 깨진다. 반대로NON_AGENT는 검토자가 명시적으로 내린 판정이라 즉시 막아도 안전하다. - 오류 코드는 §16.5 것을 쓴다.
NON_AGENT_NOT_ELIGIBLE(422) 는 설계서가 준 코드다 — 새로 발명하지 않았다.
영향
| 영역 | 변경 |
|---|---|
| 계약 | QualificationEvidence(근거 6항목)·AgentQualification(상태 + 근거) 신규, AgentRelease +qualification. APPLICABILITY_STATUSES → QUALIFICATION_STATUSES(본문으로 이동, 배선됨) |
| 오류 | shared/errors.ts +NON_AGENT_NOT_ELIGIBLE(422, §16.5) |
| 도메인 | 신규 server/domain/qualification/assessment.ts — assessQualification(후보·누락), assertQualificationAllowsPublish(비Agent 차단), emptyQualification |
| 출판 게이트 | publishReadiness 가 qualificationStatus === "NON_AGENT" 를 차단하고, PUBLISH_ISSUE_PRIORITY 최우선에 둔다 |
| 저장소 | agent_releases +qualification jsonb — 마이그레이션 0004_burly_hobgoblin.sql(기존 행은 UNASSESSED + 빈 근거로 채워짐) |
| 유스케이스 | createAgent 가 qualificationStatus·qualificationEvidence 를 받고, 신규 evaluateReleaseQualification 이 후보·누락을 돌려준다 |
| 데모 | 데모 표본은 UNASSESSED 로 둔다 — Agent 여부는 검토자가 확정한다 |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 42 파일 / 343 테스트 통과. 마이그레이션0004가 실제 PostgreSQL 에 적용되고 근거 6항목이 왕복한다.- 신규
tests/unit/qualification.test.ts(8) — 후보 상태(UNASSESSED·NEEDS_CLARIFICATION·AGENT), 누락 항목, 자동 단정 금지,NON_AGENT차단(422). - 신규
tests/integration/agentQualification.test.ts(2) — AT-38(근거 저장·누락 안내·보존), AT-37(비Agent 는 게시 불가). tests/integration/postgresStores.test.ts(+1) — 판정 상태·근거 왕복.
남은 것
- 검토 결정으로 상태를 확정하는 경로(§10.4
scope=RELEASE결정에 판정 슬롯을 붙이는 일)는 단계 2 의 결재 슬롯 작업이다. 지금은createAgent입력으로 상태를 넣는다. - 미확정 상태의 운영 승인 차단은 단계 2 게이트 모델에서
APPLICABILITY게이트로 다룬다(§7.4). - §15.3
qualification_assessment엔터티(정책 결합 스냅샷)는 D-013 과 같은 이유로 정책 모델과 함께 단계 2. - §18 집계의 "NON_AGENT 는 Agent 집계 대상에서 제외"는 집계 단계(단계 5)에서 반영한다.
D-016. 유형 코드를 DB enum 개명(RENAME VALUE) 으로 이전한다 — drizzle-kit 의 DROP+CREATE 생성안을 쓰지 않는다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 1 · 유형 코드 이전)
- 설계서 원안: §21.2-1 "기존 ID·버전·승인·감사·관계를 보존하고 스키마를 호환 확장한다", §21.2-2 "기존 DOMAIN/TASK/PERSONAL을 신규 표시 체계에 대응시키되 legacy_policy_version과 원래 판정을 보존한다. 코드 대응은 신규 정책 적합성 승인과 다르다", §21.2-8 "롤백 가능한 마이그레이션 순서를 사용한다".
결정 이유
- drizzle-kit 이 생성한 마이그레이션은 데이터를 잃는다. 원안은
ALTER COLUMN ... SET DATA TYPE text→DROP TYPE→CREATE TYPE→USING "col"::"public"."agent_type"캐스트였다. 기존 행에DOMAIN/TASK가 있으면 마지막 캐스트가invalid input value for enum agent_type로 실패해 전환이 그 자리에서 중단된다. 그래서ALTER TYPE "public"."agent_type" RENAME VALUE 'DOMAIN' TO 'ENTERPRISE_DOMAIN'로 바꿨다 — PostgreSQL 10+ 에서 라벨만 제자리에서 바뀌고 행 재작성·캐스트가 없다. - 개명은 의미 보존이지 정책 적합성 승인이 아니다. §21.2-2 가 둘을 명시적으로 분리한다. v0.1 의
DOMAIN(L4~L5)이 v0.2 의ENTERPRISE_DOMAIN(데모 L4)으로 이름이 바뀌어도 기존 L5 책임 범위는 재분류 검토 대상이다(§21.2-3). 그래서 이번 전환은 아무것도 자동 승인·자동 재판정하지 않는다 — 코드만 바꾼다. 릴리스의classificationPolicyVersion(당시 정책 기록)도 그대로 둔다. - 롤백 절차를 남긴다. 반대 방향
RENAME VALUE면 되돌아간다(값 손실 없음). 캐스트 방식이었다면 롤백도 같은 위험을 반복한다. - 검증은 옛 값이 있는 DB 로 한다. 신규 DB 전체 적용만 확인하면 "이미
DOMAIN행이 있는 DB"의 실패를 못 잡는다 — 그래서 0000~0004 를 적용한 DB 에 옛 값을 넣고 0005 를 적용하는 리허설 스크립트를 저장소에 두고(scripts/verify-type-code-migration.mjs,pnpm verify:type-migration) 두 경로를 모두 확인했다. - 이름을 남기지 않는다.
PERSONAL/UNCLASSIFIED는 그대로이고DOMAIN/TASK라벨은 제거했다. 옛 이름을 별칭으로 남기면 신규 코드가 두 이름을 쓰게 되어 §22-3("유형을 다른 필드로 유지")의 단일 축이 흐려진다.
영향
| 영역 | 변경 |
|---|---|
| 계약 | AGENT_TYPES = PERSONAL / ENTERPRISE_TASK / ENTERPRISE_DOMAIN / UNCLASSIFIED (v0.2 "선언만" 구역의 AGENT_TYPES_V02/AgentTypeV02 제거 — 단일 이름으로 통합) |
| 코드 전반 | 유형 값을 쓰는 20개 파일의 리터럴·스위치·집계 키·zod enum·화면 라벨을 개명 (19 파일은 기계적 치환, 별칭·라벨·집계 키는 수동) |
| 화면 | presenters.toPolicyRows 라벨 Enterprise-Domain/Enterprise-Task, Overview 유형별 수 라벨·키 |
| 마이그레이션 | 0005_fantastic_terror.sql 을 수기 개명으로 교체(스냅샷은 결과 스키마가 같아 drizzle 생성본 그대로) |
| 검증 도구 | scripts/verify-type-code-migration.mjs + pnpm verify:type-migration 신규 |
검증
- 전환 리허설(
TEST_DATABASE_URL=… pnpm verify:type-migration):- 개명 전
enum_range={DOMAIN,TASK,PERSONAL,UNCLASSIFIED} DOMAIN/TASK/PERSONAL행 삽입 →0005적용- 개명 후
enum_range={ENTERPRISE_DOMAIN,ENTERPRISE_TASK,PERSONAL,UNCLASSIFIED} - 기존 행 값 =
ENTERPRISE_DOMAIN, ENTERPRISE_TASK, PERSONAL→ 데이터 보존 확인
- 개명 전
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 42 파일 / 344 테스트 통과. 리허설로 DB 를 비운 뒤0000~0005를 신규 DB 에 처음부터 적용하는 경로도 함께 통과했다.- 개명을 강제로 되돌리면 실패하는 테스트를 남겼다 —
postgresStores.test.ts"개명된 유형 코드가 DB enum 에 있다"(개명 전에는invalid input value for enum agent_type로 실패함을 실제로 확인).
남은 것
- §21.2-2 의
legacy_policy_version별도 필드와 §21.2-5 의legacy_compliance_status는 정책 모델(policy_snapshot)과 함께 단계 2 에서 도입한다. 지금은 릴리스의classificationPolicyVersion이 당시 정책 기록을 남기고, 기존ACTIVE에 검증 이력을 만들지 않는다. - §21.2-3 이 요구하는 "기존 Domain 의 L5 책임 범위를 재분류 검토 대상으로 표시"는 집계·검토 화면 단계(단계 5)에서 대상 목록으로 보여준다 — 판정 규칙은 이번 전환으로 이미 그렇게 동작한다(데모 정책
L4기준 불일치 → 예외 승인 필요).
D-017. Domain 허용 레벨을 L4 로 둔다 — L1~L3 은 재분류 대상으로 남긴다
- 일자: 2026-09-22
- 상태: 팀 결정 (단계 1 산출물의 정책 값 확정 · 사내 승인은 별도)
- 설계서 원안: §0.3 "Domain의 'L4 이상' — L4 및 그보다 상위 업무 범위로 해석하되 분류체계별 허용 레벨 목록을 정책으로 확정한다. 숫자 비교 level >= 4 로 구현하지 않는다", §4.3 "G1의 'L4 이상'을 L1~L4 전체 허용으로 운영 적용할지는 확인 대상이다. 데모 정책에서는 L4를 Domain의 명확한 기준 사례로 사용한다", §21.3 Q-02.
결정
LEVEL_POLICY_TEMPLATE.domain.allowedLevels 를 ["L4"] 로 유지한다 — 코드 변경 없음(단계 1에서 쓴 값 그대로).
이유
- §4.3 이 데모 정책의 Domain 기준 사례로 L4 를 명시한다. "L4 이상"을 L1~L4 전면 허용으로 읽는 해석은 사내 분류체계 대응 확인이 필요하므로 미확정으로 남긴다 — §0.3 이 "분류체계별 허용 레벨 목록을 정책으로 확정한다"고 요구하는 그 목록이 아직 없다.
- 이 선택의 영향 범위는 L1~L3 뿐이다. L5~L7 은 어느 해석에서도 Domain 이 아니다(L4=Process 보다 세분화된 레벨이므로). 그래서 §21.2-3 의 "기존 Domain 의 L5 책임 범위는 재분류 검토 대상"은 가이드 자체의 결론이고, 이번 결정으로 생긴 것이 아니다.
- 넓히지 않는 쪽이 안전하다. L1~L3 매핑이 있는 Domain 은 불일치로 표시되어 §6.4 의 분류 예외 승인 경로를 탄다 — 자동 승인이 아니라 검토를 거치게 된다. 반대로 넓히면 그 검토 기회가 사라진다.
- 숫자 비교로 구현하지 않는다(§0.3). 목록 대조이므로 레벨 체계가 바뀌어도 정책 한 곳만 고친다.
영향
| 영역 | 변경 |
|---|---|
| 코드 | 없음 — 판정 규칙(allowedLevelsFor)과 동작이 그대로다 |
| 정책 템플릿 | domain.note 에 이 결정을 명시. provisional: true·openQuestionIds: ["Q-02"] 는 유지 — 사내 승인 전이므로 확정 정책으로 표기하지 않는다 |
| 미확정 목록 | openQuestions.ts Q-02 의 "미확정 시 처리" 문구를 현재 동작(L4 기준 · L1~L3 재분류 표시 · 예외 자동 허용 금지)으로 정리 |
| 기존 데이터 | L1~L3 대표 매핑을 가진 Domain 은 불일치 → 예외 승인 필요. 자동 재분류·자동 승인 없음(§21.2-3) |
검증
pnpm check·pnpm test통과(판정 동작이 바뀌지 않으므로 기대값 변경 없음 — 값 유지가 근거다).
남은 것
- L1~L4 전면 허용 여부와 원본 분류체계(ProcessCenter
level_code) 대응은 사내 확정 대상이다. 확정 시shared/policyTemplate.ts한 곳만 고치면 되고, 기존 매핑의 재판정은 §21.2-3 절차(자동 승인 금지)로 별도 수행한다. - 재분류 대상 목록을 화면에 보여주는 일은 집계·검토 화면 단계(단계 5)에서 한다.
D-018. 회사 적용 정책 모델을 도입한다 — 템플릿은 효력이 없고, 활성 정책이 게시의 전제다 (AT-72)
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 2 · 정책 모델)
- 설계서 원안: §0.1 "가이드 기준을 정책 템플릿으로 제공하고, 적용 조직의 승인된 정책 버전을 통해 효력을 부여한다", §15.3
governance_policy·policy_snapshot(그리고policy_rule), §16.3POST /governance-policies(초안)·/activate(적용 승인·시행 시점 확인 후 활성화), §21.3·AT-72, §6.1 역할.
결정
- 정책을 두 층으로 나눈다.
shared/policyTemplate.ts(가이드 템플릿, 효력 없음)와governance_policy+policy_snapshot(승인·활성화된 정책, 효력 있음). - 활성 스냅샷이 없으면 게시(신규 운영 승인)를
POLICY_NOT_ACTIVATED(422)로 차단한다(AT-72). 템플릿을 대체재로 쓰지 않는다 — 쓰면 §0.1 의 효력 부여 절차가 사라진다. - 스냅샷 해시는
companyId·version·sourceRefs·rules를 함께 해싱한다 — 정책을 식별하는 값(회사)과 내용(버전·참조·규칙) 을 모두 포함한다. - 활성 정책은 회사별 하나 — 부분 고유 인덱스로 보장하고, 활성화는 이전 활성 정책을
SUPERSEDED로 밀어낸다(같은 트랜잭션). - 역할: 초안은
GOVERNANCE_MANAGER, 활성화는POLICY_APPROVER.ADMIN은 승인 권한을 자동 취득하지 않는다(§6.1) — 그래서 ADMIN 을 허용하지 않았다. policy_rule테이블은 만들지 않았다. 규칙 묶음을rulesjsonb 로 담고, 규칙 단위 재정의·예외(rule_type·parameters·severity·overridable)가 실제로 필요해질 때 도입한다.
이유
- §0.1 이 두 층을 명시한다. 템플릿만 있으면 "회사가 무엇을 승인했는가"를 말할 수 없고, AT-72 는 정책 미활성 시 차단을 요구한다. 그래서 활성 스냅샷 유무가 게시 가능 여부를 가른다.
- 해시 범위는 구현 중 두 번 고쳤다 — 둘 다 테스트가 잡았다.
- 처음에는 규칙만 해싱했다 → 내용이 같은 두 버전(v1·v2)이 같은 해시를 얻어 새 버전을 활성화할 수 없었다(고유 인덱스가 막는다).
version·sourceRefs를 포함해 고쳤다. - 그 다음 회사 id 를 빼고 해싱했다 → 서로 다른 회사가 같은 정책 내용을 쓰면 해시가 충돌했다(테스트 DB 에 이전 실행의 행이 남아 있는 상태에서 재실행하자
policy_snapshots_hash_unique위반으로 드러났다).companyId를 포함해 고쳤고, 재실행이 멱등해진 것을 2회 연속 실행으로 확인했다.
- 그래서 스냅샷 해시는 "정책을 식별하는 값(회사) + 내용(버전·참조·규칙)"을 모두 담는다.
hashPolicyRules(규칙만)는 "같은 규칙인가" 비교용으로 남겼다.
- 처음에는 규칙만 해싱했다 → 내용이 같은 두 버전(v1·v2)이 같은 해시를 얻어 새 버전을 활성화할 수 없었다(고유 인덱스가 막는다).
- 부분 고유 인덱스는 drizzle-kit 이 만들지 않는다. §15.5 가 요구하는 제약이므로 마이그레이션 SQL 로 보완하고, 그 사실을 SQL 주석에 남겼다(기존 §7.8 부분 인덱스도 같은 이유로 여전히 미보완 — 남은 것 참조).
- 역할을 먼저 배선했다. §6.1 의 17개 역할 중 정책에 필요한
GOVERNANCE_MANAGER·POLICY_APPROVER가 v0.1 7-역할 집합에 없었다. 역할을 임시로ADMIN에 붙이면 "ADMIN 은 승인 권한을 자동 취득하지 않는다"를 어기므로, 역할 집합을 §6.1 로 확장하고(값 추가라 마이그레이션 불필요) 정책 역할을 정확히 검사한다. - 데모·테스트는 활성 정책을 기본으로 넣는다. 안 넣으면 등록 직후 모든 버전이 게시 불가가 되어 §20.3 의 MVP 흐름이 깨진다. 대신 AT-72 는 미활성 상태를 재현해 검증한다(
activePolicy: false, 그리고 "차단 → 정책 활성화 → 같은 버전 게시" 순서). - 기존 DB 호환: 컨테이너 시드는 에이전트 유무와 무관하게 활성 정책을 보장한다 — 정책 없이 기동해 "게시가 왜 막히는지" 모르는 상태를 만들지 않는다.
영향
| 영역 | 변경 |
|---|---|
| 계약 | PORTAL_ROLES 를 §6.1 17개로 확장(값 추가), GOVERNANCE_POLICY_STATUSES(DRAFT/ACTIVE/SUPERSEDED 제안), POLICY_NOT_ACTIVATED(422) |
| 도메인 | 신규 server/domain/governance/policy.ts — PolicyRules(4축 규칙 묶음), hashPolicyRules/hashPolicySnapshot, buildPolicySnapshot, assertPolicyActivated, buildTemplatePolicyRules |
| 포트 | PolicyStore(정책·스냅샷) + AgentCenterDeps.policy |
| 저장소 | governance_policies·policy_snapshots 테이블 — 마이그레이션 0006_harsh_chimera.sql(신규 테이블이라 기존 데이터 영향 없음) + 활성 정책 부분 고유 인덱스 |
| 유스케이스 | 신규 server/application/policyService.ts — createPolicyDraft·activatePolicy·currentSnapshot (초안·활성화를 감사에 남긴다 — §19.3) |
| 게시 게이트 | publishReadiness 가 활성 스냅샷 유무를 보고 차단. PUBLISH_ISSUE_PRIORITY 최우선에 POLICY_NOT_ACTIVATED |
| 판정 규칙의 출처 | AgentCenterDeps 의 정책 4종 정적 필드를 policies: PolicyResolver 로 교체 — 활성 스냅샷의 규칙을 쓰고, 없으면 TEMPLATE_FALLBACK(효력 없음) |
| 화면 데이터 | admin.classificationPolicy 가 유효 정책(출처·활성 여부 포함)을 돌려준다 |
| 시드 | seedDemoPolicy(데모 정책 + 스냅샷 + ACTIVE) · 컨테이너 시드 · 인메모리 저장소의 activePolicy 옵션(기본 true) |
| 컨테이너 | policyService 노출 |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 44 파일 / 363 테스트 통과. 마이그레이션0006이 실제 PostgreSQL 에 적용되고 정책 규칙(jsonb)·스냅샷 해시가 왕복한다.- 신규
tests/unit/policy.test.ts(8) — 해시 결정성(키 순서·회사·버전·참조), 활성화 가능 조건, 스냅샷 고정, AT-72 차단(422), 템플릿 파생. - 신규
tests/integration/policyService.test.ts(8) — 역할 게이트(ADMIN 거부 포함), 활성화가 이전 정책을 밀어냄, 감사 기록, AT-72 종단(미활성 차단 → 활성화 → 게시), 2a-2 종단(활성 정책의 규칙이 바뀌면 같은 버전의 판정이 바뀐다 — Domain L5 불일치 → 정책 확대 후 게시). tests/integration/memoryStores.test.ts(+1) — 기본 상태는ACTIVE_POLICY, 미활성 상태는TEMPLATE_FALLBACK(해시 null).tests/integration/postgresStores.test.ts(+1) — 정책·스냅샷 왕복(규칙 jsonb 무손실).
남은 것
- 케이스·게이트(2b), 결재 슬롯(2c), 사전심의(2d), 평가 스냅샷 저장(2e).
- 게시 시점의 정책 버전 기록: 릴리스의
classificationPolicyVersion은 생성 시점 기록이다. §5.2 가 요구하는 "평가 결과에 정책 버전 저장"은 게이트의policy_snapshot_id(2b)로 붙인다. policy_rule도입 시점(위 결정 6)과 §7.8 부분 고유 인덱스(PUBLISHED 1개·작업 버전 1개) 보완.legacy_policy_version(§21.2-2)·legacy_compliance_status(§21.2-5) 는 정책 모델이 생겼으므로 다음 전환 작업에서 붙인다.- 정책 화면(UI-18 정책·결재)은 화면 단계에서 연결한다 — 서버 응답은 출처(
source)·활성 여부(activated)까지 준비되어 있다.
D-067. 화면 2단계를 이었다 — 운영·변경·폐기·내 Agent 와 결재 템플릿 (§17.2)
- 상태: 채택 (설계서 §10.3 · §11 · §12 · §13 · §7.4 · D-066 의 남은 것)
- 배경: D-066 이 등록·심의·승인 화면을 이었지만, 운영 단계의 화면이 없었다. API 는 D-053~D-059 로 이미 노출되어 있었다(운영 적용·KPI·사건·변경·폐기). 화면이 없으면 그 API 는 사람에게 닿지 않는다 — D-051 이후 반복해 온 판단이다.
결정
- 운영 적용·배포 확인(UI-14)에서 적용 작업과 런타임을 분리해 기록한다: §10.3 의 "SUCCEEDED 는 정상 동작의 보증이 아니다"를 화면 구조로 옮겼다 — 적용 결과·런타임 관측을 각각의 패널로 두고, 게시 포인터와 실제 운영 버전을 합치지 않는다(AT-64: 실제 운영 중 / 게시되었지만 미적용 / 미완료 범위). 최소 확인 7항목은
shared/deployment카탈로그 문구를 그대로 쓰고, 확인되지 않은 항목이 있으면 기록 버튼을 열지 않는다(서버도 차단한다). - KPI·성과 점검(UI-15)에서 미수집과 "관측된 0" 을 구분한다(AT-60): 빈칸은
null(미수집)로 보내고0으로 채우지 않는다. 실측(MEASURED)·기대 효과(EXPECTED)는 조회 종류로 나눈다. 정기 점검 재계산은 앞당긴 건수만 보고한다 — 느슨한 연기는 도메인이 하지 않는다(AT-75). - 권한·Risk 점검(UI-15)은 판정을 다시 하지 않는다: Risk·환경·적용 대상 판정과 운영 준비 상태(
operation.readiness)를 보여 주고, 최소 권한 설계·승인 범위 확정만 기록받는다. 전체 허용처럼 최소 권한이 아닌 설계도 기록은 허용하고 준비 상태가 차단한다(§10.1 — 기록을 막으면 실제 부여된 권한이 어디에도 남지 않는다). - 사건·개선조치(UI-16)에서 요청과 확인을 갈라 둔다: 중지 요청과 런타임 확인은 다른 사실이고(§11.4), 미확인 사건은 화면 상단에 지속 경고로 남는다. 종료는 재개 결정·결정자·근거가 있어야 하며 결정자는 로그인 사용자를 그대로 쓴다(값을 만들어 넣지 않는다).
- 변경 영향(UI-08)은 차이와 근거를 낸다: 두 버전 스냅샷을 나란히 비교하고, 평가 결과(변경 항목·종류·Risk 상승·재검증·재승인 범위)를
review/presenters의 게이트 이름으로 표시한다. 승인 재사용은 네 확인을 사실대로 넘기고 범위 해시 비교는 서버가 한다(AT-63) — 화면이 "같다"고 주장해도 재사용으로 성립하지 않는다. - 폐기 후보·실행(UI-17)에서 후보와 실행을 구분한다: 후보 판정은 검토자가 확인한 수치 목표를 입력으로 받고(D-062), 근거가 모자란 Agent 도 이유와 함께 남긴다(AT-66). 정리 확인은
DONE만으로 끝나지 않는다 — 확인자가 없으면 미확인으로 표시하고 기록을 막으며, 공유 자원은 연결 해제만 한다(AT-70). - 내 Agent(UI-02)는 업무 책임자 기준만 센다: 서버가 주는 소유 정보는
businessOwnerId·owningOrgId뿐이라, 참여 관계(RACI·수행 역할)는 담당 연결(§19.1 · Q-04)이 선 뒤에 더한다. 화면에 무엇을 기준으로 걸렀는지를 함께 보여 준다 — 목록이 비었을 때 "없다"고 단정하지 않기 위해서다. - 결재 템플릿(관리자)을 편집 가능한 첫 관리자 화면으로 넣었다: 활성 템플릿 조회 · 초안 작성(버전·적용 유형·슬롯의 순서·선행·조건) · 활성화 · 케이스 경로 현황. 작성(
GOVERNANCE_MANAGER)과 활성화(POLICY_APPROVER)는 다른 사람이고(§7.4), 자격 범위 미해석은 사유로 표시한다(§19.3). 분류 정책 편집은 사내 값 확정(§17) 전이라 여전히 조회 전용이다 — 화면에 동작하지 않는 버튼을 두지 않는다는 D-010 의 판단을 유지한다.
영향
| 영역 | 변경 |
|---|---|
| 화면 | 신규 client/src/pages/{ChangeImpact,MyAgents,Retirements}.tsx · client/src/pages/operations/{AgentScopeField,OpsDeployment,OpsKpi,OpsMonitoring,OpsIncidents}.tsx · client/src/pages/admin/AdminApproval.tsx |
| 메뉴 | ADMIN_MENU_PORTAL 에 admin-approval(결재 템플릿) 추가 — SCREEN_REGISTRY·AdminRoutes 도 함께 |
| 배선 | client/src/App.tsx renderView / AdminRoutes — 운영·변경·폐기·내 Agent 7화면 + 결재 템플릿. SCREEN_REGISTRY.implemented 7종 true |
| 테스트 | 신규 client/src/pages/portalScreens.test.tsx(10) · admin/pages.test.tsx(+1) |
검증
pnpm check·pnpm build·pnpm test통과 — 93 파일 / 648 테스트(통과 614 · PostgreSQL 34 skip, 직전 637 + 11).- 화면은 DOM 없이 서버 렌더로 제목·입력란·규칙 문구를 확인한다(D-005). 판정 규칙은 서버 통합 테스트가 계속 소유한다 — 화면 테스트는 "규칙을 다시 만들지 않는지"를 골격으로만 본다.
남은 것 (§17)
- 정책 템플릿 화면(
policy.*— 초안·활성화): 결재 템플릿과 같은 방식으로 붙인다. 분류 정책 편집은 사내 값 확정(§17) 후. - 목표 수치 모델(D-062) · 새 버전 작성(
createReleaseVersion) · 동기화 Worker · 버전 비교(UI-09). - 운영 승인 화면의 결재 슬롯은 중앙 조직·직책 연계(§19.1)가 붙어야 실제로 돈다.
D-068. 매뉴얼을 포털 화면으로 붙였다 — 문구의 원천은 데이터 한 곳 (§17.2)
진입 자리는 D-069 에서 셸 슬롯(
manualLink)으로 옮겼다 — 매뉴얼 내용·문구 원천은 그대로다.
- 상태: 채택 (설계서 §17.2 · §16.1-11 · D-067 남은 것 아님 — 새 판단)
- 배경: §17.2 화면을 2단계(D-066·D-067)까지 이었지만 쓰는 방법은 저장소 문서에만 있었다. 사용자는 README·런북을 오가며 자기 화면 이름을 찾아야 했고, 관리자는 기동 실패 대응(D-010·런북 4.3)을 저장소에서 찾아야 했다. 매뉴얼이 배포본 밖에 있으면 화면이 바뀔 때 조용히 어긋나고, 어긋난 매뉴얼은 없는 것보다 나쁘다 — 그래서 배포본과 같이 도는 화면으로 옮겼다(§16.1-11 "검증하지 않은 항목을 완료했다고 보고하지 않는다"의 연장).
결정
- 매뉴얼은 데이터, 화면은 렌더러다:
shared/manual.ts가 절·단계·주의의 원천이고client/src/pages/ManualDocument.tsx가 유일한 렌더러다. 화면 컴포넌트에 문구를 복사하지 않는다 — 복사하면 한쪽만 고쳐진다(§15.3 카탈로그와 같은 판단). - 매뉴얼이 가리키는 화면은 실재해야 한다: 절·단계의
href는 메뉴 정의(MENU_TREE·ADMIN_MENU)의 경로만 쓴다. 테스트가 강제하므로 화면 경로가 바뀌면 매뉴얼 링크가 먼저 깨진다 — 이 저장소에서 매뉴얼은 경로 변경의 감지기이기도 하다. (작성 중 이 규칙이 실제로 잡았다: 사용자 매뉴얼 '시작하기' 가/admin/users를 링크하고 있었다 — 관리자 영역은 경로를 글로만 알려 주고 링크하지 않는다.) - 문서를 둘로 나눈다:
/manual(사용자, 메뉴manual, 역할 게이트 없음) ·/admin/manual(관리자 셸, 메뉴admin-manual, 소유 PORTAL·ADMIN). 관리자 매뉴얼에는 화면 밖 조작도 담았다(환경 변수·배포·동기화·감사·데모 모드) — 기동 실패 대응은 관리자가 자기 자리에서 읽어야 하는 절차다. - 설계서 §8 의 화면 번호(UI-xx)를 새로 만들지 않고
HELP로 둔다: §17.2 가 번호를 준 화면과 섞으면 인수 시나리오 대응표가 흐려진다. 매뉴얼은 "절차 안내"지 "업무 화면"이 아니다. - 절 목록을 한 페이지에 그린다: 절차서를 탭·접기로 숨기지 않는다. 사용자가 브라우저 찾기로 절차를 찾을 수 있어야 하고, 화면이 절을 골라 빼먹는 실수도 드러난다. 차례는 절 제목·요약을 보여 주고 번호를 붙이지 않는다.
- 절의
roles는 표시 힌트일 뿐 게이트가 아니다: 매뉴얼 화면을 역할로 막으면 읽어야 할 사람이 못 읽는다. "이 절은 이런 역할의 일이다"만 배지로 알려 준다.
영향
| 영역 | 변경 |
|---|---|
| 데이터 | 신규 shared/manual.ts — USER_MANUAL(13절) · ADMIN_MANUAL(11절) · MANUAL_DOCS · findManualSection() |
| 화면 | 신규 client/src/pages/ManualDocument.tsx(공용 렌더러) · pages/Manual.tsx · pages/admin/AdminManual.tsx — PortalPage/PortalPanel/PortalStatusBadge 재사용 |
| 메뉴 | MENU_TREE 에 manual(/manual) · ADMIN_MENU_PORTAL 에 admin-manual(/admin/manual) — SCREEN_REGISTRY 는 HELP uiId 로 등록 |
| 배선 | client/src/App.tsx renderView manual · AdminRoutes admin-manual, client/src/routing.test.ts 에 두 경로 해석 단언 추가 |
| 스타일 | client/src/index.css .ac-manual-link — 본문 안 화면 링크 |
| 문서 | 신규 docs/manual.md(원천·갱신 절차·넣지 않은 것), README 문서 목록 |
| 테스트 | 신규 shared/manual.test.ts(8) · client/src/pages/manual.test.tsx(5) |
검증
pnpm check·pnpm build·pnpm test통과 — 95 파일 / 661 테스트(통과 627 · PostgreSQL 34 skip, 직전 648 + 13).- 매뉴얼 문구는 DOM 없이 서버 렌더로 확인한다(D-005): 화면이 절을 빠뜨리지 않는지(
data-testid="manual-section-*"), 주의 문구를 요약해 줄이지 않는지,**강조**가 굵게 그려지는지만 본다. 문장이 맞는지는 사람이 읽고, 경로·중복·역할 값·강조 짝은shared/manual.test.ts가 데이터 단계에서 막는다. - 점검 중 어긋난 기록 두 곳을 함께 고쳤다:
docs/implementation-status.md의navigation.test.tsx건수(16 → 실제 17), README 「현재 구현 범위」 표가 아직 v0.1 화면(UI-01·02·06·04·07)을 "다음 단계"로 두고 있던 행(§17.2 1~2단계 반영으로 갱신).
남은 것
- 인쇄·배포용 산출물(PDF·Markdown): 같은 원천에서 생성하면 된다 — 지금은 만들지 않았다.
- 지면 내 해시 이동·전체 검색: 화면 간 이동은 셸 메뉴가 담당한다. 매뉴얼 지면 검색은 절이 더 늘면 다시 본다.
- 화면 캡처 이미지: 배포본이 바뀌면 조용히 어긋나므로 넣지 않았다. 필요하면 CI 에서 캡처를 생성하는 방식으로 붙인다(§16.1-11).
- 매뉴얼과 실제 화면의 문구 자동 대조: 지금은 "절차가 화면 항목을 언급했는가"를 테스트하지 않는다. 화면 라벨을 카탈로그로 옮기면 대조할 수 있다.
D-069. 매뉴얼 진입점을 공용 셸 슬롯으로 옮겼다 — 포털마다 자리를 다시 만들지 않는다 (MANUAL_CONVENTION)
- 상태: 채택 (D-068 보강 — 매뉴얼 내용이 아니라 진입 자리의 결정)
- 배경: D-068 은 매뉴얼을 화면으로 붙였지만 진입점을 메뉴 행(
MENU_TREE의 9번째 = 맨 아래 항목)으로 두었다. 신고: "포털에서 바로 볼 수 있는 메뉴였으면 좋겠다" — 그룹을 펼치면 맨 아래 항목이 스크롤에 묻힌다. 그리고 매뉴얼은 모든 포털에 있고 업무 도메인이 아니라 사용법이므로 포털마다 자리·모양을 새로 정할 이유가 없다. 게다가 사용자 메뉴는 L1(로컬) → L2(포털 DB) → L3(중앙) 이전 경로가 예정돼 있어(portal-foundation docs/MENU_GOVERNANCE_DESIGN.md§6) 메뉴 행으로 두면 공급처가 바뀔 때 항목이 사라질 수 있다.
검토한 대안
| 대안 | 채택하지 않은 이유 |
|---|---|
| 사이드바 메뉴 맨 위로 이동 | 자리가 포털마다 달라지고, 메뉴 공급처(L2·L3) 이전 때 다시 붙여야 한다 |
상단 바 버튼(topNavEnd) 추가 | 진입점이 둘이 되어 "한 곳에서 찾는다"가 깨진다. 알림·계정 아이콘과 성격도 섞인다 |
| AgentCenter 로컬에서만 처리 | 같은 결정을 다른 포털이 다시 내려야 하고, 매뉴얼 자리가 포털마다 달라진다 |
결정
- 진입 자리는 플랫폼(portal-foundation)이 소유한다:
PortalShell/PortalAdminShell에manualLink?: { href, label?, iconKey?, active? }슬롯을 두고 사이드바 하단 고정(sticky) 으로 그린다. 라벨 기본값(매뉴얼/Manual)·기본 아이콘(BookOpen)·접힘 상태 동작까지 셸이 정한다 — 포털은href만 넘긴다. 규약: portal-foundationdocs/MANUAL_CONVENTION.md. - 메뉴 행으로 중복 정의하지 않는다:
MENU_TREE의manual·ADMIN_MENU_PORTAL의admin-manual을 뺐다. 진입점은 하나다. - 경로는 라우팅 표에 남긴다: 메뉴 행이 아니어도 화면은 경로를 갖는다(
ROUTING_CONVENTION.md§2.4).client/src/routing.ts의STANDALONE_ROUTES가/manual·/admin/manual경로의 원천이고, 경로 상수(MANUAL_PATH·ADMIN_MANUAL_PATH)는shared/const.ts에 두어 셸 슬롯의href와 같은 값을 쓴다.ADMIN_HOME_PATH(=/admin/users)는 공통 구간 첫 항목 그대로다. - 관리자 매뉴얼도 같은 슬롯:
PortalAdminShell은PortalShellProps를 상속하므로 관리자 셸도manualLink하나로 처리된다(현재 화면이면active로aria-current표시). - 공용 UI 버전을 올려 소비 포털이 같은 자리를 쓴다: portal-foundation
0.6.156 → 0.6.157(vendor tarball 갱신). 다른 포털이 같은 슬롯을 쓰면 매뉴얼 위치가 포털마다 갈라지지 않는다.
영향
| 영역 | 변경 |
|---|---|
| 플랫폼 | portal-foundation PortalShell·PortalSidebar 에 manualLink 슬롯 + .pf-shell-sidebar__manual* 스타일, 신규 docs/MANUAL_CONVENTION.md, manualLink.contract.test.tsx(6 테스트) |
| 메뉴 | 사용자 메뉴 9 → 8 항목, 관리자 포털 구간 5 → 4 항목 — 매뉴얼은 셸 슬롯이 진입점 |
| 라우팅 | STANDALONE_ROUTES(메뉴 행이 아닌 화면) 신설 · MANUAL_PATH/ADMIN_MANUAL_PATH 상수 · 사용자 표 끝/관리자 표 끝에 배치 |
| 배선 | client/src/App.tsx — PortalShell.manualLink(사용자) · PortalAdminShell.manualLink(관리자, label: "관리자 매뉴얼") |
| 의존 | vendor/hohyun94-portal-foundation-0.6.157.tgz |
| 문서 | docs/manual.md(진입 자리 갱신) · docs/runbook.md(버전 확인 값) · docs/integration-contract.md·docs/step0-verification.md(vendor 버전) |
검증
- portal-foundation:
pnpm verify:full통과(10개 패키지 타입 검사·빌드) — 슬롯 렌더·기본 라벨(로케일)·접힘 상태·미지정 시 미렌더·관리자 셸 상속을manualLink.contract.test.tsx가 고정한다. - AgentCenter:
pnpm verify:full통과 — 95 파일 / 667 테스트(통과 633 · PostgreSQL 34 skip, 직전 661 + 6). - 회귀 방지:
routing.test.ts가 "매뉴얼 경로는 메뉴 정의에 없고 라우팅 표에만 있다"를,navigation.test.tsx가 "셸 슬롯이 링크를 그리고 메뉴에는 매뉴얼 경로가 없다"를 고정한다 — 진입점이 다시 둘로 갈라지면 먼저 깨진다.
남은 것
- 다른 포털의 같은 전환(ProcessCenter · GuideCenter · DocumentLibrary · lunchapp):
manualLink배선 + 메뉴 정리 +MANUAL_CONVENTION.md체크리스트. - 매뉴얼 본문 렌더러의 공통화: 지금은 진입 자리만 공통이고 절·단계·주의를 그리는 렌더러는 포털 소유다(
ManualDocument.tsx). 구조가 굳으면 플랫폼 컴포넌트로 승격한다.
D-070. 셸 검색창을 통합 탐색에 이었다 — 색인 하나, 경로 둘(셸 tRPC · ExploreGateway 어댑터), 답변은 아직 켜지 않는다
- 일자: 2026-09-25
- 상태: 채택 (설계서 §8.2 통합 검색 · §4.2 권한 범위 · §10.5 오류·빈 상태)
- 배경: 매뉴얼(D-068·D-069)까지 붙어 "무엇이 어디 있는지"는 문서로 남았지만 찾는 길은 메뉴를 훑는 것뿐이었다. 설계서 §8.2 는 포털을 가로지르는 통합 검색을 요구하고, 공용 셸(portal-foundation)은 검색창·대화 다이얼로그·답변 슬롯을 이미 갖고 있었다. 문제는 그 창을 누가 채우는가였다 — 포털마다 색인과 권한 판정을 따로 만들면 같은 질의가 화면과 게이트웨이에서 다른 답을 낸다.
검토한 대안
| 대안 | 채택하지 않은 이유 |
|---|---|
| 화면별 클라이언트 검색(메뉴 + 현재 목록 필터) | 권한을 클라이언트가 판정하게 된다 — 가려진 결과의 개수·제목이 번들·네트워크에 실린다(§4.2). 메뉴 밖 화면은 아예 못 찾는다 |
| 게이트웨이 어댑터만 붙인다 | 셸 검색창이 빈 채로 남고, 게이트웨이가 안 떠 있으면 이 포털 사용자에게는 검색이 없다 |
| 셸(tRPC)만 붙인다 | 다른 포털에서 찾을 때 이 포털이 보이지 않는다(§8.2 는 포털 간 통합 탐색이다) |
| 답변 생성까지 함께 켠다 | 근거 문장 생성은 ModelGateway 연계가 필요하고 이 포털에는 아직 그 경로가 없다. 빈 슬롯을 켜면 "답을 만들 수 있는데 답이 없는" 상태로 보인다(§10.5) |
결정
- 색인 소유자는 이 포털이고, 함수는 하나다 —
server/adapters/explore/index.ts의searchExplore(deps, input)이 메뉴(MENU_TREE)·매뉴얼(MANUAL_DOCS)·Agent 저장소를 한 색인으로 만들고 권한 필터 → 문장 매칭 → 결정적 순위 → 상한 순으로 끝낸다. 두 경로(셸 tRPCexplore.search· 어댑터POST /api/explore/v1/search)가 같은 함수를 지난다 — "화면에서 본 결과"와 "게이트웨이가 본 결과"가 갈리지 않는다. - 권한은 결과를 만들기 전에 건다 — 메뉴·매뉴얼은
canSeeMenu(roles, currentRoles)(ADMIN 우회, 미지정=공개), Agent 는 목록 화면과 같은filterVisibleResources. 볼 수 없는 것은 개수도 제목도 응답에 실리지 않는다(§4.2). 색인 항목의 경로를 새로 적지 않는다 — 메뉴 정의의href만 쓴다(라우팅 규약 §2.4). - 셸은 제출할 때만 질의한다 —
client/src/explore/useShellExplore.ts가onSearchSubmit을 tRPC 질의로 옮기고, 응답 전에는results: undefined를 넘긴다(빈 배열을 넘기면 "아직 못 읽었다"가 "결과 없음"으로 보인다 — PF 는undefined일 때 현재 포털 메뉴를 바로가기로 채운다). 선택은 훅이 SPA 이동으로 처리한다(셸은onSelect가 있으면 스스로 이동하지 않는다). - 어댑터는 계약 그대로, 토큰으로 열리고 닫힌다 —
Authorization: Bearer <EXPLORE_SERVICE_TOKEN>(게이트웨이PORTAL_SERVICE_TOKENS_JSON["agent-center"]와 같은 값) ·x-explore-subject-id필수(없으면 400) ·x-explore-org-id·x-explore-roles·x-explore-request-id선택 ·query1~200자 ·limit1~30(초과분은 자른다). 토큰이 다르면 401, 포털에 토큰이 없으면 503(EXPLORE_UNAVAILABLE) — 미구성 연계가 아무나 통과하는 검색 창구가 되지 않는다(§19.3). - 주체·역할 헤더를 신뢰하는 근거는 토큰이다 — 서비스 토큰을 가진 호출자만 통과하므로 게이트웨이가 아는 컨텍스트를 그대로 받는다. 대신 주체를 추정하지 않는다(없으면 400). 역할이 없으면 VIEWER 로 본다(최소 권한 — 관리자 매뉴얼 절이 새지 않는다).
- 답변 슬롯은 켜지 않는다 —
onExploreAsk·exploreAnswer를 넘기지 않는다. ModelGateway 경로(AI_ENABLED·MODEL_GATEWAY_*)가 붙을 때 함께 켠다. - 공용 UI 를 0.6.158 로 올린다 — 0.6.157 은
onSearchSubmit을 선언만 하고PortalExploreDialog로 넘기지 않아 제출이 동작하지 않았다(셸의onSubmit배선 누락). 이 결정에서vendor/hohyun94-portal-foundation-0.6.158.tgz로 갱신한다.
영향
| 영역 | 변경 |
|---|---|
| 플랫폼 | portal-foundation 0.6.157 → 0.6.158(셸 검색창 제출 배선 + 답변 슬롯) |
| 서버 | server/adapters/explore/{index,route}.ts 신설 · server/routers/explore.ts(explore.search) · shared/errors.ts EXPLORE_UNAVAILABLE(503) · server/_core/env.ts EXPLORE_SERVICE_TOKEN · server/app.ts 어댑터 등록 |
| 클라이언트 | client/src/explore/useShellExplore.ts · client/src/App.tsx 배선(onSearchSubmit·exploreResults·onExploreSelect + 오류 표시) |
| 권한 | shared/const.ts 의 canSeeMenu 가 메뉴·매뉴얼 노출 판정의 단일 원천(client/src/navigation.tsx 가 재수출) |
| 문서 | .env.example · docs/runbook.md(§2-2 어댑터 실 확인) · docs/integration-contract.md(§2-2 어댑터 계약) |
| 의존 | vendor/hohyun94-portal-foundation-0.6.158.tgz |
검증
pnpm verify:full통과 — 97 파일 / 688 테스트(통과 654 · PostgreSQL 34 skip, 직전 96 파일 / 686 테스트).tests/unit/exploreAdapter.test.ts(17) 가 색인·권한·경로·어댑터 헤더 계약을 고정한다: 빈 질의는 빈 결과 · 가려진 메뉴·매뉴얼 절은 결과 자체가 만들어지지 않음 · 다른 조직의 Agent 는 개수도 안 나옴(목록 화면과 같은 판정) ·Bearer없음·불일치 401 · 주체 헤더 누락 400 · query 0자·201자 400 · limit 초과는 절단 · 200 응답 형태와portalId=agent-center.client/src/explore/useShellExplore.test.ts(4) 가 서버 결과 → 셸 결과 변환과 알 수 없는kind의document강등을 고정한다.- 회귀 방지: 셸 검색이 클라이언트에서 다시 거르지 않고(규칙이 둘이 되지 않는다), 색인이 메뉴 정의 밖의 경로를 만들지 않는다(메뉴
href만 사용) — 메뉴 밖 경로가 생기면routing.test.ts가 먼저 깨진다.
남은 것
- 답변 생성(ModelGateway 연계) — 근거 문장·citation 은 슬롯만 있고 채우지 않는다(결정 6).
- 중앙 역할 claim — 지금은 게이트웨이가 보낸
x-explore-roles를 쓴다. §4.2 중앙 claim 이 붙으면 주체만 받고 포털이 역할을 해석하도록 바꾼다(헤더 신뢰를 없애는 방향). - 관리자 셸의 검색창 — 배선은 사용자 셸(
PortalShell)뿐이다. 관리자 매뉴얼·관리자 메뉴는 이미 색인에 있으므로 슬롯만 열면 같은 색인을 쓴다. (D-072 에서 배선했다 — 같은 훅·같은 색인.) - 게이트웨이 체인 e2e — 어댑터 단독 검증까지 했다. 카탈로그 등록(PortalConsole
portal_configurations+PORTAL_EXPLORE_CATALOG_JSON)·키 주입(게이트웨이PORTAL_SERVICE_TOKENS_JSON)과 실 스택 호출은 운영 설정이 필요하다. - 색인 확장 — 지금 색인은 메뉴·매뉴얼·Agent 다. 검증 근거·감사 이력(§16.3 노출 API)까지 넣을지는 별도 결정이다.
D-066. 화면 1단계를 이었다 — 등록 마법사와 검토 데스크 (§17.2 · §17.3)
- 상태: 채택 (설계서 §17 화면 · D-065 보강)
- 배경: §16.3 API 대조는 D-064 로 닫혔지만(노출 20/20), 화면이 없으면 API 는 사람에게 닿지 않는다(D-051 의 문장 그대로). §17.2 는 등록·상세·매핑·사전심의·검증·운영 승인·운영 적용·KPI·변경·폐기·정책 화면을 요구한다.
결정
- 등록 마법사(UI-04)를 구현했다: ① 업무·책임 → ② 매핑 적용성 → ③ 데이터·환경 → ④ 등록. 등록은 초안 버전을 만들 뿐이고 유형·Risk 는 검토 결정과 서버 계산이 정한다(§4.4 · §5.2). 등록 직후 이어지는 순서(평가 3종 → 사전심의 → 매핑 검토)를 같은 화면에서 실행하고, 케이스·게이트 차단 사유를 보여 준다.
- 단계·검증 로직은 DOM 없는 순수 모듈(
client/src/pages/registration/draft.ts)로 두었다: 필수 항목은 이름으로 지목되고, 미적용 경로는 사유(정형화 곤란은 대체 업무까지)가 필요하며, 서버 입력에 빈 값을 만들지 않는다(D-005 의 DOM 제약을 지키는 방식). - 검토 데스크를 공통 뼈대로 세웠다(
GateReviewDesk): 대기함(내 역할이 닫을 수 있는 게이트) → 케이스 상세(단계·게이트·차단 사유) → 게이트 기록. 화면마다 다른 것은 도메인 기록 UI 뿐이다 — 검토 화면이 게이트 판정 규칙을 다시 쓰지 않는다. - 메뉴를 §17.1 로 맞췄다: 심의·승인 5항목(사전심의 · 매핑·유형 검토 · 검증·보안·데이터 검토 · 운영 승인 · 폐기 승인) + 운영 관리 4항목(운영 적용·배포 확인 · KPI·성과 점검 · 권한·Risk 점검 · 사건·개선조치) + 폐기 후보·실행. 아이콘은 공통 카탈로그 키만 쓴다 — 속성 테스트가 그것을 지킨다(
shared/const.test.ts). - 운영 승인은 정직한 부분 구현이다: 결재 자격 범위(직책→사람) 해석은 중앙 조직·직책 연계 소관이라(§19.1 · Q-04), 미연계면 슬롯 자격 범위 미해석 사유를 보여 주고 경로 고정·결재 버튼을 열지 않는다. 명단을 지어내면 결재가 형식이 된다 — 알려진 제한 11 과 같은 판단이다.
- 게이트 기록의 우회로는 서버가 계속 막는다: 적용 확인은 확인 기록으로만(§10.3), 데이터·환경은 환경 판정으로만(§5.3), 운영 승인은 결재 슬롯으로만(§7.4). 화면은 그 사실을 사유로 표시할 뿐이다.
- 데모 계정 3종을 채웠다:
demo-user-validation-reviewer·demo-user-data-reviewer·demo-user-retirement-approver— 게이트 담당 역할을 데모에서 재현할 수 있어야 한다(§20.3).
영향
| 영역 | 변경 |
|---|---|
| 계약 | MENU_TREE 를 §17.1 구조로 확장(심의·승인 5 · 운영 관리 4 · 폐기 후보·실행 1), SCREEN_REGISTRY 갱신 |
| 화면 | 신규 client/src/pages/RegistrationWizard.tsx · client/src/pages/registration/{draft.ts,draft.test.ts,wizard.test.tsx} · client/src/pages/review/{GateReviewDesk.tsx,presenters.ts,presenters.test.ts,screens.tsx,screens.test.tsx} |
| 배선 | client/src/App.tsx renderView — 등록·심의·승인 6개 화면 |
| 데모 | shared/demo/accounts.ts — 검증·데이터·폐기 검토자 계정 |
검증
pnpm check·pnpm test통과 — 92 파일 / 637 테스트(통과 603 · PostgreSQL 34 skip, 직전 612 + 25).- 신규 테스트:
registration/draft.test.ts(8) ·registration/wizard.test.tsx(1) ·review/presenters.test.ts(4) ·review/screens.test.tsx(6) — 화면은 DOM 없이 서버 렌더로 상태(로딩·빈 결과)를 확인하고, 규칙은 순수 함수로 못박는다(D-005).
남은 것 (§17)
- 운영 관리 4화면(배포 확인·KPI/점검·권한/Risk 점검·사건) · 변경 영향 · 폐기 후보·실행 · 내 Agent · 정책·결재 템플릿(관리자). 각 화면의 API 는 이미 있다(D-053~D-059 · D-065).
- 목표 수치 모델(D-062) · 새 버전 작성(
createReleaseVersion) · 동기화 Worker.
D-065. 검토·승인 화면의 전제를 이었다 — 게이트 대기함·닫기·결재 경로
- 상태: 채택 (설계서 §7.4 · §19.1 · §19.3)
- 배경: 화면을 붙이려니 "내가 닫을 수 있는 게이트" 를 알 방법이 없었다: 게이트 조회는 케이스 단위이고, 닫기(
setGateStatus)·결재 슬롯(approvalService)은 라우터에 없었다.
결정
- 대기함을 서버가 판정한다(
lifecycleCase.queue): 담당 역할 표(GATE_OWNER_ROLES)는 도메인 소유라, 화면이 자기 표를 만들면 역할이 늘 때 조용히 어긋난다. - 게이트 닫기를 노출한다(
lifecycleCase.setGate): 우회 차단은 서비스가 계속 지킨다 — 운영 승인은 슬롯으로만(§7.4), 운영 적용 확인은 확인 기록으로만(§10.3), 데이터·환경은 환경 판정으로만(§5.3). - 결재 경로를 노출한다(
approval.*): 템플릿(초안GOVERNANCE_MANAGER· 활성화POLICY_APPROVER)과 슬롯 현황·결재. - 자격 범위(직책→사람)는 중앙 조직·직책 연계가 해석한다(§19.1): 미연계면 명단을 지어내지 않고
AUTHORIZATION_SOURCE_UNAVAILABLE(503)로 실패하며,routeView는assigneeResolution.resolved=false와 사유를 돌려준다. - 데모 계정을 채웠다: 검증·데이터·폐기 검토자 계정을 뒤의 D-066 에서 함께 넣었다(§20.3).
검증
pnpm check·pnpm verify:full통과 — 90 파일 / 612 테스트(통과 578 · skip 34).- 신규
tests/integration/reviewQueueRouter.test.ts(3): 대기함은 내 역할이 닫을 수 있는 게이트만 준다 · 담당이 닫으면 대기함에서 사라진다 · 우회 경로를 서비스가 막는다. - 신규
tests/integration/approvalRouter.test.ts(2): 템플릿은 거버넌스 담당이 만들고 다른 사람이 활성화한다 · 자격 범위가 미연계면 명단을 지어내지 않는다.
남은 것
- 중앙 조직·직책 연계(§19.1
organization) — 붙으면 운영 승인 화면의 경로 고정·결재가 실제로 돌아간다.
D-064. 케이스·게이트 조회를 API 로 이었다 — §16.3 마지막 조각
- 상태: 채택 (설계서 §7.1·§7.5 API 노출)
- 배경:
caseService.getCase는 주석에 "§16.3GET /lifecycle-cases/{id}" 라고 적혀 있었지만 라우터에 닿지 않았다. 단계·게이트·차단 사유는 화면(UI-04·UI-07)과 승인 판단의 기본 정보다.
결정
- 두 경로를 노출한다:
lifecycleCase.get(단계·게이트·차단 사유·미적용 사유) ·byRelease(버전 → 열린 케이스). - 조회 권한을 서비스가 지킨다:
byRelease는getCase와 같은 기준(assertCanView)을 쓰는getOpenCaseForRelease를 서비스에 두었다. 판정 경로(게시·승인)가 쓰는findOpenCaseByRelease에 권한 검사를 넣으면 그쪽이 검사를 중복하고, 라우터에 규칙을 복제하면 두 벌이 된다. - 없는 케이스와 권한 밖 케이스는 다르다: 없는 id 는
NOT_FOUND("케이스를 찾을 수 없습니다")로 구분하고, 조회 권한은 서비스가 먼저 판정한다.
영향
| 영역 | 변경 |
|---|---|
| 유스케이스 | caseService.getOpenCaseForRelease 추가(조회 권한 포함) |
| 라우터 | 신규 server/routers/lifecycleCase.ts(2 프로시저) · server/routers.ts 등록 |
검증
pnpm check·pnpm verify:full통과.- 신규
tests/integration/lifecycleCaseRouter.test.ts(2): 등록 직후 열린 케이스가 단계·게이트와 함께 서고 닫히지 않은 게이트가 차단 사유로 드러난다 · 조회 전용(VIEWER)은 케이스를 보지 못하고 없는 케이스는 "찾을 수 없음"으로 구분된다.
남은 것
- §16.3 API 30종 대조가 닫힌다 — 노출 20계통 / 미노출 0. 남은 것은 화면(§17): UI-04 등록 마법사 · UI-07 매핑/승인/폐기 검토 · UI-08 변경 영향 · 신규 흐름 화면(검증·운영 적용·점검·정책).
- 폐기 검토 화면(알려진 제한 4)과 새 버전 작성(
createReleaseVersion)은 화면 단계와 별개로 남아 있다.
D-063. 정책 초안·활성화를 API 로 이었다 — §16.3 열두 번째 조각
- 상태: 채택 (설계서 §15.3 API 노출 · AT-72)
- 배경:
policyService는 서비스·테스트에만 있었고 라우터 참조 0건이었다. 정책 화면(UI-18)과 운영자가 쓸 경로가 없었다.
결정
- 조회·초안·활성화를 노출한다:
policy.list·get·createDraft·activate·currentSnapshot. - 템플릿은 효력이 없다(§0.1):
rules를 주지 않으면 가이드 템플릿(buildTemplatePolicyRules)에서 출발하고, 효력은 활성화가 준다. 활성 스냅샷이 없으면 게시가POLICY_NOT_ACTIVATED로 막힌다(AT-72). - 역할 검사는 서비스가 한다: 초안
GOVERNANCE_MANAGER, 활성화POLICY_APPROVER. 라우터가 규칙을 다시 쓰지 않는다 —ADMIN은 승인 권한을 자동 취득하지 않는다(§6.1). - 조회는 정책 역할·관리자·감사 조회자만: 규칙 묶음은 거버넌스 설정이라 일반 사용자에게 내보내지 않는다.
- 데모 계정을 채웠다:
demo-user-governance-manager·demo-user-policy-approver— 초안과 활성화를 데모에서 재현할 수 있어야 한다(§20.3).
영향
| 영역 | 변경 |
|---|---|
| 유스케이스 | policyService.listPolicies·getPolicy 추가 |
| 라우터 | 신규 server/routers/policy.ts(5 프로시저) · server/routers.ts 등록 |
| 데모 | shared/demo/accounts.ts — 정책 관리자·정책 승인자 계정 추가 |
검증
pnpm check·pnpm verify:full통과.- 신규
tests/integration/policyRouter.test.ts(2): 초안은 정책 관리자만 만들고 템플릿에서 출발해도 효력이 없다(활성 스냅샷은 그대로) · 활성화는 정책 승인자만 하고(ADMIN·작성자 거부) 이전 활성 정책을SUPERSEDED로 밀어낸다(§15.5).
남은 것 (§16.3)
- 케이스 뷰(
caseService) → 그 뒤는 화면(§17).
D-062. 폐기 후보를 API 로 이었다 — 목표 수치는 검토자가 확인한다 (§16.3 열한 번째 조각)
- 상태: 채택 (설계서 §13.1 API 노출 · AT-66)
- 배경: 후보 판정 도메인(
retirement/candidacy)과 단위 테스트는 있었지만 라우터에 닿지 않았다. §16.3GET /retirement-candidates("근거 있는 폐기 검토 후보")이다.
결정
- 판정 함수를 노출한다:
retirementCandidates.list— 저장된 KPI 관측(최신MEASURED)·개선조치 이력으로 Agent 별로assessRetirementCandidacy를 돌린다. 목록(candidates)은CANDIDATE만이고, 근거가 모자란 것은candidacies에 판정 사유와 함께 남는다 — "왜 후보가 아닌지"가 보여야 한다. - 목표 수치를 서버가 지어내지 않는다:
MetricDefinition.target은 자유 서술("월 100건 이상")이고, 미달 비교에는 수치 목표와 방향이 필요하다(CandidacyObservation이 그것을 요구한다). 그래서 호출자가 확인한 목표(targets)만 쓰고, 넘기지 않은 지표는 미달로 세지 않는다 — 미수집을 미달로 세지 않는 것과 같은 원칙이다. - 연속 미달 기간 계산을 도메인에 넣었다(
consecutiveMonthsBelowTarget): 달 단위로 이어진 미달만 세고, 빠진 달·목표 미확인·미수집은 기간을 끊는다. 이어 세면 "6개월 지속"이 실제보다 길어진다. - 개선 후 관측을 저장된 값에서 판정한다: 개선조치(사건 조치)의 마지막 완료 시각 뒤에 관측이 있어야 "개선 후 미회복"이 성립한다 — 라우터 입력으로 받지 않는다(입력으로 받으면 그 값이 곧 우회로다).
- 권한: 소유 조직의 조회 전용이 아닌 구성원, 또는 검토 역할(
ADMIN·RETIREMENT_APPROVER·AX_REVIEWER). 목록은 남의 Agent 후보 근거를 노출하지 않는다.
영향
| 영역 | 변경 |
|---|---|
| 도메인 | server/domain/retirement/candidacy.ts — consecutiveMonthsBelowTarget 추가 |
| 유스케이스 | retirementService.candidates — 관측·개선조치 수집과 판정 |
| 라우터 | 신규 server/routers/retirementCandidates.ts(1 프로시저) · server/routers.ts 등록 |
검증
pnpm check·pnpm verify:full통과.- 신규
tests/unit/retirement.test.ts(+3) — 달 단위 연속만 세기(빠진 달이 기간을 끊는다), 목표 미확인·미수집은 기간을 잇지 않음, 지표가 여럿이면 가장 긴 연속. - 신규
tests/integration/retirementCandidatesRouter.test.ts(3): 근거가 없으면 후보가 아니고 사유가 남는다 · 미달 관측만으로는 후보가 되지 않는다(목표 확인·연속 기간·개선 이력이 함께 필요) · 확인된 사유와 함께면 후보가 서고 아무것도 폐기되지 않는다(상태 불변, 소유 조직 밖 비노출).
남은 것
- 목표 수치 모델(지표 정의의 목표를 수치·방향으로 구조화하는 일)이 정해지면 검토자 확인 없이 같은 판정을 돌릴 수 있다 — 지금은 확인이 필요하다는 사실을 계약에 남겼다.
D-061. 공식 운영 ID 를 API 로 이었다 — §16.3 열 번째 조각
- 상태: 채택 (설계서 §10.2 API 노출 · AT-55)
- 배경:
identifierService참조 0건이었다. §10.2 운영 등록(발급 → 확인, 외부 발급 연결)이다.
결정
- 네 경로를 노출한다:
operationalId.request(발급 요청) ·confirm(확인) ·connectExternal(외부 발급 연결) ·forAgent(식별자 + 대기 사유). - 자동 확인 경로를 두지 않는다: 발급 시스템이 값을 주어도 확인 전에는
PENDING= 운영 등록 대기다(§7.2). 확인은 사람이 한다. - 외부 발급 연결은 출처를 남긴다: "이미 검증되었다"는 주장을
source: EXTERNAL로 구분해, 나중에 그 주장의 출처를 구분할 수 있게 한다. - 권한: 소유 조직 또는 운영 담당(
OPERATOR)·ADMIN. 운영 등록은 소유 조직의 신청과 운영 조직의 실행이 만나는 지점이다(운영 조직 지정은 Q-04 미확정). - 멱등 규칙(같은 발급 시스템 재시도는 같은 레코드, 다른 시스템은
IDEMPOTENCY_CONFLICT, 운영 ID 재사용 금지)은 도메인이 판정한다 — 라우터는 다시 쓰지 않는다.
영향
| 영역 | 변경 |
|---|---|
| 라우터 | 신규 server/routers/operationalId.ts(4 프로시저) · server/routers.ts 등록 |
검증
pnpm check·pnpm verify:full통과.- 신규
tests/integration/operationalIdRouter.test.ts(3): 확인 전에는 대기 사유가 있고 재시도가 ID 를 중복 발급하지 않는다 · 확인하면 대기 사유가 사라지고 다른 발급 시스템의 ID 는 붙지 않는다 · 소유 조직 밖에서는 노출되지 않고 외부 발급 연결은EXTERNAL을 남긴다.
남은 것 (§16.3)
- 폐기 후보 → 정책 초안/활성화 → 케이스 뷰. 그 뒤는 화면(§17).
D-060. 운영 계획·최소 권한을 API 로 이었다 — §16.3 아홉 번째 조각
- 상태: 채택 (설계서 §10.1 API 노출 · 제출 요건)
- 배경:
operationService참조 0건이었다. §10.1 운영 승인 제출 요건 중 게이트가 아닌 항목(최소 권한 설계·운영 계획)이다 — 게이트처럼 하나씩 막지 않고 항목별로 무엇이 빠졌는지를 드러내야 한다.
결정
- 네 경로를 노출한다:
operation.upsertPlan(작성·갱신) ·registerPermission(최소 권한 설계 등록) ·readiness(준비 상태 — 누락 항목·최소 권한 문제를 한 번에) ·confirmScope(승인 범위 확정). - 작성 시점에 누락을 막지 않는다: 계획은 단계적으로 채워지고, 채우지 않은 것을 남겨야 "무엇이 빠졌는지"가 보인다. 차단은 준비 상태 검사가 한다.
- 최소 권한이 아닌 설계도 기록한 뒤 드러낸다: 전체 허용·만료 권한을 등록하지 못하게 막으면 실제로 부여된 권한이 어디에도 남지 않는다(§10.3 의 "수동 확인도 기록한다"와 같은 원칙). 준비 상태가
WILDCARD_ACTION·EXPIRED로 차단한다. - 권한을 갈랐다: 기록은 운영 담당(
OPERATOR)(§6.1 "OPERATOR — 운영 계획·적용·점검·사건·폐기 실행 확인"), 승인 범위 확정은 운영 결재(OPERATION_APPROVER) 다.ADMIN은 운영 결재를 자동 취득하지 않는다(§6.1) — 계획을 쓴 사람이 자기 계획의 범위를 확정하면 결재가 아니다. - 데모 계정을 채웠다:
demo-user-operation-approver(OPERATION_APPROVER) — 운영 결재를 데모에서 재현할 수 있어야 한다(§20.3).
영향
| 영역 | 변경 |
|---|---|
| 라우터 | 신규 server/routers/operation.ts(4 프로시저) · server/routers.ts 등록 |
| 데모 | shared/demo/accounts.ts — 운영 결재자 계정 추가 |
검증
pnpm check·pnpm verify:full통과.- 85 파일 / 612 테스트 통과(직전 596 + 16 — D-060~D-064 합계, PostgreSQL 34건은
TEST_DATABASE_URL없으면 skip). - 신규
tests/integration/operationPlanRouter.test.ts(3): 조직만 적은 계획은 누락 항목이 항목별로 드러나고 운영 담당이 아니면 기록 불가 · 전체 허용·만료 권한은 기록되되 요건 미충족으로 드러남 · 승인 범위는 계획이 준비된 뒤 운영 결재만 확정한다(계획을 쓴 사람은 거부).
남은 것 (§16.3)
- 공식 ID(
identifierService) → 폐기 후보·정책 초안/활성화·케이스 뷰. 그 뒤는 화면(§17).
D-059. 사전심의를 API 로 이었다 — §16.3 여덟 번째 조각
- 상태: 채택 (설계서 §16.3 API 노출)
- 배경:
preReviewService참조 0건이었다. §8 사전심의(기획 스냅샷 고정 → 승인·보완·반려)이다.
결정
- 신청·결정을 노출한다:
preReview.submit(기획 스냅샷: 목적·유형·대상 범위) ·preReview.decide(승인·보완·반려 + 중복 후보·재사용 검토·외부 심의 대체). - 미적용 유형은 사유와 함께 거부된다: 미분류·Personal 은 사전심의 대상이 아니고, 서버는 그 사실을 이유로 알려 준다 — 조용히 통과시키지 않는다(테스트가 이 동작을 확인한다).
- 자기 승인 금지·검토 역할: 결정은
ADMIN·PRE_REVIEWER·AX_REVIEWER만. 제출자·소유자의 자기 결정은 도메인이 막는다(§7.4). - 외부 심의 대체를 그대로 연다(
external): 요건이 빠지면 무엇이 빠졌는지 이름으로 알려 주며 거부한다(AT-47 · Q-05 미확정 기본값은 미허용). - 데모 계정을 채웠다:
demo-user-pre-reviewer(PRE_REVIEWER) — 사전심의 결정을 데모에서 재현할 수 있어야 한다(§20.3).
영향
| 영역 | 변경 |
|---|---|
| 라우터 | 신규 server/routers/preReview.ts(2 프로시저) · server/routers.ts 등록 |
| 데모 | shared/demo/accounts.ts — 사전심의 검토자 계정 추가 |
검증
pnpm check·pnpm verify:full통과.- 81 파일 / 596 테스트 통과(직전 594 + 2).
- 신규
tests/integration/preReviewRouter.test.ts(2): 미적용 유형은 사유와 함께 거부되고 다른 조직은 신청 불가 · 반려에 근거 필요·소유자 자기 결정 차단·검토 역할 결정이 상태(CHANGES_REQUESTED)로 남는다.
남은 것 (§16.3)
- 운영 계획(
operationService) → 공식 ID(identifierService) → 폐기 후보·정책 초안/활성화·케이스 뷰. 그 뒤는 화면(§17).
D-058. 평가 3종을 API 로 이었다 — §16.3 일곱 번째 조각
- 상태: 채택 (설계서 §16.3 API 노출)
- 배경:
assessmentService참조 0건이었다. §16.3 의 평가 3종(Agent 여부·Risk·데이터·환경)이다.
결정
- 평가를 계산해 스냅샷으로 남긴다:
assessment.qualification(§4.1) ·assessment.risk(§5.2) ·assessment.environment(§5.3, 적용 범위 입력) ·assessment.latestFor(세 평가의 최신). - 계산 불가를 지어내지 않는다: 최고 실행 수준·자율성이 확정되지 않은 초안에서 Risk 는
null수준으로 남는다 —LOW자동 설정 금지(AT-43). 테스트가 이 값을 직접 확인한다. - 확정은 사람이 한다: 평가는 후보·근거 스냅샷이고, 확정 유형은 검토 결정 경로에서만 입력한다(§16.3).
- 권한은 소유 조직 또는 검토 역할(AT-52 — 개발·운영 조직이 다르면 양쪽이 본다).
영향
| 영역 | 변경 |
|---|---|
| 라우터 | 신규 server/routers/assessment.ts(4 프로시저) · server/routers.ts 등록 |
검증
pnpm check·pnpm verify:full통과.- 80 파일 / 594 테스트 통과(직전 592 + 2).
- 신규
tests/integration/assessmentRouter.test.ts(2): 세 평가가 각각 스냅샷으로 서고(검토자·nullRisk 수준 확인) 현황 조회가 세 평가 키를 함께 준다 · 다른 조직은 세 평가 모두 거부되고 소유 조직은 정상적으로 세운다.
남은 것 (§16.3)
- 사전심의(
preReviewService) → 운영 계획(operationService) → 공식 ID(identifierService) → 폐기 후보·정책 초안/활성화·케이스 뷰. 그 뒤는 화면(§17).
D-057. 변경·승인 재사용을 API 로 이었다 — §16.3 여섯 번째 조각
- 상태: 채택 (설계서 §16.3 API 노출)
- 배경:
changeService참조 0건이었다. §12 는 변경 영향 평가와 승인 재사용(범위 동일·정책 호환·유효성·검토 확인)이다.
결정
- 평가·재사용·비교를 노출한다:
change.assess(영향·재검증·재승인 범위) ·change.requestReuse·change.approveReuse·change.snapshot(버전 비교 재료) ·change.reusesFor(재사용 이력). - 네 확인을 값으로 남긴다: 범위 해시는 서버가 비교해
sameScopeHash를 만들고, 정책 호환·유효성·검토 확인은 요청 값으로 남는다. 하나라도 거짓이면 재사용이 아니고, 승인은 도메인이 막는다(AT-63). - 권한을 갈랐다: 변경 평가는 소유 조직 또는 검토 역할(AT-62 — 개발·운영 조직이 다르면 양쪽이 본다), 재사용 승인은 검토 결정이라 ADMIN·AX_REVIEWER 만.
영향
| 영역 | 변경 |
|---|---|
| 라우터 | 신규 server/routers/change.ts(5 프로시저) · server/routers.ts 등록 |
검증
pnpm check·pnpm verify:full통과.- 79 파일 / 592 테스트 통과(직전 589 + 3).
- 신규
tests/integration/changeRouter.test.ts(3): 스냅샷이 비교 재료를 주고 다른 조직은 변경 평가 불가 · 네 확인이 모두 참일 때만 승인(검토 역할 아니면 승인 불가, 승인 뒤 이력 1건) · 범위가 달라졌으면sameScopeHash거짓이라 승인되지 않는다.
남은 것 (§16.3)
- 평가 3종(
assessmentService) → 사전심의(preReviewService) → 운영 계획·공식 ID → 폐기 후보·정책 초안/활성화·케이스 뷰. 그 뒤는 화면(§17): UI-04 등록 · UI-07 매핑/승인/폐기 검토 · UI-08 변경 영향 + 신규 흐름 화면.
D-056. 위험 사건을 API 로 이었다 — §16.3 다섯 번째 조각
- 상태: 채택 (설계서 §16.3 API 노출 · 5/4 조각 이후 추가)
- 배경:
incidentService참조 0건이었다. 사건 대응은 §11.4 의 즉시 대응·중지 확인·확산 차단·개선·종료 흐름이다.
결정
- 즉시 대응을 그대로 노출한다:
reportEvent(중대는 즉시 조치와 함께 등록해야 선다 — AT-61) ·triage·recordRuntimeConfirmation·contain·startRemediation·addAction·completeAction·resolve·close·byAgent. - 요청과 확인을 나눈다: 중지 요청(
stopRequested)만으로는 확산 차단을 말할 수 없다 —runtimeConfirmation이CONFIRMED여야contain이 서고, 종료(close)에는 재개 결정의 근거(결정·결정자·시각·사유)가 함께 남는다. 판정은 도메인에 둔다. - 권한은 운영 담당(관리자·운영자·소유 조직): 사건은 조직 경계를 넘지 않는다.
영향
| 영역 | 변경 |
|---|---|
| 라우터 | 신규 server/routers/incident.ts(10 프로시저) · server/routers.ts 등록 |
검증
pnpm check·pnpm verify:full통과.- 78 파일 / 589 테스트 통과(직전 586 + 3).
- 신규
tests/integration/incidentRouter.test.ts(3): 조치 없는 중대 사건은 서지 않는다(막힌 뒤 사건 0건) · 중지 확인 없이는 CONTAINED 로 기록되지 않고, 확인 뒤에는 차단→개선→해소→종료가 이어지며 재개 결정이 남는다 · 운영 담당이 아니면 접수 불가(사건 0건).
남은 것 (§16.3)
- 변경(
changeService) → 평가 3종(assessmentService) → 사전심의(preReviewService) → 운영 계획·공식 ID → 폐기 후보·정책 초안/활성화·케이스 뷰. 그 뒤는 화면(§17): UI-04 등록 · UI-07 매핑/승인/폐기 검토 · UI-08 변경 영향 + 신규 흐름 화면.
D-055. KPI 를 API 로 이었다 — §16.3 네 번째 조각
- 상태: 채택 (설계서 §16.3 API 노출 · 4/4 조각)
- 배경:
metricService참조 0건이었다. KPI 는 정의(관리자)·관측(대상 조직)·판정(미수집·계산 불가·0구분)이다.
결정
- 지표 정의는 전역이라 관리자만(
adminProcedure): 정의는 정책에 가깝다. 같은key+definitionVersion은 재사용한다(복제 금지). - 관측은 소유 조직(또는 운영 담당): 대상 조직이 값이 아니라 관측(분자·분모·표본·출처·기간) 을 남긴다(§11.1).
numerator: null은 "수집되지 않았다"는 기록이다: 미수집과 계산 불가를0으로 뭉개지 않으려면 수집 실패도 남아야 한다(AT-60). 판정은 도메인에 둔다.- 실측·기대 효과를 조회에서도 나눈다: 섞으면 달성 여부를 판단할 수 없다(
kind로 구분).
영향
| 영역 | 변경 |
|---|---|
| 라우터 | 신규 server/routers/metric.ts(4 프로시저) · server/routers.ts 등록 |
검증
pnpm check·pnpm verify:full통과.- 신규
tests/integration/metricRouter.test.ts(3): 미수집(null)과0의 판정이 갈린다(AT-60) · 기대 효과가 실측 조회에 섞이지 않는다 · 정의는 관리자만 세우고 다른 조직은 남의 Agent 관측을 기록하지 못한다(막힌 뒤 관측 0건).
남은 것 (§16.3)
- 다음 조각: 사건·변경(
incidentService·changeService) → 평가 3종·사전심의·운영 계획·공식 ID·폐기 후보·정책 초안/활성화·케이스 뷰. 그 뒤는 화면(§17).
D-054. 정기 점검을 API 로 이었다 — §16.3 세 번째 조각
- 상태: 채택 (설계서 §16.3 API 노출 · 3/4 조각)
- 배경: §20.3 의 흐름 중 마지막 끊긴 구간인 "점검"이 API 로 닿지 않았다(
periodicReviewService참조 0건).
결정
- 일정 산정·재계산·수행 결과를 노출한다:
periodicReview.plan·recalculate·complete·byAgent. - 기한은 앞당기는 방향만: 재계산은
EARLIER·UNCHANGED만 남긴다 — Risk 상승 뒤 더 느슨한 다음 기한으로 연기하지 않는다(AT-75). 판정은 도메인에 둔다. - 근거 없는 완료를 막는다:
findings는 입력 단계에서 필수이고,NEEDS_ACTION은 조치·담당자·기한이 함께 있어야 한다(도메인 판정 · §11.2). - 재계산은 사유와 함께: 왜 기한이 움직였는지 남지 않으면 이력이 쓸모없다.
- 권한은 운영 담당(관리자·운영자·소유 조직) — 점검 책임 조직·주기의 사내 확정 전이라(Q-10) 이 기준을 남긴다.
영향
| 영역 | 변경 |
|---|---|
| 라우터 | 신규 server/routers/periodicReview.ts(4 프로시저) · server/routers.ts 등록 |
검증
pnpm check·pnpm verify:full통과.- 신규
tests/integration/periodicReviewRouter.test.ts(3): 계획을 두 번 세워도 일정이 늘지 않고 이력으로 다시 읽힘 · 근거 없는 완료와 사유 없는 재계산 차단 · 운영 담당이 아니면 계획 불가(막힌 뒤 일정 없음).
남은 것 (§16.3)
- 다음 조각: KPI(
metricService— 상수 확인 후) → 사건·변경(incidentService·changeService) → 평가 3종·사전심의·운영 계획·공식 ID·폐기 후보·정책 초안/활성화·케이스 뷰.
D-053. 운영 적용·확인을 API 로 이었다 — §16.3 두 번째 조각
- 상태: 채택 (설계서 §16.3 API 노출 · 2/4 조각)
- 배경: §20.3 의 흐름 중 "운영 적용 확인" 구간이 API 로 닿지 않았다.
deploymentService의 9개 기능이 라우터에서 참조 0건이었다.
결정
- 적용 흐름 전체를 노출한다:
requestDeployment(적용 요청) ·recordResult(결과) ·observeRuntime(런타임 관측) ·confirmApplication(최초 운영 확인) ·requestChangeApplication·recordChangeResult·rollback·overviewFor·operationStatusFor. - 판정은 도메인에 둔다: 적용 완료에 적용 digest, 롤백에 근거, 최초 운영 확인에 승인 범위가 없으면 막힌다. 화면 체크만으로 게이트가 열리지 않는다(AT-54 · AT-56 · AT-65).
- 런타임 상태를 적용 상태와 분리해 기록한다: 수동 확인을 실제 API 실행으로 오인하지 않게
observeRuntime을 분리했다(AT-57). - 권한은 운영 담당이다 — 관리자·운영자(OPERATOR) 또는 소유 조직. 운영 조직·담당 지정은 사내 확정 전이라(Q-04) 이 기준을 여기에 남긴다.
- 데모 계정을 채웠다:
demo-user-operator(OPERATOR) — 운영 적용·확인을 데모에서 재현할 수 있어야 한다(§20.3 합성 데이터 요건).
영향
| 영역 | 변경 |
|---|---|
| 라우터 | 신규 server/routers/deployment.ts(9 프로시저) · server/routers.ts 등록 |
| 데모 | shared/demo/accounts.ts — 운영 담당 계정 추가 |
검증
pnpm check·pnpm verify:full통과.- 75 파일 / 580 테스트 통과(직전 576 + 4).
- 신규
tests/integration/deploymentRouter.test.ts(4): ① 같은 환경·조직 재요청은 배포를 늘리지 않고(재사용) 완료에 digest 가 없으면 422 · 런타임/적용 상태 분리 기록 ② 승인 범위 없이는 최초 운영 확인이 닫히지 않는다(확인 7항목을 다 채워도) ③ 변경 적용 → 결과(confirm 자동 생성) → 이전 승인 무효면 롤백 차단, 유효하면ROLLED_BACK과 되돌린 버전 기록 ④ 운영 담당이 아니면 적용 요청 불가(막힌 뒤 배포 없음).
남은 것 (§16.3)
- 다음 조각: KPI·점검(
metricService·periodicReviewService) → 사건·변경(incidentService·changeService) → 평가 3종·사전심의·운영 계획·공식 ID·폐기 후보·정책 초안/활성화·케이스 뷰.
D-052. 검증·보안을 API 로 이었다 — §16.3 이행의 첫 조각
- 상태: 채택 (설계서 §16.3 API 노출 · 1/4 조각)
- 배경: 설계서 §16.3 의 신규 API 30종 중 실제로 노출된 것은 등록·매핑·검토·승인·게시·중지·폐기(초안)·폐기 검토뿐이었다. 11개 서비스가 라우터에서 참조 0건이었다(
docs/implementation-status.md§16.3 대조표).
결정
- 검증부터 연다:
validation.createPlan·recordRun·latestFor— §20.3 이 요구하는 흐름(기획→심의→검증→결재→운영 적용 확인→점검)의 첫 끊긴 구간이다. - 보안을 함께 연다:
security.registerEvidence·recordReview·latestFor— 검증과 같은 구간(릴리스 품질)이고 둘 다 게이트를 닫는다. - 판정은 도메인에 둔다: 라우터는 인증·권한·입력 검증만 한다. 총평
PASS로도 필수 항목FAIL이 덮이지 않고(AT-48), 다른 digest 증빙은staleEvidence로 드러난다(AT-49) — 라우터가 규칙을 다시 쓰지 않는다. - 권한을 갈랐다: 검증은 소유 조직 또는 관리자(검증 인력·조직 지정은 Q-06 미확정), 보안 검토는 검토 역할(ADMIN·SECURITY_REVIEWER). 사내 역할 claim 은 미확정이라 기준을 여기에 남긴다.
- 데모 계정을 채웠다:
demo-user-security-reviewer(SECURITY_REVIEWER)를 추가했다 — 보안 검토를 데모에서 재현할 수 있어야 한다(§20.3 합성 데이터 요건).
영향
| 영역 | 변경 |
|---|---|
| 라우터 | 신규 server/routers/validation.ts(3) · server/routers/security.ts(3) · server/routers.ts 등록 |
| 데모 | shared/demo/accounts.ts — 보안 검토자 계정 추가 |
검증
pnpm check·pnpm verify:full통과.- 74 파일 / 576 테스트 통과(직전 572 + 4).
- 신규
tests/integration/releaseQualityRouter.test.ts(4): ① 검증 계획→실행→필수 항목 FAIL 이 총평 PASS 로 덮이지 않고 게이트가FAILED, 통과시키면PASSED② 다른 조직은 검증 기록 불가(막힌 뒤 계획 없음) ③ 증빙 등록→위협 3군 검토→게이트 일치·다른 digest 증빙은staleEvidence④ 검토 역할이 아니면 보안 검토 불가(기록·게이트 변화 없음).
남은 것 (§16.3)
- 다음 조각: 운영 적용·확인(
deploymentService) → KPI·점검(metricService·periodicReviewService) → 사건·변경(incidentService·changeService) → 평가 3종·사전심의·운영 계획·공식 ID·폐기 후보·정책 초안/활성화.
D-051. 폐기 검토를 API 로 이었다 — 만든 것이 저장소에만 있으면 쓰이지 않는다
- 상태: 채택 (v0.2 단계 10 보강 · B 작업)
- 배경: 폐기 검토는 서비스(
retirementService)·도메인·저장소·테스트에 다 있었는데 라우터에 노출된 것은 초안 폐기(retireDraftAgent) 하나였다. 있는데 닿지 않는 기능은 없는 것과 같다.
결정
- 흐름 전체를 노출한다:
retirement.request·refresh·decide·updateTask·complete·byAgent— 요청 → 실질 변화 확인 → 승인 → 정리 확인 → 완료. - 폐기 케이스는 요청 시 함께 연다(
releaseId: null): 폐기는 제출 버전이 아니라 운영 중인 대상을 다룬다. 게이트 계획의 근거가 케이스에 남아야 한다. - 배포 진행 여부를 호출자가 주장하지 않는다:
complete는 클라이언트 입력이 아니라 저장소에서 판정한다(listByAgent의REQUESTED·APPLYING). 클라이언트가 넘긴 값으로 판정하면 그 값이 곧 우회로가 된다(AT-71). - 권한은 소유 조직·검토 역할로 나눈다: 요청·정리 확인은 소유 조직 또는 관리자, 승인·완료는 검토 역할(ADMIN·AX_REVIEWER — 검토 대기함과 같은 기준). 운영 조직 협의·검토자 지정의 사내 규칙은 아직 확정되지 않아(Q-11) 이 기준으로 두고 여기에 남긴다.
- 같은 대상에 두 요청을 두지 않는다:
IN_REVIEW·APPROVED요청이 있으면INVALID_STATE_TRANSITION으로 막고 진행 중 요청 id 를 알려 준다. - 오류 매핑을 한 곳에 모았다(
server/routers/support.ts):run·actorOf가 라우터마다 흩어져 있었다. D-050 에서 412→500 문제가 났던 지점이라, 라우터가 늘 때 같은 규칙을 다시 쓰지 않게 했다.
영향
| 영역 | 변경 |
|---|---|
| 라우터 | 신규 server/routers/retirement.ts(6 프로시저) · server/routers.ts 등록 · 신규 server/routers/support.ts(run·actorOf 공통) · workflow.ts 는 공통 헬퍼를 쓴다 |
검증
pnpm check·pnpm verify:full통과.- 73 파일 / 572 테스트 통과(직전 568 + 4).
- 신규
tests/integration/retirementRouter.test.ts(4): ① 요청→승인→정리 확인→완료 완결(폐기 케이스가releaseId: null로 서고RETIRING→RETIRED전환, 이력 유지) ② 소유 조직이 아니면 시작 불가(막힌 뒤 요청·케이스 없음) ③ 검토 역할이 아니면 승인·완료 불가 ④ 확인자 없는DONE은 완료를 막고, 배포가APPLYING이면 서버 판정으로 막는다(AT-68·AT-71).
남은 것
- 폐기 검토 화면(UI) 이 아직 없다 — API 는 준비되었고 화면이 붙어야 사람이 쓴다.
- 새 버전 작성(
createReleaseVersion) 미구현(알려진 제한 5).
D-050. AT-13 을 닫았다 — 리뷰 요청에도 낙관적 잠금을 걸고 충돌은 412 로 옮긴다
- 상태: 채택 (v0.2 단계 10 보강 · D-049 의 미충족 해소)
- 배경: D-049 가 실측으로 남긴 결함(낡은 화면의 저장이 이긴다)을 고쳤다.
결정
- 리뷰 요청에도
rowVersion을 둔다: 계약(ReviewRequest)에 버전을 넣어 저장소가 비교할 대상 자체를 갖게 했다. 없던 동안에는 비교할 것이 없어 잠금이 성립하지 않았다(D-049 의 근본 원인). - 충돌은
ApiError로 던진다:RevisionConflictError가ApiError(REVISION_CONFLICT· 412)를 상속한다. 라우터는error.status로 tRPC 코드를 정하므로, 상속하지 않으면 412 가 아니라 500 이 된다 — 그동안 이 저장소의 모든 낙관적 잠금 충돌이 500 이었다(D-049 ④). - 두 저장소 모두 조건부로 쓴다: memory 는 현재 버전 비교 후 증가, PG 는
where row_version = expected+ 0행이면 충돌. 한쪽만 고치면 개발(인메모리)과 운영(PG)이 다르게 동작한다. - 호출부는 읽은 버전을 그대로 넘긴다: 요청을 읽은 사람이 그 버전으로 쓴다 — 쓰기 직전에 다시 읽어 버전을 맞추면 잠금의 의미가 사라진다.
영향
| 영역 | 변경 |
|---|---|
| 계약 | shared/mapping.ts — ReviewRequest.rowVersion(§7.1 · AT-13) |
| 도메인 | domain/ports.ts — RevisionConflictError extends ApiError, ReviewStore.updateRequest(request, expectedVersion) |
| 저장소 | memory(버전 비교 후 증가) · PG(조건부 UPDATE + requestFromRow 에 rowVersion 매핑) |
| 호출부 | agentService 3곳(제출 중지·초안 복귀·결정) · preReviewService 1곳 |
검증
pnpm check·pnpm verify:full통과.- 72 파일 / 568 테스트 통과(직전 566 + 2).
- 신규 단언:
memoryStores— 낡은 저장은RevisionConflictError이고code=REVISION_CONFLICT·status=412(라우터 매핑의 근거)이며 저장된 내용은 먼저 커밋한 것.postgresStores— 조건부 UPDATE 가 0행이라 412 이고 덮어쓰지 않는다.
남은 것
- AT-12 연계 재시도 멱등 전용 테스트, 6포트 실제 어댑터·대표 E2E.
D-049. AT-13 은 통과시키지 않는다 — 실측이 미충족을 보였기 때문이다
- 상태: 채택 (v0.2 단계 10 · 실측 기준)
- 배경: §20 AT-13 은 "동시 수정에서 낡은 화면의 저장이 이긴다"를 412 로 막는 기준이다. 테스트를 쓰면서 실제로 재현해 보니 미충족이었다.
실측 결과
| 저장소 | 기대 버전 | 실제 동작 |
|---|---|---|
reviews.updateRequest (memory) | 인자로 받음 | 무시 — 낡은 내용이 덮어쓴다 |
reviews.updateRequest (PG) | 인자로 받음 | where id 만 — 낡은 내용이 덮어쓴다 |
retirements.updateRequest (memory·PG) | 인자로 받음 | 조건부 UPDATE 걸림 — 다만 평범한 Error 라 412 로 이어지지 않는다 |
결정
- 테스트를 실제 동작에 맞추지 않는다: 통과를 위해 기대를 낮추면 기준이 사라진다. 실패하는 상태를 그대로 남기고 결함으로 기록한다.
- 고치는 방법까지 적는다: 두 저장소의 리뷰 갱신에
where row_version = expected를 세우고, 충돌은 412 로 매핑되는 도메인 오류로 던진다(폐기 저장소의Error도 같은 오류로 바꾼다). - AT-33 은 닫았다: 감사 이력 조회 권한과 읽기 전용 성질을 테스트로 못박았다(일반 사용자 403 · 감사 조회자 조회 가능 · 상태 변경 차단).
영향
| 영역 | 변경 |
|---|---|
| 테스트 | tests/integration/workflowRouter.test.ts +1 (AT-33) |
| 문서 | docs/implementation-status.md §20 AT 대조표 — AT-13 을 미충족으로, AT-33 을 커버로 |
검증
pnpm check· 72 파일 / 566 테스트 통과.
남은 것
- AT-13 구현: 리뷰·폐기 갱신의 낙관적 잠금과 412 매핑(다음 작업).
- 근본 원인:
ReviewRequest도메인 타입에rowVersion이 없다(PG 테이블review_requests에는row_version열이 있다 — 타입·매핑이 따라오지 않았다). 그래서 저장소가 기대 버전을 비교할 대상 자체가 없다. 순서: ①ReviewRequest에rowVersion추가(계약·PG 매퍼requestValues/requestFromRow) → ② 두 저장소의 갱신에 조건부 UPDATE +RevisionConflictError→ ③ 호출부(preReviewService·agentService4곳)가 읽은 버전을 넘김 → ④ 폐기 저장소의 평범한Error도RevisionConflictError로 바꾸고 라우터 매핑 확인. RevisionConflictError(§10.5)는 이미 있으나 라우터가 412 로 옮기는지 확인되지 않았다 — ③·④ 와 함께 검증한다.
- 근본 원인:
- AT-12 의 연계 재시도 멱등 전용 테스트, 6포트 실제 어댑터·대표 E2E.
D-048. 남은 PG 왕복 검증을 닫았다 — 폐기·변경 적용·승인 재사용
- 상태: 채택 (v0.2 단계 10 · 실측 기준 보강)
- 배경: 폐기 요청·정리 항목(
retirement_plan), 변경 적용(deployment_changes), 승인 재사용(approval_reuses)은 설계서 §13.2·§12.3·§12.2 의 핵심 기록인데, PG 저장소 왕복 테스트가 없어 매핑 누락·필드 유실을 잡을 그물이 없었다(D-044·D-046 기록).
결정
- 세 기록 모두 실제 PG 왕복으로 확인한다: 저장 → 조회에서 해시·봉인·확인자·근거·네 확인이 무손실이어야 한다.
- 무효화와 롤백을 갱신 경로로 확인한다:
INVALIDATED(사유·관측 해시)와ROLLED_BACK(되돌린 버전)을rowVersion증가와 함께 검증한다 — 낙관적 동시성까지 왕복에 포함한다. - 승인 재사용은 종전 결정 참조로 확인한다:
priorDecisionId가 실제 결정 행을 가리키고, 네 확인(sameScopeHash·policyCompatible·stillValid·priorApprovalVerified)이 그대로 살아야 한다.
영향
| 영역 | 변경 |
|---|---|
| 테스트 | tests/integration/postgresStores.test.ts +3 (폐기 요청·정리 항목 / 변경 적용·롤백 / 승인 재사용), truncate 목록에 approval_reuses·change_assessments·deployment_changes 추가 |
검증
pnpm check·pnpm verify:full통과.- 72 파일 / 565 테스트 통과(직전 562 + 3).
남은 것
- AT-13(ETag 412)·AT-33(감사 조회자 권한)·AT-12 의 연계 재시도 멱등 전용 테스트는 여전히 남아 있다.
D-047. 자동화 경계는 목록으로 관리한다 — 자동 승인·매핑·폐기·삭제는 목록에 있다
- 상태: 채택 (v0.2 단계 10 · 자동화 경계 · 단계 10 완료)
- 설계서 원안: §20 인수 기준이 곳곳에서 못박은 "자동으로 하지 않는 것"(AT-16 자동 삭제·재매핑 금지, AT-27 자동 폐기 금지, AT-29 미수집을 0 으로 생성 금지, AT-31 자동 승인·매핑 없음, AT-32 운영에서 Mock 시작 차단, AT-60 미수집·계산 불가를 0 으로 뭉개지 않음, §9.1 절대 차단 조건 면제 금지, §21.2-5 임의 확인 이력 생성 금지), "자동화 확대 — 별도 품질·실측 기준".
결정
- 금지 목록을 한 곳(
shared/automation.ts)에 모았다: 자동화 범위를 넓히려면 이 목록을 고쳐야 하고, 그 변경이 리뷰에서 드러난다 — 코드가 조용히 범위를 넓히지 않는다. - 결정 자체가 금지인 것과 종류가 갈리는 것을 나눴다: 승인·매핑·폐기·삭제·면제는 어떤 작업이든 자동이 아니다. 상태 변경만 종류로 갈린다(공유 자원은 삭제 금지·연결 해제 허용).
- 설명 없는 상태 변경은 허용으로 추정하지 않는다: "무엇을 하는지"를 적어야 판정한다 — 없는 것을 허용으로 두면 경계가 무너진다.
- 금지에는 근거(설계서 조항)가 붙는다: 근거 없는 금지는 지켜지지 않고, 근거 없는 허용은 리뷰되지 않는다. 목록 11건 모두 참조를 갖는다.
- 자동화 경로가 이 판정을 지난다:
assertAutomationAllowed가 금지 결정을FORBIDDEN으로 막는다(계산·대조·표시·알림만 통과).
영향
| 영역 | 변경 |
|---|---|
| 계약 | 신규 shared/automation.ts — 결정 10종·금지 목록 11건(근거 포함) |
| 도메인 | 신규 domain/automation/boundary.ts — evaluateAutomation·assertAutomationAllowed·neverAutomatedRules |
검증
pnpm check·pnpm verify:full통과.- 신규
tests/unit/automationBoundary.test.ts(5) — 계산·대조·표시·알림 허용, 자동 승인·매핑·폐기·삭제·면제 거부(근거 포함), 상태 변경의 종류 구분(공유 자원 삭제 차단·연결 해제 허용), 설명 없는 변경은 추정 금지,FORBIDDEN차단, 목록 단일 출처.
남은 것 (정직한 기록)
- 금지 목록은 설계서 문장에서 온 것이다 — 사내 확정 정책으로 자동화 범위가 넓어지면(§21.3) 이 목록을 고쳐야 한다.
- 6포트 실제 어댑터 구현·대표 E2E·일부 PG 왕복 테스트는 여전히 남아 있다(D-040·D-042·D-044·D-046 기록).
D-046. 연계 작업은 미확인 조치를 남긴다 — 멱등 키가 재실행을 안전하게 만든다
- 상태: 채택 (v0.2 단계 9 · 9b: 작업 저장소 · 단계 9 완료)
- 설계서 원안: §14.2("작업 전체 성공 후에만 커서를 확정한다. 재실행은 멱등 처리한다"), §19.3, §15(
integration_job/outbox "외부 요청·재시도·멱등 키·응답 상태·미확인 조치 추적").
결정
- 멱등 키를 DB 고유 제약으로 뒀다(
integration_jobs.idempotency_key): 재실행이 중복 요청을 만들지 않는다 — 재시도가 두 번 적용되는 사고를 저장소 수준에서 막는다. - 모든 요청은 작업으로 남는다: 포탈은 "실행"이 아니라 요청·관측을 하므로(§2), 요청·응답·시도 횟수·실패 종류가 남아야 "무엇을 못 했는가"를 셀 수 있다.
- 미확인 성공은
UNCONFIRMED로 남긴다: 응답이 성공이어도 확인하지 못했으면 성공으로 세지 않고, 미확인 조치 목록에 남는다(§14.2 · §10.3 "런타임 조치 미확인"과 같은 원칙). - 커서 확정은 같은 포트의 미확인 조치까지 본다: 내 작업이 성공이어도 같은 포트에 미확인 조치가 남아 있으면 커서를 밀지 않는다 — 확인되지 않은 구간을 건너뛰지 않기 위해서다.
- 일시 장애만 재시도 대상으로 남기고(PENDING·retryable), 권한 거부는 FAILED 로 종결한다. 무한 재시도가 권한 문제를 숨기지 않게 한다.
영향
| 영역 | 변경 |
|---|---|
| DB | integration_jobs(멱등 키 고유·시도·실패 종류·재시도 가능·커서 확정) + enum 3종 — 마이그레이션 0026 |
| 포트 | AgentCenterDeps.integrations: IntegrationStore(작업 6) |
| 유스케이스 | 신규 integrationService — requestJob(멱등)·recordResponse·advanceCursor·unconfirmed·statusFor |
| 감사 | INTEGRATION_JOB_REQUESTED·INTEGRATION_JOB_RESPONSE(HTTP·실패 종류·상태)·INTEGRATION_CURSOR_ADVANCED |
검증
pnpm check·pnpm verify:full통과.- 신규
tests/integration/integrationJobs.test.ts(3) — 멱등 키 재실행(작업 1건), 미확인 성공 →UNCONFIRMED+ 미확인 목록, 미확인 조치가 있으면 커서 확정 차단·확인 후 확정·불완전 스냅샷 차단, 403 → FAILED·비재시도 / 503 → PENDING·재시도, 포트 현황 집계.
남은 것
- 단계 10(자동화 품질·실측 기준): §20 별도 품질·실측 기준에 따른 자동 판정 확대와 회귀 기준.
- 관리적 제한(정직하게 기록): 6포트의 실제 어댑터 구현(외부 시스템별)은 여전히 Mock 이며, 대표 E2E·
retirement_plan/deployment_changes/approval_reuses의 PG 왕복 테스트도 남아 있다.
D-045. 연계 어댑터는 추정하지 않는다 — 미확인 조치는 성공이 아니고, 권한 거부는 삭제가 아니다
- 상태: 채택 (v0.2 단계 9 · 9a: 어댑터 계약 검증)
- 설계서 원안: §14.2("완전한 스냅샷이 아니면 누락·삭제를 판정하지 않는다", "작업 전체 성공 후에만 커서를 확정한다. 재실행은 멱등 처리한다", "404·403·일시 장애를 구분하고 권한 거부를 삭제로 처리하지 않는다", "기본 24시간 신선도·5분 권한 캐시는 서로 독립된 정책이다", "권한 또는 원본 재검증 실패는 관련 상태 전이를 503으로 보류한다. 기존 운영을 자동 폐기하지 않는다"), §19.3(미연계를 Mock 으로 자동 대체하지 않는다), §15(
integration_job/outbox).
결정
- 실패를 뭉뚱그리지 않는다: 403/401 은
PERMISSION_DENIED, 404 는NOT_FOUND, 5xx·408·429 는TRANSIENT, 응답 스키마 불일치는VALIDATION_FAILED로 나눈다. 권한 거부를 삭제로 바꾸지 않는다 — 그 순간 원본에서 사라진 것처럼 보이고 후보·영향 판정이 틀어진다. - 커서는 작업 전체 성공 후에만 확정한다: 성공이 아니거나, 미확인 조치가 남았거나, 완전한 스냅샷이 아니면 확정하지 않는다. 부분 성공으로 커서를 밀면 그 구간 변경을 건너뛴다.
- 미확인 조치는 성공으로 추정하지 않는다: 응답이 성공이어도 확인하지 못했으면
UNCONFIRMED다 — §10.3 의 "런타임 조치 미확인"과 같은 원칙을 연계 계층에도 적용했다. - 재시도는 일시 장애만이다: 권한 거부·검증 실패는 재시도해도 같은 결과이고, 무한 재시도가 권한 문제를 숨긴다.
- 재검증 실패는 503 보류다: 권한 실패는
AUTHORIZATION_SOURCE_UNAVAILABLE, 원본 없음은PROCESS_SOURCE_UNAVAILABLE로 상태 전이를 멈춘다. 보류는 멈춤이지 종료가 아니므로 기존 운영을 자동 폐기하지 않는다. - 신선도(24h)와 권한 캐시(5분)를 독립 정책으로 선언했다 — 한쪽이 다른 쪽을 밀어내면 오래된 권한으로 최신 판정을 하게 된다.
영향
| 영역 | 변경 |
|---|---|
| 계약 | 신규 shared/integrationJob.ts — 작업 상태(미확인 포함)·실패 종류·작업 레코드·독립 신선도 상수 |
| 도메인 | 신규 domain/integration/contract.ts — classifyAdapterResponse·assertCursorAdvanceable·assertVerificationNotBlocked·markJobFromResponse |
검증
pnpm check·pnpm verify:full통과.- 신규
tests/unit/adapterContract.test.ts(4) — 404·403·일시 장애 구분(권한 거부 ≠ 삭제), 커서 확정 조건(부분 성공·미확인·불완전 스냅샷 차단), 503 보류(자동 폐기 없음), 미확인 성공 =UNCONFIRMED·재시도는 일시 장애만.
남은 것 (단계 9)
- 9b 작업 저장소·서비스:
integration_job테이블, 요청 생성(멱등 키)·재시도·미확인 조치 추적 조회, 커서 확정 경로 배선. - 10 자동화 품질·실측 기준.
D-044. 전환 표시는 컬럼으로 남기고, 재실행은 행을 건드리지 않는다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 8 · 8b: 전환 실행 · 단계 8 완료)
- 설계서 원안: §21.2("게시만으로 ACTIVE 가 부여되었으면
legacy_compliance_status=UNVERIFIED로 표시하고 실제 운영 적용·승인 증빙을 확보한다", "미확인 Risk 는 null, 검증·보안·운영 계획은 누락 상태로 적재한다", "전환 배치·대조 수치·실패 목록·재실행 결과를 남기고 롤백 가능한 마이그레이션 순서를 사용한다", "시범 조직 검증 후 확대한다"), §15, AT-73 · AT-74.
결정
- 표시는 Agent 의 컬럼이다(
legacy_compliance_status): 상태를 문구나 감사 기록에만 두면 화면·조회가 그것을 조건으로 걸 수 없다. 신규 등록 건은null이고, 지금 이 컬럼이 서는 건은 전환 대상뿐이다. - 멱등은 "행을 건드리지 않음"으로 구현했다: 이미 같은 표시면
agents.update를 호출하지 않고, 감사에는changed: false로 남는다. 재실행이 행 버전·이력을 부풀리지 않는다. UNVERIFIED의 전제를 실제 증빙으로 판정한다: "게시만으로 ACTIVE" 는 ACTIVE 인데 적용 완료 + 확인 기록이 있는 배포가 없다는 뜻이다 — 지금 엔진은 확인 없이 ACTIVE 가 될 수 없으므로(AT-54), 이 판정은 과거 데이터에만 걸린다.- 누락·재심사는 전환 결과에 그대로 담는다(§21.2-6·7): 누락 4종과 재평가(기한·우선순위)를 감사 기록에 남겨 대조할 수 있게 했다. 값은 여전히 만들지 않는다.
- 배치 대조 수치·실패 목록·되돌릴 순서는 감사 기록으로 남긴다(§21.2-8): 별도 배치 테이블은 대조 수치를 조회 API 로 제공할 때 도입한다 — 지금은 감사가 그 역할을 하고, 이 제한을 기록한다.
- AT-74 는 서비스 진입점에서도 종류를 받지 않는다:
assertAccess({ kind, actor })가 자원 종류만 보고 판정하며, 목록·검색·집계·내보내기가 같은 함수를 쓴다.
영향
| 영역 | 변경 |
|---|---|
| 계약 | shared/agent.ts — legacyComplianceStatus?(없으면 전환 대상이 아니었다는 뜻) |
| DB | agents.legacy_compliance_status 컬럼 — 마이그레이션 0025 |
| 유스케이스 | 신규 legacyService — convertAgent(표시 저장·멱등)·recordBatch(대조 수치)·assertAccess(AT-74)·statusFor |
| 감사 | LEGACY_AGENT_CONVERTED(표시·누락·재심사·변경 여부)·LEGACY_BATCH_RECORDED(시범 조직·대조 수치·실패·되돌릴 순서·재실행 횟수) |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 69 파일 / 550 테스트 통과(PostgreSQL 30건 — 마이그레이션0025적용).- 신규
tests/integration/legacyConversion.test.ts(3) — AT-73(UNVERIFIED저장·누락 4종·정책 미확정 시 기한 미생성·재실행 멱등(행 미갱신)), §21.2-8(배치 대조 수치·시범 조직·역순 롤백 순서·재실행 횟수), AT-74(종류 무관 같은 권한 — 권한 없는 역할은 차단). - 기존 PG 왕복 사례 1건 갱신: 새 컬럼이
null로 돌아오는 것을 명시.
남은 것
- 단계 9(6포트 실연계 어댑터 계약 검증) · 10(자동화 품질·실측 기준).
- 관리적 제한(정직하게 기록): 대표 E2E와 배치 조회 API 는 아직 없다 — 배치 수치는 감사 기록으로 확인하며, 조회 API 가 필요해지면
legacy_migration_batch테이블을 도입한다.retirement_plan·deployment_changes·approval_reuses의 PostgreSQL 왕복 테스트도 여전히 없다.
D-043. 기존 데이터 전환은 표시를 맞추는 일이다 — 값을 채우거나 승인을 만들지 않는다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 8 · 8a: 전환 규칙)
- 설계서 원안: §21.2("기존 DOMAIN/TASK/PERSONAL 을 신규 표시 체계에 대응시키되
legacy_policy_version과 원래 판정을 보존한다. 코드 대응은 신규 정책 적합성 승인과 다르다", "기존 Task 의 L6 매핑은 신규 범위에 포함될 수 있지만 자동 재승인하지 않는다", "기존 Domain 의 L5 책임 범위는 재분류 검토 대상으로 표시한다", "기존 Personal 의 L7 매핑은 유지하되 개인 업무 기준으로 재검토한다", "게시만으로 ACTIVE 가 부여되었으면legacy_compliance_status=UNVERIFIED로 표시하고 실제 운영 적용·승인 증빙을 확보한다. 임의로 운영 확인 이력을 생성하지 않는다", "미확인 Risk 는 null, 검증·보안·운영 계획은 누락 상태로 적재한다. 과거 Agent 에 LOW·PASS 를 일괄 입력하지 않는다", "재평가 기한·우선순위·계속 사용 조건은 회사 전환 정책으로 승인받는다. 일괄 자동 중지·폐기는 하지 않는다", "시범 조직 검증 후 확대한다. 전환 배치·대조 수치·실패 목록·재실행 결과를 남기고 롤백 가능한 마이그레이션 순서를 사용한다"), AT-73, AT-74.
결정
- 전환은 표시 대응이지 승인이 아니다(§21.2-1): 매핑 결과에 원래 판정(
preservedJudgement)을 그대로 보존하고, 재검토 필요를 별도 플래그로 표시한다 — 대응이 원래 판정을 덮지 않는다. - L5·L6·L7 매핑은 모두 재검토 대상이다(§21.2-3·4): L5 는 재분류 검토, L6 은 포함될 수 있으나 자동 재승인 금지, L7 은 개인 업무 기준 재검토. 표시 대응만 한 건은 재검토 대상이 아니다.
UNVERIFIED는 "게시만으로 ACTIVE + 적용 증빙 없음"일 때만 선다(§21.2-5): 증빙이 있으면 그 표시를 붙이지 않고, 확인 이력을 만들어 붙이지도 않는다.- 누락은 누락으로 적재한다(§21.2-6): 미확인 Risk·검증·보안·운영 계획을
missingArtifacts로 남긴다. LOW·PASS 를 일괄 입력하지 않는다 — 값을 만들면 나중에 "왜 통과했는가"를 물을 수 없다. - 전환 정책이 미확정이면 기한·우선순위를 만들지 않는다(§21.2-7 · §21.3):
policyConfirmed: false로 남기고 사유에 "일괄 자동 중지·폐기도 하지 않습니다" 를 명시한다 — 확정 전에는 값을 지어내지 않는다. - 배치는 대조 수치·실패 목록·되돌릴 순서를 남긴다(§21.2-8): 되돌리기 순서는 전환의 역순이다.
- AT-74 는 권한 판정이 조회 종류를 받지 않도록 했다: 목록·검색·집계·내보내기가 같은 함수로 판정되므로 종류별 분기로 우회할 수 없다 — 판정 인터페이스 자체가 그 요구를 담는다.
영향
| 영역 | 변경 |
|---|---|
| 계약 | 신규 shared/legacy.ts — 전환 표시·매핑·누락 항목·재평가·배치 기록·보호 자원 조회 종류 |
| 도메인 | 신규 domain/legacy/migration.ts — mappingReassessment·convertLegacyAgent·summarizeLegacyBatch·assertProtectedAccess |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 68 파일 / 547 테스트 통과.- 신규
tests/unit/legacyMigration.test.ts(6) — AT-73(게시만 ACTIVE →UNVERIFIED·증빙 있으면 미표시, 누락 적재(LOW·PASS 미생성), 전환 정책 미확정 시 기한·우선순위 미생성 + 일괄 중지·폐기 금지, L5·L6·L7 재검토 표시·원래 판정 보존, 배치 대조 수치·실패 목록·역순 롤백), AT-74(조회 종류와 무관하게 같은 권한 — 내보내기 우회 차단).
남은 것 (단계 8)
- 8b 전환 실행 경로:
legacy_migration_batch테이블·legacyService(배치 생성 → 시범 조직 검증 → 확대 → 재실행 멱등),agents.legacy_compliance_status컬럼, 제한 노드·증빙 조회 API 에assertProtectedAccess배선, 그리고 대표 E2E. - 9(6포트 실연계) · 10(자동화)이 남았다.
D-042. 폐기 실행 — 승인은 RETIRING 을 만들 뿐이고, 완료는 확인된 정리가 만든다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 7 · 7b: 폐기 실행 경로 · 단계 7 완료)
- 설계서 원안: §13.2("폐기 요청은 현재 운영 버전·배포·의존 관계·Owner·운영 조직·정리 계획의 해시를 고정한다 … 승인 시 RETIRING 으로 전환하고 신규 운영·변경 적용을 차단한다. RETIRED 는 필수 정리 확인 후에만 가능하다", 정리 6영역 표, "각 항목은 PENDING / DONE / NOT_APPLICABLE / BLOCKED, 책임자, 실제 처리 시스템, 증빙, 확인자·시각을 가진다", "폐기는 감사·승인·과거 업무 정의의 물리 삭제를 의미하지 않는다"), §15(
retirement_plan·retirement_task), AT-66 · AT-67 · AT-68 · AT-69 · AT-70 · AT-71.
결정
- 요청 시점의 상태를 봉인한다(§13.2): 운영 버전·배포·의존 관계·조직·이관 계획을 해시로 묶고,
refreshScope가 현재 상태와 비교해 다르면 무효화한다. 무효화된 요청은 승인도 완료도 할 수 없다 — 재검토가 유일한 경로다. - 봉인하는 "운영 버전"은 실제 적용이 끝난 배포의 버전이다(AT-64 와 같은 기준): 게시만 된 버전을 운영으로 봉인하면 검토가 잘못된 대상을 가리킨다.
- 승인은
RETIRING만 만든다: 요청 승인 →RETIRING전환(§7.2 전이표가ACTIVE·SUSPENDED에서만 허용).RETIRED는 정리 확인이 만든다 — 승인과 완료를 한 동작으로 합치지 않았다. - 확인 없는
DONE은 완료로 세지 않는다(AT-68): 갱신 API 는 확인자를 명시적으로 넘길 때만 확인으로 기록한다(자동 확인을 만들지 않는다). "처리했다고 보고됨"과 "확인됨"은 다르다. - 미적용은 사유·검토자, 공유 자원은
DELETED금지(AT-70), 배포 진행 중 조치가 남으면 완료 차단(AT-71) — 규칙은 7a 도메인(evaluateRetirementCompletion)에 있고 서비스가 그것을 그대로 부른다(규칙이 두 곳에 흩어지지 않는다). - 정리 영역 6종 항목을 요청과 함께 세운다: 영역별 기본 항목을 만들고 검토자가 상태·책임자·시스템·증빙·확인자를 채운다. 항목이 없으면 완료 검사가 시작되지 않는다(빈 계획으로 "다 했다"고 말할 수 없다).
- 이력은 삭제하지 않는다(AT-69): 요청·항목·감사가 남고,
listFor가 그대로 돌려준다 — 폐기는 물리 삭제가 아니다. - 폐기 결재는 RETIREMENT 케이스의 별도 게이트를 쓴다(AT-67):
planGatesFor가RETIREMENT_APPROVAL·RETIREMENT_COMPLETION만 요구하고OPERATION_APPROVAL을 요구하지 않는다(운영 승인 경로를 재사용하지 않는다).
영향
| 영역 | 변경 |
|---|---|
| DB | retirement_requests(해시·봉인·무효화)·retirement_tasks(영역·상태·확인자·자원 처분) + enum 5종 — 마이그레이션 0024 |
| 포트 | AgentCenterDeps.retirements: RetirementStore(요청 4 + 항목 4) |
| 유스케이스 | 신규 retirementService — requestRetirement·refreshScope·approve·updateTask·complete·listFor |
| 감사 | RETIREMENT_REQUESTED·RETIREMENT_INVALIDATED·RETIREMENT_APPROVED·RETIREMENT_TASK_UPDATED·RETIREMENT_COMPLETION_BLOCKED·RETIREMENT_COMPLETED |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 67 파일 / 541 테스트 통과(PostgreSQL 30건 — 마이그레이션0024적용).- 신규
tests/integration/retirementFlow.test.ts(3) — 요청·6영역 항목·해시 봉인·AT-67(RETIREMENT 별도 게이트), 승인 →RETIRING+ 자동 복귀 차단, AT-68(확인 없는DONE→ 완료 차단,RETIRING유지)·AT-69(전 항목 확인 →RETIRED+ 이력 유지). - 7a 단위 테스트 10건이 규칙(무효화·공유 자원·배포 진행 중·미적용 요건)을 이미 덮는다.
남은 것
- 단계 8(데이터 전환·정책 활성화, AT-73~74):
UNVERIFIED표시·과거 데이터 전환, 정책 활성화 절차. - 9(6포트 실연계 어댑터 계약 검증) · 10(자동화 품질·실측 기준).
- 관리적 제한:
retirement_plan의 PostgreSQL 왕복 테스트는 아직 없다(도메인·서비스 경로로 검증) —deployment_changes·approval_reuses와 함께 다음 조각에서 확인한다.
D-041. 폐기 후보는 자동 폐기가 아니다 — 미수집은 미달이 아니고, 미확인은 완료가 아니다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 7 · 7a: 후보·요청·정리 확인 규칙)
- 설계서 원안: §13.1("후보 사유는 … 운영 KPI 6개월 이상 지속 미달 및 개선 후 미회복 … 이다. 후보는 검토 대상이며 자동 폐기가 아니다. 6개월 판단에는 지표·목표·측정 주기·연속 기간·개선조치·개선 후 관측을 확인한다. 데이터 미수집을 성과 미달로 간주하지 않는다"), §13.2("요청은 현재 운영 버전·배포·의존 관계·Owner·운영 조직·정리 계획의 해시를 고정한다. 검토 중 실질 변화가 생기면 무효화하고 재검토한다. 승인 시 RETIRING 으로 전환하고 신규 운영·변경 적용을 차단한다. RETIRED 는 필수 정리 확인 후에만 가능하다", "각 항목은 PENDING / DONE / NOT_APPLICABLE / BLOCKED, 책임자, 실제 처리 시스템, 증빙, 확인자·시각을 가진다. NOT_APPLICABLE 은 사유와 검토가 필요하다", "공유 자원은 연결만 적절히 해제", "폐기는 감사·승인·과거 업무 정의의 물리 삭제를 의미하지 않는다"), AT-66 · AT-67 · AT-68 · AT-69 · AT-70 · AT-71.
결정
- 미수집을 미달로 세지 않는다(§13.1 · AT-66): 미달 판정은
CALCULATED관측만 센다. 미수집(NOT_COLLECTED)·계산 불가(NOT_COMPUTABLE)는 미달의 근거가 아니고, 그것들만 있으면 판정은INSUFFICIENT_EVIDENCE— "모름"을 "미달"로 바꾸면 자동 폐기로 이어진다. - 목표를 비교할 수 없으면 미달이 아니다: 목표 수치나 방향이 없으면(
targetValue·targetDirectionnull) 미달로 세지 않는다 — 해석 불가를 실패로 바꾸지 않는다. - 후보는 검토 대상이고, 판정 함수는 아무것도 폐기하지 않는다(AT-66):
CANDIDATE는 상태일 뿐이다. 근거가 모자라면 판정문에 "자동 폐기하지 않습니다" 를 명시해 화면·로그가 같은 말을 하게 했다. - KPI 사유는 다섯 조건이 모두 있어야 한다: 계산된 미달 + 연속 기간(정책, 기본 6개월) + 개선 이력 + 개선 후 관측. 하나라도 빠지면
INSUFFICIENT_EVIDENCE로 무엇이 빠졌는지 적는다. - 요청은 해시를 고정하고, 실질 변화는 무효화한다(§13.2): 운영 버전·배포·의존 관계·Owner·운영 조직·정리 계획(이관 방안) 을 해시로 묶는다 — 계획이 바뀌는 것도 실질 변화다.
RETIRING을 생애주기에 추가했다(§7.2 표에 있었지만 코드에 없던 상태):ACTIVE·SUSPENDED→RETIRING,RETIRING→RETIRED만 — 자동 복귀 경로를 만들지 않는다(§7.2 "폐기 승인 취소는 별도 승인 케이스로만").- 완료는 "처리했다"가 아니라 "확인했다" 다(AT-68):
DONE이어도 확인자·시각이 없으면RETIRED로 가지 않는다. 권한·ID 회수 항목은 별도로 확인 상태를 돌려준다. - 미적용은 사유와 검토자가 있어야 한다(§13.2): 사유 없는
NOT_APPLICABLE은 "안 했다"를 "해당 없음"으로 위장하는 경로다. - 공유 자원은 삭제할 수 없다(AT-70):
sharedResource항목에resourceDisposition: "DELETED"를 적으면 차단한다 — 다른 Agent 자원을 지우는 것을 막는다. - 배포 진행 중 조치가 남으면 완료할 수 없다(AT-71): 늦게 도착하는 성공 응답을 사후에 처리하지 않도록, 조치 확인 전에는
RETIRED전환을 막는다. 그리고RETIRING은ACTIVE로 돌아가지 않으므로 늦은 성공 응답이 운영 재개를 만들지 않는다.
영향
| 영역 | 변경 |
|---|---|
| 상태 | AGENT_LIFECYCLE_STATUSES 에 RETIRING 추가 + 전이표(RETIRING → RETIRED 만) — 마이그레이션 0023 |
| 계약 | 신규 shared/retirement.ts — 후보 사유 5종·후보 판정·요청(해시 고정)·정리 영역 6종·정리 항목(책임자·시스템·증빙·확인자·공유 자원 처분) |
| 도메인 | 신규 domain/retirement/candidacy.ts(AT-66)·domain/retirement/lifecycle.ts(AT-67~71) |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 66 파일 / 538 테스트 통과(PostgreSQL 30건 — 마이그레이션0023적용).- 신규
tests/unit/retirement.test.ts(10) — AT-66(미수집·계산 불가·비교 불가를 미달로 세지 않음, 다섯 조건 충족 시에만 후보, 후보는 자동 폐기 아님), §13.2(해시 고정·무효화), AT-67·AT-71(폐기 중 신규 운영 차단,RETIRING → ACTIVE금지), AT-68(확인 없는 완료 차단), AT-69(전 항목 확인 시 완료·이력 유지), AT-70(미적용 사유·검토자, 공유 자원 삭제 차단·연결 해제 허용), AT-71(배포 진행 중 완료 차단).
남은 것 (단계 7)
- 7b 폐기 실행 경로:
retirement_plan·retirement_task테이블(§15),retirementService(후보 등록 → 요청·해시 고정 → 승인 시RETIRING→ 정리 항목 관리 →RETIRED), 폐기 결재 경로가 Owner 조직의 유형별 경로를 쓰는지 확인(AT-67 — 기존 케이스·결재 경로 기계가RETIREMENT케이스를 이미 다룬다), 의존 Agent 경고·과거 연결 보존. - 단계 8(데이터 전환·정책 활성화, AT-73~74) · 9(실연계) · 10(자동화)이 남았다.
D-040. 게시 포인터와 실제 배포 버전은 각각 추적한다 — 롤백 대상도 다시 검증한다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 6 · 6b: 적용·롤백 · 단계 6 완료)
- 설계서 원안: §12.3("재검증·필요 재승인 이후 변경을 배포하고 정상 동작을 확인한다. 실패 시 변경 적용 중단·이전 승인 버전 복구를 기록한다. 롤백 대상의 유효 승인·현재 데이터 및 보안 적합성을 재검증하며 알려진 취약·금지 구성으로 무조건 복구하지 않는다. 카탈로그 게시 포인터와 실제 배포 버전은 각각 추적한다"), §10.1("승인 범위 밖 배포 차단"), §15(
deployment_change), AT-64 · AT-65.
결정
- 운영 중 = 적용이 끝난 배포의 대상 버전이다(AT-64): 게시(
PUBLISHED)는 배포가 아니다. 새 게시본이 있어도 적용된 배포가 없으면publishedNotApplied로 따로 돌려주고 운영 적용으로 집계하지 않는다 — 그러지 않으면 "적용하지 않은 Agent 가 운영 중"으로 집계된다. - 각 버전을 각각 표시한다: 실제 운영 중인 버전(배포 id 와 함께)과 게시만 된 버전, 그리고 미완료 배포를 한 응답에 분리해 담는다(§12.3 "각각 추적").
- 승인 참조 없이는 적용 요청을 받지 않는다(§10.1): 게시되었더라도 운영 승인(또는 변경 승인) 참조가 없으면 적용을 요청할 수 없다 — 승인 우회 경로를 배포 쪽에서도 막는다.
- 적용 완료는 배포 기록을 함께 움직인다:
SUCCEEDED를 기록하면 배포의 대상 버전·적용 결과도 같이 갱신한다. 게시 포인터와 실제 배포가 어긋나면 "무엇이 운영 중인가"가 두 값으로 갈라진다. - 롤백 대상도 지금 기준으로 다시 검증한다(AT-65): ① 유효한 승인 구성인가 ② 현재 데이터·보안 조건에 맞는가 ③ 알려진 취약·금지 구성인가 — 셋 중 하나라도 어긋나면 막고 사유를 남긴다(
DEPLOYMENT_ROLLBACK_BLOCKED). "되돌리면 되지"가 통제를 무너뜨리는 경로다. - 복구 결과를 실제로 기록한다(AT-65): 롤백은 기록으로 끝나지 않고 배포의 대상 버전을 되돌린 버전으로 바꾸고 롤백 근거를 남긴다 — 실제 배포 상태와 기록이 같아야 한다.
영향
| 영역 | 변경 |
|---|---|
| 계약 | shared/deployment.ts — DeploymentChange(§15 deployment_change)·OperationStatus(AT-64 현황) |
| 도메인 | 신규 domain/releases/operationReality.ts — buildOperationStatus(운영/미적용 분리)·evaluateRollback |
| DB | deployment_changes — 마이그레이션 0022 |
| 포트 | DeploymentStore 확장 — insertChange·updateChange·getChange·listChangesByDeployment |
| 유스케이스 | deploymentService 확장 — requestChangeApplication·recordChangeResult·rollback·operationStatusFor |
| 감사 | DEPLOYMENT_CHANGE_REQUESTED·DEPLOYMENT_CHANGE_RESULT·DEPLOYMENT_ROLLED_BACK·DEPLOYMENT_ROLLBACK_BLOCKED(막은 사유) |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 65 파일 / 528 테스트 통과(PostgreSQL 30건 — 마이그레이션0022적용).- 신규
tests/unit/operationReality.test.ts(4) — AT-64(게시만 된 버전은 운영으로 집계하지 않음·각 버전 표시·미완료 범위 / 적용 완료 배포는 운영 중), AT-65(유효하지 않은 승인·알려진 금지 구성·데이터 부적합은 롤백 차단, 통과 시 "실제 복구 결과를 기록" 안내).
남은 것
- 관리적 제한(정직하게 기록):
deployment_changes의 PostgreSQL 왕복 테스트는 아직 없다 — 도메인 판정과 메모리 저장소 경로로 검증했고, 저장 왕복은 다음 조각에서approval_reuse와 함께 확인한다. deployment_change의 롤백 경로를 변경 평가와 자동으로 잇는 것(실패 → 재검증 → 롤백 대상 선정)은 다음 단계 의존이 있어 수동 인자로 남겨 두었다.- 단계 7(폐기, AT-66~71) · 8(데이터 전환·정책 활성화, AT-73~74) · 9(실연계) · 10(자동화)이 남았다.
D-039. 변경은 "면제"로 처리하지 않는다 — 같은 Risk 여도 승인 범위가 벗어나면 재승인이다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 6 · 6a: 변경 평가·승인 재사용)
- 설계서 원안: §12.1("기준 버전·기존 운영 승인 범위·대상 배포·변경 항목·영향·Risk 재산정·재검증·재승인 범위를 기록한다", "권한 축소나 유형 하향도 자동 검증 면제는 아니다", "공개 범위 확대는 실제 이용 범위 확대 여부를 확인하여 운영 승인 우회를 막는다", "같은 Risk 등급이라는 이유만으로 운영 범위 변경을 무시하지 않는다"), §12.2("전체 제출 해시는 달라지므로 종전 승인 기록을 새 해시로 덮어쓰지 않는다", 재사용 4조건 + "영향 평가 검토자가 재사용 근거를 승인한다", "재승인 불필요 판단도 근거와 확인자를 필요로 한다"), §15(
change_assessment·approval_reuse), AT-62 · AT-63.
결정
- 축소도 면제가 아니다(§12.1): 권한·자율성 축소는
revalidationScope에 남아 검증되며, 면제되는 것은 재승인뿐이다. 면제를 만들면 업무 완료·연계·통제 영향이 검토되지 않는다. - Risk 가 같아도 승인 범위가 벗어나면 재승인(AT-62): 시스템·Tool·공개 범위가 넓어지면
APPROVAL_OR_SYSTEM_SCOPE_CHANGED로 분류하고OPERATION_APPROVAL을 재승인 범위에 넣는다. Risk 등급 비교만으로 끝내지 않는다. - 공개 범위는 "실제 이용 범위"로 본다:
visibility변경만이 아니라 허용 대상 목록이 넓어졌는지까지 확인한다 — 목록이 늘었는데 공개 등급이 그대로여도 확대다(§12.1 의 운영 승인 우회 방지). - 변경은 항목으로 남긴다: 값이 아니라
changedFields(18개 후보 항목)로 기록해 "무엇이 바뀌었는가"를 사람이 읽을 수 있게 한다. - 재사용은 네 조건 + 검토자 승인이다(AT-63): 원승인·동일 scope_hash·정책 호환성·유효성 중 하나라도 빠지면
reusable: false이고, 사유에 빠진 조건을 모두 적는다 — "왜 전면 재검토인지"가 남아야 한다. 네 조건이 다 맞아도 검토자 승인 전에는 재사용이 아니다(§12.2 4단계). - 승인 시점에 네 조건을 다시 판정한다: 후보를 만든 뒤 범위가 바뀌었을 수 있다 — 승인은 후보 생성 시점의 판정을 신뢰하지 않는다.
- 종전 승인은 덮지 않는다:
approval_reuse.priorDecisionId로 참조만 한다(§12.2). - §12.2 의 "범위 재사용이 준비되지 않았다면 전면 재검토를 제공할 수 있지만, 이를 가이드의 필수 절차로 표현하지 않고 설계 제한으로 명시한다" 를 따라, 판정과 사유를 함께 돌려주고 문서에 제한을 적는다.
영향
| 영역 | 변경 |
|---|---|
| 계약 | 신규 shared/change.ts — 변경 유형 9종(§12.1 표 그대로)·평가 레코드·재사용 레코드 |
| 도메인 | 신규 domain/changes/assessment.ts — changedFieldsBetween·changeSignals·evaluateChange·evaluateReuse |
| DB | change_assessments·approval_reuses + 마이그레이션 0021 |
| 포트 | AgentCenterDeps.changes: ChangeStore(평가 2 + 재사용 4) |
| 유스케이스 | 신규 changeService — assessChange(기준/현재 스냅샷 비교)·requestReuse·approveReuse |
| 감사 | CHANGE_ASSESSED(항목·유형·재승인 여부)·APPROVAL_REUSE_REQUESTED(네 확인)·APPROVAL_REUSE_APPROVED |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 64 파일 / 524 테스트 통과(PostgreSQL 30건 — 마이그레이션0021적용, 변경 항목·Risk·범위 jsonb 왕복 포함).- 신규
tests/unit/changeAssessment.test.ts(6) — AT-62(Risk 동일 + 시스템 범위 확대 → 재승인), 축소도 재검증(면제 아님), 표시명만 변경 시 승인 유지·공개 확대 시 재승인, 데이터 범위 확대 → 데이터·환경·보안 재검토, AT-63(네 조건 + 검토자 승인, 빠진 조건을 사유에 모두 표기). tests/integration/postgresStores.test.ts(+1) — 변경 평가 왕복(항목·유형·Risk·재승인 범위).
남은 것 (단계 6)
- 6b 적용·롤백(AT-64·AT-65):
deployment_change(§15)와 "새 게시본 + 실제 구버전 운영"의 버전별 표시(AT-64 — 배포 현황이 이미 배포별 대상 버전을 돌려주므로 그 위에 얹는다), 실패·롤백 시 유효한 이전 승인 구성 확인 + 실제 복구 결과 기록(AT-65). - 관리적 제한:
approval_reuse의 PostgreSQL 왕복은 다음 조각에서 함께 검증한다(현재는 메모리 저장소·서비스 경로로 검증된다). 두 버전을 동시에 두는 제약(§7.8 Agent 당 작업 버전 1개) 때문에 PG 왕복 테스트는 같은 버전을 기준/현재로 참조해 컬럼·jsonb 만 확인한다.
D-038. 중대 사건은 다음 점검 주기를 기다리지 않는다 — 런타임 미확인은 닫지 못한다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 5 · 5c: 사건·즉시 대응 · 단계 5 완료)
- 설계서 원안: §11.4("정기 점검과 별도로 정책위반·비인가 접근·정보노출·중대 오작동은 즉시 대응 사건을 생성한다. '즉시'의 시스템 SLA는 운영 정책에서 정하며 임의의 분 단위를 만들지 않는다", "사건 상태는 OPEN / TRIAGED / CONTAINED / REMEDIATING / RESOLVED / CLOSED로 제안한다. 영향 Agent·배포·데이터·기간, 중지 요청과 실제 확인, 개선·재개 결정을 기록한다. 포탈 긴급 중지 버튼이 실행 플랫폼까지 중지하지 못한 경우 '런타임 조치 미확인' 을 남긴다"), §7.2("실제 중지가 미확인되면 지속 경고한다"), §15(
risk_event·corrective_action), AT-61.
결정
- 사건은 정기 점검 일정과 무관하게 선다(AT-61): 사건 생성은
periodic_review_schedule을 참조하지 않는다 — "다음 주기까지 대기하지 않는다"를 구조로 지킨다. - 중대(
HIGH·CRITICAL)는 조치와 함께 등록한다: 조치가 없으면 사건도 만들지 않는다. 절반만 만들어진 사건은 즉시 대응이 아니다(검증을 insert 앞에 둔다). - "즉시"의 SLA 는 우리가 정하지 않는다(§11.4): 조치 기한은 호출자가 정하고, 사건에는
slaNote로 "SLA 는 운영 정책에서 정한다 — 포탈이 임의의 분 단위를 만들지 않는다" 를 남긴다. 기본값으로 분 단위를 만들지 않았다. - 중지 요청과 실제 확인을 구분한다(§11.4): 중지를 요청하면
UNCONFIRMED로 시작하고(요청≠확인),CONFIRMED가 되기 전에는CONTAINED로 말할 수 없다. 확인되지 않은 사건은listFor의runtimeUnconfirmed로 지속 경고된다(§7.2). - 런타임 미확인은 닫지 못한다:
CLOSED는 런타임 확인 + 재개 결정이 있어야 한다. 미확인인 채로 닫으면 "런타임 조치 미확인"이 사라지고, 그 다음 아무도 그 사실을 보지 못한다. - 재개 결정에는 결정자·사유가 필요하다(§11.4 "개선·재개 결정"): 사유 없는 재개는 "왜 다시 켰는가"를 지운다.
- 상태 전이는 건너뛸 수 없다:
OPEN → RESOLVED같은 지름길을 막는다 — 그 지름길이 곧 "분류·차단·개선 없이 닫기"다.
영향
| 영역 | 변경 |
|---|---|
| 계약 | 신규 shared/incident.ts — 범주 4종(§11.4 그대로)·심각도·감지 경로·런타임 확인·재개 결정, 사건·조치 레코드 |
| 도메인 | 신규 domain/incidents/lifecycle.ts — 전이표·isImmediateResponseRequired·containmentLabel·assertCloseable·assertResumeDecision·SLA_POLICY_NOTE |
| DB | risk_events(영향·중지·재개 jsonb)·corrective_actions + enum 7종 — 마이그레이션 0020 |
| 포트 | AgentCenterDeps.incidents: IncidentStore(사건 5 + 조치 4) |
| 유스케이스 | 신규 incidentService — reportEvent·triage·recordRuntimeConfirmation·contain·startRemediation·resolve·close·addAction·completeAction·listFor·openEvents |
| 감사 | RISK_EVENT_REPORTED(즉시 조치 여부)·RISK_EVENT_STATUS_CHANGED·RISK_EVENT_CONTAINMENT_RECORDED(런타임 확인)·RISK_EVENT_CLOSED(재개 결정)·CORRECTIVE_ACTION_ADDED/COMPLETED |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 63 파일 / 517 테스트 통과(PostgreSQL 29건 — 마이그레이션0020적용, 런타임 미확인·재개 결정 왕복 포함).- 신규
tests/integration/incidentResponse.test.ts(6) — AT-61(사건+조치 동시, 일정 없음), 조치 없는 중대 사건 거부, 요청≠확인(미확인 상태에서 차단 불가), 런타임 미확인 지속 경고 + 종결 차단, 재개 결정 요건, 전이 생략 차단·조치 담당자/기한. - 심각도 값 집합은 §11.4 가 코드로 주지 않아 v0.2 에서 제안했다(사건 범주 4종은 §11.4 문장 그대로).
남은 것
- 단계 6(변경·승인 재사용, AT-62~65): 변경 평가(
change_assessment)·불변 승인과 범위별 재사용(approval_reuse)·적용/롤백, 그리고deployment_change(§15). - 사건·점검의 화면·라우터 노출(§16.3·§17)과 모니터링 연계(단계 9).
D-037. 정기 점검 주기는 정책에서 온다 — 합성 주기는 합성이라고 표시하고, Risk 상승은 기한을 앞당긴다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 5 · 5b: 정기 점검 주기·재계산)
- 설계서 원안: §11.2(성과 점검 주기 표 — Enterprise Owner 월·AX 분기, Personal Owner 분기·AX 반기, "KPI 수집과 사람이 수행하는 성과 검토는 별개다. 자동 데이터가 있어도 검토 완료로 자동 표시하지 않는다. 목표 미달·이상은 원인·조치·담당자·기한을 기록한다"), §11.3(Risk LOW 반기·MEDIUM 분기·HIGH 월, "이상 시 가이드 대응"), §11.4("일정·대상 기간·예정일·담당 조직·완료일을 분리한다", "달력 월·분기·반기 경계, 휴일·유예·도입 첫 주기의 산정은 회사 정책으로 정한다. 단순히 30·90·180일을 더해 가이드의 달력 주기와 동일하다고 주장하지 않는다. 정책 미정인 데모는 합성 주기임을 표시한다", "권한 상승·Risk 상승 시 새 주기의 다음 기한이 더 늦어지지 않도록 재계산한다"), §15(
periodic_review_schedule·periodic_review), AT-58 · AT-59 · AT-75.
결정
- 주기는 정책에서 온다. 가이드 주기는 효력 없는 템플릿이고(
confirmed: false), 일정에synthetic: true가 서며 정책 버전도 템플릿 버전으로 남는다 — §11.4 의 "합성 주기임을 표시"를 데이터로 지킨다. 사내 확정 정책을 주면 표시가 사라진다. - 달력 개월을 더한다(30·90·180일이 아니다). §11.4 가 금지한 것은 일수 덧셈으로 "같다"고 주장하는 것이므로, 달력 월을 더하고 말일을 보정한다(1/31 + 1개월 = 2월 말일). 그래도 합성임은 표시한다 — 달력 경계·휴일·유예는 회사 정책이기 때문이다.
- Risk·권한 상승은 기한을 앞당길 뿐 뒤로 미루지 않는다(AT-75): 재계산은
min(기존, 새 계산)이고, 더 늦는 결과는 무시한다(일정도 사유도 바뀌지 않는다). 무시한 사실은 감사에UNCHANGED로 남겨 "왜 안 밀렸는지"를 확인할 수 있다. - 일정은 계획 시 조용히 바뀌지 않는다: 재계획은 없는 일정만 만든다. 재계산은 별도 동작이라 기한 변경에는 항상 사유(
lastRecalculationReason)가 붙는다. - KPI 수집은 검토 완료가 아니다(§11.2): 일정은 사람이
complete로 남긴 기록으로만 다음 주기로 넘어간다. 자동 관측만으로 완료 표시가 서지 않는다. - 목표 미달은 조치·담당자·기한이 있어야 완료로 받는다(§11.2):
NEEDS_ACTION인데 셋 중 하나라도 없으면 거부한다. - 일정과 완료 기록을 분리한다(§15 · §11.4): 일정 행은 현재 예정을 가리키고 완료하면 다음 주기로 이어지며, 완료 이력은
periodic_reviews에 쌓인다. 그래서 "완료일"이 일정에 덮여 사라지지 않는다.
영향
| 영역 | 변경 |
|---|---|
| 계약 | 신규 shared/periodicReview.ts — 점검 종류·주체·주기·상태·판정, 일정·완료 기록 |
| 도메인 | 신규 domain/reviews/cadence.ts — REVIEW_CADENCE_TEMPLATE·planReviewSchedules·nextDueAt·recalculateDue |
| DB | periodic_review_schedules(Agent·종류·주체 고유)·periodic_reviews + enum 5종 — 마이그레이션 0019 |
| 포트 | AgentCenterDeps.periodicReviews: PeriodicReviewStore |
| 유스케이스 | 신규 periodicReviewService — planFor·recalculateFor·complete·listFor |
| 감사 | PERIODIC_REVIEW_PLANNED(합성 여부·정책 버전)·PERIODIC_REVIEW_RECALCULATED(앞당김/유지 수)·PERIODIC_REVIEW_COMPLETED(판정·다음 예정일·조치) |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 62 파일 / 510 테스트 통과(PostgreSQL 28건 — 마이그레이션0019적용, 합성 표시·재계산 사유 왕복 포함).- 신규
tests/unit/reviewCadence.test.ts(5) — AT-59(LOW 반기·MEDIUM 분기·HIGH 월), AT-58(유형별 주기·주체별 분리, Risk 미확정이면 권한 일정 없음), 달력 개월 덧셈, 합성 표시, AT-75(앞당김·연기 금지). - 신규
tests/integration/periodicReview.test.ts(4) — 일정 계획·조직별 담당, 계획 멱등, 재계산 앞당김/연기 금지 + 감사, 완료는 수동·목표 미달 요건·다음 주기 이월.
남은 것 (단계 5)
- 5c 사건·즉시 대응(AT-61): 정기 점검과 별도로 정책위반·비인가 접근·정보노출·중대 오작동은 즉시 대응 사건을 만들고, 사건 상태·영향 범위·중지 요청/실제 확인·런타임 조치 미확인을 기록한다(§11.4). "즉시"의 SLA 는 운영 정책이 정한다 — 임의의 분 단위를 만들지 않는다.
- 달력 경계·휴일·유예·도입 첫 주기 산정 규칙은 사내 확정 대상(§11.4) — 확정되면
ReviewCadencePolicy로 갈아끼운다.
D-036. KPI 값은 저장하지 않고 상태와 함께 계산한다 — 0 은 "관측된 0"일 때만 0 이다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 5 · 5a: KPI 정의·관측)
- 설계서 원안: §11.1("관측값은 배포·기간·출처·수집일·정의 버전·표본 또는 분모를 가진다. 미수집·관측 0·계산 불가를 구분한다. 실측과 기대 효과는 별도 저장한다. 부모·자식 Agent 절감 시간·비용은 단일 업무 추적 기준을 확보하기 전 합산하지 않는다"), §15(
metric_definition·metric_observation), AT-60("KPI 분모 0·미수집·관측 0 | 계산 불가/미수집/0을 구분").
결정
- 값을 저장하지 않는다. 관측에는 분자·분모만 저장하고 값과 상태는 계산한다(
evaluateMetric). 값을 저장하면 규칙이 바뀔 때 저장값과 계산값이 갈라지고, 그 갈라짐이 바로0·미수집·계산 불가의 구분을 무너뜨린다(AT-60). 0을 "모름"의 대표값으로 쓰지 않는다: 관측 없음 →NOT_COLLECTED, 분모 0 →NOT_COMPUTABLE(값null), 분자 0·분모 양수 →CALCULATED값0. 대시보드의0이 "0건"인지 "수집 실패"인지 알 수 없으면 KPI 가 거짓말을 한다.- 수집 실패도 관측으로 남긴다(
numerator: null+ 사유): "관측이 없다"와 "수집을 시도했지만 실패했다"를 구분해야 미수집의 원인을 추적할 수 있다. - 정의 버전을 관측에 남긴다(§11.1): 어느 정의로 계산한 값인지 나중에 대조한다. 정의는 키·버전으로 멱등 등록된다.
- 실측과 기대 효과를 조회에서도 나눈다(
kind: MEASURED | EXPECTED): 둘을 섞으면 달성 여부를 판단할 수 없다. - 절감 효과는 부모·자식 합산을 막는다(§11.1): 단일 업무 추적 기준이 없으면 같은 절감을 두 번 세게 되므로
aggregationRule이 차단한다. - 분모를 숨기지 않는다: 평가 결과는 값과 함께 분자·분모를 돌려준다 — 비율만 보면 표본 크기를 알 수 없다.
영향
| 영역 | 변경 |
|---|---|
| 계약 | 신규 shared/metric.ts — 지표 8종, 값 종류·주기·상태·관측 종류, 정의·관측 레코드 |
| 도메인 | 신규 domain/metrics/evaluation.ts — evaluateMetric·canAggregateAcrossAgents |
| DB | metric_definitions(키+버전 고유)·metric_observations + enum 3종 — 마이그레이션 0018 |
| 포트 | AgentCenterDeps.metrics: MetricStore |
| 유스케이스 | 신규 metricService — defineMetric·activateDefinition·recordObservation·overviewFor·aggregationRule |
| 감사 | METRIC_DEFINED·METRIC_DEFINITION_ACTIVATED·METRIC_OBSERVED(상태·분모를 남긴다) |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 60 파일 / 500 테스트 통과(PostgreSQL 27건 — 마이그레이션0018적용,0분모 왕복 포함).- 신규
tests/integration/metricEvaluation.test.ts(7) — AT-60 세 가지 구분, 분자·분모 보존, 수집 실패 기록, 실측·기대 분리, 정의 멱등·미정의 차단, 기간 역전 거부, 합산 금지.
남은 것 (단계 5)
- 5b 정기 점검 일정·주기(AT-58·AT-59·AT-75) — 활성 정책의 월·분기·반기 주기, Risk 등급별 권한 점검, Risk 상승 시 재계산(더 느슨한 기한으로 연기 금지).
- 5c 사건·즉시 대응(AT-61) — 정기 점검과 별도로 중대 사건 즉시 생성, 런타임 조치 미확인 표시.
- 달력 주기·즉시 SLA 는 사내 확정 대상(§11.4 "단순히 30·90·180일을 더해 … 동일하다고 주장하지 않는다") — 데모는 합성 주기임을 표시한다.
D-035. 공식 운영 ID 는 포탈이 지어내지 않는다 — 미확인 발급은 운영 등록 대기다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 4 · 4c: 공식 식별자 · 단계 4 완료)
- 설계서 원안: §10.2("
operational_agent_id| 운영 승인 후 운영 등록 | 공식 운영 식별자, 발급 시스템과 상태 포함", "운영 ID가 이미 외부에서 발급된 Agent는 검증 후 연결한다. 포탈이 발급할지 외부 플랫폼이 발급할지는 연계 계약으로 정한다. ID 미발급은 운영 등록 대기다. 발급 재시도는 멱등 처리하고 폐기 후 동일 운영 ID를 신규 Agent에 재사용하지 않는다"), §7.2(READY조건 = "유효 운영 승인, 공식 ID 및 준비 조건 충족"), AT-55.
결정
- ID 문자열을 포탈이 생성하지 않는다. 발급된 후보 값을 받아 기록하고, 확인 전에는
PENDING(= 운영 등록 대기)으로 둔다. 포탈이 ID 를 지어내면 "공식"이라는 말이 근거를 잃고, §10.2 가 연계 계약으로 남긴 결정(포탈 발급 vs 외부 발급)을 코드가 앞질러 정하게 된다. - 발급 재시도는 멱등이다: 같은 Agent·같은 발급 시스템이면 새 레코드를 만들지 않고 기존 것을 돌려준다(AT-55 "중복 ID 없음"). 발급 시스템이 다르면
IDEMPOTENCY_CONFLICT— 한 Agent 의 공식 ID 는 하나다. - 재사용 금지는 DB 고유 제약으로 강제한다:
operational_agent_id는 전역 고유이고, 폐기된 Agent 의 레코드도 지우지 않으므로 그 ID 는 계속 점유된다. 서비스가 먼저 확인해OPERATIONAL_ID_IN_USE(409, §16.5 에 없어 제안) 로 이유를 설명한다. - 미확인 발급 상태로는
READY가 될 수 없다(§7.2): 미발급·PENDING은 모두 운영 등록 대기이고, 게시 준비 검사가REQUIRED_FIELDS_MISSING(operationalAgentId)로 막는다. AT-55 의 "미확인 발급 상태 대조"가 실제 차단이 된다. - 외부 발급 ID 는 검증 후 연결하면 바로 확인 상태다(
source=EXTERNAL) — 이미 발급된 Agent 를 다시 발급 절차에 태우지 않는다(§10.2). - Agent 당 하나:
agent_id도 고유 제약이다 — 발급 시스템이 섞이거나 이력이 갈라지지 않는다.
영향
| 영역 | 변경 |
|---|---|
| 계약 | 신규 shared/identifier.ts(OperationalIdentifier·상태·출처·발급 결과) |
| 도메인 | 신규 domain/agents/identifier.ts(decideIdentifierIssue·assertIdentifierAvailable·operationalRegistrationBlocker) |
| DB | operational_identifiers + enum 2종, 고유 인덱스 2개 — 마이그레이션 0017 |
| 포트 | AgentCenterDeps.identifiers: IdentifierStore |
| 유스케이스 | 신규 identifierService — requestOperationalId·confirmOperationalId·connectExternalOperationalId·registrationBlocker |
| 게시 | publishReadiness 에 operationalIdentifier 입력 추가 — 미발급·미확인 차단 |
| 오류 | OPERATIONAL_ID_IN_USE(409) — §16.5 에 없어 제안 |
| 감사 | OPERATIONAL_ID_REQUESTED·OPERATIONAL_ID_REQUEST_RETRIED·OPERATIONAL_ID_CONFIRMED·OPERATIONAL_ID_CONNECTED |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 59 파일 / 492 테스트 통과(PostgreSQL 26건 — 마이그레이션0017적용, 고유 제약이 재사용을 실제로 거부하는지 포함).- 신규
tests/integration/operationalId.test.ts(6) — 발급 멱등(AT-55), 미발급 → 게시 차단·DRAFT 유지, 미확인 → 게시 차단(운영 등록 대기), 확인 후 게시 →READY, 폐기 후 재사용 금지 + 발급 시스템 혼선 차단, 외부 발급 연결. tests/unit/publishReadiness.test.ts는 확인된 ID 를 기본 입력으로 갖는다(공식 ID 가 게시 조건이 되었으므로).
남은 것 (단계 4 밖)
- 식별자·검증·보안·운영 API 노출(§16.3)과 화면(§17) — 지금은 서비스 계층까지다.
registration_code(기획·사전심의 단계 코드)는 기존agent.code가 그 역할을 하고 있다 — 사내 확정 시 대조한다.
D-034. 배포 이력을 남긴다 — 적용 결과와 런타임 관측은 다른 값이다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 4 · 4b-2: 배포 이력)
- 설계서 원안: §10.2("
deployment.id/external_deployment_id| 배포 등록 | 환경·조직별 설치 단위"), §10.3("배포 결과 REQUESTED / APPLYING / SUCCEEDED / FAILED / ROLLED_BACK과 런타임 상태 UNKNOWN / RUNNING / STOPPED / ERROR / RETIRED는 분리한다. SUCCEEDED는 적용 작업 완료이며 지속적인 정상 동작의 보증이 아니다", "다중 배포 중 일부만 적용된 경우 배포별 상태와 미완료 범위를 표시한다"), §7.3, §15(deployment), AT-55.
결정
- 새 테이블을 만들지 않고 기존
deployments를 확장했다. v0.1 에서 선언만 되고 아무도 쓰지 않던 테이블이었다(§0 의 "선언만 되어 있다 — 배선되지 않은 상태를 완료로 보지 않는다"). §15 확장 원칙(D-026)과 같다:agent_id·region·systems·apply_status·applied_digest·rollback_ref·confirmation·note·requested_by/at를 더했다. - 적용 결과와 런타임을 한 레코드에서 따로 관리한다(
applyStatus/runtimeStatus+statusObservedAt).recordResult는 런타임을 건드리지 않고,observeRuntime은 적용 결과를 건드리지 않는다 — "SUCCEEDED ≠ 정상 동작"을 코드가 어기지 않게 했다. - 적용 완료에는 구성 digest 가, 롤백에는 근거가 필수다(§15
applied_digest·rollback_ref). 없으면 나중에 "무엇을 적용했고 무엇으로 되돌렸는지"를 대조할 수 없다. - 적용 요청 등록은 멱등하다: 같은 버전·환경·조직에 살아 있는 배포가 있으면 새로 만들지 않는다. AT-55 가 공식 ID 재시도에 요구하는 성질(중복 없음)을 배포 등록에도 적용한 것이다.
- 운영 적용 확인은 배포 기록에 대한 것이다: 적용 결과가
SUCCEEDED인 배포가 없으면 확인을 거부한다. 확인 기록은 그 배포에 저장된다 — 확인과 배포가 따로 놀면 이력이 끊긴다(§10.3 "적용 요청을 만들고 … 결과를 확인한다"). - 다중 배포 현황은 Agent 단위로 모아 배포별 상태·런타임과 미완료 범위(
pending)를 돌려준다. §7.3("배포가 구버전에 남아 있으면 각각의 버전을 표시")을 위해 대상 버전 상태도 함께 담았다.
영향
| 영역 | 변경 |
|---|---|
| 계약 | shared/deployment.ts — DeploymentRecord·DeploymentOverview 추가, 출처 어휘를 status.ts 한 벌로 통일(MANUAL·PLATFORM, 중복 정의 제거) |
| DB | deployments 확장 + deployment_apply_status enum — 마이그레이션 0016 |
| 포트 | AgentCenterDeps.deployments: DeploymentStore(insert·get·update·listByRelease·listByAgent) |
| 유스케이스 | deploymentService — requestDeployment·recordResult·observeRuntime·overviewFor(확인은 confirmApplication) |
| 감사 | DEPLOYMENT_REQUESTED·DEPLOYMENT_RESULT_RECORDED·RUNTIME_STATUS_OBSERVED |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 58 파일 / 485 테스트 통과(PostgreSQL 25건 — 마이그레이션0016적용 왕복).- 신규
tests/integration/deploymentHistory.test.ts(5) — 요청 멱등, digest·롤백 근거 필수, 런타임과 적용 결과 분리, 미완료 범위·환경별 배포, 배포 결과 없이는 확인 불가. tests/integration/postgresStores.test.ts(+1) — 배포 왕복(적용 결과 → 런타임 관측 순서에서 값이 서로를 덮어쓰지 않음).
남은 것
- AT-55: 공식 ID(
operational_agent_id) 발급·연결 — 멱등 재시도, 미확인 발급 상태 대조, 폐기 후 재사용 금지, ID 미발급 = 운영 등록 대기. deployment_change(§15 변경 배포)는 단계 6(변경·승인 재사용)에서 다룬다 — 요청 대상 버전·승인 참조가 변경 평가와 함께 움직인다.- 신규 환경·Region 확장 시 기존 승인 범위 재평가(§10.3 후단)는 변경 평가와 연결한다.
D-033. 게시는 READY 까지다 — ACTIVE 는 운영 적용 확인이 만든다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 4 · 4a·4b-1: 생애주기
READY·적용 확인) - 설계서 원안: §7.2("READY: 유효 운영 승인, 공식 ID 및 준비 조건 충족 / ACTIVE: 운영 적용 확인 완료", "READY에서도 승인·ID·정책의 유효성을 다시 확인한다"), §7.3("게시 자체는 운영 적용을 실행하지 않는다. READY Agent의 승인 버전은 게시할 수 있고 화면에 '운영 적용 대기'로 표시한다"), §10.3, AT-54, AT-56, AT-57.
결정
- 생애주기에
READY를 도입하고 게시를DRAFT → READY로 바꿨다. 전에는 게시가 곧ACTIVE였다(v0.1 동작) — §7.2 는ACTIVE를 운영 적용 확인 완료로 정의하므로 그것은 거짓 기록이었다. DEPLOYMENT_CONFIRMATION을 게시 게이트에서 뺐다. 게시 준비 검사는 케이스의 필수 게이트를 전부 요구하므로, 그 게이트가 필수인 동안에는 "적용 확인이 끝나야 게시할 수 있고, 게시해야 적용할 수 있는" 순환이 있었다. 이제 그 게이트는required: false이고 사유에READY → ACTIVE라고 적혀 있다(게이트 자체는 남아 그 단계에서 쓰인다).- 운영 적용 확인이
READY → ACTIVE를 일으킨다(deploymentService.confirmApplication): §10.3 최소 확인 7항목이 모두 확인되어야 하고, 확인이DEPLOYMENT_CONFIRMATION게이트를 닫는다 — 게이트를 직접 닫을 수는 없다(다른 게이트와 같은 원칙). - 승인 범위 밖 적용은 차단하고 변경 평가를 요구한다(AT-56): 적용 범위가 운영 계획의
approvedScope와 다르면 ACTIVE 로 옮기지 않고 게이트를FAILED로 남긴 뒤DEPLOYMENT_SCOPE_MISMATCH로 차단한다. §16.5 에 이 코드가 없어 v0.2 에서 제안했다(409). - 수동 확인은 출처·확인자·시각·증빙을 남긴다(AT-57).
source=MANUAL인데 증빙이 없으면 거부하고, 감사 기록에source·confirmedBy·confirmedAt·증빙을 담는다 — 포탈은 적용을 실행하지 않고 기록만 하므로 "실제 API 실행으로 오인"되지 않아야 한다. - 런타임 관측 상태는
UNKNOWN으로 시작한다: §10.3 의 "SUCCEEDED는 적용 작업 완료이며 지속적인 정상 동작의 보증이 아니다" 를 그대로 반영했다 — 확인 기록과 정상 동작 관측을 분리한다.
영향
| 영역 | 변경 |
|---|---|
| 상태 | AGENT_LIFECYCLE_STATUSES 에 READY 추가, 전이표 갱신(DRAFT→READY, READY→ACTIVE, READY→DRAFT) — 마이그레이션 0015(enum) |
| 게이트 | planGatesFor — DEPLOYMENT_CONFIRMATION 은 일반 케이스에서 required: false |
| 게시 | publishRelease — DRAFT → READY(ACTIVE 자동 전환 제거) |
| 신규 | shared/deployment.ts(최소 확인 7항목·출처·확인 기록), deploymentService.confirmApplication |
| 오류 | DEPLOYMENT_SCOPE_MISMATCH(409) — §16.5 목록에 없어 제안 |
| 감사 | DEPLOYMENT_CONFIRMED(출처·확인자·시각·증빙·범위 일치) |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 57 파일 / 479 테스트 통과(마이그레이션0015적용).- 신규
tests/integration/deploymentConfirmation.test.ts(5) — AT-54(게시는 READY, 적용 확인 게이트가 게시를 막지 않음), 최소 확인 항목 누락 차단(항목별), AT-57(증빙 없는 수동 확인 거부 · 확인 기록에 출처·확인자·시각 · 런타임UNKNOWN), AT-56(승인 범위 밖 적용 차단 → ACTIVE 아님 + 게이트FAILED). - 단위 사례 2건 갱신: 필수 게이트 9종 → 8종, 전이표(게시만으로
DRAFT → ACTIVE불가).agentLifecycleOps의publishedAgent헬퍼는 적용 확인까지 마쳐 ACTIVE 를 만든다.
남은 것
- 4b-2:
deployment·deployment_change테이블(§15) — 배포 결과(REQUESTED/APPLYING/SUCCEEDED/FAILED/ROLLED_BACK)와 런타임 상태 분리, 다중 배포의 미완료 범위 표시. - AT-55: 공식 ID(
operational_agent_id) 발급·연결(멱등 재시도·미확인 상태 대조) — 게시 조건과 연결한다.
D-032. §10.1 제출 요건 중 게이트가 아닌 항목은 운영 승인 경로 확정 시 차단한다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 3 · 3d-2: 최소 권한·운영 계획 · 단계 3 완료)
- 설계서 원안: §10.1("운영 승인 전 Agent 대상·유형, 필요한 사전심의, 매핑 또는 미적용 승인, Risk 평가, 검증 결과, 데이터·환경 적합성, 보안 검토, 최소 권한 설계 및 운영 계획이 준비되어야 한다", "운영 계획에는 Owner·개발·운영·지원 조직, KPI 정의·목표·수집 방식, 정기 점검, 권한 재검토, 장애·문의·Risk Event 대응, 서비스 중지·복구·롤백, 업무 연속성 및 보존 정책 참조를 포함한다"), §11.3 권한 점검, §15(
operation_plan·permission_record), §16.3POST /releases/{id}/operation-plans.
결정
- §10.1 제출 요건 9개 중 7개는 게이트가 닫고, 나머지 2개(최소 권한 설계·운영 계획)는 제출 요건 검사가 막는다.
GATE_TYPES에 없는 것을 새 게이트로 만들지 않는다 — §15 도 이 둘을 게이트가 아닌 별도 엔티티로 둔다(운영 승인 게이트의 내용은 결재 슬롯이다, D-026). - 차단 지점은 "운영 승인 경로를 세우는 순간"(
ensureRoute)이다. §10.1 의 "운영 승인 전"이 곧 이 시점이다 — 준비되지 않은 것을 결재자에게 올리면 판단할 근거가 없다. - 누락을 한 번에 항목별로 돌려준다: 게이트처럼 하나씩 막지 않고
fieldErrors에operationPlan.kpi·operationPlan.retentionRef·permissions.<대상>처럼 전부 담는다. 결재자가 아니라 제출자가 무엇을 채울지 알아야 한다. approvedScope는 준비 상태에서 요구하지 않는다 — §10.1 은 그것을 계획의 내용이 아니라 운영 승인이 확정하는 값으로 둔다. 승인 전에 요구하면 승인 결과를 먼저 적어 넣으라고 하는 셈이다.- 최소 권한이 아닌 설계도 기록할 수 있다: 전체 허용(
*)처럼 실제로 부여된 권한은 사실대로 등록하고, 준비 상태 검사가 차단한다. 등록을 막으면 실제 권한이 어디에도 남지 않는다(§10.3 의 "수동 확인도 기록한다"와 같은 원칙). - 최소 권한 위반을 5종으로 판정한다: 설계 없음 · 대상/허용 행위 미확정 · 전체 허용 · 유효기간 만료 · 재검토 경과(§11.3). §10.1 이 "최소"의 기준을 코드로 주지 않아, 설계서가 명시한 것만 판정하고 나머지는 추정하지 않는다.
- 운영 계획보다 승인 범위가 먼저 확정될 수 없다:
confirmScope는 계획이 준비된 뒤에만 동작한다.
영향
| 영역 | 변경 |
|---|---|
| 계약 | 신규 shared/operationPlan.ts(OperationPlan·OperationPlanKpi), shared/permission.ts(PermissionRecord) |
| 도메인 | 신규 domain/operation/plan.ts(assessOperationPlan), domain/permission/assessment.ts(assessLeastPrivilege) |
| DB | 마이그레이션 0014 — operation_plans(버전당 하나, 고유 인덱스)·permission_records |
| 포트 | AgentCenterDeps.operations: OperationStore(계획 3 + 권한 2, 낙관적 잠금) |
| 유스케이스 | 신규 operationService.ts — upsertPlan·registerPermission·readinessFor·assertSubmissionReady·confirmScope |
| 차단 | approvalService.ensureRoute — 제출 요건 미충족 시 REQUIRED_FIELDS_MISSING(항목별 fieldErrors) |
| 감사 | OPERATION_PLAN_CREATED·OPERATION_PLAN_UPDATED·PERMISSION_RECORDED·OPERATION_SCOPE_APPROVED·SUBMISSION_REQUIREMENTS_UNMET(차단 근거) |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 56 파일 / 474 테스트 통과(PostgreSQL 24건 — 마이그레이션0014실제 적용 왕복).- 신규
tests/integration/operationReadiness.test.ts(6) — 요건 충족 시 경로 확립, 계획 없음 차단, 항목 누락을 한 번에 지목, 전체 허용 권한 차단(기록은 허용), 만료·재검토 경과 판정, 승인 범위는 계획 준비 후에만 확정. tests/integration/postgresStores.test.ts(+1) — 계획·권한 왕복(KPI·승인 범위·허용 행위, 낙관적 잠금rowVersion).- 기존 사례 조정:
approvalService.test.ts의setup이 제출 요건을 채우고(skipReadiness로 미준비 재현),workflowRouter의 API 흐름 대상에operations를 넘긴다. 테스트 키트approveOperationRoute가 결재 경로를 세우기 전에 요건을 채운다.
남은 것 (단계 3 밖)
- 최소 권한 실제 연계(단계 9): IAM 대조·회수 증빙·만료 감시(§10.3·§11.3)는 6포트가
NOT_CONFIGURED인 동안 Mock 으로만 돈다. - 운영 계획·권한·검증·보안의 화면·라우터 노출(§16.3)과 KPI 정의의 관측 연계(단계 5).
D-031. 환경 적합성 판정이 DATA_ENVIRONMENT 게이트를 닫는다 — 금지는 실패, 미확정은 보류
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 3 · 3d-1: 데이터·환경 게이트 연결)
- 설계서 원안: §5.3("금지·미확정 또는 필수 조건 미충족은 운영 승인과 적용을 차단한다"), §7.3, §9.1("금지된 데이터 환경 같은 절대 차단 조건은 면제하지 않는다"), §10.1(운영 승인 제출 요건의 "데이터·환경 적합성"), §15(
environment_assessment), AT-44·AT-45.
결정
- 환경 적합성 판정이
DATA_ENVIRONMENT게이트를 닫는다(§10.1 제출 요건 → §7.3 게이트). 판정을 게시 차단으로 잇는 마지막 연결이었다. PROHIBITED→FAILED: 금지는 판정이 끝난 결과이고, §9.1 이 말한 절대 차단 조건이다 — 조건부 예외로도 넘길 수 없다.UNDETERMINED(처리 위치 미확정·사내 실행환경·표에 없는 등급)·CONDITIONAL(조건 자체가 미확정, Q-08) → 보류: 실패로 만들지 않고 사유만 남긴다. 판정이 서지 않은 것을 실패로 기록하면 거짓이 된다 — D-029 와 같은 판단이다. 게이트가 닫히지 않으므로 운영 적용은 그대로 차단된다(차단 효과는 동일하고, 기록만 정확해진다).- 데이터·환경 게이트도 직접 닫을 수 없다 — 판정으로만 닫힌다(운영 승인·사전심의·검증·보안과 같은 원칙).
- API 등록 시 데이터 범위 선언을 받는다:
POST /agents(workflow.createAgent)에dataClasses·dataTags·processingLocation을 추가했다. 이 값이 없으면 판정이UNDETERMINED로 남아 API 만으로는 게시가 불가능하다는 것을 종단 테스트가 드러냈다(§5.3 은 등록 시점의 데이터 범위 선언을 전제한다). - 초안 fixture 도 데이터 범위를 선언한다: 테스트 키트의 기본 초안이
비기밀·EXTERNAL_INSIDE_REGION을 선언하고, 미확정·금지 시나리오는overrides로 명시한다. 선언 없는 초안은 실제 제출물이 아니기 때문이다.
영향
| 영역 | 변경 |
|---|---|
| 유스케이스 | applyEnvironmentToGate(신규, assessmentService.ts) — 판정 → 게이트 PASSED/FAILED/보류 |
| 게이트 규칙 | setGateStatus — DATA_ENVIRONMENT 직접 닫기 금지 |
| API | workflow.createAgent 입력에 dataClasses·dataTags·processingLocation 추가 |
| 감사 | GATE_STATUS_CHANGED(통과·금지), DATA_ENVIRONMENT_HELD(보류, 사유·미충족 조건) |
| 테스트 | 키트: createDraft(overrides)·assessDataEnvironment, Service.assessments 추가 |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 55 파일 / 467 테스트 통과(PostgreSQL 23건 포함).- 신규
tests/integration/environmentGate.test.ts(4) — 허용 → 게이트PASSED→ 게시, 금지 →FAILED→ 게시 차단, 사내 실행환경 → 보류(미확정은 실패가 아니다) +DATA_ENVIRONMENT_HELD감사 + 게시 차단, 직접 닫기 거부. - 기존 사례 조정:
agentPublishGate(AT-02)·assessmentService(판정 보류 시나리오)·workflowRouter(API 흐름) — 모두 데이터 범위 선언이 추가됐고,caseGateService의 직접 닫기 예시는 서비스가 없는 게이트(CLASSIFICATION)로 옮겼다.
남은 것
- 3d-2: 최소 권한(
permission_record)·운영 계획(operation_plan)과 §10.1 제출 요건 검사(운영 승인 요청 시 최소 권한 설계·운영 계획 누락 차단). - 데이터 검토자가 판정을 등록하는 UI/라우터 노출(§16.3)과 만료 증빙·정기 재검토 연계(§11.3).
D-030. 보안 검토가 SECURITY 게이트를 닫는다 — 망 차단 대체는 기록이 있어야 인정한다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 3 · 3c: 보안 검토·증빙·게이트 연결)
- 설계서 원안: §9.2("네트워크 통제로 정보유출 검토를 대체하는 경우 적용 환경·통제 범위·검증 결과·확인자·유효기간을 기록한다", "개발자가 '망 차단' 체크 하나로 검토를 면제하지 않는다", "증빙이 다른 구성 digest를 대상으로 하면 현재 버전 게이트는 STALE이다", "취약점 DB 버전과 검사 시각 등 검사 조건도 저장한다"), §15(
security_review/finding·evidence_record), §7.3, AT-49, AT-50.
결정
- 보안 검토가 게이트를 닫는다(§7.3): 3개 위협군이 적합하면
SECURITY게이트PASSED, 아니면FAILED— 게시 차단으로 이어진다(AT-50 의 "자동 면제 없음"이 실제 차단이 된다). - 증빙이 다른 구성을 대상으로 하면 게이트를
STALE로 되돌린다(AT-49).FAILED가 아니라STALE인 이유: 검토가 틀렸다는 뜻이 아니라 판단 근거가 낡았다는 뜻이고, 재검증으로 회복되는 상태이기 때문이다(게이트 상태 값이 그 구분을 이미 갖고 있다). - 네트워크 통제 대체는 기록이 있어야 인정한다(AT-50): 적용 환경·통제 범위·검증 결과·확인자·유효기간이 모두 있어야 하고, 검증 결과가 적합이어야 하며 유효기간이 살아 있어야 한다. "망 차단" 체크 하나로는 면제되지 않는다.
- 대체는 면제가 아니다 — 인정된 대체는 게이트 사유에
네트워크 통제 대체: EXTERNAL_LEAK로 기록되고, 검토에network_control_substitution이 남는다. - 검토하지 않은 위협은 차단이다: 3개 위협군 중 빠진 것이 있으면 게이트를 닫지 않는다.
EVIDENCE_REGISTERED·IN_REVIEW도 검토로 보지 않는다. - 증빙은 값을 복사하지 않고 참조를 남긴다(§15): 외부 식별자나 URL 중 하나는 필수이고, 구성 digest 를 남겨야 AT-49 판정이 성립한다. 접근 등급(
access_class)도 함께 남긴다. - 보안 게이트도 직접 닫을 수 없다 — 보안 검토로만 닫힌다(운영 승인·사전심의·검증과 같은 원칙).
- 위협별 결과는 jsonb 로 둔다(D-029 의 판단과 같다): 검토에 종속된 값이고 항상 부모와 함께 읽는다.
영향
| 영역 | 변경 |
|---|---|
| 계약 | 신규 shared/security.ts(SecurityReviewRecord·SecurityFinding·NetworkControlSubstitutionRecord·EvidenceRecord) |
| DB | 마이그레이션 0013 — security_reviews·evidence_records |
| 포트 | AgentCenterDeps.security: SecurityStore(검토 3 + 증빙 3) |
| 유스케이스 | 신규 securityService.ts — registerEvidence·recordReview·latestFor |
| 게이트 규칙 | setGateStatus — SECURITY 직접 닫기 금지 |
| 배선 | 컨테이너에 securityService, 테스트 키트에 security·recordSecurityReview |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 54 파일 / 463 테스트 통과(PostgreSQL 23건 — 마이그레이션0013실제 적용 왕복).- 신규
tests/integration/securityService.test.ts(4) — 적합 시 게이트PASSED→ 게시, 미검토 위협 →FAILED→ 게시 차단, AT-50(검증된 대체로 사외 유출 통과 + 대체 기록), AT-49(다른 digest 증빙 → 게이트STALE→ 게시 차단). tests/integration/postgresStores.test.ts(+1) — 검토·증빙 왕복(위협·대체 기록·증빙 참조 무손실).- 기존 사례 두 건이
SECURITY→DATA_ENVIRONMENT로 바뀌었다(direct closure 금지의 영향) — 판정 규칙이 그대로인 게이트를 쓴다.
남은 것
- 3d:
DATA_ENVIRONMENT게이트(2e 평가 스냅샷 연결)·운영 계획(operation_plan)·최소 권한(permission_record) — 단계 3 의 마지막 조각. - 증빙 만료 감시와 취약점 DB 버전·검사 시각(§9.2)의 저장은 검사 도구 연계(단계 9)와 함께.
- 보안·검증 화면(UI)과 §16.3 라우터 노출.
D-029. 검증 결과가 VALIDATION 게이트를 닫는다 — 기준 미확정은 실패가 아니라 보류다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 3 · 3b-2: 검증 유스케이스·게이트 연결)
- 설계서 원안: §9.1("정책 미정은 '기준 미확정'으로 표시하고 운영 적용을 보류한다", "결과는 PASS / FAIL / NOT_RUN / NOT_APPLICABLE과 근거를 가진다", "총평 PASS만으로 세부 실패를 숨기지 않는다", "Risk별 테스트 수·시나리오 다양성·품질 목표는 적용 조직이 승인한 검증 정책으로 정한다"), §7.3(승인 = 게이트 완료), §15(
validation_plan/item·validation_run/result), AT-48.
결정
- 검증 결과가 게이트를 닫는다(§7.3): 승인(
approved)이면VALIDATION게이트를PASSED로, 아니면FAILED로 남긴다. 그래서 AT-48(총평 PASS·필수 FAIL)이 게시 차단으로 이어진다 — 이제 규칙이 아니라 실제 차단이다. - 기준 미확정은 검증 실패가 아니다(구현 중 테스트가 드러낸 구분):
hold인 경우 게이트를FAILED로 만들지 않고 상태를 열어 둔 채 보류 사유만 남긴다. 실패로 만들면 "검증이 실패했다"는 거짓 기록이 남고, 나중에 기준이 확정되었을 때 이력이 오해를 만든다. - 검증 게이트도 직접 닫을 수 없다 — 검증 실행으로만 닫힌다(운영 승인·사전심의와 같은 원칙). 직접 닫는 길을 두면 항목 판정을 건너뛴다.
- 항목 단위로 판정한다: 계획의 항목 정의(필수·절대 차단)와 실행 결과를 합쳐 판정하고, 결과가 없는 항목은
NOT_RUN이다 — 미실행은 통과가 아니다. - 검증 기준의 확정 여부는 활성 정책으로 본다 — 검증 정책(
policy_rule)이 분리되면 그 값으로 좁힌다(D-018 결정 6). - 게이트 근거로 실행 id 를 남긴다: 무엇을 보고 닫았는지 추적할 수 있어야 한다.
implementation_digest(§15 릴리스 필드)는 아직 없다 — 실행이 digest 를 나르므로 AT-49 의 비교는 성립한다. 릴리스 필드로 승격하는 일은 배포 단계에서 한다.
영향
| 영역 | 변경 |
|---|---|
| 포트 | AgentCenterDeps.validation: ValidationStore(3b-1) |
| 유스케이스 | 신규 validationService.ts — createPlan·recordRun·latestFor(§16.3 의 검증 계획·실행 API 대응) |
| 게이트 규칙 | setGateStatus — VALIDATION 직접 닫기 금지 |
| 배선 | 컨테이너에 validationService, 테스트 키트에 validations·recordValidation |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 53 파일 / 458 테스트 통과.- 신규
tests/integration/validationService.test.ts(4) — AT-48 종단(총평 PASS + 필수 FAIL → 게이트FAILED→ 게시GATE_NOT_CLOSED), 직접 닫기·계획 없는 실행 거부, 모두 통과 시 게이트PASSED→ 게시, 기준 미확정은 보류(게이트 열린 채 + 사유). - 기존 AT-72 테스트가 정책 활성화 뒤 검증을 다시 판정하도록 바뀌었다 — 기준이 확정되면 다시 판정하는 것이 §9.1 의 요구다.
남은 것
- 3c: 보안 검토(
security_review/finding)·증빙(evidence_record) 저장소와SECURITY게이트 연결(§9.2 · AT-50), digest 결합(AT-49). - 3d:
DATA_ENVIRONMENT게이트(2e 스냅샷 연결)·운영 계획(operation_plan)·최소 권한(permission_record). - 검증 계획·실행의 화면과 §16.3 라우터 노출(AT-49 의 증빙 관리 포함).
D-028. 검증·보안은 총평이 아니라 항목으로 판정한다 — 절대 차단 조건은 면제하지 않는다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 3 · 3a: 검증·보안 규칙)
- 설계서 원안: §9.1("결과는 PASS / FAIL / NOT_RUN / NOT_APPLICABLE과 근거를 가진다", "총평 PASS만으로 세부 실패를 숨기지 않는다", "정책 미정은 '기준 미확정'으로 표시하고 운영 적용을 보류한다", "실패한 필수 항목은 개선 및 재검증이 필요하다", "예외가 허용되는 항목만 별도 예외 승인으로 처리하고 금지된 데이터 환경 같은 절대 차단 조건은 면제하지 않는다", "증빙이 다른 구성 digest를 대상으로 하면 현재 버전 게이트는 STALE이다"), §9.2("네트워크 통제로 정보유출 검토를 대체하는 경우 적용 환경·통제 범위·검증 결과·확인자·유효기간을 기록한다", "개발자가 '망 차단' 체크 하나로 검토를 면제하지 않는다", "증빙 등록·검토 중·적합·보완·부적합·적용 제외를 구분한다"), AT-48, AT-49, AT-50.
결정
- 판정 단위는 항목이지 총평이 아니다.
evaluateValidation은 필수 항목의FAIL·NOT_RUN을 총평과 무관하게 차단하고, 보고된 총평이 PASS 인데 막는 항목이 있으면summaryMismatch로 어긋남을 드러낸다(§9.1).approved만이 운영 승인의 근거다. - 절대 차단 조건은 예외로도 면제하지 않는다.
absolute: true항목의 실패는 예외 승인이 있어도BLOCKED다 — 예외는 허용되는 항목에만 쓴다(§9.1). - 검증 정책 미정은 "기준 미확정" 보류다(
hold): 차단이 아니라 보류로 구분해, 기준이 정해지면 다시 판정할 수 있게 했다(§9.1). - 미실행은 통과가 아니다. 필수 항목의
NOT_RUN은 차단이다 — 결과 값에NOT_RUN이 있는 이유가 그것이다. - 증빙은 구성 digest 로 신선도를 본다(AT-49): 다른 구성·알 수 없는 구성·만료된 증빙은
STALE이며 재검증을 요구한다. - 네트워크 통제 대체는 요건 5개가 모두 맞을 때만 인정한다(AT-50): 적용 환경·통제 범위·검증 결과·확인자·유효기간. 하나라도 없으면 자동 면제 없음이고, 빠진 항목을 이름으로 알려 준다.
- 대체는 면제가 아니다. 인정된 대체도
substitutedThreats로 기록하고, 검토가 끝나지 않은 위협(EVIDENCE_REGISTERED·IN_REVIEW)은 검토가 아닌 것으로 막는다. - 검증군·보안 결과·위협군 코드는 제안이다 — 설계서가 한국어 이름만 준다(검증 결과 값 집합만 코드로 제시). 미부여 목록에 기록했다.
- 이 조각은 규칙만이다 — 검증 계획/실행/결과·보안 검토·증빙·운영 계획의 저장소와 게이트 연결은 3b 이후다.
영향
| 영역 | 변경 |
|---|---|
| 계약 | VALIDATION_RESULTS(설계서 제시)를 v0.2 "선언만" 구역에서 본문으로 옮김, VALIDATION_CATEGORIES·SECURITY_RESULTS·SECURITY_THREAT_CATEGORIES·SECURITY_SUBSTITUTION_REQUIRED_FIELDS 신규(제안) |
| 도메인 | 신규 server/domain/validation/assessment.ts — evaluateValidation·evaluateEvidenceFreshness, server/domain/security/assessment.ts — evaluateSecurityReview·assessNetworkControlSubstitution·REQUIRED_THREAT_CATEGORIES |
| 미확정 목록 | 검증군·보안 결과·위협군 코드 집합 추가 |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 52 파일 / 453 테스트 통과.- 신규
tests/unit/validation.test.ts(9) — AT-48(총평 PASS + 필수 FAIL → 차단, 어긋남 표시), 예외 승인(근거 기록), 절대 차단 조건 면제 불가, 미실행 차단, 비필수 실패는 비차단, 기준 미확정 보류, AT-49(digest 불일치·미상·만료 → STALE). - 신규
tests/unit/securityReview.test.ts(9) — 3개 위협 커버리지, 증빙 등록·검토 중 차단, 적용 제외 통과, AT-50(기록 없으면 불인정, 빠진 항목 지목, 검증 결과·유효기간 조건, 인정 시 대체 기록).
남은 것
- 3b: 검증 저장소·유스케이스(
validation_plan/item·run/result)와VALIDATION게이트 연결 — AT-48 이 게시를 막는 경로. - 3c: 보안 검토 저장소(
security_review/finding)와SECURITY게이트 연결, 증빙(evidence_record)·digest 결합(AT-49). - 3d:
DATA_ENVIRONMENT게이트(2e 스냅샷 연결)와 운영 계획(operation_plan)·최소 권한(permission_record).
D-027. 평가 스냅샷을 정책 버전에 결합해 저장한다 — D-013 의 이행, 덮어쓰지 않고 쌓는다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 2 · 2e: 평가 스냅샷 저장)
- 설계서 원안: §5.2("평가 결과에는 입력 스냅샷·계산 근거·정책 버전·시각을 저장한다", "정보 부족을 LOW 로 대체하지 않는다"), §5.3, §4.1, §15.3(
risk_assessment·environment_assessment·qualification_assessment), §16.3(평가 3개 API), D-013("평가 스냅샷 저장은 정책 모델과 함께 단계 2에서 도입한다").
결정
- §15.3 의 세 엔터티를 그대로 만든다(마이그레이션
0011):risk_assessments·environment_assessments·qualification_assessments. 필드는 설계서가 나열한 것을 따르고, D-013 이 요구한policy_snapshot_id를 더했다. - 평가는 덮어쓰지 않고 쌓는다.
insert+latest(시각 기준)만 두고update를 두지 않았다 — "무엇으로 판단했는지"가 사라지면 재현할 수 없다(§5.2). - D-013 을 이행했다:
ResolvedPolicy에snapshotId를 더해 평가가 활성 정책 스냅샷 id 를 기록한다. 활성 정책이 없으면null이고 그 판정은 정책에 결합되지 않는다(게시는POLICY_NOT_ACTIVATED로 막힌다). - 계산은 도메인이, 기록은 서비스가 한다.
assessRisk·assessEnvironment·assessQualification을 그대로 부르고 유스케이스는 §5.2 의 네 가지(입력·근거·정책·시각)를 남긴다. 판정 로직을 옮기지 않았다 — 단계 1 의 검증이 그대로 유효하다. - 입력 스냅샷은 해시로 고정한다(
input_hash): 같은 입력으로 다시 계산했는지 비교할 수 있어야 한다. - 미확정을 낮게 추정하지 않는다: 데모 표본은 입력이 비어
INCOMPLETE(Risk)·UNDETERMINED(환경)·UNASSESSED(대상)로 남는다 — 값이 없는데 등급을 만들지 않는다(§5.2). - 게이트 연결은 단계 3 이다.
VALIDATION·SECURITY·DATA_ENVIRONMENT게이트는 그 흐름(검증 계획·보안 검토·데이터 검토)과 함께 붙는다 — 이 조각은 저장소·유스케이스다.
영향
| 영역 | 변경 |
|---|---|
| 계약 | 신규 shared/assessment.ts(RiskAssessmentRecord·EnvironmentAssessmentRecord·QualificationAssessmentRecord) |
| DB | 마이그레이션 0011 — 테이블 3개 + enum 5종 |
| 포트 | AgentCenterDeps.assessments: AssessmentStore(insert 3 + latest 3 + listByRelease) |
| 도메인 | ResolvedPolicy.snapshotId 추가 — 평가를 정책 스냅샷에 결합한다(D-013) |
| 유스케이스 | 신규 assessmentService.ts — assessRiskForRelease·assessEnvironmentForRelease·assessQualificationForRelease·latestFor(§16.3 의 평가 3개 API 대응) |
| 배선 | 컨테이너에 assessmentService |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 50 파일 / 435 테스트 통과(PostgreSQL 21건 — 마이그레이션0011실제 적용).- 신규
tests/integration/assessmentService.test.ts(4) — §5.2 의 네 가지가 남는지, 활성 정책이 없으면 스냅샷 id 가 null, 평가가 쌓이는지(덮어쓰지 않음), 환경·대상 판정 스냅샷과 감사 기록, 없는 버전 거부. tests/integration/postgresStores.test.ts(+1) — 세 평가의 왕복(입력 해시·정책 결합·근거·조건 무손실).
남은 것
- 단계 2 완료 — 2a·2b·2c·2d·2e 가 모두 끝났다.
- 단계 3(검증·보안·데이터·운영 계획·승인): 평가 스냅샷을 게이트로 연결하고(
VALIDATION·SECURITY·DATA_ENVIRONMENT), 검증 계획/실행/결과·보안 검토·증빙·운영 계획을 만든다(AT-48~50). - 화면: 평가 결과 표시(UI), §16.3 의 평가 API 라우터 노출.
D-026. 사전심의를 기존 검토 인프라로 저장하고 게이트를 닫는다 — 새 테이블을 만들지 않는다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 2 · 2d-2: 저장소·게이트 연결)
- 설계서 원안: §15("review_request | kind 확장, case_id, submission_hash, policy_snapshot_id, source_version_manifest, approval_route_snapshot, status 추가·명확화", "review_decision | request_id, gate_id, approval_slot_id, scope_hash, target_id, reviewer_id, decision, reason, external_decision_ref, decided_at"), §16.3(
POST /releases/{id}/pre-review-requests), §8, §7.3, AT-46, AT-47.
결정
- 새 테이블을 만들지 않고 §15 의 확장을 그대로 쓴다: 사전심의는
review_request.kind = PRE_REVIEW로 신청하고,review_decision.scope = PRE_REVIEW로 결과를 남긴다. 결정에는 §15 가 명시한gate_id·external_decision_ref·scope_hash를 더했다(마이그레이션0010). - 값 집합을 v0.2 로 승격한다(§7.4 의 "값을 더한다"): 요청 상태에
REJECTED·INVALIDATED, 결정에REJECT, 종류·범위에PRE_REVIEW. 사전심의 결과가 승인·보완·반려이므로 반려를 표현할 값이 필요했다. - 결과가 게이트를 닫는다(§7.3): 승인 →
PRE_REVIEW게이트PASSED, 보완·반려 →FAILED(게시 차단으로 이어진다 — AT-46). 외부 심의 대체로 승인하면 외부 참조를 게이트 근거(evidence_refs)로 남긴다(AT-47). - Enterprise 는 사전심의 결정으로만, Personal 은 미적용 기록으로만 게이트를 닫는다. 직접 닫는 길을 열어 두면 사전심의가 형식이 된다(운영 승인 게이트와 같은 원칙).
- 외부 심의 대체가 요건 미비면
REQUIRED_FIELDS_MISSING(422) 으로 거부하고, 빠진 항목을fieldErrors로 돌려준다 — 사용자가 무엇을 보완해야 하는지 알 수 있어야 한다. - 기획 스냅샷은 제출 해시로 고정하고, 결정의
scope_hash로 판단한 구성을 남긴다 — 이후 기획이 바뀌면 영향 평가의 근거가 된다(§8). - 테스트 키트가 사전심의를 대행한다(
approvePreReview): Enterprise 는 신청·승인, Personal 은 사유를 남긴 미적용 기록. 실제 흐름이 생기면 키트의 대행이 줄어든다.
영향
| 영역 | 변경 |
|---|---|
| 계약 | REVIEW_REQUEST_KINDS·REVIEW_DECISION_SCOPES·REVIEW_DECISIONS·REVIEW_REQUEST_STATUSES 에 v0.2 값 추가, ReviewDecisionRecord 에 gateId·externalDecisionRef·scopeHash |
| DB | 마이그레이션 0010 — enum 4종 확장 + review_decisions 3개 컬럼 |
| 유스케이스 | 신규 preReviewService.ts — submitPreReview·decidePreReview·applyToGate·impactAssessment |
| 게이트 규칙 | setGateStatus — Enterprise 의 PRE_REVIEW 게이트 직접 닫기 금지 |
| 배선 | 컨테이너에 preReviewService, 테스트 키트에 preReviews·approvePreReview |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 49 파일 / 430 테스트 통과.- 신규 흐름 테스트 — AT-47: Enterprise 확정 뒤 직접 닫기 거부 → 범위가 빠진 외부 심의는 거부(빠진 항목 명시) → 요건이 맞으면 인정되어 게이트가 닫히고 외부 참조가 근거로 남는다(
decision.gateId === gate.id). - AT-46 테스트가 실제 사전심의 흐름으로 바뀌었다 — 반려 기록이 게이트를
FAILED로 만들어 게시를 막는다(이전에는 게이트를 직접FAILED로 설정했다).
남은 것
- 2e: 평가 스냅샷 저장(D-013 이행) — 단계 2 의 마지막 조각.
- 사전심의 화면(UI)과 권한 밖 Agent 세부사항 비노출(§8),
approval_route_snapshot(§15)은 화면·결재 단계에서. - 기획 변경 시 영향 평가를 자동으로 트리거하는 지점(지금은 서비스 API 로 제공).
D-025. 사전심의를 규칙으로 세운다 — 외부 심의 대체는 요건이 모두 맞을 때만, 기술 변경은 재심의 아님
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 2 · 2d-1: 규칙)
- 설계서 원안: §8("Enterprise-Task와 Enterprise-Domain은 기획 후 사전심의를 신청한다", "Personal의 사전심의는 가이드 기본 템플릿에서 미적용으로 기록하되 적용 대상·기본정보 확인은 수행한다", "회사의 기존 과제 심의로 대체할 수 있는 정책은 외부 심의 ID·결과·내용 범위·유효 시점과 동등성 확인을 요구한다", "결과는 승인·보완·반려다", "중복 후보가 있다는 사실만으로 자동 반려하지 않는다", "업무 목적·유형·대상 범위가 실질적으로 바뀌면 사전심의 영향 평가를 수행한다", "기획의 모든 기술 세부 변경마다 자동 전면 재심의를 요구하지 않는다"), AT-47, Q-05.
결정
- 사전심의 적용 여부를 규칙으로 둔다(
preReviewApplicability): Enterprise 는 필요, Personal·미분류는 미적용 — 다만 "적용 대상·기본정보 확인"은 별도로 수행하므로 게이트는 사유를 남겨 닫는다(2b 의 게이트 규칙과 같은 판단). - 외부 심의 대체는 요건이 모두 맞을 때만 인정한다(
evaluateExternalPreReview): ① 정책이 허용하고 ② 외부 심의 ID 가 있고 ③ 결과가 승인이고 ④ 요구 범위를 모두 확인했고 ⑤ 유효 시점이 살아 있고 ⑥ 동등성 확인자와 근거가 있어야 한다. 하나라도 빠지면 대체하지 않고 무엇이 빠졌는지 이름으로 알려 준다. - Q-05 의 미확정 기본값("대체 승인 미허용")을 그대로 구현했다 — 정책이 허용하지 않으면 대체하지 않고 내부 사전심의를 요구한다. 미확정을 관대하게 해석하지 않는다(AT-72 와 같은 원칙).
- 중복 후보만으로 반려할 수 없다(
assertRejectionHasSubstance): 중복이 있으면 재사용·확장·통합 검토 결과가 함께 있어야 반려할 수 있다. 중복은 결론이 아니라 검토의 출발점이다(§8). - 기술 세부 변경만으로는 전면 재심의를 요구하지 않는다(
evaluatePreReviewImpact): 목적·확정 유형·대상 범위의 실질 변경은 영향 평가를 부르고, 모델·Tool·Prompt·digest 같은 기술 변경만 있으면 재심의하지 않는다. 모든 변경에 재심의를 걸면 사전심의가 형식이 된다. - 이 조각은 규칙만이다 — 신청·결과 기록(저장소·유스케이스)과
PRE_REVIEW게이트 연결은 2d-2 다.
영향
| 영역 | 변경 |
|---|---|
| 도메인 | 신규 server/domain/governance/preReview.ts — preReviewApplicability·evaluateExternalPreReview·assertRejectionHasSubstance·evaluatePreReviewImpact |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 49 파일 / 429 테스트 통과.- 신규
tests/unit/preReview.test.ts(9) — 적용 여부(Enterprise/Personal), AT-47(정책 허용 + 동일 범위 + 유효 시점 + 동등성 확인), Q-05(미허용이면 불허), 범위 누락·유효 기간 만료·확인자 누락 사유, 승인 아닌 결과·ID 누락, 중복만으로 반려 금지, 실질 변경 vs 기술 변경.
남은 것
- 2d-2: 사전심의 신청·결과 기록(저장소·유스케이스)과
PRE_REVIEW게이트 연결 — 그러면 키트의 게이트 대행이 하나 더 줄어든다. - 2e: 평가 스냅샷 저장(D-013 이행).
- 사전심의 화면(UI)·권한 밖 Agent 세부사항 비노출(§8)은 화면 단계.
D-024. 운영 승인 게이트는 결재 슬롯으로만 닫힌다 — 두 기록을 한 곳에서 맞춘다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 2 · 2c-3: 게이트 연결)
- 설계서 원안: §7.3("
APPROVED는 … 필수 … 운영 결재 게이트가 완료되었음"), §7.4("법정·직책 결재가 필요한 단계에는 결재 슬롯을 고정한다", "결재 대상자가 해석되지 않으면 제출을 차단", "자동 면제하지 않는다"), §16.4(차단 코드), AT-51·AT-53·AT-76.
결정
- 운영 승인 게이트는 결재 슬롯으로만 닫힌다. 활성 결재 경로 템플릿이 있으면
setGateStatus로 그 게이트를 직접 닫으려 하면INVALID_STATE_TRANSITION으로 거부한다 — 직접 닫는 길을 열어 두면 슬롯 고정이 형식이 된다. - 게이트와 슬롯을 한 곳에서 맞춘다(
syncOperationApprovalGate): 필수 슬롯이 모두 닫히면 게이트를PASSED로(마지막 결재자가 게이트의 결재자), 닫힌 결재가 무효가 되면 게이트를STALE로 되돌리고, 남은 슬롯이 있으면 열어 둔다. 두 기록이 어긋나면 승인의 근거가 흐려진다. - 게시 차단 사유는 저장된 슬롯만 보고 만든다(
approvalSlotBlockersForCase) — 자격 범위·해석 조직은 해석 시점에 슬롯에 새겨지므로 게시 경로가assignees를 알 필요가 없다. 그래서 게시 판정은 "게이트"라는 단일 표면을 유지한다. STALE슬롯은 재결재 대상이다(구현 중 테스트가 잡았다): 무효가 된 결재를 다시 받는 것이 경로 재계산의 목적이므로decideSlot이STALE을 허용한다.- 기본 상태는 "결재 경로 활성" 이다 — 메모리 개발 저장소가 데모 템플릿을 넣고, 차단 재현은
activeApprovalTemplate: false로 한다(정책의activePolicy와 같은 방식). - 데모 템플릿에는 조직 차이 슬롯을 두지 않는다. 배포·운영 조직 정보가 아직 없어(단계 4) "모른다"를 "같다"로 바꾸면 AT-52 의 면제가 되기 때문이다 — AT-52 재현은 테스트가 자기 템플릿으로 한다.
- 테스트 키트가 결재 슬롯을 결재해 게이트를 닫는다(
approveOperationRoute) — 실제로는 운영 조직 결재자가 결재한다. 흐름·화면이 생기면 키트의 대행이 줄어든다.
영향
| 영역 | 변경 |
|---|---|
| 도메인 | ApprovalSlot.required 추가(계약·DB·변환) |
| DB | 마이그레이션 0009 — approval_slots.required |
| 유스케이스 | syncOperationApprovalGate·approvalSlotBlockersForCase(export), decideSlot 이 게이트를 맞추고 STALE 재결재 허용, ensureRoute 도 게이트를 맞춘다 |
| 게이트 규칙 | setGateStatus — 활성 템플릿이 있으면 운영 승인 게이트 직접 닫기 금지 |
| 게시 판정 | gateBlockersForRelease 가 운영 승인 게이트 사유에 결재 슬롯 사유를 붙인다 |
| 저장소 | 메모리 개발 저장소의 데모 결재 템플릿 + activeApprovalTemplate 옵션 |
| 테스트 키트 | attachServices(케이스·결재·deps), approvalAssignees, approveOperationRoute |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 48 파일 / 420 테스트 통과.- 신규 종단 2건(
caseGateService.test.ts) — §7.3·§7.4: 결재 슬롯을 결재하지 않으면 (1) 게이트를 직접 닫을 수 없고 (2) 게시가GATE_NOT_CLOSED로 막히며 차단 사유에 결재 슬롯이 보이고 (3) 경로를 세워 슬롯을 결재하면 게이트가 닫히고 게시가 통과한다. AT-76: 확정 유형이 넓어져 결재가 무효가 되면 게이트도STALE이 되어 게시가 다시 막히고, 재결재하면 게이트가 다시 닫힌다. - 게이트를 직접 닫던 기존 흐름이 드러나 함께 고쳤다(키트가 결재 슬롯으로 닫도록) — 21건이 실패했다가 전부 통과로 돌아왔다.
남은 것
- 2d: Enterprise 사전심의(AT-47)·외부 심의 동등성 —
PRE_REVIEW게이트를 실제 흐름으로 채운다. - 2e: 평가 스냅샷 저장(D-013 이행).
- 운영 승인 요청·결재 화면(§17.1 · UI-18)과 배포 조직 정보(단계 4) — 조직 차이 슬롯이 실제로 쓰이는 시점.
- 관리 단계 전이(
stage)·케이스 종료(closedAt), Q-04 확정 시 조건 좁히기.
D-023. 결재 경로를 저장하고 결재를 기록한다 — 지우지 않고 갱신하고, 미해석은 남긴다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 2 · 2c-2: 저장소·유스케이스)
- 설계서 원안: §7.4(
approval_route_template— 회사·유형·목적·조직 조건·결재 슬롯·순서·동일인/대리 규칙,approval_slot— request_id·role/position·resolved_org·assignee 또는 자격 범위·predecessors·status, "결재 대상자가 해석되지 않으면 제출을 차단", "자동 면제하지 않는다", "반려·무효화 기록은 삭제하지 않는다"), §7.8(트랜잭션 경계), AT-51·AT-52·AT-53·AT-76.
결정
- 템플릿은 정책과 같은 방식으로 다룬다: 초안(
GOVERNANCE_MANAGER) → 활성화(POLICY_APPROVER, 이전 활성은SUPERSEDED). 작성과 승인을 분리한다. - 슬롯은 케이스(버전)에 고정한다. 유형이 확정된 뒤 경로를 세운다 — 제안 유형으로 세우면 확대된 유형의 결재를 건너뛰게 된다(AT-76).
request_id는 슬롯을 만든 요청이 있을 때만 기록한다(§7.4 컬럼 유지). - 경로 재계산은 지우지 않고 갱신한다(§7.4 "무효화 기록은 삭제하지 않는다"):
- 닫힌(통과·대리) 슬롯의 구성 해시가 달라졌으면 →
STALE+ 사유 - 요구하지 않게 된 슬롯 →
NOT_REQUIRED+ 사유 - 새로 요구되는 슬롯 →
PENDING(대상자가 없으면UNRESOLVED)
- 닫힌(통과·대리) 슬롯의 구성 해시가 달라졌으면 →
- 미해석이면 차단하되 슬롯은 남긴다 — 공석을 지우거나 자동 면제하지 않는다(§7.4). 차단은
APPROVAL_ROUTE_UNRESOLVED(422)이고 슬롯별 사유를fieldErrors로 돌려준다. - 대리 결재는 대상자와 대리자 모두 자격 범위 안이어야 하고 사유가 필요하다. Q-04(대결 규칙)가 확정되면 이 조건을 좁힌다.
- 자기 승인 금지는 결재자와 대리 대상자 모두에게 적용한다. 제출자는 검토 요청의
requestedBy에서 온다 — 버전에는 제출자가 없다. isTypeWidened를 도메인으로 올렸다: 제안이UNCLASSIFIED면 비교할 좁은 경로가 없으므로 확대가 아니다(경로는 확정 유형으로 세운다). 제안PERSONAL→ 확정ENTERPRISE_TASK는 확대다.- 게이트 연결은 다음 조각이다 —
decideSlot이 경로를 완료해도 아직OPERATION_APPROVAL게이트를 닫지 않는다(2c-3).
영향
| 영역 | 변경 |
|---|---|
| 계약 | ApprovalSlot 에 caseId·eligible 추가 |
| DB | 신규 approval_route_templates·approval_slots + 마이그레이션 0008(enum 2종) |
| 포트 | AgentCenterDeps.approvals: ApprovalStore(템플릿 5 + 슬롯 4 메서드) |
| 유스케이스 | 신규 approvalService.ts — createTemplateDraft·activateTemplate·currentTemplate·routeContextFor·ensureRoute·routeView·decideSlot |
| 도메인 | isTypeWidened 추가 |
| 배선 | 컨테이너에 approvalService |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 48 파일 / 418 테스트 통과(PostgreSQL 20건 포함).- 신규
tests/integration/approvalService.test.ts(8) — 역할 게이트와 템플릿 활성화(이전 활성 밀어냄), 활성 템플릿 없음 차단, AT-52(조직 다르면 운영 슬롯 추가·자격 범위), AT-53(공석 → 미해석 차단 + 슬롯 보존, 대리 결재 사유 요건), 자기 승인·자격 밖 결재 거부, 경로 완료 판정, AT-76(확대 시 기존 결재STALE+ 확대 슬롯PENDING). tests/unit/approvalRoute.test.ts(+3) —isTypeWidened판정.tests/integration/postgresStores.test.ts(+2) — 템플릿·슬롯 왕복(슬롯 정의·자격 범위 무손실), 슬롯 키 중복 차단.
남은 것
- 2c-3: 경로가 완료되면
OPERATION_APPROVAL게이트를 닫고, 미완료·미해석이면 게시를 막는다. 테스트 키트와 데모 시드에 결재 경로를 넣는다. - 결재 슬롯 조회·결재 화면(§17.1 · UI-18).
- Q-04(직책 매핑·대결·동일인 겸직) 확정 시
resolveApprovalRoute·decideSlot의 조건을 좁힌다. - 관리 단계 전이(
stage)·케이스 종료(closedAt).
D-022. 결재 경로를 해석하고 강제한다 — 슬롯 고정, 자동 면제 금지, 확대 시 재계산
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 2 · 2c-1: 계약·도메인 규칙)
- 설계서 원안: §7.4("동적 검토함은 유지하되 법정·직책 결재가 필요한 단계에는 결재 슬롯을 고정한다", "결재 대상자가 해석되지 않으면
APPROVAL_ROUTE_UNRESOLVED로 제출을 차단한다", "소규모 조직은 승인된 대리 경로를 설정하고 자동 면제하지 않는다", "각 직책의 실제 매핑·순서·병렬 가능 여부·대결·공석 처리·동일인 겸직 규칙은 회사 정책으로 확정한다", "Owner 또는 제출자는 본인의 심의·매핑·검증 최종 승인·운영·폐기 요청을 스스로 승인할 수 없다"), AT-51, AT-52, AT-53, AT-76, Q-04.
결정
- 슬롯은 "역할 + 조직 범위"로 정의하고 자격 범위를 계산한다. §7.4 의
approval_slot은 "assignee 또는 자격 범위" 를 갖는다 — 그래서 템플릿은 역할·조직 조건·순서를 갖고, 해석 결과는 그 슬롯을 결재할 수 있는 사람들(eligible)을 담는다. 실제 지정자는 결재 시점에 정한다. - 회사 정책(Q-04)은 입력으로 받는다. 직책 매핑·대결·동일인 겸직·공석 처리는 회사마다 다르고 Q-04 로 미확정이다 — 코드는 규칙의 뼈대(순서, 조직 조건, 자격 범위)를 제공하고 정책값은 파라미터로 받는다.
- 자동 면제하지 않는다. 요구되는 슬롯에 자격자가 없으면
UNRESOLVED로 남아 승인·제출이 차단된다(§7.4). 대리 결재(DELEGATED)는 기록이 남을 때만 유효하다. - 개발·운영 조직이 다르면 운영 조직 결재를 추가한다(AT-52). 반대로 같으면 추가하지 않고 사유를 남긴다. 다만 배포 조직을 모르면 "다르지 않다"고 단정하지 않고 차단한다 — 모르는 것을 면제로 바꾸지 않는다.
- 확정 유형이 넓어졌을 때만 확대 슬롯을 요구한다(AT-76). 그리고 경로 구성 해시(
approvalRouteHash: 유형·조직·확대 여부·슬롯 키)를 결재 시점에 남겨, 구성이 바뀌면 기존 결재가STALE로 무효가 된다 — 축소 경로 승인으로 확대 경로를 통과할 수 없다. - 자기 승인 금지(§7.4): 제출자·소유자는 자기 요청을 승인할 수 없다(
SELF_APPROVAL_NOT_ALLOWED403). - 오류 코드 두 개를 추가했다 —
APPROVAL_ROUTE_UNRESOLVED(422)와APPROVAL_STALE(409). 둘 다 §16.5 에 이미 있는 코드다. - 결재 슬롯 상태 코드는 제안이다(
PENDING·APPROVED·REJECTED·DELEGATED·UNRESOLVED·STALE·NOT_REQUIRED) — 설계서가 값 집합을 주지 않는다. 미부여 목록에 기록했다. - 저장소·유스케이스·게이트 연결은 다음 조각(2c-2) 이다. 이 조각은 계약·규칙만이라 기존 동작을 바꾸지 않는다.
영향
| 영역 | 변경 |
|---|---|
| 계약 | 신규 shared/approval.ts(ApprovalRouteTemplate·ApprovalSlot) + APPROVAL_SLOT_STATUSES·APPROVAL_ORG_SCOPES(제안) |
| 도메인 | 신규 server/domain/governance/approval.ts — resolveApprovalRoute(경로 해석·미해석), evaluateApprovalRoute(완료 판정·무효 판정), approvalRouteHash·isApprovalStale(AT-76), assertNotSelfApproval, assertRouteResolved |
| 오류 | APPROVAL_ROUTE_UNRESOLVED(422) · APPROVAL_STALE(409) |
| 미확정 목록 | 결재 슬롯 상태 코드 집합 추가 |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 47 파일 / 405 테스트 통과.- 신규
tests/unit/approvalRoute.test.ts(13) — AT-52(조직이 다르면 운영 결재 추가 / 같으면 사유를 남김 / 배포 조직 미상은 차단), AT-51(필수 슬롯 전부 결재해야 완료, 반려·미결재 차단), AT-53(공석 → 미해석 →APPROVAL_ROUTE_UNRESOLVED, 대리 결재 유효), AT-76(확대 슬롯 추가 + 기존 결재STALE), 자기 승인 금지(403), 해시 결정성(순서 무관·조직·확대 여부 반영).
남은 것
- 2c-2:
approval_route_template·approval_slot저장소·유스케이스(템플릿 초안·활성화, 요청 시 슬롯 생성, 결재 기록)와OPERATION_APPROVAL게이트 연결, 그리고 테스트 키트·데모 시드 보강. - 결재 슬롯 조회·결재 화면(§17.1 · UI-18)은 화면 단계.
- Q-04(조직·직책·대결·동일인 겸직)가 확정되면
resolveApprovalRoute의 입력·규칙을 그에 맞춰 좁힌다.
D-021. 게시가 게이트를 요구한다 — "승인 = 필수 게이트 완료"를 실제로 강제한다 (AT-46)
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 2 · 2b-2: 게시 연결)
- 설계서 원안: §7.3("
APPROVED는 … 필수 … 게이트가 완료되었음"), §16.3(GET /lifecycle-cases/{id}= 단계·게이트·차단 사유), §16.4(차단 코드 예시), §16.5(오류), AT-46("사전심의 미완료·반려 → 운영 승인 차단").
결정
- 게시 판정이 게이트 차단 사유를 입력으로 받는다(
PublishReadinessInput.gateBlockers). 도메인 판정 모듈은 케이스 저장소를 알지 않는다 — 케이스 서비스가 사유를 계산해 넘긴다(gateBlockersForRelease). GATE_NOT_CLOSED는 대표 코드 우선순위의 마지막이다. 게이트는 여러 개가 한꺼번에 막히므로 대표로 올리면 "무엇을 먼저 해야 하는가"가 흐려진다. 게이트별 상태는fieldErrors의gates.<GATE_TYPE>로 모두 돌려준다(§16.4 의blockingCodes정신).- 케이스가 없으면 게시할 수 없다(엄격). 게이트 없는 버전은 "필수 게이트가 완료되었음"을 증명할 수 없다. 케이스는 버전 생성 시 함께 열리므로, 없다는 것은 이전 데이터라는 뜻이다(전환 대상).
- 테스트 키트가 필수 게이트를 닫는다(
closeRequiredGates,except로 특정 게이트를 열어 둘 수 있다). 사전심의·검증·보안·데이터·배포 확인 흐름은 아직 없으므로 키트가 "검토가 끝났다"를 대신 만든다 — 흐름이 생기면(2c·2d·3단계) 그 흐름이 게이트를 만들고 키트의 대행은 줄어든다. - 게이트 상태 변경 API 는 노출하지 않는다. §16.3 에 게이트를 직접 바꾸는 경로가 없고, 게이트는 각 흐름(사전심의 신청·검증 결과·보안 검토·배포 확인)이 만든다. 조회(
GET /lifecycle-cases/{id}상당)도 화면 단계에서 라우터에 붙인다 — 서비스는 준비되어 있다. - 데모 시드가 완료된 게이트를 넣는다 — 데모는 "게이트가 완료된 운영 Agent"를 재현한다(합성 데모, §15.1).
영향
| 영역 | 변경 |
|---|---|
| 도메인 | publishReadiness 에 gateBlockers 입력 + GATE_NOT_CLOSED 이슈(우선순위 마지막) |
| 유스케이스 | gateBlockersForRelease(export) — 케이스·계획·scope_hash 를 함께 보고 차단 사유를 계산. agentService 의 게시 전 검토·게시 두 경로에 연결 |
| 시드 | seedClosedCase — 데모 게시 버전의 필수 게이트를 완료 상태로 넣는다 |
| 테스트 키트 | attachCases·actorWithRole·closeRequiredGates, approveEverything 에 except 옵션, setup() 이 케이스 서비스를 함께 반환 |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 46 파일 / 392 테스트 통과.- 신규 종단 2건(
caseGateService.test.ts) — AT-46: 사전심의를 반려로 남기고 나머지 게이트를 닫으면 게시가GATE_NOT_CLOSED로 막히고fieldErrors에gates.PRE_REVIEW가 담긴다(게시 전 검토도 같은 사유를 보여 준다). §7.3: 게이트를 모두 닫으면 같은 버전이 게시된다. - 게이트 강제로 드러난 기존 흐름 8건: 게이트를 닫지 않던 테스트들이 게시에 실패했다. 이것이 AT-46 의 요구이므로 키트가 게이트를 닫도록 고쳤고(§7.3 을 테스트가 명시적으로 수행),
agentFlow·workflowRouter는 게이트 통과 단계를 직접 드러내게 했다.
남은 것
- 2c(결재 슬롯) 가
OPERATION_APPROVAL·RETIREMENT_APPROVAL게이트의 슬롯을 만들고, 2d(사전심의) 가PRE_REVIEW결과를 만든다 — 그러면 키트의 대행이 실제 흐름으로 바뀐다. - 검증·보안·데이터 검토·배포 확인 흐름(3단계)이 나머지 게이트를 만든다.
- 게이트 조회 API·화면(§16.3 · UI-18)은 화면 단계.
- 관리 단계 전이(
stage)와 케이스 종료(closedAt)는 각 흐름이 단계를 옮기는 조각에서 만든다.
D-020. 케이스·게이트를 저장하고 닫는다 — 계획은 확정 상태에서 파생, 담당 역할이 닫는다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 2 · 2b-1b 저장소·유스케이스)
- 설계서 원안: §7.1(
lifecycle_case, 관리 단계), §7.3(APPROVED= 필수 게이트 완료), §7.4(governance_gate—gate_type·scope_hash·policy_rule_id·evidence_refs·게이트 상태·NOT_APPLICABLE요건), §16.3(GET /lifecycle-cases/{id}= 단계·게이트·차단 사유), §16.5(오류), §10.1(검토 주체), §7.8(트랜잭션 경계), §17.1(화면 권한).
결정
- 테이블 두 개를 만든다 —
lifecycle_cases(케이스 종류·관리 단계·기간)와governance_gates(11종·상태·계획 스냅샷·scope_hash·증빙 참조). 마이그레이션0007은drizzle-kit generate로 만들고, 열린 케이스는 Agent·종류마다 하나라는 부분 고유 인덱스만 SQL 로 보완한다(0006 과 같은 방식). - 케이스는 버전이 만들어질 때 함께 열린다(
createAgent). 케이스와 게이트 11종 계획이 같은 트랜잭션에서 생기고 감사에CASE_OPENED가 남는다(§7.8). - 요구 게이트는 저장된 계획이 아니라 확정 상태에서 파생한다(
planGatesFor(kind, approvedType ?? proposedType, applicability)). 저장된required·reason은 "그때 무엇이 계획이었나"의 기록이다. 유형이 확정되면 요구가 달라지므로 저장값을 그대로 쓰면 Enterprise 확정 뒤에도 "Enterprise 가 아님"이라는 사유로 판단하게 된다. - 게이트를 닫는 담당을 역할로 못박는다(
GATE_OWNER_ROLES):PRE_REVIEWER·VALIDATION_REVIEWER·SECURITY_REVIEWER·DATA_REVIEWER·OPERATION_APPROVER·RETIREMENT_APPROVER·AX_REVIEWER·PROCESS_REVIEWER·OPERATOR. 설계서가 "누가 닫는가"를 코드로 주지 않아 §10.1 의 검토 주체를 그대로 대응시켰다.ADMIN예외는 열지 않았다 — 정책 활성화(POLICY_APPROVER)와 같은 원칙이다. scope_hash로 구성 변경을 감지한다. 통과·미적용으로 닫을 때 판단 대상 구성의 해시(유형·적용성·대표 매핑·직접 매핑)를 남기고, 읽는 시점의 구성과 다르면 다시 판단 대상으로 표시한다(STALE). 미적용도 해시를 남긴다 — 유형이 바뀌면 "미적용"이 더 이상 맞지 않을 수 있기 때문이다.- 미적용은 사유·확인자가 있어야 닫히고(§7.4), 닫힌 게이트를 진행 상태로 되돌릴 수 없다(무효화는
STALE, 재검토 부적합은FAILED). GATE_NOT_CLOSED(422) 를 제안 코드로 추가했다 — §16.5 의 422 목록에 게이트 코드가 없는데 §7.3 이 "승인 = 게이트 완료"라고 정의하기 때문이다. 게이트별 상태는 §16.4 예시처럼blockers배열로 담는다. 미부여 목록에 기록했다.- 조회는 열람자(VIEWER)를 제외한다. 세분 권한은 §17.1 화면 매트릭스가 코드가 되는 화면 단계에서 좁힌다.
- 게시 연결은 다음 조각이다. 지금 게이트는 판정 근거와 차단 사유를 제공하지만 게시를 아직 막지 않는다 — 이 조각은 저장소·유스케이스이고,
assertPublishReady에 연결하는 일은 2b-2 로 분리했다(연결하면 기존 흐름·키트·시드가 함께 바뀌어야 한다).
영향
| 영역 | 변경 |
|---|---|
| 계약 | 신규 shared/case.ts(LifecycleCase·GovernanceGate·EvidenceRef·LifecycleCaseView) |
| DB | 신규 lifecycle_cases·governance_gates + 마이그레이션 0007(enum 4종, 부분 고유 인덱스 1종) |
| 포트 | AgentCenterDeps.cases: CaseStore(케이스 4 + 게이트 4 메서드) |
| 도메인 | case.ts 에 GATE_OWNER_ROLES·hashGateScope·isGateScopeStale, evaluateCaseReadiness 에 currentScopeHash(구성 변경 차단) |
| 유스케이스 | 신규 caseService.ts — openCaseForRelease(export: 버전 생성에서 호출)·getCase·setGateStatus |
| 오류 | GATE_NOT_CLOSED(422) |
| 배선 | agentService.createAgent 가 초기 케이스를 함께 연다, 컨테이너에 caseService |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 46 파일 / 390 테스트 통과(PostgreSQL 왕복 18건 포함).- 신규
tests/integration/caseGateService.test.ts(12) — 케이스 개설·게이트 11종, 담당 역할 거부(소유자·AX 검토자), 미적용 요건, 닫힌 게이트 되돌리기 금지, 감사 기록, 열람자 조회 금지, 구성 변경으로 미적용 판단 무효화, Enterprise 확정 뒤 사전심의 미시작 차단(AT-46), 폐기 케이스 계획(§13.2). tests/integration/postgresStores.test.ts(+3) — 케이스·게이트 왕복(scope_hash·증빙 참조 무손실), 열린 케이스 부분 고유 인덱스, 게이트 종류 중복 차단.
남은 것
- 2b-2:
assertPublishReady에 게이트 연결(GATE_NOT_CLOSED) — 테스트 키트가 필수 게이트를 닫고, 데모 시드가 게시 버전의 게이트를 채우고, AT-46 종단(사전심의 반려 → 게시 차단)을 확인한다. - 관리 단계 전이(
stage): 지금은 개설 시PLANNING이고 전이 규칙이 없다 — 각 흐름(사전심의·검증·배포)이 단계를 옮기는 조각에서 만든다. policy_rule_id는 컬럼만 있고 아직 채우지 않는다(policy_rule도입 시점, D-018 결정 6).- 결재 슬롯(2c)·사전심의(2d)·평가 스냅샷(2e)이 각 게이트의 결과를 만드는 흐름이다.
D-019. 케이스·게이트의 필수 게이트를 계산한다 — 11종을 한 번에 도입하고, 미시작도 차단한다
- 일자: 2026-09-22
- 상태: 채택 (v0.2 단계 2 · 2b-1a: 규칙·도메인)
- 설계서 원안: §7.1 관리 단계(그림), §7.3 "
APPROVED는 … 필수 매핑·유형·검증·데이터·보안·운영 결재 게이트가 완료되었음", §7.4 케이스 4종·게이트 11종·게이트 상태 6종("NOT_APPLICABLE에는 정책 근거·사유·확인자가 필요하다"), §8 Personal 사전심의 미적용 기록, §4.5 매핑 미적용 경로, §10.1 운영 승인 제출 요건, §7.2 READY/ACTIVE, §13.2 폐기 승인·정리 완료 분리.
결정
- 11종을 한 번에 도입하고, "이 케이스에서 닫아야 하는가"를 계산한다(사용자 결정). 유형·케이스 종류·적용성에서 요구를 파생시키고, 요구하지 않는 게이트는 근거 문구와 함께 돌려준다.
required는 "닫혀야 하는가" 다. Personal 의 사전심의도required: true이며 사유를 남긴NOT_APPLICABLE로 닫는다 — §8 이 "미적용으로 기록"이라고 했기 때문이다(게이트 자체를 없애지 않는다).- 미시작(
NOT_STARTED)도 차단이다. AT-46 의 "사전심의 미완료·반려 → 운영 승인 차단"에서 미완료를 차단으로 읽었다. - 미적용은 사유와 확인자가 있어야 닫힌 것으로 본다(§7.4). 둘 중 하나라도 없으면 차단 사유로 남는다.
- 닫힌 게이트를 진행 상태로 되돌릴 수 없다. 재검토가 필요하면
STALE(무효)로, 재검토 결과 부적합이면FAILED로 닫는다 — 둘 다 "닫힌 승인이 무효가 되었다"를 드러내므로 허용한다(구현 중 테스트가 잡은 구분). - 관리 단계 코드는 제안이다. §7.1 은 단계를 그림으로만 제시해 영문 코드가 없다 →
shared/openQuestions.ts의 "설계서가 코드를 주지 않은 항목"에 기록했다. - 이 조각은 규칙·도메인만이다. 저장소(
lifecycle_case·governance_gate), 케이스 생성, 게이트 상태 변경 API, 게시 게이트 연결은 다음 조각(2b-1b)이다.
영향
| 영역 | 변경 |
|---|---|
| 계약 | LIFECYCLE_CASE_KINDS·GATE_TYPES·GATE_STATUSES 를 v0.2 "선언만" 구역에서 본문으로 옮김(이제 배선됨) + LIFECYCLE_CASE_STAGES 신규(제안) |
| 도메인 | 신규 server/domain/governance/case.ts — planGatesFor(11종 요구 계산), evaluateCaseReadiness(차단 판정), assertGateStatusChange(상태 변경 규칙), CLOSED_GATE_STATUSES·BLOCKING_GATE_STATUSES |
| 미확정 목록 | 관리 단계 코드 집합을 "설계서가 코드를 주지 않은 항목"에 추가 |
검증
pnpm check·pnpm verify:full통과.TEST_DATABASE_URL=… pnpm test— 45 파일 / 378 테스트 통과.- 신규
tests/unit/caseGate.test.ts(15) — 11종 계산, Enterprise/Personal 사전심의(AT-46), 매핑 미적용 경로(§4.5), 폐기 케이스 게이트 2종(§13.2), 미시작·반려·무효 차단, 사유 없는 미적용 차단, 상태 변경 규칙(STALE·FAILED 허용 / 진행 상태 복귀 금지).
남은 것
- 2b-1b(저장소·배선) 는 D-020 에서 완료했다. 남은 것은 2b-2(게시 연결) 다.
- 게이트의
scope_hash(§7.4)와policy_rule_id·evidence_refs컬럼은 2b-1b 에서 함께 만든다. - 결재 슬롯(2c)이
OPERATION_APPROVAL·RETIREMENT_APPROVAL게이트의 슬롯을 채운다 — 지금은 게이트만 있다. - 사전심의 흐름(2d)이
PRE_REVIEW게이트의 결과를 만든다.
D-071. 통합 탐색의 답변을 켠다 — 검색은 즉시, 답변은 근거 문서로만, 미구성이면 슬롯을 켜지 않는다
- 일자: 2026-09-25
- 상태: 채택 (portal-foundation
docs/EXPLORE_ANSWER_CONVENTION.md§2-4·§2-7 · 설계서 §4.2 권한 범위 · §10.5 오류·빈 상태 · §14.1 비밀 노출 금지) - 배경: D-070 에서 셸 검색창을 색인에 이었지만 답변 슬롯은 꺼 두었다 — 근거 문장 생성은 모델 연계가 필요했고, 빈 슬롯을 켜면 답을 만들 수 있는데 답이 없는 상태로 보인다(§10.5). 규약 §2-7 은 답변 생성을 포털 서버가 ModelGateway 로 하라고 정하고, 플랫폼은 이미
@hohyun94/portal-ai의 OpenAI 호환 provider(createOpenAiCompatibleProvider)를 배포하고 있었다. 남은 문제는 무엇을 근거로 보내는가였다.
검토한 대안
| 대안 | 채택하지 않은 이유 |
|---|---|
| 모델에게 포털 지식을 묻는다(색인 없이) | 권한 범위 밖 내용이 문장으로 새어 나온다 — 화면 필터(§4.2)를 문장이 우회한다. 출처를 댈 수 없어 사용자가 검증할 수 없다 |
| 검색과 답변을 한 요청으로 | 첫 결과가 답변 생성만큼 느려진다(규약 §1 이 onAsk 를 분리한 이유) |
| 답변 슬롯을 항상 켠다 | 모델 연계가 없는 배포에서 눌러도 아무 일이 없는 버튼이 된다 |
| 미구성이면 빈 답변을 돌려준다 | 못 만든 것과 근거가 없는 것이 화면에서 같아진다 — 503(EXPLORE_ANSWER_UNAVAILABLE)으로 구분한다(§19.3) |
| 자체 HTTP 클라이언트 작성 | 같은 게이트웨이에 포털마다 다른 타임아웃·파싱·재시도 관례가 생긴다. 공용 provider 를 쓰고 이 포털은 구성 판정과 귀속 헤더만 소유한다 |
결정
- 검색 먼저, 생성은 그 근거로만 —
server/application/exploreAnswerService.ts가searchExplore(권한 필터 §4.2)로 결과를 얻고, 문서 → 업무 데이터 → 메뉴 순으로 최대 5건을 골라 프롬프트에 넣는다. 같은 종류 안에서는 검색 순위를 유지한다 — 검색 화면과 인용 순서가 갈리면 사용자가 출처를 다시 찾는다. - 근거가 없으면 모델을 부르지 않는다 — 결과 0건이면
reason: NO_EVIDENCE로 답변 없이 돌려준다(규약 §2-4). 호출 자체가 없으므로 근거 없는 문장이 만들어질 경로가 없다. - 인용은 프롬프트에 넣은 근거에서만 만든다 — 모델이 말한 출처를 그대로 옮기지 않는다(검증 불가). 인용이 빈 답변은 셸이 렌더하지 않는다(
toShellExploreAnswer). - 답변은 포털 서버가 게이트웨이로, 비스트리밍으로 —
POST {MODEL_GATEWAY_URL}/v1/chat/completions(OpenAI 호환) ·Authorization: Bearer <MODEL_GATEWAY_SERVICE_TOKEN>· 귀속 헤더x-portal-id: agent-center·x-purpose: explore-answer·x-actor·x-organization-id. 키·프롬프트는 서버에만 둔다(§14.1). 출력 상한 500토큰. - 미구성이면 슬롯을 켜지 않는다 —
explore.capabilities.answer(=AI_ENABLED+ URL·토큰·모델)가 false 면 셸은onExploreAsk를 넘기지 않고 PF 는 질문 버튼을 그리지 않는다. 절차를 직접 부르면 503EXPLORE_ANSWER_UNAVAILABLE이다. AI_ENABLED=true인데 값이 없으면 기동을 막는다 — 반쪽 구성을 조용히 띄우지 않는다(§19.3). 타임아웃은MODEL_GATEWAY_TIMEOUT_MS(기본 20초 — 검색과 달리 문서 독해 뒤에 온다).- 관리자 화면에 구성 여부를 노출한다 — 「연계·동기화」에 「근거 기반 답변」 배지를 두어 켜져 있는지 아닌지를 배포 후 확인할 수 있게 한다(§14.1 — 값이 아니라 구성 여부).
영향
| 영역 | 변경 |
|---|---|
| 서버 | server/adapters/model-gateway/index.ts(공용 provider + 귀속 헤더) · server/application/exploreAnswerService.ts · server/routers/explore.ts(capabilities·ask) · shared/errors.ts EXPLORE_ANSWER_UNAVAILABLE(503) · server/_core/env.ts·container.ts(MODEL_GATEWAY_*·기동 가드) |
| 클라이언트 | client/src/explore/useShellExplore.ts(답변 상태·슬롯 게이팅·toShellExploreAnswer) · client/src/App.tsx(셸 배선) · client/src/pages/admin/AdminSync.tsx(구성 배지) |
| 계약 | docs/integration-contract.md §2-3(이 포털 → ModelGateway) · docs/runbook.md §2-3(실 확인) · .env.example |
| 검증 | tests/unit/modelGateway.test.ts(6) · tests/unit/exploreAnswer.test.ts(6 — 권한 누출 검사 포함) · env.test.ts(+4) · useShellExplore.test.ts(+5) · pnpm verify:full 통과 |
남은 것
- 실 ModelGateway 배포·
purpose매핑·쿼터: 포털 쪽 경로는 구현되어 있고, 게이트웨이 기동·서비스 토큰 합의·purpose매핑(/admin/api)이 남았다. 미구성이면 셸은 질문 버튼을 켜지 않는다(검색은 정상). - AI 기능 대장 등록(규약 §2-7): 이 포털은 그 대장을 아직 소유하지 않는다 — 플랫폼 차원의 등록 절차가 정해지면
explore-answer용도를 함께 올린다. - 답변 품질 측정: 근거 밖으로 나가지 않는 것은 프롬프트·권한 필터로 고정했지만, 요약 정확도는 실 모델로 측정하지 않았다.
- 관리자 셸 검색창(D-070 남은 것)은 D-072 에서 배선했다. ExploreGateway 실 체인 e2e 는 그대로 남아 있다.
D-072. 관리자 셸도 같은 검색창을 쓴다 — 배선만 저장, 색인·권한은 그대로 하나
- 일자: 2026-09-25
- 상태: 채택 (D-070·D-071 남은 것 · portal-foundation
docs/EXPLORE_ANSWER_CONVENTION.md§2-1) - 배경: 관리자 셸(
PortalAdminShell)에는 검색 버튼이 이미 그려지고 있었다 —PortalShellProps를 상속하므로showSearch기본값이 살아 있다. 그런데onSearchSubmit·exploreResults를 넘기지 않아 관리자가Ctrl/⌘ K로 연 창은 관리자 메뉴 바로가기 목록이었고 색인에 이어져 있지 않았다. 셸이 검색 표면을 그려 놓고 창구가 없으면 사용자는 검색이 고장 난 것으로 읽는다.
검토한 대안
| 대안 | 채택하지 않은 이유 |
|---|---|
관리자 셸에서 검색을 숨긴다(showSearch={false}) | 규약 §2-1 은 입력을 셸 상단 검색창·페이지 내 검색창 두 표면으로 못 박는다 — 화면에 따라 창구가 사라지면 하나의 진입점 원칙이 깨진다. 관리자 매뉴얼·관리자 메뉴는 이미 색인에 있다 |
| 관리자 전용 색인을 따로 만든다 | 같은 질의가 셸에 따라 다른 답을 낸다(D-070 이 거부한 이유와 같다). 권한은 서버가 주체 역할로 판정하므로 색인은 하나로 충분하다 |
결정
관리자 분기(client/src/App.tsx)에서 사용자 셸과 같은 useShellExplore() 결과를 PortalAdminShell 에 넘긴다 — onSearchSubmit·exploreResults·onExploreSelect·onExploreAsk·exploreAnswer·exploreAnswerLoading 여섯 슬롯. 역할 판정은 서버가 하므로 ADMIN 은 관리자 절까지 근거로 받고 VIEWER 는 받지 않는다 — 셸이 달라도 규칙은 하나다(§4.2).
영향
| 영역 | 변경 |
|---|---|
| 클라이언트 | client/src/App.tsx(관리자 셸에 탐색 슬롯 6종) · client/src/navigation.test.tsx(관리자 셸 렌더에 검색 표면·탐색 슬롯 실재) |
| 문서 | docs/implementation-status.md(제한·다음 작업) · docs/runbook.md §2-3(관리자 셸 확인) |
검증
pnpm verify:full통과(98파일/710테스트) — 관리자 셸 렌더에서pf-shell-topnav__search를 확인한다.- 화면 조작(다이얼로그 열기·질문 버튼 클릭) 테스트는 저장소의 DOM 제약으로 없다(
docs/implementation-status.md§3 제한 15) — 셸 슬롯 계약은 portal-foundation 의portalShellExplore.contract.test.tsx가 소유한다.
남은 것
- ExploreGateway 실 체인 e2e(카탈로그 등록·서비스 토큰 주입)와 실 ModelGateway 배포(외부).
- 관리자 화면 안의 페이지 내 검색창(
PortalExploreSearch)은 아직 쓰지 않는다 — 화면별 목록 필터가 필요해지면 같은 훅을 재사용한다.
D-073. 감사 이벤트 구성을 한 곳으로 모은다 — 사본 19벌 · 호출 87곳
- 일자: 2026-09-26
- 상태: 채택 (§16.1-9 · §7.7 · §19.3 · AT-33)
- 배경: 감사 이벤트를 적재하는 헬퍼가 19벌 복사되어 있었다 —
recordAudit17벌(서비스마다 하나)과writeAudit2벌(approvalService·caseService에 문자 단위로 동일하고 서로 import 하지 않았다). 사본은 이미 다섯 갈래로 갈려 있었다:before를 넘기는 곳 5 ·beforeSummary: null고정 15 ·objectType을 호출부에서 생략 5 ·requestId전달 1 · 기록 시각을 상태 변경과 같은 값으로 넘기는 인라인 블록 4. 이 상태에서는 식별자 접두사·요약 매핑·기록 시각이 경로마다 어긋날 수 있고, 어긋나면 감사 이력이 "무엇이 일어났는가"에 답하지 못한다.
검토한 대안
| 대안 | 채택하지 않은 이유 |
|---|---|
| 라우터 계층에서만 합친다 | 감사 기록은 서비스(application)의 책임이다 — 라우터는 업무 흐름을 호출하고 이력 남기기는 유스케이스 안에서 일어나야 한다. 라우터로 올리면 서비스 내부 흐름(게이트 동기화 등)이 남기는 이력이 다시 갈라진다 |
| 권한 판정까지 함께 도메인으로 내린다 | 감사 기록과 권한 판정은 변경 이유가 다르다 — 커밋도 나눈다(D-074) |
결정
server/application/audit.ts 의 recordAuditEvent(deps, input) 하나가 이벤트를 만든다.
- 호출부 리터럴이 곧
AuditInput이다 —await recordAuditEvent(deps, { actor, action, objectType, objectId, before?, after?, reason?, requestId?, occurredAt? }). 누락·오타는 tsc 가 잡는다(excess property 검사 포함). - 기록 시각을 인자로 받는다(
occurredAt) — 운영 적용 확인처럼 상태 변경과 감사가 같은 시각을 남겨야 하는 경로는 그 시각을 그대로 쓴다. 감사가 상태보다 뒤에 찍히면 원인과 결과의 순서가 흐려진다(deploymentService·assessmentService의 인라인 블록 4곳). - 행위자는 객체(
{ subjectId })로 받는다 — 문자열subjectId를 직접 넣던 인라인 블록은 헬퍼가actor.subjectId만 읽으므로 다른 필드를 잘못 넣을 자리가 없다. - 반환 객체의
recordAudit노출 5곳은 제거했다 —tests·client참조 0건을 확인했다(서비스 조립은container.ts가deps만 넘긴다).
영향
| 영역 | 변경 |
|---|---|
| 서버 | 신규 server/application/audit.ts · 서비스 18개에서 지역 recordAudit 17벌 · writeAudit 2벌 · AuditInput 2벌 · 별칭 1줄 제거 — 131줄 추가 / 502줄 삭제 |
| 호출부 | await recordAudit( 87곳 → await recordAuditEvent(deps, · objectType 을 생략하던 5개 서비스(identifier·incident·integration·legacy·periodicReview)는 기존 하드코딩 값을 호출부로 옮겨 명시 |
검증
pnpm verify:full통과 — 98 파일 / 710 테스트(통과 676 · skip 34) 로 리팩터 전과 개수·결과가 동일하다.- 감사 필드를 실제로 검증하는 통합 테스트가 그대로 통과한다:
deploymentConfirmation(DEPLOYMENT_CONFIRMED의afterSummary4필드) ·incidentResponse·metricEvaluation·legacyConversion·assessmentService·caseGateService·agentFlow. - 치환 도중 tsc 가 잔여 케이스 4종을 잡았고, 그 목록을 근거로 고쳤다:
writeAudit직접 호출 3 · 인라인 블록의 문자열actor4 · 반환 객체 잔여 2 ·policyService의policyId2.
남은 것
- 감사 조회·보존 정책은 그대로다(변경 없음) — 보존 기간은 사내 정책 확정(§17) 뒤다(D-010).
- 포맷 도구:
prettier를 부분 적용하려 했으나.prettierrc에arrowParens가 없고 저장소 코드는avoid스타일이라,prettier --write이 무관한 줄(row => row.key→(row) => row.key)까지 재포맷한다. 그래서 포맷은 손대지 않았다(implementation-status.md§3-16).
D-074. 접근 판정을 도메인 표로 내린다 — 라우터 가드 22개, 사본은 문자 단위로 같았다
- 일자: 2026-09-26
- 상태: 채택 (§4.2 · §7.4 · §8.2 · §10.1 · §10.3 · §10.4 · §11.1~§11.4 · §12.2 · §13.2 · §16.1-9 · AT-52 · AT-76)
- 배경: 라우터 14개에
assertCan*가드가 22개 흩어져 있었고, 판정 규칙(역할 목록·소유 조직·거부 문구)이 값이 아니라 코드였다. 그래서 같은 판정이 사본으로 갈렸다 —assessment.ts:15와change.ts:16의assertCanAssess는 거부 문구와 오류 field 만 다른 동일한 함수였고,approval.ts와policy.ts의 조회 가드는 역할 집합이 같았으며, "역할 중 하나 또는 소유 조직"이라는 여덟 군데가 각자 배열을 다시 적었다. 새 라우터를 여는 사람은 이 배열을 또 쓰게 되고, 한쪽만 고치면 경로에 따라 같은 행위의 권한이 달라진다. §16.1-9 는 "비즈니스 규칙은 공통 도메인 서비스에서 검증하고 API 로 우회할 수 없게" 하라고 요구한다.
검토한 대안
| 대안 | 채택하지 않은 이유 |
|---|---|
라우터 폴더 안에서만 합친다(routers/guards.ts 에 규칙까지) | 규칙이 여전히 HTTP 경계에 산다 — 서비스나 다른 경로에서 같은 판정이 필요해지면 또 새로 쓰게 된다. GATE_OWNER_ROLES(governance/case.ts)가 "표는 도메인 소유"로 이미 자리 잡아 계보도 어긋난다 |
도메인이 deps 를 받아 읽기까지 한다 | 판정이 흐름에 묶여 다시 갈라진다(§16.1-4 의 "프로세스 레벨·검토자는 서버에서 결정하고 보호한다"는 읽기와 결정의 분리로 지킨다) |
결정
규칙은 도메인 표가, 읽기는 라우터 공용 가드가 소유한다.
server/domain/access/rules.ts—ACCESS_RULES19개 규칙. 축은roles(역할 목록) ·ownerOrg(소유 조직이면 통과) ·message(거부 문구, 설계서 절 포함) ·detail(오류field와 값)이다. 판정은assertAccess(subject, ruleKey, owningOrgId)하나이며 저장소를 읽지 않는다.server/routers/guards.ts— 읽기와 오류 변환만:requireRole·requireOwnership·requireRelease·requireAgent·requireReleaseAccess·requireAgentAccess.NOT_FOUND문구는 서비스와 글자까지 같게 유지한다 — 없는 대상은 어느 경로에서나 같은 404 여야 한다(D-049 의 교훈).- 소유 조직을 모르면 그 값으로 만든 상세를 지어내지 않는다 — 거부 문구·코드만 남기고
fieldErrors는 빈 배열이다. 모르는 것을 허용으로 추정하지 않는 원칙(§21.3)의 거부 쪽 대응이다. - 검사 순서가 원래 다르던 두 경로는 그대로 둔다:
validation은 버전·Agent 를 읽은 뒤 판정하고,security는 버전만 읽고 역할을 본다(소유 조직으로 열지 않는다 — 검토자는 본문으로 지정받지 않는다 §10.4). - 가드가 돌려주던 값(
{release, agent}·event·agent)은 호출부가 쓰지 않는 것을 확인했다. 예외적으로incident는 사건 → 대상(Agent) 읽기가 필요해, 공용 가드를 조합한 지역 헬퍼(requireEventAccess) 하나로 남겼다 — 규칙은 여전히 도메인 표가 소유한다.
영향
| 영역 | 변경 |
|---|---|
| 도메인 | 신규 server/domain/access/rules.ts — 규칙 19개 + assertAccess(판정만, 저장소 접근 없음) |
| 라우터 | 신규 server/routers/guards.ts · 14개 라우터에서 가드 22개 제거, 호출 49곳을 규칙 키로 교체 — 68줄 추가 / 310줄 삭제 · 미사용 import 정리(ApiError·AgentCenterDeps·Actor·Agent, 지나가며 발견한 workflow.ts 의 기존 미사용 ApiError) |
| 검증 | 신규 tests/unit/accessRules.test.ts(10) — 규칙 표를 스토어 fake 없이 판정 |
검증
pnpm verify:full통과 — 99 파일 / 720 테스트(통과 686 · skip 34). 라우터·서비스 통합 테스트가 그대로 통과하므로(그중expectApiError(..., "FORBIDDEN")16곳) 403/404 판정 결과가 리팩터 전과 같다.- 신규 단위 테스트가 결함 하나를 잡았다: 소유 조직을 모르는 상태에서
소유 조직: 확인되지 않음이라는 상세를 지어내고 있었다 → 상세를 만들지 않도록 고쳤다(결정 3). 라우터 통합 테스트로는 잡히지 않던 경로다. - 정리 스크립트가 부분 문자열로 import 를 찾아
periodicReview.ts의requireAgentAccessimport 를 지웠고 tsc 가 즉시 잡았다 — 이후 단어 경계로 고쳤다. 도구가 만든 실수를 게이트가 막은 사례다.
남은 것
- 서비스 계층의 역할 검사 11곳은 그대로다:
agentService.assertCanEdit(4) ·caseService.assertCanView(3) ·retirementService(2) ·policyService.assertRole(1) ·approvalService(1). 흐름에 얽혀 있어(예: 검토 범위에 따라 역할 집합이 달라지는assertReviewerRole(scope)) 이번 커밋에 묶지 않았다 — 같은 표로 옮기면 판정이 완전히 한 곳이 된다. - 403 vs 404 의 정보 노출: 존재하는 대상을 권한 없이 요청하면 여전히 403 이다(존재가 드러난다). §10.5 의 "권한이 없는 객체의 존재를 노출하지 않기 위해 404 로 처리할 수 있다"를 전면 적용하는 일은 별건이다 — 이번 리팩터는 동작을 바꾸지 않았다.
- 화면이 역할 게이트를 클라이언트에서 다시 판단하는 곳(
client/src/navigation.tsx의roles)은 표시용이며 서버 판정을 대신하지 않는다.
D-075. 규칙 표를 서비스 계층까지 넓힌다 — 역할 배열이 남은 곳은 두 곳뿐이다
- 일자: 2026-09-26
- 상태: 채택 (§4.1 · §4.3 · §6.1 · §7.4 · §15.3 · §17.1 · D-074 의 남은 것)
- 배경: D-074 로 라우터 가드 22개는 도메인 표(
ACCESS_RULES)로 갔지만, 서비스 계층에 역할 목록이 코드로 남아 있었다 —agentService.assertCanEdit(업무 책임자 또는 관리자) ·assertReviewerRole(scope)(검토 범위별) ·resumeAgent(관리자) ·caseService.assertCanView(VIEWER 제외 전 역할) ·policyService.assertRole(초안·활성화) ·approvalService.assertRole(템플릿 초안·활성화). 판정이 반쪽만 한 곳에 있으면 "이 포털에서 누가 무엇을 할 수 있는가"를 한 파일에서 읽을 수 없다.
결정
- 표에 축을 하나 늘렸다:
ownerSubject— 주체가 대상의 업무 책임자인가(§4.3). 판정 순서는 역할 → 소유 조직(§4.2) → 업무 책임자(§4.3) 이고, 넘기지 않은 축은 열리지 않는다. - 판정 대상을 객체로 받는다(
AccessTarget): 축이 늘어도 서명이 길어지지 않고, 호출부가 "무엇을 아는지"를 이름으로 밝힌다({ owningOrgId }·{ businessOwnerId }). - 파생 목록을 표가 소유한다:
case.read는PORTAL_ROLES에서 VIEWER 를 뺀 목록을 계산해서 쓴다 — 손으로 적으면 새 역할이 조용히 빠진다. - 문구는 글자까지 보존했다. 옮겨 온 문구들은 설계서 절을 가리키지 않던 것들이므로, 테스트가 "절을 안 가리키는 문구 = 옮겨 온 9개" 를 못 박는다 — 새 규칙이 절 없이 들어오거나 문구가 조용히 바뀌면 막힌다. 문구에 절을 붙이는 일은 문구 변경이라 별건이다.
영향
| 영역 | 변경 |
|---|---|
| 도메인 | server/domain/access/rules.ts — 규칙 19 → 28개, AccessSubject 에 subjectId 추가, AccessTarget 축 도입 |
| 서비스 | agentService(편집 6곳 · 검토 결정 · 중지 해제) · caseService(조회 2곳 + CASE_VIEW_ROLES 상수 제거) · policyService(2곳) · approvalService(2곳) — 지역 헬퍼 6개 제거, assertCanEdit·assertReviewerRole 의 미사용 export 제거 |
| 검증 | tests/unit/accessRules.test.ts 10 → 15건 — 업무 책임자 축 · 검토 범위 분리 · 작성자≠활성화자 · 파생 목록 |
검증
pnpm verify:full통과 — 99 파일 / 725 테스트(통과 691 · skip 34). 라우터·서비스 통합 테스트가 그대로 통과한다(문구를 글자까지 보존했다).- 치환 중 tsc 가 잡은 것: 반환 객체에 남은 죽은
assertReviewerRole항목,guards.ts의 중복 import, 그리고 앞 단계의assertAccess서명 변경 누락. 도구가 만든 실수를 타입 검사가 매번 막았다.
남은 것
retirementService.canAssess하나만 남았다 — 폐기 후보 목록을 거르는 불리언 판정이고 조건이 합성이다(소유 조직 **이면서** VIEWER 아닌 역할 보유). 표에 boolean 판정(decideAccess)과 "소유 조직 + 역할" 축을 더하면 옮길 수 있지만, 지금 두 축(roles·ownerOrg)으로는 표현되지 않아 추측으로 축을 만들지 않았다.agentService의actorIsOwnerOrAdmin(도메인 판정 함수에 넘기는 입력값)과caseService의GATE_OWNER_ROLES사용은 접근 검사가 아니라 이미 도메인이 소유한 규칙이다 — 표로 옮길 대상이 아니다.
D-076. 정책 템플릿 화면을 붙인다 — 초안·활성화를 사용자 경로로 (UI-18)
- 일자: 2026-09-27
- 상태: 채택 (§0.1 · §15.3 · §15.5 · §17.2 UI-18 · §21.3 · AT-72 · D-063 의 남은 것 = 화면)
- 배경:
policy.*API(D-063)는 초안·활성화·스냅샷을 노출하고 있었지만 화면이 없었다. 그래서 관리자는 "지금 효력을 갖는 규칙"(UI-06 분류 정책, 읽기 전용)만 볼 수 있었고 정책을 활성화하려면 API 를 직접 불러야 했다. 활성 정책이 없으면 신규 운영 승인이POLICY_NOT_ACTIVATED(422) 로 차단되므로(§21.3), 화면의 부재는 막힌 상태를 풀 수단이 없는 상태였다.
검토한 대안
| 대안 | 채택하지 않은 이유 |
|---|---|
기존 AdminPolicy(UI-06)에 초안·활성화를 얹는다 | 그 화면은 지금 효력을 갖는 규칙을 읽는 자리이고 문구·테스트가 그 계약에 맞춰져 있다. 읽기와 쓰기를 한 화면에 섞으면 "여기 보이는 값이 곧 효력"이라는 성질이 흐려진다 |
| 규칙 값 편집(허용 레벨·예외 허용)까지 함께 붙인다 | 사내 값이 확정되기 전이라(§17) 화면이 값을 만들면 지어낸 값이 효력을 갖는다. 초안은 가이드 템플릿에서 출발시키고 편집은 확정 뒤로 미룬다(§21.3) |
결정
- 메뉴 행과 화면을 하나 더 둔다:
admin-policy-template(/admin/policy-template, 라벨 "정책 템플릿", UI-18). 메뉴 정의 · 라우팅 표 · 화면 레지스트리 ·AdminRoutes네 곳이 같은 id 를 쓴다(경로의 단일 원천은 메뉴 정의 — 규약 §2.4). - 화면이 하는 일은 셋: ① 현재 활성 스냅샷(버전 · 해시 앞 12자 · 활성자) 또는 "활성 정책 없음 → 게시가 422 로 막힌다"는 경고, ② 정책 버전 목록(초안·활성·대체 이력), ③ 초안 만들기(버전만)와 활성화(정책 id·사유).
- 규칙을 화면이 만들지 않는다: 초안 생성은
rules를 넘기지 않아 라우터가 가이드 템플릿에서 출발시키고, 화면은 "템플릿은 효력이 없다"를 문구로 밝힌다(§0.1). "초안만 활성화할 수 있다"는 규칙은 서버가 강제하며(도메인assertPolicyActivatable) 화면은 그 사실을 안내한다. - 상태 코드를 그대로 표시한다:
DRAFT·ACTIVE·SUPERSEDED는 감사 로그·문서가 쓰는 값이라 표시 변환이 낱말을 새로 만들지 않고 색만 더한다.
영향
| 영역 | 변경 |
|---|---|
| 화면 | 신규 client/src/pages/admin/AdminGovernancePolicy.tsx · client/src/pages/admin/presenters.ts(toGovernancePolicyRows·shortenHash) |
| 배선 | shared/const.ts(ADMIN_MENU_PORTAL 행 추가) · client/src/const.ts(UI-18) · client/src/App.tsx(AdminRoutes) |
| 매뉴얼 | shared/manual.ts — 관리자 매뉴얼에 "정책 템플릿" 절 추가, "다섯 화면"→"여섯 화면", "결재 템플릿이 유일한 쓰기 화면"이라는 서술 정정 |
| 검증 | pages.test.tsx(스모크 1건) · presenters.test.ts(2건) · const.test.ts(관리자 화면 5종) · policyRouter.test.ts(초안이 아닌 정책 활성화 거부 1건) |
검증
pnpm verify:full통과 — 99 파일 / 729 테스트(통과 695 · skip 34).- 조회 권한 게이트(
policy.list·policy.get→ 정책 역할 밖 403)는 이미policyRouter.test.ts가 덮고 있었다 — 새 테스트를 만들지 않고 확인만 했다(같은 것을 두 번 검증하지 않는다). - 화면이 안내하는 "초안(DRAFT)만 활성화" 규칙이 API 경로에서도 막히는지는 비어 있어 1건 추가했다: 활성 정책 재활성화와 밀려난 정책 되살리기가 모두 거부된다.
남은 것
- 규칙 값 편집(허용 레벨·예외 허용·위험 행렬·환경 규칙)은 사내 확정(§17) 뒤다 — 지금 초안은 가이드 템플릿 값으로 만들어진다.
- 시행 시점(
effectiveFrom) 입력은 화면에 없다 — API 는 받고 라우터 기본값은null(활성화 시각)이다. "시행 예약"이 필요해지면 붙인다. - 스냅샷 이력 화면은 없다 — 현재 스냅샷 하나만 보여 준다(버전 비교(UI-09)와 함께 판단).
D-077. 타입 검사 게이트를 강화한다 — 의존성 없이 죽은 코드를 잡는다
- 일자: 2026-09-27
- 상태: 채택 (P3 중 의존성 없이 가능한 몫 · D-073~D-075 의 실수에서 나온 교훈)
- 배경: 감사 헬퍼·접근 판정 리팩터(D-073~D-075)에서 도구가 만든 실수가 반복됐다 — 정리 스크립트가
requireAgentAccessimport 를 지웠고(tsc 가 잡았다), 반환 객체에 죽은 항목이 남았고(tsc 가 잡았다), 미사용 import 가 남았다(tsc 가 못 잡았다 —noUnusedLocals가 꺼져 있었다). 이 저장소의 게이트는 "타입은 보지만 쓰지 않는 선언은 보지 않는" 상태였고, 예정된 기계적 분할(레지스트리 파일 분할)에서 같은 부류의 잔여물이 생길 것이 확실했다. 그 잔여물은 컴파일도 테스트도 통과한다 — 사람이 읽어야 발견된다.
검토한 대안
| 대안 | 채택하지 않은 이유 |
|---|---|
| ESLint 를 도입한다(원래 P3 안) | 미사용 선언은 tsc 플래그로 의존성 없이 잡힌다. ESLint 가 추가로 주는 것은 순환 의존·hooks 규칙인데 이 저장소에 그 위반이 있는지 측정하지 않았다 — 측정 없이 의존성과 CI 시간을 늘리지 않는다. ESLint 10 과 typescript-eslint 의 호환 조합도 확인이 필요하다 |
| 그대로 둔다 | 앞으로의 기계적 분할에서 죽은 import·미사용 지역이 조용히 쌓인다. 그 비용은 리뷰어의 눈에만 달려 있다 |
결정
tsconfig.json 에 noUnusedLocals·noUnusedParameters 를 켠다(이미 strict 는 켜져 있었다). CI(verify.yml)는 pnpm check 를 돌리므로 원격에서도 같은 게이트가 선다 — CI 파일은 손대지 않았다.
영향
| 영역 | 변경 |
|---|---|
| 설정 | tsconfig.json — 플래그 2개. 의존성 변화 없음(package.json·락파일 무변경) |
| 죽은 코드 8건 정리 | ChangeImpact.tsx(CHANGE_KINDS) · Retirements.tsx(PortalTableColumn, 쓰지 않는 onNotice prop → 인자와 호출부에서 제거) · drizzle/schema.ts(RetirementTask) · agentService.ts(AgentQualification, assertSingleWorkingRelease) · reviews/cadence.ts(REVIEW_CADENCES) · postgres/stores.ts(ReviewStore) |
검증
- 플래그를 켜고 측정한 결과 기존 위반은 8건뿐이었고 모두 죽은 선언이었다(동작 영향 없음).
pnpm verify:full통과 — 100 파일 / 729 테스트(통과 729 · skip 0), 빌드 통과. 이 실행은TEST_DATABASE_URL을 준 상태라 그동안 skip 이던 PostgreSQL 통합 34건까지 실제로 돌았다(로컬agent_center_test).
남은 것
- 순환 의존·
react-hooks규칙은 여전히 게이트가 없다 — 도입 여부를 정하기 전에 위반이 실제로 있는지 측정한다(§4-11). noUnusedParameters는 밑줄 접두사(_param)를 허용하므로, 의도적으로 쓰지 않는 인자는 그 관례로 남긴다(지금 그런 자리는 없다).
D-078. 레지스트리 파일을 모듈 묶음으로 나눈다 — 포트부터 (P2 1단계)
- 일자: 2026-09-27
- 상태: 채택 (P2 1/4 — 코드 진단에서 나온 권고)
- 배경: 최근 200커밋의 파일 변경 터치 741건 중 171건(23%)이 7개 파일에 몰려 있었다 —
drizzle/schema.ts(28) ·postgres/stores.ts(26) ·memory/stores.ts(25) ·container.ts(25) ·domain/ports.ts(24) ·routers.ts(22) ·tests/support/agentTestKit.ts(21). 기능 하나를 더할 때마다 같은 파일 끝을 열어야 하고 여러 작업이 같은 자리에서 충돌한다. 포트는 그중 가장 작고 경계가 분명한 파일이다(358줄 · 인터페이스 22개) — 먼저 여기서 방법을 검증한다.
결정
server/domain/ports.ts→server/domain/ports/모듈 묶음으로 나눈다:agents(등록·매핑·검토) ·governance(정책·결재·케이스) ·quality(평가·검증·보안) ·operations(운영 6종) ·lifecycle(변경·폐기) ·integration(연계·감사·알림) ·runtime(시계·식별자) ·errors(낙관적 잠금 충돌) ·index(재수출 +AgentCenterDeps).- 인터페이스는 글자 그대로 옮긴다 — 이 커밋은 이동이다. 규칙·주석·이름을 함께 고치지 않는다(바꿀 것이 보이면 따로 한다).
- 바깥 경로를 바꾸지 않는다:
ports/index.ts가 전부 재수출하므로 40여 곳의from "../domain/ports"가 그대로 동작한다. 이동의 위험을 "경로 수정 누락"에서 "재수출 누락"으로 옮기는데, 후자는 tsc 가 즉시 잡는다. AgentCenterDeps(서비스가 받는 타입)는index.ts에 남긴다 — 이것은 저장소 전체의 목록이라 어차피 기능마다 한 줄이 늘어난다. 숨기지 않고 한 곳에 둔다.
영향
| 영역 | 변경 |
|---|---|
| 도메인 | ports.ts(358줄) 삭제 → ports/ 9파일(합계 동일 + 묶음별 머리 주석) |
| 다른 코드 | import 경로 변경 없음 — memory/stores.ts 에서 파일 경로를 가리키던 주석 1줄만 갱신 |
검증
pnpm verify:full통과 — 100 파일 / 729 테스트(729 통과 · skip 0), 빌드 통과.TEST_DATABASE_URL을 준 상태라 PostgreSQL 통합 34건이 포함된다.- 분할 중 D-077(
noUnusedLocals)이 곧바로 값을 했다: 묶음 파일에 원본 import 를 그대로 옮기면 미사용 import 가 남는데 tsc 가 그것을 잡는다 — 이 커밋을 안전하게 만든 것이 D-077 이다.
남은 것 (P2 2~4단계)
server/infrastructure/memory/stores.ts(1046줄) ·server/infrastructure/postgres/stores.ts(2063줄) — 같은 방식으로 분할한다. PostgreSQL 통합 34건이 실제로 돌아가므로(2026-09-27 확인) 회귀를 잡을 수 있다.drizzle/schema.ts(1467줄 · 테이블 42개) — 분할하려면drizzle.config.ts의schema를 디렉터리·글로브로 바꿔야 한다.pnpm db:generate가 빈 diff 를 내는지로 동등성을 검증한다(마이그레이션을 만들지 않는다).server/routers.ts(502줄) — 라우터 조립과 카탈로그 조회 로직이 섞여 있다. 조회 로직을 서비스로 내리는 일은 이동이 아니라 판단 이동이라 별도 결정이 필요하다.tests/support/agentTestKit.ts(594줄)는 테스트 키트라 급하지 않다.
D-079. 스키마도 모듈 묶음으로 나눈다 — 설정 1줄 변경 + 무diff 검증 (P2 2단계)
- 일자: 2026-09-27
- 상태: 채택 (P2 2/4 · D-078 의 남은 것)
- 배경:
drizzle/schema.ts는 1467줄 · 테이블 42개 · pgEnum 64개로 저장소에서 가장 자주 바뀌는 파일이었다(최근 200커밋 중 28회 변경). 테이블 하나를 더하려면 1400줄 파일 끝을 열어야 한다.
분할 전에 측정한 구조
- enum 64개가 테이블 사이에 섞여 있다(예: 1147~1149줄).
- 모든 테이블이 공유하는 상수
stamps(created_at·updated_at·row_version)가 하나 있다. - 테이블 간 참조
.references(() => X.id)가 52곳 — 파일을 나누면 파일 간 import 가 필요하다. - 스키마를 읽는 곳은 2곳뿐이다(
postgres/stores.ts·postgres/connection.ts) +drizzle.config.ts의 schema 경로.
결정
drizzle/schema.ts→drizzle/schema/9파일:enums(64) ·stamps(공유 상수) ·agents(9테이블) ·governance(6) ·quality(7) ·operations(11) ·lifecycle(4) ·integration(5) +index(재수출).- 선언은 글자 그대로 옮긴다 — 위치만 바뀐다. import 는 그 파일이 실제로 쓰는 심볼만 남겨 다시 만들고, 참조하는 테이블은 자동으로 import 를 붙인다(52곳).
drizzle.config.ts의schema를./drizzle/schema/index.ts로 바꾼다 — drizzle-kit 은 import 를 따라 읽는다.postgres/stores.ts·connection.ts의 import 경로는 그대로다(디렉터리 index 재수출). 이 커밋에서 바뀐 설정은 한 줄이다.
영향
| 영역 | 변경 |
|---|---|
| 스키마 | drizzle/schema.ts(1467줄) 삭제 → schema/ 9파일(합계 동일 + 묶음별 머리 주석) |
| 설정 | drizzle.config.ts — schema 경로 1줄 |
검증 (동등성)
pnpm db:generate→No schema changes, nothing to migrate, 새 마이그레이션 없음 — drizzle-kit 이 42테이블·64enum·컬럼·인덱스·FK 를 같은 것으로 읽었다(구조 동등성의 직접 증거).pnpm verify:full통과 — 100 파일 / 729 테스트(729 통과 · skip 0), 빌드 통과.TEST_DATABASE_URL을 준 상태라 PostgreSQL 통합 34건이 마이그레이션을 적용하고 실제 저장소를 돌린다.
남은 것 (P2 3~4단계)
server/infrastructure/memory/stores.ts(1046줄) — 컬렉션 37개를 클로저로 공유하고snapshot/restore가 전체를 복제한다.state.ts로 컬렉션을 빼고 참조를state.*로 바꾸는 작업이 필요하다(단순 이동이 아니다).server/infrastructure/postgres/stores.ts(2063줄) — 같은 방식 + 스키마 테이블 import 재배치. PG 통합 34건이 검증한다.server/routers.ts(502줄) — 조립과 카탈로그 조회 로직이 섞여 있어 판단 이동이 필요하다(별도 결정).
D-080. 인메모리 저장소도 모듈 묶음으로 나눈다 — 공유 상태는 구조분해로 (P2 3단계)
- 일자: 2026-09-27
- 상태: 채택 (P2 3/4 · D-078·D-079 의 남은 것)
- 배경:
server/infrastructure/memory/stores.ts는 1046줄로 최근 200커밋에서 25회 바뀐 상위 파일이었다. 포트(D-078)·스키마(D-079)와 달리 단순 이동이 아니었다: 컬렉션 37개를 클로저로 공유하고,snapshot/restore가 전체를 복제하며, 기본 시드(정책·결재 템플릿)까지 같은 클로저에 있었다.
결정
- 공유 상태를
state.ts로 뺀다: 컬렉션 37개 +nowIso·ids+snapshot/restore+ 헬퍼(latestOf·commit·cloneMap·Stamped).createMemoryState(options)가 state 를 만들고MemoryState가 그 타입이다. - 저장소 본문은 글자 그대로 옮긴다 — 참조를 고치지 않는다: 각 묶음이 필요한 이름만 구조분해한다(
const { agentRows, decisionRows, nowIso } = state;). 이 선택이 이 단계를 안전하게 만든 핵심이다 — 1000줄의 참조를 손으로 고치는 대신 구조분해 목록만 기계적으로 만들면 된다. - 반환 타입을
Pick<AgentCenterDeps, ...>로 밝힌다: 원래는const deps: AgentCenterDeps = {...}리터럴이 메서드 파라미터에 문맥 타입을 주고 있었다. 그 타입이 사라지면agent·id같은 파라미터가any가 되므로(TS7006) 묶음마다 자기 키만Pick으로 명시한다. - 시드는 governance 묶음에 둔다(
seedGovernanceDefaults): 정책·결재 템플릿 기본값은 거버넌스 저장소의 초기 상태다. index.ts(조립)만AgentCenterDeps를 만든다.policy/cases/approvals는 거버넌스 묶음을 한 번 만들어 참조한다 —createPolicyResolver(governance.policy)가 같은 인스턴스를 쓰게 하기 위해서다.
영향
| 영역 | 변경 |
|---|---|
| 인프라 | stores.ts(1046줄) 삭제 → stores/ 8파일 1207줄(state 286 · operations 242 · governance 242 · agents 132 · quality 91 · lifecycle 85 · index 67 · integration 62) |
| 다른 코드 | import 경로 변경 없음 — createInMemoryAgentCenterDeps 를 stores/index.ts 가 그대로 내보낸다 |
검증
pnpm verify:full통과 — 100 파일 / 729 테스트(729 통과 · skip 0), 빌드 통과.TEST_DATABASE_URL을 준 상태라 PostgreSQL 통합 34건이 포함되고, 인메모리 저장소는 거의 모든 테스트가 지나는 경로라 회귀가 곧바로 드러난다(트랜잭션 롤백·낙관적 잠금 테스트 포함).- 분할 중 tsc 가 잡은 것: 헬퍼
export누락,Pick유니온 구분자,ids가 메서드 파라미터 이름과 겹치는 문제(구조분해 대상에서 제외), extras 경로 한 단계 누락. D-077 게이트가 없었으면 미사용 구조분해가 조용히 남았을 것이다.
남은 것 (P2 4단계)
server/infrastructure/postgres/stores.ts(2063줄) — 같은 방식(공유 커넥션·헬퍼는 state, 묶음은 테이블 단위). PG 통합 34건이 검증한다.server/routers.ts(502줄) — 조립과 카탈로그 조회 로직이 섞여 있어 판단 이동이 필요하다(별도 결정).
D-081. PG 저장소도 모듈 묶음으로 나눈다 — current() 를 파라미터로 (P2 4단계)
- 일자: 2026-09-27
- 상태: 채택 (P2 4/4 · D-078~D-080 의 남은 것)
- 배경:
server/infrastructure/postgres/stores.ts는 2062줄로 최근 200커밋에서 26회 바뀐 파일이었다(저장소에서 두 번째로 잦은 변경). 구조는 인메모리와 달랐다: 행↔도메인 매퍼 73개(xFromRow/xValues)가 모듈 스코프에 있고, 그 아래 공장 안에 타입 붙은 store const 15개 + 인라인 store 7개(agents·releases·mappings·mappingHealth·reviews·audit·notifications) +clock·ids·policies·transaction이 있었다.
결정
- 매퍼는 모듈 파일로, 저장소는
current()를 받는 공장으로. 저장소 본문은 글자 그대로 옮긴다 —current()가 파라미터가 되므로 본문의 호출이 그대로 동작한다(D-080 과 같은 방식). - 공용은
shared.ts로:Executor타입 ·transactionContext(AsyncLocalStorage) ·toIso/toDate/isUniqueViolation·CreatePostgresDepsOptions+createExecutor(connection). 트랜잭션 전파(§7.8)가 한 곳에 남는다. - 반환 타입은
Pick<AgentCenterDeps, ...>— 인메모리와 같은 이유(원래 팩토리 안의 타입 주석이 메서드 파라미터에 문맥 타입을 주고 있었다). index.ts는 조립만:clock·ids·policies(같은 정책 인스턴스 공유)·transaction+ 묶음 스프레드.
영향
| 영역 | 변경 |
|---|---|
| 인프라 | postgres/stores.ts(2062줄) 삭제 → postgres/stores/ 8파일 2099줄(operations 603 · agents 376 · governance 374 · quality 310 · lifecycle 232 · integration 135 · index 40 · shared 29) |
| 다른 코드 | import 경로 변경 없음 — createPostgresAgentCenterDeps 를 stores/index.ts 가 그대로 내보낸다 |
검증
pnpm verify:full통과 — 100 파일 / 729 테스트(729 통과 · skip 0), 빌드 통과.TEST_DATABASE_URL을 준 상태라 PostgreSQL 통합 34건이 분할된 저장소를 실제 DB 로 돌린다(마이그레이션·jsonb 왕복·낙관적 잠금·dedupe·트랜잭션 롤백).- 분할 중 tsc 가 잡은 스크립트 결함 4종: ① 공장이
const deps가 아니라return {직접 반환이었다 ② 이름 붙은 store const 파서 누락 ③code_only가 주석 안의 따옴표를 문자열 시작으로 오인해 뒤 코드를 잘라냄(주석을 먼저 지우도록 수정) ④ 인라인type X표기 때문에 심볼 매칭 실패. 모두 게이트가 잡았고, 사람이 눈으로 찾아야 하는 종류가 아니었다 — D-077 의 게이트가 값을 한 또 한 번의 사례다.
남은 것 (P2 잔여)
server/routers.ts(502줄) — 라우터 조립과 카탈로그 조회 로직이 섞여 있어 판단 이동(로직 소유권)이 필요하다. 파일 이동이 아니므로 별도 결정 대상이다.
D-082. 공용 타입을 도메인으로 옮겨 순환 의존을 없앤다 — 그리고 게이트를 세운다 (P3 나머지)
일자: 2026-09-27
상태: 채택 (§4-11 의 미측정 항목을 측정으로 확정 · D-077 의 게이트 강화 계보)
배경: §4-11 에 "순환 의존은 게이트가 없다 — 도입 여부를 정하기 전에 위반이 실제로 있는지 측정한다"고 적어 두었다. 그래서 상대 import 그래프를 만들어 재 봤다: 파일 189개 · 사이클 2개.
agentService → caseService → agentServiceagentService → caseService → approvalService → operationService → agentService
원인은 하나였다:
Actor타입이agentService안에 정의되어 15개 서비스가 그 파일을 import 했고, 그 타입 전용 간선이 실제 값 의존(agentService → caseService.openCaseForRelease등)과 합쳐져 원을 이뤘다. 즉 "공용 타입이 서비스 안에 산다"는 배치 문제였다.
결정
- 공용 타입은 도메인이 소유한다:
Actor를server/domain/access/rules.ts로 옮긴다(D-074 가 만든AccessSubject와 같은 모양이라AccessSubject = Actor별칭으로 두 이름을 모두 유지한다). 서비스·라우터·시드·테스트 키트 19개 파일의 import 를 도메인으로 돌린다. - 순환을 상시 게이트로 막는다:
tests/unit/moduleGraph.test.ts— 상대 import 그래프를 만들어 사이클이 있으면 경로를 그대로 보여주며 실패한다. 의존성 추가 없이 기존pnpm test안에서 돈다. - 타입 전용 import 도 센다: 런타임에 지워지는 간선이라 위험은 낮지만 이번 순환이 정확히 그 종류였다 — 타입이 잘못된 자리에 있으면 값 의존이 한 방향이어도 그래프가 원을 이룬다. 스캔이 조용히 비지 않는지(파일 수 하한)도 함께 검증한다.
영향
| 영역 | 변경 |
|---|---|
| 도메인 | domain/access/rules.ts — Actor 정의 + AccessSubject 별칭(정의는 한 곳) |
| 서비스·라우터 | agentService 의 Actor 정의 제거 · 19개 파일이 도메인에서 import · 미사용이 된 PortalRole 정리 |
| 검증 | 신규 tests/unit/moduleGraph.test.ts(2) — 순환 0 + 스캔 하한 |
검증
- 재측정: 파일 189개 · 사이클 0개.
pnpm verify:full통과 — 101 파일 / 731 테스트(731 통과 · skip 0), 빌드 통과(PostgreSQL 통합 34건 포함).- 옮긴 뒤
PortalRole이 미사용이 된 것을 tsc 가 즉시 잡았다(D-077) — 타입 이동이 남기는 잔여물을 사람이 찾지 않아도 된다.
남은 것
react-hooks규칙은 별도 도구(ESLint)가 필요하다. 다만 이 저장소의 클라이언트는 훅을 소수 파일에만 두고 있어 위반 위험이 낮고, 위반이 생기기 전에 도구를 늘리지 않는다는 원칙(§4-11)을 유지한다.ApprovalActor·PolicyActor도 같은 모양({subjectId, orgId, roles})을 각자 정의하고 있다 — 지금은 순환이 없으므로 급하지 않다. 통합하면 세 벌이 한 벌이 된다(별건).
D-083. 같은 모양의 행위자 타입 세 벌을 한 벌로 모은다
- 일자: 2026-09-27
- 상태: 채택 (D-082 의 남은 것)
- 배경:
Actor(도메인 · D-082 에서 옮김) ·ApprovalActor(approvalService) ·PolicyActor(policyService) 가 같은 모양({ subjectId, orgId, roles })을 세 곳에 각자 정의하고 있었다. 모양이 같은데 이름이 셋이면 "무엇이 같은 것인가"가 코드에 드러나지 않고, 하나만 바뀌면 나머지 둘과 조용히 어긋난다.
결정
- 정의는 도메인
Actor하나로 둔다(server/domain/access/rules.ts). ApprovalActor·PolicyActor정의를 지우고 서비스 내부와 테스트가 도메인 타입을 쓴다. 사용처가 서비스 내부(치환 7곳)와 테스트 3파일(11곳)뿐이라 라우터·클라이언트는 손대지 않았다.- 통합 뒤 남은
PortalRoleimport 두 곳은 tsc 가 즉시 잡았다(D-077) — 타입을 옮길 때 생기는 잔여물을 사람이 찾지 않아도 된다.
영향
| 영역 | 변경 |
|---|---|
| 서비스 | approvalService.ts(548줄) · policyService.ts(143줄) — 지역 타입 정의 제거, 도메인 Actor 사용 |
| 테스트 | agentTestKit.ts · approvalService.test.ts · policyService.test.ts — 도메인에서 Actor import |
검증
pnpm verify:full통과 — 101 파일 / 731 테스트(731 통과 · skip 0), 빌드 통과.- 순환 재측정 0개(D-082 의 게이트가 지킨다) ·
ApprovalActor·PolicyActor참조 0건.
남은 것
- 서비스-지역 타입(예:
agentService의PublishNodeContext)은 그 서비스에만 쓰이므로 도메인으로 옮길 이유가 없다 — 공용이 되는 순간에 옮긴다(D-082 의 기준).
D-084. 카탈로그 조회 판단을 응용 계층으로 내린다 — routers.ts 502줄 → 336줄 (P2 마지막 한 곳)
- 일자: 2026-09-27
- 상태: 채택 (§4-12 의 실행 계획을 그대로 수행 · D-078 ~ D-083 의 마지막 잔여)
- 배경: D-078(포트) · D-079(스키마) · D-080(인메모리) · D-081(PG 저장소) 로 레지스트리를 나눈 뒤에도
routers.ts만 라우터 조립과 카탈로그 조회 판단이 한 파일에 섞여 502줄이었다. 그 안의 조회 판단에는 §4.2 노출 범위(visibleEntries)와 접근 제한(toAgentDetail)이 들어 있는데, 판단이 HTTP 경계에 있으면 같은 규칙을 다른 입구(집계·탐색·운영 도구)가 쓰려 할 때 라우터 파일을 import 해야 한다 — 무엇이 어디에 속하는지가 코드에 드러나지 않는다.
대안
| 안 | 내용 | 판단 |
|---|---|---|
| A. 응용 계층으로 판단 이동 (채택) | server/application/catalogService.ts 를 만들고 조회 판단을 명시 인자 함수로 내보낸다 | 저장소(deps)·어댑터·뷰어를 인자로만 받으므로 라우터·tRPC 컨텍스트에 결합하지 않는다. 기존 관례(openCaseForRelease(deps, input))와 같은 모양이다 |
B. routers.ts 안에서 정리만 | 지역 함수를 묶고 이름만 바꾼다 | 판단이 여전히 HTTP 경계에 남고 파일도 줄지 않는다 — 분할의 목적(소유권)을 달성하지 못한다 |
C. createCatalogService(deps) 팩토리 | ctx.container.catalog 로 넣고 서비스가 스스로 판정한다 | 뷰어(요청자)와 프로세스 어댑터는 요청마다 달라지는 값이라 팩토리에 담을 수 없다 — 결국 메서드 인자로 되돌아오거나 서비스가 컨텍스트를 알게 된다 |
결정
- 새 파일은
server/application/catalogService.ts(235줄) —routers.ts46~250행의 조회 판단을 옮긴다:toNodeKey·CatalogRow/AgentMappingDto/AgentDetailDto/CatalogEntry·RELEASE_PRIORITY·DIRECT_RELATION_SET·loadCatalog→loadCatalogEntries(deps)·visibleEntries(viewer, entries)·toSummaryInput(entry)·loadNodeAccess(processAdapter, taxonomyCode, subjectId, nodeCodes)·toAgentDetail({ processAdapter, taxonomyCode, viewer, entry, nodeIdFilter }). - 라우터에는 인증 경계만 남긴다:
requirePrincipal(ctx)(401 — HTTP 경계의 일)와 그 결과에서 조회 주체를 만드는viewerOf(ctx)한 줄. 호출부 5곳은loadCatalogEntries(ctx.container.deps)·visibleEntries(viewer, …)·toAgentDetail({ … })로만 바뀐다. - 동작 변경 0:
visibleEntries는 §4.2 판정·순서·필터를 글자 그대로 옮긴다(도메인canViewResource호출 유지). DTO 필드 순서도 그대로다. - 계획과 다른 지점 두 곳(이유를 남긴다):
processAdapter타입은TrpcContext["processAdapter"]대신 그 타입의 정의처ProcessPortalAdapter(shared/processPortal)를 쓴다 — 같은 타입이면서 응용 계층이 HTTP 경계(_core/trpc)를 알지 않게 한다(server/application/안에_core를 import 하는 파일 0곳 유지).loadNodeAccess는 내보낸다 —processNodes.agents가 같은 판정을 쓰므로 private 로 두면 라우터가 어댑터 호출을 다시 구현하게 되어 판단이 두 곳으로 갈라진다.
영향
| 영역 | 변경 |
|---|---|
| 신규 | server/application/catalogService.ts(235줄) — 조회 유스케이스와 그 DTO 타입 |
| 라우터 | server/routers.ts 502 → 336줄 — nodeKeySchema(입력 검증)·requirePrincipal·viewerOf 만 남고, 카탈로그 판단은 import 로 대체 |
| 다른 코드 | 클라이언트·테스트 변경 없음 — 응답 모양이 같아 AppRouterOutputs 파생 타입도 그대로다(tests/integration/router.test.ts 등이 수정 없이 통과) |
| 순환 | catalogService 는 shared/* 와 도메인만 본다 — D-082 게이트가 순환 0 을 확인 |
검증
pnpm verify:full통과 — 101 파일 / 731 테스트(731 통과 · skip 0), 빌드 통과.TEST_DATABASE_URL을 준 상태라 PostgreSQL 통합 34건이 실제 DB 로 돌았다(인메모리·PG 두 경로가 같은 서비스 함수를 부른다).pnpm check(tscnoUnusedLocals·noUnusedParameters) 통과 — 이동으로 쓰지 않게 된 import(Agent·MappingHealth·ProcessMapping·NodeKey·ProcessNode·canViewResource·SummaryAgentInput·상태 타입 9종)를 함께 지웠고, 하나라도 남았다면 D-077 의 게이트가 막았을 자리다.- D-082 모듈 그래프 게이트가 새 파일을 스캔 대상에 포함해 순환 0 을 유지했다(파일 190개).
- 인증 경로 무변경:
toAgentDetail안에 있던requirePrincipal호출은 라우터의viewerOf로 올라갔지만, 그 경로는 전부authedProcedure라 401 지점이 달라지지 않는다.
남은 것
routers.ts336줄의 나머지는 라우터 조립 +server/routers/*(20모듈) 위임이다 — 판단이 섞여 있지 않으므로 더 나눌 대상이 아니다. P2(레지스트리 분할)는 이 결정으로 닫는다.viewerOf는 인증 방식이 바뀔 때 바뀌는 자리라 라우터에 남긴다 — 인증 어댑터 계약이 확정되면(§17) 이 한 줄이 그 계약을 따르는지 확인한다.- §4-11 의
react-hooks규칙 후보는 그대로 남는다(위반이 생기기 전에 도구를 늘리지 않는다).