문서와 심볼

aidlc-docs 를 그대로 읽는다 · 읽기 전용
/home/sunny/enode-fixup-workshop2-visualizer연결 중
construction/u-observe-api/code/code-generation-summary.md251 줄

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 규약이 진행자만 커밋한다고 적었다.