문서와 심볼

aidlc-docs 를 그대로 읽는다 · 읽기 전용
/home/sunny/enode-fixup-workshop2-visualizer연결 중
construction/u-foundation/functional-design/domain-entities.md318 줄

U-바닥 — 타입과 열

이 유닛은 라우트를 하나도 안 만든다. 끝난 시점에 밖에서 보이는 변화가
없고 기존 라우트 15개가 그대로 돈다는 것이 이 유닛의 성질이다
(unit-of-work.md). 그래서 이 문서가 내는 것은 화면도 응답도 아니라 다른
넷이 딛는 타입과 열이다.

질문 셋의 답은 A · A · A 다 (functional-design-questions.md).
그 값이 아래 1.2 · 3 · 4 에 들어 있다.


1. 스키마

1.1 nodes 에 정책 열 하나

-- drain 은 노드가 스스로 건 회수 정책이다 (ADR-063).
--
-- 정본은 노드의 파일이고 이 열은 광고가 나른 복사본이다 (decisions.md 1절).
-- 그래서 값이 없다는 것과 "" 를 가르지 않는다 — 광고는 델타가 아니라 매번
-- 전부이므로(ADR-012) 안 실린 것이 곧 안 걸려 있는 것이다.
--
-- capabilities 와 달리 jsonb 가 아니다. 저쪽이 jsonb 인 이유는 속성 어휘가
-- 창발하기 때문인데, 이 값은 셋으로 닫힌 열거다.
ALTER TABLE nodes ADD COLUMN IF NOT EXISTS drain text NOT NULL DEFAULT '';

값은 셋뿐이다.

   ""             안 걸려 있다.  기본값
   graceful       새 임대를 안 받는다.  도는 것은 그대로 둔다
   at-boundary    도는 단계를 끝까지 두고 결과 보고 뒤 임대를 놓는다

1.2 runs.state 어휘에 QUEUED — 스키마 변경이 아니다

runs.state 에 CHECK 가 없다. steps.state 쪽 주석이 그 이유를 이미
적었다 — 「어휘가 늘 때 마이그레이션을 강요하지 않기 위해서다」. 그래서
QUEUED 는 ALTER 한 줄도 필요 없다. 스키마 파일은 안 건드린다.

1.3 steps.chosen 은 이미 있다

schema.sql 의 마지막 ALTER TABLE steps ADD COLUMN IF NOT EXISTS chosen boolean NOT NULL DEFAULT false 가 그것이다. FR-3 은 그 열을 뷰에 싣는
일
이지 열을 만드는 일이 아니다.

1.4 인덱스를 안 더한다

DrainingNodes 와 ListQueued 가 새 조건으로 훑는다. 둘 다 안 더한다.

   nodes       PRIMARY KEY 하나짜리 표이고 함대가 이틀 안에 두 자리를 안 넘는다
   runs.state  runs_work_idx (work_id, created_at) 가 이미 있고 QUEUED 는
               전체의 소수다.  ListQueued 가 도는 것은 깨우기 여섯 지점뿐이다

수가 커지면 그때 더한다 — 페이지네이션을 안 넣은 것과 같은 조건이다
(decisions.md 2절).


2. 와이어 타입 — internal/contract

2.1 Policy

// Policy 는 노드가 스스로 건 회수 정책이다 (ADR-063).
//
// 정본은 노드의 파일이고 이것은 광고에 실려 오는 복사본이다. 중앙은
// 이 값으로 후보에서 빼고, 같은 값을 광고 응답으로 되돌려 준다 —
// 데몬이 파일을 직접 읽었는데도 응답의 값을 쓰는 이유는 Worker 가
// "Mediator 가 확인한 값" 으로 움직여야 하기 때문이다. 그러지 않으면
// 노드는 임대를 놓았는데 중앙은 아직 후보로 아는 창이 생긴다.
type Policy struct {
    Drain string `json:"drain,omitempty"` // "" | "graceful" | "at-boundary"
}

panel_token 이 없다. 정책 파일에는 있지만(unit-boundaries.md §4)
그것은 기계 하나의 로컬 비밀이라 광고에 실으면 Mediator 의 DB 로 흘러간다.
파일 타입은 U-drain 의 internal/enode/policy.go 가 따로 갖는다 — 와이어
타입과 파일 타입이 다른 것이 여기서는 흠이 아니라 경계다.

2.2 Advert 에 필드 하나 (Q1 의 답 A)

type Advert struct {
    NodeID       string       `json:"node_id"`
    Label        string       `json:"label"`
    Instance     string       `json:"instance,omitempty"`
    Capabilities []Capability `json:"capabilities"`

    // Policy 는 이 노드가 스스로 건 정책이다 (ADR-063).
    //
    // 광고에 실려 온다 — 중앙이 받는 라우트를 새로 만들지 않는 것이
    // ADR-042 방향의 핵심이다. 중앙 라우트로 받으면 토큰만 있으면 남의
    // 노드를 함대에서 뺄 수 있고, 소유의 증거가 "그 기계의 파일을 쓸 수
    // 있다" 로 닫히지 않는다.
    //
    // 비어 있으면 안 걸려 있는 것이다. 광고는 델타가 아니라 매번 전부이므로
    // (ADR-012) 안 싣는 것이 곧 푸는 것이 된다 — capability 를 빼는 것이
    // "지금은 못 한다" 가 되는 것과 같은 기계다.
    Policy Policy `json:"policy,omitempty"`
}

이 자리가 internal/api/api.go 를 안 건드리는 이유 — postNodes 가 본문
전체를 json.NewDecoder(r.Body).Decode(&a) 로 Advert 하나에 읽는다. 필드가
늘면 파싱이 저절로 받는다.


3. 조회 타입 — internal/store/list.go (신설)

Q2 의 답 A 다. store 가 페이지를 통째로 낸다.

3.1 노드

// LeaseView 는 관측된 임대다. store.LeaseRow 가 아니다.
//
// LeaseRow 에는 Nonce 가 있고 그것은 ADR-010 의 허가 아티팩트라 노드에게만
// 내려간다. 재사용하면 토큰을 가진 누구나 현황판에서 남의 nonce 를 읽는다.
// ADR-065 의 응답 예시가 둘뿐인 것이 그 뜻이다 — principal 을 안 내는 것과
// 같은 결정이다.
type LeaseView struct {
    RunID    string    `json:"run_id"`
    NotAfter time.Time `json:"not_after"`
}

// NodeView 는 GET /v1/nodes 의 노드 항목 하나다 (ADR-065 + draining).
type NodeView struct {
    NodeID       string                `json:"node_id"`
    Label        string                `json:"label"`
    Instance     string                `json:"instance,omitempty"`
    Capabilities []contract.Capability `json:"capabilities"`
    SeenAt       time.Time             `json:"seen_at"`
    ExpiresAt    time.Time             `json:"expires_at"`
    // Lease 는 응답 시점에 저장돼 있던 사실이다 (ADR-065).
    // 없다고 다음 제출에서 잡힌다는 뜻은 아니다.
    Lease *LeaseView `json:"lease,omitempty"`
    // Draining 은 납작하다. 광고 응답의 policy 와 이름이 다른 것은
    // 저쪽이 왕복이고 이쪽이 관측이기 때문이다 (requirements.md §4.2).
    Draining string `json:"draining,omitempty"`
}

type NodePage struct {
    ObservedAt time.Time  `json:"observed_at"`
    Nodes      []NodeView `json:"nodes"`
}

principal 이 없다. free · available · 순위 · 필터도 없다 (ADR-065 §2).

3.2 Run

// CurrentStep 은 지금 도는 단계 하나다 (requirements.md §8.4 ①-a).
//
// 배열인 이유 — 폭이 1 을 넘는 Run 에서 단수면 S1 카드가 남의 노드 단계를
// 그린다. CP3 의 계약이 "단계 둘 이상" 을 요구하므로 실제로 걸린다.
type CurrentStep struct {
    Node      string     `json:"node"`
    Step      string     `json:"step"` // steps.name = 계약의 steps[].id
    Attempt   int        `json:"attempt"`
    StartedAt *time.Time `json:"started_at,omitempty"`
}

// RunView 는 GET /v1/runs 의 한 행이다 (decisions.md 2절 + §8.4 ①).
//
// 본문이 없다 — StepView 와 같은 이유다. 목록에 판정의 근거까지 실으면
// "이걸로 충분한데 왜 Record 를 기다리나" 가 되고 그 순간 I4 가 형해화된다.
type RunView struct {
    RunID     string          `json:"run_id"`
    State     string          `json:"state"`
    Verdict   *Verdict        `json:"verdict,omitempty"`
    WorkID    string          `json:"work_id,omitempty"`
    CreatedAt time.Time       `json:"created_at"`
    EndedAt   *time.Time      `json:"ended_at,omitempty"`
    Assigned  []Assigned      `json:"assigned,omitempty"`
    Current   []CurrentStep   `json:"current,omitempty"`
    Reject    *match.Reject   `json:"reject,omitempty"`
}

type RunPage struct {
    ObservedAt time.Time `json:"observed_at"`
    Runs       []RunView `json:"runs"`
}

// RunFilter 는 GET /v1/runs 의 질의 인자다. 제로값이 "안 자른다" 다.
type RunFilter struct {
    State string    // "" 면 전체
    Work  string    // 정확 일치
    Since time.Time // 제로면 안 자른다.  created_at 을 자른다 (Q3 의 답 A)
    Limit int       // 0 이면 100.  1000 을 넘으면 1000 으로 조인다
}

principal 이 없다 — ADR-065 와 같은 이유다 (decisions.md 2절).

Current 에 omitempty 가 붙었다. unit-of-work.md 가 「없으면 안 낸다」로
적었다 — CLAIMED 단계가 없는 Run(대기 · 종료)에 빈 배열을 매번 싣지 않는다.

3.3 대기열 — internal/store/queue.go (신설)

// StateQueued 는 배정을 못 받아 기다리는 Run 이다 (ADR-064).
//
// store.go 의 상수 블록에 안 둔다 — 그 블록의 주석이 "상태 이름은
// INVARIANTS §1.1 그대로다" 인데 이것은 ADR-064 가 더한 것이라 그 문장을
// 거짓으로 만든다. 같은 패키지이므로 읽는 쪽은 store.StateQueued 하나다.
//
// runs.state 에 CHECK 가 없어 마이그레이션이 필요 없다.
const StateQueued = "QUEUED"

// QueuedRun 은 승격 후보 하나다. U-대기열의 WakeQueued 가 이것을 훑는다.
//
// Contract 를 통째로 싣는 이유 — 승격은 match.Match(c.Requires, ...) 를
// 다시 부르는 일이고, 폭 상한 검사와 Work 키도 계약에서 나온다.
// Requires 만 실으면 부르는 쪽이 나머지를 다시 읽어야 한다.
type QueuedRun struct {
    RunID     string
    Contract  contract.Contract
    CreatedAt time.Time
}

Principal 이 없다. 승격은 이미 있는 행의 UPDATE 라 INSERT 가 요구하던
값이 필요 없다.


4. 기존 타입에 붙는 것

4.1 StepView.Chosen — internal/store/observe.go

    // Chosen 은 이 단계가 갈림길에 골라진 적이 있는가다 (ADR-060 §3).
    //
    // 비어 있어도 내보낸다 — Needs 와 같은 이유다. SKIPPED 하나로는
    // "안 골랐다" 와 "골랐는데 못 닿았다" 가 구별되지 않고, 그 둘을 가르는
    // 유일한 글자가 이것이다. 안 내보내면 읽는 쪽이 없음과 false 를 같게
    // 봐야 하고, 그것은 추측이다.
    Chosen bool `json:"chosen"`

omitempty 를 안 붙인다. 같은 구조체의 Attempt 가 붙인 것과 갈리는데,
attempt 는 0 이 「아직 안 돌았다」로 자명하고 chosen 은 없음과 false 가
같은 글자가 되기 때문이다.

4.2 match.Reject 의 태그 — internal/match/match.go

type Reject struct {
    Code   int    `json:"code"`
    As     string `json:"as,omitempty"`
    Reason string `json:"reason"`
}

Match 의 시그니처도 로직도 안 건드린다 — NFR-I4 가 지켜진다.
unit-boundaries.md §2 의 「아무 유닛도 안 만진다」가 이 세 줄만큼 바뀌고,
그 정정은 unit-of-work-dependency.md §3 이 이미 적었다.

As 에 omitempty 가 붙은 것은 폭 상한 거절이 As 를 안 채우기 때문이다
(api.go 의 MaxPerRun 분기가 Reject{Code, Reason} 만 만든다).


5. 열과 필드와 키

   DB 열                          Go 필드                     json 키
   ────────────────────────────── ─────────────────────────── ────────────────
   nodes.node_id                  NodeView.NodeID             node_id
   nodes.label                    NodeView.Label              label
   nodes.instance                 NodeView.Instance           instance
   nodes.capabilities             NodeView.Capabilities       capabilities
   nodes.seen_at                  NodeView.SeenAt             seen_at
   nodes.expires_at               NodeView.ExpiresAt          expires_at
   nodes.drain                    NodeView.Draining           draining
   leases.run_id                  LeaseView.RunID             lease.run_id
   leases.not_after               LeaseView.NotAfter          lease.not_after
   leases.nonce                   (안 낸다)                    (없다)
   nodes.principal                (안 낸다)                    (없다)

   runs.run_id                    RunView.RunID               run_id
   runs.state                     RunView.State               state
   runs.verdict                   RunView.Verdict             verdict
   runs.work_id                   RunView.WorkID              work_id
   runs.created_at                RunView.CreatedAt           created_at
   runs.ended_at                  RunView.EndedAt             ended_at
   runs.assigned                  RunView.Assigned            assigned
   runs.reject                    RunView.Reject              reject.{code,as,reason}
   steps (CLAIMED 조인)            RunView.Current             current[]
   runs.contract                  (안 낸다)                    (없다)
   runs.principal                 (안 낸다)                    (없다)

   steps.chosen                   StepView.Chosen             chosen
   now()                          NodePage/RunPage.ObservedAt observed_at

6. 안 만드는 것

   라우트          하나도.  등록 줄은 U-관측API 의 몫이다
   WakeQueued      U-대기열.  queue.go 파일은 이 유닛이 만들고 함수는 저쪽이 더한다
   정책 파일 파서   U-drain 의 internal/enode/policy.go
   페이지네이션     limit 뿐 (decisions.md 2절)
   runs.state 의 CHECK   없던 것을 새로 만들지 않는다.  만들면 다음 어휘가
                   마이그레이션을 요구하게 된다
   내부 관측 테이블  없다.  관측 기준은 DB 의 지금 행 하나다 (ADR-065 §3)