fix(dashboard): address PR review 56

This commit is contained in:
2026-10-05 08:41:33 +00:00
parent a02aceee41
commit f01dc182ec
9 changed files with 1785 additions and 848 deletions
@@ -1,31 +1,32 @@
# Issue #9 authenticated browser evidence
This directory binds the dashboard menu-transition acceptance evidence to the exact working-tree files listed in `manifest.json`.
This directory binds the dashboard menu-transition acceptance evidence to the exact Git head and working-tree files listed in `manifest.json`.
## Captured transition
- **Path:** `요약` → `분류 규칙`
- **Input:** native Streamlit menu click in an authenticated HTTPS browser session
- **Fixture:** generated synthetic SQLite database only
- **Sampling:** 35 DOM/style frames at 50 ms intervals
- **Sampling:** 35 DOM/computed-style frames at 50 ms intervals
- **Viewport:** 1280 × 720
- **Browser:** Headless Chrome 154 on Linux x86_64
- **Browser:** Headless Chrome 154; Client Hints architecture `arm`, bitness `64`
- **Fixture host:** Linux aarch64, Python 3.13.5, Streamlit 1.58.0, Uvicorn 0.49.0
## Verified browser observations
All eight manifest checks are true:
All twelve manifest checks are true:
- the HTML document marker stayed identical;
- the sidebar marker stayed identical;
- the page background stayed identical;
- no post-click full-document `load` event occurred;
- the target loading state was observed;
- the HTML document and sidebar markers stayed identical;
- `[data-testid="stAppViewContainer"]` retained a non-`none` computed `backgroundImage` and remained visible/rendered in every frame;
- `[data-testid="stSidebar"]` retained a non-`none` computed `backgroundImage` and remained visible/rendered in every frame;
- computed `backgroundImage`, `backgroundColor`, `visibility`, `display`, `opacity`, and rendered geometry were captured for both surfaces in every frame;
- the click and all 35 frames completed inside one uninterrupted Playwright evaluation context, so no full-document navigation occurred;
- the target loading state was observed in two frames;
- the previous `요약` body was absent in every loading frame;
- the unrelated `fixture=preserved` query key survived;
- the unrelated `source=evidence` query key survived;
- final URL, selected menu, target body, and cleared loading state agreed.
`browser-observations.json` contains every sampled frame, body hashes, URL state, history events, document/sidebar markers, computed background, runtime metadata, and explicit limitations. `final-state.jpg` is the final visible browser state; it contains no credential or secret.
`browser-observations.json` contains every frame, body hashes, URL state, markers, computed surface styles, runtime metadata, and explicit limitations. `final-state.jpg` is the final visible browser state and contains no credential or secret.
## Recheck
@@ -33,15 +34,14 @@ Run the exact validator recorded in `manifest.json`. The captured result was:
```text
exit code: 0
PASS source_hashes=5 artifact_hashes=2 checks=8/8 frames=35 full_document_loads=0
PASS source_hashes=5 artifact_hashes=2 checks=12/12 frames=35 full_document_loads=0 surfaces=2
```
The browser action was executed through `mcp__playwright__browser_run_code_unsafe`. That API reports success/error rather than a numeric process exit code; this boundary is explicit in the manifest instead of being represented as a fabricated exit status.
The browser actions used Playwright MCP. That API reports success/error rather than a numeric process exit code; the manifest records this boundary instead of fabricating an exit status.
## Evidence boundaries
- This is synthetic-fixture browser evidence, not operating-ledger evidence.
- A fixture-only external wrapper delayed the rules renderer by two seconds so the loading interval could be sampled. It did not alter repository source.
- A temporary HTTPS ingress adapter supplied trusted proxy scheme/Host metadata. Repository authentication and same-origin enforcement remained active.
- The tunnel URL is intentionally ephemeral and is retained in the frame record only for provenance.
- A temporary HTTPS ingress adapter normalized the request scheme. Repository authentication and same-origin enforcement remained active.
- The tunnel URL is intentionally ephemeral and retained only as provenance.
- The screenshot supplements, but does not replace, the machine-readable continuous-frame observations.
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 75 KiB

@@ -1,23 +1,23 @@
{
"schema": "jarca.issue9.evidence-manifest.v1",
"schema": "jarca.issue9.evidence-manifest.v2",
"issue": "jsfamily/jarca-ledger-mcp#9",
"captured_at": "2026-10-04T16:18:18.832Z",
"captured_at": "2026-10-05T08:17:17.108Z",
"source": {
"base_revision": "ffd943fe65875e67148c28ba44bae32918e338c9",
"git_head_base": "a02aceee416c5770dfd1f35ac94367f3c4e0c8c8",
"working_tree": true,
"files_sha256": {
"dashboard/app.py": "4c7b3a6264b4fb4ae0e445ddc04c625b295443816b198aa5a60615ac44846ff2",
"dashboard/common.py": "9a0b9dbb4ca88f27757fc293173821c48a56a30bc23f16b0f24731f246411c5a",
"scripts/streamlit_browser_evidence.py": "766beefb0d861ea6b08561c3aaa0c6d8915c9166e4221725e27b5e26cea63798",
"scripts/streamlit_browser_evidence.py": "3cfb449ce1d6fae3abb795727d7ca1bc10dbb759bb811f121abd74fa42993dd7",
"tests/test_dashboard_menu_transition.py": "d23c35d88f6289b7c7e84a271df6ff3f6ff1f25a3a885aa696cc93db0b37035e",
"tests/test_streamlit_browser_evidence.py": "b922593f96da5af9a0b1fe33cacbe3abe139ef00554e7c535fe7cc8f24679b23"
"tests/test_streamlit_browser_evidence.py": "7f595ccf4009961225328486c19258bb0f7bc7dbfb584c60616860274112724a"
}
},
"execution": {
"browser_tool": "mcp__playwright__browser_run_code_unsafe",
"browser_tool": "mcp__playwright__browser_evaluate + mcp__playwright__browser_run_code_unsafe",
"tool_status": "success",
"exit_code": null,
"exit_code_note": "The Playwright MCP call is not a process and exposes success/error status rather than a numeric exit code.",
"exit_code_note": "Playwright MCP reports success/error instead of a numeric process exit code.",
"transition": "요약 -> 분류 규칙",
"sampling_interval_ms": 50,
"frame_count": 35,
@@ -26,7 +26,11 @@
"height": 720
},
"browser": "HeadlessChrome/154.0.0.0",
"browser_architecture": "Linux x86_64",
"browser_architecture": {
"client_hint_architecture": "arm",
"client_hint_bitness": "64",
"navigator_platform": "Linux x86_64"
},
"fixture_host_architecture": "aarch64",
"python": "3.13.5",
"streamlit": "1.58.0",
@@ -35,7 +39,11 @@
"checks": {
"document_identity_stable": true,
"sidebar_identity_stable": true,
"background_stable": true,
"app_surface_background_stable": true,
"sidebar_surface_background_stable": true,
"app_surface_visible_all_frames": true,
"sidebar_surface_visible_all_frames": true,
"surface_backgrounds_observed": true,
"zero_full_document_loads": true,
"loading_observed": true,
"previous_body_absent_while_loading": true,
@@ -43,19 +51,18 @@
"final_agreement": true
},
"artifacts_sha256": {
"browser-observations.json": "d5dcd18f074b0f3e44e1b46eb9cf685f028b0f229957d35dc2330bedca5f0cbb",
"final-state.jpg": "5a9c94598776a9ddaf4bb1aed1c758f6d1c489c320f7a81ecb3acd0cb702760d"
"browser-observations.json": "414b62efb2f40483aae4227896040c1b5d887b2c31333f63046e6f34aec2fe8f",
"final-state.jpg": "cc4e1c28463f033ab1c26d1dc27c9bb93c6fc5495321c0cef73fdb1098221234"
},
"validator": {
"command": "python3 -c \"import hashlib,json,pathlib; root=pathlib.Path('.'); m=json.loads((root/'docs/evidence/issue-9-dashboard-menu-transition/manifest.json').read_text()); assert all(hashlib.sha256((root/p).read_bytes()).hexdigest()==h for p,h in m['source']['files_sha256'].items()); evidence=root/'docs/evidence/issue-9-dashboard-menu-transition'; assert all(hashlib.sha256((evidence/p).read_bytes()).hexdigest()==h for p,h in m['artifacts_sha256'].items()); d=json.loads((evidence/'browser-observations.json').read_text()); assert all(d['checks'].values()) and not d['document_load_events']; assert any(f['loading'] and not f['previous_body_visible'] for f in d['frames']); print('PASS source_hashes=5 artifact_hashes=2 checks=8/8 frames=%d full_document_loads=0' % len(d['frames']))\"",
"command": "python3 -c \"import hashlib,json,math,pathlib; r=pathlib.Path('.'); e=r/'docs/evidence/issue-9-dashboard-menu-transition'; m=json.loads((e/'manifest.json').read_text()); d=json.loads((e/'browser-observations.json').read_text()); frames=d['frames']; surfaces=('stAppViewContainer','stSidebar'); assert all(hashlib.sha256((r/p).read_bytes()).hexdigest()==h for p,h in m['source']['files_sha256'].items()); assert all(hashlib.sha256((e/p).read_bytes()).hexdigest()==h for p,h in m['artifacts_sha256'].items()); assert all(d['checks'].values()) and not d['document_load_events'] and len(frames)==35; assert all(isinstance(f['previous_body_visible'],bool) for f in frames); assert any(f['loading'] and f['previous_body_visible'] is False for f in frames); valid=lambda x: isinstance(x,dict) and str(x.get('backgroundImage','')).strip().lower() not in ('','none') and bool(str(x.get('backgroundColor','')).strip()) and x.get('visibility')=='visible' and x.get('display')!='none' and math.isfinite(float(x.get('opacity','nan'))) and float(x['opacity'])>0 and x.get('rendered') is True; assert all(valid(f['surfaces'][s]) for f in frames for s in surfaces); ss=d['stable_shell']; assert all(isinstance(ss[k]['pre'],str) and ss[k]['pre'] and ss[k]['pre']==ss[k]['post'] for k in ('document_marker','sidebar_marker','background')); assert ss['surfaces']['pre']==frames[0]['surfaces'] and ss['surfaces']['post']==d['post']['surfaces'] and ss['surfaces']['pre']==ss['surfaces']['post']; print('PASS source_hashes=5 artifact_hashes=2 checks=12/12 frames=%d full_document_loads=0 surfaces=2' % len(frames))\"",
"exit_code": 0,
"output": "PASS source_hashes=5 artifact_hashes=2 checks=8/8 frames=35 full_document_loads=0"
"output": "PASS source_hashes=5 artifact_hashes=2 checks=12/12 frames=35 full_document_loads=0 surfaces=2"
},
"limitations": [
"The browser run used only a generated synthetic SQLite fixture; no operating ledger data was used.",
"A fixture-only external module wrapper delayed the rules renderer by two seconds to expose the loading interval; repository source was not modified for the delay.",
"A temporary HTTPS ingress adapter normalized trusted proxy scheme and Host metadata; repository authentication and same-origin checks remained active.",
"The ephemeral tunnel URL in browser-observations.json is expired and is retained only as provenance.",
"Playwright same-document URL events are recorded separately from full document load events."
"Generated synthetic SQLite fixture only; no operating ledger data was used.",
"A temporary HTTPS ingress adapter normalized the trusted request scheme for repository same-origin authentication.",
"The ephemeral tunnel URL is expired provenance and is not a rerun target.",
"The same-document no-load assertion is bound to the uninterrupted Playwright evaluation context, stable document marker, and empty document-load event record."
]
}
+5 -1
View File
@@ -17,6 +17,7 @@ dashboard/
├── auth_server.py
├── authentication.py
├── app.py
├── common.py
└── page_views/
```
@@ -25,9 +26,12 @@ dashboard/
| `dashboard/server.py` | source | ASGI app 조립 | `app` | route wiring only | source review required | developer |
| `dashboard/auth_server.py` | source | login/cleanup/logout response | `create_auth_routes()` | no DB/page imports | source review required | developer |
| `dashboard/authentication.py` | source | config, constant-time compare, token verify, gate | `require_dashboard_authentication()` | server-only secret use | source review required | developer |
| `dashboard/app.py` | source | gate then page dispatch | `main()` | no token signing details | source review required | developer |
| `dashboard/app.py` | source | auth gate first, then DB path resolution, common shell/sidebar/refresh assembly, and target body renderer dispatch | `main()` | shell/background remain outside the replaceable body; no token signing details | source review required | developer |
| `dashboard/common.py` | source | shared Dashboard shell, menu/query transition, stable body container, loading/error/retry rendering, and app background/style | `render_sidebar_nav()`, `render_dashboard_body()`, `inject_app_css()` | no authentication transport or financial write orchestration | source review required | developer |
| `dashboard/page_views/` | source | financial UI pages | `page_*` functions | no auth transport calls | source review required | developer |
`dashboard/common.py` owns the shared visual theme through `JARCA_COLORS`, `JARCA_PLOTLY_TEMPLATE`, and `inject_app_css()`. Page renderers consume that theme but do not own or replace the authenticated shell/sidebar background.
### verification-tests — 인증 검증
```text
@@ -54,38 +54,119 @@ Issue #9의 Dashboard 메뉴 전환 계약이다. REQ-001의 인증 경계, cook
메뉴는 정확히 다음 순서와 label을 유지한다: `월정산`, `요약`, `자산/마감`, `거래내역`, `확인필요 큐`, `미분류`, `분류 규칙`, `카테고리`, `계좌`, `Import`. selected state는 현재 page와 일치한다.
**Impact**
- Interfaces: `dashboard_page` radio and `page` query parameter
- Persistence: none
- Consumers: dashboard page renderers
- Migration: none
- Compatibility: existing labels, order, and selected-state contract
- Deployment prerequisites: none
### REQ-002.02 — URL과 직접 접근
메뉴 선택은 query의 `page`만 대상 label로 갱신하고 `month`, `account_id`, `filter` 등 sibling query key를 보존한다. 직접 `?page=<known>`은 해당 page를 렌더링하고, missing/unknown 값은 `요약`으로 fallback한다.
**Impact**
- Interfaces: browser query parameters
- Persistence: none
- Consumers: direct links and page renderers
- Migration: none
- Compatibility: sibling query keys and direct page access
- Deployment prerequisites: none
### REQ-002.03 — 연속 화면
메뉴 클릭은 document navigation을 일으키지 않는다. 공통 shell과 background는 유지하고 body만 선택된 renderer의 내용으로 교체한다. history/back-forward와 path URL은 이 요구사항의 판정 범위가 아니다.
**Impact**
- Interfaces: Streamlit shell/body rendering
- Persistence: none
- Consumers: authenticated dashboard users
- Migration: none
- Compatibility: existing shell, background, and path behavior
- Deployment prerequisites: none
### REQ-002.04 — 전환 중 표시
느린 renderer가 실행되는 동안 target page label을 포함한 `불러오는 중…` loading을 대상 body 영역에 표시하고, renderer 완료 후 loading placeholder를 제거한다.
**Impact**
- Interfaces: target body loading state
- Persistence: none
- Consumers: dashboard users
- Migration: none
- Compatibility: existing renderer output after completion
- Deployment prerequisites: none
### REQ-002.05 — local failure와 retry
target renderer 예외는 공통 shell을 무너뜨리지 않고 body 영역을 generic error와 `다시 시도` control로 교체한다. 예외 상세·secret은 UI에 노출하지 않는다.
**Impact**
- Interfaces: body error and retry control
- Persistence: none
- Consumers: dashboard users and support operators
- Migration: none
- Compatibility: shell remains available; secrets remain redacted
- Deployment prerequisites: none
### REQ-002.06 — 최신 선택 우선
연속 메뉴 선택은 serialized callback 순서의 마지막 선택을 query와 body에 반영하며 이전 선택이 마지막 상태를 덮어쓰지 않는다.
**Impact**
- Interfaces: serialized menu callback and query state
- Persistence: none
- Consumers: page selection state
- Migration: none
- Compatibility: latest-selection ordering contract
- Deployment prerequisites: none
### REQ-002.07 — 인증 경계 보존
모든 menu/page dispatch는 REQ-001 gate와 cookie 검증 뒤에만 실행된다. no-cookie/invalid-cookie는 page body에 도달하지 않으며 valid cookie는 기존 Dashboard에 접근한다.
**Impact**
- Interfaces: REQ-001 authentication gate and cookie verification
- Persistence: none
- Consumers: authenticated dashboard dispatch
- Migration: none
- Compatibility: REQ-001 expiry, rotation, cleanup, and logout behavior
- Deployment prerequisites: existing REQ-001 settings
### REQ-002.08 — 금융 page 회귀 없음
요약, 자산/마감, 분류 규칙, 확인필요 큐, 월정산의 기존 synthetic fixture DB 핵심 내용과 기존 page query 계약을 유지한다.
**Impact**
- Interfaces: existing page/query contracts
- Persistence: existing synthetic fixture DB reads and writes
- Consumers: financial page renderers
- Migration: none
- Compatibility: existing financial calculations and gateway semantics
- Deployment prerequisites: existing fixture and page setup
### REQ-002.09 — 운영 control 회귀 없음
refresh control은 기존 `clear_cache` callable 계약을 유지하고, 메뉴 전환은 cache-clear 동작을 변경하지 않는다.
**Impact**
- Interfaces: `clear_cache` callable and refresh control
- Persistence: none
- Consumers: dashboard operators
- Migration: none
- Compatibility: existing cache-clear behavior
- Deployment prerequisites: none
## 6. 제약과 불변조건
- `dashboard/app.py`의 auth gate와 page dispatch는 유지하며 인증 전에 DB path를 resolve하지 않는다.
@@ -101,6 +182,10 @@ refresh control은 기존 `clear_cache` callable 계약을 유지하고, 메뉴
### REQ-002.01-AC01 — label/order/selected
**Cases**: positive, unchanged-state
**State Invariants**: labels/order and selected state remain exact.
**판정 기준**
정확한 10개 label과 순서가 radio options로 전달되고 `dashboard_page` selected state가 유지된다.
@@ -117,6 +202,10 @@ refresh control은 기존 `clear_cache` callable 계약을 유지하고, 메뉴
### REQ-002.02-AC01 — query와 fallback
**Cases**: positive, boundary, unchanged-state
**State Invariants**: only `page` changes; sibling query keys remain unchanged.
**판정 기준**
callback은 `page`만 갱신하고 sibling key를 보존하며, missing/unknown은 `요약`, known direct query는 요청 page를 선택한다.
@@ -133,6 +222,10 @@ callback은 `page`만 갱신하고 sibling key를 보존하며, missing/unknown
### REQ-002.03-AC01 — body-only transition
**Cases**: positive, unchanged-state
**State Invariants**: document, shell, and background remain stable; body changes only.
**판정 기준**
실제 browser에서 authenticated menu click 후 document URL path와 full-document identity가 바뀌지 않고 공통 sidebar/background가 남으며 target body만 바뀐다. history/back-forward와 path URL은 판정하지 않는다.
@@ -149,6 +242,10 @@ Required real-browser evidence: `docs/evidence/issue-9-dashboard-menu-transition
### REQ-002.04-AC01 — target loading
**Cases**: positive, boundary
**State Invariants**: loading appears before completion and is removed after rendering.
**판정 기준**
slow renderer 동안 target label loading이 먼저 표시되고 완료 후 placeholder가 제거된다.
@@ -162,10 +259,14 @@ slow renderer 동안 target label loading이 먼저 표시되고 완료 후 plac
- 수준: unit + browser evidence
- 백엔드: pytest fake Streamlit placeholder, authenticated Playwright browser
- 증거: ordered event list와 50 ms 연속 frame에서 `loading=true`, `previous_body_visible=false`, 최종 `loading=false`
- 한계: browser evidence의 2초 지연은 repository source가 아닌 fixture-only wrapper로 주입한다.
- 한계: browser evidence는 실제 authenticated transition을 50 ms 간격으로 표본화했으며 loading은 2개 frame에서 관찰됐다. 표본 사이의 모든 순간을 보장하지 않는다.
### REQ-002.05-AC01 — local error/retry
**Cases**: negative, positive
**State Invariants**: shell remains intact; renderer details and secrets are not exposed.
**판정 기준**
renderer 예외가 generic error/retry body로 국소 대체되고 예외 상세가 노출되지 않는다.
@@ -182,6 +283,10 @@ renderer 예외가 generic error/retry body로 국소 대체되고 예외 상세
### REQ-002.06-AC01 — latest selection
**Cases**: concurrency, unchanged-state
**State Invariants**: latest serialized selection is final.
**판정 기준**
연속 선택 `거래내역` → `자산/마감` → `Import` 후 query가 마지막 `Import`이고 sibling key가 보존된다.
@@ -198,6 +303,10 @@ renderer 예외가 generic error/retry body로 국소 대체되고 예외 상세
### REQ-002.07-AC01 — auth regression
**Cases**: negative, positive
**State Invariants**: auth gate precedes DB/page dispatch; cookie semantics remain REQ-001.
**판정 기준**
valid cookie가 기존 Dashboard를 열고 cookie가 session state에 저장되지 않으며 gate rejection은 DB/page dispatch 전에 종료된다.
@@ -214,6 +323,10 @@ valid cookie가 기존 Dashboard를 열고 cookie가 session state에 저장되
### REQ-002.08-AC01 — finance page regression
**Cases**: positive, unchanged-state
**State Invariants**: existing financial page results and query contracts remain unchanged.
**판정 기준**
기존 핵심 금융 page의 expected text와 synthetic fixture DB 결과가 유지된다.
@@ -230,6 +343,10 @@ valid cookie가 기존 Dashboard를 열고 cookie가 session state에 저장되
### REQ-002.09-AC01 — cache-clear regression
**Cases**: positive, unchanged-state
**State Invariants**: `clear_cache` remains callable and menu transition does not alter it.
**판정 기준**
refresh control이 callable `clear_cache` 계약을 유지한다.
@@ -244,15 +361,67 @@ refresh control이 callable `clear_cache` 계약을 유지한다.
- 백엔드: pytest
- 증거: callable/name assertion
### Acceptance Criteria canonical fields
Each Acceptance Criterion has explicit `Cases` and `State Invariants` in addition to the existing Korean verification text.
| Acceptance Criterion | Cases | State Invariants |
|---|---|---|
| `REQ-002.01-AC01` | positive, unchanged-state | labels/order and selected state remain exact |
| `REQ-002.02-AC01` | positive, boundary, unchanged-state | only `page` changes; sibling query keys remain unchanged |
| `REQ-002.03-AC01` | positive, unchanged-state | document, shell, and background remain stable; body changes only |
| `REQ-002.04-AC01` | positive, boundary | loading appears before completion and is removed after rendering |
| `REQ-002.05-AC01` | negative, positive | shell remains intact; renderer details and secrets are not exposed |
| `REQ-002.06-AC01` | concurrency, unchanged-state | latest serialized selection is final |
| `REQ-002.07-AC01` | negative, positive | auth gate precedes DB/page dispatch; cookie semantics remain REQ-001 |
| `REQ-002.08-AC01` | positive, unchanged-state | existing financial page results and query contracts remain unchanged |
| `REQ-002.09-AC01` | positive, unchanged-state | `clear_cache` remains callable and menu transition does not alter it |
## 8. 오류 및 충돌 계약
인증 실패는 항상 REQ-001 gate가 우선하며 메뉴·query로 우회할 수 없다. renderer 실패는 해당 body만 generic error/retry로 대체한다. 빠른 선택은 Streamlit serialized callback 순서를 따르며 마지막 serialized selection이 최종 상태다. history/back-forward와 path URL은 요구하지 않는다.
## 9. 추적성
## 9. 데이터 및 인터페이스 계약
- `dashboard_page` is the canonical selected menu field; `page` is the only query field written by menu selection.
- `month`, `account_id`, `filter`, and other sibling query keys are preserved verbatim.
- Body loading, error, retry, and renderer output are local to the stable body container; shell/background are not replaced.
## 10. 하위호환성
- Authentication expiry, rotation, invalid-cookie cleanup, and explicit logout remain governed by `REQ-001.05-AC01`, `REQ-001.07-AC01`, `REQ-001.08-AC01`, and `REQ-001.09-AC01`.
- Existing exact tests remain the authority: `tests/test_dashboard_persistent_authentication.py::test_token_expiry_is_absolute_and_not_sliding`, `tests/test_dashboard_persistent_authentication.py::test_tampering_and_rotations_invalidate_token`, and `tests/test_dashboard_auth_server.py::test_logout_clears_cookie_with_matching_scope`.
- Menu/query reruns/rendering do not initiate or replay financial writes or external calls. Financial writes and external calls remain behind explicit page user actions; this adds no new idempotency contract.
## 11. 검증 계획
- Menu transition tests and required browser evidence verify REQ-002 criteria; REQ-001 authentication tests above verify expiry, rotation, and logout regressions.
### Test Traceability Contract
Test definitions reference the exact Acceptance Criterion IDs and execution evidence records revision, commit, environment, and result separately.
## 12. 추적성
- 근거 이슈: `#9`
- 구현 PR: `#10`
- 관련 요구사항: `REQ-001`
- 관련 설계 문서: `docs/project-structure.md`, `docs/runtime-flow.md`
- 관련 requirement: `REQ-001`
- 구현 진입점: `dashboard/app.py::main`, `dashboard/common.py::render_sidebar_nav`, `dashboard/common.py::validate_dashboard_page`, `dashboard/common.py::render_dashboard_body`, `dashboard/common.py::_sync_dashboard_page_query`
- 검증 파일: `tests/test_dashboard_menu_transition.py`, `tests/test_streamlit_browser_evidence.py`, `tests/test_dashboard_app.py`, `tests/test_dashboard_authentication_app.py`
- browser evidence 도구: `scripts/streamlit_browser_evidence.py --mode transition`
## 13. 확정된 결정
- Keep the authenticated shell and background stable while replacing only the body.
- Preserve REQ-001 authentication semantics and keep financial writes/external calls behind explicit page actions.
## 14. Open Questions
- 없음
## 15. 승인 기록
- 상태: `approved`
- 승인 주체: existing requirement approval
- 승인 근거: `REQ-002` revision `1.0`
+1
View File
@@ -137,6 +137,7 @@ ASGI startup부터 login, token restoration, invalid-cookie cleanup, explicit lo
- known direct `?page=` selects that page; missing/unknown selects `요약`.
- serialized rapid selections commit the latest selection only.
- menu transition never runs before auth and never changes cookie/logout/DB schema/MCP semantics.
- menu/query reruns and rendering do not initiate or replay financial writes or external calls; those remain behind explicit page user actions, with no new idempotency contract.
- history/back-forward and path URL behavior are explicitly outside this flow.
### transition verification boundary