{ "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", "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", "sealed", "switch" ], "basis-version": "TypeScript 5.x 의 Record 키 전수 요구 · 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" ] }, { "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" ], "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-grouped.png", "../../../final/evidence/browser/home-topic-tabs.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": "4f2c6b31f152a7d786e76d8efda5fe27c056ff685a031ba8b772c9595e7d03ad" }