{ "schema_version": "1.0", "document": "/home/donghyeon/workspace/chat-gpt-container/document-haness/docs/clean-architecture-backend-template/final/document.md", "document_sha256": "8071fe71b3359d9cf60b95909c26c7b50653ce2f22bbc5fcf6988719bb91236d", "line_count": 47035, "line_number_space": "canonical-source-with-managed-blocks-collapsed", "anchor": { "kind": "line", "value": 39711, "line": 39711 }, "current_section": { "heading": { "line": 39711, "level": 5, "text": "4.1 `MessagingAdapterContract` — 7개가 \"지원한다\"의 정의" }, "start_line": 39711, "end_line": 39768, "text": "##### 4.1 `MessagingAdapterContract` — 7개가 \"지원한다\"의 정의\n\n```java\n// MessagingAdapterContract.java:10-19\n/**\n * The behaviour every adapter must exhibit, regardless of broker.\n *\n *
This suite is the platform's actual definition of \"supported\". A broker is Stable when it\n * passes these unchanged — not when it has an adapter that compiles. …\n */\npublic abstract class MessagingAdapterContract {\n protected abstract MessagingAdapterHarness harness();\n```\n\n7개 테스트와 각각이 못 박는 것:\n\n| 테스트 | 못 박는 불변식 |\n|---|---|\n| `publishesAndConfirms` | 확인은 `CONFIRMED` + `brokerAccepted` + `confirmationLevel != NONE` + `TRANSMITTED` 넷이 동시에 |\n| `returnsAmbiguousWhenConfirmIsLost` | 확인 유실은 추측이 아니라 `AMBIGUOUS` + `mayHaveBeenStored()` |\n| `redeliversWhenSettlementIsLost` | 정산 미확인 → `settled=false` → 2회차 `attempt=2, redelivered=true` |\n| `preservesMessageIdAcrossRetryAndDlq` | 재시도·DLQ 를 거쳐도 `MessageId` 동일 |\n| `keepsSourceUnsettledWhenDlqPublishFails` | DLQ 발행 실패 시 원본을 **정산하지 않는다** |\n| `rejectsOversizedPayloadBeforeTransport` | 크기 초과는 브로커가 아니라 로컬에서 거절 |\n| `stopsAcceptingNewWorkDuringShutdown` | 종료 시작 후 발행은 로컬 거절 |\n\n계약을 `abstract class` + `@Test` 로 만든 결정의 효과는 `InMemoryHarnessContractTest` 의 javadoc 에 있다.\n\n```java\n// InMemoryHarnessContractTest.java:5-11\n/**\n * Runs the shared adapter contract against the in-memory harness.\n *\n *
This is the contract testing itself. Every broker adapter adds the same nested class over its\n * own harness, so a guarantee can only be weakened by editing the contract, where the change is\n * visible, rather than by an adapter quietly not implementing it.\n */\n```\n\n즉 어댑터가 `@Test` 를 **삭제하는 방법이 없다**. 상속받는 순간 7개가 전부 실행된다. 어댑터 쪽에서 하나를 빼려면 이 파일을 고쳐야 하고, 그것은 리뷰에 보인다.\n\n그리고 그 7개가 침묵으로 줄어드는 것을 막는 자물쇠가 하나 더 있다.\n\n```java\n// CompatibilityMatrixTest.java:13-21, 71-80\nprivate static final List The expected outcome is part of the scenario rather than left to each test, because the whole\n * value of a fault suite is that every adapter answers the same way. A scenario that let each\n * adapter declare its own expectation would pass while the adapters disagreed — which is exactly\n * the situation the shared contract exists to catch.\n *\n * The distinction across these scenarios is what evidence survives. …\n */\n```\n\n5개 시나리오, 그리고 각각이 `rationale` 을 **비어 있으면 생성 자체가 실패하도록** 강제한다.\n\n```java\n// NetworkFaultScenario.java:59-62\nif (rationale == null || rationale.isBlank()) {\n throw new IllegalArgumentException(\n \"a scenario without a rationale cannot be reviewed; state why this outcome is correct\");\n}\n```\n\n| 시나리오 | Phase | Expectation | rationale 요지 |\n|---|---|---|---|\n| `connection-refused` | `BEFORE_TRANSMISSION` | `REJECTED` | 바이트가 안 나갔으니 브로커가 가질 수 없다 |\n| `connection-cut-after-write` | `AFTER_TRANSMISSION` | `AMBIGUOUS` | 여기서 거절이라 답하는 게 \"잃어버린 확인 하나를 주문 둘로\" 만든다 |\n| `confirm-timeout` | `AFTER_TRANSMISSION` | `AMBIGUOUS` | \"타임아웃은 부재의 증거가 아니라 증거의 부재\" |\n| `settlement-lost` | `DURING_SETTLEMENT` | `REDELIVERED` | 재배달은 설계이며, 핸들러가 멱등이어야지 플랫폼이 정산된 척하면 안 된다 |\n| `high-latency` | `AFTER_TRANSMISSION` | `AMBIGUOUS` | 결정 시점에 지연과 유실은 구별 불가 |\n\n`REJECTED` 가 `BEFORE_TRANSMISSION` 하나뿐이라는 사실이 테스트로 잠겨 있다(`CrossBrokerContractSuite.aFailureBeforeTransmissionIsTheOnlyOneReportedAsRejected`).\n\n`byName` 은 알 수 없는 이름을 건너뛰지 않고 거절한다.\n\n```java\n// NetworkFaultScenario.java:125-136\n.orElseThrow(() -> new IllegalArgumentException(\n \"no fault scenario is named '\" + name + \"'; evidence for a scenario this release does not\"\n + \" define is coverage of nothing\"));\n```\n"
},
"context_range": {
"start_line": 39709,
"end_line": 39811
},
"context_lines": [
{
"line": 39709,
"text": "#### 4. 계약·불변식·상태 모델"
},
{
"line": 39710,
"text": ""
},
{
"line": 39711,
"text": "##### 4.1 `MessagingAdapterContract` — 7개가 \"지원한다\"의 정의"
},
{
"line": 39712,
"text": ""
},
{
"line": 39713,
"text": "```java"
},
{
"line": 39714,
"text": "// MessagingAdapterContract.java:10-19"
},
{
"line": 39715,
"text": "/**"
},
{
"line": 39716,
"text": " * The behaviour every adapter must exhibit, regardless of broker."
},
{
"line": 39717,
"text": " *"
},
{
"line": 39718,
"text": " * This suite is the platform's actual definition of \"supported\". A broker is Stable when it"
},
{
"line": 39719,
"text": " * passes these unchanged — not when it has an adapter that compiles. …"
},
{
"line": 39720,
"text": " */"
},
{
"line": 39721,
"text": "public abstract class MessagingAdapterContract {"
},
{
"line": 39722,
"text": " protected abstract MessagingAdapterHarness harness();"
},
{
"line": 39723,
"text": "```"
},
{
"line": 39724,
"text": ""
},
{
"line": 39725,
"text": "7개 테스트와 각각이 못 박는 것:"
},
{
"line": 39726,
"text": ""
},
{
"line": 39727,
"text": "| 테스트 | 못 박는 불변식 |"
},
{
"line": 39728,
"text": "|---|---|"
},
{
"line": 39729,
"text": "| `publishesAndConfirms` | 확인은 `CONFIRMED` + `brokerAccepted` + `confirmationLevel != NONE` + `TRANSMITTED` 넷이 동시에 |"
},
{
"line": 39730,
"text": "| `returnsAmbiguousWhenConfirmIsLost` | 확인 유실은 추측이 아니라 `AMBIGUOUS` + `mayHaveBeenStored()` |"
},
{
"line": 39731,
"text": "| `redeliversWhenSettlementIsLost` | 정산 미확인 → `settled=false` → 2회차 `attempt=2, redelivered=true` |"
},
{
"line": 39732,
"text": "| `preservesMessageIdAcrossRetryAndDlq` | 재시도·DLQ 를 거쳐도 `MessageId` 동일 |"
},
{
"line": 39733,
"text": "| `keepsSourceUnsettledWhenDlqPublishFails` | DLQ 발행 실패 시 원본을 **정산하지 않는다** |"
},
{
"line": 39734,
"text": "| `rejectsOversizedPayloadBeforeTransport` | 크기 초과는 브로커가 아니라 로컬에서 거절 |"
},
{
"line": 39735,
"text": "| `stopsAcceptingNewWorkDuringShutdown` | 종료 시작 후 발행은 로컬 거절 |"
},
{
"line": 39736,
"text": ""
},
{
"line": 39737,
"text": "계약을 `abstract class` + `@Test` 로 만든 결정의 효과는 `InMemoryHarnessContractTest` 의 javadoc 에 있다."
},
{
"line": 39738,
"text": ""
},
{
"line": 39739,
"text": "```java"
},
{
"line": 39740,
"text": "// InMemoryHarnessContractTest.java:5-11"
},
{
"line": 39741,
"text": "/**"
},
{
"line": 39742,
"text": " * Runs the shared adapter contract against the in-memory harness."
},
{
"line": 39743,
"text": " *"
},
{
"line": 39744,
"text": " * This is the contract testing itself. Every broker adapter adds the same nested class over its"
},
{
"line": 39745,
"text": " * own harness, so a guarantee can only be weakened by editing the contract, where the change is"
},
{
"line": 39746,
"text": " * visible, rather than by an adapter quietly not implementing it."
},
{
"line": 39747,
"text": " */"
},
{
"line": 39748,
"text": "```"
},
{
"line": 39749,
"text": ""
},
{
"line": 39750,
"text": "즉 어댑터가 `@Test` 를 **삭제하는 방법이 없다**. 상속받는 순간 7개가 전부 실행된다. 어댑터 쪽에서 하나를 빼려면 이 파일을 고쳐야 하고, 그것은 리뷰에 보인다."
},
{
"line": 39751,
"text": ""
},
{
"line": 39752,
"text": "그리고 그 7개가 침묵으로 줄어드는 것을 막는 자물쇠가 하나 더 있다."
},
{
"line": 39753,
"text": ""
},
{
"line": 39754,
"text": "```java"
},
{
"line": 39755,
"text": "// CompatibilityMatrixTest.java:13-21, 71-80"
},
{
"line": 39756,
"text": "private static final List The expected outcome is part of the scenario rather than left to each test, because the whole"
},
{
"line": 39775,
"text": " * value of a fault suite is that every adapter answers the same way. A scenario that let each"
},
{
"line": 39776,
"text": " * adapter declare its own expectation would pass while the adapters disagreed — which is exactly"
},
{
"line": 39777,
"text": " * the situation the shared contract exists to catch."
},
{
"line": 39778,
"text": " *"
},
{
"line": 39779,
"text": " * The distinction across these scenarios is what evidence survives. …"
},
{
"line": 39780,
"text": " */"
},
{
"line": 39781,
"text": "```"
},
{
"line": 39782,
"text": ""
},
{
"line": 39783,
"text": "5개 시나리오, 그리고 각각이 `rationale` 을 **비어 있으면 생성 자체가 실패하도록** 강제한다."
},
{
"line": 39784,
"text": ""
},
{
"line": 39785,
"text": "```java"
},
{
"line": 39786,
"text": "// NetworkFaultScenario.java:59-62"
},
{
"line": 39787,
"text": "if (rationale == null || rationale.isBlank()) {"
},
{
"line": 39788,
"text": " throw new IllegalArgumentException("
},
{
"line": 39789,
"text": " \"a scenario without a rationale cannot be reviewed; state why this outcome is correct\");"
},
{
"line": 39790,
"text": "}"
},
{
"line": 39791,
"text": "```"
},
{
"line": 39792,
"text": ""
},
{
"line": 39793,
"text": "| 시나리오 | Phase | Expectation | rationale 요지 |"
},
{
"line": 39794,
"text": "|---|---|---|---|"
},
{
"line": 39795,
"text": "| `connection-refused` | `BEFORE_TRANSMISSION` | `REJECTED` | 바이트가 안 나갔으니 브로커가 가질 수 없다 |"
},
{
"line": 39796,
"text": "| `connection-cut-after-write` | `AFTER_TRANSMISSION` | `AMBIGUOUS` | 여기서 거절이라 답하는 게 \"잃어버린 확인 하나를 주문 둘로\" 만든다 |"
},
{
"line": 39797,
"text": "| `confirm-timeout` | `AFTER_TRANSMISSION` | `AMBIGUOUS` | \"타임아웃은 부재의 증거가 아니라 증거의 부재\" |"
},
{
"line": 39798,
"text": "| `settlement-lost` | `DURING_SETTLEMENT` | `REDELIVERED` | 재배달은 설계이며, 핸들러가 멱등이어야지 플랫폼이 정산된 척하면 안 된다 |"
},
{
"line": 39799,
"text": "| `high-latency` | `AFTER_TRANSMISSION` | `AMBIGUOUS` | 결정 시점에 지연과 유실은 구별 불가 |"
},
{
"line": 39800,
"text": ""
},
{
"line": 39801,
"text": "`REJECTED` 가 `BEFORE_TRANSMISSION` 하나뿐이라는 사실이 테스트로 잠겨 있다(`CrossBrokerContractSuite.aFailureBeforeTransmissionIsTheOnlyOneReportedAsRejected`)."
},
{
"line": 39802,
"text": ""
},
{
"line": 39803,
"text": "`byName` 은 알 수 없는 이름을 건너뛰지 않고 거절한다."
},
{
"line": 39804,
"text": ""
},
{
"line": 39805,
"text": "```java"
},
{
"line": 39806,
"text": "// NetworkFaultScenario.java:125-136"
},
{
"line": 39807,
"text": ".orElseThrow(() -> new IllegalArgumentException("
},
{
"line": 39808,
"text": " \"no fault scenario is named '\" + name + \"'; evidence for a scenario this release does not\""
},
{
"line": 39809,
"text": " + \" define is coverage of nothing\"));"
},
{
"line": 39810,
"text": "```"
},
{
"line": 39811,
"text": ""
}
],
"numbered_context": "39709 | #### 4. 계약·불변식·상태 모델\n39710 | \n39711 | ##### 4.1 `MessagingAdapterContract` — 7개가 \"지원한다\"의 정의\n39712 | \n39713 | ```java\n39714 | // MessagingAdapterContract.java:10-19\n39715 | /**\n39716 | * The behaviour every adapter must exhibit, regardless of broker.\n39717 | *\n39718 | * This suite is the platform's actual definition of \"supported\". A broker is Stable when it\n39719 | * passes these unchanged — not when it has an adapter that compiles. …\n39720 | */\n39721 | public abstract class MessagingAdapterContract {\n39722 | protected abstract MessagingAdapterHarness harness();\n39723 | ```\n39724 | \n39725 | 7개 테스트와 각각이 못 박는 것:\n39726 | \n39727 | | 테스트 | 못 박는 불변식 |\n39728 | |---|---|\n39729 | | `publishesAndConfirms` | 확인은 `CONFIRMED` + `brokerAccepted` + `confirmationLevel != NONE` + `TRANSMITTED` 넷이 동시에 |\n39730 | | `returnsAmbiguousWhenConfirmIsLost` | 확인 유실은 추측이 아니라 `AMBIGUOUS` + `mayHaveBeenStored()` |\n39731 | | `redeliversWhenSettlementIsLost` | 정산 미확인 → `settled=false` → 2회차 `attempt=2, redelivered=true` |\n39732 | | `preservesMessageIdAcrossRetryAndDlq` | 재시도·DLQ 를 거쳐도 `MessageId` 동일 |\n39733 | | `keepsSourceUnsettledWhenDlqPublishFails` | DLQ 발행 실패 시 원본을 **정산하지 않는다** |\n39734 | | `rejectsOversizedPayloadBeforeTransport` | 크기 초과는 브로커가 아니라 로컬에서 거절 |\n39735 | | `stopsAcceptingNewWorkDuringShutdown` | 종료 시작 후 발행은 로컬 거절 |\n39736 | \n39737 | 계약을 `abstract class` + `@Test` 로 만든 결정의 효과는 `InMemoryHarnessContractTest` 의 javadoc 에 있다.\n39738 | \n39739 | ```java\n39740 | // InMemoryHarnessContractTest.java:5-11\n39741 | /**\n39742 | * Runs the shared adapter contract against the in-memory harness.\n39743 | *\n39744 | * This is the contract testing itself. Every broker adapter adds the same nested class over its\n39745 | * own harness, so a guarantee can only be weakened by editing the contract, where the change is\n39746 | * visible, rather than by an adapter quietly not implementing it.\n39747 | */\n39748 | ```\n39749 | \n39750 | 즉 어댑터가 `@Test` 를 **삭제하는 방법이 없다**. 상속받는 순간 7개가 전부 실행된다. 어댑터 쪽에서 하나를 빼려면 이 파일을 고쳐야 하고, 그것은 리뷰에 보인다.\n39751 | \n39752 | 그리고 그 7개가 침묵으로 줄어드는 것을 막는 자물쇠가 하나 더 있다.\n39753 | \n39754 | ```java\n39755 | // CompatibilityMatrixTest.java:13-21, 71-80\n39756 | private static final List The expected outcome is part of the scenario rather than left to each test, because the whole\n39775 | * value of a fault suite is that every adapter answers the same way. A scenario that let each\n39776 | * adapter declare its own expectation would pass while the adapters disagreed — which is exactly\n39777 | * the situation the shared contract exists to catch.\n39778 | *\n39779 | * The distinction across these scenarios is what evidence survives. …\n39780 | */\n39781 | ```\n39782 | \n39783 | 5개 시나리오, 그리고 각각이 `rationale` 을 **비어 있으면 생성 자체가 실패하도록** 강제한다.\n39784 | \n39785 | ```java\n39786 | // NetworkFaultScenario.java:59-62\n39787 | if (rationale == null || rationale.isBlank()) {\n39788 | throw new IllegalArgumentException(\n39789 | \"a scenario without a rationale cannot be reviewed; state why this outcome is correct\");\n39790 | }\n39791 | ```\n39792 | \n39793 | | 시나리오 | Phase | Expectation | rationale 요지 |\n39794 | |---|---|---|---|\n39795 | | `connection-refused` | `BEFORE_TRANSMISSION` | `REJECTED` | 바이트가 안 나갔으니 브로커가 가질 수 없다 |\n39796 | | `connection-cut-after-write` | `AFTER_TRANSMISSION` | `AMBIGUOUS` | 여기서 거절이라 답하는 게 \"잃어버린 확인 하나를 주문 둘로\" 만든다 |\n39797 | | `confirm-timeout` | `AFTER_TRANSMISSION` | `AMBIGUOUS` | \"타임아웃은 부재의 증거가 아니라 증거의 부재\" |\n39798 | | `settlement-lost` | `DURING_SETTLEMENT` | `REDELIVERED` | 재배달은 설계이며, 핸들러가 멱등이어야지 플랫폼이 정산된 척하면 안 된다 |\n39799 | | `high-latency` | `AFTER_TRANSMISSION` | `AMBIGUOUS` | 결정 시점에 지연과 유실은 구별 불가 |\n39800 | \n39801 | `REJECTED` 가 `BEFORE_TRANSMISSION` 하나뿐이라는 사실이 테스트로 잠겨 있다(`CrossBrokerContractSuite.aFailureBeforeTransmissionIsTheOnlyOneReportedAsRejected`).\n39802 | \n39803 | `byName` 은 알 수 없는 이름을 건너뛰지 않고 거절한다.\n39804 | \n39805 | ```java\n39806 | // NetworkFaultScenario.java:125-136\n39807 | .orElseThrow(() -> new IllegalArgumentException(\n39808 | \"no fault scenario is named '\" + name + \"'; evidence for a scenario this release does not\"\n39809 | + \" define is coverage of nothing\"));\n39810 | ```\n39811 | ",
"headings": [
{
"line": 1,
"level": 1,
"text": "clean-architecture-backend-template — 상세 분석 (통합 정본)"
},
{
"line": 40,
"level": 2,
"text": "0. 이 문서를 읽는 법"
},
{
"line": 60,
"level": 2,
"text": "1. Project map — 숫자로 먼저"
},
{
"line": 62,
"level": 3,
"text": "1.1 빌드와 레지스트리"
},
{
"line": 81,
"level": 3,
"text": "1.2 가족별 분모와 출하 여부"
},
{
"line": 94,
"level": 3,
"text": "1.3 leaf별 규모 (main Java 기준 상위)"
},
{
"line": 119,
"level": 3,
"text": "1.4 이 표에서 읽어야 할 것"
},
{
"line": 168,
"level": 2,
"text": "2. Architectural boundaries — 무엇이 경계를 강제하는가"
},
{
"line": 173,
"level": 3,
"text": "2.1 강제 장치 목록"
},
{
"line": 189,
"level": 3,
"text": "2.2 `CleanArchitectureTest`의 규칙 14종"
},
{
"line": 212,
"level": 3,
"text": "2.3 검증된 경계 — 실제로 성립하는 것"
},
{
"line": 266,
"level": 3,
"text": "2.4 경계가 열려 있는 지점"
},
{
"line": 300,
"level": 2,
"text": "3. Representative execution paths"
},
{
"line": 302,
"level": 3,
"text": "3.1 HTTP 요청 — 출하 경로"
},
{
"line": 364,
"level": 3,
"text": "3.2 트랜잭션 — `application-core` 포트에서 PostgreSQL local timeout까지"
},
{
"line": 453,
"level": 3,
"text": "3.3 메시지 발행 — messaging 플랫폼"
},
{
"line": 494,
"level": 3,
"text": "3.4 gRPC — 채택 시점 경로"
},
{
"line": 518,
"level": 3,
"text": "3.5 알림 발송 — 논리적 수락과 provider 불확실성"
},
{
"line": 539,
"level": 2,
"text": "4. Data and state"
},
{
"line": 541,
"level": 3,
"text": "4.1 관계형 — `persistence-jpa` (605 파일 / main 350 / 27,744 LOC)"
},
{
"line": 654,
"level": 3,
"text": "4.2 문서형 — `persistence-mongo` (497 파일 / main 351 / 22,924 LOC)"
},
{
"line": 705,
"level": 3,
"text": "4.3 messaging 신뢰성 저장소 (`19` §7)"
},
{
"line": 757,
"level": 3,
"text": "4.4 fileserver / objectstorage / cache-redis"
},
{
"line": 788,
"level": 2,
"text": "5. Failure and operational behavior"
},
{
"line": 790,
"level": 3,
"text": "5.1 실패 분류 — 세 개의 계층"
},
{
"line": 824,
"level": 3,
"text": "5.2 관측 — 태그를 유한하게, 그리고 그 대가"
},
{
"line": 854,
"level": 3,
"text": "5.3 시작 검증기 — 법칙과 그 예외"
},
{
"line": 903,
"level": 3,
"text": "5.4 admin plane — 가장 잘 조립된 게이트"
},
{
"line": 939,
"level": 3,
"text": "5.5 gRPC 구현 층의 원자성 (`20` §7)"
},
{
"line": 1011,
"level": 2,
"text": "6. Tests and verification coverage"
},
{
"line": 1013,
"level": 3,
"text": "6.1 실행한 것"
},
{
"line": 1025,
"level": 3,
"text": "6.2 실행하지 않은 것과 그 이유"
},
{
"line": 1047,
"level": 3,
"text": "6.3 fail-closed 레인 규약"
},
{
"line": 1071,
"level": 3,
"text": "6.4 완전히 닫힌 게이트 하나 — messaging 인증 체인"
},
{
"line": 1111,
"level": 3,
"text": "6.5 evidence manifest — JPA의 R1/R2 분리"
},
{
"line": 1125,
"level": 3,
"text": "6.6 게이트가 통과하면서 아무것도 증명하지 않는 경우 — 14건"
},
{
"line": 1156,
"level": 2,
"text": "7. 이 저장소에서 반복된 네 가지 형태"
},
{
"line": 1160,
"level": 3,
"text": "7.1 형태 A — 판정하는 코드는 있고, 부르는 코드가 없다"
},
{
"line": 1203,
"level": 3,
"text": "7.2 형태 B — 게이트가 통과하면서 아무것도 증명하지 않는다"
},
{
"line": 1214,
"level": 3,
"text": "7.3 형태 C — 중복 장치에서 조립된 쪽이 약한 쪽이다"
},
{
"line": 1239,
"level": 3,
"text": "7.4 형태 D — 문서 드리프트, 그리고 그 방향"
},
{
"line": 1274,
"level": 3,
"text": "7.5 공시 스펙트럼 — 자기 미완성을 얼마나 말했는가"
},
{
"line": 1289,
"level": 3,
"text": "7.6 학습 전이 — messaging → grpc"
},
{
"line": 1308,
"level": 2,
"text": "8. Confirmed problems"
},
{
"line": 1310,
"level": 3,
"text": "8.1 P1 — 지금 출하되는 아티팩트에서 틀린 동작"
},
{
"line": 1349,
"level": 3,
"text": "8.2 P2 — 명확한 실패 시나리오를 가진 실질적 공백"
},
{
"line": 1392,
"level": 3,
"text": "8.3 심각도가 등급 때문에 낮아진 것"
},
{
"line": 1403,
"level": 2,
"text": "9. Reusable criteria and rules"
},
{
"line": 1452,
"level": 2,
"text": "10. Explicit project decisions"
},
{
"line": 1457,
"level": 3,
"text": "10.1 계약과 경계"
},
{
"line": 1468,
"level": 3,
"text": "10.2 실패와 불확실성"
},
{
"line": 1480,
"level": 3,
"text": "10.3 조립과 활성화"
},
{
"line": 1492,
"level": 3,
"text": "10.4 데이터와 경계값"
},
{
"line": 1506,
"level": 3,
"text": "10.5 증거와 게이트"
},
{
"line": 1523,
"level": 2,
"text": "11. Unresolved questions"
},
{
"line": 1564,
"level": 2,
"text": "12. Evidence index"
},
{
"line": 1581,
"level": 2,
"text": "13. Limits of this analysis"
},
{
"line": 1632,
"level": 2,
"text": "14. 사이클 2 — 18개 리프 재검증과 23개 리프 전수 통독"
},
{
"line": 1634,
"level": 3,
"text": "14.1 18개 리프 재검증"
},
{
"line": 1668,
"level": 3,
"text": "14.2 23개 리프 전수 통독"
},
{
"line": 1747,
"level": 2,
"text": "부록 A. 모듈 문서 지도"
},
{
"line": 1779,
"level": 2,
"text": "부록 B. 자주 쓸 명령"
},
{
"line": 1825,
"level": 2,
"text": "부록 C. 다시 읽는다면 이 순서"
},
{
"line": 1839,
"level": 1,
"text": "제2부 — 모듈 분석 전문"
},
{
"line": 1845,
"level": 2,
"text": "A00. project-overview"
},
{
"line": 1849,
"level": 3,
"text": "Project Overview"
},
{
"line": 1856,
"level": 4,
"text": "분석 기준 revision"
},
{
"line": 1867,
"level": 4,
"text": "최종 커버리지"
},
{
"line": 1884,
"level": 4,
"text": "Build and module map"
},
{
"line": 1939,
"level": 4,
"text": "Dependency direction"
},
{
"line": 1945,
"level": 4,
"text": "Runtime entry points"
},
{
"line": 1951,
"level": 4,
"text": "Persistence / messaging / external systems"
},
{
"line": 1955,
"level": 4,
"text": "Test topology"
},
{
"line": 1960,
"level": 4,
"text": "Configuration and operational surfaces"
},
{
"line": 1964,
"level": 4,
"text": "분석할 bounded scopes (계획 — 실제 문서 배치는 위 \"최종 커버리지\" 참조)"
},
{
"line": 1977,
"level": 4,
"text": "아직 단정하지 않는 것 (분석 시작 시점의 목록)"
},
{
"line": 1993,
"level": 2,
"text": "A01. domain-core"
},
{
"line": 1997,
"level": 3,
"text": "domain-core 상세 분석"
},
{
"line": 2000,
"level": 4,
"text": "SSOT identity — 2026-08-31 재검증"
},
{
"line": 2015,
"level": 4,
"text": "분석 범위와 결론 상태"
},
{
"line": 2026,
"level": 4,
"text": "1. Quantified scope map"
},
{
"line": 2028,
"level": 5,
"text": "Owned source"
},
{
"line": 2042,
"level": 4,
"text": "2. Coverage ledger"
},
{
"line": 2062,
"level": 4,
"text": "3. 이 모듈이 실제로 소유하는 것"
},
{
"line": 2064,
"level": 5,
"text": "관찰: 재사용 가능한 도메인 “내용”보다 도메인 모델링 계약을 소유한다"
},
{
"line": 2073,
"level": 4,
"text": "4. Identifier contract"
},
{
"line": 2075,
"level": 5,
"text": "`ResourceId