
    OjQ                       d Z ddlmZ ddlZddlZddlZddlmZmZmZ ddl	m
Z
mZmZ ddlmZmZ ddlmZ ddlmZmZ dd	lmZ dd
lmZmZmZ ddlmZ ddlmZ ddl m!Z! ddl"m#Z$  ejJ                  e&      Z' eddg      Z( e ed            Z) G d de      Z* G d de      Z+ddlm,Z, d/dZ-d0dZ.d1dZ/e(ja                  d       ee       ee      f	 	 	 d2d       Z1 G d de      Z2e(ja                  d        ee       ee      f	 	 	 d3d!       Z3e(ji                  d"e#      d4d5d$       Z5e(ji                  d%e#       ee       ee      f	 	 	 d6d&       Z6e(ji                  d'       ee       ee      f	 	 	 d7d(       Z7d8d)Z8d9d*Z9d:d+Z:d0d,Z;e(ji                  d-       ee       ee      f	 	 	 d6d.       Z<y);u0  고증 판정 — 사람이 정한 것을 **DB 에** 적는 유일한 문.

★★★계약 (Codex 2026-09-02):

    · 정본은 이 표다. `artifact/` 의 HTML·JSON 은 화면 산출이지 결정이 아니다
    · 판정자는 **서버의 인증 주체**에서 온다 — 클라이언트가 임의로 못 보낸다
    · append-only. 정정은 `supersedes_id` 를 든 **새 행**
    · 사람이 **실제로 본** 사진 SHA·좌표가 붙일 것과 다르면 **안 받는다**
    · CP 나 산출 JSON 을 직접 고치는 길은 만들지 않는다
    )annotationsN)datetime	timedeltatimezone)AnyDictOptional)	APIRouterDepends)HTMLResponse)	BaseModelField)Session)get_current_userget_dbverify_project_access)AppError)UserAccount) GroundingReferenceFidelityReview)grounding_fidelity_reviewz/api/v1/grounding-fidelityzgrounding-fidelity)prefixtags	   )hoursc                  8    e Zd ZU dZded<   dZded<   dZded<   y)
ObservedInud   사람이 **실제로 화면에서 본 것**. ★이것과 붙일 것이 달라지면 안 받는다.strimage_sha256 eraregionN)__name__
__module____qualname____doc____annotations__r    r!        S/Users/manta/Documents/Projects/TheRoad-I1/backend/app/api/v1/grounding_fidelity.pyr   r   &   s    nCMFCr(   r   c                  ~    e Zd ZU dZded<   ded<   ded<    edd      Zded	<   d
Zded<   ded<   ded<   dZded<   y)	VerdictInud  판정 하나.

    ★★★**입력 신원을 클라이언트가 못 만든다** (Codex BLOCK 2026-09-02).
    앞 판은 canonical payload 를 body 에서 통째로 만들고 `observed` 도 **같은
    body** 와만 견줬다 — 항진식이다. 지어낸 입력과 지어낸 `observed` 를 함께
    보내면 정본 표에 그대로 들어갔다.
    이제 서버가 **얼어붙은 CP·세계 규칙·지금 파일 SHA** 로 후보를 다시 만들고,
    `review_input_hash` 로 **정확히 한 줄**을 찾는다. 없거나 사진·좌표가
    달라졌으면 **쓰기 0** 으로 거절한다.
    r   
project_id
episode_idreview_input_hash.zverified | rejected)descriptionverdictr   reasonr   observedidempotency_keyNzOptional[str]supersedes_id)	r"   r#   r$   r%   r&   r   r0   r1   r4   r'   r(   r)   r+   r+   .   sK    	 OO*?@GS@FC#'M='r(   r+   )require_project_writec                    dd l }|j                         }t        | d      5 t        fdd      D ]  }|j	                  |        	 d d d        |j                         S # 1 sw Y   |j                         S xY w)Nr   rbc                 &     j                  d      S )Ni   )read)fs   r)   <lambda>z_sha_of_file.<locals>.<lambda>M   s    !&&/r(   r(   )hashlibsha256openiterupdate	hexdigest)pathr<   hchunkr:   s       @r)   _sha_of_filerE   H   sb    A	dD	Q137EHHUO 8 
 ;;= 
 ;;=s   &A!!A9c                  
 ddl m} t        | |      \  }}
|xs i j                  d      xs i }t	        |j                  d      xs d      j                         t	        |j                  d      xs d      j                         d}
fd}t        j                  || |||d	
      }|D 	cg c]  }	|	j                  d      r|	 c}	 |
      fS c c}	w )u$  서버가 **얼어붙은 것에서 다시 만든** 후보들. ★클라이언트를 안 믿는다.

    사진 SHA 는 **지금 디스크의 파일**에서 낸다 — 사진이 바뀌었으면 신원이
    달라지고, 그러면 옛 결정도 새 결정도 그 사진에 안 붙는다.
    r   Pathdatar    r   r!   r    r!   c                $    ddl m}  ||       S )Nr   )row_content_sha256)root)-app.modules.pipeline.grounding_sidecar_writerrL   )rowrL   rM   s     r)   _shaz_candidates.<locals>._sha_   s     	U!#D11r(   F)r,   r-   coordinatessha_ofstrictfault)pathlibrH   _frozengetr   stripfrreview_inputs_for)r,   r-   rH   cpworldrI   coordsrP   gotxrM   s             @r)   _candidatesr`   R   s     j*5OBtKRV$*D%.B/557DHHX.4"5;;=?F2 

rj*4&&*5:C
 1s!!%%.As14:==1s   .CCc                    t        | |      \  }}|D cg c]  }|d   t        |      k(  s| }}t        |      dk7  rt        ddd      |d   |fS c c}w )u2   그 신원의 후보 **하나**. 없으면 선다.hash   z$grounding_fidelity.no_such_candidateu   그 신원의 후보가 지금 없다 — 사진이나 좌표가 달라졌을 수 있다. 안 본 것에 결정을 붙이지 않는다  codemessagestatus_coder   )r`   r   lenr   )r,   r-   	want_hashr^   rM   r_   mines          r)   _one_candidaterl   m   sn    J
3IC:s!ai3y>9AsD:
4yA~7P	 	
 7D= ;s
   AAz/verdictc                T   t        | j                  ||       t        || j                  | j                         | j                  t
        j                  vr6t        dd| j                  dt        t
        j                         dd      t        | j                  | j                  | j                        \  }}t        |d         }	 t        j                  || j                  j                                t        j                  |      }t        j                   |      }|j'                  t(              j+                  | j                  | j                  | j,                  
      j/                         }	|	%|	j0                  |	j                  |	j                  ddS | j2                  r?|j5                  t(        | j2                        }
|
|
j                  |k7  rt        ddd      t7        j8                  t:              j=                  d      }t)        d,i dt?        j@                         jB                  d| j                  d| j                  d|d   d|d   d|d   d|d   d|d   d|d   d|d   d|d|d| j                  d| jD                  d t%        tG        |d!d	      xs tG        |dd"            d#|d$t
        jH                  d%| j                  jK                         d&| j2                  d'| j,                  d(|}|jM                  |       |jO                          tP        jS                  d)| j                  |d	d* |jT                         |j0                  ||j                  d+dS # t
        j"                  $ r}t        dt%        |      d      |d	}~ww xY w)-u^   사람의 판정을 **덧붙인다**. 같은 것을 두 번 보내면 같은 행이 나온다.z"grounding_fidelity.unknown_verdictu   모르는 판정 u    — u    중 하나여야 한다i  re   payloadzgrounding_fidelity.input_brokenNr,   r-   r3   F)idr.   r0   createdz&grounding_fidelity.supersedes_mismatchu}   대신하려는 앞 결정이 없거나 **다른 입력**의 것이다 — 무엇을 고치는지 모르는 채 안 적는다rd   secondstimespecrp   r,   r-   research_subject_idfinal_idpurposeacquisition_identityr   r    r!   review_input_jsonr.   r0   r1   reviewer_actorusernamer   reviewed_atreview_contract_versionobserved_jsonr4   r3   
created_atz [grounding-fidelity] %s %s by %s   Tr'   )+r5   r,   require_episoder-   r0   rY   KNOWN_VERDICTSr   listrl   r.   dictassert_observed_matchesr2   
model_dumpcanonical_input
input_hashReviewInputBrokenr   queryr   	filter_byr3   one_or_nonerp   r4   rW   r   nowKST	isoformatuuiduuid4hexr1   getattrREVIEW_CONTRACT_VERSIONmodel_dump_jsonaddcommitloggerinforz   )bodydbcurrent_userone_rootrn   	canonicalwantexcseenprevr   rO   s                r)   record_verdictr   z   s   
 $//2|<B9||2,,,@"3DLL3C5#'(9(9#:";;S!U#&( 	(
   $ 6 68JC3y>"GC
""7DMM,D,D,FG&&w/	}}W% HH56Y$//"&//'+';';  ==H[] 	 ggD4J4J<<E; 	; vv68J8JK<411T9=N	! ! ,,s

%
%y
%
9C
* ::<??/3 $$9: $	 /6i.@	
 %%;< ^, 3:%. x  $ 8<  &*[[ 7<TB >%lD"=?  !# : : mm335  ((!" ,,#$ %C& FF3KIIK
KK2DLLSb	3--/&&t{{t5 5]  C="3xS:?B	CCs   AM7 7N'
N""N'c                  :    e Zd ZU dZded<   ded<   ded<   ded<   y)SelectInu   이 대상에 **쓸 사진 한 장** — 판정(맞다/아니다)과 다른 물음이다 (Codex BLOCK 2026-09-02 밤).
    새 선택은 같은 대상의 앞 선택을 서버가 `supersedes_id` 로 대신한다(append-only · 원자적).r   r,   r-   r.   r3   N)r"   r#   r$   r%   r&   r'   r(   r)   r   r      s     z OOr(   r   z/selectc                   ddl m} t        | j                  ||       t	        || j                  | j
                         t        | j                  | j
                  | j                        \  }}|d   }t        |d         }|j                  |      j                  | j                  | j
                  | j                        j                         }|%|j                  |j                  |j                  ddS |j                  |      j                  | j                  | j
                  |	      j                         j!                         }	t#        j$                  |	D 
cg c]<  }
|
j                  |
j                  |
j                  |
j&                  |
j(                  d
> c}
|      }|r|d   nd}t+        j,                  t.              j1                  d      } |t3        j4                         j6                  | j                  | j
                  || j                  t        |j9                  d      xs d      t        t;        |dd      xs t;        |dd            ||rdj=                  d |D              nd| j                  |      }|j?                  |       |jA                          t#        jB                  t#        jD                  || j                  | j
                        | j                        d   }tF        jI                  d| j                  dd ||jJ                  |xs i j9                  dd      dd        |j                  || j                  |xs i j9                  d      |t"        jL                  k(  ddS c c}
w )u~   이 대상에 쓸 사진을 고른다. ★선택만으로는 안 붙는다 — 그 사진의 판정이 verified 여야 한다.r   )GroundingReferenceSelectionrn   ru   ro   NF)rp   ru   r.   rq   )r,   r-   ru   )rp   ru   r.   selected_atr4   
subject_idrr   rs   r   r   r{   rp   ;c              3  8   K   | ]  }t        |d            yw)rp   N)r   ).0r_   s     r)   	<genexpr>zselect_photo.<locals>.<genexpr>   s     #?$QC$L$s   )rp   r,   r-   ru   r.   r   selected_byr   r4   r3   r   r,   r-   )rj   r0   z9[grounding-fidelity] select %s for %s by %s (replaced %s)r   r.   T)rp   ru   r.   replacedverifiedrq   )'app.models.projectr   r5   r,   r   r-   rl   r.   r   r   r   r3   r   rp   ru   with_for_updateallrY   live_selectionsr   r4   r   r   r   r   r   r   r   rW   r   joinr   r   effective_verdictread_reviewsr   r   r   VERDICT_VERIFIED)r   r   r   Sr   r   rn   subjectr   lockedrliver   r   rO   r0   s                   r)   select_photor      s   
 D$//2|<B9$BXBXYJC)nG'/01GHHQK!!T__262F2F " HHS 	ggd6N6N%)%;%;O 	O hhqk##t4??8? $ AAPARSVSVSX KQSKQa '(dd1CXCX565H5HYZYfYf12 BKQS_fhD 48D
,,s

%
%y
%
9C
tzz|DOO '4;Q;QW[[8>B?GL*dCfw|]aceGfgCG388#?$#??T 00SBC FF3KIIK""
t4??S((**35G KKKTMcMcdgegMh4:2*:*:;NPR*STWUW*XZ&&tOeOe(()<=7VXViViKi )Ss   AM?z/login)response_classc                   ddl }t        | xs d      xs t        j                   d}|j	                  t        j                   d      r|j                  d|      st        ddd	
      t        dt        j                  |       d      S )u  작은 로그인 화면. ★같은 origin 이라 쿠키가 그대로 남는다.

    ★★로그인은 `POST` 라 브라우저 주소창으로는 못 연다. 이 판정 화면을
    **격리 canary backend** 에서 볼 때 들어갈 문이 없어서 둔다.
    ★인증 자체는 기존 `/api/v1/auth/login` 이 한다 — 여기는 그것을 부르는
    양식일 뿐이고, 이 파일에 비밀번호가 한 글자도 없다.
    r   Nr   	/verdicts/z[A-Za-z0-9_\-./?=&%]+zgrounding_fidelity.bad_nextu#   갈 곳이 이 문 안이 아니다i  re   u{  <meta charset="utf-8"><title>로그인</title><style>body{font:15px/1.7 system-ui,sans-serif;padding:40px;background:#faf9f7}form{max-width:320px}input{display:block;width:100%;padding:8px;margin:6px 0 14px;font:15px system-ui;border:1px solid #ccc;border-radius:6px}button{padding:8px 20px;font:15px system-ui;border-radius:6px;border:1px solid #bbb;background:#fff;cursor:pointer}</style><form onsubmit="go(event)"><h2>로그인</h2><label>아이디</label><input id="u" autocomplete="username"><label>비밀번호</label><input id="p" type="password" autocomplete="current-password"><button>들어가기</button><p id="said"></p></form><script>async function go(e){e.preventDefault();const r=await fetch("/api/v1/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},credentials:"include",body:JSON.stringify({username:u.value,password:p.value})});if(r.ok){location.href=ug   ;}else{said.textContent="안 들어가진다 — 아이디나 비밀번호를 보십시오";}}</script>)
rer   routerr   
startswith	fullmatchr   r   jsondumps)next_rewheres      r)   
login_pager      s     
 
O:&--	:Ea016>9DRUW 	W	#  $(::e#4"5 6!	 r(   z/review-pagec                   t        | ||       t        || |       ddlm} t	        | |      \  }}t        | |      \  }}}	|xs i j                  d      xs i }
|j                  |d       }t        j                  || |      }|D ch c]   }t        |j                  d      xs d	      " }}|D ch c]9  }t        t        j                  ||
      xs i j                  d      xs d	      ; }}t        |j                  |t        t        j                        t        j                   d|  d| dt        |
j                  d      xs d	      t        |
j                  d      xs d	      d|            S c c}w c c}w )u  판정 화면을 **backend 가 직접** 내준다. ★같은 origin 이어야 한다.

    ★★★왜 (실측 2026-09-02): 화면을 정적 서버(8940)에 두고 API(8000)로
    보내면 브라우저가 **CORS 로 막는다** — 이 판에는 CORS 미들웨어가 아예
    없다. 같은 문에서 내주면 세션 쿠키가 그대로 간다.

    ★사진은 `/photo?…&key=<신원>` 이 **서버가 다시 만든 후보만** 내준다 —
    임의 경로를 안 받는다 (Codex BLOCK).
    r   userr   )fidelity_review_pagerI   c                    | d   S )Nrb   r'   )r_   s    r)   r;   zreview_page.<locals>.<lambda>7  s    qyr(   )key_ofr   ru   r   r   r.   z/photo?project_id=z&episode_id=z&key=r    r!   rJ   )api_base
image_baser]   selected)r   r   tools.grounding_auditr   r`   rV   rW   
cards_fromrY   read_selectionsr   effective_selectionr   renderr   r   )r,   r-   r   r   pgr^   r   _cpr\   _rrI   rowsselsssubjectssidpickeds                    r)   review_pager   "  sy    *,?B
J/@Z4JCZ4NCKRV$*D==%8=9DbZJODAEFAAEE/06B7HF!#!# 2))$3?E2JJK^_ecef!  #		s6==)}}o%7
|":,e5488E?0b1dhhx06B79 "    G#s   
%E=5>Fz/photoc                ^   ddl m} t        | ||       t        || |       t	        | ||      \  }}||d   z  j                         }t        |t        |             r|j                         st        ddd      t        |      |d	   d
   k7  rt        ddd       |t        |            S )u  받아 둔 사진 한 장. ★**서버가 다시 만든 후보**만 열쇠로 찾는다.

    ★★★앞 판은 임의 `path` 를 받아 `PROJECTS_DIR` 의 **부모** 아래를 열어
    줬다 — 프로젝트 하나에 권한만 있으면 같은 뿌리의 다른 프로젝트·장부·
    산출까지 볼 수 있었다 (Codex BLOCK). 이제 열쇠는 **판정 신원**이고,
    그 후보의 파일만, 그것도 **내용 SHA 가 맞을 때만** 연다.
    r   )FileResponser   chosen_pathzgrounding_fidelity.no_photou   그 사진이 없다  re   rn   r   z grounding_fidelity.photo_changedu3   사진 내용이 신원과 다르다 — 안 연다rd   )rB   )fastapi.responsesr   r   r   rl   resolve_inside_project_rootis_filer   rE   r   )	r,   r-   keyr   r   r   r   rM   r   s	            r)   photor   E  s     /*,?B
J/z:s;IC3}%%..0D4z234<<>953H 	HDS^N;;3IWZ\ 	\ SY''r(   c                    ddl m} | j                  |      j                  |j                  t        |      k(  |j                  t        |      k(        j                         }|t        ddd      y)u   `episode_id` 가 **이 프로젝트의** 에피소드인가 — DB 로 본다 (Codex PR #82 P1: 인증은 project_id 에만 걸리고
    episode_id 는 그대로 경로에 붙어 다른 프로젝트의 CP·사진을 읽을 수 있었다).r   )EpisodeNzepisode.not_foundu-   그 프로젝트의 에피소드가 아니다r   re   )	r   r   r   filterrp   r   r,   firstr   )r   r,   r-   r   rO   s        r)   r   r   a  si     +88GF7::Z0'2D2DJ2WXEG  {/9h#&( 	( r(   c                r    ddl m} ddlm}  ||j                        t        |       z  j                         S )Nr   rG   settings)rU   rH   app.core.configr   projects_dirr   r   )r,   rH   r   s      r)   r   r   n  s+    (&&'#j/9BBDDr(   c                l    | j                         |j                         }}||k(  xs ||j                  v S )N)r   parents)rB   basepbs       r)   r   r   u  s.    <<>4<<>qA6#Q!))^#r(   c                   ddl }ddlm} ddlm}  ||j
                        j                  } ||j
                        | z  dz  dz  |z  }t        |t        |             st        ddd	
      |dz  dz  }|dz  dz  }|j                         st        ddd	
      |j                         r!|j                  |j                  d            ni }	|j                  |j                  d            |	|fS )u   이 에피소드의 **얼어붙은 것들**. ★디스크에서 읽기만 한다. ★경로는 이 프로젝트 디렉토리 안이어야 한다.r   NrG   r   checkpointsepisodesz grounding_fidelity.no_checkpointu'   중앙 조사 체크포인트가 없다r   re   reference_acquisitionzmanifest.jsonvisual_world_ruleszutf-8)encoding)r   rU   rH   r   r   r   parentr   r   r   r   loads	read_text)
r,   r-   _jsonrH   r   rM   epcp_pw_pr\   s
             r)   rV   rV   z  s    (%%&--D
x$$
%

2]
B#$B2}Z01>HVY[ 	[''/9D
#
#o
5C<<>>HVY[ 	[  [[89#% 
;;t~~w~78%EEr(   r   c                   t        | ||       t        || |       |j                  t              j	                  | |      j                  t        j                        j                         }|D cg c]]  }|j                  |j                  |j                  |j                  |j                  |j                  |j                  |j                  d_ c}t        j                   dS c c}w )uG   이 에피소드의 판정 전부. ★CP 투영이 이것을 읽는다.r   r   )rp   r.   r0   r1   rz   r|   r4   r}   )r   review_contract)r   r   r   r   r   order_byr|   r   rp   r.   r0   r1   rz   r4   r}   rY   r   )r,   r-   r   r   r   r   s         r)   list_verdictsr    s    
 *,?B
J/HH56Y*YDX6BBCCCE 	  ! dd)<)<99**1==#$#<#<  *,)C)CE E s   1A"C')returnr   )r,   r   r-   r   )r,   r   r-   r   rj   r   )r   r+   r   
OrmSessionr   r   )r   r   r   r  r   r   )r   )r   r   )r,   r   r-   r   r   r  r   r   )
r,   r   r-   r   r   r   r   r  r   r   )r   r  r,   r   r-   r   r  None)r,   r   )r  bool)=r%   
__future__r   r   loggingr   r   r   r   typingr   r   r	   fastapir
   r   r   r   pydanticr   r   sqlalchemy.ormr   r  app.api.depsr   r   r   app.core.errorsr   app.models.catalogr   r   r   app.modules.pipeliner   rY   	getLoggerr"   r   r   r   r   r+   r5   rE   r`   rl   postr   r   r   rW   r   r   r   r   r   r   rV   r  r'   r(   r)   <module>r#     s(  	 #    2 2 & & & * % 0 H H $ * ? @			8	$	6-.
0 yq!" (	 (. />6
 Z$+FO/67G/HC5!C5!,C5 C5Ly  Y")&/-45E-F**** *Z H\2% 3%P N<8!(,34D,E) 9D H"6?&-.>&?((#( (6
(E$
F. K#*6?.56F.GE E +E Er(   