문서와 심볼

aidlc-docs 를 그대로 읽는다 · 읽기 전용
/home/sunny/enode-fixup-workshop2-visualizer연결 중
construction/plans/u-foundation-code-generation-plan.md351 줄

U-바닥 — Code Generation 계획

이 파일이 U-바닥 Code Generation 의 정본이다. 여기 적힌 단계만 실행하고,
여기 없는 것은 만들지 않는다. 단계를 끝낼 때마다 그 자리에서 [x] 로 바꾼다.

설계는 셋이다. 이 계획은 그것을 파일과 명령으로 옮긴 것이지 새 결정을 하는
자리가 아니다.

   domain-entities.md       타입과 열
   business-logic-model.md  절차와 질의
   business-rules.md        값과 금지

1. 유닛 맥락

1.1 이 유닛이 지는 FR

unit-of-work-story-map.md 의 U-바닥 블록 그대로다.

   FR-1   GET /v1/nodes 의 저장 쪽 — ListNodes
   FR-2   GET /v1/runs 의 저장 쪽 — ListRuns
   FR-3   StepView.chosen.  DB 에 있고 뷰에 없었다
   FR-6   runs.state 어휘에 QUEUED (상수와 자리)
   FR-13  정책 자리 — nodes 열과 UpsertAdvert 시그니처

라우트를 하나도 안 만든다. 끝난 시점에 밖에서 보이는 변화가 없고 기존
라우트 15개가 그대로 도는 것이 이 유닛의 성질이다.

1.2 의존

이 유닛은 아무 유닛도 안 딛는다. 첫 유닛이고, 나머지 넷이 이것을 딛는다.

   U-관측API -> U-바닥    ListNodes · ListRuns · StepView.Chosen
   U-대기열  -> U-바닥    StateQueued · DrainingNodes · ListQueued
   U-drain   -> U-바닥    contract.Policy · UpsertAdvert 의 이전 정책 반환
   U-제어판  -> U-바닥    (간접.  U-관측API 를 거친다)

그래서 이 유닛이 늦으면 전원이 선다. CP0 가 초록이어야 U-관측API 가
시작된다.

1.3 뒤 유닛에 내주는 접점

   store.ListNodes(ctx) (NodePage, error)
   store.ListRuns(ctx, RunFilter) (RunPage, error)
   store.DrainingNodes(ctx) (map[string]bool, error)
   store.ListQueued(ctx) ([]QueuedRun, error)
   store.ValidState(s string) bool
   store.StateQueued
   contract.Policy{Drain string}
   contract.Advert.Policy
   (*Store).UpsertAdvert(...) (contract.Policy, error)
   store.StepView.Chosen

WakeQueued 는 안 만든다 — queue.go 파일만 이 유닛이 만들고 함수는
U-대기열이 같은 파일에 더한다.

1.4 이 유닛이 소유하는 DB 실체

   nodes.drain          새 열.  ADD COLUMN IF NOT EXISTS 로 가산
   runs.state = QUEUED  어휘만 는다.  CHECK 가 없어 마이그레이션이 없다
   steps.chosen         이미 있는 열.  뷰에 싣는 일뿐이다

2. 파일 행렬

브라운필드다. 있는 파일은 제자리에서 고친다. _new · _modified 같은
사본을 만들지 않는다.

   신설    internal/store/list.go
           internal/store/queue.go
           internal/store/list_test.go
           internal/store/queue_test.go
           internal/store/advert_policy_test.go
           internal/contract/advert_policy_test.go
           internal/match/reject_json_test.go
           internal/boundary/imports_test.go

   수정    internal/store/schema.sql       nodes.drain 한 줄
           internal/store/store.go         UpsertAdvert 의 시그니처와 질의
           internal/store/observe.go       Steps 가 chosen 을 싣는다
           internal/contract/advert.go     Policy 타입 · Advert.Policy
           internal/match/match.go         Reject 에 json 태그 셋
           internal/api/api.go             호출 한 줄 (208)

   안 건드림 internal/api 의 나머지 · cmd/ 다섯 · packaging/ · internal/config
           internal/enode · internal/record · internal/runctl · internal/schema

internal/api/api.go 한 줄은 unit-boundaries.md §2 의 파일 행렬과 어긋난다.
근거는 business-logic-model.md §2.2 에 있고 aidlc-state.md 의 「설계 중
나온 정정 셋」이 이미 실었다. 반환값 자리 하나이고 등록 줄도 핸들러 로직도
아니다.


3. 단계

Step 1 — 스키마에 정책 열 하나

  • internal/store/schema.sql 끝의 ALTER 무리에 nodes.drain 을 더한다
  • ALTER TABLE nodes ADD COLUMN IF NOT EXISTS drain text NOT NULL DEFAULT ''
  • 주석은 domain-entities.md §1.1 의 것을 쓴다 — 왜 jsonb 가 아닌지,
    왜 없음과 "" 를 안 가르는지가 거기 있다
  • runs.state 에 CHECK 를 안 만든다. 인덱스도 안 더한다
    (domain-entities.md §1.4)

FR-13 · FR-6

Step 2 — 와이어 타입 contract.Policy

  • internal/contract/advert.go 에 Policy 타입을 더한다 —
    Drain string 하나, json:"drain,omitempty"
  • Advert 에 Policy Policy 필드를 더한다 (json:"policy,omitempty")
  • panel_token 을 안 넣는다 (R5 · SECURITY-12)
  • postNodes 가 본문을 Advert 하나로 디코드하므로 라우트 코드는
    안 건드린다

FR-13

Step 3 — 와이어 타입 테스트 (DB 불필요)

  • internal/contract/advert_policy_test.go 신설
  • policy 가 있는 광고와 없는 광고가 왕복한다
  • Policy 가 제로값이면 직렬화에서 policy 키가 빠진다
  • 옛 광고(필드 없음)를 디코드하면 Drain 이 "" 다 — R4 가 그것을
    「안 걸려 있다」로 읽는다

Step 4 — match.Reject 에 json 태그

  • internal/match/match.go 의 Reject 세 필드에 태그를 단다 —
    code · as,omitempty · reason
  • Match 의 시그니처도 로직도 안 건드린다 (NFR-I4)

FR-2 의 저장 쪽 부수 조건

Step 5 — Reject 직렬화 테스트 (DB 불필요)

  • internal/match/reject_json_test.go 신설
  • 새 태그로 {"code","as","reason"} 가 나온다
  • 옛 대문자 키 행이 그대로 읽힌다 — {"Code":409,"As":"a","Reason":"b"}
    를 언마샬해 값이 채워지는 것을 센다 (Go 의 키 대조가 대소문자 무시다)
  • 폭 상한 거절처럼 As 가 빈 경우 as 키가 빠진다

Step 6 — UpsertAdvert 가 이전 정책을 돌려준다

  • internal/store/store.go 의 UpsertAdvert 시그니처를
    (contract.Policy, error) 로 바꾼다
  • 질의를 business-logic-model.md §2.2 의 CTE 형태로 바꾼다 —
    WITH prev AS (SELECT drain FROM nodes WHERE node_id = $1) 를 앞에 두고
    RETURNING coalesce((SELECT drain FROM prev), '')
  • 트랜잭션 둘로 쪼개지 않는다. 왕복 하나다
  • 값 조이기를 이 함수 안에서 한다 (R2) — "" · graceful ·
    at-boundary 셋만 통과, 그 밖은 graceful 로 조이고 경고 로그
    (R3). 400 으로 거절하지 않는다 — 광고가 곧 하트비트다
  • 조인 값이 저장되고 조인 값이 반환된다 — 노드와 중앙이 같은 값으로 움직인다

FR-13

Step 7 — 호출자 한 줄

  • internal/api/api.go:208 의 호출을 if _, err := s.st.UpsertAdvert(...)
    로 고친다
  • 이전 정책을 여기서 쓰지 않는다 — 값을 읽어 WakeQueued 를 부르는
    것은 U-drain 의 일이다
  • 이 파일에서 그 줄 말고 아무것도 안 만진다

Step 8 — internal/store/queue.go 신설

  • StateQueued = "QUEUED" 상수. store.go 의 상수 블록에 안 둔다 —
    그 블록의 주석이 「상태 이름은 INVARIANTS §1.1 그대로다」인데 이것은
    ADR-064 가 더한 것이라 그 문장을 거짓으로 만든다
  • QueuedRun 타입 — RunID · Contract 전문 · CreatedAt.
    Principal 없음
  • DrainingNodes(ctx) (map[string]bool, error) —
    WHERE drain <> '' AND expires_at > now(). 반환 모양이 match.Match 의
    busy 와 같다
  • ListQueued(ctx) ([]QueuedRun, error) —
    coalesce(contract_versions -> -1, contract) · ORDER BY created_at, run_id
  • WakeQueued 를 안 만든다 (U-대기열의 몫)

FR-6 · FR-13

Step 9 — internal/store/list.go 신설

  • 타입 — LeaseView · NodeView · NodePage · CurrentStep ·
    RunView · RunPage · RunFilter (domain-entities.md §3 그대로)
  • LeaseRow 를 재사용하지 않는다 — Nonce 가 새어 나간다 (R14)
  • ValidState(s string) bool — 실제로 저장되는 다섯.
    RUNNING · VERIFYING · SUCCEEDED · FAILED · QUEUED.
    RESOLVING · ALLOCATING 은 안 넣는다 (R7 · 3절 정정)
  • ListNodes(ctx) (NodePage, error) — SELECT now() 를 먼저 읽고
    그다음 행 질의 (R11). LEFT JOIN leases, WHERE expires_at > now(),
    ORDER BY node_id
  • SELECT 목록에서 nonce 와 principal 을 안 고른다 (R14 · R15)
  • ListRuns(ctx, RunFilter) (RunPage, error) — 같은 순서 규칙.
    필터 셋 + LIMIT, current[] 는 상관 부질의로 (조인이면 LIMIT 이
    Run 이 아니라 단계를 센다), ORDER BY created_at DESC, run_id DESC
  • limit 조이기를 store 가 한다 (R6) — <=0 이면 100, >1000 이면 1000.
    거절하지 않는다
  • RunFilter.State 가 어휘 밖이면 오류를 낸다 (R7). 문구에 SQL 도 경로도
    안 싣는다 (R17 · SECURITY-09)
  • since 는 created_at > since — 배타 경계 (R8)
  • runs.contract 를 안 싣는다 (R16)

FR-1 · FR-2

Step 10 — Steps 가 chosen 을 싣는다

  • internal/store/observe.go 의 StepView 에 Chosen bool json:"chosen"
    을 더한다. omitempty 를 안 붙인다 — 없음과 false 가 같은 글자가
    되면 읽는 쪽이 추측한다 (domain-entities.md §4.1)
  • Steps 의 SELECT 목록과 Scan 인자에 chosen 을 하나씩 더한다
  • 열은 이미 있다. 스키마를 안 건드린다

FR-3

Step 11 — store 테스트 (DB 필요)

business-rules.md §8.1 의 「DB 필요」 열 그대로다. 기존
reap_deterministic_test.go 의 scratchDB 를 재사용한다 — 판 하나를 따로
파는 이유가 그 파일 주석에 이미 있다. 스크래치 헬퍼를 두 벌로 만들지 않는다.

  • internal/store/advert_policy_test.go 신설
    • 이전 정책이 "" -> graceful -> "" 순서로 나온다
    • 알 수 없는 값이 graceful 로 조여 저장되고 조인 값이 반환된다
  • internal/store/list_test.go 신설
    • ListNodes 가 만료된 노드를 뺀다
    • ListNodes 가 임대를 붙이고 nonce 를 안 낸다
    • ListRuns 의 current[] 가 CLAIMED 단계마다 하나다 (폭 2 로 센다)
    • ListRuns 의 필터 셋과 limit 의 기본 · 상한
    • ListRuns 가 created_at DESC, run_id DESC 로 결정적이다
    • observed_at 이 모든 expires_at 보다 앞이다 (음수 카운트다운 없음)
  • internal/store/queue_test.go 신설
    • DrainingNodes 가 만료된 draining 노드를 뺀다
    • ListQueued 가 FIFO 다
    • Steps 가 chosen 을 낸다
  • ValidState 의 다섯과 limit 경계값(0 · 1 · 1000 · 1001)은 DB 없이
    도는 자리에 둔다 — list_test.go 안의 별도 함수로
  • t.Skip 을 안 쓴다. ENODE_TEST_DATABASE_URL 이 없으면 t.Fatal
    이다 — 기존 reapTestStore 와 같은 규칙 (.ci-allowed-skips 는 비어 있다)

Step 12 — 경계 검사기

  • internal/boundary/imports_test.go 신설. 테스트 파일 하나뿐인 패키지다
  • 규칙 표는 슬라이스 리터럴 하나 (business-rules.md §7 의 넷)
  • 모듈 경로를 go.mod 에서 읽는다. 적어 두지 않는다
  • from 디렉터리가 없으면 t.Logf 로 「없음 — 검사 안 함」을 적고 넘어간다.
    t.Skip 을 안 쓴다 (R19). 없는 것을 통과로 세지 않는다 (R18)
  • 하위 디렉터리를 전부 걷는다 (R21). _test.go 도 읽는다 (R20)
  • go/parser 를 parser.ImportsOnly 로 돌린다
  • 끝에 요약을 적는다 — 검사한 규칙 수 · 건너뛴 규칙 수

NFR-P1 ~ NFR-P5

Step 13 — CP0 를 실제로 돌린다

이 단계가 이 계획의 게이트다. 명령을 적는 것이 아니라 돌려서 초록을
확인한다.

  • go build ./...
  • go test ./... && go vet ./...
  • go run ./scripts/glyphscan.go
  • golangci-lint run
  • 커버리지 게이트 — .coverage-contract.yml 의 명령 그대로
    (ENODE_TEST_DATABASE_URL · -coverpkg=./... · -count=1),
    패키지별 하한 80%
  • go test -v ./internal/boundary — 더한 줄. 이것이 없으면
    「검사했다」와 「패키지가 없어 건너뛰었다」가 똑같이 ok 한 줄로 보인다
  • curl 로 기존 라우트 하나 — GET /v1/capabilities
  • 기존 라우트 15개 회귀 0 · 기존 테스트 전부 초록

빨간 것이 남으면 다음 유닛에 안 들어간다 (scene-gates.md §4).

Step 14 — 문서

  • aidlc-docs/construction/u-foundation/code/ 에 요약 하나를 낸다 —
    만든 파일 · 고친 파일 · CP0 결과 · 뒤 유닛에 내준 접점
  • API 문서를 안 고친다 — 라우트가 안 늘었다. GET /v1/runs/{id} 응답에
    chosen 이 느는 것만 한 줄 적는다
  • README.md 를 안 고친다 — 사용자에게 보이는 변화가 없다

Step 15 — 해당 없음을 적어 둔다

규칙이 요구하는 단계 중 이 유닛에 없는 것을 이름으로 남긴다. 없다고 조용히
빠지면 다음에 읽는 사람이 빠뜨린 것인지 없는 것인지 모른다.

  • Project Structure Setup — 해당 없음 (브라운필드. 구조가 이미 있다)
  • API Layer Generation — 해당 없음 (라우트를 안 만든다. U-관측API 의 몫)
  • Frontend Components — 해당 없음 (U-바닥에 화면이 없다)
  • Database Migration Scripts — schema.sql 의 ALTER 한 줄이 전부다.
    이 저장소는 별도 마이그레이션 도구를 안 쓴다 (Migrate 가 이 파일을 돌린다)
  • Deployment Artifacts — 해당 없음 (새 실행파일 없음 · packaging/ 안 건드림)

4. 실행 규칙

   계획대로만          여기 없는 파일을 안 만든다.  여기 없는 함수를 안 더한다
   제자리에서 고친다    사본을 안 만든다.  _new · _modified 금지
   체크박스는 즉시      단계를 끝낸 그 응답에서 [x] 로 바꾼다
   상태 파일도 같이     aidlc-state.md 의 Current Status 를 함께 갱신한다
   코드는 저장소 뿌리   aidlc-docs/ 아래에 .go 파일을 안 만든다

표기와 언어 — CONVENTIONS.md 다. 주석은 한국어, 에러 문자열과 테스트
이름과 실패 메시지는 영어. 코드 어디에도 장식 문자를 안 넣는다.


5. 보안 확장 대조 — 이 유닛에서

security-baseline 이 켜져 있다. 처리는 decisions.md 3절이 미리 갈랐다.

   SECURITY-08  접근 제어    새 표면 없음 — 라우트를 안 만든다.  다만 안 내는 것을
                            SELECT 목록에서 막는다 (nonce · principal · contract)
   SECURITY-09  오류 노출    store 오류에 SQL 도 경로도 스택도 없다 (R17)
   SECURITY-12  자격증명     panel_token 이 contract.Policy 에 없다 (R5)
   입력 검증                 정책 값 셋으로 조이기 (R1 ~ R3) · limit 상한 (R6) ·
                            state 어휘 (R7)
   SECURITY-01 · 03 · 07     기존 코드 사실.  이 유닛에서 수정하지 않는다

6. 안 만드는 것

   라우트           하나도.  등록 줄은 U-관측API
   WakeQueued       U-대기열.  queue.go 파일만 이 유닛이 만든다
   정책 파일 파서    U-drain 의 internal/enode/policy.go
   페이지네이션      limit 뿐
   runs.state CHECK  없던 것을 새로 만들지 않는다
   인덱스           nodes 도 runs.state 도.  수가 커지면 그때
   내부 관측 테이블  없다.  관측 기준은 DB 의 지금 행 하나다