kundli-rs 0.2.0

A Rust library for calculating astrological kundli charts using the Swiss Ephemeris.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
# Kundli derive 구현 개요

이 문서는 현재 `kundli-rs`의 derive 레이어가 `AstroResult`를 어떻게 Kundli 도메인 값으로 해석하는지 정리한다. 현재 구조의 핵심은 **raw astronomy 결과를 먼저 `KundliDeriveInput`으로 정규화/전처리한 뒤**, D1 / D9 / Dasha가 그 intermediate를 재사용하는 것이다.

---

## 1. 문서 범위

설명 대상 파일은 다음과 같다.

- crate root: `src/lib.rs:1`
- kundli module: `src/kundli/mod.rs:1`
- astro public API: `src/kundli/astro/mod.rs:7`
- astro result contract: `src/kundli/astro/result.rs:21`
- config: `src/kundli/config.rs:3`
- error: `src/kundli/error.rs:5`
- model: `src/kundli/model.rs:3`
- derive module: `src/kundli/derive/mod.rs:1`
- derive intermediate: `src/kundli/derive/input.rs:11`
- derive helpers:
  - `src/kundli/derive/sign.rs:18`
  - `src/kundli/derive/nakshatra.rs:24`
  - `src/kundli/derive/house.rs:23`
- D1 assembly: `src/kundli/derive/d1.rs:12`
- D9 assembly: `src/kundli/derive/d9.rs:9`
- Dasha assembly: `src/kundli/derive/dasha.rs:10`
- integration tests:
  - `tests/derive_d1.rs:1`
  - `tests/derive_d9.rs:1`
  - `tests/derive_dasha.rs:1`
- smoke test: `tests/astro_smoke.rs:148`

---

## 2. 구현 목표

derive 레이어의 역할은 Swiss Ephemeris 기반의 raw 결과인 `AstroResult`를 Kundli 의미 정보로 해석하는 것이다.

현재 제공하는 결과는 다음과 같다.

- `LagnaResult`
- `PlanetPlacement`
- `HouseResult`
- `D1Chart`
- `D9Chart`
- `VimshottariDasha`

핵심 원칙은 다음과 같다.

1. `astro` 레이어는 천문 계산만 담당한다.
2. `derive` 레이어는 해석과 조립만 담당한다.
3. 공통 longitude 해석은 `KundliDeriveInput`에서 한 번 수행한다.
4. D9와 Dasha는 현재 sidereal 입력만 지원한다.
5. D9 하우스는 현재 WholeSign만 지원한다.

---

## 3. 현재 모듈 구조

### 3.1 공개 모듈

`src/lib.rs:1`

```rust
pub mod kundli;
```

`src/kundli/mod.rs:1-5`

```rust
pub mod astro;
pub mod config;
pub mod derive;
pub mod error;
pub mod model;
```

### 3.2 astro 공개 surface

`src/kundli/astro/mod.rs:7-10`

```rust
pub use engine::{AstroEngine, SwissEphAstroEngine, SwissEphConfig};
pub use error::AstroError;
pub use request::{AstroBody, AstroRequest, Ayanamsha, HouseSystem, NodeType, ZodiacType};
pub use result::{AstroBodyPosition, AstroMeta, AstroResult};
```

### 3.3 derive 모듈 구조

`src/kundli/derive/mod.rs:1-7`

```rust
pub mod d1;
pub mod d9;
pub mod dasha;
pub(crate) mod house;
pub(crate) mod input;
pub(crate) mod nakshatra;
pub(crate) mod sign;
```

외부 호출자가 직접 쓰는 public entry point는 현재 다음 세 모듈이다.

- `kundli::derive::d1`
- `kundli::derive::d9`
- `kundli::derive::dasha`

반면 아래는 derive 내부 구현용이다.

- `input`
- `sign`
- `nakshatra`
- `house`

---

## 4. 핵심 계약

### 4.1 AstroResult

`src/kundli/astro/result.rs:21-28`

```rust
pub struct AstroResult {
    pub bodies: [AstroBodyPosition; AstroBody::ALL.len()],
    pub ascendant_longitude: f64,
    pub mc_longitude: f64,
    pub house_cusps: [f64; 12],
    pub meta: AstroMeta,
}
```

`AstroResult`는 더 이상 request-shaped partial result가 아니라 derive가 바로 소비하는 canonical full snapshot이다. `bodies`는 항상 `AstroBody::ALL` 순서를 따르고, `house_cusps`도 항상 12개를 보장한다.

derive가 실제로 사용하는 필드는 다음과 같다.

- `bodies`
- `ascendant_longitude`
- `house_cusps`
- `meta.jd_ut`
- `meta.zodiac`
- `meta.ayanamsha`, `meta.ayanamsha_value`, `meta.sidereal_time`는 현재 `KundliDeriveInput.meta`로 보존되지만, derive 계산 핵심 분기에는 직접적으로 많이 쓰이지 않는다.
- `mc_longitude`는 현재 derive 레이어에서 사용하지 않는다.

### 4.2 KundliConfig

`src/kundli/config.rs:1-101`

현재 `KundliConfig`는 여전히 duplicated astro settings를 보존하지만, 권장 조립 경로는 struct literal보다 생성 메서드다.

```rust
let request = AstroRequest::new(jd_ut, latitude, longitude)
    .with_zodiac(zodiac)
    .with_ayanamsha(ayanamsha)
    .with_house_system(house_system)
    .with_node_type(node_type);

let config = KundliConfig::from_request(&request)
    .with_charts(&[
    KnownChart::D1,
    KnownChart::D9,
    KnownChart::VimshottariDasha,
]);
```

이 패턴의 핵심 의도는 request/config에 중복된 zodiac-related 설정이 처음부터 일치하도록 만드는 것이다.

현재 derive 레이어에서 직접 의미가 있는 값은 주로 `house_system`이다.

- D1: `house_system`에 따라 WholeSign / cusp 기반 분기
- D9: `house_system == WholeSign`인지 검증

`charts` selection은 현재 derive 함수 내부에서는 소비되지 않는다. 이 값은 상위 orchestration에서 어떤 chart layer를 `KundliResult.charts` 컬렉션에 조립할지 결정할 때 의미를 가진다.

### 4.3 결과 모델

핵심 결과 타입은 `src/kundli/model.rs:85-167`에 정의되어 있다.

특히 중요한 계약은 다음과 같다.

- `PlanetPlacement`
  - body / longitude / sign / degrees_in_sign / house / nakshatra / is_retrograde
- `HouseResult`
  - house 번호는 pipeline reference 기준으로 부여된다.
  - `cusp_longitude`는 materialize된 house의 시작 longitude를 뜻한다.
  - cusp 기반 system에서는 절대 cusp를 reference-relative로 재번호화한 결과를, WholeSign에서는 reference가 속한 sign boundary 기준 house 시작점을 나타낸다.
- `VimshottariDasha`
  - `moon_nakshatra`
  - `current_mahadasha`
  - `mahadashas` 전체 시퀀스

### 4.4 에러 계약

`src/kundli/error.rs:5-129`

derive 전용 에러는 다음과 같다.

- `MissingMoon`
- `InvalidHouseCusps(usize)`
- `InvalidLongitude(f64)`
- `InvalidPada(u8)`
- `UnsupportedZodiac(ZodiacType)`
- `UnsupportedD9HouseSystem(HouseSystem)`

의미는 다음과 같다.

- `MissingMoon`: Dasha 계산에 Moon이 없을 때
- `InvalidHouseCusps`: cusp 기반 하우스 계산에 cusp 개수가 12가 아닐 때
- `InvalidLongitude`: NaN / infinity 등 비정상 longitude
- `InvalidPada`: pada 생성 불가
- `UnsupportedZodiac`: D9 / Dasha가 sidereal만 허용할 때 tropical 입력이 들어온 경우
- `UnsupportedD9HouseSystem`: D9에서 WholeSign 외 house system이 들어온 경우

추가로 high-level API는 `KundliError::InputConfigMismatch(InputConfigMismatchField)`를 통해 request/config 불일치를 typed하게 노출한다.

- `InputConfigMismatchField::Zodiac`
- `InputConfigMismatchField::Ayanamsha`
- `InputConfigMismatchField::HouseSystem`
- `InputConfigMismatchField::NodeType`

즉, 소비자는 에러 문자열 파싱 대신 enum pattern match로 어떤 public field가 어긋났는지 식별할 수 있다.

---

## 5. 중심 intermediate: KundliDeriveInput

현재 구조에서 가장 중요한 변화는 `src/kundli/derive/input.rs`에 intermediate가 생긴 점이다.

### 5.1 구조

`src/kundli/derive/input.rs:11-37`

- `PreparedAngle`
  - `longitude`
  - `sign`
  - `degrees_in_sign`
- `PreparedBody`
  - `body`
  - `longitude`
  - `sign`
  - `degrees_in_sign`
  - `nakshatra`
  - `nakshatra_progress_ratio`
  - `is_retrograde`
- `KundliDeriveInput`
  - `meta`
  - `ascendant`
  - `bodies`
  - `house_cusps`

즉, derive 레이어는 더 이상 매 chart 함수에서 raw longitude를 처음부터 반복 해석하지 않고, 먼저 **준비된 ascendant/body snapshot**을 만든다.

### 5.2 from_astro

`src/kundli/derive/input.rs:39-51`

`KundliDeriveInput::from_astro(&AstroResult)`는 다음을 한 번에 수행한다.

- ascendant longitude normalize
- ascendant sign / degrees_in_sign 계산
- 각 body longitude normalize
- 각 body sign / degrees_in_sign 계산
- 각 body nakshatra / pada 계산
- 각 body `nakshatra_progress_ratio` 계산
- 각 body retrograde 여부 계산 (`speed_longitude < 0.0`)
- `house_cusps``meta` 보존

### 5.3 body lookup

`src/kundli/derive/input.rs:53-55`

```rust
pub(crate) fn body(&self, body: AstroBody) -> Option<&PreparedBody>
```

Dasha가 Moon을 찾을 때 이 lookup을 사용한다.

### 5.4 to_navamsa

`src/kundli/derive/input.rs:57-68`

D9 전용 변환은 `d9.rs`가 아니라 `input.rs`에 있다. `to_navamsa()`는 base input에서 navamsa 변환본을 만든다.

핵심 규칙:

```rust
normalize_longitude(longitude * 9.0)
```

이 변환은 ascendant와 모든 body에 적용된다. 변환 후에는 다시 아래 값들을 재계산한다.

- longitude
- sign
- degrees_in_sign
- nakshatra
- nakshatra_progress_ratio

`is_retrograde`는 그대로 보존한다.

`house_cusps`는 현재 그대로 복사되지만, D9는 WholeSign만 허용하므로 실제 하우스 계산에는 사용되지 않는다.

### 5.5 intermediate 도입의 의미

이 intermediate 덕분에 다음이 가능해졌다.

- D1이 sign / nakshatra 재해석 없이 조립에 집중
- Dasha가 Moon의 nakshatra/progress를 바로 재사용
- D9가 base input과 navamsa input을 명확히 분리
- 내부적으로 `*_from_input` 함수를 만들어 공통 해석과 chart 조립을 분리

---

## 6. helper 레이어

### 6.1 sign helper

파일: `src/kundli/derive/sign.rs:18`

제공 함수:

- `normalize_longitude(longitude)`
- `sign_from_longitude(longitude)`
- `degrees_in_sign(longitude)`

역할:

- 모든 longitude를 `[0, 360)`으로 정규화
- longitude → sign 변환
- sign 내부 degree 계산

규칙:

- finite 값이 아니면 `InvalidLongitude`
- 음수 longitude 허용 후 normalize
- `360`, `720` 등은 `0`으로 normalize

### 6.2 nakshatra helper

파일: `src/kundli/derive/nakshatra.rs:24`

제공 함수:

- `nakshatra_from_longitude`
- `pada_from_longitude`
- `degrees_in_nakshatra`
- `nakshatra_placement_from_longitude`
- `nakshatra_progress_ratio`
- `dasha_lord_for_nakshatra`

역할:

- longitude → nakshatra / pada 해석
- nakshatra 내부 progress 계산
- nakshatra → dasha lord 매핑

규칙:

- 먼저 `normalize_longitude` 재사용
- nakshatra span = `360 / 27`
- pada span = `nakshatra / 4`
- progress ratio는 `[0.0, 1.0)`

이름이 예전의 Moon 전용 helper가 아니라 **generic `nakshatra_progress_ratio`**인 점이 현재 구조를 잘 보여준다. Dasha는 Moon에 적용하고, D9는 navamsa 변환 후 다시 계산한다.

### 6.3 house helper

파일: `src/kundli/derive/house.rs:23`

제공 함수:

- `derive_house(planet_longitude, ascendant_longitude, house_cusps, house_system)`

역할:

- D1 planet placement house 계산
- D9 planet placement house 계산

분기:

- `WholeSign`
  - ascendant sign anchoring
- 그 외 house system
  - `house_cusps` 기반 판정

추가 규칙:

- cusp 기반 계산은 wrap-around 처리
- non-WholeSign이어도 ascendant longitude를 먼저 normalize/validate
- cusp 개수가 12가 아니면 `InvalidHouseCusps`

---

## 7. D1 구현

파일: `src/kundli/derive/d1.rs`

### 7.1 entry points

- internal `derive_lagna_from_input`: `src/kundli/derive/d1.rs:12`
- public `derive_lagna`: `src/kundli/derive/d1.rs:22`
- internal `derive_planet_placements_from_input`: `src/kundli/derive/d1.rs:27`
- public `derive_planet_placements`: `src/kundli/derive/d1.rs:53`
- internal `derive_houses_from_input`: `src/kundli/derive/d1.rs:61`
- public `derive_houses`: `src/kundli/derive/d1.rs:107`
- internal `derive_d1_chart_from_input`: `src/kundli/derive/d1.rs:115`
- public `derive_d1_chart`: `src/kundli/derive/d1.rs:126`

현재 D1의 public 함수들은 모두 다음 패턴을 따른다.

1. `KundliDeriveInput::from_astro(astro)` 생성
2. `*_from_input(...)` 호출

### 7.2 lagna

`derive_lagna_from_input`은 이미 준비된 `input.ascendant`를 그대로 `LagnaResult`로 옮긴다.

즉, D1 lagna 단계에서는 별도의 longitude 재해석이 없다.

### 7.3 planet placements

`derive_planet_placements_from_input`은 각 `PreparedBody`를 순회하며 `PlanetPlacement`를 만든다.

사용 값:

- `body.body`
- `body.longitude`
- `body.sign`
- `body.degrees_in_sign`
- `body.nakshatra`
- `body.is_retrograde`

추가로 house만 `derive_house(...)`로 계산한다.

즉, D1의 planet placement는 **snapshot 재사용 + house 조립** 구조다.

### 7.4 houses

`derive_houses_from_input`은 `config.house_system`에 따라 분기한다.

- WholeSign
  - ascendant가 속한 sign boundary를 1st house 시작점으로 사용
- cusp 기반
  - `input.house_cusps`를 normalize해서 `HouseResult` 생성

`HouseResult.cusp_longitude` 의미는 model의 주석 계약과 일치한다.

### 7.5 D1 chart 조립

`derive_d1_chart_from_input`은 다음 세 결과를 합친다.

- lagna
- planets
- houses

---

## 8. D9 구현

파일: `src/kundli/derive/d9.rs`

### 8.1 entry points

- internal `derive_d9_chart_from_input`: `src/kundli/derive/d9.rs:9`
- public `derive_d9_chart`: `src/kundli/derive/d9.rs:29`
- internal `derive_d9_planet_placements_from_input`: `src/kundli/derive/d9.rs:34`

### 8.2 처리 순서

`derive_d9_chart_from_input`의 실제 순서는 다음과 같다.

1. sidereal guard 검사
2. `config.house_system == WholeSign` 검사
3. `input.to_navamsa()` 호출
4. navamsa input으로 lagna 계산
5. navamsa input으로 planet placements 계산

### 8.3 핵심 특징

#### 8.3.1 navamsa 변환 위치

D9의 navamsa 변환 로직은 `d9.rs`가 아니라 `input.rs`에 들어 있다. 따라서 D9 구현은 변환 로직 자체보다 **guard + transformed input 조립**에 가깝다.

#### 8.3.2 lagna 재사용

D9 lagna는 별도 전용 함수가 아니라 `d1::derive_lagna_from_input`를 그대로 재사용한다 (`src/kundli/derive/d9.rs:3`, `src/kundli/derive/d9.rs:24`).

즉, lagna 해석은 "어떤 input을 넣느냐"의 문제이고, D1/D9가 서로 다른 전용 lagna 로직을 갖는 구조가 아니다.

#### 8.3.3 house 정책

D9 planet placement는 다음 호출로 house를 계산한다.

```rust
derive_house(body.longitude, input.ascendant.longitude, &[], HouseSystem::WholeSign)
```

즉:

- D9는 항상 WholeSign
- cusp는 쓰지 않음
- navamsa lagna를 anchor로 삼음

### 8.4 지원 범위

- sidereal만 허용 (`UnsupportedZodiac`)
- WholeSign만 허용 (`UnsupportedD9HouseSystem`)

---

## 9. Vimshottari Dasha 구현

파일: `src/kundli/derive/dasha.rs`

### 9.1 entry points

- internal `derive_vimshottari_dasha_from_input`: `src/kundli/derive/dasha.rs:10`
- public `derive_vimshottari_dasha`: `src/kundli/derive/dasha.rs:52`

### 9.2 처리 순서

1. sidereal guard 검사
2. `input.body(AstroBody::Moon)`으로 Moon lookup
3. Moon의 `nakshatra`에서 현재 dasha lord 계산
4. Moon의 `nakshatra_progress_ratio`로 현재 mahadasha 시작점 역산
5. `DashaLord::SEQUENCE`를 현재 lord 위치부터 순환
6. 9개 mahadasha timeline 생성

### 9.3 기간 계산 정책

`src/kundli/derive/dasha.rs:7-8`

```rust
const DAYS_PER_YEAR: f64 = 365.25;
```

Vimshottari의 연 단위를 Julian day로 매핑할 때 tropical year 근사값 365.25일을 사용한다.

### 9.4 현재 구조의 장점

Dasha는 raw Moon longitude를 다시 해석하지 않는다. `KundliDeriveInput`에 들어 있는 Moon snapshot을 그대로 사용한다.

재사용되는 값:

- `moon.nakshatra.nakshatra`
- `moon.nakshatra_progress_ratio`
- `input.meta.jd_ut`

---

## 10. 전체 호출 그래프

```mermaid
graph TD
    A["AstroResult"] --> B["KundliDeriveInput::from_astro"]

    B --> C["Prepared ascendant"]
    B --> D["Prepared bodies"]
    B --> E["house_cusps/meta passthrough"]

    D --> F["sign/degrees"]
    D --> G["nakshatra/pada"]
    D --> H["nakshatra_progress_ratio"]
    D --> I["retrograde flag"]

    B --> J["d1::*_from_input"]
    B --> K["dasha::derive_vimshottari_dasha_from_input"]
    B --> L["to_navamsa"]

    L --> M["navamsa PreparedInput"]
    M --> N["d9::derive_d9_chart_from_input"]

    J --> O["D1Chart"]
    K --> P["VimshottariDasha"]
    N --> Q["D9Chart"]
```

핵심은 다음 두 단계 분리다.

1. **해석 단계**: `AstroResult -> KundliDeriveInput`
2. **조립 단계**: D1 / D9 / Dasha

---

## 11. 현재 public wrapper 기준 실행 시퀀스

현재 public API는 `*_from_input`를 외부에 노출하지 않으므로, 각 public 함수는 자기 안에서 `from_astro()`를 다시 호출한다.

```mermaid
sequenceDiagram
    participant Caller
    participant D1 as derive::d1 public API
    participant D9 as derive::d9 public API
    participant Dasha as derive::dasha public API
    participant Input as KundliDeriveInput

    Caller->>D1: derive_d1_chart(&astro, &config)
    D1->>Input: from_astro(&astro)
    D1->>D1: derive_d1_chart_from_input(...)
    D1-->>Caller: D1Chart

    Caller->>D9: derive_d9_chart(&astro, &config)
    D9->>Input: from_astro(&astro)
    D9->>Input: to_navamsa()
    D9->>D9: derive_d9_chart_from_input(...)
    D9-->>Caller: D9Chart

    Caller->>Dasha: derive_vimshottari_dasha(&astro)
    Dasha->>Input: from_astro(&astro)
    Dasha->>Dasha: derive_vimshottari_dasha_from_input(...)
    Dasha-->>Caller: VimshottariDasha
```

즉, **내부 구조는 intermediate 재사용형이지만, 외부 public wrapper 레벨에서는 아직 `from_astro()`가 호출마다 반복된다.**

이 점 때문에 향후 상위 orchestration 함수가 생기면 다음 개선 여지가 있다.

- `from_astro()`를 한 번만 수행
- 같은 prepared input을 D1 / D9 / Dasha에 fan-out

---

## 12. 테스트 구조

### 12.1 unit tests

현재 unit test는 helper와 intermediate에 붙어 있다.

- `src/kundli/derive/sign.rs:81`
- `src/kundli/derive/nakshatra.rs:181`
- `src/kundli/derive/house.rs:126`
- `src/kundli/derive/input.rs:114`

검증 내용:

- longitude normalization
- sign boundary
- nakshatra / pada boundary
- nakshatra progress ratio
- dasha lord mapping
- WholeSign / cusp 기반 house 판정
- wrap-around 처리
- `KundliDeriveInput::from_astro()` snapshot 생성
- `KundliDeriveInput::to_navamsa()` 변환과 재계산

### 12.2 integration tests

- `tests/derive_d1.rs:1`
- `tests/derive_d9.rs:1`
- `tests/derive_dasha.rs:1`

검증 내용:

- D1 chart 조립
- non-WholeSign house 시스템 처리
- D9 navamsa 변환
- D9 sidereal / WholeSign guard
- Dasha current period와 full sequence
- missing Moon / invalid longitude 등 에러 케이스

### 12.3 smoke test

- `tests/astro_smoke.rs:148`

검증 내용:

- 실제 astro engine 결과가 derive public API로 이어지는지
- D1 / D9 / Dasha end-to-end가 모두 성립하는지

---

## 13. 현재 제약 사항

현재 구조는 의도적으로 다음 제약을 가진다.

1. `calculate_kundli()` 같은 최상위 orchestration 함수는 아직 없다.
2. D9는 sidereal + WholeSign만 지원한다.
3. Dasha는 sidereal만 지원한다.
4. public wrapper는 호출마다 `KundliDeriveInput::from_astro()`를 다시 만든다.
5. `include_d9`, `include_dasha`는 현재 derive 함수 내부에서는 사용되지 않는다.
6. `mc_longitude`는 현재 derive 레이어에서 사용되지 않는다.

---

## 14. 현재 구조를 읽는 추천 순서

빠르게 이해하려면 아래 순서로 읽는 것이 좋다.

1. `src/kundli/astro/result.rs:21`
2. `src/kundli/config.rs:3`
3. `src/kundli/model.rs:85`
4. `src/kundli/error.rs:5`
5. `src/kundli/derive/input.rs:11`
6. `src/kundli/derive/sign.rs:18`
7. `src/kundli/derive/nakshatra.rs:24`
8. `src/kundli/derive/house.rs:23`
9. `src/kundli/derive/d1.rs:115`
10. `src/kundli/derive/d9.rs:9`
11. `src/kundli/derive/dasha.rs:10`
12. `tests/astro_smoke.rs:148`

이 순서대로 보면 입력 계약 → intermediate → helper → chart 조립 → end-to-end 흐름이 자연스럽게 이어진다.