refactor: 문서 개선 중

This commit is contained in:
donghyeon-ka
2026-09-21 14:30:55 +09:00
parent c93cdea150
commit 805a18f486
1497 changed files with 525837 additions and 59152 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,164 @@
{
"version": "1.1",
"id": "composition-root-seam",
"title": "테스트가 끊긴 곳과 실제 런타임이 지나는 합성 루트",
"question": "게이트웨이 테스트와 화면 테스트가 통과했는데 왜 합성 루트의 credential 결함은 운영에서만 드러났는가?",
"type": "component",
"direction": "LR",
"audience": [
"프론트엔드 개발자",
"테스트 설계자"
],
"summary": "화면 테스트는 게이트웨이를 스텁하고 게이트웨이 테스트는 실행기를 스텁해서, 실제 런타임만 지나는 합성 루트의 credential 결정이 두 테스트에서 빠졌다.",
"alt": "화면, 게이트웨이, 합성 루트의 credential 결정, 실제 런타임 어댑터가 이어진 경로. 화면에는 게이트웨이 스텁, 게이트웨이에는 실행기 스텁이 표시되고 합성 루트가 테스트 공백으로 강조되어 있다.",
"long_description": "왼쪽에서 오른쪽으로 실제 런타임 경로를 읽는다. 화면에서 게이트웨이로 가고 합성 루트에서 credential을 결정한 뒤 실제 런타임 어댑터가 배포된 백엔드의 실제 404 본문을 읽는다. 화면 테스트는 게이트웨이를 스텁하고 게이트웨이 테스트는 실행기를 스텁했기 때문에 가운데 합성 루트의 credential 결정은 두 테스트가 지나지 않았다.",
"source_context": {
"document": "docs/TechLog/final/document.md",
"document_sha256": "c3a7de37b778fff7b6ea555a3ad7338c91c6fb15d685e7734f89472b4924d955",
"anchor": {
"kind": "heading",
"value": "7. 테스트가 지나지 않는 이음매",
"line": 690
}
},
"composition": {
"profile": "component-flow",
"diagram_only": true,
"reference_ids": [
"payment-event-flow"
],
"rationale": "본문은 화면·게이트웨이 테스트가 스텁에서 끊기는 반면 실제 런타임 어댑터가 합성 루트의 credential 결정을 지나가는 경로를 설명한다.",
"focus_node": "composition-root"
},
"groups": [],
"nodes": [
{
"id": "screen",
"label": "화면",
"kind": "component",
"role": "source",
"details": [
"화면 테스트: gateway stub"
],
"evidence": [
{
"start_line": 740,
"end_line": 742
},
{
"start_line": 770,
"end_line": 771
}
],
"assumption": false
},
{
"id": "gateway",
"label": "Gateway",
"kind": "service",
"role": "service",
"details": [
"gateway 테스트: executor stub"
],
"evidence": [
{
"start_line": 766,
"end_line": 771
}
],
"assumption": false
},
{
"id": "composition-root",
"label": "Composition Root",
"kind": "component",
"role": "service",
"details": [
"credential decision",
"테스트 공백"
],
"evidence": [
{
"start_line": 748,
"end_line": 767
}
],
"assumption": false,
"emphasis": "warning"
},
{
"id": "runtime-adapter",
"label": "Runtime Adapter + 404",
"kind": "service",
"role": "sink",
"details": [
"실제 어댑터",
"배포된 백엔드 404 본문"
],
"evidence": [
{
"start_line": 769,
"end_line": 771
}
],
"assumption": false
}
],
"edges": [
{
"id": "screen-gateway",
"from": "screen",
"to": "gateway",
"label": "요청",
"kind": "request",
"evidence": [
{
"start_line": 740,
"end_line": 742
},
{
"start_line": 766,
"end_line": 771
}
],
"assumption": false
},
{
"id": "gateway-root",
"from": "gateway",
"to": "composition-root",
"label": "runtime 조립",
"kind": "request",
"evidence": [
{
"start_line": 766,
"end_line": 771
}
],
"assumption": false
},
{
"id": "root-adapter",
"from": "composition-root",
"to": "runtime-adapter",
"label": "credential 판정",
"kind": "request",
"evidence": [
{
"start_line": 755,
"end_line": 767
},
{
"start_line": 769,
"end_line": 771
}
],
"assumption": false
}
],
"legend": [],
"metadata": {
"rationale": "테스트 두 개를 별도 카드로 다시 그리지 않고 실제 런타임 경로에 각 테스트가 어디에서 스텁되는지 details로 표시했다.",
"layout_note": "선형 4단계라 LR을 유지한다. 테스트가 끊기는 두 지점과 가운데 composition-root 공백을 한 시야에서 비교하는 것이 목적이다."
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -5,23 +5,28 @@
"question": "계약은 앵커 주소를 적어 두었는데 방문자는 왜 404 를 만났는가?",
"type": "sequence",
"direction": "TB",
"audience": ["백엔드 개발자", "프론트엔드 개발자"],
"audience": [
"백엔드 개발자",
"프론트엔드 개발자"
],
"summary": "만드는 쪽 두 곳이 계약과 다른 슬래시 주소를 만들었고, 그 주소가 게시 시점에 저장돼 방문자에게 그대로 나갔다.",
"alt": "계약, 게시 시점 경로 생성, 저장 테이블, 조회 시점 경로 생성, 방문자, 공개 라우트 여섯 참가자 사이에서 주소가 만들어져 저장되고 방문 시 404 로 끝나는 순서도.",
"long_description": "위에서 아래로 여섯 번의 이동이 있다. 계약 ProjectDecisionItem 은 공개 주소가 decisions#{slug} 앵커라고 규정한다. 게시 시점의 PublicPaths.forKind 는 그 대신 decisions/{slug} 를 만들어 public_resource_projection 에 저장한다. 조회 시점의 PublicSql.pathOf 가 저장된 주소를 읽고 방문자에게 링크로 내보낸다. 방문자가 그 주소를 요청하면 공개 라우트에는 projects/{slug}/decisions 하나뿐이라 맞는 라우트가 없고 404 가 돌아온다.",
"source_context": {
"document": "document.md",
"document_sha256": "93b9fec4884efa0e6231de07dc27e2b0ac36c9052d3720e28d102d9747ac4f8f",
"document": "docs/TechLog/final/document.md",
"document_sha256": "c3a7de37b778fff7b6ea555a3ad7338c91c6fb15d685e7734f89472b4924d955",
"anchor": {
"kind": "marker",
"value": "decision-path-404",
"line": 673
"line": 916
}
},
"composition": {
"profile": "sequence",
"diagram_only": true,
"reference_ids": ["payment-approval-sequence"],
"reference_ids": [
"payment-approval-sequence"
],
"rationale": "본문은 주소가 계약에서 규정되고, 게시 시점에 만들어져 저장되고, 조회 시점에 읽혀 방문자에게 나가고, 방문했을 때 404 가 되는 순서를 적는다. 「주소가 게시 시점에 굳는다」가 이 결함의 핵심이라 시점의 순서가 그림의 뼈대여야 한다."
},
"nodes": [
@@ -31,7 +36,12 @@
"kind": "participant",
"role": "participant",
"description": "공개 주소를 앵커로 규정한 OpenAPI 계약.",
"evidence": [{ "start_line": 676, "end_line": 678 }],
"evidence": [
{
"start_line": 919,
"end_line": 921
}
],
"assumption": false
},
{
@@ -39,10 +49,17 @@
"label": "PublicPaths.forKind",
"kind": "participant",
"role": "participant",
"details": ["게시 시점"],
"details": [
"게시 시점"
],
"emphasis": "warning",
"description": "게시할 때 공개 주소를 만드는 코드.",
"evidence": [{ "start_line": 677, "end_line": 678 }],
"evidence": [
{
"start_line": 919,
"end_line": 921
}
],
"assumption": false
},
{
@@ -51,7 +68,12 @@
"kind": "participant",
"role": "participant",
"description": "만들어진 주소가 저장되는 투영 테이블.",
"evidence": [{ "start_line": 682, "end_line": 683 }],
"evidence": [
{
"start_line": 925,
"end_line": 926
}
],
"assumption": false
},
{
@@ -59,10 +81,17 @@
"label": "PublicSql.pathOf",
"kind": "participant",
"role": "participant",
"details": ["조회 시점"],
"details": [
"조회 시점"
],
"emphasis": "warning",
"description": "조회할 때 공개 주소를 만드는 코드.",
"evidence": [{ "start_line": 677, "end_line": 678 }],
"evidence": [
{
"start_line": 919,
"end_line": 921
}
],
"assumption": false
},
{
@@ -71,7 +100,12 @@
"kind": "actor",
"role": "participant",
"description": "「다음에 읽을 것」 링크를 따라간 사람.",
"evidence": [{ "start_line": 670, "end_line": 671 }],
"evidence": [
{
"start_line": 913,
"end_line": 914
}
],
"assumption": false
},
{
@@ -79,9 +113,16 @@
"label": "공개 라우트",
"kind": "participant",
"role": "participant",
"details": ["/projects/{slug}/decisions 하나뿐"],
"details": [
"/projects/{slug}/decisions 하나뿐"
],
"description": "결정에는 상세 화면이 없어 라우트가 하나뿐이다.",
"evidence": [{ "start_line": 675, "end_line": 676 }],
"evidence": [
{
"start_line": 918,
"end_line": 919
}
],
"assumption": false
}
],
@@ -93,7 +134,12 @@
"label": "…/decisions#{slug} 로 규정",
"kind": "request",
"order": 1,
"evidence": [{ "start_line": 676, "end_line": 678 }],
"evidence": [
{
"start_line": 919,
"end_line": 921
}
],
"assumption": false
},
{
@@ -104,7 +150,16 @@
"kind": "request",
"order": 2,
"emphasis": "warning",
"evidence": [{ "start_line": 675, "end_line": 678 }],
"evidence": [
{
"start_line": 919,
"end_line": 921
},
{
"start_line": 925,
"end_line": 926
}
],
"assumption": false
},
{
@@ -114,7 +169,12 @@
"label": "저장된 주소 조회",
"kind": "response",
"order": 3,
"evidence": [{ "start_line": 682, "end_line": 683 }],
"evidence": [
{
"start_line": 919,
"end_line": 926
}
],
"assumption": false
},
{
@@ -124,7 +184,12 @@
"label": "같은 형태로 링크 전달",
"kind": "response",
"order": 4,
"evidence": [{ "start_line": 677, "end_line": 678 }],
"evidence": [
{
"start_line": 913,
"end_line": 921
}
],
"assumption": false
},
{
@@ -134,7 +199,12 @@
"label": "…/decisions/{slug} 요청",
"kind": "request",
"order": 5,
"evidence": [{ "start_line": 670, "end_line": 675 }],
"evidence": [
{
"start_line": 913,
"end_line": 919
}
],
"assumption": false
},
{
@@ -145,7 +215,12 @@
"kind": "response",
"order": 6,
"emphasis": "warning",
"evidence": [{ "start_line": 670, "end_line": 675 }],
"evidence": [
{
"start_line": 913,
"end_line": 919
}
],
"assumption": false
}
],
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,155 @@
{
"version": "1.1",
"id": "record-kind-fanout",
"title": "새 CONCEPT 종류가 세 영역의 손 목록으로 퍼진 구조",
"question": "CONCEPT 하나를 추가했는데 왜 계약·백엔드·프론트엔드 여러 위치를 동시에 고쳐야 했는가?",
"type": "dependency",
"direction": "LR",
"audience": [
"프론트엔드 개발자",
"백엔드 개발자",
"계약 설계자"
],
"summary": "종류를 손으로 나열한 코드가 여러 영역에 흩어져 있어 CONCEPT 하나가 프론트엔드 9곳, 백엔드 1곳, 계약 3곳에서 따로 빠졌다.",
"alt": "새 CONCEPT 노드에서 프론트엔드 손 목록, 백엔드 손 목록, 계약 enum 세 갈래로 퍼지는 팬아웃 그림. 각 갈래에는 실제 누락 건수 9, 1, 3이 적혀 있다.",
"long_description": "왼쪽의 새 CONCEPT가 세 갈래로 퍼진다. 프론트엔드에는 게이트웨이 분기, 매퍼, 필터 같은 손 목록이 아홉 곳 있었고, 백엔드에는 PublicSql.pathOf 한 곳이 빠졌다. 계약에는 CatalogEntry.kind, ResolvedRelation.targetKind, RelatedEntry.type 세 enum 누락이 있었다. 아래 본문 표가 열세 위치를 정확히 나열하고, 그림은 왜 한 종류 변경이 세 영역으로 퍼졌는지만 보여 준다.",
"source_context": {
"document": "docs/TechLog/final/document.md",
"document_sha256": "c3a7de37b778fff7b6ea555a3ad7338c91c6fb15d685e7734f89472b4924d955",
"anchor": {
"kind": "heading",
"value": "3. 손으로 나열한 목록이 새 종류를 삼킨다",
"line": 311
}
},
"composition": {
"profile": "component-flow",
"diagram_only": true,
"reference_ids": [
"payment-event-flow"
],
"rationale": "정확한 열세 위치는 이미 표가 있으므로, 그림은 새 종류 하나에서 계약·백엔드·프론트엔드로 퍼지는 의존 방향만 보여 준다.",
"focus_node": "concept"
},
"groups": [],
"nodes": [
{
"id": "concept",
"label": "CONCEPT",
"kind": "message",
"role": "source",
"details": [
"새 Record Kind"
],
"evidence": [
{
"start_line": 316,
"end_line": 331
}
],
"assumption": false,
"emphasis": "primary"
},
{
"id": "frontend",
"label": "Frontend 손 목록",
"kind": "component",
"role": "service",
"details": [
"9곳",
"gateway · mapper · filter"
],
"evidence": [
{
"start_line": 335,
"end_line": 345
}
],
"assumption": false
},
{
"id": "backend",
"label": "Backend 손 목록",
"kind": "component",
"role": "service",
"details": [
"1곳",
"PublicSql.pathOf"
],
"evidence": [
{
"start_line": 345,
"end_line": 349
}
],
"assumption": false
},
{
"id": "contract",
"label": "Contract enums",
"kind": "component",
"role": "sink",
"details": [
"3곳",
"CatalogEntry.kind",
"ResolvedRelation.targetKind",
"RelatedEntry.type"
],
"evidence": [
{
"start_line": 346,
"end_line": 352
}
],
"assumption": false
}
],
"edges": [
{
"id": "to-frontend",
"from": "concept",
"to": "frontend",
"label": "kind 추가",
"kind": "data",
"evidence": [
{
"start_line": 330,
"end_line": 345
}
],
"assumption": false
},
{
"id": "to-backend",
"from": "concept",
"to": "backend",
"label": "kind 추가",
"kind": "data",
"evidence": [
{
"start_line": 345,
"end_line": 349
}
],
"assumption": false
},
{
"id": "to-contract",
"from": "concept",
"to": "contract",
"label": "enum 추가",
"kind": "data",
"evidence": [
{
"start_line": 346,
"end_line": 352
}
],
"assumption": false
}
],
"legend": [],
"metadata": {
"rationale": "13개의 동일한 카드 대신 세 영역으로만 묶는다. 정확한 누락 위치와 증상은 본문 표가 유지한다."
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,198 @@
{
"version": "1.1",
"id": "route-fanout",
"title": "라우트 하나가 건드리는 손 목록과 검출 시점",
"question": "새 라우트 하나가 어디까지 퍼지고, 빠뜨린 항목은 어느 시점에 처음 드러나는가?",
"type": "dependency",
"direction": "LR",
"audience": [
"프론트엔드 개발자",
"배포 파이프라인 유지보수자"
],
"summary": "Route Contract에서 런타임·빌드·CI·nginx 쪽 손 목록으로 갈라지고, 누락은 빌드 매니페스트·배포 직전·배포 뒤 서로 다른 시점에 드러난다.",
"alt": "새 Route가 Route Contract를 거쳐 Runtime 계약, 배포 전 검사, Edge 서빙 세 묶음으로 갈라지는 팬아웃 그림. 각 묶음에는 누락이 처음 드러나는 시점이 적혀 있다.",
"long_description": "왼쪽의 새 Route가 Route Contract로 들어간 뒤 세 갈래로 퍼진다. Runtime 계약에는 runtime 등록과 메시지 카탈로그가 있다. 배포 전 검사에는 vite chunk 이름 표와 접근성 증거·아티팩트 기준선·gate digest가 묶여 있고 누락은 빌드 매니페스트 또는 배포 직전에 드러난다. Edge 서빙 규칙 누락은 하드 로드나 새로고침 때 배포 뒤 404로 드러난다.",
"source_context": {
"document": "docs/TechLog/final/document.md",
"document_sha256": "c3a7de37b778fff7b6ea555a3ad7338c91c6fb15d685e7734f89472b4924d955",
"anchor": {
"kind": "heading",
"value": "8. 라우트를 하나 더하면 함께 울리는 손 목록",
"line": 814
}
},
"composition": {
"profile": "query-fanout",
"diagram_only": true,
"reference_ids": [
"metrics-query-fanout"
],
"rationale": "본문의 핵심은 새 Route 하나가 여러 손 목록으로 퍼지는 fan-out이다. 정확한 여덟 위치는 표가 맡고, 그림은 네 소유 묶음과 검출 시점 차이를 보여 준다.",
"focus_node": "route-contract"
},
"groups": [],
"nodes": [
{
"id": "route",
"label": "새 Route",
"kind": "request",
"role": "query",
"evidence": [
{
"start_line": 816,
"end_line": 821
}
],
"assumption": false
},
{
"id": "route-contract",
"label": "Route Contract",
"kind": "component",
"role": "router",
"details": [
"tech-log-route-contract.ts"
],
"evidence": [
{
"start_line": 819,
"end_line": 831
}
],
"assumption": false,
"emphasis": "primary"
},
{
"id": "runtime",
"label": "Runtime 계약",
"kind": "component",
"role": "store",
"details": [
"route-runtime-contract",
"메시지 카탈로그",
"검출: 실행 경로"
],
"evidence": [
{
"start_line": 824,
"end_line": 826
}
],
"assumption": false
},
{
"id": "predeploy",
"label": "배포 전 검사",
"kind": "component",
"role": "store",
"details": [
"vite chunk 표 · 빌드 매니페스트",
"접근성 증거 · 아티팩트 기준선 · gate digest",
"검출: 빌드 / 배포 직전"
],
"evidence": [
{
"start_line": 828,
"end_line": 831
},
{
"start_line": 854,
"end_line": 877
}
],
"assumption": false
},
{
"id": "nginx",
"label": "Edge serving",
"kind": "component",
"role": "store",
"details": [
"nginx serving contract",
"검출: 배포 뒤 404"
],
"evidence": [
{
"start_line": 827,
"end_line": 827
},
{
"start_line": 834,
"end_line": 852
}
],
"assumption": false
}
],
"edges": [
{
"id": "register",
"from": "route",
"to": "route-contract",
"label": "등록",
"kind": "request",
"evidence": [
{
"start_line": 819,
"end_line": 831
}
],
"assumption": false
},
{
"id": "runtime-edge",
"from": "route-contract",
"to": "runtime",
"label": "반영",
"kind": "data",
"evidence": [
{
"start_line": 824,
"end_line": 826
}
],
"assumption": false
},
{
"id": "predeploy-edge",
"from": "route-contract",
"to": "predeploy",
"label": "대조",
"kind": "data",
"evidence": [
{
"start_line": 828,
"end_line": 831
},
{
"start_line": 854,
"end_line": 877
}
],
"assumption": false
},
{
"id": "nginx-edge",
"from": "route-contract",
"to": "nginx",
"label": "서빙 패턴",
"kind": "data",
"evidence": [
{
"start_line": 827,
"end_line": 827
},
{
"start_line": 834,
"end_line": 852
}
],
"assumption": false
}
],
"legend": [],
"metadata": {
"rationale": "8개 항목을 같은 카드로 반복하지 않고, 독자가 먼저 알아야 할 fan-out과 검출 시점만 묶었다. 정확한 항목 수와 커밋별 수치는 본문 표에 남긴다.",
"layout_note": "세 fan-out 가지를 한 화면에 유지한다. 9px shared-edge-run advisory가 남더라도 의미상 간선이 다른 노드를 통과하지 않는지 overlap 검사와 PNG preview에서 재확인한다."
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,153 @@
{
"version": "1.1",
"id": "summary-drop-path",
"title": "summary가 세 경계에서 사라진 경로",
"question": "계약과 DB에 있던 summary가 공개 relation 목록까지 오지 못한 세 유실 지점은 어디였는가?",
"type": "data-flow",
"direction": "LR",
"audience": [
"프론트엔드 개발자",
"계약 설계자"
],
"summary": "계약에는 summary가 있었지만 flattenRelations가 담지 않았고, ResolvedRelation에는 칸이 없었고, 화면 목록으로 넘길 때 다시 버렸다.",
"alt": "Contract summary에서 flattenRelations, ResolvedRelation, 화면 목록으로 이어지는 흐름. 세 중간 지점에 DROP 1, 칸 없음, DROP 3이 표시되어 있다.",
"long_description": "왼쪽 Contract에는 summary가 있다. flattenRelations가 그 값을 담지 않아 첫 번째로 끊긴다. 다음 ResolvedRelation 계약에는 summary 칸 자체가 없어 두 번째로 막힌다. 그 칸을 추가한 뒤에도 화면 목록으로 넘길 때 값을 버려 세 번째로 끊겼다. 최종 수정에서는 세 경계를 모두 이어 공개 relation 목록까지 summary가 도착하게 했다.",
"source_context": {
"document": "docs/TechLog/final/document.md",
"document_sha256": "c3a7de37b778fff7b6ea555a3ad7338c91c6fb15d685e7734f89472b4924d955",
"anchor": {
"kind": "heading",
"value": "5. 계약에 자리가 없어 값이 경계에서 사라진다",
"line": 516
}
},
"composition": {
"profile": "component-flow",
"diagram_only": true,
"reference_ids": [
"payment-event-flow"
],
"rationale": "본문 자체가 계약에서 화면 목록까지 summary가 지나가는 순서를 네 단계로 적고 세 유실 지점을 명시한다.",
"focus_node": "resolved-relation"
},
"groups": [],
"nodes": [
{
"id": "contract",
"label": "Contract summary",
"kind": "message",
"role": "source",
"details": [
"summary 있음"
],
"evidence": [
{
"start_line": 540,
"end_line": 547
}
],
"assumption": false
},
{
"id": "flatten",
"label": "flattenRelations",
"kind": "component",
"role": "service",
"details": [
"DROP #1"
],
"evidence": [
{
"start_line": 544,
"end_line": 547
}
],
"assumption": false,
"emphasis": "warning"
},
{
"id": "resolved-relation",
"label": "ResolvedRelation",
"kind": "component",
"role": "service",
"details": [
"summary 칸 없음",
"additionalProperties: false"
],
"evidence": [
{
"start_line": 546,
"end_line": 552
}
],
"assumption": false,
"emphasis": "warning"
},
{
"id": "screen-list",
"label": "화면 relation 목록",
"kind": "component",
"role": "sink",
"details": [
"DROP #3"
],
"evidence": [
{
"start_line": 547,
"end_line": 552
}
],
"assumption": false,
"emphasis": "warning"
}
],
"edges": [
{
"id": "to-flatten",
"from": "contract",
"to": "flatten",
"label": "summary",
"kind": "data",
"evidence": [
{
"start_line": 544,
"end_line": 545
}
],
"assumption": false
},
{
"id": "to-model",
"from": "flatten",
"to": "resolved-relation",
"label": "렌더 모델",
"kind": "data",
"evidence": [
{
"start_line": 545,
"end_line": 550
}
],
"assumption": false
},
{
"id": "to-screen",
"from": "resolved-relation",
"to": "screen-list",
"label": "화면 전달",
"kind": "data",
"evidence": [
{
"start_line": 546,
"end_line": 552
}
],
"assumption": false
}
],
"legend": [],
"metadata": {
"rationale": "상위 Concept의 11경계 그림과 겹치지 않도록 이번 사고에서 실제로 summary가 끊긴 세 지점만 그린다.",
"layout_note": "네 경계를 한 줄로 따라가는 사고 경로라 LR을 유지한다. 이 그림은 전체 11경계가 아니라 세 유실 지점만 좁힌다."
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -13,12 +13,12 @@
"alt": "왼쪽부터 topic, topic_variant, record_variant 로 이어지고 record_variant 가 document·open_question·project_decision 세 테이블을 가리키는 구조도.",
"long_description": "왼쪽에 topic 이 있고 variant_label 로 축의 이름을 스스로 정한다. 그 오른쪽에 topic_variant 가 있고 SPA, Mediator, BFF, Forward-Auth 같은 축의 값들을 담는다. 그 오른쪽에 record_variant 가 있고 어느 기록이 어느 축에 걸리는지를 종류와 아이디의 쌍으로 적는다. record_variant 는 오른쪽의 document, open_question, project_decision 세 테이블을 가리키는데, 기록이 종류마다 다른 테이블에 살기 때문에 외래키를 걸지 못하고 쌍으로만 가리킨다.",
"source_context": {
"document": "document.md",
"document_sha256": "93b9fec4884efa0e6231de07dc27e2b0ac36c9052d3720e28d102d9747ac4f8f",
"document": "docs/TechLog/final/document.md",
"document_sha256": "c3a7de37b778fff7b6ea555a3ad7338c91c6fb15d685e7734f89472b4924d955",
"anchor": {
"kind": "marker",
"value": "topic-variant-model",
"line": 1064
"line": 1375
}
},
"composition": {
@@ -38,8 +38,8 @@
"role": "zone",
"evidence": [
{
"start_line": 1071,
"end_line": 1073
"start_line": 1382,
"end_line": 1384
}
],
"assumption": false
@@ -58,12 +58,12 @@
"description": "주제. 축의 이름을 주제가 정한다.",
"evidence": [
{
"start_line": 1057,
"end_line": 1059
"start_line": 1368,
"end_line": 1370
},
{
"start_line": 1067,
"end_line": 1068
"start_line": 1377,
"end_line": 1379
}
],
"assumption": false
@@ -80,12 +80,12 @@
"description": "축의 값들.",
"evidence": [
{
"start_line": 1060,
"end_line": 1060
"start_line": 1371,
"end_line": 1371
},
{
"start_line": 1047,
"end_line": 1048
"start_line": 1358,
"end_line": 1359
}
],
"assumption": false
@@ -103,12 +103,12 @@
"description": "어느 기록이 어느 축에 걸리는지 적는 자리. 외래키를 걸지 못한다.",
"evidence": [
{
"start_line": 1061,
"end_line": 1061
"start_line": 1372,
"end_line": 1372
},
{
"start_line": 1071,
"end_line": 1073
"start_line": 1382,
"end_line": 1384
}
],
"assumption": false
@@ -123,8 +123,8 @@
"description": "기록 테이블 하나.",
"evidence": [
{
"start_line": 1071,
"end_line": 1072
"start_line": 1382,
"end_line": 1384
}
],
"assumption": false
@@ -139,8 +139,8 @@
"description": "기록 테이블 하나.",
"evidence": [
{
"start_line": 1071,
"end_line": 1072
"start_line": 1382,
"end_line": 1384
}
],
"assumption": false
@@ -155,8 +155,8 @@
"description": "기록 테이블 하나.",
"evidence": [
{
"start_line": 1071,
"end_line": 1072
"start_line": 1382,
"end_line": 1384
}
],
"assumption": false
@@ -172,8 +172,8 @@
"style": "solid",
"evidence": [
{
"start_line": 1057,
"end_line": 1060
"start_line": 1368,
"end_line": 1371
}
],
"assumption": false
@@ -187,8 +187,8 @@
"style": "solid",
"evidence": [
{
"start_line": 1060,
"end_line": 1061
"start_line": 1371,
"end_line": 1372
}
],
"assumption": false
@@ -202,8 +202,8 @@
"style": "dashed",
"evidence": [
{
"start_line": 1061,
"end_line": 1073
"start_line": 1372,
"end_line": 1384
}
],
"assumption": false
@@ -217,8 +217,8 @@
"style": "dashed",
"evidence": [
{
"start_line": 1061,
"end_line": 1073
"start_line": 1372,
"end_line": 1384
}
],
"assumption": false
@@ -232,8 +232,8 @@
"style": "dashed",
"evidence": [
{
"start_line": 1061,
"end_line": 1073
"start_line": 1372,
"end_line": 1384
}
],
"assumption": false
@@ -249,4 +249,4 @@
"rationale": "축에 걸리지 않은 기록이 공통 기록이 된다는 규칙과 editorial 칸(thesis·summary·conclusion) 이야기는 같은 절의 문장으로 남긴다. 그림은 자리와 참조 방향만 담는다.",
"profile_deviation": "techviz references 가 고른 후보(two-zone-pipeline, sequence, comparison) 밖의 프로필이다. 이 절에는 시간 순서도 두 구역도 비교 대상도 없어 후보로는 그릴 수 없었다."
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -11,15 +11,15 @@
"아키텍처 검토자"
],
"summary": "열한 개의 경계를 지나는 자리별로 묶으면 저장 둘, 백엔드 조립 넷, 전선 하나, 프론트엔드 조립 셋, 화면 하나다.",
"alt": "저장·백엔드 조립·HTTP envelope·프론트엔드 조립·화면 다섯 묶음을 tech-log-backend·전선·tech-log-frontend 구역으로 나눠 이은 흐름도. 묶음마다 그 안에 든 경계 수가 2·4·1·3·1 로 적혀 있다.",
"alt": "열한 개 경계를 저장·백엔드 조립·전선·프론트엔드 조립·화면 다섯 구간으로 묶고, tech-log-backend·HTTP·tech-log-frontend 소유 구역을 함께 표시한 흐름도.",
"long_description": "왼쪽에서 오른쪽으로 읽는다. tech-log-backend 구역에 저장 묶음과 백엔드 조립 묶음이 있고 각각 경계 둘과 넷을 담는다. 전선 구역에는 HTTP envelope 하나가 있다. tech-log-frontend 구역에는 프론트엔드 조립 묶음과 화면 컴포넌트가 있고 각각 경계 셋과 하나다. 다 더하면 열한 개이고, 각 경계의 이름은 그림 위 목록에 있다.",
"source_context": {
"document": "document.md",
"document_sha256": "93b9fec4884efa0e6231de07dc27e2b0ac36c9052d3720e28d102d9747ac4f8f",
"document": "docs/TechLog/final/document.md",
"document_sha256": "c3a7de37b778fff7b6ea555a3ad7338c91c6fb15d685e7734f89472b4924d955",
"anchor": {
"kind": "marker",
"value": "value-boundaries",
"line": 82
"line": 85
}
},
"composition": {
@@ -235,6 +235,7 @@
"metadata": {
"rationale": "각 경계의 이름은 바로 위 목록이 이미 순서대로 적는다. 그림은 그 열한 개가 어느 소유 구역에 몇 개씩 놓이는지만 담는다.",
"profile_deviation": "techviz references 가 고른 후보 밖의 프로필이다. 후보 셋으로는 사슬을 그릴 수 없어 component-flow 로 갔고 lint 는 0 error 로 통과했다.",
"layout_note": "LR 로 둔다. TB 는 aspect-ratio 경고를 없애지만 그룹 이름이 잘리고(tech-log-fro) 오른쪽이 비어, 읽기에는 LR 이 낫다. 남는 경고는 advisory 다."
"layout_note": "LR 로 둔다. TB 는 aspect-ratio 경고를 없애지만 그룹 이름이 잘리고(tech-log-fro) 오른쪽이 비어, 읽기에는 LR 이 낫다. 남는 경고는 advisory 다.",
"advisory_acceptance": "LR aspect-ratio advisory를 허용한다. TB로 바꾸면 그룹 이름이 잘리고 소유 구역 비교가 더 어려워져, 값의 진행 방향과 세 소유 구역을 한 줄에 유지하는 편이 독해에 낫다."
}
}
}
@@ -0,0 +1,22 @@
# 테스트가 끊긴 곳과 실제 런타임이 지나는 합성 루트
## Alternative text
화면, 게이트웨이, 합성 루트의 credential 결정, 실제 런타임 어댑터가 이어진 경로. 화면에는 게이트웨이 스텁, 게이트웨이에는 실행기 스텁이 표시되고 합성 루트가 테스트 공백으로 강조되어 있다.
## Long description
왼쪽에서 오른쪽으로 실제 런타임 경로를 읽는다. 화면에서 게이트웨이로 가고 합성 루트에서 credential을 결정한 뒤 실제 런타임 어댑터가 배포된 백엔드의 실제 404 본문을 읽는다. 화면 테스트는 게이트웨이를 스텁하고 게이트웨이 테스트는 실행기를 스텁했기 때문에 가운데 합성 루트의 credential 결정은 두 테스트가 지나지 않았다.
## Elements and evidence
- **화면** (component): No additional description. Evidence: L740L742, L770L771.
- **Gateway** (service): No additional description. Evidence: L766L771.
- **Composition Root** (component): No additional description. Evidence: L748L767.
- **Runtime Adapter + 404** (service): No additional description. Evidence: L769L771.
## Relationships
- **Gateway → Composition Root:** runtime 조립. Evidence: L766L771.
- **Composition Root → Runtime Adapter + 404:** credential 판정. Evidence: L755L767, L769L771.
- **화면 → Gateway:** 요청. Evidence: L740L742, L766L771.
@@ -0,0 +1,18 @@
# 테스트가 끊긴 곳과 실제 런타임이 지나는 합성 루트
# Question: 게이트웨이 테스트와 화면 테스트가 통과했는데 왜 합성 루트의 credential 결함은 운영에서만 드러났는가?
direction: right
n0: "화면" {
shape: rectangle
}
n1: "Gateway" {
shape: rectangle
}
n2: "Composition Root" {
shape: rectangle
}
n3: "Runtime Adapter + 404" {
shape: rectangle
}
n0 -> n1: "요청"
n1 -> n2: "runtime 조립"
n2 -> n3: "credential 판정"
@@ -0,0 +1,12 @@
digraph techviz {
graph [rankdir=LR, splines=ortho, nodesep=0.55, ranksep=0.85];
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
n0 [label="화면", shape=box, style="rounded,filled"];
n1 [label="Gateway", shape=box, style="rounded,filled"];
n2 [label="Composition Root", shape=box, style="rounded,filled"];
n3 [label="Runtime Adapter + 404", shape=box, style="rounded,filled"];
n0 -> n1 [label="요청", style=solid];
n1 -> n2 [label="runtime 조립", style=solid];
n2 -> n3 [label="credential 판정", style=solid];
}
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
<diagram id="composition-root-seam" name="테스트가 끊긴 곳과 실제 런타임이 지나는 합성 루트">
<mxGraphModel dx="1333" dy="300" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1333" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="n_screen" value="화면&lt;br/&gt;화면 테스트: gateway stub" tooltip="component | Evidence: L740-L742, L770-L771" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="70.0" y="68.5" width="174.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_gateway" value="Gateway&lt;br/&gt;gateway 테스트: executor stub" tooltip="service | Evidence: L766-L771" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="404.0" y="68.5" width="216.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_composition-root" value="Composition Root&lt;br/&gt;credential decision&lt;br/&gt;테스트 공백" tooltip="component | Evidence: L748-L767" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
<mxGeometry x="780.0" y="60.0" width="167.0" height="88.0" as="geometry"/>
</mxCell>
<mxCell id="n_runtime-adapter" value="Runtime Adapter + 404&lt;br/&gt;실제 어댑터&lt;br/&gt;배포된 백엔드 404 본문" tooltip="service | Evidence: L769-L771" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="1107.0" y="60.0" width="181.0" height="88.0" as="geometry"/>
</mxCell>
<mxCell id="e_gateway-root" value="runtime 조립" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_gateway" target="n_composition-root">
<mxGeometry relative="1" as="geometry">
<mxPoint x="700.0" y="76.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_root-adapter" value="credential 판정" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_composition-root" target="n_runtime-adapter">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1027.0" y="76.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_screen-gateway" value="요청" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_screen" target="n_gateway">
<mxGeometry relative="1" as="geometry">
<mxPoint x="324.0" y="76.0" as="offset"/>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
@@ -0,0 +1,586 @@
{
"type": "excalidraw",
"version": 2,
"source": "techviz-harness",
"elements": [
{
"id": "edge-gateway-root",
"type": "arrow",
"x": 620.0,
"y": 104.0,
"width": 160.0,
"height": 0.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 327602906,
"version": 1,
"versionNonce": 1078118842,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
0.0
],
[
80.0,
0.0
],
[
80.0,
0.0
],
[
160.0,
0.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-gateway",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-composition-root",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-gateway-root",
"type": "text",
"x": 655.0,
"y": 64.0,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1007189003,
"version": 1,
"versionNonce": 80325236,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "runtime 조립",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "runtime 조립",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-root-adapter",
"type": "arrow",
"x": 947.0,
"y": 104.0,
"width": 160.0,
"height": 0.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 358009000,
"version": 1,
"versionNonce": 1415432956,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
0.0
],
[
80.0,
0.0
],
[
80.0,
0.0
],
[
160.0,
0.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-composition-root",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-runtime-adapter",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-root-adapter",
"type": "text",
"x": 975.0,
"y": 64.0,
"width": 104,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1363224969,
"version": 1,
"versionNonce": 878347516,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "credential 판정",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "credential 판정",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-screen-gateway",
"type": "arrow",
"x": 244.0,
"y": 104.0,
"width": 160.0,
"height": 0.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 1497458713,
"version": 1,
"versionNonce": 208440406,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
0.0
],
[
80.0,
0.0
],
[
80.0,
0.0
],
[
160.0,
0.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-screen",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-gateway",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-screen-gateway",
"type": "text",
"x": 279.0,
"y": 64.0,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1813770200,
"version": 1,
"versionNonce": 1860428082,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "요청",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "요청",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-screen",
"type": "rectangle",
"x": 70.0,
"y": 68.5,
"width": 174.0,
"height": 71.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1341250523,
"version": 1,
"versionNonce": 1431512902,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-screen",
"type": "text",
"x": 80.0,
"y": 78.5,
"width": 154.0,
"height": 51.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1751546587,
"version": 1,
"versionNonce": 1327173583,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "화면\n화면 테스트: gateway stub",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "화면\n화면 테스트: gateway stub",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-gateway",
"type": "rectangle",
"x": 404.0,
"y": 68.5,
"width": 216.0,
"height": 71.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1264474515,
"version": 1,
"versionNonce": 568841533,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-gateway",
"type": "text",
"x": 414.0,
"y": 78.5,
"width": 196.0,
"height": 51.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 222918616,
"version": 1,
"versionNonce": 1589548629,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "Gateway\ngateway 테스트: executor stub",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "Gateway\ngateway 테스트: executor stub",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-composition-root",
"type": "rectangle",
"x": 780.0,
"y": 60.0,
"width": 167.0,
"height": 88.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1480993774,
"version": 1,
"versionNonce": 95918647,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-composition-root",
"type": "text",
"x": 790.0,
"y": 70.0,
"width": 147.0,
"height": 68.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1973216795,
"version": 1,
"versionNonce": 169159091,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "Composition Root\ncredential decision\n테스트 공백",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "Composition Root\ncredential decision\n테스트 공백",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-runtime-adapter",
"type": "rectangle",
"x": 1107.0,
"y": 60.0,
"width": 181.0,
"height": 88.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1916862381,
"version": 1,
"versionNonce": 1065856754,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-runtime-adapter",
"type": "text",
"x": 1117.0,
"y": 70.0,
"width": 161.0,
"height": 68.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 38827956,
"version": 1,
"versionNonce": 780247791,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "Runtime Adapter + 404\n실제 어댑터\n배포된 백엔드 404 본문",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "Runtime Adapter + 404\n실제 어댑터\n배포된 백엔드 404 본문",
"autoResize": true,
"lineHeight": 1.25
}
],
"appState": {
"gridSize": 10,
"viewBackgroundColor": "#ffffff",
"currentItemFontFamily": 5
},
"files": {}
}
@@ -0,0 +1,32 @@
{
"harness_version": "0.2.0",
"spec_id": "composition-root-seam",
"spec_version": "1.1",
"spec_sha256": "c0a7d549c0473287614576b8474c3a21ac594de835dad9ed9dc9383e01b7b5e0",
"source_context": {
"document": "docs/TechLog/final/document.md",
"document_sha256": "c3a7de37b778fff7b6ea555a3ad7338c91c6fb15d685e7734f89472b4924d955",
"anchor": {
"kind": "heading",
"value": "7. 테스트가 지나지 않는 이음매",
"line": 690
}
},
"outputs": [
"composition-root-seam.svg",
"composition-root-seam.drawio",
"composition-root-seam.mmd",
"composition-root-seam.d2",
"composition-root-seam.dot",
"composition-root-seam.excalidraw",
"composition-root-seam.alt.md"
],
"lint_issue_count": 1,
"assumption_count": 0,
"assumptions_allowed": false,
"composition_profile": "component-flow",
"reference_ids": [
"payment-event-flow"
],
"diagram_only": true
}
@@ -0,0 +1,10 @@
%% 테스트가 끊긴 곳과 실제 런타임이 지나는 합성 루트
%% question: 게이트웨이 테스트와 화면 테스트가 통과했는데 왜 합성 루트의 credential 결함은 운영에서만 드러났는가?
flowchart LR
n0["화면"]
n1["Gateway"]
n2["Composition Root"]
n3["Runtime Adapter + 404"]
n0 -->|"요청"| n1
n1 -->|"runtime 조립"| n2
n2 -->|"credential 판정"| n3
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1333" height="300" viewBox="0 0 1333 300" role="img" aria-labelledby="diagram-title diagram-description">
<title id="diagram-title">테스트가 끊긴 곳과 실제 런타임이 지나는 합성 루트</title>
<desc id="diagram-description">왼쪽에서 오른쪽으로 실제 런타임 경로를 읽는다. 화면에서 게이트웨이로 가고 합성 루트에서 credential을 결정한 뒤 실제 런타임 어댑터가 배포된 백엔드의 실제 404 본문을 읽는다. 화면 테스트는 게이트웨이를 스텁하고 게이트웨이 테스트는 실행기를 스텁했기 때문에 가운데 합성 루트의 credential 결정은 두 테스트가 지나지 않았다.</desc>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;composition-root-seam&quot;,&quot;profile&quot;:&quot;component-flow&quot;},&quot;source_context&quot;:{&quot;document&quot;:&quot;docs/TechLog/final/document.md&quot;,&quot;document_sha256&quot;:&quot;c3a7de37b778fff7b6ea555a3ad7338c91c6fb15d685e7734f89472b4924d955&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;heading&quot;,&quot;value&quot;:&quot;7. 테스트가 지나지 않는 이음매&quot;,&quot;line&quot;:690}},&quot;evidence_policy&quot;:&quot;Each factual element cites source lines or is marked assumption.&quot;,&quot;diagram_only&quot;:true}</metadata>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
<style>
:root { color-scheme: light; }
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
.canvas { fill: #ffffff; }
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
.group-label-bg { fill: #ffffff; }
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
.edge.style-dotted { stroke-dasharray: 2 5; }
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.edge.emphasis-muted { stroke: #9ca3af; }
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
.edge-label-bg { fill: #ffffff; }
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
.node-shape.assumption { stroke-dasharray: 4 4; }
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
.controller-led { fill: #4b5563; }
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
.node-detail { font-size: 11px; fill: #374151; }
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
</style>
</defs>
<rect class="canvas" width="1333" height="300" />
<polyline class="edge kind-request style-solid emphasis-normal" points="620.0,104.0 700.0,104.0 700.0,104.0 780.0,104.0" data-evidence="766-771" />
<rect class="edge-label-bg" x="657.5" y="62.0" width="85.0" height="22" rx="3" />
<text class="edge-label" x="700.0" y="77.0">runtime 조립</text>
<polyline class="edge kind-request style-solid emphasis-normal" points="947.0,104.0 1027.0,104.0 1027.0,104.0 1107.0,104.0" data-evidence="755-767,769-771" />
<rect class="edge-label-bg" x="974.5" y="62.0" width="105.1" height="22" rx="3" />
<text class="edge-label" x="1027.0" y="77.0">credential 판정</text>
<polyline class="edge kind-request style-solid emphasis-normal" points="244.0,104.0 324.0,104.0 324.0,104.0 404.0,104.0" data-evidence="740-742,766-771" />
<rect class="edge-label-bg" x="302.0" y="62.0" width="44.0" height="22" rx="3" />
<text class="edge-label" x="324.0" y="77.0">요청</text>
<g id="node-screen">
<rect class="node-shape kind-component emphasis-normal role-source" data-evidence="740-742,770-771" x="70.0" y="68.5" width="174.0" height="71.0" rx="7" />
<text class="node-label" x="157.0" y="95.5">화면</text>
<line class="node-detail-divider" x1="84.0" y1="116.5" x2="230.0" y2="116.5" />
<text class="node-detail" x="86.0" y="133.5">화면 테스트: gateway stub</text>
</g>
<g id="node-gateway">
<rect class="node-shape kind-service emphasis-normal role-service" data-evidence="766-771" x="404.0" y="68.5" width="216.0" height="71.0" rx="7" />
<text class="node-label" x="512.0" y="95.5">Gateway</text>
<line class="node-detail-divider" x1="418.0" y1="116.5" x2="606.0" y2="116.5" />
<text class="node-detail" x="420.0" y="133.5">gateway 테스트: executor stub</text>
</g>
<g id="node-composition-root">
<rect class="node-shape kind-component emphasis-warning role-service" data-evidence="748-767" x="780.0" y="60.0" width="167.0" height="88.0" rx="7" />
<text class="node-label" x="863.5" y="87.0">Composition Root</text>
<line class="node-detail-divider" x1="794.0" y1="108.0" x2="933.0" y2="108.0" />
<text class="node-detail" x="796.0" y="125.0">credential decision</text>
<text class="node-detail" x="796.0" y="141.0">테스트 공백</text>
</g>
<g id="node-runtime-adapter">
<rect class="node-shape kind-service emphasis-normal role-sink" data-evidence="769-771" x="1107.0" y="60.0" width="181.0" height="88.0" rx="7" />
<text class="node-label" x="1197.5" y="87.0">Runtime Adapter + 404</text>
<line class="node-detail-divider" x1="1121.0" y1="108.0" x2="1274.0" y2="108.0" />
<text class="node-detail" x="1123.0" y="125.0">실제 어댑터</text>
<text class="node-detail" x="1123.0" y="141.0">배포된 백엔드 404 본문</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

@@ -10,18 +10,18 @@
## Elements and evidence
- **계약 ProjectDecisionItem** (participant): 공개 주소를 앵커로 규정한 OpenAPI 계약. Evidence: L676L678.
- **PublicPaths.forKind** (participant): 게시할 때 공개 주소를 만드는 코드. Evidence: L677L678.
- **public_resource_projection** (participant): 만들어진 주소가 저장되는 투영 테이블. Evidence: L682L683.
- **PublicSql.pathOf** (participant): 조회할 때 공개 주소를 만드는 코드. Evidence: L677L678.
- **방문자** (actor): 「다음에 읽을 것」 링크를 따라간 사람. Evidence: L670L671.
- **공개 라우트** (participant): 결정에는 상세 화면이 없어 라우트가 하나뿐이다. Evidence: L675L676.
- **계약 ProjectDecisionItem** (participant): 공개 주소를 앵커로 규정한 OpenAPI 계약. Evidence: L919L921.
- **PublicPaths.forKind** (participant): 게시할 때 공개 주소를 만드는 코드. Evidence: L919L921.
- **public_resource_projection** (participant): 만들어진 주소가 저장되는 투영 테이블. Evidence: L925L926.
- **PublicSql.pathOf** (participant): 조회할 때 공개 주소를 만드는 코드. Evidence: L919L921.
- **방문자** (actor): 「다음에 읽을 것」 링크를 따라간 사람. Evidence: L913L914.
- **공개 라우트** (participant): 결정에는 상세 화면이 없어 라우트가 하나뿐이다. Evidence: L918L919.
## Relationships
- **계약 ProjectDecisionItem → PublicPaths.forKind:** …/decisions#{slug} 로 규정. Evidence: L676L678.
- **PublicPaths.forKind → public_resource_projection:** …/decisions/{slug} 저장. Evidence: L675L678.
- **public_resource_projection → PublicSql.pathOf:** 저장된 주소 조회. Evidence: L682L683.
- **PublicSql.pathOf → 방문자:** 같은 형태로 링크 전달. Evidence: L677L678.
- **방문자 → 공개 라우트:** …/decisions/{slug} 요청. Evidence: L670L675.
- **공개 라우트 → 방문자:** 404. Evidence: L670L675.
- **계약 ProjectDecisionItem → PublicPaths.forKind:** …/decisions#{slug} 로 규정. Evidence: L919L921.
- **PublicPaths.forKind → public_resource_projection:** …/decisions/{slug} 저장. Evidence: L919L921, L925L926.
- **public_resource_projection → PublicSql.pathOf:** 저장된 주소 조회. Evidence: L919L926.
- **PublicSql.pathOf → 방문자:** 같은 형태로 링크 전달. Evidence: L913L921.
- **방문자 → 공개 라우트:** …/decisions/{slug} 요청. Evidence: L913L919.
- **공개 라우트 → 방문자:** 404. Evidence: L913L919.
@@ -5,22 +5,22 @@
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="n_contract" value="계약 ProjectDecisionItem" tooltip="공개 주소를 앵커로 규정한 OpenAPI 계약. | Evidence: L676-L678" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxCell id="n_contract" value="계약 ProjectDecisionItem" tooltip="공개 주소를 앵커로 규정한 OpenAPI 계약. | Evidence: L919-L921" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="45.0" y="35.0" width="188.0" height="64.0" as="geometry"/>
</mxCell>
<mxCell id="n_publish-path" value="PublicPaths.forKind&lt;br/&gt;게시 시점" tooltip="게시할 때 공개 주소를 만드는 코드. | Evidence: L677-L678" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
<mxCell id="n_publish-path" value="PublicPaths.forKind&lt;br/&gt;게시 시점" tooltip="게시할 때 공개 주소를 만드는 코드. | Evidence: L919-L921" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
<mxGeometry x="255.0" y="35.0" width="167.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_projection" value="public_resource_projection" tooltip="만들어진 주소가 저장되는 투영 테이블. | Evidence: L682-L683" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxCell id="n_projection" value="public_resource_projection" tooltip="만들어진 주소가 저장되는 투영 테이블. | Evidence: L925-L926" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="465.0" y="35.0" width="190.0" height="64.0" as="geometry"/>
</mxCell>
<mxCell id="n_read-path" value="PublicSql.pathOf&lt;br/&gt;조회 시점" tooltip="조회할 때 공개 주소를 만드는 코드. | Evidence: L677-L678" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
<mxCell id="n_read-path" value="PublicSql.pathOf&lt;br/&gt;조회 시점" tooltip="조회할 때 공개 주소를 만드는 코드. | Evidence: L919-L921" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
<mxGeometry x="675.0" y="35.0" width="150.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_visitor" value="방문자" tooltip="「다음에 읽을 것」 링크를 따라간 사람. | Evidence: L670-L671" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;dashed=1;fillColor=#f5f7fa;" vertex="1" parent="1">
<mxCell id="n_visitor" value="방문자" tooltip="「다음에 읽을 것」 링크를 따라간 사람. | Evidence: L913-L914" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;dashed=1;fillColor=#f5f7fa;" vertex="1" parent="1">
<mxGeometry x="885.0" y="35.0" width="150.0" height="78.0" as="geometry"/>
</mxCell>
<mxCell id="n_public-route" value="공개 라우트&lt;br/&gt;/projects/{slug}/decisions 하나뿐" tooltip="결정에는 상세 화면이 없어 라우트가 하나뿐이다. | Evidence: L675-L676" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxCell id="n_public-route" value="공개 라우트&lt;br/&gt;/projects/{slug}/decisions 하나뿐" tooltip="결정에는 상세 화면이 없어 라우트가 하나뿐이다. | Evidence: L918-L919" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="1095.0" y="35.0" width="190.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="e_m1" value="…/decisions#{slug} 로 규정" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_contract" target="n_publish-path">
@@ -2,22 +2,22 @@
"harness_version": "0.2.0",
"spec_id": "decision-path-404",
"spec_version": "1.1",
"spec_sha256": "277ea0b16980c58b052895bdd09c2dcc15aa24b33173785c3b9b37d6888e0daf",
"spec_sha256": "ea5cad43e1a5d76650bbe4bd30ce3bc070c30aa2bf778fbf7b2a81cee8e6c4e5",
"source_context": {
"document": "document.md",
"document_sha256": "93b9fec4884efa0e6231de07dc27e2b0ac36c9052d3720e28d102d9747ac4f8f",
"document": "docs/TechLog/final/document.md",
"document_sha256": "c3a7de37b778fff7b6ea555a3ad7338c91c6fb15d685e7734f89472b4924d955",
"anchor": {
"kind": "marker",
"value": "decision-path-404",
"line": 673
"line": 916
}
},
"outputs": [
"decision-path-404.svg",
"decision-path-404.drawio",
"decision-path-404.mmd",
"decision-path-404.d2",
"decision-path-404.dot",
"decision-path-404.drawio",
"decision-path-404.excalidraw",
"decision-path-404.alt.md"
],
@@ -2,7 +2,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1330" height="542" viewBox="0 0 1330 542" role="img" aria-labelledby="diagram-title diagram-description">
<title id="diagram-title">결정 주소가 게시 시점에 굳어져 방문자가 404 를 만나기까지</title>
<desc id="diagram-description">위에서 아래로 여섯 번의 이동이 있다. 계약 ProjectDecisionItem 은 공개 주소가 decisions#{slug} 앵커라고 규정한다. 게시 시점의 PublicPaths.forKind 는 그 대신 decisions/{slug} 를 만들어 public_resource_projection 에 저장한다. 조회 시점의 PublicSql.pathOf 가 저장된 주소를 읽고 방문자에게 링크로 내보낸다. 방문자가 그 주소를 요청하면 공개 라우트에는 projects/{slug}/decisions 하나뿐이라 맞는 라우트가 없고 404 가 돌아온다.</desc>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;decision-path-404&quot;,&quot;profile&quot;:&quot;sequence&quot;},&quot;source_context&quot;:{&quot;document&quot;:&quot;document.md&quot;,&quot;document_sha256&quot;:&quot;93b9fec4884efa0e6231de07dc27e2b0ac36c9052d3720e28d102d9747ac4f8f&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;marker&quot;,&quot;value&quot;:&quot;decision-path-404&quot;,&quot;line&quot;:673}},&quot;evidence_policy&quot;:&quot;Each factual element cites source lines or is marked assumption.&quot;,&quot;diagram_only&quot;:true}</metadata>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;decision-path-404&quot;,&quot;profile&quot;:&quot;sequence&quot;},&quot;source_context&quot;:{&quot;document&quot;:&quot;docs/TechLog/final/document.md&quot;,&quot;document_sha256&quot;:&quot;c3a7de37b778fff7b6ea555a3ad7338c91c6fb15d685e7734f89472b4924d955&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;marker&quot;,&quot;value&quot;:&quot;decision-path-404&quot;,&quot;line&quot;:916}},&quot;evidence_policy&quot;:&quot;Each factual element cites source lines or is marked assumption.&quot;,&quot;diagram_only&quot;:true}</metadata>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" />
@@ -50,48 +50,48 @@
</style>
</defs>
<rect class="canvas" width="1330" height="542" />
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="676-678" x="45.0" y="35.0" width="188.0" height="64.0" rx="7" />
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="919-921" x="45.0" y="35.0" width="188.0" height="64.0" rx="7" />
<text class="node-label" x="139.0" y="65.0">계약 ProjectDecisionItem</text>
<line class="lifeline" x1="139.0" y1="99.0" x2="139.0" y2="512.0" />
<rect class="node-shape kind-participant emphasis-warning role-participant" data-evidence="677-678" x="255.0" y="35.0" width="167.0" height="71.0" rx="7" />
<rect class="node-shape kind-participant emphasis-warning role-participant" data-evidence="919-921" x="255.0" y="35.0" width="167.0" height="71.0" rx="7" />
<text class="node-label" x="338.5" y="62.0">PublicPaths.forKind</text>
<line class="node-detail-divider" x1="269.0" y1="83.0" x2="408.0" y2="83.0" />
<text class="node-detail" x="271.0" y="100.0">게시 시점</text>
<line class="lifeline" x1="338.5" y1="106.0" x2="338.5" y2="512.0" />
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="682-683" x="465.0" y="35.0" width="190.0" height="64.0" rx="7" />
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="925-926" x="465.0" y="35.0" width="190.0" height="64.0" rx="7" />
<text class="node-label" x="560.0" y="65.0">public_resource_projection</text>
<line class="lifeline" x1="560.0" y1="99.0" x2="560.0" y2="512.0" />
<rect class="node-shape kind-participant emphasis-warning role-participant" data-evidence="677-678" x="675.0" y="35.0" width="150.0" height="71.0" rx="7" />
<rect class="node-shape kind-participant emphasis-warning role-participant" data-evidence="919-921" x="675.0" y="35.0" width="150.0" height="71.0" rx="7" />
<text class="node-label" x="750.0" y="62.0">PublicSql.pathOf</text>
<line class="node-detail-divider" x1="689.0" y1="83.0" x2="811.0" y2="83.0" />
<text class="node-detail" x="691.0" y="100.0">조회 시점</text>
<line class="lifeline" x1="750.0" y1="106.0" x2="750.0" y2="512.0" />
<g class="actor-symbol emphasis-normal" data-evidence="670-671"><circle cx="960.0" cy="55.0" r="11.0" /><line x1="960.0" y1="71.0" x2="960.0" y2="74.0" /><line x1="942.0" y1="81.0" x2="978.0" y2="81.0" /><line x1="960.0" y1="74.0" x2="945.0" y2="91.0" /><line x1="960.0" y1="74.0" x2="975.0" y2="91.0" /></g>
<g class="actor-symbol emphasis-normal" data-evidence="913-914"><circle cx="960.0" cy="55.0" r="11.0" /><line x1="960.0" y1="71.0" x2="960.0" y2="84.0" /><line x1="942.0" y1="81.0" x2="978.0" y2="81.0" /><line x1="960.0" y1="84.0" x2="945.0" y2="101.0" /><line x1="960.0" y1="84.0" x2="975.0" y2="101.0" /></g>
<text class="node-label" x="960.0" y="106.0">방문자</text>
<line class="lifeline" x1="960.0" y1="113.0" x2="960.0" y2="512.0" />
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="675-676" x="1095.0" y="35.0" width="190.0" height="71.0" rx="7" />
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="918-919" x="1095.0" y="35.0" width="190.0" height="71.0" rx="7" />
<text class="node-label" x="1190.0" y="62.0">공개 라우트</text>
<line class="node-detail-divider" x1="1109.0" y1="83.0" x2="1271.0" y2="83.0" />
<text class="node-detail" x="1111.0" y="100.0">/projects/{slug}/decisions 하나뿐</text>
<line class="lifeline" x1="1190.0" y1="106.0" x2="1190.0" y2="512.0" />
<polyline class="edge kind-request style-solid emphasis-normal" points="139.0,140.0 338.5,140.0" data-evidence="676-678" />
<polyline class="edge kind-request style-solid emphasis-normal" points="139.0,140.0 338.5,140.0" data-evidence="919-921" />
<rect class="edge-label-bg" x="142.6" y="114.0" width="192.2" height="22" rx="3" />
<text class="edge-label" x="238.8" y="129.0">1. …/decisions#{slug} 로 규정</text>
<polyline class="edge kind-request style-solid emphasis-warning" points="338.5,202.0 560.0,202.0" data-evidence="675-678" />
<polyline class="edge kind-request style-solid emphasis-warning" points="338.5,202.0 560.0,202.0" data-evidence="919-921,925-926" />
<rect class="edge-label-bg" x="359.9" y="176.0" width="178.8" height="22" rx="3" />
<text class="edge-label" x="449.2" y="191.0">2. …/decisions/{slug} 저장</text>
<line class="failure-mark" x1="547.0" y1="189.0" x2="573.0" y2="215.0" />
<line class="failure-mark" x1="573.0" y1="189.0" x2="547.0" y2="215.0" />
<polyline class="edge kind-response style-solid emphasis-normal semantic-dashed" points="560.0,264.0 750.0,264.0" data-evidence="682-683" />
<polyline class="edge kind-response style-solid emphasis-normal semantic-dashed" points="560.0,264.0 750.0,264.0" data-evidence="919-926" />
<rect class="edge-label-bg" x="605.8" y="238.0" width="98.4" height="22" rx="3" />
<text class="edge-label" x="655.0" y="253.0">3. 저장된 주소 조회</text>
<polyline class="edge kind-response style-solid emphasis-normal semantic-dashed" points="750.0,326.0 960.0,326.0" data-evidence="677-678" />
<polyline class="edge kind-response style-solid emphasis-normal semantic-dashed" points="750.0,326.0 960.0,326.0" data-evidence="913-921" />
<rect class="edge-label-bg" x="795.8" y="300.0" width="118.5" height="22" rx="3" />
<text class="edge-label" x="855.0" y="315.0">4. 같은 형태로 링크 전달</text>
<polyline class="edge kind-request style-solid emphasis-normal" points="960.0,388.0 1190.0,388.0" data-evidence="670-675" />
<polyline class="edge kind-request style-solid emphasis-normal" points="960.0,388.0 1190.0,388.0" data-evidence="913-919" />
<rect class="edge-label-bg" x="985.6" y="362.0" width="178.8" height="22" rx="3" />
<text class="edge-label" x="1075.0" y="377.0">5. …/decisions/{slug} 요청</text>
<polyline class="edge kind-response style-solid emphasis-warning semantic-dashed" points="1190.0,450.0 960.0,450.0" data-evidence="670-675" />
<polyline class="edge kind-response style-solid emphasis-warning semantic-dashed" points="1190.0,450.0 960.0,450.0" data-evidence="913-919" />
<rect class="edge-label-bg" x="1045.9" y="424.0" width="58.2" height="22" rx="3" />
<text class="edge-label" x="1075.0" y="439.0">6. 404</text>
<line class="failure-mark" x1="947.0" y1="437.0" x2="973.0" y2="463.0" />

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

@@ -0,0 +1,22 @@
# 새 CONCEPT 종류가 세 영역의 손 목록으로 퍼진 구조
## Alternative text
새 CONCEPT 노드에서 프론트엔드 손 목록, 백엔드 손 목록, 계약 enum 세 갈래로 퍼지는 팬아웃 그림. 각 갈래에는 실제 누락 건수 9, 1, 3이 적혀 있다.
## Long description
왼쪽의 새 CONCEPT가 세 갈래로 퍼진다. 프론트엔드에는 게이트웨이 분기, 매퍼, 필터 같은 손 목록이 아홉 곳 있었고, 백엔드에는 PublicSql.pathOf 한 곳이 빠졌다. 계약에는 CatalogEntry.kind, ResolvedRelation.targetKind, RelatedEntry.type 세 enum 누락이 있었다. 아래 본문 표가 열세 위치를 정확히 나열하고, 그림은 왜 한 종류 변경이 세 영역으로 퍼졌는지만 보여 준다.
## Elements and evidence
- **CONCEPT** (message): No additional description. Evidence: L316L331.
- **Frontend 손 목록** (component): No additional description. Evidence: L335L345.
- **Backend 손 목록** (component): No additional description. Evidence: L345L349.
- **Contract enums** (component): No additional description. Evidence: L346L352.
## Relationships
- **CONCEPT → Backend 손 목록:** kind 추가. Evidence: L345L349.
- **CONCEPT → Contract enums:** enum 추가. Evidence: L346L352.
- **CONCEPT → Frontend 손 목록:** kind 추가. Evidence: L330L345.
@@ -0,0 +1,18 @@
# 새 CONCEPT 종류가 세 영역의 손 목록으로 퍼진 구조
# Question: CONCEPT 하나를 추가했는데 왜 계약·백엔드·프론트엔드 여러 위치를 동시에 고쳐야 했는가?
direction: right
n0: "CONCEPT" {
shape: rectangle
}
n1: "Frontend 손 목록" {
shape: rectangle
}
n2: "Backend 손 목록" {
shape: rectangle
}
n3: "Contract enums" {
shape: rectangle
}
n0 -> n1: "kind 추가"
n0 -> n2: "kind 추가"
n0 -> n3: "enum 추가"
@@ -0,0 +1,12 @@
digraph techviz {
graph [rankdir=LR, splines=ortho, nodesep=0.55, ranksep=0.85];
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
n0 [label="CONCEPT", shape=box, style="rounded,filled"];
n1 [label="Frontend 손 목록", shape=box, style="rounded,filled"];
n2 [label="Backend 손 목록", shape=box, style="rounded,filled"];
n3 [label="Contract enums", shape=box, style="rounded,filled"];
n0 -> n1 [label="kind 추가", style=solid];
n0 -> n2 [label="kind 추가", style=solid];
n0 -> n3 [label="enum 추가", style=solid];
}
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
<diagram id="record-kind-fanout" name="새 CONCEPT 종류가 세 영역의 손 목록으로 퍼진 구조">
<mxGraphModel dx="680" dy="547" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="n_concept" value="CONCEPT&lt;br/&gt;새 Record Kind" tooltip="message | Evidence: L316-L331" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="70.0" y="245.5" width="150.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_backend" value="Backend 손 목록&lt;br/&gt;1곳&lt;br/&gt;PublicSql.pathOf" tooltip="component | Evidence: L345-L349" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="416.5" y="60.0" width="150.0" height="88.0" as="geometry"/>
</mxCell>
<mxCell id="n_contract" value="Contract enums&lt;br/&gt;3곳&lt;br/&gt;CatalogEntry.kind&lt;br/&gt;ResolvedRelation.targetKind&lt;br/&gt;RelatedEntry.type" tooltip="component | Evidence: L346-L352" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="380.0" y="220.0" width="223.0" height="122.0" as="geometry"/>
</mxCell>
<mxCell id="n_frontend" value="Frontend 손 목록&lt;br/&gt;9곳&lt;br/&gt;gateway · mapper · filter" tooltip="component | Evidence: L335-L345" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="387.0" y="414.0" width="209.0" height="88.0" as="geometry"/>
</mxCell>
<mxCell id="e_to-backend" value="kind 추가" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_concept" target="n_backend">
<mxGeometry relative="1" as="geometry">
<mxPoint x="342.2" y="183.5" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_to-contract" value="enum 추가" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_concept" target="n_contract">
<mxGeometry relative="1" as="geometry">
<mxPoint x="300.0" y="253.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_to-frontend" value="kind 추가" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_concept" target="n_frontend">
<mxGeometry relative="1" as="geometry">
<mxPoint x="327.5" y="378.5" as="offset"/>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
@@ -0,0 +1,586 @@
{
"type": "excalidraw",
"version": 2,
"source": "techviz-harness",
"elements": [
{
"id": "edge-to-backend",
"type": "arrow",
"x": 220.0,
"y": 104.0,
"width": 196.5,
"height": 159.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 1147872718,
"version": 1,
"versionNonce": 125969590,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
159.0
],
[
98.25,
159.0
],
[
98.25,
0.0
],
[
196.5,
0.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-concept",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-backend",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-to-backend",
"type": "text",
"x": 297.25,
"y": 171.5,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 627649163,
"version": 1,
"versionNonce": 1917519641,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "kind 추가",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "kind 추가",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-to-contract",
"type": "arrow",
"x": 220.0,
"y": 281.0,
"width": 160.0,
"height": 0.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 1165353134,
"version": 1,
"versionNonce": 1321109342,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
0.0
],
[
80.0,
0.0
],
[
80.0,
0.0
],
[
160.0,
0.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-concept",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-contract",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-to-contract",
"type": "text",
"x": 255.0,
"y": 241.0,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1321731880,
"version": 1,
"versionNonce": 1614676317,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "enum 추가",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "enum 추가",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-to-frontend",
"type": "arrow",
"x": 220.0,
"y": 299.0,
"width": 167.0,
"height": 159.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 1693329255,
"version": 1,
"versionNonce": 696160331,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
0.0
],
[
83.5,
0.0
],
[
83.5,
159.0
],
[
167.0,
159.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-concept",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-frontend",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-to-frontend",
"type": "text",
"x": 282.5,
"y": 366.5,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1043396341,
"version": 1,
"versionNonce": 269712647,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "kind 추가",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "kind 추가",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-concept",
"type": "rectangle",
"x": 70.0,
"y": 245.5,
"width": 150.0,
"height": 71.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1783847077,
"version": 1,
"versionNonce": 1835405496,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-concept",
"type": "text",
"x": 80.0,
"y": 255.5,
"width": 130.0,
"height": 51.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 956305639,
"version": 1,
"versionNonce": 1674164108,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "CONCEPT\n새 Record Kind",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "CONCEPT\n새 Record Kind",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-backend",
"type": "rectangle",
"x": 416.5,
"y": 60.0,
"width": 150.0,
"height": 88.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 430450037,
"version": 1,
"versionNonce": 288855291,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-backend",
"type": "text",
"x": 426.5,
"y": 70.0,
"width": 130.0,
"height": 68.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 349920174,
"version": 1,
"versionNonce": 257387561,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "Backend 손 목록\n1곳\nPublicSql.pathOf",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "Backend 손 목록\n1곳\nPublicSql.pathOf",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-contract",
"type": "rectangle",
"x": 380.0,
"y": 220.0,
"width": 223.0,
"height": 122.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 179232308,
"version": 1,
"versionNonce": 606575263,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-contract",
"type": "text",
"x": 390.0,
"y": 230.0,
"width": 203.0,
"height": 102.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 165738200,
"version": 1,
"versionNonce": 285005378,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "Contract enums\n3곳\nCatalogEntry.kind\nResolvedRelation.targetKind\nRelatedEntry.type",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "Contract enums\n3곳\nCatalogEntry.kind\nResolvedRelation.targetKind\nRelatedEntry.type",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-frontend",
"type": "rectangle",
"x": 387.0,
"y": 414.0,
"width": 209.0,
"height": 88.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 286799291,
"version": 1,
"versionNonce": 1685391574,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-frontend",
"type": "text",
"x": 397.0,
"y": 424.0,
"width": 189.0,
"height": 68.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1548189542,
"version": 1,
"versionNonce": 237285990,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "Frontend 손 목록\n9곳\ngateway · mapper · filter",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "Frontend 손 목록\n9곳\ngateway · mapper · filter",
"autoResize": true,
"lineHeight": 1.25
}
],
"appState": {
"gridSize": 10,
"viewBackgroundColor": "#ffffff",
"currentItemFontFamily": 5
},
"files": {}
}
@@ -0,0 +1,32 @@
{
"harness_version": "0.2.0",
"spec_id": "record-kind-fanout",
"spec_version": "1.1",
"spec_sha256": "4edc64969f367edfd400651cd9e0c1787d63847dab3cdc3835ac6eee75fb377e",
"source_context": {
"document": "docs/TechLog/final/document.md",
"document_sha256": "c3a7de37b778fff7b6ea555a3ad7338c91c6fb15d685e7734f89472b4924d955",
"anchor": {
"kind": "heading",
"value": "3. 손으로 나열한 목록이 새 종류를 삼킨다",
"line": 311
}
},
"outputs": [
"record-kind-fanout.svg",
"record-kind-fanout.drawio",
"record-kind-fanout.mmd",
"record-kind-fanout.d2",
"record-kind-fanout.dot",
"record-kind-fanout.excalidraw",
"record-kind-fanout.alt.md"
],
"lint_issue_count": 0,
"assumption_count": 0,
"assumptions_allowed": false,
"composition_profile": "component-flow",
"reference_ids": [
"payment-event-flow"
],
"diagram_only": true
}
@@ -0,0 +1,10 @@
%% 새 CONCEPT 종류가 세 영역의 손 목록으로 퍼진 구조
%% question: CONCEPT 하나를 추가했는데 왜 계약·백엔드·프론트엔드 여러 위치를 동시에 고쳐야 했는가?
flowchart LR
n0["CONCEPT"]
n1["Frontend 손 목록"]
n2["Backend 손 목록"]
n3["Contract enums"]
n0 -->|"kind 추가"| n1
n0 -->|"kind 추가"| n2
n0 -->|"enum 추가"| n3
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="547" viewBox="0 0 680 547" role="img" aria-labelledby="diagram-title diagram-description">
<title id="diagram-title">새 CONCEPT 종류가 세 영역의 손 목록으로 퍼진 구조</title>
<desc id="diagram-description">왼쪽의 새 CONCEPT가 세 갈래로 퍼진다. 프론트엔드에는 게이트웨이 분기, 매퍼, 필터 같은 손 목록이 아홉 곳 있었고, 백엔드에는 PublicSql.pathOf 한 곳이 빠졌다. 계약에는 CatalogEntry.kind, ResolvedRelation.targetKind, RelatedEntry.type 세 enum 누락이 있었다. 아래 본문 표가 열세 위치를 정확히 나열하고, 그림은 왜 한 종류 변경이 세 영역으로 퍼졌는지만 보여 준다.</desc>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;record-kind-fanout&quot;,&quot;profile&quot;:&quot;component-flow&quot;},&quot;source_context&quot;:{&quot;document&quot;:&quot;docs/TechLog/final/document.md&quot;,&quot;document_sha256&quot;:&quot;c3a7de37b778fff7b6ea555a3ad7338c91c6fb15d685e7734f89472b4924d955&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;heading&quot;,&quot;value&quot;:&quot;3. 손으로 나열한 목록이 새 종류를 삼킨다&quot;,&quot;line&quot;:311}},&quot;evidence_policy&quot;:&quot;Each factual element cites source lines or is marked assumption.&quot;,&quot;diagram_only&quot;:true}</metadata>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
<style>
:root { color-scheme: light; }
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
.canvas { fill: #ffffff; }
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
.group-label-bg { fill: #ffffff; }
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
.edge.style-dotted { stroke-dasharray: 2 5; }
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.edge.emphasis-muted { stroke: #9ca3af; }
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
.edge-label-bg { fill: #ffffff; }
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
.node-shape.assumption { stroke-dasharray: 4 4; }
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
.controller-led { fill: #4b5563; }
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
.node-detail { font-size: 11px; fill: #374151; }
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
</style>
</defs>
<rect class="canvas" width="680" height="547" />
<polyline class="edge kind-data style-solid emphasis-normal" points="220.0,263.0 318.2,263.0 318.2,104.0 416.5,104.0" data-evidence="345-349" />
<rect class="edge-label-bg" x="309.8" y="169.5" width="64.9" height="22" rx="3" />
<text class="edge-label" x="342.2" y="184.5">kind 추가</text>
<polyline class="edge kind-data style-solid emphasis-normal" points="220.0,281.0 300.0,281.0 300.0,281.0 380.0,281.0" data-evidence="346-352" />
<rect class="edge-label-bg" x="267.6" y="239.0" width="64.9" height="22" rx="3" />
<text class="edge-label" x="300.0" y="254.0">enum 추가</text>
<polyline class="edge kind-data style-solid emphasis-normal" points="220.0,299.0 303.5,299.0 303.5,458.0 387.0,458.0" data-evidence="330-345" />
<rect class="edge-label-bg" x="295.1" y="364.5" width="64.9" height="22" rx="3" />
<text class="edge-label" x="327.5" y="379.5">kind 추가</text>
<g id="node-concept">
<rect class="node-shape kind-message emphasis-primary role-source" data-evidence="316-331" x="70.0" y="245.5" width="150.0" height="71.0" rx="7" />
<text class="node-label" x="145.0" y="272.5">CONCEPT</text>
<line class="node-detail-divider" x1="84.0" y1="293.5" x2="206.0" y2="293.5" />
<text class="node-detail" x="86.0" y="310.5">새 Record Kind</text>
</g>
<g id="node-backend">
<rect class="node-shape kind-component emphasis-normal role-service" data-evidence="345-349" x="416.5" y="60.0" width="150.0" height="88.0" rx="7" />
<text class="node-label" x="491.5" y="87.0">Backend 손 목록</text>
<line class="node-detail-divider" x1="430.5" y1="108.0" x2="552.5" y2="108.0" />
<text class="node-detail" x="432.5" y="125.0">1곳</text>
<text class="node-detail" x="432.5" y="141.0">PublicSql.pathOf</text>
</g>
<g id="node-contract">
<rect class="node-shape kind-component emphasis-normal role-sink" data-evidence="346-352" x="380.0" y="220.0" width="223.0" height="122.0" rx="7" />
<text class="node-label" x="491.5" y="247.0">Contract enums</text>
<line class="node-detail-divider" x1="394.0" y1="268.0" x2="589.0" y2="268.0" />
<text class="node-detail" x="396.0" y="285.0">3곳</text>
<text class="node-detail" x="396.0" y="301.0">CatalogEntry.kind</text>
<text class="node-detail" x="396.0" y="317.0">ResolvedRelation.targetKind</text>
<text class="node-detail" x="396.0" y="333.0">RelatedEntry.type</text>
</g>
<g id="node-frontend">
<rect class="node-shape kind-component emphasis-normal role-service" data-evidence="335-345" x="387.0" y="414.0" width="209.0" height="88.0" rx="7" />
<text class="node-label" x="491.5" y="441.0">Frontend 손 목록</text>
<line class="node-detail-divider" x1="401.0" y1="462.0" x2="582.0" y2="462.0" />
<text class="node-detail" x="403.0" y="479.0">9곳</text>
<text class="node-detail" x="403.0" y="495.0">gateway · mapper · filter</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.2 KiB

@@ -0,0 +1,24 @@
# 라우트 하나가 건드리는 손 목록과 검출 시점
## Alternative text
새 Route가 Route Contract를 거쳐 Runtime 계약, 배포 전 검사, Edge 서빙 세 묶음으로 갈라지는 팬아웃 그림. 각 묶음에는 누락이 처음 드러나는 시점이 적혀 있다.
## Long description
왼쪽의 새 Route가 Route Contract로 들어간 뒤 세 갈래로 퍼진다. Runtime 계약에는 runtime 등록과 메시지 카탈로그가 있다. 배포 전 검사에는 vite chunk 이름 표와 접근성 증거·아티팩트 기준선·gate digest가 묶여 있고 누락은 빌드 매니페스트 또는 배포 직전에 드러난다. Edge 서빙 규칙 누락은 하드 로드나 새로고침 때 배포 뒤 404로 드러난다.
## Elements and evidence
- **새 Route** (request): No additional description. Evidence: L816L821.
- **Route Contract** (component): No additional description. Evidence: L819L831.
- **Runtime 계약** (component): No additional description. Evidence: L824L826.
- **배포 전 검사** (component): No additional description. Evidence: L828L831, L854L877.
- **Edge serving** (component): No additional description. Evidence: L827L827, L834L852.
## Relationships
- **Route Contract → Edge serving:** 서빙 패턴. Evidence: L827L827, L834L852.
- **Route Contract → 배포 전 검사:** 대조. Evidence: L828L831, L854L877.
- **새 Route → Route Contract:** 등록. Evidence: L819L831.
- **Route Contract → Runtime 계약:** 반영. Evidence: L824L826.
@@ -0,0 +1,22 @@
# 라우트 하나가 건드리는 손 목록과 검출 시점
# Question: 새 라우트 하나가 어디까지 퍼지고, 빠뜨린 항목은 어느 시점에 처음 드러나는가?
direction: right
n0: "새 Route" {
shape: rectangle
}
n1: "Route Contract" {
shape: rectangle
}
n2: "Runtime 계약" {
shape: rectangle
}
n3: "배포 전 검사" {
shape: rectangle
}
n4: "Edge serving" {
shape: rectangle
}
n0 -> n1: "등록"
n1 -> n2: "반영"
n1 -> n3: "대조"
n1 -> n4: "서빙 패턴"
@@ -0,0 +1,14 @@
digraph techviz {
graph [rankdir=LR, splines=ortho, nodesep=0.55, ranksep=0.85];
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
n0 [label="새 Route", shape=box, style="rounded,filled"];
n1 [label="Route Contract", shape=box, style="rounded,filled"];
n2 [label="Runtime 계약", shape=box, style="rounded,filled"];
n3 [label="배포 전 검사", shape=box, style="rounded,filled"];
n4 [label="Edge serving", shape=box, style="rounded,filled"];
n0 -> n1 [label="등록", style=solid];
n1 -> n2 [label="반영", style=solid];
n1 -> n3 [label="대조", style=solid];
n1 -> n4 [label="서빙 패턴", style=solid];
}
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
<diagram id="route-fanout" name="라우트 하나가 건드리는 손 목록과 검출 시점">
<mxGraphModel dx="1045" dy="427" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1045" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="n_route" value="새 Route" tooltip="request | Evidence: L816-L821" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="35.0" y="145.0" width="190.0" height="64.0" as="geometry"/>
</mxCell>
<mxCell id="n_route-contract" value="Route Contract&lt;br/&gt;tech-log-route-contract.ts" tooltip="component | Evidence: L819-L831" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="470.0" y="155.0" width="175.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_runtime" value="Runtime 계약&lt;br/&gt;route-runtime-contract&lt;br/&gt;메시지 카탈로그&lt;br/&gt;검출: 실행 경로" tooltip="component | Evidence: L824-L826" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="745.0" y="40.0" width="255.0" height="105.0" as="geometry"/>
</mxCell>
<mxCell id="n_predeploy" value="배포 전 검사&lt;br/&gt;vite chunk 표 · 빌드 매니페스트&lt;br/&gt;접근성 증거 · 아티팩트 기준선 · gate digest&lt;br/&gt;검출: 빌드 / 배포 직전" tooltip="component | Evidence: L828-L831, L854-L877" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="745.0" y="165.0" width="255.0" height="105.0" as="geometry"/>
</mxCell>
<mxCell id="n_nginx" value="Edge serving&lt;br/&gt;nginx serving contract&lt;br/&gt;검출: 배포 뒤 404" tooltip="component | Evidence: L827-L827, L834-L852" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="745.0" y="290.0" width="255.0" height="92.0" as="geometry"/>
</mxCell>
<mxCell id="e_nginx-edge" value="서빙 패턴" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_route-contract" target="n_nginx">
<mxGeometry relative="1" as="geometry">
<mxPoint x="719.0" y="272.2" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_predeploy-edge" value="대조" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_route-contract" target="n_predeploy">
<mxGeometry relative="1" as="geometry">
<mxPoint x="719.0" y="204.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_register" value="등록" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_route" target="n_route-contract">
<mxGeometry relative="1" as="geometry">
<mxPoint x="371.5" y="183.8" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_runtime-edge" value="반영" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_route-contract" target="n_runtime">
<mxGeometry relative="1" as="geometry">
<mxPoint x="719.0" y="132.5" as="offset"/>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
@@ -0,0 +1,754 @@
{
"type": "excalidraw",
"version": 2,
"source": "techviz-harness",
"elements": [
{
"id": "edge-nginx-edge",
"type": "arrow",
"x": 645.0,
"y": 208.5,
"width": 100.0,
"height": 127.5,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 1580851045,
"version": 1,
"versionNonce": 1051493990,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
0.0
],
[
50.0,
0.0
],
[
50.0,
127.5
],
[
100.0,
127.5
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-route-contract",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-nginx",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-nginx-edge",
"type": "text",
"x": 674.0,
"y": 260.25,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1521247797,
"version": 1,
"versionNonce": 951657351,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "서빙 패턴",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "서빙 패턴",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-predeploy-edge",
"type": "arrow",
"x": 645.0,
"y": 190.5,
"width": 100.0,
"height": 27.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 1726580064,
"version": 1,
"versionNonce": 253377512,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
0.0
],
[
50.0,
0.0
],
[
50.0,
27.0
],
[
100.0,
27.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-route-contract",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-predeploy",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-predeploy-edge",
"type": "text",
"x": 674.0,
"y": 192.0,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 362342019,
"version": 1,
"versionNonce": 1709430833,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "대조",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "대조",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-register",
"type": "arrow",
"x": 225.0,
"y": 177.0,
"width": 245.0,
"height": 13.5,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 396101324,
"version": 1,
"versionNonce": 525555079,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
0.0
],
[
122.5,
0.0
],
[
122.5,
13.5
],
[
245.0,
13.5
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-route",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-route-contract",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-register",
"type": "text",
"x": 326.5,
"y": 171.75,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1844405344,
"version": 1,
"versionNonce": 937765626,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "등록",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "등록",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-runtime-edge",
"type": "arrow",
"x": 645.0,
"y": 92.5,
"width": 100.0,
"height": 80.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 1322211266,
"version": 1,
"versionNonce": 777996886,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
80.0
],
[
50.0,
80.0
],
[
50.0,
0.0
],
[
100.0,
0.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-route-contract",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-runtime",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-runtime-edge",
"type": "text",
"x": 674.0,
"y": 120.5,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 942197722,
"version": 1,
"versionNonce": 1561589515,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "반영",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "반영",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-route",
"type": "rectangle",
"x": 35.0,
"y": 145.0,
"width": 190.0,
"height": 64.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 350599865,
"version": 1,
"versionNonce": 259087579,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-route",
"type": "text",
"x": 45.0,
"y": 155.0,
"width": 170.0,
"height": 44.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 560883849,
"version": 1,
"versionNonce": 1395574706,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "새 Route",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "새 Route",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-route-contract",
"type": "rectangle",
"x": 470.0,
"y": 155.0,
"width": 175.0,
"height": 71.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1323795186,
"version": 1,
"versionNonce": 1653377524,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-route-contract",
"type": "text",
"x": 480.0,
"y": 165.0,
"width": 155.0,
"height": 51.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1027984965,
"version": 1,
"versionNonce": 179365678,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "Route Contract\ntech-log-route-contract.ts",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "Route Contract\ntech-log-route-contract.ts",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-runtime",
"type": "rectangle",
"x": 745.0,
"y": 40.0,
"width": 255.0,
"height": 105.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 84262181,
"version": 1,
"versionNonce": 1920393701,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-runtime",
"type": "text",
"x": 755.0,
"y": 50.0,
"width": 235.0,
"height": 85.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 225724411,
"version": 1,
"versionNonce": 318497432,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "Runtime 계약\nroute-runtime-contract\n메시지 카탈로그\n검출: 실행 경로",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "Runtime 계약\nroute-runtime-contract\n메시지 카탈로그\n검출: 실행 경로",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-predeploy",
"type": "rectangle",
"x": 745.0,
"y": 165.0,
"width": 255.0,
"height": 105.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1090005214,
"version": 1,
"versionNonce": 409254688,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-predeploy",
"type": "text",
"x": 755.0,
"y": 175.0,
"width": 235.0,
"height": 85.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 401365795,
"version": 1,
"versionNonce": 297641764,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "배포 전 검사\nvite chunk 표 · 빌드 매니페스트\n접근성 증거 · 아티팩트 기준선 · gate digest\n검출: 빌드 / 배포 직전",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "배포 전 검사\nvite chunk 표 · 빌드 매니페스트\n접근성 증거 · 아티팩트 기준선 · gate digest\n검출: 빌드 / 배포 직전",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-nginx",
"type": "rectangle",
"x": 745.0,
"y": 290.0,
"width": 255.0,
"height": 92.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 721013300,
"version": 1,
"versionNonce": 1520646945,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-nginx",
"type": "text",
"x": 755.0,
"y": 300.0,
"width": 235.0,
"height": 72.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 117243520,
"version": 1,
"versionNonce": 936748041,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "Edge serving\nnginx serving contract\n검출: 배포 뒤 404",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "Edge serving\nnginx serving contract\n검출: 배포 뒤 404",
"autoResize": true,
"lineHeight": 1.25
}
],
"appState": {
"gridSize": 10,
"viewBackgroundColor": "#ffffff",
"currentItemFontFamily": 5
},
"files": {}
}
@@ -0,0 +1,32 @@
{
"harness_version": "0.2.0",
"spec_id": "route-fanout",
"spec_version": "1.1",
"spec_sha256": "4a14c651c1b5371be0e96be58e2c3dd6be94785bc6fb2349f395c6a7cf5b1224",
"source_context": {
"document": "docs/TechLog/final/document.md",
"document_sha256": "c3a7de37b778fff7b6ea555a3ad7338c91c6fb15d685e7734f89472b4924d955",
"anchor": {
"kind": "heading",
"value": "8. 라우트를 하나 더하면 함께 울리는 손 목록",
"line": 814
}
},
"outputs": [
"route-fanout.svg",
"route-fanout.drawio",
"route-fanout.mmd",
"route-fanout.d2",
"route-fanout.dot",
"route-fanout.excalidraw",
"route-fanout.alt.md"
],
"lint_issue_count": 1,
"assumption_count": 0,
"assumptions_allowed": false,
"composition_profile": "query-fanout",
"reference_ids": [
"metrics-query-fanout"
],
"diagram_only": true
}
@@ -0,0 +1,12 @@
%% 라우트 하나가 건드리는 손 목록과 검출 시점
%% question: 새 라우트 하나가 어디까지 퍼지고, 빠뜨린 항목은 어느 시점에 처음 드러나는가?
flowchart LR
n0["새 Route"]
n1["Route Contract"]
n2["Runtime 계약"]
n3["배포 전 검사"]
n4["Edge serving"]
n0 -->|"등록"| n1
n1 -->|"반영"| n2
n1 -->|"대조"| n3
n1 -->|"서빙 패턴"| n4
@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1045" height="427" viewBox="0 0 1045 427" role="img" aria-labelledby="diagram-title diagram-description">
<title id="diagram-title">라우트 하나가 건드리는 손 목록과 검출 시점</title>
<desc id="diagram-description">왼쪽의 새 Route가 Route Contract로 들어간 뒤 세 갈래로 퍼진다. Runtime 계약에는 runtime 등록과 메시지 카탈로그가 있다. 배포 전 검사에는 vite chunk 이름 표와 접근성 증거·아티팩트 기준선·gate digest가 묶여 있고 누락은 빌드 매니페스트 또는 배포 직전에 드러난다. Edge 서빙 규칙 누락은 하드 로드나 새로고침 때 배포 뒤 404로 드러난다.</desc>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;route-fanout&quot;,&quot;profile&quot;:&quot;query-fanout&quot;},&quot;source_context&quot;:{&quot;document&quot;:&quot;docs/TechLog/final/document.md&quot;,&quot;document_sha256&quot;:&quot;c3a7de37b778fff7b6ea555a3ad7338c91c6fb15d685e7734f89472b4924d955&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;heading&quot;,&quot;value&quot;:&quot;8. 라우트를 하나 더하면 함께 울리는 손 목록&quot;,&quot;line&quot;:814}},&quot;evidence_policy&quot;:&quot;Each factual element cites source lines or is marked assumption.&quot;,&quot;diagram_only&quot;:true}</metadata>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
<style>
:root { color-scheme: light; }
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
.canvas { fill: #ffffff; }
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
.group-label-bg { fill: #ffffff; }
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
.edge.style-dotted { stroke-dasharray: 2 5; }
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.edge.emphasis-muted { stroke: #9ca3af; }
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
.edge-label-bg { fill: #ffffff; }
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
.node-shape.assumption { stroke-dasharray: 4 4; }
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
.controller-led { fill: #4b5563; }
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
.node-detail { font-size: 11px; fill: #374151; }
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
</style>
</defs>
<rect class="canvas" width="1045" height="427" />
<polyline class="edge kind-data style-solid emphasis-normal" points="645.0,208.5 695.0,208.5 695.0,336.0 745.0,336.0" data-evidence="827-827,834-852" />
<rect class="edge-label-bg" x="693.2" y="258.2" width="51.5" height="22" rx="3" />
<text class="edge-label" x="719.0" y="273.2">서빙 패턴</text>
<polyline class="edge kind-data style-solid emphasis-normal" points="645.0,190.5 695.0,190.5 695.0,217.5 745.0,217.5" data-evidence="828-831,854-877" />
<rect class="edge-label-bg" x="697.0" y="190.0" width="44.0" height="22" rx="3" />
<text class="edge-label" x="719.0" y="205.0">대조</text>
<polyline class="edge kind-request style-solid emphasis-normal" points="225.0,177.0 347.5,177.0 347.5,190.5 470.0,190.5" data-evidence="819-831" />
<rect class="edge-label-bg" x="349.5" y="169.8" width="44.0" height="22" rx="3" />
<text class="edge-label" x="371.5" y="184.8">등록</text>
<polyline class="edge kind-data style-solid emphasis-normal" points="645.0,172.5 695.0,172.5 695.0,92.5 745.0,92.5" data-evidence="824-826" />
<rect class="edge-label-bg" x="697.0" y="118.5" width="44.0" height="22" rx="3" />
<text class="edge-label" x="719.0" y="133.5">반영</text>
<g id="node-route">
<rect class="node-shape kind-request emphasis-normal role-query" data-evidence="816-821" x="35.0" y="145.0" width="190.0" height="64.0" rx="7" />
<text class="node-label" x="130.0" y="175.0">새 Route</text>
</g>
<g id="node-route-contract">
<rect class="node-shape kind-component emphasis-primary role-router" data-evidence="819-831" x="470.0" y="155.0" width="175.0" height="71.0" rx="7" />
<text class="node-role" x="557.5" y="174.0">«router»</text>
<text class="node-label" x="557.5" y="201.0">Route Contract</text>
<line class="node-detail-divider" x1="484.0" y1="222.0" x2="631.0" y2="222.0" />
<text class="node-detail" x="486.0" y="239.0">tech-log-route-contract.ts</text>
</g>
<g id="node-runtime">
<rect class="node-shape kind-component emphasis-normal role-store" data-evidence="824-826" x="745.0" y="40.0" width="255.0" height="105.0" rx="7" />
<text class="node-label" x="872.5" y="67.0">Runtime 계약</text>
<line class="node-detail-divider" x1="759.0" y1="88.0" x2="986.0" y2="88.0" />
<text class="node-detail" x="761.0" y="105.0">route-runtime-contract</text>
<text class="node-detail" x="761.0" y="121.0">메시지 카탈로그</text>
<text class="node-detail" x="761.0" y="137.0">검출: 실행 경로</text>
</g>
<g id="node-predeploy">
<rect class="node-shape kind-component emphasis-normal role-store" data-evidence="828-831,854-877" x="745.0" y="165.0" width="255.0" height="105.0" rx="7" />
<text class="node-label" x="872.5" y="192.0">배포 전 검사</text>
<line class="node-detail-divider" x1="759.0" y1="213.0" x2="986.0" y2="213.0" />
<text class="node-detail" x="761.0" y="230.0">vite chunk 표 · 빌드 매니페스트</text>
<text class="node-detail" x="761.0" y="246.0">접근성 증거 · 아티팩트 기준선 · gate digest</text>
<text class="node-detail" x="761.0" y="262.0">검출: 빌드 / 배포 직전</text>
</g>
<g id="node-nginx">
<rect class="node-shape kind-component emphasis-normal role-store" data-evidence="827-827,834-852" x="745.0" y="290.0" width="255.0" height="92.0" rx="7" />
<text class="node-label" x="872.5" y="318.0">Edge serving</text>
<line class="node-detail-divider" x1="759.0" y1="339.0" x2="986.0" y2="339.0" />
<text class="node-detail" x="761.0" y="356.0">nginx serving contract</text>
<text class="node-detail" x="761.0" y="372.0">검출: 배포 뒤 404</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.9 KiB

@@ -0,0 +1,22 @@
# summary가 세 경계에서 사라진 경로
## Alternative text
Contract summary에서 flattenRelations, ResolvedRelation, 화면 목록으로 이어지는 흐름. 세 중간 지점에 DROP 1, 칸 없음, DROP 3이 표시되어 있다.
## Long description
왼쪽 Contract에는 summary가 있다. flattenRelations가 그 값을 담지 않아 첫 번째로 끊긴다. 다음 ResolvedRelation 계약에는 summary 칸 자체가 없어 두 번째로 막힌다. 그 칸을 추가한 뒤에도 화면 목록으로 넘길 때 값을 버려 세 번째로 끊겼다. 최종 수정에서는 세 경계를 모두 이어 공개 relation 목록까지 summary가 도착하게 했다.
## Elements and evidence
- **Contract summary** (message): No additional description. Evidence: L540L547.
- **flattenRelations** (component): No additional description. Evidence: L544L547.
- **ResolvedRelation** (component): No additional description. Evidence: L546L552.
- **화면 relation 목록** (component): No additional description. Evidence: L547L552.
## Relationships
- **Contract summary → flattenRelations:** summary. Evidence: L544L545.
- **flattenRelations → ResolvedRelation:** 렌더 모델. Evidence: L545L550.
- **ResolvedRelation → 화면 relation 목록:** 화면 전달. Evidence: L546L552.
@@ -0,0 +1,18 @@
# summary가 세 경계에서 사라진 경로
# Question: 계약과 DB에 있던 summary가 공개 relation 목록까지 오지 못한 세 유실 지점은 어디였는가?
direction: right
n0: "Contract summary" {
shape: rectangle
}
n1: "flattenRelations" {
shape: rectangle
}
n2: "ResolvedRelation" {
shape: rectangle
}
n3: "화면 relation 목록" {
shape: rectangle
}
n0 -> n1: "summary"
n1 -> n2: "렌더 모델"
n2 -> n3: "화면 전달"
@@ -0,0 +1,12 @@
digraph techviz {
graph [rankdir=LR, splines=ortho, nodesep=0.55, ranksep=0.85];
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
n0 [label="Contract summary", shape=box, style="rounded,filled"];
n1 [label="flattenRelations", shape=box, style="rounded,filled"];
n2 [label="ResolvedRelation", shape=box, style="rounded,filled"];
n3 [label="화면 relation 목록", shape=box, style="rounded,filled"];
n0 -> n1 [label="summary", style=solid];
n1 -> n2 [label="렌더 모델", style=solid];
n2 -> n3 [label="화면 전달", style=solid];
}
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
<diagram id="summary-drop-path" name="summary가 세 경계에서 사라진 경로">
<mxGraphModel dx="1268" dy="300" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1268" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="n_contract" value="Contract summary&lt;br/&gt;summary 있음" tooltip="message | Evidence: L540-L547" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="70.0" y="68.5" width="150.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_flatten" value="flattenRelations&lt;br/&gt;DROP #1" tooltip="component | Evidence: L544-L547" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
<mxGeometry x="380.0" y="68.5" width="150.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_resolved-relation" value="ResolvedRelation&lt;br/&gt;summary 칸 없음&lt;br/&gt;additionalProperties: false" tooltip="component | Evidence: L546-L552" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
<mxGeometry x="690.0" y="60.0" width="223.0" height="88.0" as="geometry"/>
</mxCell>
<mxCell id="n_screen-list" value="화면 relation 목록&lt;br/&gt;DROP #3" tooltip="component | Evidence: L547-L552" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
<mxGeometry x="1073.0" y="68.5" width="150.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="e_to-flatten" value="summary" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_contract" target="n_flatten">
<mxGeometry relative="1" as="geometry">
<mxPoint x="300.0" y="76.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_to-model" value="렌더 모델" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_flatten" target="n_resolved-relation">
<mxGeometry relative="1" as="geometry">
<mxPoint x="610.0" y="76.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_to-screen" value="화면 전달" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_resolved-relation" target="n_screen-list">
<mxGeometry relative="1" as="geometry">
<mxPoint x="993.0" y="76.0" as="offset"/>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
@@ -0,0 +1,586 @@
{
"type": "excalidraw",
"version": 2,
"source": "techviz-harness",
"elements": [
{
"id": "edge-to-flatten",
"type": "arrow",
"x": 220.0,
"y": 104.0,
"width": 160.0,
"height": 0.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 141665806,
"version": 1,
"versionNonce": 1232986493,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
0.0
],
[
80.0,
0.0
],
[
80.0,
0.0
],
[
160.0,
0.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-contract",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-flatten",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-to-flatten",
"type": "text",
"x": 255.0,
"y": 64.0,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 871217595,
"version": 1,
"versionNonce": 976098011,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "summary",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "summary",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-to-model",
"type": "arrow",
"x": 530.0,
"y": 104.0,
"width": 160.0,
"height": 0.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 1957426780,
"version": 1,
"versionNonce": 962714772,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
0.0
],
[
80.0,
0.0
],
[
80.0,
0.0
],
[
160.0,
0.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-flatten",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-resolved-relation",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-to-model",
"type": "text",
"x": 565.0,
"y": 64.0,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1794075843,
"version": 1,
"versionNonce": 139335072,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "렌더 모델",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "렌더 모델",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-to-screen",
"type": "arrow",
"x": 913.0,
"y": 104.0,
"width": 160.0,
"height": 0.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 864108480,
"version": 1,
"versionNonce": 1322732292,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
0.0
],
[
80.0,
0.0
],
[
80.0,
0.0
],
[
160.0,
0.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-resolved-relation",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-screen-list",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-to-screen",
"type": "text",
"x": 948.0,
"y": 64.0,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1304142647,
"version": 1,
"versionNonce": 1362646482,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "화면 전달",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "화면 전달",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-contract",
"type": "rectangle",
"x": 70.0,
"y": 68.5,
"width": 150.0,
"height": 71.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 179232308,
"version": 1,
"versionNonce": 606575263,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-contract",
"type": "text",
"x": 80.0,
"y": 78.5,
"width": 130.0,
"height": 51.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 165738200,
"version": 1,
"versionNonce": 285005378,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "Contract summary\nsummary 있음",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "Contract summary\nsummary 있음",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-flatten",
"type": "rectangle",
"x": 380.0,
"y": 68.5,
"width": 150.0,
"height": 71.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 232992741,
"version": 1,
"versionNonce": 46540854,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-flatten",
"type": "text",
"x": 390.0,
"y": 78.5,
"width": 130.0,
"height": 51.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1419734197,
"version": 1,
"versionNonce": 1137639440,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "flattenRelations\nDROP #1",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "flattenRelations\nDROP #1",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-resolved-relation",
"type": "rectangle",
"x": 690.0,
"y": 60.0,
"width": 223.0,
"height": 88.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 971199429,
"version": 1,
"versionNonce": 57456686,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-resolved-relation",
"type": "text",
"x": 700.0,
"y": 70.0,
"width": 203.0,
"height": 68.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 541790523,
"version": 1,
"versionNonce": 622968494,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "ResolvedRelation\nsummary 칸 없음\nadditionalProperties: false",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "ResolvedRelation\nsummary 칸 없음\nadditionalProperties: false",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-screen-list",
"type": "rectangle",
"x": 1073.0,
"y": 68.5,
"width": 150.0,
"height": 71.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1285194132,
"version": 1,
"versionNonce": 1968297606,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-screen-list",
"type": "text",
"x": 1083.0,
"y": 78.5,
"width": 130.0,
"height": 51.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 922636927,
"version": 1,
"versionNonce": 39716581,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "화면 relation 목록\nDROP #3",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "화면 relation 목록\nDROP #3",
"autoResize": true,
"lineHeight": 1.25
}
],
"appState": {
"gridSize": 10,
"viewBackgroundColor": "#ffffff",
"currentItemFontFamily": 5
},
"files": {}
}
@@ -0,0 +1,32 @@
{
"harness_version": "0.2.0",
"spec_id": "summary-drop-path",
"spec_version": "1.1",
"spec_sha256": "3ee9cea60c33ed818caaf9d14b2b36992295e4a2d2e956d52ac70fae2aab72cb",
"source_context": {
"document": "docs/TechLog/final/document.md",
"document_sha256": "c3a7de37b778fff7b6ea555a3ad7338c91c6fb15d685e7734f89472b4924d955",
"anchor": {
"kind": "heading",
"value": "5. 계약에 자리가 없어 값이 경계에서 사라진다",
"line": 516
}
},
"outputs": [
"summary-drop-path.svg",
"summary-drop-path.drawio",
"summary-drop-path.mmd",
"summary-drop-path.d2",
"summary-drop-path.dot",
"summary-drop-path.excalidraw",
"summary-drop-path.alt.md"
],
"lint_issue_count": 1,
"assumption_count": 0,
"assumptions_allowed": false,
"composition_profile": "component-flow",
"reference_ids": [
"payment-event-flow"
],
"diagram_only": true
}
@@ -0,0 +1,10 @@
%% summary가 세 경계에서 사라진 경로
%% question: 계약과 DB에 있던 summary가 공개 relation 목록까지 오지 못한 세 유실 지점은 어디였는가?
flowchart LR
n0["Contract summary"]
n1["flattenRelations"]
n2["ResolvedRelation"]
n3["화면 relation 목록"]
n0 -->|"summary"| n1
n1 -->|"렌더 모델"| n2
n2 -->|"화면 전달"| n3
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1268" height="300" viewBox="0 0 1268 300" role="img" aria-labelledby="diagram-title diagram-description">
<title id="diagram-title">summary가 세 경계에서 사라진 경로</title>
<desc id="diagram-description">왼쪽 Contract에는 summary가 있다. flattenRelations가 그 값을 담지 않아 첫 번째로 끊긴다. 다음 ResolvedRelation 계약에는 summary 칸 자체가 없어 두 번째로 막힌다. 그 칸을 추가한 뒤에도 화면 목록으로 넘길 때 값을 버려 세 번째로 끊겼다. 최종 수정에서는 세 경계를 모두 이어 공개 relation 목록까지 summary가 도착하게 했다.</desc>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;summary-drop-path&quot;,&quot;profile&quot;:&quot;component-flow&quot;},&quot;source_context&quot;:{&quot;document&quot;:&quot;docs/TechLog/final/document.md&quot;,&quot;document_sha256&quot;:&quot;c3a7de37b778fff7b6ea555a3ad7338c91c6fb15d685e7734f89472b4924d955&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;heading&quot;,&quot;value&quot;:&quot;5. 계약에 자리가 없어 값이 경계에서 사라진다&quot;,&quot;line&quot;:516}},&quot;evidence_policy&quot;:&quot;Each factual element cites source lines or is marked assumption.&quot;,&quot;diagram_only&quot;:true}</metadata>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
<style>
:root { color-scheme: light; }
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
.canvas { fill: #ffffff; }
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
.group-label-bg { fill: #ffffff; }
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
.edge.style-dotted { stroke-dasharray: 2 5; }
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.edge.emphasis-muted { stroke: #9ca3af; }
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
.edge-label-bg { fill: #ffffff; }
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
.node-shape.assumption { stroke-dasharray: 4 4; }
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
.controller-led { fill: #4b5563; }
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
.node-detail { font-size: 11px; fill: #374151; }
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
</style>
</defs>
<rect class="canvas" width="1268" height="300" />
<polyline class="edge kind-data style-solid emphasis-normal" points="220.0,104.0 300.0,104.0 300.0,104.0 380.0,104.0" data-evidence="544-545" />
<rect class="edge-label-bg" x="267.6" y="62.0" width="64.9" height="22" rx="3" />
<text class="edge-label" x="300.0" y="77.0">summary</text>
<polyline class="edge kind-data style-solid emphasis-normal" points="530.0,104.0 610.0,104.0 610.0,104.0 690.0,104.0" data-evidence="545-550" />
<rect class="edge-label-bg" x="584.2" y="62.0" width="51.5" height="22" rx="3" />
<text class="edge-label" x="610.0" y="77.0">렌더 모델</text>
<polyline class="edge kind-data style-solid emphasis-normal" points="913.0,104.0 993.0,104.0 993.0,104.0 1073.0,104.0" data-evidence="546-552" />
<rect class="edge-label-bg" x="967.2" y="62.0" width="51.5" height="22" rx="3" />
<text class="edge-label" x="993.0" y="77.0">화면 전달</text>
<g id="node-contract">
<rect class="node-shape kind-message emphasis-normal role-source" data-evidence="540-547" x="70.0" y="68.5" width="150.0" height="71.0" rx="7" />
<text class="node-label" x="145.0" y="95.5">Contract summary</text>
<line class="node-detail-divider" x1="84.0" y1="116.5" x2="206.0" y2="116.5" />
<text class="node-detail" x="86.0" y="133.5">summary 있음</text>
</g>
<g id="node-flatten">
<rect class="node-shape kind-component emphasis-warning role-service" data-evidence="544-547" x="380.0" y="68.5" width="150.0" height="71.0" rx="7" />
<text class="node-label" x="455.0" y="95.5">flattenRelations</text>
<line class="node-detail-divider" x1="394.0" y1="116.5" x2="516.0" y2="116.5" />
<text class="node-detail" x="396.0" y="133.5">DROP #1</text>
</g>
<g id="node-resolved-relation">
<rect class="node-shape kind-component emphasis-warning role-service" data-evidence="546-552" x="690.0" y="60.0" width="223.0" height="88.0" rx="7" />
<text class="node-label" x="801.5" y="87.0">ResolvedRelation</text>
<line class="node-detail-divider" x1="704.0" y1="108.0" x2="899.0" y2="108.0" />
<text class="node-detail" x="706.0" y="125.0">summary 칸 없음</text>
<text class="node-detail" x="706.0" y="141.0">additionalProperties: false</text>
</g>
<g id="node-screen-list">
<rect class="node-shape kind-component emphasis-warning role-sink" data-evidence="547-552" x="1073.0" y="68.5" width="150.0" height="71.0" rx="7" />
<text class="node-label" x="1148.0" y="95.5">화면 relation 목록</text>
<line class="node-detail-divider" x1="1087.0" y1="116.5" x2="1209.0" y2="116.5" />
<text class="node-detail" x="1089.0" y="133.5">DROP #3</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.9 KiB

@@ -10,18 +10,18 @@
## Elements and evidence
- **Boundary: 종류별 테이블** (system): No additional description. Evidence: L1071L1073.
- **topic** (database): 주제. 축의 이름을 주제가 정한다. Evidence: L1057L1059, L1067L1068.
- **topic_variant** (database): 축의 값들. Evidence: L1060L1060, L1047L1048.
- **record_variant** (database): 어느 기록이 어느 축에 걸리는지 적는 자리. 외래키를 걸지 못한다. Evidence: L1061L1061, L1071L1073.
- **document** (database): 기록 테이블 하나. Evidence: L1071L1072.
- **open_question** (database): 기록 테이블 하나. Evidence: L1071L1072.
- **project_decision** (database): 기록 테이블 하나. Evidence: L1071L1072.
- **Boundary: 종류별 테이블** (system): No additional description. Evidence: L1382L1384.
- **topic** (database): 주제. 축의 이름을 주제가 정한다. Evidence: L1368L1370, L1377L1379.
- **topic_variant** (database): 축의 값들. Evidence: L1371L1371, L1358L1359.
- **record_variant** (database): 어느 기록이 어느 축에 걸리는지 적는 자리. 외래키를 걸지 못한다. Evidence: L1372L1372, L1382L1384.
- **document** (database): 기록 테이블 하나. Evidence: L1382L1384.
- **open_question** (database): 기록 테이블 하나. Evidence: L1382L1384.
- **project_decision** (database): 기록 테이블 하나. Evidence: L1382L1384.
## Relationships
- **topic → topic_variant:** 1 : N. Evidence: L1057L1060.
- **topic_variant → record_variant:** 축에 건다. Evidence: L1060L1061.
- **record_variant → document:** (kind, id). Evidence: L1061L1073.
- **record_variant → open_question:** (kind, id). Evidence: L1061L1073.
- **record_variant → project_decision:** (kind, id). Evidence: L1061L1073.
- **topic → topic_variant:** 1 : N. Evidence: L1368L1371.
- **topic_variant → record_variant:** 축에 건다. Evidence: L1371L1372.
- **record_variant → document:** (kind, id). Evidence: L1372L1384.
- **record_variant → open_question:** (kind, id). Evidence: L1372L1384.
- **record_variant → project_decision:** (kind, id). Evidence: L1372L1384.
@@ -3,7 +3,7 @@ digraph techviz {
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
subgraph cluster_0 {
label="기록은 종류마다 다른 테이블에 산다";
label="종류별 테이블";
style="rounded,dashed";
color="#66788a";
n3 [label="document", shape=cylinder, style="rounded,filled"];
@@ -8,22 +8,22 @@
<mxCell id="g_record-tables" value="종류별 테이블" style="swimlane;html=1;rounded=1;startSize=30;horizontal=1;dashed=1;strokeWidth=1.5;fillColor=#f7f9fb;strokeColor=#66788a;fontStyle=1;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="1130.0" y="35.0" width="210.0" height="408.0" as="geometry"/>
</mxCell>
<mxCell id="n_topic" value="topic&lt;br/&gt;variant_label" tooltip="주제. 축의 이름을 주제가 정한다. | Evidence: L1057-L1059, L1067-L1068" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxCell id="n_topic" value="topic&lt;br/&gt;variant_label" tooltip="주제. 축의 이름을 주제가 정한다. | Evidence: L1368-L1370, L1377-L1379" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="70.0" y="213.5" width="150.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_topic-variant" value="topic_variant&lt;br/&gt;SPA · Mediator · BFF · Forward-Auth" tooltip="축의 값들. | Evidence: L1060-L1060, L1047-L1048" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxCell id="n_topic-variant" value="topic_variant&lt;br/&gt;SPA · Mediator · BFF · Forward-Auth" tooltip="축의 값들. | Evidence: L1371-L1371, L1358-L1359" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="380.0" y="213.5" width="279.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_record-variant" value="record_variant&lt;br/&gt;(kind, id) 쌍 · 외래키 없음" tooltip="어느 기록이 어느 축에 걸리는지 적는 자리. 외래키를 걸지 못한다. | Evidence: L1061-L1061, L1071-L1073" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
<mxCell id="n_record-variant" value="record_variant&lt;br/&gt;(kind, id) 쌍 · 외래키 없음" tooltip="어느 기록이 어느 축에 걸리는지 적는 자리. 외래키를 걸지 못한다. | Evidence: L1372-L1372, L1382-L1384" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="819.0" y="213.5" width="181.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_document" value="document" tooltip="기록 테이블 하나. | Evidence: L1071-L1072" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxCell id="n_document" value="document" tooltip="기록 테이블 하나. | Evidence: L1382-L1384" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="1160.0" y="81.0" width="150.0" height="64.0" as="geometry"/>
</mxCell>
<mxCell id="n_open-question" value="open_question" tooltip="기록 테이블 하나. | Evidence: L1071-L1072" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxCell id="n_open-question" value="open_question" tooltip="기록 테이블 하나. | Evidence: L1382-L1384" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="1160.0" y="217.0" width="150.0" height="64.0" as="geometry"/>
</mxCell>
<mxCell id="n_project-decision" value="project_decision" tooltip="기록 테이블 하나. | Evidence: L1071-L1072" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxCell id="n_project-decision" value="project_decision" tooltip="기록 테이블 하나. | Evidence: L1382-L1384" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="1160.0" y="353.0" width="150.0" height="64.0" as="geometry"/>
</mxCell>
<mxCell id="e_t1" value="1 : N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_topic" target="n_topic-variant">
@@ -2,14 +2,14 @@
"harness_version": "0.2.0",
"spec_id": "topic-variant-model",
"spec_version": "1.1",
"spec_sha256": "9877bf8caf67db577f45c1576cb30b722b4339578a9880267164f759c1c63df4",
"spec_sha256": "22de32de9a4de3c4b242a01071f8461d27ca4bcbbcdec3c37606db6e2b21cdb9",
"source_context": {
"document": "document.md",
"document_sha256": "93b9fec4884efa0e6231de07dc27e2b0ac36c9052d3720e28d102d9747ac4f8f",
"document": "docs/TechLog/final/document.md",
"document_sha256": "c3a7de37b778fff7b6ea555a3ad7338c91c6fb15d685e7734f89472b4924d955",
"anchor": {
"kind": "marker",
"value": "topic-variant-model",
"line": 1064
"line": 1375
}
},
"outputs": [
@@ -17,6 +17,7 @@
"topic-variant-model.drawio",
"topic-variant-model.mmd",
"topic-variant-model.d2",
"topic-variant-model.dot",
"topic-variant-model.excalidraw",
"topic-variant-model.alt.md"
],
@@ -2,7 +2,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1385" height="488" viewBox="0 0 1385 488" role="img" aria-labelledby="diagram-title diagram-description">
<title id="diagram-title">주제 안의 축과 기록을 잇는 자리</title>
<desc id="diagram-description">왼쪽에 topic 이 있고 variant_label 로 축의 이름을 스스로 정한다. 그 오른쪽에 topic_variant 가 있고 SPA, Mediator, BFF, Forward-Auth 같은 축의 값들을 담는다. 그 오른쪽에 record_variant 가 있고 어느 기록이 어느 축에 걸리는지를 종류와 아이디의 쌍으로 적는다. record_variant 는 오른쪽의 document, open_question, project_decision 세 테이블을 가리키는데, 기록이 종류마다 다른 테이블에 살기 때문에 외래키를 걸지 못하고 쌍으로만 가리킨다.</desc>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;topic-variant-model&quot;,&quot;profile&quot;:&quot;component-flow&quot;},&quot;source_context&quot;:{&quot;document&quot;:&quot;document.md&quot;,&quot;document_sha256&quot;:&quot;93b9fec4884efa0e6231de07dc27e2b0ac36c9052d3720e28d102d9747ac4f8f&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;marker&quot;,&quot;value&quot;:&quot;topic-variant-model&quot;,&quot;line&quot;:1064}},&quot;evidence_policy&quot;:&quot;Each factual element cites source lines or is marked assumption.&quot;,&quot;diagram_only&quot;:true}</metadata>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;topic-variant-model&quot;,&quot;profile&quot;:&quot;component-flow&quot;},&quot;source_context&quot;:{&quot;document&quot;:&quot;docs/TechLog/final/document.md&quot;,&quot;document_sha256&quot;:&quot;c3a7de37b778fff7b6ea555a3ad7338c91c6fb15d685e7734f89472b4924d955&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;marker&quot;,&quot;value&quot;:&quot;topic-variant-model&quot;,&quot;line&quot;:1375}},&quot;evidence_policy&quot;:&quot;Each factual element cites source lines or is marked assumption.&quot;,&quot;diagram_only&quot;:true}</metadata>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" />
@@ -53,49 +53,49 @@
<rect class="group-box" x="1130.0" y="35.0" width="210.0" height="408.0" rx="8" />
<rect class="group-label-bg" x="1144.0" y="25.0" width="90.0" height="22" />
<text class="group-label" x="1154.0" y="40.0">종류별 테이블</text>
<polyline class="edge kind-data style-solid emphasis-normal" points="220.0,249.0 300.0,249.0 300.0,249.0 380.0,249.0" data-evidence="1057-1060" />
<polyline class="edge kind-data style-solid emphasis-normal" points="220.0,249.0 300.0,249.0 300.0,249.0 380.0,249.0" data-evidence="1368-1371" />
<rect class="edge-label-bg" x="274.2" y="207.0" width="51.5" height="22" rx="3" />
<text class="edge-label" x="300.0" y="222.0">1 : N</text>
<polyline class="edge kind-data style-solid emphasis-normal" points="659.0,249.0 739.0,249.0 739.0,249.0 819.0,249.0" data-evidence="1060-1061" />
<polyline class="edge kind-data style-solid emphasis-normal" points="659.0,249.0 739.0,249.0 739.0,249.0 819.0,249.0" data-evidence="1371-1372" />
<rect class="edge-label-bg" x="713.2" y="207.0" width="51.5" height="22" rx="3" />
<text class="edge-label" x="739.0" y="222.0">축에 건다</text>
<polyline class="edge kind-data style-dashed emphasis-normal" points="1000.0,231.0 1080.0,231.0 1080.0,113.0 1160.0,113.0" data-evidence="1061-1073" />
<polyline class="edge kind-data style-dashed emphasis-normal" points="1000.0,231.0 1080.0,231.0 1080.0,113.0 1160.0,113.0" data-evidence="1372-1384" />
<rect class="edge-label-bg" x="1061.5" y="158.0" width="85.0" height="22" rx="3" />
<text class="edge-label" x="1104.0" y="173.0">(kind, id)</text>
<polyline class="edge kind-data style-dashed emphasis-normal" points="1000.0,249.0 1080.0,249.0 1080.0,249.0 1160.0,249.0" data-evidence="1061-1073" />
<polyline class="edge kind-data style-dashed emphasis-normal" points="1000.0,249.0 1080.0,249.0 1080.0,249.0 1160.0,249.0" data-evidence="1372-1384" />
<rect class="edge-label-bg" x="1037.5" y="207.0" width="85.0" height="22" rx="3" />
<text class="edge-label" x="1080.0" y="222.0">(kind, id)</text>
<polyline class="edge kind-data style-dashed emphasis-normal" points="1000.0,267.0 1080.0,267.0 1080.0,385.0 1160.0,385.0" data-evidence="1061-1073" />
<polyline class="edge kind-data style-dashed emphasis-normal" points="1000.0,267.0 1080.0,267.0 1080.0,385.0 1160.0,385.0" data-evidence="1372-1384" />
<rect class="edge-label-bg" x="1061.5" y="312.0" width="85.0" height="22" rx="3" />
<text class="edge-label" x="1104.0" y="327.0">(kind, id)</text>
<g id="node-topic">
<rect class="node-shape kind-database emphasis-normal role-source" data-evidence="1057-1059,1067-1068" x="70.0" y="213.5" width="150.0" height="71.0" rx="7" />
<rect class="node-shape kind-database emphasis-normal role-source" data-evidence="1368-1370,1377-1379" x="70.0" y="213.5" width="150.0" height="71.0" rx="7" />
<text class="node-label" x="145.0" y="240.5">topic</text>
<line class="node-detail-divider" x1="84.0" y1="261.5" x2="206.0" y2="261.5" />
<text class="node-detail" x="86.0" y="278.5">variant_label</text>
</g>
<g id="node-topic-variant">
<rect class="node-shape kind-database emphasis-normal role-store" data-evidence="1060-1060,1047-1048" x="380.0" y="213.5" width="279.0" height="71.0" rx="7" />
<rect class="node-shape kind-database emphasis-normal role-store" data-evidence="1371-1371,1358-1359" x="380.0" y="213.5" width="279.0" height="71.0" rx="7" />
<text class="node-label" x="519.5" y="240.5">topic_variant</text>
<line class="node-detail-divider" x1="394.0" y1="261.5" x2="645.0" y2="261.5" />
<text class="node-detail" x="396.0" y="278.5">SPA · Mediator · BFF · Forward-Auth</text>
</g>
<g id="node-record-variant">
<rect class="node-shape kind-database emphasis-primary role-store" data-evidence="1061-1061,1071-1073" x="819.0" y="213.5" width="181.0" height="71.0" rx="7" />
<rect class="node-shape kind-database emphasis-primary role-store" data-evidence="1372-1372,1382-1384" x="819.0" y="213.5" width="181.0" height="71.0" rx="7" />
<text class="node-label" x="909.5" y="240.5">record_variant</text>
<line class="node-detail-divider" x1="833.0" y1="261.5" x2="986.0" y2="261.5" />
<text class="node-detail" x="835.0" y="278.5">(kind, id) 쌍 · 외래키 없음</text>
</g>
<g id="node-document">
<rect class="node-shape kind-database emphasis-normal role-store" data-evidence="1071-1072" x="1160.0" y="81.0" width="150.0" height="64.0" rx="7" />
<rect class="node-shape kind-database emphasis-normal role-store" data-evidence="1382-1384" x="1160.0" y="81.0" width="150.0" height="64.0" rx="7" />
<text class="node-label" x="1235.0" y="111.0">document</text>
</g>
<g id="node-open-question">
<rect class="node-shape kind-database emphasis-normal role-store" data-evidence="1071-1072" x="1160.0" y="217.0" width="150.0" height="64.0" rx="7" />
<rect class="node-shape kind-database emphasis-normal role-store" data-evidence="1382-1384" x="1160.0" y="217.0" width="150.0" height="64.0" rx="7" />
<text class="node-label" x="1235.0" y="247.0">open_question</text>
</g>
<g id="node-project-decision">
<rect class="node-shape kind-database emphasis-normal role-store" data-evidence="1071-1072" x="1160.0" y="353.0" width="150.0" height="64.0" rx="7" />
<rect class="node-shape kind-database emphasis-normal role-store" data-evidence="1382-1384" x="1160.0" y="353.0" width="150.0" height="64.0" rx="7" />
<text class="node-label" x="1235.0" y="383.0">project_decision</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

@@ -2,7 +2,7 @@
## Alternative text
저장·백엔드 조립·HTTP envelope·프론트엔드 조립·화면 다섯 묶음을 tech-log-backend·전선·tech-log-frontend 구역으로 나눠 이은 흐름도. 묶음마다 그 안에 든 경계 수가 2·4·1·3·1 로 적혀 있다.
열한 개 경계를 저장·백엔드 조립·전선·프론트엔드 조립·화면 다섯 구간으로 묶고, tech-log-backend·HTTP·tech-log-frontend 소유 구역을 함께 표시한 흐름도.
## Long description
@@ -2,22 +2,22 @@
"harness_version": "0.2.0",
"spec_id": "value-boundaries",
"spec_version": "1.1",
"spec_sha256": "2c99d7aadf7942c550c5d0b8ee2bd11f3347ad33d99b233fe57a44989ef0e2c7",
"spec_sha256": "3e2cb33febfa2cfe2a66f9e16a18b96e5710c2a9ec8ef681a0b5941570682038",
"source_context": {
"document": "document.md",
"document_sha256": "93b9fec4884efa0e6231de07dc27e2b0ac36c9052d3720e28d102d9747ac4f8f",
"document": "docs/TechLog/final/document.md",
"document_sha256": "c3a7de37b778fff7b6ea555a3ad7338c91c6fb15d685e7734f89472b4924d955",
"anchor": {
"kind": "marker",
"value": "value-boundaries",
"line": 82
"line": 85
}
},
"outputs": [
"value-boundaries.svg",
"value-boundaries.drawio",
"value-boundaries.mmd",
"value-boundaries.d2",
"value-boundaries.dot",
"value-boundaries.drawio",
"value-boundaries.excalidraw",
"value-boundaries.alt.md"
],
@@ -2,7 +2,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1535" height="300" viewBox="0 0 1535 300" role="img" aria-labelledby="diagram-title diagram-description">
<title id="diagram-title">공개 화면 한 줄까지 값이 지나는 열한 개의 경계 — 다섯 묶음</title>
<desc id="diagram-description">왼쪽에서 오른쪽으로 읽는다. tech-log-backend 구역에 저장 묶음과 백엔드 조립 묶음이 있고 각각 경계 둘과 넷을 담는다. 전선 구역에는 HTTP envelope 하나가 있다. tech-log-frontend 구역에는 프론트엔드 조립 묶음과 화면 컴포넌트가 있고 각각 경계 셋과 하나다. 다 더하면 열한 개이고, 각 경계의 이름은 그림 위 목록에 있다.</desc>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;value-boundaries&quot;,&quot;profile&quot;:&quot;component-flow&quot;},&quot;source_context&quot;:{&quot;document&quot;:&quot;document.md&quot;,&quot;document_sha256&quot;:&quot;93b9fec4884efa0e6231de07dc27e2b0ac36c9052d3720e28d102d9747ac4f8f&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;marker&quot;,&quot;value&quot;:&quot;value-boundaries&quot;,&quot;line&quot;:82}},&quot;evidence_policy&quot;:&quot;Each factual element cites source lines or is marked assumption.&quot;,&quot;diagram_only&quot;:true}</metadata>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;value-boundaries&quot;,&quot;profile&quot;:&quot;component-flow&quot;},&quot;source_context&quot;:{&quot;document&quot;:&quot;docs/TechLog/final/document.md&quot;,&quot;document_sha256&quot;:&quot;c3a7de37b778fff7b6ea555a3ad7338c91c6fb15d685e7734f89472b4924d955&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;marker&quot;,&quot;value&quot;:&quot;value-boundaries&quot;,&quot;line&quot;:85}},&quot;evidence_policy&quot;:&quot;Each factual element cites source lines or is marked assumption.&quot;,&quot;diagram_only&quot;:true}</metadata>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" />

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

+58 -14
View File
@@ -108,9 +108,11 @@ PostgreSQL 테이블
```
레지스트리가 없습니다. 공개 Hub 는 소스가 들어간 이미지라 쓸 수 없고, k3s 의 containerd 소켓은
root 전용이라 사용자 셸에서 닿지 않습니다. 그래서 클러스터 안 일회성 Job 을 띄워 tar 를
import 합니다. 배포 단위는 `hyeonworks.com`(prod) 하나이고 서브도메인은 쓰지 않습니다 —
공개는 `/`, API 는 `/api` 입니다.
root 전용이라 사용자 셸에서 닿지 않습니다. 그래서 클러스터 안 일회성 Job 으로 tar 를
import 하는 경로를 씁니다. 이 경로가 성립하려면 Job 이 host 의 containerd 소켓을 명시적으로
mount 하고 그 소켓을 열 수 있는 권한으로 실행되어야 합니다. `kube-system` namespace 나
클러스터 RBAC 권한만으로 host 소켓에 접근되는 것은 아닙니다. 배포 단위는
`hyeonworks.com`(prod) 하나이고 서브도메인은 쓰지 않습니다 — 공개는 `/`, API 는 `/api` 입니다.
---
@@ -1185,11 +1187,33 @@ nginx 도 서빙했지만 **브라우저는 `/favicon.ico` 를 물었고** 404
### 12.8 npm 환경 변수 누출 (운영 아님, 검증 절차)
vitest 를 `npm`/`npx` 로 돌리면 `npm_config_*` 환경 변수가 설정되고
`ci-workflow-generation.test.ts` 가 실패합니다. 이 저장소에서 테스트를 돌릴 때는:
`ci-workflow-generation.test.ts` 가 실패합니다. `tech-log-frontend` 저장소 루트에서 먼저
남아 있는 변수를 확인합니다.
<!-- command-mode: operator -->
```bash
env $(env | grep -i "^npm_config" | cut -d= -f1 | sed 's/^/-u /' | tr '\n' ' ') \
./node_modules/.bin/vitest run …
env | grep -i '^npm_config'
```
출력이 있으면 같은 셸에서 그 변수만 `unset` 한 뒤 다시 확인합니다. 아래 loop 는 현재 환경의
`npm_config_*` 이름만 골라 지웁니다.
<!-- command-mode: operator -->
```bash
while IFS='=' read -r name _; do
if [[ ${name,,} == npm_config_* ]]; then
unset "$name"
fi
done < <(env)
env | grep -i '^npm_config'
```
두 번째 확인에서 아무것도 나오지 않으면 npm/npx 래퍼를 거치지 않고 실행기를 직접 부릅니다.
<!-- command-mode: operator -->
```bash
./node_modules/.bin/vitest run …
```
---
@@ -1504,19 +1528,39 @@ jpa-feed-query-performance 축 이름 「조회 전략」 축 3개
`check:types` 는 tsconfig 여섯 개를 차례로 돌립니다 — app · node · test · recipes ·
web-worker · service-worker. 루트 tsconfig 를 직접 부르는 명령은 그중 어느 것도 지나지 않습니다.
프론트 검증은 `tech-log-frontend` 저장소 루트에서 실행합니다. §12.8의 환경 변수 정리를
먼저 끝낸 뒤 아래 세 줄을 실행합니다. 마지막 vitest 호출이 unit · component · tech-log 기능
테스트를 함께 돕니다.
<!-- command-mode: operator -->
```bash
# 프론트 — 다섯 개를 다 돌린다. npx tsc --noEmit 은 아무것도 검사하지 않는다
npm run check:types
npm run lint
env $(env | grep -i "^npm_config" | cut -d= -f1 | sed 's/^/-u /' | tr '\n' ' ') \
./node_modules/.bin/vitest run tests/unit tests/component tests/features/tech-log
./node_modules/.bin/vitest run tests/unit tests/component tests/features/tech-log
```
# 백엔드 — 커밋한 뒤에 돌린다(산출물 이름에 커밋 해시가 들어간다)
cd src && ./gradlew cleanStaleTraceableJars build
세 명령이 모두 0으로 끝나야 프론트 검증이 끝난 것입니다. `npx tsc --noEmit` 성공은 이 저장소의
타입 검증을 대신하지 않습니다.
# 설계 패키지
python3 scripts/check-openapi.py && python3 scripts/check-consistency.py \
&& python3 scripts/check-contract-parity.py
백엔드는 `tech-log-backend` 저장소 루트에서 먼저 작업 트리를 확인합니다. 빌드 산출물 이름에
커밋 해시가 들어가므로 `git status --short` 출력이 남아 있다면 먼저 그 변경이 의도한 것인지
확인하고 커밋 상태를 정리합니다.
<!-- command-mode: operator -->
```bash
git status --short
cd src
./gradlew cleanStaleTraceableJars build
```
`src` 로 이동한 같은 위치에서 설계 패키지의 세 검사를 각각 실행합니다. 한 줄로 묶지 않아 어느
검사가 실패했는지 그대로 남깁니다.
<!-- command-mode: operator -->
```bash
python3 scripts/check-openapi.py
python3 scripts/check-consistency.py
python3 scripts/check-contract-parity.py
```
---