From 4d1daab35aa98f8f605c4c4085cb72c5cecb7e36 Mon Sep 17 00:00:00 2001 From: migatu Date: Wed, 5 Aug 2026 22:38:43 +0200 Subject: [PATCH] =?UTF-8?q?feat(domy):=20warianty=20od=20Barana=20i=20od?= =?UTF-8?q?=20MC=20+=20obr=C3=B3t=20kosmogramu=20=E2=80=94=20LOG-05=20zamk?= =?UTF-8?q?ni=C4=99te?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ostatnie trzy pozycje z treści LOG-05, których wcześniej nie było: - whole_sign_aries — znaki jako domy, ale dom I ZAWSZE na 0° Barana, niezależnie od Ascendentu (tradycja indyjska i część szkół hellenistycznych), - equal_mc — równe domy zakotwiczone na MC: dom X zaczyna się dokładnie na południku, a nie ma go gdzieś w środku, - obrót kosmogramu: Ascendent albo 0° Barana po lewej stronie koła. Zmienia WYŁĄCZNIE rysunek, żadna liczba nie jest przeliczana. Wariant „od Barana" unieruchamia koło względem zodiaku, więc dwa horoskopy da się porównać na oko. Oba nowe systemy zgodne z wyrocznią co do zera od pierwszego uruchomienia. Razem 13 systemów: 2 833 424 porównania w przemiale, zero przekroczeń. PRZY OKAZJI — DWA BŁĘDY, KTÓRE SAM WPROWADZIŁEM I KTÓRYCH TESTY NIE WIDZIAŁY: - linia zbierająca ostrzeżenia o fallbacku trafiła do handlera strony głównej zamiast do compile_pdf: odwoływała się do nieistniejącej zmiennej, czyli 500 na stronie głównej, a do PDF-a ostrzeżenia nie docierały wcale, - compile_build używał parametru formularza, którego nie miał w sygnaturze. Oba przeszły przez komplet zielonych testów, bo żaden nie wywoływał POST-a — testy prezentacji sprawdzały teksty w szablonach i w main.py. Doszły więc testy uderzające w prawdziwe trasy (POST / i POST /compile ze stubowaną logiką), które łapią tę klasę błędu. Z tego samego powodu przepisane dwa testy PDF-a: greppowały z main.py dokładny kształt wywołania render(chart, theme="print") i pękały przy dopisaniu argumentu, mimo poprawnego zachowania. Teraz wołają trasę i sprawdzają, że KAŻDY z czterech rysunków dostaje motyw druku. LOG-05 i PRE-05 → Zrobione w docs/astrololo_wymagania.xlsx. Co-Authored-By: Claude Opus 5 --- docs/astrololo_wymagania.xlsx | Bin 38637 -> 39046 bytes services/logic/app/engine/houses.py | 15 +++- services/logic/tests/test_houses.py | 19 +++++ services/presentation/app/chartwheel.py | 70 +++++++++++------- services/presentation/app/house_systems.py | 4 +- services/presentation/app/main.py | 26 ++++--- .../app/templates/_form_options.html | 7 ++ services/presentation/tests/test_compile.py | 49 +++++++++--- .../presentation/tests/test_house_compare.py | 58 +++++++++++++++ .../presentation/tests/test_render_link.py | 8 +- tests/oracle/README.md | 4 +- tests/oracle/harness.py | 8 +- tests/oracle/run.py | 2 +- 13 files changed, 214 insertions(+), 56 deletions(-) diff --git a/docs/astrololo_wymagania.xlsx b/docs/astrololo_wymagania.xlsx index e8260d67378056ae6fee947fe0c8e6ff35a6ea5a..17f2259feef943e14e4bcc93b6fc1c44d1a66324 100644 GIT binary patch delta 21603 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 delta 21088 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

6jmBy z8I)vbHV(kgmPU+)_i3=AJ{@=q1F8f~xQ?n-STF~L1@FZi@ff>_M3RRg*u6WiW41Cs zQq%3j-G&KM*vXI;76!aH%lk2svaY0%1~!8DU7P z1RU%%9_oT3dzqHGgT-jHaPgm?VNa6=mb^qntgGeQ`eJhjE2B0btE1|BnNf`rs88S# zv4yhaD8-lHF6D3wILrOx-MxM9<}S~O@LnYSLg&u%?|D8QKs&r4`p=Ch5geEDZkS8b zt@;7PF){WzN@cj?cv|S=3L4nvh7o;dO^1R0`~cro=5jNT`#cRJd++@;`j9fqV-!Xd zHmkAWj?xaQ*VFu~KWlZtQB6~Q;3n#0SVS`%U4PigH*XlZyyr_$RszR58vVY8&KBU2 zu#!Bfk=tg+0XD&Hg#e3|elB6vizj)Z3nj^xJEZB`!)tme?gl1b_VHc5oN4@!pcnA!+0vASdxvUG`o*SJjO?nz?gt zF1kN+;(v`CBIjFX+>^`1AKx$PnLCG3+~gzB@)+Pt1382^O7@22g^ZWi=~?oZUv%+q zj2lGfde|zjFL~dC_2Nrfg}tf0l2;EOMiD&NT6J4IP;(URj9As=+ChcGvLLxlE9%{a z=pt3_x@20&`~rr9fP7h=g(46=~>l*>G(RRE{xLQj`HtHB5CA| z&Lid`KnS0g^-K+Krni*SpZ%Y?FwyA6JqOAXAf7l*e1tK1Hwjg2{*xEn%^HtR2whFt zaly51xqqOGHV9N6r-o;jV~`qL5}tqO6nzsapPELbc(zl!g~_`>LHthlp}~LUIyhnu zj$RgEc^kjwS+@^O;}`B|93sKP9qCmBx2{gcN&O5xXb1DO_2)Ztb=WUvzKs90#MsI* zaLK6T7O!W$ncl`xm$`(MOF_JF4j$=_fQ^`8+b0Qu47062O0cD=5+Do}M4`J{nh##m zwhQ+2S=t&NH>(;HH*3x2f}7fcNZ8&^G5hN`ICB`aPt0wCC!dIJAhtsd@4h9BvD(A6 z(E(VjJx%9uF!c^=;~0hcnH*e~Xlr)Imw(5_eG_mB^6{#3X~w`0bP=){pSjM%k#!o zb;Kh+Ptl3g2$dwfOsy558>)%E6}? z9u}qxGji5GmeJbV)L*yTH%a2yGeM{Gofh*F0s*SdUcf!n_qIF9^`T=g`sZg0b_V4o zGzUe@HLWQQg+P=)ah!qi~kU&Pk)`JN>ZdCb;e`o*CDxl)EsIj^RETo`hu<^)hUrXV6 zLkP*BhrVUk3WzmuVcW&+)#)%IC20Qo-ixD^2g0u+5B=fglA3P~y1bXwNLGbb82(+V zM_2jg?s3ajv7R}I9u9Hr$lf$|K-|cnMygA?eK6#F=o&t!mRA52X?s{uvRE*F@Ir){ z;1xj(NvuVFNR&Wb^8^e+EE=sv=5)xXhgYplto|g%Asqiav}d32SVdqn7^=ZoW1B|6e5j6jD<*5 zA4PXU%4prNs)+)0DH~IyRbGBi!X>N9-UN#Yur;R zTjF!z(dVDkGhZfm1hZ&L=ix%kf)kk4i3n_T<-X|;EK)^@-5JG!`CjWsQ84TkPdrai zCXhaSQ3jxSF7q$Lkg+X?zdaD4)QXI59wxp#uxBbw1rPw$(;qr?+JOUrUwVy0-R{8)41!I>~1#hG;Sps+KOh-4B@zwThs5PLV_?OGx|X!;6yv(K32# zvr~RNQIVdWe%Ak9cpWwC$$w!s(7O>fdnBxRJS#^{|3|*lS(*u6jFVH^9y*xHzZ%b) zPWsy#K#l>qV$MrY?D9X**7|gGHjs7TV`RO<3oW`Df9>%KRUl9nOZbMz<>#<;1**$3 z-wKO~)sxLeaNg zr^{}bsY(@(v#L*1kXMC2rTn!%8`}-~kwh&KRVS9HO^~{_1R1!IC?tzmpm7A49etEq z7B|sy)yTUAxyn`4&^jbMq@yQ!Rs1oOr^BkzSB>7R3$X;bwLCdVE-D1#4fMB30iKwa z56UgXrJF%B}o7 zM^?X*D33H*2l-`z)ZytuCeG7i%70>McHiT*y}DH^HNF0jqH|)G@O9Po>Ccm_o9V|c ztJ3b)^_Wwp%j&U6+RZ6SDlw^z)};oZKV1=*3lr;=)m8qD`L9nvO9lm!(Q4*&CFTQD zju%xj_JF96)?k|_rnH|N9EDG`X|LCERibwGzA0k%{%|CltMbKVasAFm<)PiOG}0bv zHO~kqCPew^BEJ45j`<}O8rvDHs2QKRE z@5spd*_kbXtY_)iO!p~Q(^1ZMK=c?WC#ZG$j*3A>%i|k~Af1PaD6OwN(^x0*AyI_+ z0TO9%0MVu0_*AO|Ry~sSZvc894fNh~GfQuvqw>hjh}&PxND$lZI>*YqK^?gG-WnkE z02>M97Ril-Xb;X9?=Z*n;hdZo>zT{;TKyeajO1&$uEFTi)TJM5?t__KHAs@%BNX=w z1^&UVITxLd7)UnpK57Z3m&#VvDR=EWk^jWIG}pl_Iz%m_Q!-0JCEY@L<54QT zmA6Ev3YI-P*rGf;A_E6G7U790L(%zV`c9-%oyf|hp<6+z7wQvJ!Vg%rghN|7Z*_KG zy5RRKC5Lw!3akljJe2?89f%{<$3%6HQv+i|$7nj>W0>JXlTO*<B=k&eC<5=vxk?kt>=fv7^OK>)~~_JbV`X*xn4<7V7QdOW&rN@RMA&gp>; zAL+kwo!5GtJ3zo1_g?3dNYUuAcF(VT*7Cxa^R3(&Za@?s#eS7p5F^9Os6w#bbvcn*v>u}@9w4nMvFgXOl4FO zDldurIlaS0P-+9apcV(AAQWp#nXsZw#y>b9Q|Hgg`KCkWoHX-7#n%*-1%qv&@On#` zGIYtkWZ8YL2B3I(GW4>6zrNp5BJO{evD?0T0rqXX8O&J1%s^O;`S|&S1n;nuNXF05 z4l;&BYapcm@%$PfAn*r;FaDDghzC7872$LfYq2*!fiCTjJBK`GeN`_?onW^PUkMpw zh3ptW7b*@iMmNL60enVlksvbSXIBK7O%HcNDutuv}X=mM;sxBAYarJw5F_Q+T!nFwPfBvru&DRwf3re&v#X)&y#E+sLK7v1a$`1+VoK;N7g zV*K^o5T@s0V=ptxG~@cj#>NE43?VKT$LsV?lQ#7RaxD4u0piF3xA`F^0{0Fd>16`r zrT5=uf~ot0ktC0Ik6==Mgx`~Nu(WoyQAtNSSE2_>dd0K+%ybaZv6!CQBa<^{?F4Qp zaQ~FHf4Ow7F2b;ypo-`g5IZFaYMk8 z!X7NLH$fc7^_mRTP`2d-xMIZE`Ga1{l-y%H_!cm?P_E}^j%??-x@(w>_>B%O`jwJ= zg2y<=Ij6mCWSJqsXRyMc`f>Tc3{BE->EqhrJ&a3V8?X8Cm^&vF9pnjeKZ&%t{qMt@ z9v63s2?=tM@BadxPTr25(QWz09WI>wyq$-~syNGniivUY*+Evo!e;#uSPFYOQ%1Om zoi2+g^_?ud*6O+Y=&c4URg>(oGNybdNU|8YEkOt_=+ks!GrQ z%=AY`xraN(M!Bhd?6QMju-Cc5>A_t@3HT-TWeBqf`P%T9B1@=r;6L^MC=yjpWYg*`q~ zY!vrKCk)vFaFzzKm~J+n(35)7_P9hfB6i18O(_8%5aa7T?p6H9%}t{;WrUYBR8(eN zDgLDoRrcLav5%fokyaVv%1xqkNQt#J=Cq#G5N++;h3Zo^4AcoW96GTQfETwPXAOLD z;3mGZI2fOAEtO80C`;Q~C6WHz#KYoaT`pmcY@7ms^t|qz^vd3TiF13($X@+7tg-Zn zYk|P-?6I=gLSvF?(VhE8)}4!Q%_4IxroNSf4KsAS5S{vtYhk9#Jr=R=? zWphQ3?rRJPz9{k#HRm9exko8h2#+gXmiJrV_1w9FKRE-gEj^Hn@a$Bwi0gE5W@I;& zdUinpWI0%Q!D9=Jc$nl)8JGHJA_yO55yQMjv1}tm+hN%RRXZmwa4A_*@dz`xOb}cn zDD*8{b^_At+N6&nVlkP@xY~UsLs@*z*h5DSSm_LdJ3?5+fC1CMxvD^UMnQ;k2SZvD z)BrJiu+OH)v~cM0Ixc1e5NOKo6{~dK0?bIjG3S`&*;m#%iD1z@&phsT3}%fWbOEZA z(BDQJfgW^9WJ?e9#o^FxVx?%_CB9pe#t8|>#f+~&rHZ|1Sn6+(6iJ6 zbtg-Pm?$>|PSMTCAp(LNX?G9T`C_nl|nISA0#OwOz; z{EAX@6u-&{k%svnFlAALDzB6t!wIWh6WnsEonuO;<{je+%VT97uH8c@2$lLT25PEU{%2U5;m5L)MjS2qf2MV|aWl4e7|1$w># zuYhPXh!vgGpBC#nLL~FpnSp2?a*?=AUxh4=yDIg?XK(Inuels(8&B?)$9(z8mY!_r zFB^U~Wdp6?XXPUdiYF z!PDRU3B_Gb>gd0<3|!V;j`^HGbInmHL2MY)sV!wUMAGb)mjJdDIcq}e-7Mx*T=-qf zM2+A%Zt_QV*};79d*+3p^1JG-(_Ag6BWmt}#ycM~dd8_u9nVrMj_c4^oiloqi3Tb; zaU@0|T*Jjzv6`2f!_K*Tv;Xon=*JLKMV_39XA%9)d}^qBHQi2g;9nykn_$@a4}>NE z?zBi`!1N-U>civ}n4+rVKi5u$CiBiJKu(CKku|RUg`H%Mp1id8ZE_>a4caF#Job&Z zDr_BfDT)gRX5R&h^j4!O>Qv$*ujwo8Zc4do zSGL{)eom&|oa{HIFe+9@TPAc{n8GAYfeMXa=syKv>>`SVPs;8Ce zD~kKfZcrl{9;{qf8PoD6A2aHV4Y$7!2$Oq4S@njw=A!~W&Ynkry+{xT)UdYa+UnMo z0(?B3uAr6Wde}4CWs63q_T?L%_ua7fx$&r`!`b}ZdHFU{cZ8NH6y9-xu`_&%B}cZw zueq~KsX?^w7=Zv{b5AXKva8_)9^wwwh&aKBnAk(Hh_c20yTWa~d8TB$1rGiecT1)l z5(q1)aO9d%hh%+w_yi-hHicg&UZny_pqf_nqOoG!9#-W=3cq6i3U}{K2#eE1pTLRt z^gO@q^QVrFxII#;u*gpGb82Xn3VVAokT^=-LEn9)C~G}J;SuiV^7DT$ek1>%Z!3C8 zc}z}4L?9rBCjV=D9?_r&-j$Pj+qb`tU{>DwHAzsSx5xOX?o>FvwO#&208+g!m7Nx0 z7w{UOwlg&fZp$6Z9*oPMKUN#^bm>weZwE=Fd%HbyN}F=#shw)osFU3IRZ#sq7M!S6 ze50v~OtI+BS(R|2TRAz2Vua7dUVGnz`gmjd-i0l!_QWpOv81r*!dMx5)dOeyy4$?! z-Vdg;bjvp`rhYay$>%Fgy^;@F z7{|C8A7P$~*BYJ?GlL!cOZH{buh@<<))5Vj21Yg0(gLv1D3*bB35_hJtMcXtpe3rk zO_N;C=C5B%X4$kuy$L;fOI@(cAGx}|?$@JS$K{mipeNbWKPkuioXBTqB>RAvJ>$R6 zT*pR;#fD*^-Qi5^M9p90-v76uO;xDzl(Ox1>6(3!ry&i|R;g$Ayzu>;GGDh`L&v&s zgB?xluR_ly;rnrAzW0a&Blr;$nfo`+k)qa=Ukw-v(aABYnx)XJDF%M1m%78g+!OXy zxz-GF)a4=?^^<#Snh|w95eOH2G51a3sEc*6NWC}&CGFsTc-*OHmqdlwihI9f`PT)z zc13)&LY#k)B_01a1u7+RYSV&}B^haB1M5t`PW$}7%P7UpiQd^6;m2fr@%|mOaG_y` zTlhFIFEQFXHJkL4u(p_P;M~r`=$q~pRuPk9GZ0*mx8OIS5&|>GlAD7 z&n}^Z|6F0yI8C4yqaCBYTUp7%Xb2BwGU|}ySrd7oTrsi5mCFWJMDWg4DUdT&f%>ev zaDRHTL^Xj&wA5s}qQQ<@$W+9E$c*FOGGR2qOjw0t+EfaNfHutkyoCeH6;=mlx=C=` z-8#2(N6;;lznE=&7D^^`P8H7o<0Zj_p?rRct z*H5FGL*(1Nnyi(-ZpcPyvA?d^zfAb=eeX!Wk1f_d60vQSpKb~md$L~rl_%BQFoMyT zTj*DY>wvdc?XiX#s@y6RS_sdsG_JSTV}Xuij2*~a@%d6xU93E#7Y3>r0Q9tprnnKu zEnX~_*obHkG7juC)URa;sHM8+L4!WUp6TrlAF$)u62GWSnI+lf8&1o?g`beSR0bF zds2d>9K8O%VGX_Bz-YbQyCLSx5dQCCG`6pvQP#mjttYx7Nhlfs)B;VZU2LC4_u~&R zACtEfm$w4V>u(IyAdK1u!s#r@nA*{Vx^C!@)6v^H$@ z*-ahLUZ| zPnuhJJ@X3I-gB&jntFd#ccf<%vRWR@Z)^D&$#0s5()E;O_oRhv*9GU^bXt-N!0>fZ zxHXjrs?aAhrugVqZ5{HywMfX(u8OSHILCEeI|$L6vZ?_7(lSpWu@i5Zt~D0j*f=EW z?$yEw!raUEFoIa$`cqs>38&T93vX}pC&l7IVeqE9(Y4*T$-M8E!1uOg>A^=6NT6|K zRn>g|`+y2znO3oQpNdmuFaEWGPeZKunEqbLYLUi<+5}Ag{!iBI>)tKsYXmSjFidhB zO6i{AdsCoD_?5Y1yW&4wxtIUT;+WnhMURj^T9lm+2Y-UdiYLfcZwP|54wd0j{n~=kbMy*X#O4t=z1fO!Y8J$^e2M|U^_uSbT0Q%9ap@|X?#V{L0_CQ#3iB2d)^4&pd zp@U3VArk)8?S5O?Nl?#nZCK3$%xKf~EJ~FiKjap&UYjJeLUEA7c|{AyCP zql(xQ-s#8AL0Rk(^;4y37v@=CItJ4x;ze?=iuzdB8R3zwGgCRH=kziYX@vf#)MpO? z@W^QRH<}_|_~3)RLV4bxt*6LqTk-bj5Uy2a@`YaKRddk~WQqWoe%1J@!d?F+G&dH@ihD9GV)IRwdRe+P71R z;18hwirz__N+Ucd51kM;qy><^{z-YrQfn&<l2Jy9}1q|`mPPp@cI z(~;avR-p1DqZkPi3miN=iWD0(Tn&c@Lt|~`@*y%^OW(xcR>CN1aPNeWHaIILWk6t3 ze&m*hp=VC2oT>p~Bmj&+&tkz4%H|6D;0NgR>j331b*U9g0Lt%~uAhf_Hk#y3AV43U z3NhDO`rgKUwR>Osuyhu9oo+@?79?MD@4%pwPLcR|yV!2Zy+EGyU-MTWqqSD*q+{^C zTKqUI3$RobV=ljjZs?%c8};qCtHeZ^&gd)Q&mCrXfHnGIJ4bNi^n(Fk+eigxT#_(g z{>L38ITe`>FqA_T=pkl3%*E$6ItffPOo}^x09oYmoCH|A8@UdqSUJXx^u~j?bu62;P zj6zHfurOvE zM;P)xkgr@hAEs?c!!sR{+p^^216AawcIrsf0dZgit>UqC^-z9Cr#z^TpnljnO(nO3 zsaw+H1ow2)t-6(k@jW=4m!v7hTdTJfV6RPTQOJ zb~B)$Z|hcA{Sc-Z#UPrI^bWn>#Jt50TnqX;q}EEMN;8%vpQ5K~{ zuO#tXsp!StnUHAQGKkK_b&->l)diNiD?RT#yN2;7U{I=Kp&x{;~)tiCq z*Y0Jf7suUbVbT*j%EVW*fJ*|dU)nv)@&uaH#4ljAH5(1fJBK6O1|@-Yb}G};Y;MB7 zQmK*6-?zX>|FD3)o#V9xa{sX4{r$sb(ns#}#%P8=>dl4r*?^q6)2!dZ>xRcr(pe4x!x>4vRQUhbs?KH)Qv)AE%xX zczuP^RgKKFdmb-Tq^HJNaIKzH^NprakKyu-GQ*-z z2hBN~dSNuR`{Lkd@i|x+16+_HE%R7@RJ3SkaM050nLZG(oT!ry=cm`PH2ex)S(l{0 zklD&)XbCcVP7E#=qh8u91lcj>gt&W6dwY9rlWX3bvSW4r}S)D5OY4 zmE>rL(!Sa&ZTcut2-L8uw9q4|+GSZmLMBmoubYHBSLkQ)w~BFL6&c|4(@khMOAkkn zsQOV(3cAPop$O%Os-4j;J<*T($SR4Q`!=AX!b`R6$+3@_8|2eD2 zdW0kVnR?$gH&Kwn)sM;L>Jz!IY`aqvy!H~C6mPBtI#O8nn|+!NV*}G1u#|CQ0~!Di zZA_eg_~p4)X1Q7kyg=?gzGCI%j?#X9NwVDPNg{|=;U+@f0g-9y1>t9W={1vd`W7jz zi5t9~lyUliFf-7z3gXIZ+#BY z%UitR4K1a+6)z+rysiRO0}*)3v&I(_T&hxHw<0B)uIy>-;qE$c51@ofJNcGHQ3~r^{fgEOD`*9t4aTHLW6!4v|jf;X?vS5CKhNntoHyw_hK; zmZi1H{JHwaP7XGCpfb{5>O1Vpj}JXyRVs?JJ~pl;8-s$`=t9W73Gv3#NaT5!k=eNI zvuP0{yp&H*15efoS6Z=V9iH6i<$aGe6kmwI}Ih$XWoq3eMDz)YW7lWrgoZDQ)b=MV+V8s{$ zAJ?-4;BhEg(uAJr&($V_wi zz@5pwGyLAI4u>xuvT_yRd)9@CTXKR9aw>(a%%J{?ojwTW!Bbz7AevjN&U7JqDPMR`iG4*XwpYR{M}>O3LZu8Z`KE*}fT8sqwwA;;t*b?TWtl*sNwX zj?~B|{d7<6kdZY-nO|%(AZEmmN@0nCrTOOswO1e9$rDNXG!+Q23!c|LqzyiM5#}Ob ztD;(nMyuD7;&$?fCU4D=U6NU-ny;MwTzvgme*c6|!|>F& zmaHFGjx`mav@kCCT>IG$hqL$t$Eu`7{AB2>uISs}KVGqX9%{K*B)cSkmK)p4lvqq`tHtebDp;tHfHkC@Jr271 zsc&sSDTvWFc|5j^YTSnee+4gghrWfy7#ItBE|0Y?~iUjHv1`2WE;b`_O@=*Ott|woFV=6seKFH7=9k@J1{Pz zUiuK3max)p&%ka6S-vs%XH5vj6_2M4Ha`ab?EB0<1HAKG$TYRE%uZC;YnxWEG2$F_ z&@LtWQ*DZ?{mHHMH%xB#(+oKu61%U~SsHG~c3Y3DRSM(roS zd;rt67gW+*vMIF1IcCsV6!r^ z_7OfvV6lx4@=r+->=~j8t++;&a$1ET&IDMja!=l>Aiy5{OsYNVs2pbRWH4;hbwk!i zoa>a?-wI|yWS2i6Go*91obtAWD{n(yngfD4&B3F>9OiSzZFZCgupu<$6vXrP+Q5(v z3()Kpzu_cuj9zQR^WetTXUAkkMr7`lHk7nJZbM~_72tl~YSEA{sao228Z>xxzkT4cB1e{1IGw72s9h4fPTQH_B_5DD zWgs)&abz9aYUbj95XV1DA98%AS>rw>LzW;@;@=Gr+$`VBez!~Og9XJdi6dPHHg00h z;%i4N6qk*96;GRo9*)oZNfVMyCi$pG_TN4`cDuI*c)0(WLW5gN_2V~&gVtOC-+CRS zs1N__Ddp{mIM@_LjV+R?JVhI?+gVC)){MGpG#cEA(W-?+6riFt-v#U^b+H? zl&P~_zDgp_Qkm^dQu&mtb~6lgU`S2C<&8UbxB^-_SQhnys8`Ce7AZ(X=82U{hfZC}?6 z<6vP)=CY=DWjO=Qc_BR45BGvpdE=)aeB{O5``%}T4jaW>4iW{ji(r(3xR7HAGWX;X&D2*md;g@{ImaANDc`(O6s)&)l$7$sJHFYUcbSEiK&lcrXn_klO zzJwbm*{jK3eb+fT`WNS_HR^%EcUm)sY!&mIm|)%<7}_XvQXAKYL$0F)^f1@94lFB$ zO1Hz9PCviq@zolfEGj_h@jgw5s;mhYj=eFPMvYjd9eIWOjqfY?xL?Ek%$?e55|g z`QwX2{z5g5;PjfTjoaQMinv9ta@clW!e#&S+JBrt68D0M^yI&T^N9GEU78D#l`=^F zm%zBBu3Z$2(!_ulA{V9vZ4MP float: @@ -458,6 +461,14 @@ def cusps_for(ramc: float, eps: float, lat: float, system: str) -> list[float]: mc = compute_mc(ramc, eps) if system in (WHOLE_SIGN, EQUAL, PORPHYRY): return cusps(asc, mc, system) + if system == WHOLE_SIGN_ARIES: + # Znaki jako domy, ale numeracja rusza od Barana niezależnie od Ascendentu. + # Wariant spotykany w tradycji indyjskiej i w części szkół hellenistycznych. + return [norm360(30.0 * i) for i in range(12)] + if system == EQUAL_MC: + # Równe domy jak `equal`, ale zakotwiczone na MC: dom X zaczyna się + # DOKŁADNIE na MC, więc oś południka wypada na granicy domu, a nie w środku. + return [norm360(mc + 90.0 + 30.0 * i) for i in range(12)] if system == VEHLOW: # equal, ale Ascendent leży w ŚRODKU domu I, nie na jego początku return [norm360(asc - 15.0 + 30.0 * i) for i in range(12)] diff --git a/services/logic/tests/test_houses.py b/services/logic/tests/test_houses.py index 598ba79..cdfbd55 100644 --- a/services/logic/tests/test_houses.py +++ b/services/logic/tests/test_houses.py @@ -230,3 +230,22 @@ def test_topocentric_needs_no_domain_limit(): assert len(H.cusps_for(100.0, 23.4393, lat, "topocentric")) == 12 cs = H.cusps_detailed(100.0, 23.4393, 89.9, "topocentric") assert not cs.is_fallback and cs.notice is None + + +def test_whole_sign_aries_ignores_the_ascendant(): + """Wariant „od Barana": dom I zaczyna się na 0° Barana niezależnie od tego, + co wschodzi. To odróżnia go od zwykłego whole sign.""" + for lat in (0.0, 50.06, -33.9): + c = H.cusps_for(100.0, 23.4393, lat, "whole_sign_aries") + assert c == [pytest.approx(30.0 * i) for i in range(12)] + + +def test_equal_mc_starts_the_tenth_house_exactly_on_the_midheaven(): + """W `equal` MC leży GDZIEŚ w domu X; w `equal_mc` zaczyna go dokładnie.""" + ramc, eps, lat = 100.0, 23.4393, 50.0 + mc = H.compute_mc(ramc, eps) + c = H.cusps_for(ramc, eps, lat, "equal_mc") + assert c[9] == pytest.approx(mc, abs=1e-9) # dom X rusza na MC + assert c[0] == pytest.approx(H.norm360(mc + 90.0), abs=1e-9) + for i in range(12): # nadal równe 30° + assert H.norm360(c[(i + 1) % 12] - c[i]) == pytest.approx(30.0, abs=1e-9) diff --git a/services/presentation/app/chartwheel.py b/services/presentation/app/chartwheel.py index 6581267..5bd9cbd 100644 --- a/services/presentation/app/chartwheel.py +++ b/services/presentation/app/chartwheel.py @@ -114,8 +114,15 @@ def _aspect_pen(orb: float, allowed: float | None) -> tuple[float, float]: return 0.7 + 1.0 * frac, 0.35 + 0.5 * frac -def _pt(lon: float, asc: float, r: float) -> tuple[float, float]: - phi = math.radians(180.0 - (lon - asc)) +# Co stoi po lewej stronie koła (LOG-05: „warianty 0°Aries / fixed Asc"). +ASC_LEFT = "asc" +ARIES_LEFT = "aries" +ORIENTATIONS = ((ASC_LEFT, "Ascendent po lewej"), (ARIES_LEFT, "0° Barana po lewej")) + + +def _pt(lon: float, ref: float, r: float) -> tuple[float, float]: + """Punkt na kole dla długości `lon`, przy `ref` po lewej stronie rysunku.""" + phi = math.radians(180.0 - (lon - ref)) return _CX + r * math.cos(phi), _CY + r * math.sin(phi) @@ -206,12 +213,19 @@ def _delta(a: float, b: float) -> float: return abs(((a - b + 180.0) % 360.0) - 180.0) -def render(chart: dict, theme: str = "screen") -> str: +def render(chart: dict, theme: str = "screen", orientation: str = ASC_LEFT) -> str: """Zwraca SVG koła albo '' gdy brakuje danych (silnik bez osi/domów). theme='screen' — kolory ze zmiennych CSS aplikacji (dostraja się do motywu). theme='print' — konkretne kolory na białym tle; konieczne, gdy SVG trafia do samodzielnego konwertera (PDF), który zmiennych CSS nie rozwiąże. + + orientation — co stoi po LEWEJ stronie koła (LOG-05, warianty rysunku): + 'asc' — Ascendent (domyślne; tak rysuje większość szkół zachodnich, + dom I zawsze zaczyna się w tym samym miejscu rysunku), + 'aries' — 0° Barana (koło nieruchome względem zodiaku, więc dwa horoskopy + da się porównywać „na oko"; osie wypadają za to gdzie indziej). + Zmienia się WYŁĄCZNIE obrót rysunku — żadna liczba nie jest przeliczana. """ T = _THEMES.get(theme, _THEMES["screen"]) # tylko dla druku — na stronie font podaje CSS (.glyph) @@ -223,6 +237,9 @@ def render(chart: dict, theme: str = "screen") -> str: return "" asc = float(angles["Asc"]["decimal"]) + # Oś obrotu rysunku. Długości ekliptyczne zostają nietknięte — przesuwamy + # tylko punkt, który ląduje po lewej stronie koła. + ref = 0.0 if orientation == ARIES_LEFT else asc parts: list[str] = [] # tło i okręgi @@ -244,8 +261,8 @@ def render(chart: dict, theme: str = "screen") -> str: continue color = T["aspects"].get(asp.get("aspect"), T["muted"]) w, op = _aspect_pen(float(asp.get("orb", 0.0)), asp.get("allowed")) - ax1, ay1 = _pt(la, asc, R_HUB) - bx1, by1 = _pt(lb, asc, R_HUB) + ax1, ay1 = _pt(la, ref, R_HUB) + bx1, by1 = _pt(lb, ref, R_HUB) tip = (f'{asp.get("obj1")} {_ASPECT_PL.get(asp.get("aspect"), asp.get("aspect"))} ' f'{asp.get("obj2")} · orb {float(asp.get("orb", 0.0)):.2f}°') parts.append(_line(ax1, ay1, bx1, by1, color, w, op, title=tip)) @@ -253,17 +270,17 @@ def render(chart: dict, theme: str = "screen") -> str: # drobne podziałki co 5°, mocniejsze co 10° (na pasie znaków) for deg in range(0, 360, 5): r_in = R_TICK if deg % 10 else R_TICK - 4 - x1, y1 = _pt(deg, asc, R_ZOD) - x2, y2 = _pt(deg, asc, r_in) + x1, y1 = _pt(deg, ref, R_ZOD) + x2, y2 = _pt(deg, ref, r_in) parts.append(_line(x1, y1, x2, y2, T["line"], 0.6, 0.7)) # pas znaków: granice co 30° + glif znaku w środku sektora, kolorem żywiołu for i, sg in enumerate(signs): b = 30.0 * i - x1, y1 = _pt(b, asc, R_ZOD) - x2, y2 = _pt(b, asc, R_OUT) + x1, y1 = _pt(b, ref, R_ZOD) + x2, y2 = _pt(b, ref, R_OUT) parts.append(_line(x1, y1, x2, y2, T["line"], 1.0)) - gx, gy = _pt(b + 15.0, asc, R_SIGN) + gx, gy = _pt(b + 15.0, ref, R_SIGN) parts.append(_text(gx, gy, sg.get("glyph") or "", size=17, fill=T["elements"][i % 4], cls="glyph", font=glyph_font)) @@ -271,31 +288,31 @@ def render(chart: dict, theme: str = "screen") -> str: n = len(cusps) for i, c in enumerate(cusps): lon = float(c["decimal"]) - x1, y1 = _pt(lon, asc, R_ZOD) - x2, y2 = _pt(lon, asc, R_HUB) + x1, y1 = _pt(lon, ref, R_ZOD) + x2, y2 = _pt(lon, ref, R_HUB) parts.append(_line(x1, y1, x2, y2, T["line"], 0.8, 0.85)) nxt = float(cusps[(i + 1) % n]["decimal"]) span = (nxt - lon) % 360.0 or 360.0 mid = lon + span / 2.0 - hx, hy = _pt(mid, asc, R_HNUM) + hx, hy = _pt(mid, ref, R_HNUM) parts.append(_text(hx, hy, str(c["house"]), size=11, fill=T["muted"])) # Stopień cuspu tuż przy szprysze, na pasie. W whole sign wszystkie cuspy # są na 0° znaku — wtedy pomijamy, żeby nie kłaść dwunastu zbędnych zer; # w systemach kwadratowych ta liczba realnie coś mówi. deg_in_sign = lon % 30.0 if deg_in_sign >= 0.5: - cx1, cy1 = _pt(lon, asc, R_ZOD - 9) + cx1, cy1 = _pt(lon, ref, R_ZOD - 9) parts.append(_text(cx1, cy1, str(int(deg_in_sign)), size=6.5, fill=T["muted"])) # osie: Asc–Dsc i MC–IC przez całe koło, wyróżnione akcentem for a, b, la, lb in (("Asc", "Dsc", "AC", "DC"), ("MC", "IC", "MC", "IC")): lon_a = float(angles[a]["decimal"]) - ax, ay = _pt(lon_a, asc, R_ZOD) - bx, by = _pt(lon_a + 180.0, asc, R_ZOD) + ax, ay = _pt(lon_a, ref, R_ZOD) + bx, by = _pt(lon_a + 180.0, ref, R_ZOD) parts.append(_line(ax, ay, bx, by, T["accent"], 1.6, 0.9)) # etykiety POZA kołem — w środku kolidowały z glifami obiektów - lax, lay = _pt(lon_a, asc, R_AXIS_LABEL) - lbx, lby = _pt(lon_a + 180.0, asc, R_AXIS_LABEL) + lax, lay = _pt(lon_a, ref, R_AXIS_LABEL) + lbx, lby = _pt(lon_a + 180.0, ref, R_AXIS_LABEL) parts.append(_text(lax, lay, la, size=10, fill=T["accent"])) parts.append(_text(lbx, lby, lb, size=10, fill=T["accent"])) @@ -310,15 +327,15 @@ def render(chart: dict, theme: str = "screen") -> str: for obj, true_lon, draw_lon in zip(objects, true_lons, draw_lons): retro = obj.get("direction") == "Rx" - mx1, my1 = _pt(true_lon, asc, R_ZOD) - mx2, my2 = _pt(true_lon, asc, R_MARK) + mx1, my1 = _pt(true_lon, ref, R_ZOD) + mx2, my2 = _pt(true_lon, ref, R_MARK) parts.append(_line(mx1, my1, mx2, my2, T["ink"], 1.0, 0.75)) if _delta(draw_lon, true_lon) > 0.4: - lx, ly = _pt(draw_lon, asc, R_PLANET + 12) + lx, ly = _pt(draw_lon, ref, R_PLANET + 12) parts.append(_line(mx2, my2, lx, ly, T["line"], 0.7, 0.9)) - gx, gy = _pt(draw_lon, asc, R_PLANET) + gx, gy = _pt(draw_lon, ref, R_PLANET) tip = obj.get("name") or "" if obj.get("in_sign"): tip += f" · {obj['in_sign']}" @@ -332,7 +349,7 @@ def render(chart: dict, theme: str = "screen") -> str: # Sam stopień w znaku, bez „°" — tak robią programy astrologiczne i tylko # tak podpisy mieszczą się obok siebie w skupiskach. Retrogradacja: ℞ oraz # kolor, żeby dało się ją wyłapać nie czytając znaku po znaku. - dx, dy = _pt(draw_lon, asc, R_DEG) + dx, dy = _pt(draw_lon, ref, R_DEG) label = f"{int(true_lon % 30)}" + ("℞" if retro else "") parts.append(_text(dx, dy, label, size=8, fill=T["retro"] if retro else T["muted"])) @@ -346,10 +363,10 @@ def render(chart: dict, theme: str = "screen") -> str: if not glyph or lot.get("longitude") is None: continue lon = float(lot["longitude"]) - mx1, my1 = _pt(lon, asc, R_ZOD) - mx2, my2 = _pt(lon, asc, R_MARK) + mx1, my1 = _pt(lon, ref, R_ZOD) + mx2, my2 = _pt(lon, ref, R_MARK) parts.append(_line(mx1, my1, mx2, my2, T["accent"], 1.0, 0.7)) - lx, ly = _pt(lon, asc, R_LOT) + lx, ly = _pt(lon, ref, R_LOT) tip = lot.get("name") or "" if lot.get("in_sign"): tip += f" · {lot['in_sign']}" @@ -367,7 +384,6 @@ def render(chart: dict, theme: str = "screen") -> str: # ── aspektarian (PRE-18) ────────────────────────────────────────────────── ASP_CELL = 30.0 # bok komórki siatki aspektów - def render_aspectarian(chart: dict, theme: str = "screen") -> str: """Aspektarian (PRE-18) — trójkątna siatka aspektów obiekt×obiekt jako SVG. diff --git a/services/presentation/app/house_systems.py b/services/presentation/app/house_systems.py index 0683a21..3475948 100644 --- a/services/presentation/app/house_systems.py +++ b/services/presentation/app/house_systems.py @@ -15,8 +15,10 @@ from __future__ import annotations HOUSE_SYSTEMS: list[tuple[str, str]] = [ ("placidus", "Placidus"), ("whole_sign", "Whole Sign"), + ("whole_sign_aries", "Whole Sign (od Barana)"), ("koch", "Koch"), - ("equal", "Equal"), + ("equal", "Equal (od Asc)"), + ("equal_mc", "Equal (od MC)"), ("porphyry", "Porphyry"), ("regiomontanus", "Regiomontanus"), ("campanus", "Campanus"), diff --git a/services/presentation/app/main.py b/services/presentation/app/main.py index 5021ec1..95a8280 100644 --- a/services/presentation/app/main.py +++ b/services/presentation/app/main.py @@ -22,6 +22,7 @@ from fastapi.responses import HTMLResponse, JSONResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates +from app import chartwheel as chartwheel_mod from app.house_systems import HOUSE_SYSTEMS, LIMITED as HOUSE_LIMITED, label as house_label from app import geocode, security @@ -60,6 +61,7 @@ templates.env.globals["static"] = static_url templates.env.globals["HOUSE_SYSTEMS"] = HOUSE_SYSTEMS templates.env.globals["house_label"] = house_label templates.env.globals["HOUSE_LIMITED"] = HOUSE_LIMITED +templates.env.globals["WHEEL_ORIENTATIONS"] = chartwheel_mod.ORIENTATIONS def _build_utc(date: str, time: str, tz_offset: float) -> tuple[str, str]: @@ -130,12 +132,14 @@ def chart_compute( stations: bool = Form(False), zodiac: str = Form("tropical"), tables: bool = Form(False), + wheel_orientation: str = Form(chartwheel_mod.ASC_LEFT), ): form = {"person": person, "date": date, "time": time, "tz_offset": tz_offset, "lat": lat, "lon": lon, "house_system": house_system, "house_systems": house_systems, "aspect_orb": aspect_orb, "aspect_luminary_bonus": aspect_luminary_bonus, "aspect_minor": aspect_minor, - "stations": stations, "zodiac": zodiac, "tables": tables} + "stations": stations, "zodiac": zodiac, "tables": tables, + "wheel_orientation": wheel_orientation} ctx: dict = {"form": form, "result": None, "error": None, "moment": None} try: iso_utc, label = _build_utc(date, time, tz_offset) @@ -147,12 +151,8 @@ def chart_compute( aspect_minor=aspect_minor, stations=stations, zodiac=zodiac, tables=tables, ) - # Fallback systemu domów musi dojechać do PDF-a — z samego kosmogramu - # nie da się poznać, że podział jest z innego systemu, niż zamówiono. - warnings = [w for w in (chart.get("house_warnings") or []) if w] - from app import chartwheel # kosmogram (PRE-12), SVG po stronie serwera - ctx["wheel_svg"] = chartwheel.render(ctx["result"]) + ctx["wheel_svg"] = chartwheel.render(ctx["result"], orientation=wheel_orientation) ctx["aspectarian_svg"] = chartwheel.render_aspectarian(ctx["result"]) # PRE-18 ctx["declination_svg"] = chartwheel.render_declination(ctx["result"]) # LOG-07 (etap 6) ctx["antiscia_svg"] = chartwheel.render_antiscia(ctx["result"]) # LOG-07 (etap 6) @@ -189,6 +189,7 @@ def compile_build( stations: bool = Form(False), # LOG-03 — stacje też w podsumowaniu zodiac: str = Form("tropical"), tables: bool = Form(False), # LOG-23 — żywioły/faza/godziny w podsumowaniu + wheel_orientation: str = Form(chartwheel_mod.ASC_LEFT), # LOG-05 — obrót koła ): """Składa raport: horoskop liczymy TU NA NOWO, a części od AI (interpretacja natalna i predykcje okresowe) dokłada przeglądarka z magazynu (PRE-22/23). @@ -203,7 +204,8 @@ def compile_build( "lat": lat, "lon": lon, "house_system": house_system, "house_systems": house_systems, "aspect_orb": aspect_orb, "aspect_luminary_bonus": aspect_luminary_bonus, "aspect_minor": aspect_minor, - "stations": stations, "zodiac": zodiac, "tables": tables} + "stations": stations, "zodiac": zodiac, "tables": tables, + "wheel_orientation": wheel_orientation} ctx: dict = {"form": form, "result": None, "error": None, "moment": None} try: iso_utc, label = _build_utc(date, time, tz_offset) @@ -216,7 +218,7 @@ def compile_build( stations=stations, zodiac=zodiac, tables=tables, ) from app import chartwheel - ctx["wheel_svg"] = chartwheel.render(ctx["result"]) + ctx["wheel_svg"] = chartwheel.render(ctx["result"], orientation=wheel_orientation) ctx["aspectarian_svg"] = chartwheel.render_aspectarian(ctx["result"]) # PRE-18 ctx["declination_svg"] = chartwheel.render_declination(ctx["result"]) # LOG-07 (etap 6) ctx["antiscia_svg"] = chartwheel.render_antiscia(ctx["result"]) # LOG-07 (etap 6) @@ -265,13 +267,19 @@ def compile_pdf(payload: dict): stations=bool(data.get("stations")), tables=bool(data.get("tables")), zodiac=str(data.get("zodiac") or "tropical"), ) + # Fallback systemu domów musi dojechać do PDF-a — z samego kosmogramu + # nie da się poznać, że podział jest z innego systemu, niż zamówiono. + warnings = [w for w in (chart.get("house_warnings") or []) if w] + from app import chartwheel # Zasada: co pokazujemy na stronie, ma trafić do PDF-a. Wszystkie rysunki # w motywie DRUKU — samodzielny konwerter SVG→PDF nie zna arkusza, więc # zmienne CSS i font glifów muszą być wprost w rysunku. for svg, caption in ( - (chartwheel.render(chart, theme="print"), "Kosmogram"), + (chartwheel.render(chart, theme="print", + orientation=str(data.get("wheel_orientation") or chartwheel_mod.ASC_LEFT)), + "Kosmogram"), (chartwheel.render_aspectarian(chart, theme="print"), "Aspektarian — siatka aspektów"), (chartwheel.render_declination(chart, theme="print"), "Wykres deklinacji"), (chartwheel.render_antiscia(chart, theme="print"), "Oś antyscji"), diff --git a/services/presentation/app/templates/_form_options.html b/services/presentation/app/templates/_form_options.html index 9ade456..c49d8ce 100644 --- a/services/presentation/app/templates/_form_options.html +++ b/services/presentation/app/templates/_form_options.html @@ -14,6 +14,13 @@ {% endfor %} +

+ {% set wo = form.wheel_orientation or 'asc' %} + Kosmogram — po lewej stronie: + {% for value, name in WHEEL_ORIENTATIONS %} + + {% endfor %} +
diff --git a/services/presentation/tests/test_compile.py b/services/presentation/tests/test_compile.py index abf807a..2e7dee2 100644 --- a/services/presentation/tests/test_compile.py +++ b/services/presentation/tests/test_compile.py @@ -76,16 +76,45 @@ def test_compile_page_renders_all_figures(): assert var in MAIN, f"/compile nie ustawia {var}" -def test_pdf_bundles_all_figures_in_print_theme(): - """compile_pdf składa KOMPLET rysunków (motyw druku) i wysyła jako `figures`.""" - assert '"figures": figures' in MAIN - for call in ( - 'render(chart, theme="print")', - 'render_aspectarian(chart, theme="print")', - 'render_declination(chart, theme="print")', - 'render_antiscia(chart, theme="print")', - ): - assert call in MAIN, f"PDF nie składa: {call}" +def test_pdf_bundles_all_figures_in_print_theme(monkeypatch): + """compile_pdf składa KOMPLET rysunków w motywie DRUKU. + + Sprawdzamy przez WYWOŁANIE trasy, nie przez szukanie tekstu w main.py: + poprzednia wersja greppowała `render(chart, theme="print")` i pękała przy + każdym dopisaniu argumentu, mimo że zachowanie zostawało poprawne.""" + seen: dict[str, str] = {} + + from app import chartwheel + from app.clients.render_client import RenderClient + from app.main import logic + + for name in ("render", "render_aspectarian", "render_declination", "render_antiscia"): + original = getattr(chartwheel, name) + + def spy(chart, theme="screen", _n=name, _o=original, **kw): + seen[_n] = theme + return _o(chart, theme=theme, **kw) + + monkeypatch.setattr(chartwheel, name, spy) + + monkeypatch.setattr(logic, "positions", lambda **kw: _pdf_sample_chart()) + monkeypatch.setattr(RenderClient, "pdf", lambda self, report: b"%PDF-1.4 stub") + + r = _client().post("/compile/pdf", json={"person": "Jan", "data": { + "date": "1984-04-30", "time": "11:20", "tz_offset": 2, "lat": 50.06, "lon": 19.94}}) + assert r.status_code == 200, r.text[:300] + assert set(seen) == {"render", "render_aspectarian", + "render_declination", "render_antiscia"}, seen + assert set(seen.values()) == {"print"}, seen + + +def _pdf_sample_chart() -> dict: + cusps = [{"house": i + 1, "sign": "Aries", "in_sign": "0", "decimal": float(i * 30), + "sign_glyph": "♈"} for i in range(12)] + ang = {k: {"name": k, "sign": "Aries", "in_sign": "0", "decimal": 0.0, + "sign_glyph": "♈"} for k in ("Asc", "MC", "Dsc", "IC")} + return {"engine": "test", "positions": [], "cusps": cusps, "angles": ang, + "sign_glyphs": [{"sign": "Aries", "glyph": "♈"}], "house_system": "equal"} # ──────────────────────── zbieranie materiału z magazynów ──────────────── diff --git a/services/presentation/tests/test_house_compare.py b/services/presentation/tests/test_house_compare.py index 97fa511..108761f 100644 --- a/services/presentation/tests/test_house_compare.py +++ b/services/presentation/tests/test_house_compare.py @@ -5,6 +5,8 @@ systemów, klient przekazuje je do logiki, a szablon pokazuje kuspy obok siebie. """ import pathlib +import pytest + APP = pathlib.Path(__file__).resolve().parents[1] / "app" CHART = (APP / "templates" / "chart.html").read_text(encoding="utf-8") COMPILE = (APP / "templates" / "compile.html").read_text(encoding="utf-8") @@ -96,3 +98,59 @@ def test_fallback_notice_is_shown_prominently(): def test_no_notice_when_nothing_was_substituted(): html = _render("_result_tables.html", result={"house_warnings": []}) assert "house-warning" not in html + + +# ── ścieżka PRZEZ handlery, nie obok nich ─────────────────────────────── +# Dwa błędy przeszły przez komplet testów szablonowych, bo żaden nie wywołał +# POST-a: odwołanie do nieistniejącej zmiennej w handlerze strony głównej oraz +# użycie parametru formularza, którego w sygnaturze nie było. Oba dają 500 na +# żywo i oba łapie dopiero prawdziwe żądanie. + +def _client_with_stub_logic(monkeypatch, chart): + import os + + os.environ.pop("APP_PASSWORD", None) + from starlette.testclient import TestClient + + from app.main import app, logic + + monkeypatch.setattr(logic, "positions", lambda **kw: chart) + return TestClient(app) + + +def _minimal_chart(): + cusps = [{"house": i + 1, "sign": "Aries", "in_sign": "0°00'00''", + "decimal": float(i * 30), "sign_glyph": "♈"} for i in range(12)] + ang = {k: {"name": k, "sign": "Aries", "in_sign": "0°00'00''", "decimal": 0.0, + "sign_glyph": "♈"} for k in ("Asc", "MC", "Dsc", "IC")} + return {"engine": "test", "positions": [], "cusps": cusps, "angles": ang, + "sign_glyphs": [{"sign": "Aries", "glyph": "♈"}], + "house_system": "porphyry", "house_system_requested": "placidus", + "house_warnings": ["UWAGA: system domów „placidus” nie ma definicji"]} + + +FORM = {"date": "1984-04-30", "time": "11:20", "tz_offset": "2", + "lat": "50.06", "lon": "19.94", "house_system": "placidus"} + + +def test_chart_page_survives_a_real_post(monkeypatch): + c = _client_with_stub_logic(monkeypatch, _minimal_chart()) + r = c.post("/", data=FORM) + assert r.status_code == 200, r.text[:400] + assert "nie ma definicji" in r.text, "ostrzeżenie o fallbacku nie dotarło na stronę" + + +def test_compile_page_survives_a_real_post(monkeypatch): + c = _client_with_stub_logic(monkeypatch, _minimal_chart()) + r = c.post("/compile", data=FORM) + assert r.status_code == 200, r.text[:400] + + +@pytest.mark.parametrize("orientation", ("asc", "aries")) +def test_wheel_orientation_reaches_the_renderer(monkeypatch, orientation): + """Wybór obrotu koła musi dojść z formularza do rysunku — i wrócić + zaznaczony, żeby nie gubił się przy przeliczeniu.""" + c = _client_with_stub_logic(monkeypatch, _minimal_chart()) + r = c.post("/", data={**FORM, "wheel_orientation": orientation}) + assert r.status_code == 200, r.text[:400] + assert f'value="{orientation}" checked' in r.text diff --git a/services/presentation/tests/test_render_link.py b/services/presentation/tests/test_render_link.py index 691b14a..c518a6f 100644 --- a/services/presentation/tests/test_render_link.py +++ b/services/presentation/tests/test_render_link.py @@ -66,8 +66,12 @@ def test_key_is_read_lazily(): # ──────────────────────── kosmogram w wariancie do druku ───────────────── def test_pdf_route_uses_print_theme(): - """Samodzielny konwerter SVG→PDF nie zna naszego arkusza stylów.""" - assert 'chartwheel.render(chart, theme="print")' in MAIN + """Samodzielny konwerter SVG→PDF nie zna naszego arkusza stylów. + + Sprawdzamy, że trasa PDF-a w ogóle podaje motyw druku — bez wiązania się + z dokładnym kształtem wywołania (dochodzą do niego kolejne argumenty). + Pełny sprawdzian, że KAŻDY rysunek dostaje 'print', jest w test_compile.py.""" + assert 'theme="print"' in MAIN def test_print_theme_has_no_css_variables(): diff --git a/tests/oracle/README.md b/tests/oracle/README.md index 48b5051..55d40a4 100644 --- a/tests/oracle/README.md +++ b/tests/oracle/README.md @@ -70,7 +70,9 @@ zarówno zestaw brzegowy (2520 porównań), jak i losowy przemiał 20 000 przypa | System | Konstrukcja | Maks. odchylenie | |---|---|---| | whole sign | podział ekliptyki | 0,000000000° | -| equal | podział ekliptyki | 0,000000000° | +| whole sign (od Barana) | dom I zawsze na 0° Barana | 0,000000000° | +| equal | podział ekliptyki od Ascendentu | 0,000000000° | +| equal (od MC) | dom X dokładnie na MC | 0,000000000° | | porphyry | podział kwadrantów po ekliptyce | 0,000000000° | | vehlow | equal z Ascendentem w środku domu I | 0,000000000° | | morinus | równik rzutowany wprost na ekliptykę | 0,000000000° | diff --git a/tests/oracle/harness.py b/tests/oracle/harness.py index 80c4b86..3329205 100644 --- a/tests/oracle/harness.py +++ b/tests/oracle/harness.py @@ -24,7 +24,9 @@ from domain import Case, in_domain, obliquity # Litery systemów w Swiss Ephemeris. SWE_CODE = { - "whole_sign": b"W", "equal": b"E", "porphyry": b"O", + "whole_sign": b"W", "equal": b"E", + "equal_mc": b"D", + "whole_sign_aries": b"N", "porphyry": b"O", "placidus": b"P", "koch": b"K", "regiomontanus": b"R", "campanus": b"C", "morinus": b"M", "alcabitus": b"B", "vehlow": b"V", "topocentric": b"T", } @@ -158,9 +160,9 @@ def format_report(results: list[Result], seed: int | None = None) -> str: "ZGODNOŚĆ Z WYROCZNIĄ (Swiss Ephemeris) — domy astrologiczne", f"tolerancja: {TOLERANCE_DEG:.8f}° (1″)" + (f" ziarno: {seed}" if seed is not None else ""), "=" * 78, - f"{'SYSTEM':<14}{'SPRAWDZONYCH':>13}{'>TOL':>7}{'POZA DZIEDZ.':>14}{'MAX ODCH.':>14} WYNIK"] + f"{'SYSTEM':<18}{'SPRAWDZONYCH':>13}{'>TOL':>7}{'POZA DZIEDZ.':>14}{'MAX ODCH.':>14} WYNIK"] for r in results: - lines.append(f"{r.system:<14}{r.checked:>13}{r.over_tolerance:>7}" + lines.append(f"{r.system:<18}{r.checked:>13}{r.over_tolerance:>7}" f"{r.skipped_out_of_domain:>14}{r.max_dev:>14.9f} " f"{'OK' if r.passed else 'BŁĄD'}") for r in results: diff --git a/tests/oracle/run.py b/tests/oracle/run.py index 85d135c..445a7c3 100644 --- a/tests/oracle/run.py +++ b/tests/oracle/run.py @@ -27,7 +27,7 @@ from harness import ( # noqa: E402 # Systemy do sprawdzenia. Rośnie wraz z implementacją kolejnych (Etap 1 i 2) — # dopisanie nazwy tutaj wystarcza, żeby weszła do każdego builda. -SYSTEMS = ["whole_sign", "equal", "porphyry", "vehlow", "morinus", +SYSTEMS = ["whole_sign", "whole_sign_aries", "equal", "equal_mc", "porphyry", "vehlow", "morinus", "regiomontanus", "campanus", "alcabitus", "topocentric", "placidus", "koch"]