P03 산모 계정·프로필 부분 검증
이 페이지는 P03-mother-account-profile의 부분 증거다. 신규 무이력 합성 산모의 clean-pinned
동일 composition machine actor에서 여섯 acceptance case와 primary state는 통과했다. 기존 실제
브라우저는 withdrawal-only다. P03 phase 전체 판정은 red, 구현 증거는 partial,
phaseAcceptancePassed: false, completionClaim: false다. 온라인 배포는 수행하지 않았다.
상위 결정은
ADR-034와
ADR-033이다. 기계 증거는
docs/evidence/p03-mother-account-profile/가 소유하며, actor-chain SSOT는
workflow/sanmopia_actor_chain.v1.yaml이다.
| 근거 | 실행 경계 | 현재 판정 | 증명하지 않는 것 |
|---|---|---|---|
actual-actor-run.json + primary-state-manifest.json | 실제 local Supabase Auth/PostgREST, disposable SpiceDB, actual disposable Restate의 한 clean composition | 신규 무이력 6-case machine actor와 revision/state 전이 PASS | 이력 계정 privacy lifecycle |
actual-browser-source-actor-run.json + actual-browser-run.json + WebP 5개 | 과거 실제 local Supabase·SpiceDB·Restate network와 Chromium | 탈퇴·exact replay·reload/pageshow recovery PASS | 가입·수정 및 authorization·validation·conflict browser coverage |
cleanup-manifest.json | 6-case wrapper teardown cardinality | Auth/operational/relationship/container/process/temp-directory residue 0 | retained record expiry, delayed purge, source별 eraser |
| WebP 6개 | frontend commit bdb8d7cc2a8f19bf14470157db8718e3474b650e의 synthetic UI fixture | UI contract 회귀 PASS | API/network, Supabase primary state, 실제 actor transition |
| 관리자 검토 provisional WebP 4개 | frontend 23d999a, contract c4b2488, backend ed8174ca, actual loopback Astro/Chromium | Material rail, 1280/390 responsive, unauthenticated fail-closed PASS | HQ 권한 성공, review completion, source purge |
| four-repository pin | assembly 9cab9b2, contract dc3c210, backend 0a12678, frontend fbed8f8, 각 revision/tree/clean: true | 등록 | P03 전체 PASS를 뜻하지 않음 |
레거시 근거와 새 경계
Section titled “레거시 근거와 새 경계”| Anchor | 레거시 동작 | 새 경계 |
|---|---|---|
source-refs/sanmopia_web/static/js/signup.js:7-121 | 이메일 중복, 필수 입력, 비밀번호 일치, 브라우저 약관 DOM을 검사한다. | native Auth가 credential·contact verification을 소유하고 backend가 published consent를 재검증한다. |
source-refs/sanmopia_web/application/controllers/api/Account.php:36-100 | 이메일·이름·전화·비밀번호를 받아 자체 bcrypt와 auth token을 만든다. | POST /mother/accounts는 credential·OTP·actor id·role·branch scope를 받지 않는다. |
source-refs/sanmopia_web/application/controllers/api/Account.php:153-203 | 산모 정보와 주소를 별도 table에 저장하며 주소 실패가 성공에 가려질 수 있다. | profile, address revision, verification receipt, consent receipt, audit, owner relation intent를 한 authority로 묶는다. |
source-refs/sanmopia_web/application/models/User_model.php:57-115 | 일반·소셜 계정의 profile completeness 규칙이 다르다. | 하나의 lifecycle·completeness 규칙과 server-owned allowed next action을 사용한다. |
A→Z 검증 절차
Section titled “A→Z 검증 절차”-
A — 범위 고정
신규 무이력 합성 산모 한 계정의 동일 composition machine 6-case를 검증 대상으로 고정한다. 이력 계정은 포함하지 않고 별도 RED로 남긴다.
-
B — 레거시 anchor 확인
가입, profile, 주소, completeness의 PHP·JavaScript source line을 읽고 새 경계와 대조한다.
-
C — public contract 확인
GET /mother/account-consent-offer,POST /mother/accounts,GET|PATCH /mother/profile,POST /mother/account-withdrawals의 closed schema와 server-resolved scope를 확인한다. -
D — 실제 local runtime 준비
local Supabase Auth/PostgREST, disposable SpiceDB, disposable actual Restate를 사용한다. 네 repository의 revision과 tree가 clean인지 먼저 확인한다. managed cloud와 mock route는 쓰지 않는다.
-
E — 합성 Auth identity 생성
fresh confirmed test identity를 만들고 원문 token, credential, contact, subject id는 증거에 남기지 않는다.
-
F — verified contact 재확인
backend가 bearer subject와 verified Auth contact를 다시 읽고 E.164 projection을 만든다.
-
G — 가입 command 제출
인증 없는 요청
401과 필수값 누락 요청422를 먼저 보내 primary state 무변경을 확인한다. 이어 profile·주소·published consent reference와Idempotency-Key만 제출한다. password, OTP, provider token, member/profile id, role, branch scope는 body에 넣지 않는다. -
H — 가입 authority 확인
HTTP
201, lifecycleactive, profile revision1, address revision[1], consent receipt2를 확인한다. 같은 가입 key replay는200이고 primary state를 바꾸지 않아야 한다. -
I — 관계 authority 확인
disposable SpiceDB에 mother profile
owner가 적용됐는지 확인한다. -
J — 접근 capability 확인
active mother capability가 다음 예약 action의 prerequisite가 되는지 server projection으로 확인한다.
-
K — 자기 profile 수정 경계 확인
PATCH /mother/profile이 expected profile/address revision만 받고 actor·account scope를 받지 않는지 확인한다. 첫 수정은201과 profile revision2, address revisions[1,2]; exact replay는200무변경; payload mismatch는 typed409무변경이어야 한다. 이 단계의 실제 browser 증거는 없다. -
L — body-free 탈퇴 제출
POST /mother/account-withdrawals에 body 없이 bearer session과Idempotency-Key만 보낸다. machine actor와 별개인 browser capture는 이 시점부터의 withdrawal UI만 증명한다. -
M — 탈퇴 접수 확인
첫 응답 HTTP
202와 raw profile access 선차단을 확인한다. terminal state는 profile revision3, address revisions[], verified contacts[], owner delete receipt로 고정한다. -
N — durable contract baseline 실행
새 6-case actor wrapper와 기존 browser run은 각각 disposable 실제 Restate service의 network invocation을 사용한다. 두 런의 source lineage를 섞지 않는다. browser manifest는 byte-preserved
actual-browser-source-actor-run.json만 가리켜야 한다. -
O — Restate 시간 계약 RED→GREEN
WorkflowContext.time()journal이 epoch seconds float임을 확인한다. 실제 값1784236748.7164872를/1000하던 코드는 1970년대transitioned_at을 만들어requested_at보다 과거라는 DB guard로 실패했다./1000을 제거하고datetime.fromtimestamp(await ctx.time(), timezone.utc)로 바꾼 뒤 fresh actual Restate invocation이completed/success인지 확인한다. -
P — lifecycle 완료 확인
withdrawal, privacy workflow, account lifecycle과 profile erasure가 terminal 상태인지 확인한다.
-
Q — Auth masking 확인
실제 local Auth identity의 Sanmopia privacy status와 contact masking이 적용됐는지 확인한다.
-
R — profile erasure 확인
mother profile PII erasure가 완료되고 no-history 계정의 retained snapshot이
0인지 확인한다. -
S — 관계 삭제 확인
SpiceDB mother profile
owner가 삭제되고 relationship receipt가 남는지 확인한다. -
T — lost-response exact replay
가입, profile 수정, 탈퇴에 같은 key를 재전송해 모두 HTTP
200, idempotent replay, primary state 무변경을 확인한다. browser에서는 탈퇴 exact replay HTTP200만 확인한다. -
U — 새 command 차단
탈퇴 후 새 key 요청은 HTTP
401이며 primary state와 workflow effect가 증가하지 않는지 확인한다. -
V — cardinality 확인
withdrawal request, privacy lifecycle, profile erasure가 각각
1, relationship receipt가2인지 확인한다. authorization401, validation422, conflict409가 각각 stateChangedfalse인지도 같은 manifest에서 확인한다. -
W — actual browser recovery 검증
최초 HTTP
202뒤 저장된 pending 상태, Auth mask 뒤 full reload fallback을 재현한다. 같은 body-free submission key의 exact replay HTTP200으로 terminal receipt를 복원하고, exact receipt ID와 requested-at이 모두 맞을 때만 storage를withdrawn으로 승격한다. 실제 Chromium에서 수동 dispatch한pageshow와astro:page-load뒤에도 terminal 상태가 유지되는지 확인한다. -
X — actual browser responsive·privacy 검토
mobile/desktop horizontal overflow
0, console error/warning0, raw credential·PII·withdrawal receipt0과 3-frame actual animated WebP를 확인한다. wrapper teardown에서 Auth, operational, relationship, container, process, temp-directory residue가 모두0인지 확인한다. -
Y — synthetic UI 회귀 분리
join desktop/mobile, profile edit cards, withdrawal confirmation/result와 4-frame UI contract WebP를 확인한다. 이 여섯 자산은 실제 browser 증거로 승격하지 않는다.
-
Z — privacy와 phase 판정
actor/primary-state/browser manifest와 문서에서 raw credential, PII, identifier, body, private topology가 없는지 검토한다. 신규 무이력 6-case machine actor와 withdrawal-only browser를 각각 PASS로 기록한다. 이력·retention 3-gap 때문에 P03은
partial/red, P04는 blocked, 전체 완료는 false다.
역사 계정 operator-resume 후속 workflow
Section titled “역사 계정 operator-resume 후속 workflow”backend pin ed8174ca는 기존 signal-only 경계를 실제 완료 경계로 교체했다. 아래 순서는 코드·fresh
Supabase smoke·전체 backend regression으로 검증됐지만, HQ reviewer의 실제 브라우저 성공 pack은
아직 없으므로 P03 phase 판정을 올리지 않는다.
| 순서 | actor/authority | 검증된 동작 | 현재 증거 |
|---|---|---|---|
| 1 | 산모 | 이력이 있는 계정이 탈퇴를 요청한다. | SQL/fresh actor |
| 2 | privacy lifecycle | 열린 예약·분쟁 권위·source drift가 있으면 irreversible effect 전에 waiting_for_operator_review로 멈춘다. | Restate unit + Supabase smoke |
| 3 | HQ 검토자 | UI는 탈퇴 receipt와 expected review revision만 전송한다. actor·role·branch·PII·note는 받지 않는다. | TS contract + endpoint test + actual browser |
| 4 | backend | bearer actor와 전용 SpiceDB 권한, 최신 review cycle, server-owned blocker facts를 다시 읽는다. | API/application tests |
| 5 | review command | 해소되지 않은 blocker는 typed 409; 정확한 요청은 durable signal receipt를 만든다. | API + Supabase smoke |
| 6 | Restate | reservation, pricing/settlement, care-delivery owner adapter가 source별 separation/redaction과 purge schedule을 각각 journaled step으로 적용한다. | full regression + fresh database gate |
| 7 | atomic completion | 전용 RPC가 signal consumption, 원 privacy lifecycle, withdrawal request, immutable completion receipt를 한 transaction으로 완료한다. | review-completion smoke |
| 8 | projection | 완료된 reviewed lifecycle은 completed를 우선 표시하고 역사적 review flag만 보존한다. | projection unit test |
| 9 | purge worker | SKIP LOCKED lease로 due item을 claim하고 DB-authoritative terminal drift만 멈춘다. 일시적 network failure는 Restate retry로 올린다. | claim race + purge tests |
backend 전체 regression은 6667 passed, P03 fresh Supabase gate는 migration/lint/registration/profile/
withdrawal/review-resumption/review-completion/source-retention/source-purge/source-drift/purge-race/replay/
authorization/cleanup을 모두 통과했다. contract 전체 gate는 484 passed; frontend 전체 run은 새 nav
목적지 기대값을 갱신한 뒤 569개 테스트가 모두 통과한다.
관리자 검토 actual-browser provisional WebP
Section titled “관리자 검토 actual-browser provisional WebP”아래 자산은 실제 Astro 7 loopback 서버와 Chromium을 사용했고 route interception은 없었다. 데스크톱
Material navigation rail은 88px, 모바일은 app bar와 temporary drawer로 치환됐으며 1280/390 모두
horizontal overflow 0, console error/warning 0이었다. 로그인하지 않은 제출은 결과 card를 열지
않고 authentication_required로 닫혔다.




원본 관찰값, 네 repository pin, SHA-256, frame count, 명시적 한계는
manifest.json에 있다. 이 pack은
actual-browser-provisional이며 authenticated HQ reviewer가 없었으므로 성공 acceptance나 source purge를
증명하지 않는다.
동일 composition 실제 machine actor 결과
Section titled “동일 composition 실제 machine actor 결과”| 항목 | 관찰값 |
|---|---|
| happy path | 가입 201, profile 수정 201, 탈퇴 202, Restate completed, owner apply/delete |
| authorization | 미인증 가입 401, 탈퇴 뒤 새 key 401, stateChanged false |
| validation | closed-schema 누락 요청 422, stateChanged false |
| conflict | profile replay payload mismatch 409, stateChanged false |
| exact replay | 가입/profile/탈퇴 각각 200, 세 primary state 모두 unchanged |
| cleanup | Auth/operational/relationship/container/process/temp-directory residue 0 |
| primary state | profile revision 1 → 2 → 3, address [1] → [1,2] → [], consent receipt 2 |
| effect count | withdrawal/privacy/erasure 1/1/1, relationship receipts 2 |
actual-actor-run.json의 runtime 표기는 actual_local Supabase Auth/PostgREST,
actual_disposable SpiceDB, actual_disposable_restate다. caseSetPassed: true지만
evidenceStatus: provisional, completionClaim: false다. primary-state-manifest.json이 동일 actor
SHA-256과 six-case state를 잠근다.
실제 browser·Restate network 결과
Section titled “실제 browser·Restate network 결과”별도 browser run은 clean frontend commit
1a314bc6a7bc09a7518bba819079c4abe99fb285와 served Astro build aggregate
sha256:4a0596fa85b6b6245c6835c4ce4eaae127f19f924b5734f4a8a64ecc2bb189ec를 사용했다.
local Supabase CLI, disposable SpiceDB v1.54.0, disposable Restate service, Chromium을 실제로
연결했다. route interception과 mock network는 사용하지 않았다.
이 browser run의 network source는 당시 actor JSON을 byte-preserve한
actual-browser-source-actor-run.json이다. 새 actual-actor-run.json의 6-case 결과나 최신 frontend
pin으로 browser capture를 소급해 재분류하지 않는다. 아래 browser 결과는 withdrawal-only다.
| 항목 | 실제 관찰 |
|---|---|
| Restate | network invocation completed, completion success |
| terminal authority | account/profile/privacy/withdrawal `withdrawn |
| exact recovery | 같은 body-free submission replay HTTP 200 |
| reload lifecycle | full reload, pageshow, astro:page-load 모두 terminal recovery PASS |
| responsive | mobile 390×844, desktop 1440×900, horizontal overflow 모두 0 |
| diagnostics | console error 0, warning 0 |
| privacy | PII·credential·raw withdrawal receipt 0; unsafe intermediate는 assembly에 없음 |
Restate seconds 계약 결함과 수정
Section titled “Restate seconds 계약 결함과 수정”실제 network 첫 invocation에서 WorkflowContext.time() journal 값
1784236748.7164872가 epoch seconds float임을 확인했다. 기존 /1000은 1970년대
transitioned_at을 만들었고 DB가 transitioned_at < requested_at으로 terminal transition을
거부했다. 수정은 아래 한 줄이다.
datetime.fromtimestamp(await ctx.time(), timezone.utc)/1000 제거 뒤 fresh Restate invocation은 completed/success, DB workflow와 withdrawal status는
모두 completed였다. 이 증거에서 ctx.time()을 epoch milliseconds라고 부르지 않는다.
reload recovery 결함과 수정
Section titled “reload recovery 결함과 수정”수정 전에는 최초 HTTP 202의 withdrawal_pending만 storage에 남았다. manual pageshow도
pending이었고, Auth masking 뒤 full reload는 profile controller 없이 auth-required fallback만
그렸다. 수정 후 같은 stored submission의 exact body-free replay가 HTTP 200 terminal authority를
가져온다. exact receipt ID와 requested-at이 모두 일치할 때만 storage를 withdrawn으로 승격하고,
fallback을 숨긴 뒤 PII-free recovery panel을 보인다. reload 1회와 동시에 들어온 lifecycle event의
deduplicated replay 1회로 network 200은 총 2회였고, 이후 pageshow와 astro:page-load에서도
terminal 상태가 바뀌지 않았다. full reload는 실제 navigation이고, 두 lifecycle event는 실제
Chromium page에서 수동 dispatch했다.
실제 browser WebP
Section titled “실제 browser WebP”




Synthetic UI-only WebP
Section titled “Synthetic UI-only WebP”아래 자산은 UI 계약 회귀다. synthetic fixture와 frontend clean commit을 사용했지만 실제 backend, Supabase, SpiceDB와 연결한 브라우저 캡처는 아니다.






여섯 acceptance case 상태
Section titled “여섯 acceptance case 상태”| Case | 동일 composition machine actor | Browser 범위 | 판정 |
|---|---|---|---|
P03-happy-path | 가입 201, profile 수정 201, 탈퇴 202, owner apply/delete | withdrawal UI만 | machine PASS |
P03-authorization | 미인증 401, 탈퇴 뒤 새 key 401, 무변경 | 없음 | machine PASS |
P03-validation | closed-schema 요청 422, 무변경 | 없음 | machine PASS |
P03-conflict | replay payload mismatch 409, 무변경 | 없음 | machine PASS |
P03-exact-replay | 가입/profile/탈퇴 200, primary state 무변경 | 탈퇴 reload replay 200 | machine PASS + withdrawal browser PASS |
P03-cleanup | 여섯 residue category 0 | redacted terminal recovery | machine PASS + withdrawal browser PASS |
남은 RED
Section titled “남은 RED”기존 gate 용어 operator resume, source-specific retention erasure, retention expiry는 이제 backend
구현 누락이 아니라 아래 actual actor/browser 증거 누락을 가리킨다.
- 실제 역사 계정 + HQ reviewer의 동일 composition operator-resume actor/browser pack
- source별 retained payload와 operational erasure를 실제 actor가 확인하는 browser/primary-state chain
- expiry 뒤 delayed purge, retry, child-record drift, exact replay의 실제 clock actor/browser chain
- P04 complaint/dispute owner authority가 없을 때 계속 fail-closed라는 cross-phase 증거