요구사항 — enode 기능 다섯
이 문서는 requirements/ 여섯 문서를 대체하지 않는다. 값은 거기 있고
어긋나면 enode-design/protocol/INVARIANTS.md 가 이긴다 (canon.md).
여기 있는 것은 넷이다.
1절 ~ 2절 의도 분석과 입력. 무엇을 받았고 어떻게 읽었나
3절 확인 질문 첫 묶음의 답. decisions.md 에 행으로 올라갈 값이다
4절 ~ 7절 기능 요구사항의 추적표 · 비기능 요구사항 · 수용 기준 · 안 하는 것
8절 화면 대조. design/ 을 실제로 열고 나온 것과 둘째 묶음의 답 (8.4)
1. 의도 분석
| 항목 | 값 |
|---|---|
| 사용자 요청 | aidlc-docs/audit.md 2026-09-04T11:48:04Z 항목에 원문 그대로 있다 |
| 요청 유형 | New Feature — 기존 시스템에 기능 다섯을 더한다 (넷으로 시작해 2026-09-05 에 Mediator MCP 가 들어왔다). 마이그레이션도 리팩터도 아니다 |
| 범위 | Multiple Components — internal/store · internal/api · internal/contract · internal/enode · cmd/enodectl · cmd/mediator 와 새 패키지 둘(internal/panel · internal/api/ui) |
| 복잡도 | Complex — 저장 상태 어휘가 늘고(QUEUED), 노드와 중앙 사이에 새 축(정책)이 생기고, HTTP 서버가 노드에 처음 선다 |
| 명확성 | Clear. enode-features.md §5 가 미정 없음을 선언했고 확인 질문 셋으로 남은 계약을 닫았다 |
| 깊이 | Comprehensive — 이틀 예산에 유닛 여섯이 병렬로 돌고 접점이 여덟이다. 추적표가 없으면 접점에서 갈린다 |
| 프로젝트 성격 | Brownfield. 코드 기준선 1c290a6. 설계 정본 enode-design @ 0fd304b |
1.1 가치의 고정점
scene-gates.md §1 의 dhseo 장면이다. 완결성은 그 장면이 끝까지 도는가로
판단한다. 기능 넷을 각각 완성하는 것이 목표가 아니라 넷이 한 장면을
만드는 것이 목표다. 그래서 수용 기준이 유닛 완료가 아니라 조각 게이트다 (6절).
2. 입력
| 문서 | 이 문서에서 쓰는 자리 |
|---|---|
requirements/decisions.md |
값의 정본. 1절 사용자 확정 · 2절 권장값 · 3절 보안 취급 · 4절 이월 |
requirements/enode-features.md |
기능 다섯의 목적 · 기능요구사항 · 데이터 · 보안 · 수용기준 |
requirements/unit-boundaries.md |
유닛 일곱 · 파일 행렬 · 접점 표 · CP0 경계 검사 |
requirements/scene-gates.md |
조각 게이트 CP0 ~ CP4. 이것이 수용 기준이다 |
requirements/canon.md |
정본 연결 · 정본이 코드와 어긋난 자리 · 선행 조건 셋 |
requirements/constraints.md |
안 만드는 것 여덟 범주 + 보안 확장의 경계 |
design/README.md |
화면 여덟 장 · 컴포넌트 다섯 · 두 표면이 갈리는 규칙 |
aidlc-docs/inception/reverse-engineering/ |
산출물 아홉. 다시 훑지 않았다. 신선도 델타는 reverse-engineering-timestamp.md 머리 |
코드 1c290a6 |
계약 확인용으로만 읽었다 — 2.1 절 |
2.1 코드에서 확인한 것 — 요구사항을 줄인 자리
기준선을 다시 훑지 않고, 계약이 이미 참인지만 대조했다. 둘이 요구사항을
줄였다.
store.Cancel 이 이미 verdict 를 쓴다
reap.go:170. verdict{state:FAILED, checks:[{what:"cancelled",
ok:false, note:"cancelled by: <by>"}]} 를 쓰고 sealRecord 까지 부른다.
at-boundary 가 놓은 Run 을 Cancel(ctx, run, "drain:<node_id>") 로 닫으면
decisions.md 2절의 「verdict 가 drain 취소와 진짜 실패를 가른다」가
새 열도 새 필드도 없이 참이 된다. GET /v1/runs 가 verdict 를 내면 끝이다
runctl 은 고칠 것이 없다
internal/runctl/client.go 의 do 가 StatusCode/100 == 2 로 받으므로 202 가
통과한다. cmd/runctl/main.go 는 r.State 를 그대로 찍고, Terminal(state) 은
SUCCEEDED 와 FAILED 뿐이라 --wait 도 QUEUED 에서 계속 폴링한다.
⇒ CP2 의 「runctl status 가 QUEUED 를 표시한다」가 코드 변경 0 으로 닫힌다.
unit-boundaries.md §2 의 파일 행렬에 runctl 이 없는 것은 누락이 아니다
또 하나 — internal/store/store.go 가 이미 internal/match 를 임포트한다
(match.Reject). 그래서 store.WakeQueued 가 match.Match 를 부를 수 있고
임포트 순환이 없다. 매처는 여전히 한 벌이다 (ADR-014 결정 3).
3. 확인 질문 첫 묶음의 답 — decisions.md 에 올릴 값
사용자 답 A · A · A (2026-09-04). 이 셋은 decisions.md 2절에 행으로
올라간다. 아래가 그 행의 내용이다.
둘째 묶음은 8.4절에 있다. 그림을 열고 나온 셋이고 같은 날 같은 형식으로
닫혔다 — 두 묶음을 한 절에 합치지 않은 것은 나온 경로가 다르기 때문이다.
이 절은 decisions.md 를 읽고 나온 것이고 8.4절은 design/ 을 열고 나왔다.
3.1 QUEUED Run 의 steps 는 제출 시점에 만든다
| 항목 | 값 |
|---|---|
| 결정 | steps 행을 제출 시점에 전부 PENDING 으로 넣는다. node_id 는 빈 문자열. assigned 는 NULL |
| 미루는 것 | runAcquires 와 raiseAsks 는 승격 시점으로 미룬다 — 둘 다 자원을 잡거나 사람을 부르는 일이라 대기 중에 하면 안 된다 |
| 승격이 하는 일 | 매칭 · 임대 발급 · runs.state 를 RUNNING 으로 · assigned 채우기 · steps.node_id 채우기 · runAcquires · raiseAsks |
| 근거 | 계약의 구조 검증(st.Kind() · contract.NeedsOf)이 제출에 동기로 붙는다. 미루면 202 를 받은 Run 이 승격 시점에 구조 오류로 죽고 그때는 보고할 호출자가 없다. 그리고 S2 작업 그래프가 대기 중에도 그려진다 (design/README.md §3.2) |
| 딸린 것 | Records.Open(runID) 은 제출 시점에 한다. Cancel 이 sealRecord 를 부르므로 대기 중 취소가 봉인까지 닿아야 한다 |
GET /v1/runs/{id} 가 QUEUED Run 에 내는 것 — state: "QUEUED" ·
steps[] 전부 PENDING 이고 node 가 빈 값 · assigned 없음 · reject 없음.
3.2 WakeQueued 는 도착 순서로 전부 훑는다
| 항목 | 값 |
|---|---|
| 결정 | created_at 오름차순으로 QUEUED 를 전부 훑고, 지금 매칭되는 것을 전부 승격한다 |
| FIFO 의 뜻 | 같은 자원을 다투는 것들 사이의 도착 순서다. 앞의 Run 이 못 가도 뒤를 막지 않는다 |
| 왜 머리 하나가 아닌가 | 보드를 기다리는 Run 이 머리에 있으면 전혀 다른 능력을 기다리는 뒤의 Run 이 자원이 비어 있어도 못 간다. 그것은 공정성 정책이 아니라 결함이다 |
| 정책 틀이 아닌가 | 아니다. 조건이 하나(도착 순서)이고 계약이 그것을 못 건드린다. constraints.md §2 가 뺀 것은 FIFO 외의 정책과 그것을 갈아끼우는 틀이다 |
| 안전성 | 재진입 · 동시 호출 안전. 승격은 Run 하나마다 독립 트랜잭션이고, 임대 기본키 충돌이 경쟁을 막는다 (I5) |
| 실패 격리 | Run 하나의 승격이 실패해도 나머지를 계속 본다. 로그에 남기고 넘어간다 |
3.3 SECURITY-04 는 네 헤더를 걸고 HSTS 는 N/A 다
| 항목 | 값 |
|---|---|
| 적용 대상 | 새 HTML 표면 둘 — 중앙 현황판 GET /ui/ · 호스트 제어판 / |
| 거는 헤더 | Content-Security-Policy: default-src 'self' · X-Content-Type-Options: nosniff · X-Frame-Options: DENY · Referrer-Policy: strict-origin-when-cross-origin |
| HSTS | N/A. 평문 HTTP 에서 뜻이 없고 TLS 는 constraints.md §6 이다. 근거를 적었으므로 차단성 발견이 아니다 |
| 딸린 제약 | CSP 에 unsafe-inline 을 안 쓴다 ⇒ 화면의 JS 와 CSS 는 인라인이 아니라 별도 파일이다. 이것이 U-화면의 파일 구성을 정한다 |
| 적용 안 하는 자리 | 기존 JSON 라우트 15개. constraints.md §7 이 기존 계약 변경을 뺐고 HTML 을 내지 않는다 |
4. 기능 요구사항 — 추적표
절 번호는 enode-features.md 의 것이다. 이 표가 요구사항과 유닛과 게이트를
잇는다. 유닛은 Units Generation 이 확정하고, 여기서는 unit-boundaries.md
의 권장을 그대로 딛는다.
| # | 요구사항 | 절 | 유닛 | 게이트 |
|---|---|---|---|---|
| FR-1 | GET /v1/nodes 신규. ADR-065 응답 모양 + draining. principal 안 냄 · 필터 없음 · 만료 안 된 광고만 |
3.1.1 | U-바닥 · U-관측API | CP1 |
| FR-2 | GET /v1/runs 신규. 응답 모양과 필터는 decisions.md 2절 + 행마다 current[] 와 reject (§8.4 ① · ①-a). 시간 역순 · limit 기본 100 · 상한 1000 · 페이지네이션 없음 |
3.1.1 | U-바닥 · U-관측API | CP1 · CP2 |
| FR-3 | StepView.chosen 추가. DB 에 있고 뷰에 없다 (ADR-060) |
3.1.1 | U-바닥 | CP1 |
| FR-4 | 임대 표시는 GET /v1/nodes 의 lease{run_id, not_after} 에 내장. 별도 표면 없음 |
3.1.1 | U-관측API | CP1 |
| FR-5 | 중앙 현황판 정적 파일. S0 · S0b · S1 · S1b · S2. Mediator 에 embed, GET /ui/, 무인증. 토큰은 S0 에서 받아 브라우저 세션 저장소에만 둔다 |
3.1.1 | U-화면 | CP1 · CP2 · CP4 |
| FR-6 | runs.state 어휘에 QUEUED 추가. 409 -> QUEUED, 제출 응답 202. 422 는 그대로 FAILED |
3.2.1 | U-바닥 · U-대기열 | CP2 |
| FR-7 | QUEUED Run 의 steps 를 제출 시점에 만든다. runAcquires · raiseAsks 는 승격으로 미룬다 (3.1절) |
3.2.1 | U-대기열 | CP2 |
| FR-8 | 승격. store.WakeQueued 가 도착 순서로 전부 훑는다 (3.2절) |
3.2.1 | U-대기열 | CP2 |
| FR-9 | 깨우기 지점 여섯 — postResult · postCancel · Reap · 재기동 감지 · 부분 반납 · drain 해제를 받은 광고 처리. 주기에 안 얹는다 |
3.2.1 | U-대기열 · U-drain | CP2 · CP3 |
| FR-10 | 기동 시 WakeQueued 한 번 (cmd/mediator/main.go). QUEUED 는 아무것도 쥐지 않으므로 재기동 후에도 참이다 |
3.2.1 | U-대기열 | CP2 |
| FR-11 | 매칭에서 draining 노드 제외. submit 이 BusyNodes 에 DrainingNodes 를 합쳐 넘긴다. match.go 는 아무도 안 만진다 |
3.2.1 · 3.2.2 | U-대기열 · U-drain | CP3 |
| FR-12 | 정책 자리. 정본은 노드의 파일. 위치와 형식은 Functional Design 몫 — 기존 enode 설정 파일 옆, 같은 yaml |
3.2.2 | U-drain | CP3 |
| FR-13 | 경로. 데몬이 광고 직전에 정책을 읽어 광고에 싣고, UpsertAdvert 가 nodes 열에 복사본을 두고, 광고 응답이 되돌려 주고, 데몬이 그 값을 Worker 에 넘긴다. 중앙 라우트 신설 없음 |
3.2.2 | U-바닥 · U-drain | CP3 |
| FR-14 | 두 모드. graceful 은 새 임대만 막는다. at-boundary 는 도는 단계를 끝까지 두고 결과 보고 뒤 임대를 놓고, Mediator 가 postResult 끝에서 store.Cancel(ctx, run, "drain:<node_id>") 로 닫는다 |
3.2.2 | U-drain | CP3 |
| FR-15 | 해제는 소유자 명시. 도는 Run 이 끝나도 자동으로 안 풀린다. 풀면 다음 광고에서 후보로 돌아가고 WakeQueued 가 대기 Run 을 보낸다 |
3.2.2 | U-drain | CP3 |
| FR-16 | drain 중에도 광고는 계속 보낸다. draining 을 실어서. 현황판에 「있는데 안 빌려준다」로 보인다 |
3.2.2 | U-drain · U-화면 | CP3 · CP4 |
| FR-17 | enodectl serve [name]. 별도 프로세스 · 새 실행파일 없음. 이 기계의 노드를 전부 서브한다 — 이름은 처음 선택될 노드일 뿐이라 여럿이어도 생략할 수 있다(생략하면 names() 의 첫째). 라우트가 ?node=<name> 을 받는다 (§8.4 ②) |
3.1.2 | U-제어판 | CP4 |
| FR-18 | 프로세스 제어. status · start · stop · logs 를 감싼다. 재료는 cmd/enodectl/main.go 의 names · pidOf · identityOf · ownsConfig · signalStop 과 proc_*.go 의 processAlive — internal/panel 로 내려 같이 쓴다 |
3.1.2 | U-제어판 | CP4 |
| FR-19 | 신원 표시. node_id · label · principal · instance 전부. 자기 기계다 |
3.1.2 | U-제어판 | CP4 |
| FR-20 | 탐지 능력을 Detect() 결과로 읽기 전용 표시. 편집 자리를 만들지 않는다 (ADR-017 결정 3) |
3.1.2 | U-제어판 | CP4 |
| FR-21 | 현재 작업. 임대 유무 · Run · not_after · CLAIMED 단계 이름 · attempt · 시작 시각. 출처는 Mediator 조회 — GET /v1/nodes 의 자기 행과 GET /v1/runs/{id}. 데몬 로그를 읽지 않는다 |
3.1.2 | U-제어판 | CP4 |
| FR-22 | drain 토글. 걸기 · 모드 고르기 · 풀기. 건 뒤에는 현재 모드와 「풀기」가 보인다 (C3) | 3.1.2 · 3.2.2 | U-제어판 | CP4 |
| FR-23 | Mediator 연결 상태. 마지막 응답 시각. 끊기면 drain 통보가 늦어진다는 문구 | 3.1.2 | U-제어판 | CP4 |
| FR-24 | 호스트 화면 S3 · S5. 멈춘 노드에서 주 동작은 start |
3.1.2 | U-화면 | CP4 |
| FR-26 | runctl mcp. stdio JSON-RPC 2.0 하나. streamable HTTP 를 안 연다. 새 실행파일 없음 — cmd/runctl 의 하위명령이다 (decisions.md 5.2) |
3.3.1 | U-MCP | CP5 |
| FR-27 | 토큰은 ENODE_MEDIATOR · ENODE_TOKEN 환경변수에서 읽는다. runctl 과 같은 규칙이고 도구 인자로 안 받는다 — 받으면 토큰이 대화 기록에 남는다 (SECURITY-12) |
3.3.1 | U-MCP | CP5 |
| FR-28 | 도구 일곱 — capabilities.list · fleet.list · runs.list · run.submit · run.plan · run.cancel · record.get. 전부 기존 REST 를 감싼다. 새 라우트 0 · Mediator 무변경 (decisions.md 5.3) |
3.3.1 | U-MCP | CP5 |
| FR-29 | internal/runctl/client.go 에 Nodes · Runs 두 메서드를 더한다. fleet.list · runs.list 가 그것을 쓴다. HTTP 클라이언트를 새로 안 짠다 — do() 가 인증과 오류 사상을 이미 진다 |
3.3.1 | U-MCP | CP5 |
| FR-25 | 마지막 관측 캐시. 제어판이 노드마다 마지막 성공 관측을 메모리에 든다. serve 가 5초마다 GET /v1/nodes 를 한 번 받아 이 기계 노드 전부의 행을 채운다 (§8.4 ③-a). GET /api/status 가 observed{seen_at, expires_at, lease, instance, at} 과 stale 을 낸다. serve 가 도는 동안만 살고 재시작하면 빈다 — 비면 화면이 — 로 그린다 (§8.4 ③) |
3.1.2 | U-제어판 | CP4 |
4.1 확정된 계약 — 접점에서 갈리면 안 되는 것
unit-boundaries.md §4 의 접점 표에 3절의 답을 얹은 것이다. Functional
Design 이 내부를 다듬을 수 있으나 아래 모양은 못 바꾼다 — 유닛 둘 이상이
읽기 때문이다.
contract.Policy{Drain string} "" | "graceful" | "at-boundary". 문자열 그대로
store.DrainingNodes(ctx) map[string]bool. match.Match 의 busy 와 같은 모양
store.WakeQueued(ctx) error 도착 순서로 전부. 재진입 · 동시 호출 안전
store.UpsertAdvert(...) 의 반환 이전 정책을 돌려준다. 해제 감지가 이 값을 읽는다
store.ListNodes · store.ListRuns decisions.md 2절의 응답 모양을 그대로 낸다
GET /v1/runs 의 행 decisions.md 2절 + current[] + reject (8.4절 ①)
current[] [{node, step, attempt, started_at}]. CLAIMED 마다 하나
reject {code, as, reason}. match.Reject 에 태그를 단다
GET /v1/nodes 노드 항목 ADR-065 모양 + draining
GET /api/status 의 observed · stale 8.4절 ③. 캐시가 비면 observed 를 안 내고 stale 이 true
광고 응답의 정책 왕복 advertResponse 에 policy 를 더한다 (4.2절)
policy.Read(path) · policy.Write(path, Policy) U-drain 이 정본. 제어판은 파서를 안 만든다
enodectl serve 의 라우트 4.3절. 전부 ?node=<name> 을 받는다
MCP 도구 이름 일곱 decisions.md 5.3. Claude 설정에 박히면 못 바꾼다
runctl.Client.Nodes · .Runs GET /v1/nodes · GET /v1/runs 를 그대로 낸다
MCP 도구 이름이 계약인 이유 — 사용자의 Claude 설정과 대화에 그 글자가
박힌다. 라우트 이름을 바꾸는 것과 같은 무게이고, 그래서 Functional Design 이
내부는 다듬어도 일곱 이름은 못 바꾼다.
4.2 광고 응답의 정책 왕복 — 권장값을 벗어난 자리
unit-boundaries.md §4 는 이 접점의 모양을 안 정했다. advertResponse 에
policy 를 더한다 — GET /v1/nodes 의 납작한 draining 과 이름을 달리 한다.
근거 — 광고 요청이 올려보낸 것과 같은 객체를 그대로 되돌리는 왕복이다.
같은 것에 이름을 둘로 두면 어느 쪽이 정본인지가 갈린다. GET /v1/nodes 쪽은
관측이고 ADR-065 가 납작한 필드로 응답을 짜 뒀으므로 거기서는 draining 이
맞다. 데몬은 policy.drain 만 읽는다.
왜 왕복이 필요한가 — 데몬이 파일을 직접 읽었는데도 응답의 값을 쓰는 이유는
Worker 가 Mediator 가 확인한 값으로 움직여야 하기 때문이다. 그렇지 않으면
노드는 at-boundary 로 임대를 놓았는데 중앙은 아직 후보로 알고 있는 창이
생긴다. 그 창을 없애는 것이 이 왕복이다 (enode-features.md 3.2.2 「데몬은
응답의 값을 Worker 에 넘긴다」).
4.3 제어판 라우트 — 권장값을 넘긴 자리
unit-boundaries.md §4 는 /api/status · /api/policy · 정적 / 셋을
시작값으로 뒀다. 그 셋으로는 FR-18(프로세스 제어)이 안 닫힌다. Units
Generation 이 확정한다고 같은 절이 적었으므로 여기서 늘린다. 8.4절 ② 의 답
A 가 여기에 ?node= 와 /api/nodes 를 더했다.
GET / 정적 파일. S3 · S5
GET /api/nodes 이 기계의 노드 목록. 사이드바가 읽는다 (8.4절 ②)
GET /api/status?node= 신원 · 프로세스 살았나 · 탐지 능력 · 현재 작업
· Mediator 마지막 응답 시각 · observed · stale
GET /api/policy?node= 현재 정책
PUT /api/policy?node= 정책 쓰기. drain 걸기와 풀기. 모드 바꾸기는 못 한다 (아래)
POST /api/start?node= 데몬 띄우기
POST /api/stop?node= 데몬 멈추기
GET /api/logs?node= 로그 꼬리
?node= 는 이 기계의 노드 이름이다. 생략하면 names() 의 첫째이고,
names() 에 없는 이름이면 404 다. 노드가 하나뿐인 기계에서는 붙일 일이 없다.
/api/nodes 가 하나 는 것은 사이드바가 목록을 어디선가 받아야 하기 때문이다 —
/api/status 에 목록을 얹으면 노드마다 부르는 라우트가 전체 목록을 매번 나른다.
PUT /api/policy 하나로 걸기와 풀기를 받는다 — 정책은 상태이지 명령이 아니고,
명령으로 쪼개면 「지금 무엇이 걸려 있나」가 라우트 셋에 흩어진다.
GET /api/status 가 종합이라 화면이 폴링 하나로 돈다.
모드 바꾸기는 받지 않는다. 이 절의 1판은 「걸기 · 모드 바꾸기 · 풀기가
전부 이것 하나」였고 그것이 틀렸다. C3 컴포넌트의 각주가 「걸 때 고른
모드(graceful · at-boundary)는 풀기 전까지 바뀌지 않는다」이고 C3 에
모드 라디오가 없다 (§8.5). 그래서 전이는 둘뿐이다.
"" -> graceful | at-boundary 받는다
graceful | at-boundary -> "" 받는다. 언제나
graceful <-> at-boundary 409. 먼저 풀어라
정본은 여전히 노드의 파일이므로 소유자가 파일을 직접 고쳐 모드를 갈아치울 수
있다 — 라우트가 막는 것은 화면에서 오는 길뿐이다. 그것으로 충분하다:
막는 이유가 안전이 아니라 「되찾았다」의 뜻을 지키는 것이기 때문이다.
at-boundary 를 graceful 로 바꾸면 이미 놓인 임대가 안 돌아오고, 반대로
바꾸면 도는 Run 이 예고 없이 경계에서 죽는다.
5. 비기능 요구사항
5.1 보안 — 새 표면에만 건다
security-baseline 이 켜져 있다. decisions.md 3절이 규칙마다 취급을 미리
적었고, 여기에 3.3 절이 SECURITY-04 를 더했다. 기존 코드의 사실은 기록만
하고 고치지 않는다.
| # | 요구사항 | 규칙 |
|---|---|---|
| NFR-S1 | 제어판 바인딩 기본값 127.0.0.1:8081. LAN 노출은 --listen 명시로만 |
SECURITY-07 |
| NFR-S2 | --listen 이 127.0.0.1 밖이면 정책 파일의 panel_token 필수. 없으면 serve 가 기동 거부. Mediator 토큰을 재사용하지 않는다 |
SECURITY-12 |
| NFR-S3 | 정책 파일은 소유자만 쓸 수 있는 권한으로 만든다 | SECURITY-09 · SECURITY-12 |
| NFR-S4 | GET /v1/nodes · GET /v1/runs 는 Mediator 토큰으로 보호. 기존 표면과 같은 auth 미들웨어 |
SECURITY-08 |
| NFR-S5 | 입력 검증. limit 은 상한을 갖고 넘으면 잘린다. state · draining 은 enum. since 는 RFC3339. work 는 정확 일치 |
SECURITY-05 |
| NFR-S6 | 새 HTML 표면 둘에 헤더 넷. HSTS 는 N/A (3.3절) | SECURITY-04 |
| NFR-S7 | 자격증명을 소스에 박지 않는다. 제어판이 Mediator 를 부를 토큰은 그 기계의 기존 enode 설정에서 읽는다 |
SECURITY-12 |
| NFR-S8 | 제어판 127.0.0.1 무인증은 명시적 public 표면이다. 객체 수준 권한은 해당 없음 — ADR-015 §1 이 principal 을 식별로 뒀고 그 기계에 접속한 것이 소유의 증거다 (ADR-042 · ADR-063 §3) |
SECURITY-08 |
| NFR-S9 | 새 핸들러의 외부 호출은 전부 오류 처리를 갖고 실패하면 닫는다. 사용자에게 나가는 오류 문구에 내부 경로 · 스택을 안 싣는다 | SECURITY-15 |
limit 상한 — 새로 정하는 값
decisions.md 2절이 기본 100 만 정했고 상한은 안 정했다. SECURITY-05 가
명시적 상한을 요구하므로 여기서 정한다 — 상한 1000, 넘으면 1000 으로
자른다. 기본의 열 배이고, 이틀 안에 그만큼 쌓이지 않으며, 페이지네이션이
없는 것과 모순되지 않는다 (limit 이 유일한 손잡이다).
하지 말 것
TLS 를 넣는 것 constraints.md §6
cmd/mediator · internal/config 를 보안 이유로 고치는 것 constraints.md §6. 유닛 경계 밖
bootstrapToken 의 stderr 토큰 출력을 고치는 것 decisions.md 3절 SECURITY-03
Mediator Listen :8080 을 좁히는 것 decisions.md 3절 SECURITY-07
sslmode=disable 을 고치는 것 decisions.md 3절 SECURITY-01
5.2 불변식 — 건드리면 안 되는 것
| # | 요구사항 |
|---|---|
| NFR-I1 | I1 · I2 · I4 · I5 를 건드리지 않는다. leases.node_id 기본키를 안 바꾼다 |
| NFR-I2 | 상태 READY 를 만들지 않는다 (ADR-064 §1.1). 저장되는 어휘는 RUNNING · SUCCEEDED · FAILED 와 새로 오는 QUEUED 넷이다 |
| NFR-I3 | drain 을 위한 중앙 라우트를 만들지 않는다. 경로는 광고뿐이다 |
| NFR-I4 | match.Match 를 안 고친다. 순수 함수이고 busy 는 api.go 의 submit 이 만들어 넘긴다 (ADR-014 결정 3) |
| NFR-I5 | 단계 도중에 끊지 않는다. at-boundary 는 경계에서만 놓는다 (INVARIANTS §2 재개 없음) |
| NFR-I6 | POST /v1/runs/dry-run 의 응답을 안 바꾼다. busy 를 안 보듯 draining 도 안 본다 |
| NFR-I7 | 같은 run_id 재제출은 200 + 기존 Run 이다 (INVARIANTS §4). 202 는 새로 QUEUED 가 되는 순간만 |
5.3 패키지 경계
| # | 요구사항 |
|---|---|
| NFR-P1 | internal/panel -> internal/store 금지. 제어판은 HTTP 로 Mediator 를 본다 |
| NFR-P2 | internal/panel -> internal/api 금지 |
| NFR-P3 | internal/api/ui -> internal/store 금지. 화면은 정적 파일이다 |
| NFR-P4 | internal/enode -> internal/panel 금지. 데몬은 제어판을 모른다 |
| NFR-P5 | 위 넷을 go/parser 로 임포트를 읽어 기계로 센다. U-바닥의 산출물이고 CP0 가 돈다 (unit-boundaries.md §5) |
| NFR-P6 | 유닛끼리 서로 임포트하지 않는다. 새 기능은 새 패키지, api.go 는 등록 줄만 |
5.4 관측과 갱신
| # | 요구사항 |
|---|---|
| NFR-O1 | 두 화면 다 폴링 5초. SSE · WebSocket 없음 (constraints.md §5) |
| NFR-O2 | 화면은 값 대신 **「마지막 갱신 시각」**을 보인다 |
| NFR-O3 | expires_at 까지 30초 이하면 카드가 흐려진다 |
| NFR-O4 | 갱신이 연속 3회 실패하면 카운트다운을 멈추고 S1b 로 간다 |
| NFR-O5 | 중앙 카드에 observed_at 과 expires_at 을 함께 얹는다 — 보고 있는 것이 관측 시각의 사실이지 지금의 사실이 아니다 (design/README.md §2.1) |
| NFR-O6 | draining 배지는 두 국면을 가른다 — 「draining · 진행 중」(C4) 과 「draining · 대기 중」(C5). 안 가르면 사람이 「지금 되찾았나」를 모른다 |
5.5 품질 게이트
| # | 요구사항 |
|---|---|
| NFR-Q1 | .coverage-contract.yml 의 바닥 80% 를 새 패키지에도 그대로 건다. 계약 파일을 안 고친다 |
| NFR-Q2 | internal/panel · internal/api/ui 는 DB 없이 도는 테스트로 채운다 — store 를 안 부르므로 ENODE_TEST_DATABASE_URL 이 필요 없다 |
| NFR-Q3 | go test ./... · go vet ./... · golangci-lint run · go run ./scripts/glyphscan.go 가 초록이다 |
| NFR-Q4 | 표기와 언어는 CONVENTIONS.md. 강조는 마크다운 굵게로만, 코드에 장식 문자 금지, 에러 · 로그 · CLI 출력 · 테스트 이름은 영어, 주석과 커밋 메시지는 한국어 |
| NFR-Q5 | 기존 라우트 15개가 회귀 없이 돈다 |
5.6 성능
상한을 두지 않는 것이 결정이다. 근거는 decisions.md 1절.
대기 상한 없다. QUEUED 는 무기한. 빠져나가는 길은 cancel 뿐
at-boundary 지연 상한 없다. 도는 단계의 남은 시간 + 광고 주기가 곧 값
페이지네이션 없다. limit 뿐
노드 수 여섯 규모. LISTEN/NOTIFY 는 ADR-015 가 적어둔 나중의 최적화다
6. 수용 기준
scene-gates.md 가 정본이다. 조각마다 실행 명령이 있고, 조각이 초록이
아니면 다음 유닛에 들어가지 않는다. 집행자는 그 유닛을 구현하지 않은 사람이다.
| 조각 | 무엇을 확인하나 | 딛는 유닛 |
|---|---|---|
| CP0 | 기동이 안 깨졌다. 기존 테스트 · 커버리지 · 린트 · glyphscan · 라우트 15개 회귀 · 경계 검사 테스트 | U-바닥 |
| CP1 | 보인다. GET /v1/runs · GET /v1/nodes 가 실데이터 JSON. lease.not_after 와 draining 이 실려 있다. 화면 S1 · S1b |
U-관측API (+ U-화면) |
| CP2 | 기다린다. 노드 하나에 Run 둘 -> 둘째 202 · QUEUED -> 첫째가 끝나면 RUNNING. 없는 능력은 여전히 422 · FAILED. 화면 S1 의 QUEUED 행이 어느 카드에도 안 얹혀 있다 |
U-대기열 |
| CP3 | 돌려받는다. at-boundary 로 걸면 경계에서 임대가 풀리고 후보에서 빠진다. 풀면 대기 Run 이 집는다. 화면 없음 — 정책 파일과 GET /v1/nodes 로 닫는다 |
U-drain |
| CP4 | 한 장면. scene-gates.md §1 ① ~ ⑦ 을 끝까지. 현황판과 제어판이 같은 사실을 보인다. LED 가 새 패턴이다 |
전부 |
6.1 화면 검증은 API 검증과 따로 적는다
API 어휘(「임대가 풀린다」)는 화면 없이도 초록이 된다. 그래서 화면이 있는
유닛의 완료 조건은 그 화면의 버튼을 전부 나열한다. 「누르면 걸린다」만으로는
「풀기」가 빠진 채 통과된다 (scene-gates.md §2.1 · design/README.md §7).
Units Generation 이 유닛마다 그 목록을 낸다.
6.2 게이트가 그 유닛 밖의 이유로 빨간 경우
진행자가 사실을 aidlc-docs/audit.md 에 적고 조각을 보류로 표시한 뒤
넘어간다. 보류는 통과가 아니다 — CP4 전에 전부 초록이어야 한다
(scene-gates.md §4).
7. 범위 밖
constraints.md 여덟 범주와 decisions.md 4절 이월이 정본이다. 요약만 둔다.
실행 중 선점 · FIFO 외의 공정성 정책 · 격리 강화 · 중앙에서의 소유권 판정
SSE · WebSocket · 트랜스크립트 (S4 는 자리만)
대시보드 인증 · TLS · 리버스 프록시 · 다중 Mediator · 페이지네이션
기존 계약 변경 (success_when 문법 · 봉인 규칙 · 임대 키 · READY · 중앙 drain 라우트)
역할 주입 · 우선순위 · 별칭 · 메모
트랜스크립트 구독 (MCP 는 범위 안이지만 구독 표현은 여전히 이월이다)
Mediator MCP 가 이 목록에서 빠졌다 (2026-09-05). CP2 가 닫힌 뒤 진행자가
이월을 되돌렸고 근거는 decisions.md 5절이다 — 이월의 이유가 결합이 아니라
값 둘(전송 · 토큰 전달)이었고 5.2 가 그 둘을 닫았다. 유닛 U-MCP · 게이트 CP5
로 들어왔고 FR-26 ~ FR-29 가 그것이다.
7.1 정본 쪽 선행 조건 — 이 저장소의 일이 아니다
canon.md §3 이 셋을 적었다. enode-design 의 PR 이고 이 저장소가 하지
않는다. 다만 이 팩은 개정된 문장을 따른다.
ADR-065 개정 GET /v1/nodes 에 draining 필드를 더한다
ADR-063 개정 §2.1 을 graceful 한정으로 하고 at-boundary 를 갈라 적는다
ADR-063 · 064 초안을 결정으로 올린다. decisions.md 가 미결에 값을 줬다
ADR-065 는 「구현됨」이라고 적혀 있으나 1c290a6 에 GET /v1/nodes 가 없다
(canon.md §2). v1 은 1c290a6 만 본다 — U-관측API 는 ADR-065 §2 의
응답 모양에서 새로 만든다.
8. 화면 대조 — design/ 을 실제로 열고 나온 것
Workflow Planning 까지 이 절이 비어 있었다. 읽은 것은 design/README.md
(그리는 규칙)뿐이었고 그림 자체를 안 봤다. 사용자 지적으로 열었다.
pencil MCP 안 붙는다. failed to connect to running Pencil app: code-server
MCP 는 실행 중인 앱에 붙는 통로다. 이 작업에는 필요 없었다
@pen.dev/cli 0.3.6. pen login 뒤 헤드리스로 .pen 을 직접 연다 —
pen interactive --in design/enode-ux.pen --out <스크래치패드>
save() 를 안 부르고 --out 을 저장소 밖에 뒀다. 원본 무변경
PNG 로 읽은 것 design/exports/*.png 일곱 — S0 · S0b · S1 · S1b · S2 · S3 · S5
C1 · C2 는 S1 안에 인스턴스로 들어 있어 같이 봤다
.pen 으로 읽은 것 C3 · C4 · C5. export 가 없어 원본을 열어야 보였다 (8.5절)
S1 · S1b · S2 의 배지와 필터 글자를 원본에서 대조했다 (8.6절)
안 읽어도 되는 것 S4. 범위 밖이고 자리만이다 (constraints.md §5)
그림은 장식이 아니었다. decisions.md 와 ADR-065 가 정한 응답 모양으로는
못 그리는 자리가 셋 있다 — 8.3 절.
8.1 확인된 것 — 문서가 맞았다
design/README.md §4.1 은 「중앙 카드는 label 만. principal 을 내지 않는다」
인데 S1 카드 제목이 taeels@thinkpad:ws-a 라 언뜻 principal@host:label 로
보인다. 아니다. internal/enode/identity.go 의 deriveLabel 이
local@host:configbase 를 만든다 — 그 문자열 전체가 label 이다. S3 의 신원
블록이 principal: taeels@corp 과 label: taeels@thinkpad:ws-a 를 따로
보이며 「principal 은 이 화면에만 있다」고 못 박아 뒀다. ADR-065 를
안 어긴다.
node_id 도 맞다 — 화면의 a3f19c2b8e04 는 12 자이고 deriveFrom 이
hex[:12] 를 쓴다. 자르는 것이 아니라 그것이 전부다.
8.2 근거를 적고 닫은 것 — 묻지 않는다
| 자리 | 그림이 보인 것 | 처리 |
|---|---|---|
S1b 의 SEALED |
필터 칩과 배지의 보이는 글자가 SEALED 다. S1 은 같은 자리가 SUCCEEDED 다 |
SUCCEEDED 로 간다. design/README.md §6 이 「SEALED 는 Run 상태가 아니다」로 이미 판정했다. §6 은 S1 의 프레임 이름표만 그렇다고 적었으나 S1b 는 보이는 글자까지 SEALED 다 — 그림 쪽 오류이고 규칙이 이긴다. 행 부제도 봉인 이 아니라 완료 시각으로 적는다 |
S2 의 produced |
단계 상세에 produced 행이 있다. StepView 에는 그 필드가 없다 |
StepView 에 안 더한다. GET /v1/runs/{id}/ledger 가 이미 단계별 산출물 이름을 준다 (entries[].by = "step:<id>"). 화면이 그것을 조인한다. 새 필드 0 개 |
| draining 배지 두 국면 (C4 · C5) | 「진행 중」과 「대기 중」을 갈라야 한다 | 파생이다. draining != "" 이고 lease != null 이면 진행 중, lease == null 이면 대기 중. 새 필드 없음 |
| at-boundary 카드 | 카운트다운 대신 경계에서 · 최대 13:02:38 |
표시 규칙. draining == "at-boundary" 면 남은 시간을 세지 않고 lease.not_after 를 상한으로 적는다. 경계가 언제인지는 아무도 모르므로 카운트다운이 거짓말이 된다 |
| S3 의 「재시작」 | 요구사항에는 status · start · stop · logs 넷뿐이다 |
합성이다. stop 뒤 start. 새 라우트를 안 만든다 |
| S3 의 트랜스크립트 카드 | 「하네스가 지금 뱉는 출력」 카드에 → 가 있다 |
자리만. 비활성으로 그린다. constraints.md §5 가 범위 밖이고 design/README.md §3 이 S4 를 자리만으로 뒀다. 「헤더의 로그 는 데몬 로그다. 다른 물건이다」는 문구는 남긴다 |
| S5 에 drain 블록 | 없다 | 맞다. 그림의 문구 그대로 — 「drain 은 도는 노드에만 건다. 멈춘 노드는 이미 자원을 내놓은 상태」. 유닛 완료 조건이 상태별로 갈린다 |
| S3 의 임대 진행 막대 | 임대 시간 — 4분 39초 중 2분 41초 남음 |
파생이다. not_after 와 granted_at 대신 lease.not_after 와 설정의 TTLSeconds 로 그린다. 새 필드 없음 |
8.3 화면의 버튼 전부 — 유닛 완료 조건의 재료
scene-gates.md §2.1 과 design/README.md §7 이 「화면이 있는 유닛의 완료
조건은 그 화면의 버튼을 전부 나열한다」고 요구한다. Units Generation 이 이
목록을 쓴다.
S0 토큰 표시 · 숨김 토글 (눈 아이콘)
현황판 열기
S0b 같은 둘. 입력 테두리가 붉고 그 아래 「401 — Mediator 가 이 토큰을
받지 않았다」 가 인라인으로 붙는다. 버튼은 그대로 산다
S1 필터 칩 다섯 — 전체 · RUNNING · QUEUED · SUCCEEDED · FAILED
Run 행의 열기 (>) -> S2
(카드에는 조작이 없다. 중앙은 읽기 전용이다)
S2 닫기 (X) -> S1
단계 노드 고르기 -> 오른쪽 상세
「함대 격자의 이 노드로」 (->) -> S1 의 그 카드
S3 사이드바의 노드 고르기
「로그」 · 「재시작」 · 「정지」 (헤더 셋. 프레임 이름 Btn 로그 · Btn 재시작 · Btn 정지)
graceful (라디오) · at-boundary (라디오)
「drain 걸기」
[건 뒤 = C3] 「drain 풀기 · 함대로 돌려주기」 <- 이것이 빠지면 통과가 아니다
모드 배지 (graceful | at-boundary). 라디오는 사라진다
트랜스크립트 (->) 자리만 · 비활성
S5 사이드바의 노드 고르기
「마지막 로그」 (헤더 하나. 로그·재시작·정지가 아니다)
「시작」
(drain 블록 없음)
8.4 그림이 연 계약 구멍 셋 — 답으로 닫았다
사용자 답 A · A · A (2026-09-04). 질문 전문은
design-verification-questions.md 에 그대로 남아 있다. 셋 다 decisions.md 도
ADR-065 도 답을 갖고 있지 않던 자리이고, 셋 다 결정표 2절의 권장값을 넘긴
자리라 근거를 적는다 — 그것이 결정표의 게이트 규칙이다.
① Run 행과 S1 카드의 「지금 무슨 단계」 — GET /v1/runs 를 넓힌다
| 항목 | 값 |
|---|---|
| 결정 | GET /v1/runs 의 행에 current 와 reject 를 더한다. GET /v1/nodes 는 안 건드린다 |
current |
도는 단계 — step · attempt · started_at. 도는 것이 없으면 안 낸다 |
reject |
runs.reject 열의 투영. Run 행 부제의 「함대에 능력 없음」이 여기서 온다 |
| 새 열 | 0. reject 와 verdict 는 runs 에 이미 있고 current 는 steps 조인이다 (2.1절과 같은 성질의 확인) |
| 폴링 비용 | 화면 하나당 요청 둘로 유지된다 — GET /v1/nodes 와 GET /v1/runs. C 안이면 1 + 임대 수 로 늘었다 |
ADR-065 개정 범위 |
draining 하나 그대로. B 안이면 lease 세 필드까지 늘었다 |
| 근거 | GET /v1/runs 는 이 팩이 새로 만드는 라우트라 정본 개정 부담이 없다. ADR-065 는 이미 착지한 결정이고 거기에 얹으면 canon.md §3 의 선행 PR 이 커진다 |
| 사는 곳 | FR-2 · §4.1 |
② enodectl serve 의 범위 — 이 기계의 노드를 전부 서브한다
| 항목 | 값 |
|---|---|
| 결정 | enodectl serve [name]. 이름은 처음 선택될 노드일 뿐이고 여럿이어도 생략할 수 있다 |
| 라우트 | 전부 ?node=<name> 을 받는다. 사이드바용 GET /api/nodes 가 하나 는다 (§4.3) |
| 넘긴 문면 | enode-features.md 3.1.2 의 「names() 가 하나면 생략, 여럿이면 필수」. 그 문장은 하나만 서브한다는 뜻이었다 |
| 근거 | S3 · S5 의 사이드바가 「멈춘 노드는 이 화면에만 나온다」를 말하는 자리다. 기계에 노드가 둘인데 둘을 보려고 포트 둘에 serve 를 띄우는 것은 소유자 화면으로 이상하다. 프로세스 하나 · 포트 하나가 「이 기계의 제어판」의 뜻과 맞다 |
| 안 늘어나는 것 | 프로세스도 포트도 여전히 하나다. decisions.md 2절의 127.0.0.1:8081 이 그대로다 |
| 사는 곳 | FR-17 · §4.3 |
③ 멈춘 노드의 「마지막 관측」 — 제어판이 메모리에 든다
| 항목 | 값 |
|---|---|
| 결정 | 제어판이 노드마다 마지막 성공 관측을 메모리에 든다. serve 가 도는 동안만 살고 재시작하면 빈다 |
| 응답 | GET /api/status 가 observed{seen_at, expires_at, lease, instance, at} 과 stale: true | false 를 낸다. at 은 그 관측을 받은 시각이다 |
| 빈 경우 | observed 를 안 내고 stale 이 true. 화면이 — 로 그린다 |
| 근거 | FR-23(「Mediator 마지막 응답 시각」)이 이미 같은 성질의 메모리 상태를 요구한다. 부품이 하나 느는 것이 아니라 같은 부품이다. 디스크에 안 쓰므로 정본이 갈리지 않는다 |
| 기각한 C 안 | 데몬이 마지막 광고를 로컬 파일에 남기는 것. decisions.md 2절이 「데몬이 상태 파일을 남기는 것은 새 기계다」로 이미 기각한 자리다 |
| 「겉면은 얇게」와의 관계 | unit-boundaries.md 의 그 줄은 store 를 직접 부르지 말라는 뜻이다. 제어판은 여전히 HTTP 로만 Mediator 를 본다 — 캐시는 그 응답의 사본이지 두 번째 관측 경로가 아니다 |
| 사는 곳 | FR-25 · §4.3 |
답 A 가 덜 정한 자리 셋 — 셋도 닫혔다
값으로 굳히면서 코드와 대조했더니 답 자체는 맞는데 모양이 하나로 안 정해지는
자리가 셋 남았다. 유닛 둘 이상이 읽는 접점이라 unit-of-work-plan.md §1 이
물었고 사용자 답 A · A · A (2026-09-04) 로 닫혔다.
| 자리 | 값 | 근거 | |
|---|---|---|---|
| ①-a | current 의 폭 |
배열이다. current: [{node, step, attempt, started_at}] — 지금 CLAIMED 인 단계마다 한 항목. 도는 것이 없으면 안 낸다 |
S1 카드는 노드 하나를 세고 Run 행은 Run 하나를 센다. 단수면 폭이 1 을 넘는 Run 에서 카드가 남의 노드가 도는 단계를 그린다. CP3 계약이 「단계 둘 이상」을 요구하므로(scene-gates.md §3) 가정이 아니라 실제로 걸린다. steps 조인에 node_id 가 하나 더 실릴 뿐 새 열은 여전히 0 |
| ①-b | reject 의 키 |
match.Reject 에 json 태그를 단다. 두 라우트가 reject{code, as, reason} 로 합쳐진다 |
지금 GET /v1/runs/{id} 가 {Code, As, Reason} 을 낸다 (match.go:49 에 태그가 없고 api.go:155 가 그대로 싣는다). 소비자가 0 이다 — 저장소에서 runView.Reject 의 키를 읽는 코드가 없고 runctl 이 읽는 것은 별개 타입 errBody 다. mediator-api.md 의 GET /v1/runs/{id} 절이 reject 를 안 적었으므로 개정할 문장이 없다. 옛 DB 행 {"Code":…} 는 Go 의 언마샬이 키 대소문자를 무시해 그대로 읽힌다 |
| ③-a | 제어판 폴링 범위 | serve 가 5초마다 GET /v1/nodes 를 한 번 받아 이 기계 노드 전부의 행을 캐시에 넣는다. /api/status?node= 는 캐시를 읽는다. GET /v1/runs/{id}(현재 작업)는 화면이 보고 있는 노드에 임대가 있을 때만 부른다 |
Mediator 로 나가는 요청이 노드 수와 무관하게 5초에 하나다. 안 열어 본 노드의 「마지막 관측」도 차므로 멎은 노드를 처음 열었을 때 S5 가 그림대로 그려진다 — 요청이 올 때만 부르면 그 칸이 영영 빈다 |
①-b 는 constraints.md §7 을 넘지 않는다. 그 절의 목록은 success_when
문법 · 봉인 규칙 · 임대 키 · READY · drain 중앙 라우트 · I1·I2·I4·I5
다. 태그를 다는 것은 그중 어느 것도 아니고 match.Match 의 시그니처와 로직을
안 건드리므로 NFR-I4 도 그대로다. 다만 internal/match/match.go 를 만지는
유닛이 하나 생긴다 — unit-boundaries.md §2 의 「아무 유닛도 안 만진다」가
이 한 줄만큼 바뀐다. 처리는 unit-of-work-dependency.md 에 있다.
8.5 C3 · C4 · C5 — .pen 을 열어 읽었다
@pen.dev/cli 로 로그인한 뒤 pen interactive --in design/enode-ux.pen --out <스크래치패드> 로 헤드리스로 열었다. MCP 는 필요 없다 — MCP 는 실행 중인
앱에 붙는 통로이고 CLI 헤드리스는 파일을 직접 연다. save() 를 안 불렀고
--out 을 저장소 밖으로 두어 원본을 안 건드렸다 (START.md §5 — design/
은 진행자만 고친다).
C3 자원 회수 · 활성 (pxDrainActive)
제목 자원 회수 (drain) · 활성
모드 배지 graceful 걸 때 고른 모드를 그대로 보인다
상태 1 지금 도는 작업 없음 — 되찾은 상태
상태 2 이 기계는 온전히 당신 것이다. 풀기 전까지 새 임대가 안 온다
버튼 drain 풀기 · 함대로 돌려주기 단 하나다. 모드 라디오가 없다
각주 푸는 순간 매칭 후보로 돌아온다 — 대기 중인 Run이 있으면 곧바로
배정될 수 있다. 걸 때 고른 모드(graceful · at-boundary)는
풀기 전까지 바뀌지 않는다
각주의 마지막 문장이 §4.3 의 라우트 설계를 정정했다. 모드 전이를 라우트가
받지 않는다.
C3 은 「자유」 국면(C5)만 그렸다. 「진행 중」 국면(C4)의 상태 문구는 그림에
없으므로 구현이 유도한다 — 임대가 남아 있으면 도는 Run 과 그 단계를 적고
「경계를 기다린다」로 쓴다. 근거는 C4 배지의 문구와 decisions.md 1절의
at-boundary 정의다.
C4 · C5 배지 — 문구가 design/README.md 와 다르다
C4 draining · 진행 중 채운 배지. $state-drain 바탕에 어두운 글자
C5 draining · 대기 중(owner) 테두리 배지. $state-drain 글자
design/README.md §2.2 는 C5 를 「draining · 대기 중」으로 적었으나 원본의
글자는 draining · 대기 중(owner) 다. (owner) 가 붙는 것이 더 정확하다 —
무엇을 기다리는지가 「소유자가 풀기」임을 그 자리에서 말한다. 원본을 따른다.
S1 카드의 배지는 지금 그냥 draining 이고 범례도 draining 이다. 구현이
C4 · C5 를 조건부로 끼운다 (design/README.md §3). 범례는 그대로 둔다 —
범례는 색의 뜻이고 배지가 국면을 말한다.
8.6 원본이 코드와 어긋난 자리 둘 — 규칙이 이긴다
.pen 을 열어 보이는 글자를 직접 확인했다. 둘 다 design/README.md §6 의
「API 값을 보이는 자리는 원문 enum 그대로」가 판정한다.
| 화면 | 원본의 보이는 글자 | 코드의 값 | 구현 |
|---|---|---|---|
| S1 Run 필터 · 배지 | SUCCEEDED |
SUCCEEDED |
맞다. 프레임 이름만 Filter SEALED 다 — §6 이 적어 둔 그것 |
| S1b Run 필터 · 배지 | SEALED |
SUCCEEDED |
SUCCEEDED 로 간다. §6 의 괄호는 S1 만 덮었고 S1b 는 보이는 글자까지 SEALED 다. SEALED 는 Run 상태가 아니다 (ADR-005 의 별도 행위) |
| S2 단계 배지 · 범례 | RUNNING (「실행 중 RUNNING」) |
CLAIMED |
CLAIMED 로 간다. steps.state 어휘는 PENDING · CLAIMED · DONE · FAILED · SKIPPED · ASKED 이고 RUNNING 은 Run 의 상태다. 단계에 RUNNING 은 없다 |
S2 범례가 넷(DONE · RUNNING · PENDING · SKIPPED)뿐이다. 여섯 중
CLAIMED(범례의 RUNNING 자리) 말고 FAILED 와 ASKED 가 빠져 있다.
구현이 둘을 더한다 — 안 더하면 실패한 단계와 되묻는 단계가 색 없이 뜬다.
.pen 자체의 스키마 흠 하나
여는 동안 검증 경고가 하나 났다.
/children/10/children/3/textGrowth
expected one of: "auto", "fixed-width", "fixed-width-height", got "auto-height"
children[10] 이 C3 이고 그 children[3] 이 각주 텍스트다. 고치지 않았다 —
design/ 은 진행자만 고친다 (START.md §5). 파일은 그래도 열리고 읽힌다.