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)
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하나로 디코드하므로 라우트 코드는
안 건드린다
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 으로 거절하지 않는다 — 광고가 곧 하트비트다 - 조인 값이 저장되고 조인 값이 반환된다 — 노드와 중앙이 같은 값으로 움직인다
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-대기열의 몫)
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)
Step 10 — Steps 가 chosen 을 싣는다
-
internal/store/observe.go의StepView에Chosen bool json:"chosen"
을 더한다.omitempty를 안 붙인다 — 없음과false가 같은 글자가
되면 읽는 쪽이 추측한다 (domain-entities.md§4.1) -
Steps의SELECT목록과Scan인자에chosen을 하나씩 더한다 - 열은 이미 있다. 스키마를 안 건드린다
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로 돌린다 - 끝에 요약을 적는다 — 검사한 규칙 수 · 건너뛴 규칙 수
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 의 지금 행 하나다