Files
document-haness/docs/TechLog/tech-log-studio/tech-log-tree.json
T

3026 lines
136 KiB
JSON

{
"schemaVersion": 4,
"project": "TechLog",
"ssot": "final/document.md",
"sourceRepository": [
{
"name": "tech-log-frontend",
"path": "/home/donghyeon/workspace/desktop-server-git/tech-log-frontend",
"revision": "3374f286be99b2e25ef3a920c2f283b12bf16bb1",
"verified": "main 의 HEAD 이고, 이 문서가 인용한 프론트엔드 커밋 108개가 전부 이 커밋의 조상이다 (2026-09-07 확인)"
},
{
"name": "tech-log-backend",
"path": "/home/donghyeon/workspace/desktop-server-git/tech-log-backend",
"revision": "cc2a4887948669a41867ba8d0c146c1925f58434",
"verified": "main 의 HEAD 이고, 이 문서가 인용한 백엔드 커밋 48개가 전부 이 커밋의 조상이다 (2026-09-07 확인)"
},
{
"name": "tech-log-design-package",
"path": "/home/donghyeon/workspace/desktop-server-git/tech-log-backend/src/config/openapi/MANIFEST.sha256",
"revisions": {
"studio-v1.yaml": "ca1bbfe",
"public-v1.yaml": "1aae8dc",
"studio-management-v1.yaml": "ffa088b"
},
"verified": "설계 패키지 저장소는 이 기계에 없다. 반입된 계약의 MANIFEST.sha256 이 세 파일의 원본 리비전과 해시를 고정하고 있어 그것을 리비전으로 적는다 (2026-09-07 확인)"
}
],
"sourceRevision": "tech-log@2026-09-02",
"generatedAt": "2026-09-07",
"candidateScope": {
"document": "final/document.md",
"sections": [
"§1",
"§3",
"§4",
"§5",
"§6",
"§7",
"§8",
"§9",
"§10",
"§11",
"§12",
"§13",
"§14",
"§15",
"§16",
"§17"
],
"excluded": [
"§2 결함을 어떻게 갈랐나",
"부록 A 커밋 색인"
],
"excludedAnchorPattern": "#§(?:2|A)(?=[.\\-]|$)",
"note": "처음부터 한 편으로 쓴 글이라 제2부가 없다. §2 는 갈래를 가른 방법과 건수를 세는 기준이고 부록 A 는 커밋 색인이라 후보 자리가 아니다. §1 과 §15 는 후보 범위 안에 두었다 — §1.2 의 경계 열한 개와 §15 머리말의 가드 확인 절차가 각각 독립 기록이 된다"
},
"note": "이 프로젝트의 글감 전부다. 분해 계약이자 색인이고, 이 파일이 정본이다. 노드의 칸은 사람이 적고 file·publication·status 는 기록 파일에서 읽어 채운다 — python3 scripts/build-tech-log-tree.py TechLog",
"contract": {
"decomposition": [
"글감을 찾는 입력은 final/document.md 하나다. 거기에 없는 근거는 먼저 SSOT 에 넣는다.",
"후보 전부는 candidates 에 처분과 함께 남고 PROMOTE 만 topics 로 올라간다.",
"없애고 관련 Case 나 Concept 의 한 절로 넣어도 이해·결정·재사용성이 그대로라면 독립 기록으로 만들지 않는다.",
"Topic 은 독자 질문 하나다. 그 물음에 답하지 않는 글감은 다른 Topic 으로 옮긴다.",
"Concept 은 Case·Decision·Question 을 먼저 고른 뒤 그것을 이해하는 데 필요한 것만 거꾸로 더한다."
],
"readinessValues": [
"READY",
"OPEN",
"NEEDS_EVIDENCE",
"NEEDS_DECISION",
"BLOCKED"
],
"dispositionValues": {
"PROMOTE": "독립 Tech Log 로 쓴다",
"MERGE_INTO": "다른 기록의 한 절로 흡수한다",
"KEEP_IN_SSOT": "분석에는 남기고 독립 기록으로 만들지 않는다 — 정상적인 성공 결과다",
"NEEDS_EVIDENCE": "주장에 아직 검증이 없다",
"NEEDS_DECISION": "방향이 그럴듯하지만 프로젝트가 정하지 않았다",
"BLOCKED": "원본이 불완전하거나 서로 어긋난다"
}
},
"topics": {
"hand-listed-kinds": {
"title": "손으로 나열한 종류 목록",
"readerQuestion": "문서 종류를 하나 더했을 때, 왜 오류가 나지 않고 잘못된 값이 나가는가?",
"kinds": {
"case": [
{
"title": "개념을 하나 더하자 열세 곳이 그것을 조용히 삼켰다",
"slug": "one-new-kind-fell-through-thirteen-places",
"readiness": "READY",
"source": [
"final/document.md#§3.1",
"final/document.md#§3.2",
"final/document.md#§3.4",
"final/document.md#§10.3"
],
"code": [
"Record<PublicRecord[\"kind\"], string>",
"PublicSql.pathOf",
"LATEST_ENTRY_TYPES"
],
"evidence": [
"evidence/raw/guards/kind-tables-now.txt"
],
"classification": "증상 열셋이 전부 같은 원인 하나로 모이고, 표와 sealed switch 식으로 바꾼 뒤 계약 대조 가드가 실제로 잡는 것까지 확인해 닫힌다",
"missing-verification": "열세 건 중 지금 코드에서 되돌려 확인한 것은 계약 대조 가드 셋뿐이다. 나머지 열 건은 커밋 메시지와 현재 상태로만 확인했고 옛 증상은 재현하지 않았다",
"relations": [
"concept:exhaustive-switch-and-record-tables",
"reference:enumerate-kinds-where-the-compiler-sees-it",
"question:two-kind-tables-outside-the-compiler",
"case:an-operation-you-can-see-but-cannot-call"
],
"kind": "case",
"ssot-evidence": [
"raw/guards/kind-tables-now.txt"
],
"publication": "초안",
"file": "hand-listed-kinds/case/case-one-new-kind-fell-through-thirteen-places.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": [
"../../../final/evidence/raw/guards/kind-tables-now.txt"
]
}
],
"concept": [
{
"title": "컴파일러가 빠진 가지를 요구하게 만드는 두 가지 — Record 표와 sealed switch 식",
"slug": "exhaustive-switch-and-record-tables",
"readiness": "READY",
"source": [
"final/document.md#§3.1",
"final/document.md#§3.3"
],
"code": [
"Record<Kind, Value>",
"sealed",
"switch"
],
"basis-version": "TypeScript 5.x 의 Record<K, V> 키 전수 요구 · Java 21 sealed interface 와 switch 식 · 이 저장소의 RecordKind 다섯 값",
"classification": "같은 언어 안에서도 문으로 쓴 switch 는 아무것도 요구하지 않고 식으로 쓴 switch 는 요구한다는 갈림길이라, 이것을 먼저 알아야 §3 의 고친 방법이 왜 통하는지 읽힌다",
"relations": [
"case:one-new-kind-fell-through-thirteen-places",
"reference:enumerate-kinds-where-the-compiler-sees-it",
"concept:where-typescript-stops-checking"
],
"kind": "concept",
"publication": "초안",
"file": "hand-listed-kinds/concept/concept-exhaustive-switch-and-record-tables.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"reference": [
{
"title": "종류를 나열하는 곳은 컴파일러나 계약 대조 검사가 세게 만든다",
"slug": "enumerate-kinds-where-the-compiler-sees-it",
"readiness": "READY",
"source": [
"final/document.md#§3.5",
"final/document.md#§3.4",
"final/document.md#§17.2"
],
"classification": "열세 건이 같은 규칙 하나로 닫히고, 그 규칙이 종류가 아닌 다른 유한 집합 — 라우트, 오류 코드 — 에도 이 저장소에서 그대로 적용됐다",
"scope": "값이 유한한 집합인 것을 코드나 계약 여러 곳에서 분기하거나 열거하는 자리",
"exceptions": "그 칸이 집합 밖의 값도 담으면 표로 좁힐 수 없다. 공개 투영의 resource_type 이 그런 자리이고, 그때는 계약을 읽어 대조하는 검사를 대신 둔다",
"relations": [
"case:one-new-kind-fell-through-thirteen-places",
"concept:exhaustive-switch-and-record-tables",
"reference:compare-the-contract-with-both-implementations"
],
"kind": "reference",
"publication": "초안",
"file": "hand-listed-kinds/reference/reference-enumerate-kinds-where-the-compiler-sees-it.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"question": [
{
"title": "종류를 세는 곳 둘이 아직 컴파일러의 보호 밖에 있다",
"slug": "two-kind-tables-outside-the-compiler",
"readiness": "OPEN",
"source": [
"final/document.md#§16.7"
],
"evidence": [
"evidence/raw/guards/kind-tables-now.txt"
],
"known": "PublicSql.pathOf 는 RecordKind 가 아니라 공개 투영의 resource_type(String)으로 switch 하고 default -> null 이 남아 있다. validate-working-copy.ts 의 stringFields 는 아직 삼항 사슬이다. CONCEPT 이 실제로 pathOf 에서 빠져 있었다",
"unknown": "pathOf 가 읽는 칸은 RecordKind 에 더해 PROJECT 와 RELEASE 도 담는다. 그 둘을 어디로 옮기면 나머지를 sealed 로 좁힐 수 있는지",
"next-verification": "pathOf 를 RecordKind switch 와 PROJECT/RELEASE 분기로 갈라 보고, PublicPathsTest 가 닿지 않는 경로 — 홈 focus 의 recentDecision — 까지 덮이는지 본다",
"decision-criterion": "새 종류를 더했을 때 이 두 자리가 컴파일 오류로 먼저 멈추면 닫는다",
"relations": [
"case:one-new-kind-fell-through-thirteen-places",
"concept:exhaustive-switch-and-record-tables",
"case:an-address-frozen-at-publish-time"
],
"kind": "question",
"ssot-evidence": [
"raw/guards/kind-tables-now.txt"
],
"publication": "초안",
"file": "hand-listed-kinds/question/openquestion-two-kind-tables-outside-the-compiler.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": [
"../../../final/evidence/raw/guards/kind-tables-now.txt"
]
}
],
"decision": []
},
"topic": "hand-listed-kinds"
},
"declared-but-not-implemented": {
"title": "계약에 선언만 있고 구현이 없다",
"readerQuestion": "계약이 선언한 연산에 구현이 없으면 화면은 무엇으로 보이는가?",
"kinds": {
"case": [
{
"title": "계약에 선언만 있고 구현이 없어 화면 다섯 곳이 비어 있었다",
"slug": "five-screens-were-quietly-empty",
"readiness": "READY",
"source": [
"final/document.md#§4.1",
"final/document.md#§4.2",
"final/document.md#§10.4"
],
"code": [
"home_focus_config",
"public_resource_projection",
"JdbcCatalogQueryAdapter",
"HomeFocusView.resolve"
],
"classification": "빈 화면 다섯 곳이 전부 「구현이 없다」 하나로 모이고, 계약과 컨트롤러를 전수 대조하는 가드로 닫힌다",
"missing-verification": "홈 focus 는 세 슬롯이 다 비면 그 영역을 아예 그리지 않아 운영에서 한 번도 나타난 적이 없다. 지금 고쳐져 있는 것은 확인했지만 옛 증상은 재현할 수 없다",
"relations": [
"case:an-operation-you-can-see-but-cannot-call",
"reference:compare-the-contract-with-both-implementations",
"case:it-said-there-were-no-open-questions"
],
"kind": "case",
"publication": "초안",
"file": "declared-but-not-implemented/case/case-five-screens-were-quietly-empty.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
},
{
"title": "타입에는 보이는데 부를 수 없는 연산이 네 번 나왔다",
"slug": "an-operation-you-can-see-but-cannot-call",
"readiness": "READY",
"source": [
"final/document.md#§4.4"
],
"code": [
"tech-log-management-contract-contribution.ts",
"getPublicConcept",
"deleteConceptDraft"
],
"classification": "네 건이 전부 「계약에서 타입은 생성되는데 기여 목록에 등록하지 않았다」 하나이고, 공개 계약 전수 대조와 관리 계약 부분 대조 두 가드로 닫힌다",
"missing-verification": "관리 계약 쪽 가드는 「한 종류만 빠진 자리」만 본다. 종류가 아니라 연산 전체를 빠뜨리는 경우는 이 가드가 잡지 않는다",
"relations": [
"case:five-screens-were-quietly-empty",
"reference:compare-the-contract-with-both-implementations",
"case:a-narrow-implementation-satisfied-a-wide-port"
],
"kind": "case",
"publication": "초안",
"file": "declared-but-not-implemented/case/case-an-operation-you-can-see-but-cannot-call.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"concept": [],
"reference": [
{
"title": "계약과 구현은 서버와 화면 양쪽에서 전수 대조한다",
"slug": "compare-the-contract-with-both-implementations",
"readiness": "READY",
"source": [
"final/document.md#§4.3",
"final/document.md#§4.4"
],
"classification": "한쪽만 대조하면 다른 쪽을 지운 것이 잡히지 않는다는 것을 이 저장소에서 확인했고, 계약을 소유한 저장소가 따로 있는 어떤 구조에도 그대로 적용된다",
"scope": "계약이 한 곳에 있고 두 저장소가 그것을 반입해 각자 구현하는 구조",
"exceptions": "구현하지 않기로 한 연산은 이유와 함께 명시 목록에 넣는다. 「빠뜨린 것」과 구분되지 않으면 대조 결과가 곧 무시된다. 봉투 없이 바이트를 주는 연산도 면제 목록에 적는다",
"relations": [
"case:five-screens-were-quietly-empty",
"case:an-operation-you-can-see-but-cannot-call",
"reference:enumerate-kinds-where-the-compiler-sees-it"
],
"kind": "reference",
"publication": "초안",
"file": "declared-but-not-implemented/reference/reference-compare-the-contract-with-both-implementations.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"question": [],
"decision": []
},
"topic": "declared-but-not-implemented"
},
"values-lost-between-boundaries": {
"title": "값이 경계에서 사라진다",
"readerQuestion": "DB 에는 작성자가 쓴 값이 그대로 있는데 화면이 비었다면, 그 값은 어느 경계에서 사라졌는가?",
"kinds": {
"case": [
{
"title": "공개 Reference 가 통째로 비어 있었다 — 이름이 어긋났고 본문은 다른 테이블에 있었다",
"slug": "a-public-reference-was-entirely-empty",
"readiness": "READY",
"source": [
"final/document.md#§5.1",
"final/document.md#§6.2"
],
"code": [
"purposeSummary",
"scopeSummary",
"reference_detail",
"body_markdown"
],
"classification": "Studio 에서는 다 보이고 공개 화면만 빈 것을 원인 둘 — 계약에 없는 이름을 읽은 것과 본문이 다른 테이블에 있던 것 — 로 갈랐고, 값이 아니라 이름을 지키는 테스트로 닫힌다",
"missing-verification": "as 단언을 걷어낸 자리를 전수로 세지 않았다. 같은 모양이 남아 있는지는 확인하지 않았다",
"relations": [
"concept:eleven-boundaries-a-value-crosses",
"reference:a-missing-contract-field-has-a-signature",
"concept:where-typescript-stops-checking"
],
"kind": "case",
"publication": "초안",
"file": "values-lost-between-boundaries/case/case-a-public-reference-was-entirely-empty.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
},
{
"title": "관계의 요약이 경계 세 곳을 지나며 사라졌다",
"slug": "a-summary-vanished-at-three-boundaries",
"readiness": "READY",
"source": [
"final/document.md#§5.2",
"final/document.md#§5.3"
],
"code": [
"flattenRelations",
"ResolvedRelation",
"additionalProperties"
],
"classification": "한 경계를 고치고 「고쳤다」고 판단한 것이 두 번 틀렸고, 세 경계를 다 이은 뒤에야 값이 화면에 닿았다. 그 과정에서 한 칸에 뭉쳐 있던 셋을 갈라 닫는다",
"missing-verification": "라벨·문장·요약 셋으로 가른 뒤 각 경로에서 무엇이 나오는지는 화면으로 확인했고, 세 경로 전부를 자동 검사로 고정하지는 않았다",
"relations": [
"concept:eleven-boundaries-a-value-crosses",
"reference:verify-at-the-end-of-the-value-journey",
"case:a-list-item-had-to-carry-the-whole-document"
],
"kind": "case",
"publication": "초안",
"file": "values-lost-between-boundaries/case/case-a-summary-vanished-at-three-boundaries.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
},
{
"title": "결정에는 상세 화면이 없어 목록 항목이 문서 전체를 실어야 했다",
"slug": "a-list-item-had-to-carry-the-whole-document",
"readiness": "READY",
"source": [
"final/document.md#§5.4"
],
"code": [
"ProjectDecisionItem",
"statement",
"consequences"
],
"classification": "어긋난 네 가지가 「상세 endpoint 가 없다」는 구조 하나로 모이고, 목록 항목이 화면이 그리는 칸을 전부 싣게 해서 닫힌다",
"missing-verification": "DB 에 있던 제목·여러 줄 요약·영향 4건이 그대로였다는 것은 조회로 확인했고, 그 시점의 화면 캡처는 남기지 않았다",
"relations": [
"reference:a-missing-contract-field-has-a-signature",
"case:an-address-frozen-at-publish-time",
"case:a-summary-vanished-at-three-boundaries"
],
"kind": "case",
"publication": "초안",
"file": "values-lost-between-boundaries/case/case-a-list-item-had-to-carry-the-whole-document.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"concept": [
{
"title": "공개 화면 한 줄이 그려지기까지 값이 지나는 경계 열한 개",
"slug": "eleven-boundaries-a-value-crosses",
"readiness": "READY",
"source": [
"final/document.md#§1.2",
"final/document.md#§17.1"
],
"code": [
"public_resource_projection",
"http-public-content-gateway"
],
"basis-version": "tech-log-backend · tech-log-frontend 2026-09-02 시점 · OpenAPI 3.1 계약 3종을 반입해 쓰는 구조",
"classification": "이 문서의 결함 절반 이상이 「이 중 한 경계가 값을 버렸다」는 같은 모양이라, 경계의 수와 순서를 모르면 어느 절에서 무엇을 보고 있는지 위치를 잡을 수 없다",
"relations": [
"case:a-public-reference-was-entirely-empty",
"case:a-summary-vanished-at-three-boundaries",
"reference:verify-at-the-end-of-the-value-journey"
],
"kind": "concept",
"ssot-assets": [
"value-boundaries"
],
"publication": "초안",
"file": "values-lost-between-boundaries/concept/concept-eleven-boundaries-a-value-crosses.md",
"status": "게시 전",
"studioId": "",
"assets": [
"value-boundaries"
],
"assetFiles": [
"value-boundaries"
],
"evidenceFiles": []
}
],
"reference": [
{
"title": "Studio 에서는 보이는데 공개 쪽만 비면 그 사이에 계약이 있다",
"slug": "a-missing-contract-field-has-a-signature",
"readiness": "READY",
"source": [
"final/document.md#§5.6",
"final/document.md#§5.5"
],
"classification": "같은 신호가 여덟 건에서 같은 원인을 가리켰고, 두 표면이 각자의 계약으로 같은 데이터를 읽는 구조라면 어디서든 성립한다",
"scope": "같은 데이터를 두 표면이 각자의 계약으로 읽고, 한쪽만 비어 보이는 자리",
"exceptions": "두 표면이 같은 계약을 쓰면 이 신호는 성립하지 않는다. 칸을 더할 때 required 로 올릴지는 따로 판단한다 — 이미 나가 있는 응답을 깨지 않으려면 required 가 아니어야 한다",
"relations": [
"case:a-public-reference-was-entirely-empty",
"case:a-list-item-had-to-carry-the-whole-document",
"reference:verify-at-the-end-of-the-value-journey"
],
"kind": "reference",
"publication": "초안",
"file": "values-lost-between-boundaries/reference/reference-a-missing-contract-field-has-a-signature.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
},
{
"title": "한 경계를 고쳤으면 값의 여정 끝에서 확인한다",
"slug": "verify-at-the-end-of-the-value-journey",
"readiness": "READY",
"source": [
"final/document.md#§17.1",
"final/document.md#§5.2",
"final/document.md#§6.6"
],
"classification": "한 경계만 보고 「고쳤다」고 판단해 세 번 틀렸고, 값이 여러 번 갈아타는 구조라면 어디서든 같은 판단 오류가 난다",
"scope": "값이 계약·매퍼·포트를 여러 번 갈아타는 구조에서 「고쳤다」를 판단하는 자리",
"exceptions": "경계가 하나뿐이거나 고친 그 자리가 여정의 끝이면 중간 확인으로 충분하다",
"relations": [
"concept:eleven-boundaries-a-value-crosses",
"case:a-summary-vanished-at-three-boundaries",
"concept:where-typescript-stops-checking"
],
"kind": "reference",
"publication": "초안",
"file": "values-lost-between-boundaries/reference/reference-verify-at-the-end-of-the-value-journey.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"question": [],
"decision": []
},
"topic": "values-lost-between-boundaries"
},
"what-the-compiler-lets-through": {
"title": "타입 검사가 통과시키는 곳",
"readerQuestion": "「타입 검사가 통과했다」는 왜 반영의 증거가 아닌가?",
"kinds": {
"case": [
{
"title": "구현이 종류를 좁게 적어도 넓은 포트를 만족했다",
"slug": "a-narrow-implementation-satisfied-a-wide-port",
"readiness": "READY",
"source": [
"final/document.md#§6.1"
],
"code": [
"deleteDocument",
"RecordFilters",
"satisfies"
],
"classification": "타입 통과를 반영으로 읽은 판단이 왜 틀렸는지를 bivariance 하나로 확정했고, 타입을 하나로 합치고 되돌려 깨지는 것을 확인해 닫는다",
"missing-verification": "배포된 번들에 옛 삼항이 남아 있던 것은 서버 로그의 404 로 확인했다. 그 로그 원문은 이 저장소에 없다",
"relations": [
"concept:where-typescript-stops-checking",
"case:an-operation-you-can-see-but-cannot-call",
"reference:verify-at-the-end-of-the-value-journey"
],
"kind": "case",
"publication": "초안",
"file": "what-the-compiler-lets-through/case/case-a-narrow-implementation-satisfied-a-wide-port.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
},
{
"title": "npx tsc --noEmit 이 한 파일도 검사하지 않고 성공했다",
"slug": "the-typecheck-command-checked-no-files",
"readiness": "READY",
"source": [
"final/document.md#§6.4"
],
"code": [
"\"files\": []",
"npm run check:types"
],
"classification": "운영에서 난 ReferenceError 의 원인이 코드가 아니라 검사 명령이었음을 확정했고, 올바른 명령으로 돌려 저장소에 남아 있던 다른 오류까지 드러나며 닫힌다",
"missing-verification": "루트 tsconfig 를 그대로 두었으므로 누군가 다시 그 명령을 쓰는 것을 막는 장치는 없다. 메모리에 남긴 것이 전부다",
"relations": [
"concept:where-typescript-stops-checking",
"case:the-guard-worked-and-i-did-not-run-it",
"reference:what-a-person-must-run-before-deploying"
],
"kind": "case",
"publication": "초안",
"file": "what-the-compiler-lets-through/case/case-the-typecheck-command-checked-no-files.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"concept": [
{
"title": "TypeScript 가 검사를 놓아 주는 네 자리 — 메서드 매개변수의 bivariance · as 단언 · never 캐스트 · 검사 대상을 갖지 않은 tsconfig",
"slug": "where-typescript-stops-checking",
"readiness": "READY",
"source": [
"final/document.md#§6.6",
"final/document.md#§6.2",
"final/document.md#§6.3"
],
"code": [
"as string",
"(input: never)",
"project references"
],
"basis-version": "TypeScript 5.x · project references 로 나눈 여섯 프로젝트 구성 · 2026-09-02 시점의 tech-log-frontend",
"classification": "네 자리가 각각 다른 결함을 통과시켰고, 이 네 가지를 알아야 「타입 통과」를 무엇의 증거로 읽을지 정할 수 있다",
"relations": [
"case:a-narrow-implementation-satisfied-a-wide-port",
"case:the-typecheck-command-checked-no-files",
"case:a-public-reference-was-entirely-empty"
],
"kind": "concept",
"publication": "초안",
"file": "what-the-compiler-lets-through/concept/concept-where-typescript-stops-checking.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"reference": [],
"question": [],
"decision": []
},
"topic": "what-the-compiler-lets-through"
},
"seams-no-test-crosses": {
"title": "테스트가 지나지 않는 이음매",
"readerQuestion": "모든 검사가 통과했는데 운영에서 깨졌다면, 어느 이음매를 아무 테스트도 지나지 않았는가?",
"kinds": {
"case": [
{
"title": "파드가 두 번 CrashLoopBackOff 로 들어갔다 — 어떤 테스트도 애플리케이션 컨텍스트를 띄우지 않았다",
"slug": "two-pods-crashlooped-with-no-test-starting-the-context",
"readiness": "READY",
"source": [
"final/document.md#§7.1",
"final/document.md#§6.5",
"final/document.md#§12.1"
],
"code": [
"@Autowired",
"tools.jackson.databind",
"com.fasterxml.jackson.databind.ObjectMapper"
],
"classification": "원인이 다른 두 건 — 생성자 둘, Jackson 2 import — 이 「컨텍스트를 띄우는 테스트가 없다」 하나로 모이고, ArchUnit 규칙을 결함으로 되돌려 확인해 닫힌다",
"missing-verification": "D20 규칙은 생성자 쪽만 막는다. 클래스패스에 남은 옛 라이브러리 타입을 import 하는 것은 이 규칙이 잡지 않고, 그 자리를 막는 검사는 아직 없다",
"relations": [
"reference:put-one-check-on-each-seam",
"concept:where-typescript-stops-checking",
"case:the-sql-had-never-been-executed"
],
"kind": "case",
"publication": "초안",
"file": "seams-no-test-crosses/case/case-two-pods-crashlooped-with-no-test-starting-the-context.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
},
{
"title": "그 SQL 은 한 번도 실행된 적이 없었다",
"slug": "the-sql-had-never-been-executed",
"readiness": "READY",
"source": [
"final/document.md#§7.2"
],
"code": [
"public_resource_projection",
"resource_type",
"resource_id"
],
"classification": "500 의 원인이 컬럼 이름 하나였고, 진짜 실패는 그 SQL 이 한 번도 실행된 적이 없다는 것이었다. 삭제 경로 전용 통합 테스트 태스크로 닫힌다",
"missing-verification": "여덟 시나리오를 실제 PostgreSQL 에서 돌렸지만, 표준 check 는 여전히 Testcontainers 를 띄우지 않는다. 새 어댑터 SQL 이 그 태스크에 등록되는지를 보는 장치는 없다",
"relations": [
"reference:put-one-check-on-each-seam",
"case:two-pods-crashlooped-with-no-test-starting-the-context",
"question:the-refusal-does-not-name-what-blocks-it"
],
"kind": "case",
"publication": "초안",
"file": "seams-no-test-crosses/case/case-the-sql-had-never-been-executed.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
},
{
"title": "합성 루트에 테스트가 없어 공개 사이트 전체가 오류 화면이었다",
"slug": "the-composition-root-had-no-test",
"readiness": "READY",
"source": [
"final/document.md#§7.4",
"final/document.md#§7.3"
],
"code": [
"attachCredentials",
"envelopeError",
"ApiError"
],
"classification": "세 결함이 겹쳐 각각 다음 것을 가리고 있었고, 실제 어댑터를 배포된 백엔드의 실제 404 본문에 대고 조립하는 회귀 테스트로 닫힌다",
"missing-verification": "공개 소스가 HTTP 가 된 뒤에야 나타날 수 있던 경로다. 다른 프로파일의 합성 루트에도 같은 구멍이 있는지는 세지 않았다",
"relations": [
"reference:put-one-check-on-each-seam",
"case:records-disappeared-without-a-trace",
"reference:verify-at-the-end-of-the-value-journey"
],
"kind": "case",
"publication": "초안",
"file": "seams-no-test-crosses/case/case-the-composition-root-had-no-test.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
},
{
"title": "생성기가 계약 필드 넷을 조용히 빠뜨렸다 — 파생 스펙에 남은 YAML alias 34곳",
"slug": "the-generator-dropped-four-contract-fields",
"readiness": "READY",
"source": [
"final/document.md#§7.6"
],
"code": [
"LatestEntry",
"publishedAt",
"validateSpec",
"verifyPublicGeneratedModels"
],
"classification": "스키마 15개가 거절당한 원인을 prepare 단계의 Map 재사용까지 따라갔고, alias 를 원천 차단하는 fail-closed 게이트와 property 단위 대조로 닫힌다",
"missing-verification": "지금 세는 것은 schema 62개 · property 250개다. 이 수를 갱신하는 것은 사람이라, 계약이 줄어드는 방향의 누락은 이 게이트가 잡지 않는다",
"relations": [
"reference:put-one-check-on-each-seam",
"reference:compare-the-contract-with-both-implementations",
"case:a-list-item-had-to-carry-the-whole-document"
],
"kind": "case",
"publication": "초안",
"file": "seams-no-test-crosses/case/case-the-generator-dropped-four-contract-fields.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"concept": [],
"reference": [
{
"title": "이음매마다 그 이음매를 실제로 지나는 검사를 하나씩 둔다",
"slug": "put-one-check-on-each-seam",
"readiness": "READY",
"source": [
"final/document.md#§7.7"
],
"classification": "다섯 이음매가 각각 「지나지 않았다」는 같은 이유로 통과했고, 층을 스텁으로 나눠 시험하는 어떤 구조에도 같은 목록이 만들어진다",
"scope": "스텁으로 나뉜 층 사이 — 스프링 컨텍스트, persistence SQL, HTTP 매퍼, 합성 루트, 코드 생성기",
"exceptions": "그 층을 실제로 지나는 검사가 이미 있으면 더 두지 않는다. 스텁을 쓰는 테스트를 늘리는 것은 이 문제를 덮지 않는다",
"relations": [
"case:two-pods-crashlooped-with-no-test-starting-the-context",
"case:the-sql-had-never-been-executed",
"case:the-composition-root-had-no-test",
"case:the-generator-dropped-four-contract-fields"
],
"kind": "reference",
"publication": "초안",
"file": "seams-no-test-crosses/reference/reference-put-one-check-on-each-seam.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"question": [],
"decision": []
},
"topic": "seams-no-test-crosses"
},
"one-route-many-hand-kept-lists": {
"title": "라우트 하나가 울리는 손 목록",
"readerQuestion": "라우트를 하나 더하면 어디가 함께 울리고, 왜 어떤 것은 배포 뒤에야 우는가?",
"kinds": {
"case": [
{
"title": "nginx 가 모르는 라우트는 새로고침에서 404 다",
"slug": "a-route-the-web-server-never-heard-of",
"readiness": "READY",
"source": [
"final/document.md#§8.2",
"final/document.md#§12.6"
],
"code": [
"tech-log-serving-contract.json",
"location",
"robots.txt"
],
"classification": "SPA 안에서는 닿는데 하드 로드만 404 인 이유를 서빙 계약의 손 목록으로 확정했고, 라우트 계약에서 패턴을 유도하게 해서 닫힌다",
"missing-verification": "경로 27개가 얼어 있던 상태의 생성 결과물은 남기지 않았다. 지금 생성되는 패턴이 라우트 계약과 같은지만 테스트가 본다",
"relations": [
"reference:derive-the-route-lists-from-the-route-contract",
"decision:do-not-translate-the-catch-all-route",
"case:eight-places-a-single-route-touches"
],
"kind": "case",
"publication": "초안",
"file": "one-route-many-hand-kept-lists/case/case-a-route-the-web-server-never-heard-of.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
},
{
"title": "라우트 하나가 건드리는 여덟 곳과, 그것들이 우는 시점",
"slug": "eight-places-a-single-route-touches",
"readiness": "READY",
"source": [
"final/document.md#§8.1",
"final/document.md#§8.3",
"final/document.md#§8.4"
],
"code": [
"tech-log-route-contract.ts",
"TECH_LOG_STUDIO_TOPIC_EDIT",
"FE-GATE-009"
],
"classification": "여덟 곳을 목록으로 확정하고 각각이 우는 시점 — 빌드 직전, 배포 직전, 배포 뒤 — 까지 갈랐다. 유도할 수 있는 것은 유도하고 기준값은 재현 절차로 닫는다",
"missing-verification": "게이트 기준값 셋은 여전히 손으로 움직인다. 옛 값을 먼저 재현하는 절차는 사람이 기억해야 하고 검사가 강제하지 않는다",
"relations": [
"reference:derive-the-route-lists-from-the-route-contract",
"question:nobody-signed-the-accessibility-evidence",
"case:the-guard-worked-and-i-did-not-run-it"
],
"kind": "case",
"publication": "초안",
"file": "one-route-many-hand-kept-lists/case/case-eight-places-a-single-route-touches.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"concept": [],
"reference": [
{
"title": "라우트에 딸린 목록은 라우트 계약에서 유도하고, 유도할 수 없는 것은 대조 검사를 둔다",
"slug": "derive-the-route-lists-from-the-route-contract",
"readiness": "READY",
"source": [
"final/document.md#§8.3",
"final/document.md#§8.2"
],
"classification": "같은 목록이 손으로 유지되는 동안 두 번 같은 방식으로 실패했고, 라우트가 여러 목록을 함께 움직이는 어떤 프론트엔드에도 적용된다",
"scope": "라우트 하나가 서빙 패턴·청크 이름·게이트 기준값 같은 목록을 함께 움직이는 구조",
"exceptions": "기준값처럼 유도할 수 없는 상수는 옛 값을 먼저 재현해 계산 방법을 확인한 뒤 갱신한다. 그 절차 없이 새 값을 적으면 계산이 바뀐 것과 파일이 바뀐 것을 구분할 수 없다",
"relations": [
"case:a-route-the-web-server-never-heard-of",
"case:eight-places-a-single-route-touches",
"reference:enumerate-kinds-where-the-compiler-sees-it"
],
"kind": "reference",
"publication": "초안",
"file": "one-route-many-hand-kept-lists/reference/reference-derive-the-route-lists-from-the-route-contract.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"question": [
{
"title": "라우트마다 요구하는 수동 접근성 증거를 아무도 서명하지 않았다",
"slug": "nobody-signed-the-accessibility-evidence",
"readiness": "OPEN",
"source": [
"final/document.md#§16.5",
"final/document.md#§8.4"
],
"known": "FE-GATE-009 는 설치된 라우트마다 증거 파일 하나를 요구하고 집합이 정확히 일치하지 않으면 거절한다. artifacts/tests/a11y-manual/*.md 는 전부 pending-manual-review 이고, review:a11y-manual 이 실패하는 것이 지금은 정상이다",
"unknown": "게이트가 파일의 존재만 세고 서명을 보지 않는 것이 의도인지, 서명을 게이트 조건에 넣어 라우트를 더할 때마다 배포가 막히는 것을 감수할지",
"next-verification": "라우트 하나를 실제로 사람이 검토해 서명한 뒤 게이트가 서명 여부까지 보게 고쳐, 나머지 라우트에서 실제로 빨개지는지 확인한다",
"decision-criterion": "서명이 게이트의 통과 조건이 되거나, 서명을 요구하지 않기로 적고 게이트에서 그 파일 요구를 빼면 닫는다",
"relations": [
"case:eight-places-a-single-route-touches",
"reference:revert-the-defect-and-watch-the-guard-fail",
"case:the-guard-worked-and-i-did-not-run-it"
],
"kind": "question",
"publication": "초안",
"file": "one-route-many-hand-kept-lists/question/openquestion-nobody-signed-the-accessibility-evidence.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"decision": [
{
"title": "catch-all 라우트는 nginx 패턴으로 번역하지 않는다",
"slug": "do-not-translate-the-catch-all-route",
"readiness": "READY",
"source": [
"final/document.md#§8.2"
],
"decision-status": "ADOPTED",
"decision-evidence": "라우트 계약에서 유도한 정규식이 등록된 Public 라우트만 열고 파라미터는 한 세그먼트만 잡아, /cases/a/b 가 404 로 남는 것을 배포 뒤 감사에서 확인했다 — evidence/raw/audit/dead-link-sweep.txt",
"grounds": "모든 미매치 URL 에 index.html 을 주면 엣지 404 가 soft 200 이 되어 깨진 링크를 크롤러와 우리에게서 숨긴다. 감수한 비용은 라우트를 더할 때마다 서빙 패턴이 함께 움직인다는 것이고, 그 비용은 계약에서 유도해 없앴다",
"classification": "프로젝트가 실제로 이 방향을 골랐고 그 대가를 적었다",
"evidence": [
"evidence/raw/audit/dead-link-sweep.txt"
],
"relations": [
"case:a-route-the-web-server-never-heard-of",
"reference:derive-the-route-lists-from-the-route-contract",
"reference:do-not-draw-a-link-that-does-not-resolve"
],
"kind": "decision",
"ssot-evidence": [
"raw/audit/dead-link-sweep.txt"
],
"publication": "초안",
"file": "one-route-many-hand-kept-lists/decision/decision-do-not-translate-the-catch-all-route.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": [
"../../../final/evidence/raw/audit/dead-link-sweep.txt"
]
}
]
},
"topic": "one-route-many-hand-kept-lists"
},
"addresses-frozen-at-publish-time": {
"title": "주소가 만들어지고 굳어지는 곳",
"readerQuestion": "공개 주소는 누가 언제 만들고, 그것이 틀리면 어디까지 번지는가?",
"kinds": {
"case": [
{
"title": "계약은 앵커라고 적었고 만드는 쪽은 경로를 만들었다 — 이미 저장된 행까지 고쳤다",
"slug": "an-address-frozen-at-publish-time",
"readiness": "READY",
"source": [
"final/document.md#§9.2"
],
"code": [
"PublicPaths",
"PublicSql",
"public_route"
],
"evidence": [
"evidence/raw/db/decision-path-after-v15.txt",
"evidence/raw/api/decision-anchor-fixed.txt",
"evidence/raw/audit/dead-link-sweep.txt"
],
"classification": "계약이 이미 앵커라고 적어 둔 것을 만드는 쪽 두 곳이 따르지 않았다는 것을 확정했고, 코드와 이미 게시된 행을 함께 고친 뒤 서버가 내보내는 주소 35개를 전수로 확인해 닫힌다",
"missing-verification": "전수 감사는 서버가 내보내는 주소만 본다. 본문 안에 작성자가 손으로 쓴 링크는 이 감사의 대상이 아니다",
"relations": [
"reference:do-not-draw-a-link-that-does-not-resolve",
"case:a-list-item-had-to-carry-the-whole-document",
"case:a-link-that-pointed-at-itself"
],
"kind": "case",
"ssot-assets": [
"decision-path-404"
],
"ssot-evidence": [
"raw/db/decision-path-after-v15.txt",
"raw/api/decision-anchor-fixed.txt",
"raw/audit/dead-link-sweep.txt",
"raw/audit/link-audit.py"
],
"publication": "초안",
"file": "addresses-frozen-at-publish-time/case/case-an-address-frozen-at-publish-time.md",
"status": "게시 전",
"studioId": "",
"assets": [
"decision-path-404"
],
"assetFiles": [
"decision-path-404"
],
"evidenceFiles": [
"../../../final/evidence/raw/db/decision-path-after-v15.txt",
"../../../final/evidence/raw/api/decision-anchor-fixed.txt",
"../../../final/evidence/raw/audit/dead-link-sweep.txt",
"../../../final/evidence/raw/audit/link-audit.py"
]
},
{
"title": "축 링크가 자기 자신을 가리켰고, 고친 뒤에는 백엔드를 먼저 배포했다",
"slug": "a-link-that-pointed-at-itself",
"readiness": "READY",
"source": [
"final/document.md#§9.1",
"final/document.md#§9.3"
],
"code": [
"record_variant",
"variant"
],
"classification": "주소를 두 번 옮겨 본 끝에 축에 자기 화면을 주는 것으로 닫혔고, 그 과정에서 배포 순서 때문에 생긴 2차 사고까지 원인이 확정된다",
"missing-verification": "축 slug 가 주제 안에서만 유일하다는 전제를 조회에 반영했지만, 주제를 빼고 조회하는 경로가 남아 있는지는 세지 않았다",
"relations": [
"decision:a-new-route-ships-frontend-first",
"reference:write-down-what-would-undo-a-workaround",
"concept:topic-variant-and-record-variant"
],
"kind": "case",
"publication": "초안",
"file": "addresses-frozen-at-publish-time/case/case-a-link-that-pointed-at-itself.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
},
{
"title": "slug 생성이 한글을 버려 주제 만들기가 간헐적으로 실패했다",
"slug": "a-slug-rule-that-threw-korean-away",
"readiness": "READY",
"source": [
"final/document.md#§13.6"
],
"code": [
"[a-z0-9]",
"baekendeu-akitekcheo"
],
"classification": "「간헐적」이 실은 두 가지 결정적 어긋남이었음을 확정했고, 음절을 초성·중성·종성으로 산술 분해해 로마자로 옮기는 것으로 닫힌다",
"missing-verification": "음운 변화 규칙을 일부러 뺐으므로 같은 발음의 다른 이름이 다른 slug 가 된다. 그 충돌 빈도는 재지 않았다",
"relations": [
"case:an-address-frozen-at-publish-time",
"case:nine-names-for-five-kinds",
"reference:do-not-draw-a-link-that-does-not-resolve"
],
"kind": "case",
"publication": "초안",
"file": "addresses-frozen-at-publish-time/case/case-a-slug-rule-that-threw-korean-away.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"concept": [],
"reference": [
{
"title": "서버가 준 주소는 라우트 표에 맞춰 보고, 맞는 라우트가 없으면 링크로 그리지 않는다",
"slug": "do-not-draw-a-link-that-does-not-resolve",
"readiness": "READY",
"source": [
"final/document.md#§9.2"
],
"classification": "같은 부류가 또 생겨도 방문자가 404 를 만나지는 않게 하는 두 겹 가드로 닫혔고, 주소를 서버가 만들어 내보내는 어떤 구조에도 적용된다",
"scope": "주소를 서버가 만들어 내보내고 화면이 그것을 그대로 링크로 그리는 자리",
"exceptions": "외부 주소는 라우트 표에 없으므로 이 대조의 대상이 아니다. 만드는 쪽에도 같은 검사를 두어야 링크가 아예 안 그려지는 것으로 끝나지 않는다",
"relations": [
"case:an-address-frozen-at-publish-time",
"decision:do-not-translate-the-catch-all-route",
"reference:derive-the-route-lists-from-the-route-contract"
],
"kind": "reference",
"publication": "초안",
"file": "addresses-frozen-at-publish-time/reference/reference-do-not-draw-a-link-that-does-not-resolve.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
},
{
"title": "우회를 남길 때는 되돌릴 조건을 함께 적는다",
"slug": "write-down-what-would-undo-a-workaround",
"readiness": "READY",
"source": [
"final/document.md#§9.4"
],
"classification": "우회 자체는 틀리지 않았고 남겨 두는 것이 문제였다는 것을 뒤집힌 판단 하나로 확정했으며, 조건을 커밋 메시지에 적어 두고 실제로 그 조건에서 되돌리며 닫혔다",
"scope": "그 화면이 줄 수 있는 것이 아직 비어 있어 링크를 다른 자리로 돌리는 임시 조치",
"exceptions": "되돌릴 생각이 없는 변경은 우회가 아니라 결정이다. 그때는 조건이 아니라 근거와 감수한 비용을 적는다",
"relations": [
"case:a-link-that-pointed-at-itself",
"case:the-comparison-band-changed-three-times",
"decision:a-new-route-ships-frontend-first"
],
"kind": "reference",
"publication": "초안",
"file": "addresses-frozen-at-publish-time/reference/reference-write-down-what-would-undo-a-workaround.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"question": [],
"decision": [
{
"title": "새 라우트는 프론트엔드를 먼저 배포한다",
"slug": "a-new-route-ships-frontend-first",
"readiness": "READY",
"source": [
"final/document.md#§9.1"
],
"decision-status": "ADOPTED",
"decision-evidence": "축 화면을 만들고 백엔드를 먼저 배포했더니 서버는 이미 그 주소를 내보내는데 엣지에는 그 경로가 없어, 사용자가 네 링크 전부 404 인 화면을 봤다 — final/document.md#§9.1",
"grounds": "nginx 설정이 라우트 계약에서 생성되므로 프론트가 배포되기 전까지 그 경로는 엣지에서 404 다. 감수한 비용은 새 주소를 내보내는 백엔드 변경이 한 배포 늦게 나간다는 것이다",
"classification": "프로젝트가 이 순서를 실제로 택했고 그 전에 반대 순서로 사고를 냈다",
"relations": [
"case:a-link-that-pointed-at-itself",
"case:a-route-the-web-server-never-heard-of",
"reference:do-not-draw-a-link-that-does-not-resolve"
],
"kind": "decision",
"publication": "초안",
"file": "addresses-frozen-at-publish-time/decision/decision-a-new-route-ships-frontend-first.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
]
},
"topic": "addresses-frozen-at-publish-time"
},
"failure-drawn-as-absence": {
"title": "실패를 없음으로 그린다",
"readerQuestion": "화면이 못 읽은 것을 「없다」고 그리면 무엇이 잘못되는가?",
"kinds": {
"case": [
{
"title": "「이 프로젝트에 열린 질문이 없습니다」 — 실제로는 넷이 있었다",
"slug": "it-said-there-were-no-open-questions",
"readiness": "READY",
"source": [
"final/document.md#§10.1"
],
"code": [
"home focus",
"getHomeFocus"
],
"classification": "빈 배열로 삼킨 실패가 작성자에게 「아직 안 쓴 글」로 읽혔다는 것을 확정했고, 못 읽었다고 말하게 해서 닫힌다",
"missing-verification": "화면이 못 읽은 것을 없다고 그리는 자리를 전수로 세지 않았다. 고친 것은 홈 편집기와 주제 탭 둘이다",
"relations": [
"reference:say-you-could-not-read-it",
"case:five-screens-were-quietly-empty",
"case:one-cell-failing-took-its-neighbour-down"
],
"kind": "case",
"publication": "초안",
"file": "failure-drawn-as-absence/case/case-it-said-there-were-no-open-questions.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
},
{
"title": "한 칸의 실패가 옆 칸을 끌고 내려갔다",
"slug": "one-cell-failing-took-its-neighbour-down",
"readiness": "READY",
"source": [
"final/document.md#§10.2"
],
"code": [
"Promise.all"
],
"classification": "묶어 읽은 것이 원인이라는 것과, 호출이 동기적으로 던지면 rejection handler 를 지나지도 못한다는 더 미묘한 변종까지 갈랐고, 둘을 따로 읽게 해서 닫힌다",
"missing-verification": "같은 모양으로 묶어 읽는 자리를 전수로 세지 않았다. 홈 편집기와 주제 탭 둘에만 적용했다",
"relations": [
"reference:say-you-could-not-read-it",
"case:it-said-there-were-no-open-questions",
"case:the-comparison-band-changed-three-times"
],
"kind": "case",
"publication": "초안",
"file": "failure-drawn-as-absence/case/case-one-cell-failing-took-its-neighbour-down.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
},
{
"title": "매퍼가 null 을 돌려주고 호출부가 걸러 내, 기록이 조용히 사라졌다",
"slug": "records-disappeared-without-a-trace",
"readiness": "READY",
"source": [
"final/document.md#§10.6"
],
"code": [
"filter",
"CASE",
"REFERENCE"
],
"classification": "응답 모양이 다른 목록에 다른 목록의 매퍼를 쓴 것이 원인이고, 오류도 빈 자리도 남기지 않고 목록이 한 줄 짧아질 뿐이라는 관측까지 확정된다",
"missing-verification": "null 을 돌려주고 호출부가 거르는 매퍼가 다른 목록에도 남아 있는지는 세지 않았다",
"relations": [
"reference:say-you-could-not-read-it",
"case:one-new-kind-fell-through-thirteen-places",
"case:the-composition-root-had-no-test"
],
"kind": "case",
"publication": "초안",
"file": "failure-drawn-as-absence/case/case-records-disappeared-without-a-trace.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"concept": [],
"reference": [
{
"title": "화면은 못 읽은 것을 없다고 말하지 않는다",
"slug": "say-you-could-not-read-it",
"readiness": "READY",
"source": [
"final/document.md#§10.1",
"final/document.md#§17.3"
],
"classification": "같은 규칙이 목록·탭·편집기 세 자리에서 같은 결함을 막았고, 「비어 있음」이 정상값인 어떤 화면에도 적용된다",
"scope": "목록·요약처럼 「비어 있음」이 정상값이라 실패와 구분되지 않는 자리",
"exceptions": "정말로 0건인 것과 못 읽은 것을 구분할 수 없는 자리라면 그 구분을 먼저 만든다. 구분 없이 문구만 바꾸면 0건이 실패로 읽힌다",
"relations": [
"case:it-said-there-were-no-open-questions",
"case:one-cell-failing-took-its-neighbour-down",
"case:records-disappeared-without-a-trace"
],
"kind": "reference",
"publication": "초안",
"file": "failure-drawn-as-absence/reference/reference-say-you-could-not-read-it.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
},
{
"title": "매번 우는 검사는 읽히지 않는다 — 기대된 실패는 조건을 적어 빼고 나머지는 전부 실패시킨다",
"slug": "an-expected-failure-must-not-be-counted-as-a-failure",
"readiness": "READY",
"source": [
"final/document.md#§10.5"
],
"classification": "같은 빨간 줄이 매번 남아 읽히지 않게 된 것을 확정했고, 기대된 두 응답을 조건으로 빼고 나머지 4xx·5xx 를 전부 실패로 세는 것으로 닫힌다",
"scope": "배포 뒤 전 화면을 훑는 스모크 스윕처럼 결과를 사람이 훑어보는 검사",
"exceptions": "기대된 실패의 조건을 적을 수 없으면 빼지 않는다. 조건 없이 빼면 진짜 실패도 같이 빠진다",
"relations": [
"reference:say-you-could-not-read-it",
"reference:revert-the-defect-and-watch-the-guard-fail",
"case:an-address-frozen-at-publish-time"
],
"kind": "reference",
"publication": "초안",
"file": "failure-drawn-as-absence/reference/reference-an-expected-failure-must-not-be-counted-as-a-failure.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"question": [],
"decision": []
},
"topic": "failure-drawn-as-absence"
},
"css-rules-that-leak": {
"title": "CSS 규칙이 구역을 넘어 샌다",
"readerQuestion": "「디자인이 안 된 것처럼 보인다」는 보고는 왜 대부분 선택자 문제인가?",
"kinds": {
"case": [
{
"title": "구역 전체에 건 격자가 제목까지 잡아 h2 높이가 199px 이 됐다",
"slug": "a-section-wide-rule-caught-the-heading",
"readiness": "READY",
"source": [
"final/document.md#§11.1",
"final/document.md#§11.3",
"final/document.md#§16.3"
],
"code": [
".home-comparison a",
"grid-template-columns",
"li > a"
],
"classification": "세 건이 전부 「규칙이 샜다」 하나로 모이고, 선택자를 좁히고 구조까지 바꾼 뒤 배치 규칙의 범위를 보는 검사로 닫힌다",
"missing-verification": "CSS module 을 쓰는 화면은 전역 규칙이 닿지 않아 따로 고쳤고, 「지금 집중하는 것」 탭과 주제 탭의 표시 방식이 아직 다르다. 한 화면 안에 두 언어가 남아 있다",
"relations": [
"reference:scope-a-layout-rule-to-what-uses-it",
"case:the-rule-was-not-missing-it-was-half-there",
"reference:measure-what-is-visible-not-a-proxy"
],
"kind": "case",
"publication": "초안",
"file": "css-rules-that-leak/case/case-a-section-wide-rule-caught-the-heading.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
},
{
"title": "규칙이 없었던 게 아니라 절반만 있었다",
"slug": "the-rule-was-not-missing-it-was-half-there",
"readiness": "READY",
"source": [
"final/document.md#§11.2"
],
"code": [
".section-heading-row h2",
"font-size",
"font-weight"
],
"classification": "한 구역만 급이 낮아 보인 이유가 크기만 각자 적고 굵기를 아무도 정하지 않은 것임을 확정했고, 나란히 서는 제목들이 정본과 같은 값을 쓰는지 보는 검사로 닫힌다",
"missing-verification": "처음에 격자와 열만 재고 정상이라 답했다가 전체 페이지 스크린샷을 찍고서야 봤다. 지금 검사가 보는 것은 CSS 값이고, 실제로 그려진 크기는 아니다",
"relations": [
"reference:measure-what-is-visible-not-a-proxy",
"case:a-section-wide-rule-caught-the-heading",
"reference:scope-a-layout-rule-to-what-uses-it"
],
"kind": "case",
"publication": "초안",
"file": "css-rules-that-leak/case/case-the-rule-was-not-missing-it-was-half-there.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"concept": [],
"reference": [
{
"title": "배치를 거는 규칙은 그 배치를 쓰는 요소까지 좁혀 적는다",
"slug": "scope-a-layout-rule-to-what-uses-it",
"readiness": "READY",
"source": [
"final/document.md#§11.1"
],
"classification": "같은 모양이 다른 구역 아홉 곳에도 있어 선택자 51개를 고쳤고, 구역 클래스 아래에 태그 선택자로 배치를 거는 어떤 CSS 에도 적용된다",
"scope": "구역 클래스 아래에 태그 선택자로 display·grid-template-columns·padding 을 거는 자리",
"exceptions": "색이나 글꼴만 거는 규칙은 새어도 티가 나지 않으므로 대상이 아니다. 이미 좁혀 둔 자리는 명시해 검사에서 뺀다",
"relations": [
"case:a-section-wide-rule-caught-the-heading",
"case:the-rule-was-not-missing-it-was-half-there",
"reference:measure-what-is-visible-not-a-proxy"
],
"kind": "reference",
"publication": "초안",
"file": "css-rules-that-leak/reference/reference-scope-a-layout-rule-to-what-uses-it.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
},
{
"title": "프록시 지표가 아니라 보이는 것을 측정한다",
"slug": "measure-what-is-visible-not-a-proxy",
"readiness": "READY",
"source": [
"final/document.md#§17.5",
"final/document.md#§11.2"
],
"evidence": [
"evidence/browser/tab-metrics.txt",
"evidence/browser/home-topic-tabs.png"
],
"classification": "격자와 열만 재고 정상이라 답한 것이 두 번 틀렸고, 촬영을 스크립트로 고정해 폭마다 측정을 함께 남기는 것으로 닫힌다",
"scope": "화면이 잘못됐다는 보고를 확인하는 자리",
"exceptions": "측정 대상이 좌표나 간격 자체라면 바운딩 박스는 프록시가 아니라 대상이다",
"relations": [
"case:the-rule-was-not-missing-it-was-half-there",
"case:a-section-wide-rule-caught-the-heading",
"case:the-comparison-band-changed-three-times"
],
"kind": "reference",
"ssot-evidence": [
"browser/tab-metrics.txt"
],
"publication": "초안",
"file": "css-rules-that-leak/reference/reference-measure-what-is-visible-not-a-proxy.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": [
"../../../final/evidence/browser/tab-metrics.txt"
]
}
],
"question": [],
"decision": []
},
"topic": "css-rules-that-leak"
},
"only-visible-after-deploying": {
"title": "배포해 봐야 드러난 것",
"readerQuestion": "컨테이너가 healthy 인데 사이트가 동작하지 않으면, 무엇이 배포 산출물과 어긋나 있는가?",
"kinds": {
"case": [
{
"title": "컨테이너는 healthy 였고 SPA 가 부팅에 필요한 파일 하나만 403 이었다",
"slug": "a-healthy-container-that-served-one-403",
"readiness": "READY",
"source": [
"final/document.md#§12.4",
"final/document.md#§12.5"
],
"code": [
"config.json",
"favicon.svg",
"favicon.ico"
],
"classification": "healthy 로 올라온 컨테이너가 파일 하나만 못 내주는 원인을 빌드가 쓴 권한으로 확정했고, 이미지가 권한을 정규화하게 해서 닫힌다",
"missing-verification": "이미지 안의 다른 파일 권한을 전수로 확인하지 않았다. 고친 것은 빌드가 쓰는 자리 하나다",
"relations": [
"concept:deploying-without-a-registry",
"case:a-route-the-web-server-never-heard-of",
"case:a-build-argument-left-out"
],
"kind": "case",
"publication": "초안",
"file": "only-visible-after-deploying/case/case-a-healthy-container-that-served-one-403.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
},
{
"title": "배포 인자를 빠뜨려 배포본이 존재하지 않는 주소를 불렀다",
"slug": "a-build-argument-left-out",
"readiness": "READY",
"source": [
"final/document.md#§12.2"
],
"code": [
"RUNTIME_API_BASE_URL",
"api.example.com",
"kubectl rollout undo"
],
"classification": "Dockerfile 이 문자 그대로 그 경고를 적어 두고 있었는데도 빠뜨린 것이고, 되돌리고 다시 빌드해 닫혔다. 인자 목록을 적어 두는 것이 지금의 대책이다",
"missing-verification": "빌드가 이 인자를 요구하도록 막지 않았다. 빠뜨리면 여전히 빌드는 성공하고 배포본만 틀린다",
"relations": [
"concept:deploying-without-a-registry",
"reference:what-a-person-must-run-before-deploying",
"case:a-healthy-container-that-served-one-403"
],
"kind": "case",
"publication": "초안",
"file": "only-visible-after-deploying/case/case-a-build-argument-left-out.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"concept": [
{
"title": "레지스트리 없이 tar 를 import 하는 배포 경로",
"slug": "deploying-without-a-registry",
"readiness": "READY",
"source": [
"final/document.md#§1.3",
"final/document.md#§12.2"
],
"code": [
"docker save",
"containerd",
"kubectl set image"
],
"basis-version": "k3s + containerd · hyeonworks.com 단일 배포 단위 · 2026-09 시점",
"classification": "이미지가 어떤 경로로 클러스터에 들어가고 무엇이 빌드 인자로만 들어가는지를 알아야, §12 의 두 건이 왜 배포에서만 드러나는지 읽힌다",
"relations": [
"case:a-build-argument-left-out",
"case:a-healthy-container-that-served-one-403",
"reference:what-a-person-must-run-before-deploying"
],
"kind": "concept",
"publication": "초안",
"file": "only-visible-after-deploying/concept/concept-deploying-without-a-registry.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"reference": [],
"question": [],
"decision": []
},
"topic": "only-visible-after-deploying"
},
"one-thing-many-names": {
"title": "같은 것이 화면마다 다른 이름",
"readerQuestion": "같은 것을 화면마다 손으로 적으면 무엇이 갈라지고, 그 말은 누가 정하는가?",
"kinds": {
"case": [
{
"title": "한 화면에 종류 이름이 아홉 개 떠 있었다 — 표가 여섯 벌이었다",
"slug": "nine-names-for-five-kinds",
"readiness": "READY",
"source": [
"final/document.md#§13.1",
"final/document.md#§13.5"
],
"code": [
"Record<RecordKind, string>"
],
"classification": "같은 QUESTION 이 화면마다 다른 이름으로 나온 원인을 여섯 벌로 복사된 표로 확정했고, 표 하나로 모으고 편집기 칸 이름까지 공개 화면과 맞춰 닫는다",
"missing-verification": "이름을 바꾸기 전보다 나빠진 자리가 홈 하나였다는 것은 화면으로 확인했고, 다른 화면에서 두 이름이 같이 뜨는 자리를 전수로 세지는 않았다",
"relations": [
"case:renaming-the-kinds-twice",
"case:one-new-kind-fell-through-thirteen-places",
"reference:ask-which-words-to-use"
],
"kind": "case",
"publication": "초안",
"file": "one-thing-many-names/case/case-nine-names-for-five-kinds.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
},
{
"title": "종류 이름을 두 번 바꿨다 — 화면의 이름과 계약의 kind 를 갈랐다",
"slug": "renaming-the-kinds-twice",
"readiness": "READY",
"source": [
"final/document.md#§13.2"
],
"code": [
"CASE",
"CONCEPT",
"REFERENCE",
"DECISION",
"QUESTION"
],
"classification": "독자가 담아 둔 방식의 이름을 먼저 배워야 목록을 읽을 수 있었다는 문제에서 출발해, 역할을 말하되 기술 기록의 톤에 맞는 이름으로 두 번 만에 닫혔다. 계약의 kind 는 건드리지 않는다",
"missing-verification": "바꾼 이름이 읽기 쉬워졌는지는 재지 않았다. 첫 번째 안이 가볍다는 판단은 사용자의 지적이고 측정이 아니다",
"relations": [
"case:nine-names-for-five-kinds",
"reference:ask-which-words-to-use",
"case:a-slug-rule-that-threw-korean-away"
],
"kind": "case",
"publication": "초안",
"file": "one-thing-many-names/case/case-renaming-the-kinds-twice.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
},
{
"title": "서버는 하나를 답했는데 화면은 추측 셋을 출력했다",
"slug": "an-error-message-that-guessed",
"readiness": "READY",
"source": [
"final/document.md#§13.4"
],
"code": [
"another record still links to this one"
],
"classification": "버전 충돌이 「사용 중」으로 읽혀 작성자가 두 실패를 구분할 수 없던 것을 확정했고, 서버의 클라이언트 안전 메시지를 그대로 실어 나르게 해서 닫는다",
"missing-verification": "서버가 답하는 그 한 문장이 다섯 이유를 다 같은 말로 덮고 있다. 그 자리는 아직 열려 있다",
"relations": [
"question:the-refusal-does-not-name-what-blocks-it",
"reference:say-you-could-not-read-it",
"case:nine-names-for-five-kinds"
],
"kind": "case",
"publication": "초안",
"file": "one-thing-many-names/case/case-an-error-message-that-guessed.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"concept": [],
"reference": [
{
"title": "톤을 지적받으면 고쳐 쓰지 말고 어떤 말을 쓸지 묻는다",
"slug": "ask-which-words-to-use",
"readiness": "READY",
"source": [
"final/document.md#§13.3"
],
"classification": "고쳐 쓴 첫 번째 안이 거절당하고 사용자가 직접 쓴 텍스트를 그대로 실은 것으로 닫혔고, 사이트의 글이 특정한 사람의 목소리인 어떤 자리에도 적용된다",
"scope": "공개 화면의 글이 작성자의 목소리인 자리 — 프로필, 프로젝트 소개, 구역 제목",
"exceptions": "오류 문구처럼 서버가 답한 사실을 그대로 실어야 하는 자리는 목소리의 문제가 아니다. 그때는 무엇을 실을지가 문제다",
"relations": [
"case:nine-names-for-five-kinds",
"case:renaming-the-kinds-twice",
"case:an-error-message-that-guessed"
],
"kind": "reference",
"publication": "초안",
"file": "one-thing-many-names/reference/reference-ask-which-words-to-use.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"question": [
{
"title": "삭제를 막는 이유 다섯 가지가 전부 같은 한 문장으로 나온다",
"slug": "the-refusal-does-not-name-what-blocks-it",
"readiness": "OPEN",
"source": [
"final/document.md#§16.1"
],
"evidence": [
"evidence/raw/db/delete-blocked-by-project-link.txt"
],
"known": "참조 검사는 다섯 테이블을 UNION ALL 로 묶어 하나라도 걸리면 같은 문장을 낸다. 실제 사례에서 막은 것은 project_document_link 의 PRIMARY 링크 한 행이었고, 그것은 「관계」 편집기가 아니라 문서의 Project 필드다",
"unknown": "무엇이 막는지 말하면서 내부 테이블 이름을 노출하지 않는 문구",
"next-verification": "참조 검사를 종류별로 갈라 어느 것이 걸렸는지 돌려주고, 화면이 그 종류를 사용자가 고칠 수 있는 자리의 이름으로 옮겨 보인다",
"decision-criterion": "삭제가 막혔을 때 어디를 고쳐야 하는지 문구만 보고 알 수 있으면 닫는다",
"relations": [
"case:an-error-message-that-guessed",
"case:the-sql-had-never-been-executed",
"reference:say-you-could-not-read-it"
],
"kind": "question",
"ssot-evidence": [
"raw/db/delete-blocked-by-project-link.txt"
],
"publication": "초안",
"file": "one-thing-many-names/question/openquestion-the-refusal-does-not-name-what-blocks-it.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": [
"../../../final/evidence/raw/db/delete-blocked-by-project-link.txt"
]
}
],
"decision": []
},
"topic": "one-thing-many-names"
},
"an-axis-inside-a-topic": {
"title": "주제 안의 축",
"readerQuestion": "한 질문에 네 개의 답을 만들었을 때 그것을 어디에 담고, 무엇이 자동으로 따라오지 않는가?",
"kinds": {
"case": [
{
"title": "홈의 비교 구역이 세 번 바뀌었다 — 상한을 없애고 요청을 목록 하나와 주제 하나로 고정했다",
"slug": "the-comparison-band-changed-three-times",
"readiness": "READY",
"source": [
"final/document.md#§14.2"
],
"evidence": [
"evidence/browser/home-tabs-grouped.png",
"evidence/browser/home-topic-tabs.png",
"evidence/browser/tab-metrics.txt"
],
"classification": "세 단계가 각각 무엇을 고치려 했는지와 왜 다음 단계로 갔는지가 화면 캡처와 실측값으로 남아 있고, 주제 수와 무관하게 첫 요청이 고정되는 것으로 닫힌다",
"missing-verification": "주제가 스무 개일 때의 화면은 만들어 보지 않았다. 상한을 없앤 근거는 구조이지 그 규모의 측정이 아니다",
"relations": [
"concept:topic-variant-and-record-variant",
"question:the-conclusion-line-does-not-follow-the-records",
"case:one-cell-failing-took-its-neighbour-down"
],
"kind": "case",
"ssot-evidence": [
"browser/home-tabs-keycloak.png",
"browser/home-topic-tabs.png",
"browser/home-topic-tabs-2.png",
"browser/home-tabs-grouped.png",
"browser/tab-metrics.txt"
],
"publication": "초안",
"file": "an-axis-inside-a-topic/case/case-the-comparison-band-changed-three-times.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": [
"../../../final/evidence/browser/home-tabs-keycloak.png",
"../../../final/evidence/browser/home-topic-tabs.png",
"../../../final/evidence/browser/home-topic-tabs-2.png",
"../../../final/evidence/browser/home-tabs-grouped.png",
"../../../final/evidence/browser/tab-metrics.txt"
]
}
],
"concept": [
{
"title": "축은 주제가 이름을 정하고, 기록은 종류와 아이디의 쌍으로 축에 걸린다",
"slug": "topic-variant-and-record-variant",
"readiness": "READY",
"source": [
"final/document.md#§14.1"
],
"code": [
"topic_variant",
"record_variant",
"variant_label",
"variantIds"
],
"basis-version": "tech-log-backend 2026-09-01 의 축 스키마 · record_variant 에 외래키 없음 · studio_validation 과 publication 이 쓰는 방식을 따름",
"classification": "기록이 종류마다 다른 테이블에 살아 외래키를 걸 수 없다는 것과, 아무 축에도 걸리지 않은 기록이 공통 기록으로 읽힌다는 두 가지를 알아야 축 화면과 비교 구역이 읽힌다",
"relations": [
"decision:an-axis-inside-a-topic-not-four-topics",
"case:a-link-that-pointed-at-itself",
"case:the-comparison-band-changed-three-times"
],
"kind": "concept",
"ssot-assets": [
"topic-variant-model"
],
"ssot-evidence": [
"raw/db/topic-variant-rows.txt",
"raw/db/record-variant-links.txt"
],
"publication": "초안",
"file": "an-axis-inside-a-topic/concept/concept-topic-variant-and-record-variant.md",
"status": "게시 전",
"studioId": "",
"assets": [
"topic-variant-model"
],
"assetFiles": [
"topic-variant-model"
],
"evidenceFiles": [
"../../../final/evidence/raw/db/topic-variant-rows.txt",
"../../../final/evidence/raw/db/record-variant-links.txt"
]
}
],
"reference": [],
"question": [
{
"title": "축의 결론 문장과 기록 수는 기록을 붙여도 따라오지 않는다",
"slug": "the-conclusion-line-does-not-follow-the-records",
"readiness": "OPEN",
"source": [
"final/document.md#§14.3",
"final/document.md#§16.2"
],
"evidence": [
"evidence/raw/db/record-variant-links.txt"
],
"known": "홈의 줄은 문서가 아니라 축이라, 기록을 20개 붙여도 줄 수는 그대로다. 결론은 축에 손으로 쓴 글이라 누가 고치기 전까지 그대로다. 주제 화면에는 축마다 「기록 N」이 붙는데 홈에는 없다",
"unknown": "결론을 사람이 갱신하도록 남길지, 기록 수처럼 유도할 수 있는 것만 유도해 갱신이 필요한 자리를 좁힐지",
"next-verification": "홈 비교표에 기록 수를 붙여 보고, 축에 기록을 더했을 때 홈에서 무엇이 달라지고 무엇이 그대로인지 화면으로 가른다",
"decision-criterion": "축에 기록을 더했을 때 자동으로 따라오는 것과 사람이 고쳐야 하는 것이 화면에서 구분되면 닫는다",
"relations": [
"decision:an-axis-inside-a-topic-not-four-topics",
"case:the-comparison-band-changed-three-times",
"reference:measure-what-is-visible-not-a-proxy"
],
"kind": "question",
"ssot-evidence": [
"raw/db/record-variant-links.txt"
],
"publication": "초안",
"file": "an-axis-inside-a-topic/question/openquestion-the-conclusion-line-does-not-follow-the-records.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": [
"../../../final/evidence/raw/db/record-variant-links.txt"
]
}
],
"decision": [
{
"title": "주제를 넷으로 쪼개지 않고 주제 안에 축을 하나 뒀다",
"slug": "an-axis-inside-a-topic-not-four-topics",
"readiness": "READY",
"source": [
"final/document.md#§14.1",
"final/document.md#§14.3"
],
"decision-status": "ADOPTED",
"decision-evidence": "두 주제가 같은 구조를 쓰고 축에 걸린 기록 수만 다른 것을 실제 행으로 확인했다 — evidence/raw/db/topic-variant-rows.txt · evidence/raw/db/record-variant-links.txt",
"grounds": "주제를 쪼개면 PKCE·CSRF·Authorization Code 처럼 네 구조가 함께 쓰는 기록을 어디에 둘지 애매해지고 비교도 어려워진다. 감수한 비용은 축의 이름·요약·결론이 기록에서 자동으로 나오지 않고 사람이 써야 한다는 것이다",
"classification": "프로젝트가 실제로 이 방향을 골랐고 그 대가를 editorial 칸으로 명시했다",
"evidence": [
"evidence/raw/db/topic-variant-rows.txt",
"evidence/raw/db/record-variant-links.txt"
],
"relations": [
"concept:topic-variant-and-record-variant",
"question:the-conclusion-line-does-not-follow-the-records",
"case:the-comparison-band-changed-three-times"
],
"kind": "decision",
"ssot-evidence": [
"raw/db/topic-variant-rows.txt",
"raw/db/record-variant-links.txt"
],
"publication": "초안",
"file": "an-axis-inside-a-topic/decision/decision-an-axis-inside-a-topic-not-four-topics.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": [
"../../../final/evidence/raw/db/topic-variant-rows.txt",
"../../../final/evidence/raw/db/record-variant-links.txt"
]
}
]
},
"topic": "an-axis-inside-a-topic"
},
"when-a-guard-can-be-trusted": {
"title": "가드를 언제 믿을 수 있는가",
"readerQuestion": "가드를 넣었다는 것만으로는 왜 부족한가?",
"kinds": {
"case": [
{
"title": "가드는 작동했는데 제가 그것을 돌리지 않아 두 번 새어 나갔다",
"slug": "the-guard-worked-and-i-did-not-run-it",
"readiness": "READY",
"source": [
"final/document.md#§7.5",
"final/document.md#§8.5",
"final/document.md#§17.4"
],
"code": [
"test:tech-log",
"test:unit"
],
"classification": "두 건 다 가드가 아니라 실행이 빠진 것이었음을 확정했고, 사람이 기억해서 돌리는 가드는 절반만 존재한다는 결론으로 닫힌다",
"missing-verification": "다섯 명령을 CI 에 묶는 작업은 하지 않았다. 지금 남은 것은 메모리와 이 문서의 목록이다",
"relations": [
"reference:what-a-person-must-run-before-deploying",
"reference:revert-the-defect-and-watch-the-guard-fail",
"case:eight-places-a-single-route-touches"
],
"kind": "case",
"publication": "초안",
"file": "when-a-guard-can-be-trusted/case/case-the-guard-worked-and-i-did-not-run-it.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"concept": [],
"reference": [
{
"title": "가드는 결함을 되돌려 실제로 멈추는 것을 확인한 뒤 커밋한다",
"slug": "revert-the-defect-and-watch-the-guard-fail",
"readiness": "READY",
"source": [
"final/document.md#§15",
"final/document.md#§3.5",
"final/document.md#§17.4"
],
"evidence": [
"evidence/raw/guards/guards-actually-fail.txt"
],
"classification": "가드 셋을 각각 결함으로 되돌려 실제로 빨개지는 것을 확인한 기록이 남아 있고, 재발 방지로 넣는 어떤 검사에도 같은 절차가 적용된다",
"scope": "재발 방지로 넣는 테스트·아키텍처 규칙·CI 게이트",
"exceptions": "결함을 되돌릴 수 없는 것 — 이미 마이그레이션으로 고친 데이터 — 은 현재 상태가 고쳐져 있음을 대신 증거로 남긴다",
"relations": [
"case:the-guard-worked-and-i-did-not-run-it",
"reference:put-one-check-on-each-seam",
"reference:enumerate-kinds-where-the-compiler-sees-it"
],
"kind": "reference",
"ssot-evidence": [
"raw/guards/guards-actually-fail.txt"
],
"publication": "초안",
"file": "when-a-guard-can-be-trusted/reference/reference-revert-the-defect-and-watch-the-guard-fail.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": [
"../../../final/evidence/raw/guards/guards-actually-fail.txt"
]
},
{
"title": "배포 전에 사람이 돌려야 하는 것과 그 함정",
"slug": "what-a-person-must-run-before-deploying",
"readiness": "READY",
"source": [
"final/document.md#§15.4",
"final/document.md#§12.3",
"final/document.md#§12.7",
"final/document.md#§12.8",
"final/document.md#§16.6"
],
"classification": "빠뜨려서 실제로 새어 나간 명령과, 순서를 몰라 두 번 헤맨 자리와, 환경 변수 때문에 실패하는 자리가 한 목록으로 모인다",
"scope": "CI 에 묶이지 않은 검증이 남아 있는 저장소에서 배포 직전에 하는 일",
"exceptions": "CI 가 그 명령을 돌리면 이 목록에서 뺀다. 사람이 기억해서 돌리는 가드는 절반만 존재한다",
"relations": [
"case:the-guard-worked-and-i-did-not-run-it",
"case:the-typecheck-command-checked-no-files",
"concept:deploying-without-a-registry"
],
"kind": "reference",
"publication": "초안",
"file": "when-a-guard-can-be-trusted/reference/reference-what-a-person-must-run-before-deploying.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": []
}
],
"question": [],
"decision": []
},
"topic": "when-a-guard-can-be-trusted"
}
},
"candidates": [
{
"id": "SSOT-§1.1",
"kindCandidate": "CONCEPT",
"sourceRefs": [
"final/document.md#§1.1"
],
"summary": "세 저장소와 계약의 반입 흐름",
"disposition": "KEEP_IN_SSOT",
"dispositionReview": "CONFIRMED",
"reason": "배경이다. 이것만으로 서는 기록이 없고 경계 열한 개를 설명하는 자리에서 함께 읽힌다"
},
{
"id": "SSOT-§1.2",
"kindCandidate": "CONCEPT",
"sourceRefs": [
"final/document.md#§1.2"
],
"summary": "공개 화면 한 줄이 그려지기까지 값이 지나는 경계 열한 개",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "concept:eleven-boundaries-a-value-crosses",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§1.3",
"kindCandidate": "CONCEPT",
"sourceRefs": [
"final/document.md#§1.3"
],
"summary": "레지스트리 없이 tar 를 import 하는 배포",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "concept:deploying-without-a-registry",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§3.1",
"kindCandidate": "CONCEPT",
"sourceRefs": [
"final/document.md#§3.1"
],
"summary": "삼항 사슬의 마지막 else 가 모르는 것을 받아 간다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "concept:exhaustive-switch-and-record-tables",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§3.2",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§3.2"
],
"summary": "개념을 더하자 열세 자리가 조용히 삼켰다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:one-new-kind-fell-through-thirteen-places",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§3.3",
"kindCandidate": "CONCEPT",
"sourceRefs": [
"final/document.md#§3.3"
],
"summary": "Record 표와 sealed switch 식으로 바꾸면 컴파일러가 요구한다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "concept:exhaustive-switch-and-record-tables",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§3.4",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§3.4"
],
"summary": "계약을 읽어 대조하는 가드",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "reference:enumerate-kinds-where-the-compiler-sees-it",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§3.5",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§3.5"
],
"summary": "종류를 나열하는 자리의 규칙 셋",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "reference:enumerate-kinds-where-the-compiler-sees-it",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§4.1",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§4.1"
],
"summary": "화면 다섯 곳이 조용히 비어 있었다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:five-screens-were-quietly-empty",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§4.2",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§4.2"
],
"summary": "편집기가 부르는 두 목록에 컨트롤러가 없었다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "case:five-screens-were-quietly-empty",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§4.3",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§4.3"
],
"summary": "계약과 컨트롤러 전수 대조",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "reference:compare-the-contract-with-both-implementations",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§4.4",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§4.4"
],
"summary": "등록되지 않은 연산은 타입에는 보이는데 부를 수 없다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:an-operation-you-can-see-but-cannot-call",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§5.1",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§5.1"
],
"summary": "공개 Reference 가 통째로 비어 있었다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:a-public-reference-was-entirely-empty",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§5.2",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§5.2"
],
"summary": "관계의 요약이 경계 세 곳을 지나며 사라졌다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:a-summary-vanished-at-three-boundaries",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§5.3",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§5.3"
],
"summary": "관계 한 줄에 세 가지가 뭉쳐 있었다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "case:a-summary-vanished-at-three-boundaries",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§5.4",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§5.4"
],
"summary": "결정 화면이 네 가지를 못 그렸다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:a-list-item-had-to-carry-the-whole-document",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§5.5",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§5.5"
],
"summary": "계약에 칸이 없어 비어 있던 나머지 여덟 자리",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "reference:a-missing-contract-field-has-a-signature",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§5.6",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§5.6"
],
"summary": "계약의 빈칸을 알아보는 신호와 required 판단",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "reference:a-missing-contract-field-has-a-signature",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§6.1",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§6.1"
],
"summary": "메서드 매개변수는 bivariant 다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:a-narrow-implementation-satisfied-a-wide-port",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§6.2",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§6.2"
],
"summary": "as 단언이 계약과의 어긋남을 가렸다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "case:a-public-reference-was-entirely-empty",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§6.3",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§6.3"
],
"summary": "TypeScript 가 검사를 놓아 주는 네 자리 — 메서드 매개변수의 bivariance · as 단언 · never 캐스트 · 검사 대상을 갖지 않은 tsconfig",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "concept:where-typescript-stops-checking",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§6.4",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§6.4"
],
"summary": "npx tsc --noEmit 이 한 파일도 검사하지 않고 성공했다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:the-typecheck-command-checked-no-files",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§6.5",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§6.5"
],
"summary": "클래스패스에 남은 Jackson 2 를 컴파일이 통과시켰다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "case:two-pods-crashlooped-with-no-test-starting-the-context",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§6.6",
"kindCandidate": "CONCEPT",
"sourceRefs": [
"final/document.md#§6.6"
],
"summary": "TypeScript 가 검사를 놓아 주는 네 자리 — 메서드 매개변수의 bivariance · as 단언 · never 캐스트 · 검사 대상을 갖지 않은 tsconfig",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "concept:where-typescript-stops-checking",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§7.1",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§7.1"
],
"summary": "컨텍스트를 띄우지 않는 테스트",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:two-pods-crashlooped-with-no-test-starting-the-context",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§7.2",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§7.2"
],
"summary": "SQL 이 한 번도 실행되지 않았다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:the-sql-had-never-been-executed",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§7.3",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§7.3"
],
"summary": "HTTP 게이트웨이의 매핑을 지나는 테스트가 없었다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "case:the-composition-root-had-no-test",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§7.4",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§7.4"
],
"summary": "합성 루트에 테스트가 없었다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:the-composition-root-had-no-test",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§7.5",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§7.5"
],
"summary": "화면 테스트를 아예 돌리지 않았다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:the-guard-worked-and-i-did-not-run-it",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§7.6",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§7.6"
],
"summary": "생성기가 계약 필드를 조용히 빠뜨렸다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:the-generator-dropped-four-contract-fields",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§7.7",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§7.7"
],
"summary": "이음매와 그것을 덮은 방법의 표",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "reference:put-one-check-on-each-seam",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§8.1",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§8.1"
],
"summary": "라우트 하나가 건드리는 여덟 자리",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:eight-places-a-single-route-touches",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§8.2",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§8.2"
],
"summary": "nginx 가 모르는 라우트는 404 다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:a-route-the-web-server-never-heard-of",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§8.2b",
"kindCandidate": "DECISION",
"sourceRefs": [
"final/document.md#§8.2"
],
"summary": "catch-all 라우트는 번역하지 않는다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "decision:do-not-translate-the-catch-all-route",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§8.3",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§8.3"
],
"summary": "vite chunk 이름 표도 손 목록이라 대조하게 했다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "reference:derive-the-route-lists-from-the-route-contract",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§8.4",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§8.4"
],
"summary": "CI 게이트 기준값이 라우트와 함께 움직인다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "case:eight-places-a-single-route-touches",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§8.5",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§8.5"
],
"summary": "주제 화면 셋을 더할 때 그 목록을 또 빠뜨렸다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "case:the-guard-worked-and-i-did-not-run-it",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§9.1",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§9.1"
],
"summary": "축 링크가 자기 자신을 가리켰다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:a-link-that-pointed-at-itself",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§9.1b",
"kindCandidate": "DECISION",
"sourceRefs": [
"final/document.md#§9.1"
],
"summary": "새 라우트는 프론트가 먼저다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "decision:a-new-route-ships-frontend-first",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§9.2",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§9.2"
],
"summary": "결정 링크가 404 였다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:an-address-frozen-at-publish-time",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§9.2b",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§9.2"
],
"summary": "열리지 않는 주소는 링크로 그리지 않는다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "reference:do-not-draw-a-link-that-does-not-resolve",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§9.3",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§9.3"
],
"summary": "주제가 없는 기록이 이름 없는 링크를 달았다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "case:a-link-that-pointed-at-itself",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§9.4",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§9.4"
],
"summary": "주제 화면이 셋만 열어 우회했고, 조건이 충족되자 되돌렸다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "reference:write-down-what-would-undo-a-workaround",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§10.1",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§10.1"
],
"summary": "「이 프로젝트에 열린 질문이 없습니다」",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:it-said-there-were-no-open-questions",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§10.1b",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§10.1"
],
"summary": "거짓말을 하느니 못 읽었다고 말한다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "reference:say-you-could-not-read-it",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§10.2",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§10.2"
],
"summary": "한 칸의 실패가 옆 칸을 끌고 내려간다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:one-cell-failing-took-its-neighbour-down",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§10.3",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§10.3"
],
"summary": "계약 밖 값이 500 을 만들어 질의가 먼저 걸러 냈다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "case:one-new-kind-fell-through-thirteen-places",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§10.4",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§10.4"
],
"summary": "배포 직후 첫 요청부터 홈이 깨졌다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "case:five-screens-were-quietly-empty",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§10.5",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§10.5"
],
"summary": "스모크 스윕이 늑대를 외쳤다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "reference:an-expected-failure-must-not-be-counted-as-a-failure",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§10.6",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§10.6"
],
"summary": "기록이 조용히 사라졌다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:records-disappeared-without-a-trace",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§11.1",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§11.1"
],
"summary": "구역 전체에 건 격자가 제목까지 잡았다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:a-section-wide-rule-caught-the-heading",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§11.1b",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§11.1"
],
"summary": "배치 규칙은 그 배치를 쓰는 요소까지 좁힌다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "reference:scope-a-layout-rule-to-what-uses-it",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§11.2",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§11.2"
],
"summary": "규칙이 없었던 게 아니라 절반만 있었다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:the-rule-was-not-missing-it-was-half-there",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§11.3",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§11.3"
],
"summary": "CSS module 은 전역 규칙이 닿지 않는다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "case:a-section-wide-rule-caught-the-heading",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§12.1",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§12.1"
],
"summary": "파드가 CrashLoopBackOff 로 들어간 두 건",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "case:two-pods-crashlooped-with-no-test-starting-the-context",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§12.2",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§12.2"
],
"summary": "배포 인자를 빠뜨려 배포본이 api.example.com 을 불렀다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:a-build-argument-left-out",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§12.3",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§12.3"
],
"summary": "stale JAR 검사는 커밋한 뒤에 돌린다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "reference:what-a-person-must-run-before-deploying",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§12.4",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§12.4"
],
"summary": "컨테이너가 읽을 수 없는 설정 파일",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:a-healthy-container-that-served-one-403",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§12.5",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§12.5"
],
"summary": "favicon 이 404 였다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "case:a-healthy-container-that-served-one-403",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§12.6",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§12.6"
],
"summary": "robots.txt 가 404 였다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "case:a-route-the-web-server-never-heard-of",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§12.7",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§12.7"
],
"summary": "테스트 JVM 이 OOM 났고 증상이 원인을 가렸다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "reference:what-a-person-must-run-before-deploying",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§12.8",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§12.8"
],
"summary": "npm 환경 변수 누출을 피해 vitest 를 돌린다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "reference:what-a-person-must-run-before-deploying",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§13.1",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§13.1"
],
"summary": "한 화면에 종류 이름이 아홉 개",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:nine-names-for-five-kinds",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§13.2",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§13.2"
],
"summary": "종류 이름을 두 번 바꿨다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:renaming-the-kinds-twice",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§13.3",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§13.3"
],
"summary": "AI 스러운 문구를 지적받고 고쳐 쓴 안도 거절당했다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "reference:ask-which-words-to-use",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§13.4",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§13.4"
],
"summary": "오류 문구가 추측을 출력했다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:an-error-message-that-guessed",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§13.5",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§13.5"
],
"summary": "편집기 칸 이름을 공개 화면과 맞췄다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "case:nine-names-for-five-kinds",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§13.6",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§13.6"
],
"summary": "한글 slug 가 간헐적으로 충돌했다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:a-slug-rule-that-threw-korean-away",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§14.1",
"kindCandidate": "DECISION",
"sourceRefs": [
"final/document.md#§14.1"
],
"summary": "하나의 질문에 네 개의 답 — 주제 안에 축을 뒀다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "decision:an-axis-inside-a-topic-not-four-topics",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§14.1b",
"kindCandidate": "CONCEPT",
"sourceRefs": [
"final/document.md#§14.1"
],
"summary": "topic_variant 와 record_variant 의 모양",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "concept:topic-variant-and-record-variant",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§14.2",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§14.2"
],
"summary": "홈의 비교 구역이 세 번 바뀌었다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:the-comparison-band-changed-three-times",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§14.3",
"kindCandidate": "QUESTION",
"sourceRefs": [
"final/document.md#§14.3"
],
"summary": "축이 무엇을 기준으로 묶이나 — 자동으로 안 따라오는 것",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "question:the-conclusion-line-does-not-follow-the-records",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§15",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§15"
],
"summary": "가드를 각각 결함으로 되돌려 멈추는 것을 확인하고 커밋했다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "reference:revert-the-defect-and-watch-the-guard-fail",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§15.1",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§15.1"
],
"summary": "프론트엔드 가드 열두 개의 목록",
"disposition": "KEEP_IN_SSOT",
"dispositionReview": "CONFIRMED",
"reason": "가드 목록이다. 각 Case 의 재발 방지 절이 그 자리에서 필요한 줄만 가리킨다"
},
{
"id": "SSOT-§15.2",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§15.2"
],
"summary": "백엔드 가드 아홉 개의 목록",
"disposition": "KEEP_IN_SSOT",
"dispositionReview": "CONFIRMED",
"reason": "가드 목록이다. 각 Case 의 재발 방지 절이 그 자리에서 필요한 줄만 가리킨다"
},
{
"id": "SSOT-§15.3",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§15.3"
],
"summary": "설계 패키지 가드 다섯 개의 목록",
"disposition": "KEEP_IN_SSOT",
"dispositionReview": "CONFIRMED",
"reason": "가드 목록이다. 각 Case 의 재발 방지 절이 그 자리에서 필요한 줄만 가리킨다"
},
{
"id": "SSOT-§15.4",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§15.4"
],
"summary": "배포 전 사람이 돌려야 하는 검증",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "reference:what-a-person-must-run-before-deploying",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§16.1",
"kindCandidate": "QUESTION",
"sourceRefs": [
"final/document.md#§16.1"
],
"summary": "삭제를 막는 이유를 문구가 말하지 않는다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "question:the-refusal-does-not-name-what-blocks-it",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§16.2",
"kindCandidate": "QUESTION",
"sourceRefs": [
"final/document.md#§16.2"
],
"summary": "홈 비교표에 기록 수가 없다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "question:the-conclusion-line-does-not-follow-the-records",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§16.3",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§16.3"
],
"summary": "두 탭 줄의 표시 방식이 다르다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "case:a-section-wide-rule-caught-the-heading",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§16.4",
"kindCandidate": "QUESTION",
"sourceRefs": [
"final/document.md#§16.4"
],
"summary": "릴리즈 0.3.0 이 초안 상태다",
"disposition": "KEEP_IN_SSOT",
"dispositionReview": "CONFIRMED",
"reason": "이 저장소 밖의 게시 상태다. 독자가 쓸 것이 없고 사용자 검토로 닫힌다"
},
{
"id": "SSOT-§16.5",
"kindCandidate": "QUESTION",
"sourceRefs": [
"final/document.md#§16.5"
],
"summary": "수동 접근성 증거가 전부 미서명이다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "question:nobody-signed-the-accessibility-evidence",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§16.6",
"kindCandidate": "QUESTION",
"sourceRefs": [
"final/document.md#§16.6"
],
"summary": "환경 의존으로 실패하는 테스트 3개",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "reference:what-a-person-must-run-before-deploying",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§16.7",
"kindCandidate": "QUESTION",
"sourceRefs": [
"final/document.md#§16.7"
],
"summary": "종류를 세는 곳 둘이 아직 컴파일러의 보호 밖에 있다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "question:two-kind-tables-outside-the-compiler",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§16.8",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§16.8"
],
"summary": "검토용 스크린샷 3장이 저장소에 커밋돼 있다",
"disposition": "KEEP_IN_SSOT",
"dispositionReview": "CONFIRMED",
"reason": "정리하면 끝나는 작업 항목이다. 독립 기록으로 만들 것이 없다"
},
{
"id": "SSOT-§16.9",
"kindCandidate": "QUESTION",
"sourceRefs": [
"final/document.md#§16.9"
],
"summary": "주제 논지와 축 결론의 출처가 미검토 초안이다",
"disposition": "KEEP_IN_SSOT",
"dispositionReview": "CONFIRMED",
"reason": "자료의 상태를 적어 둔 것이다. 축의 결론을 사람이 갱신해야 한다는 물음은 따로 세워 두었다"
},
{
"id": "SSOT-§17.1",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§17.1"
],
"summary": "값의 여정 끝에서 확인한다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "reference:verify-at-the-end-of-the-value-journey",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
},
{
"id": "SSOT-§17.2",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§17.2"
],
"summary": "손으로 나열한 목록은 반드시 갈라진다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "reference:enumerate-kinds-where-the-compiler-sees-it",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§17.3",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§17.3"
],
"summary": "화면은 못 읽은 것을 없다고 말하면 안 된다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "reference:say-you-could-not-read-it",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§17.4",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§17.4"
],
"summary": "가드는 넣는 것보다 돌리는 것이 어렵다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "case:the-guard-worked-and-i-did-not-run-it",
"reason": "없애고 대상 기록의 한 절로 넣어도 이해·결정·재사용성이 그대로다"
},
{
"id": "SSOT-§17.5",
"kindCandidate": "REFERENCE",
"sourceRefs": [
"final/document.md#§17.5"
],
"summary": "프록시 지표가 아니라 보이는 것을 측정한다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "reference:measure-what-is-visible-not-a-proxy",
"reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다"
}
],
"unlisted": [],
"history": {},
"counts": {
"topics": 13,
"nodes": 56,
"written": 56,
"unwritten": 0,
"unlisted": 0,
"candidates": 91
},
"ssotSha256": "974abab805e33daa531fa23fde85309c0de17becf6c5e460ef5ff00d057e20c9"
}