U-관측API — Code Generation 결과
CP1 이 초록이다. 계획
aidlc-docs/construction/plans/u-observe-api-code-generation-plan.md 의 단계
열여덟을 순서대로 돌렸고 체크박스가 전부 [x] 다.
이 유닛이 밖에서 보이는 첫 변화를 낸다 — 라우트가 15개에서 18개가 되고,
Mediator 가 HTML 을 내는 자리가 처음 선다.
1. 만든 파일
internal/api/list.go getNodes · getRuns. 76줄
internal/api/list_test.go API 테스트 열셋
internal/api/ui/ui.go embed + Handler + 헤더 넷
internal/api/ui/ui_test.go DB 없이 도는 테스트 아홉
internal/api/ui/assets/index.html 뷰 셋의 뼈대 한 장 + 아이콘 SVG
internal/api/ui/assets/app.css 어두운 팔레트 · 상태 토큰 · 배치
internal/api/ui/assets/api.js 토큰 · fetch · 실패 카운트
internal/api/ui/assets/fleet.js 카드 · 배지 우선순위 · 카운트다운
internal/api/ui/assets/runs.js Run 목록 · 필터 칩 · 부제
internal/api/ui/assets/graph.js 위상 배치 · 간선 · 단계 상세
internal/api/ui/assets/app.js 진입 · 뷰 전환 · 5초 폴링
새 Go 의존이 0 이고 프런트엔드 빌드 도구가 0 이다. embed · io/fs ·
net/http 만 쓰고 go.mod 를 안 건드렸다. npm 도 번들러도 프레임워크도
외부 폰트도 CDN 도 없다.
2. 고친 파일
internal/api/api.go 하나뿐이고 임포트 하나와 등록 줄 셋이다.
mux.HandleFunc("GET /v1/nodes", s.auth(s.getNodes))
mux.HandleFunc("GET /v1/runs", s.auth(s.getRuns))
mux.Handle("GET /ui/", http.StripPrefix("/ui", ui.Handler()))
앞 둘은 s.auth 로 감쌌고 (NFR-S4) /ui/ 는 안 감쌌다 (FR-5). 화면을 위한
인증 예외 경로를 만든 것이 아니다 — /v1/* 은 그대로 토큰을 요구하고,
화면이 브라우저에서 받은 토큰을 거기에 붙인다. 정적 파일 자체는 자료를 안
나른다.
store 에 함수를 하나도 안 더했다. ListNodes · ListRuns 는 U-바닥이
이미 냈고 이 유닛은 부르기만 한다. 스키마도 안 건드렸다 — 새 열 0 개다.
3. 계획에서 갈린 것 없음 — 대신 화면이 잡은 결함 다섯
응답 계약과 화면 규칙은 계획 3절 · 4절 그대로 냈다. 갈린 자리는 없고,
돌려 보는 동안 잡힌 결함이 다섯이다. 셋은 테스트가, 둘은 화면이 잡았다.
| 무엇 | 어떻게 드러났나 | 고친 것 |
|---|---|---|
runs.js 주석이 금지어를 스스로 썼다 |
Step 14 의 어휘 검사가 runs.js 를 지적했다 — 「그 이름을 쓰지 않는다」고 적은 주석이 그 이름을 담고 있었다 |
주석을 「봉인을 Run 의 상태 이름으로 쓰지 않는다」로 고쳤다 |
| 인라인 검사가 오탐했다 | 여는 태그만 보는 정규식이 <script src=...></script> 를 인라인으로 셌다 |
여는 태그가 아니라 본문이 비었는지를 본다 |
/index.html 의 Content-Type 이 비어 온다 |
파일 서버가 /index.html 을 ./ 로 리다이렉트한다 (301) |
검사 대상을 뿌리 / 로 바꿨다 |
| 안 돈 단계에 구분자가 매달렸다 | S2 에서 ws-a · 회차 0 · 로 끝났다 — started_at 이 없는데 자리를 비워 뒀다 |
있는 것만 잇는다. where() 를 함수로 뺐다 |
chosen false 칩이 모든 단계에 붙었다 |
갈림에 없던 단계까지 「고려됐는데 떨어졌다」로 읽힌다 | chosen 이 참이거나 SKIPPED 일 때만 붙인다. 상세 패널은 언제나 그 값을 적으므로 잃는 것이 없다 |
뒤의 둘은 API 검사로는 안 잡힌다. 응답은 두 경우 모두 옳았고 틀린 것은
그것을 읽는 화면이었다 — scene-gates.md 2.1절이 화면 검증을 따로 세운
이유가 이 자리에서 실제로 값을 냈다.
4. CP1 — 돌린 결과
명령을 적은 것이 아니라 돌려서 확인했다.
go build ./... exit 0
go vet ./... exit 0
go test ./... 17개 패키지 전부 ok. 스킵 0건
glyphscan 85 파일 무결
golangci-lint run ./... (v2.13.2) 20건 — 기준선 그대로. 새 파일과
고친 파일에는 0건
커버리지 게이트 16개 패키지 전부 하한 80% 통과, 미달 0개
go test -v ./internal/boundary 2 checked, 2 skipped, 4 total
패키지 수가 늘어난 것이 둘 다 예측대로다 — 테스트가 16개에서 17개로
(internal/api/ui 신설), 커버리지 프로파일이 15개에서 16개로 늘었다
(internal/boundary 는 문장 0 이라 프로파일에 안 나온다).
4.1 커버리지
internal/api 81.3% -> 82.8% (361/436). 오류 분기 넷을 덮은 값이다
internal/api/ui 88.9% (8/9). 안 덮인 하나는 embed 실패 panic
전체 87.4% -> 87.5% (5503/6290)
계획이 얇다고 적어 둔 internal/api 의 여유가 실제로 늘었다. 400 셋과 503 을
안 덮었으면 게이트가 빨개졌을 자리다.
4.2 경계 검사기가 「없음」에서 「검사함」으로 넘어갔다
internal/api/ui 가 생기면서 규칙 하나가 실제 검사로 바뀌었다.
CP0 1 checked, 3 skipped, 4 total
CP1 2 checked, 2 skipped, 4 total internal/api/ui -> internal/store
숫자가 안 바뀌었으면 검사기가 새 패키지를 못 본 것이다. 계획이 이것을 게이트
항목으로 박아 둔 이유이고, 바뀌었다.
4.3 라우트 18개 회귀 0
살아 있는 Mediator 를 스크래치 DB 에 띄워 열여덟을 다 쳤다. mux 의
404 page not found 는 하나도 없었다 (핸들러의 404 와 본문으로 갈랐다).
없어야 하는 자리도 확인했다 — GET /v1/nope 만 mux 404 이고
DELETE /v1/runs 는 405 다.
GET /v1/nodes 200 observed_at · lease{run_id, not_after} · draining
nonce 도 principal 도 본문에 없다 (글자로 셌다)
GET /v1/runs?limit=5 200 시간 역순 · current[] · reject{code, as, reason}
runs.contract 를 안 싣는다
GET /ui/ 200 토큰 없이 온다. 헤더 넷이 붙는다
GET /ui/nope.js 404 404 에도 헤더 넷이 붙는다
GET /v1/capabilities 200 CP0 의 그 줄이 그대로 산다
GET /ui/app.js 의 Content-Type 이 text/javascript; charset=utf-8 이다.
이것이 틀리면 type="module" 이 조용히 안 실린다.
5. 화면 검증 — 브라우저에서 실제로 돌렸다
Chrome 확장이 안 붙어서 Playwright 로 헤드리스 크로미움을 띄워 돌렸다.
scene-gates.md 2.1절이 CP1 에 건 것 전부다.
S0 토큰 화면이 뜬다. 눈 아이콘이 password 와 text 를 오간다
S0b 틀린 토큰이 401 인라인 오류를 낸다. 입력 테두리가 붉고 버튼 둘이 산다
S1 카드가 노드 수만큼 (5 노드 · 5 카드)
임대된 카드에 not_after 카운트다운이 돈다 (00:40 남음)
draining 두 국면이 갈린다 — C4 진행 중(채운 배지) · C5 대기 중(owner)(테두리 배지)
at-boundary 카드가 세는 대신 「경계에서 · 최대 01:01:01」을 적는다
카드에 observed_at(머리띠)과 expires_at(카드 바닥)이 함께 얹혀 있다
마지막 갱신 시각이 머리띠에 있다 (마지막 갱신 2초 전)
만료 30초 이내 카드가 흐려진다 (광고가 12초 뒤 만료된다)
S1b Mediator 를 멈추면 폴링 셋째 실패에서 카운트다운이 멎고 배너가 뜬다
「이 화면은 <observed_at> 의 함대다. 지금의 함대가 아니다」
머리띠가 「갱신 실패 · 39초 낡았다」로 바뀐다
카운트다운이 「00:40 남았다」로 멎는다 — 시제가 바뀐다
Mediator 가 돌아오면 스스로 풀린다
S2 위상 배치가 맞다 — plan -> build.kernel -> {test.boot, test.smoke} -> seal
간선이 그려진다. 범례 여섯. 단계 배지에 RUNNING 이 없다
단계를 고르면 상세 일곱 줄이 바뀐다
produced 가 GET /v1/runs/{id}/ledger 조인으로 채워진다 (choice)
버튼 4.4절의 자리 전부를 눌렀다 — 목록은 아래
브라우저 콘솔에 CSP 위반이 하나도 없다. 새로 연 페이지의 콘솔이 완전히
비어 있었다. 인라인을 하나라도 남겼으면 브라우저가 조용히 멈췄을 자리다.
data-testid 전부가 문서에 있고 전부 걸린다.
token-input · token-reveal-toggle · token-submit · token-error
fleet-filter-{all|RUNNING|QUEUED|SUCCEEDED|FAILED}
fleet-card-{node_id} · fleet-stale-banner · fleet-updated-at
run-row-{run_id} · run-close · run-step-{step_id} · run-goto-node
QUEUED 칩은 빈 목록을 낸다. 라우트가 그 값을 받고 store 가 그것으로
거르지만 QUEUED 상태의 행이 아직 생기지 않는다 — 그것을 만드는 것이
U-대기열(CP2)이다. 화면이 「이 조건에 걸리는 Run 이 없다」를 적는다.
6. 뒤 유닛에 내주는 접점
unit-of-work-dependency.md 5절의 7 · 8 · 9 번이다. HTTP 접점이라 패키지
임포트가 아니다.
GET /v1/nodes 의 노드 항목 U-제어판 · 화면 ADR-065 모양 + draining
GET /v1/runs 의 행 화면 + current[] + reject
current[] 의 항목 화면 {node, step, attempt, started_at}
6.1 뒤 유닛이 알아야 하는 것 셋
하나 — attempt 는 0 부터다. 화면이 「회차 0」을 그대로 적는다.
claim.go:156 의 주석이 「0 부터. 재시도면 1 이상」이라 적었고, +1 을 하면
같은 화면의 단계 상세가 내는 attempt 와 어긋난다. 그림의 「회차 1」·「회차 2」
는 가상 자료다.
둘 — 임대 카운트다운의 기준은 not_after 이지 lease.ttl_seconds 가
아니다. 실측으로 not_after 가 renew_seconds * not_after_factor 만큼
앞이다. 화면은 응답이 준 값만 뺀다.
셋 — 시각을 브라우저 시계로 세지 않는다. 기준점이 observed_at 이고 그
뒤로 흐른 만큼만 더한다. DB 시계와 브라우저 시계가 다르면 카운트다운이 통째로
틀리므로 U-제어판도 같은 방식을 쓴다.
7. 정본에 아직 없다
enode-design/ 을 안 고쳤다. 정본 개정은 이 저장소의 일이 아니다
(canon.md 3절 · requirements.md 7.1절). 선행 조건 둘이 아직 착지 전이다.
ADR-065 노드 항목에 draining 을 더하는 개정
mediator-api.md 라우트 셋 — GET /v1/nodes · GET /v1/runs · GET /ui/
이 팩은 개정된 문장을 따르고 있고, 착지 전이라도 그 문면대로 구현했다.
8. 안 만든 것
계획 8절 그대로다. store 함수 0 · 새 열 0 · 페이지네이션(자리만) ·
GET /v1/nodes 의 질의 인자 0 · VERIFYING 칩 · SSE · WebSocket ·
프런트엔드 의존 · S3 · S4 · S5 · 202 와 QUEUED 분기 · drain 라우트 ·
/ui/ 인증 · StepView 의 새 필드.
README.md 도 안 고쳤다 — 워크숍 스캐폴드 설명이고 enode 사용자 문서가 아니다.
9. 규칙이 요구하지만 이 유닛에 없는 것
없다고 조용히 빠지면 다음에 읽는 사람이 빠뜨린 것인지 없는 것인지 모른다.
Project Structure Setup 해당 없음. 브라운필드이고 구조가 이미 있다
Business Logic Generation 해당 없음. 이 유닛에 업무 규칙이 없다 —
투영과 표시뿐이고 판정은 앞 유닛과 기존 코드에 있다
Repository Layer 해당 없음. ListNodes · ListRuns 는 U-바닥이 냈다
Database Migration Scripts 해당 없음. 소유하는 DB 실체가 없다
Deployment Artifacts 해당 없음. 새 실행파일 없음 · packaging/ 안 건드림
화면이 기존 Mediator 포트에 얹힌다
10. 보안 확장 대조
security-baseline 이 켜져 있고 이 유닛이 새 표면 셋을 여는 첫 유닛이다.
| 규칙 | 판정 | 근거 |
|---|---|---|
| SECURITY-04 | 준수 | 헤더 넷을 200 과 404 둘 다에서 글자로 확인했다. HSTS 는 N/A (requirements.md 3.3절) |
| SECURITY-05 | 준수 | state 어휘 · since RFC3339 · limit 정수. limit 범위는 store 가 조인다 |
| SECURITY-08 | 준수 | 두 라우트가 기존 auth 미들웨어 뒤에 있다 (테스트가 401 을 센다). /ui/ 는 무인증이지만 정적 파일뿐이다. 객체 수준 권한은 해당 없음 (ADR-015 §1) |
| SECURITY-09 | 준수 | 400 문구 셋과 503 문구 하나에 SQL · 경로 · 스택 · 받은 값이 없다. 테스트가 되비침을 글자로 센다 |
| SECURITY-12 | 준수 | 응답 본문에 nonce 도 principal 도 없다 (글자로 셌다). 화면은 토큰을 sessionStorage 에만 둔다 — localStorage 를 안 쓴다 |
| SECURITY-01 · 03 · 07 | 해당 없음 | 기존 코드 사실이고 이 유닛에서 수정하지 않았다 |
blocking 없음.
11. 커밋
코드를 커밋하지 않았다. audit.md 규약이 진행자만 커밋한다고 적었다.