docs(TechLog): 주제 셋을 스킬대로 다시 쓰고 SSOT 를 저장소 실물로 고친다

건너뛴 참조 다섯을 읽고 나서 다시 썼다 — from-ssot-to-records.md 의 「그림과 증거는
배정 대상이다」, code-tables-diagrams.md 의 표·코드 규칙, explaining.md 의 「이름을
댔으면 왜 있는지도 댄다」.

**SSOT 를 먼저 고쳤다.** §3.3 의 코드블록이 저장소와 달랐다 — PATH_PREFIX_KINDS 는
Record 표가 아니라 튜플 배열이고, 진짜 경로 표는 EXPLORE_KIND_PATHS 다. 저장소에서
확인해 실물로 바꾸고, javadoc 이 적어 둔 이유를 함께 옮겼다. §16.7 에 BRANCH_FIELDS 와
pathOf 실물을, §4.3 에 ContractRouteCoverageTest 의 javadoc 과 면제 상수 둘을 더했다.
62,643 → 65,737 자.

**계약에 ssot-assets·ssot-evidence 를 배정했다.** 그 절차를 건너뛰어서 SSOT 가 이미
가진 그림과 측정이 글감에 배정되지 않은 채였다. TechLog 12 글감, keycloak-session-store
는 그림 21장·증거 19건을 배정하고 붙일 글감이 없는 그림 4장은 이유를 계약에 적었다.
배정하자 검사기가 「배정한 증거를 기록이 쓰지 않는다」 4건을 드러냈다.

**주제 셋을 다시 썼다.**
  hand-listed-kinds            중앙값 1,925 → 3,760 자
  declared-but-not-implemented          → 2,608 자
  values-lost-between-boundaries        → 2,602 자

게시된 기록은 keycloak 4,546 · n+1liner 3,190 이다. 표와 코드를 SSOT 에서 옮기고,
Reference 에 담을 수 없던 표(§5.5 의 여덟 자리)를 짝이 되는 Case 로 내렸다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-07 18:54:45 +09:00
co-authored by Claude Opus 5
parent 6feee5ba57
commit a8ce0dda07
15 changed files with 484 additions and 132 deletions
+93 -8
View File
@@ -188,18 +188,48 @@ const path = kind === "CASE" ? "/cases/"
### 3.3 고친 방법 — 표로 바꾸고 컴파일러에게 맡긴다
삼항 사슬을 `Record<Kind, Value>` 로 바꿨습니다.
삼항 사슬을 `Record<RecordKind, Value>` 로 바꿨습니다. 종류별 목록 주소가 그 예입니다
(`presentation/shared/document-kind-labels.ts`):
```ts
// 종류가 늘면 이 자리가 비어 있다고 컴파일러가 잡는다
const PATH_PREFIX_KINDS: Record<PublicRecord["kind"], string> = {
CASE: "/cases/",
REFERENCE: "/references/",
QUESTION: "/questions/",
CONCEPT: "/concepts/",
export const EXPLORE_KIND_PATHS: Record<RecordKind, string> = {
CASE: "/explore/cases",
CONCEPT: "/explore/concepts",
REFERENCE: "/explore/references",
QUESTION: "/explore/questions",
PROJECT_DECISION: "/projects",
};
```
같은 파일의 javadoc 이 이 표가 왜 한 곳에 있는지 적어 두었습니다:
> 이 대응이 세 화면에 흩어져 있었고 셋 다 개념을 빠뜨렸다 — 홈의 「종류별로 읽기」에는 개념이
> 아예 없었고, 문서 머리말의 종류 링크는 삼항의 마지막 else 를 타 개념 문서에서 `/projects` 로
> 갔다. `/explore/concepts` 는 처음부터 열려 있었는데 그리로 가는 길이 없었다.
>
> 결정은 프로젝트 안에서만 읽히므로 자기 목록이 없다. 그 자리를 `/projects` 로 두는 것은
> 빠뜨린 것이 아니라 그렇게 정한 것이고, 표에 적혀 있으니 다음 사람이 구분할 수 있다.
**표로 바꿀 수 없는 자리도 있습니다.** 공개 주소에서 종류를 거꾸로 알아내는 자리
(`public-document-header.tsx`)는 키가 종류가 아니라 주소 앞머리라서 `Record<RecordKind, _>`
성립하지 않습니다. 배열로 두고 못 찾은 것을 조각으로 가릅니다:
```ts
const PATH_PREFIX_KINDS: ReadonlyArray<readonly [string, TargetKind]> = [
["/cases/", "CASE"],
["/references/", "REFERENCE"],
["/questions/", "QUESTION"],
["/concepts/", "CONCEPT"],
];
function targetKindOf(path: string): TargetKind {
const matched = PATH_PREFIX_KINDS.find(([prefix]) => path.startsWith(prefix));
if (matched) return matched[1];
// 결정은 프로젝트 화면 안의 앵커로 산다. 그래서 앞머리가 아니라 조각으로 가른다.
return path.includes("/decisions#") ? "PROJECT_DECISION" : "PROJECT";
}
```
백엔드에서는 **sealed switch 를 식(expression)으로** 쓴 자리가 이 일을 이미 하고 있었습니다.
`fa5158d`(개념 종류 추가) 커밋 메시지에 그 효과가 적혀 있습니다:
@@ -272,7 +302,27 @@ const PATH_PREFIX_KINDS: Record<PublicRecord["kind"], string> = {
### 4.3 재발 방지 — 계약↔컨트롤러 전수 대조
`ContractRouteCoverageTest`(백엔드)를 세웠습니다. `@RestController` 들을 리플렉션으로 훑어
매핑을 모으고, 계약이 선언한 경로와 대조합니다.
매핑을 모으고, 계약이 선언한 경로와 대조합니다. 클래스 javadoc 이 이 검사가 왜 생겼는지를
적어 두었습니다:
> `listStudioQuestions` 와 `listStudioProjectDecisions` 는 계약에 있고 모델도 생성됐는데
> 컨트롤러가 없었다. 생성 모델 검사(`verifyManagementGeneratedModels`)는 schema 와 property 만
> 보므로 이 구멍을 잡지 못한다. 프론트는 계약을 믿고 불렀고 서버는 404 를 돌려줬으며, 화면은
> 그것을 「이 프로젝트에 열린 질문이 없습니다」로 그렸다 — 실제로는 넷이 있었다.
>
> 기대 목록을 손으로 적지 않고 계약에서 읽는다. 연산을 더하고 컨트롤러를 잊으면 여기서 멈춘다.
면제는 상수 둘로 명시합니다. 대조에서 빠지는 것이 코드에 이름으로 남습니다:
```java
private static final Set<String> ELSEWHERE = Set.of("getPublicMedia");
private static final Set<String> SUPERSEDED_BY_WORKING_COPY_API =
Set.of(
"acceptProjectDecision",
"addQuestionUpdate",
"archiveCase",
);
```
- 작업본 API 로 대체된 **옛 연산 51개**는 `SUPERSEDED_BY_WORKING_COPY_API` 로 명시해 둡니다 —
"구현하지 않기로 한 것"과 "빠뜨린 것"은 다릅니다
@@ -1307,10 +1357,45 @@ UNION ALL SELECT 1 FROM project_decision WHERE source_case_id = :id
경로가 `null` 로 나갑니다. 실제로 CONCEPT 이 여기서 빠져 있었습니다(§3.2 의 13번). 지금은
`PublicPathsTest` 가 막지만, `pathOf` 만 쓰는 경로(홈 focus 의 `recentDecision`)에는 그
테스트가 닿지 않습니다.
```java
static String pathOf(String resourceType, String slug, String projectSlug) {
return switch (resourceType) {
case "CASE" -> "/cases/" + slug;
case "REFERENCE" -> "/references/" + slug;
case "QUESTION" -> "/questions/" + slug;
case "CONCEPT" -> "/concepts/" + slug;
case "PROJECT" -> "/projects/" + slug;
case "PROJECT_DECISION" ->
projectSlug == null ? null : "/projects/" + projectSlug + "/decisions#" + slug;
case "RELEASE" -> "/releases/" + slug;
default -> null;
};
}
```
- **`validate-working-copy.ts` 의 `stringFields`** — 아직 삼항 사슬입니다. 다만 배타적 사슬이
아니라 가산형이라 종류를 빠뜨리면 "잘못된 분기로 떨어진다"가 아니라 "그 종류의 추가 칸을
검사하지 않는다"가 됩니다. 덜 위험하지만 조용하기는 마찬가지입니다.
같은 함수 안에서 두 목록의 상태가 갈립니다. 유형별 칸은 표로 바뀌었고 문자열 칸은 사슬로
남았습니다:
```ts
const BRANCH_FIELDS: Record<RecordKind, string[]> = {
CASE: ["problem", "conclusion", "environment", "reproduction", "lastVerifiedOn", "bodyMarkdown"],
CONCEPT: ["bodyMarkdown", "basisVersion"],
REFERENCE: ["purpose", "rules", "applyWhen", "exceptions", "examples", "verifiedOn"],
QUESTION: ["questionStatus", "facts", "assumptions", "unknowns", "constraints", "options", "nextValidation", "resolution"],
PROJECT_DECISION: ["decisionStatus", "decidedOn", "statement", "rationale", "consequences"],
};
```
그 표 위의 주석이 왜 바꿨는지를 적어 두었습니다:
> 유형별 칸. 삼항 사슬이던 동안 개념이 어디에도 없어서, 개념 작업본은 「알 수 없는 종류」로
> 거절되고 그 문서의 모든 칸이 「허용되지 않은 속성」이 됐다 — 손으로 나열한 목록에 새 종류를
> 빠뜨리는 일이 이 저장소에서 반복됐다. `Record<종류, …>` 로 두면 컴파일러가 빈 자리를 잡는다.
즉 §3 의 「표로 바꿨다」는 대부분 사실이지만 전부는 아닙니다.
### 16.8 검토용 스크린샷 3장이 저장소에 커밋돼 있다