From 7211dd1a92ad7f43684e168ab0911a6cc9115ea9 Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Tue, 25 Aug 2026 19:08:37 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EA=B3=B5=EA=B0=9C=20Reference=20?= =?UTF-8?q?=EA=B0=80=20=EA=B3=84=EC=95=BD=EC=9D=B4=20=EC=A3=BC=EB=8A=94=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=EC=9D=84=20=EC=9D=BD=EA=B2=8C=20=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reference 를 공개했는데 Studio 에서는 다 보이고 공개 화면만 비어 있었다. 게이트웨이가 읽던 이름이 계약에 없는 것들이었다 — `purposeSummary`, `applyWhenMarkdown`, `exceptionsMarkdown`, `examplesMarkdown`. 계약이 주는 이름은 `scopeSummary`, `appliesTo`, `excludedScope` 다. 전부 undefined 로 떨어졌고, `as string` 단언 때문에 타입 검사는 아무 말도 하지 않았다. 규칙은 `content` 마크다운을 잘라 만들고 있었다. Reference 의 본문은 마크다운 한 덩어리가 아니라 제목이 붙은 규칙의 목록이고, Studio 의 편집기가 그렇게 받아 `body_markdown` 은 비워 둔다 — 자를 것이 없으니 언제나 빈 목록이었다. 계약이 구조로 주는 것을 그대로 쓴다. 값이 아니라 이름을 지키는 테스트를 둔다. 계약에서 그 칸이 사라지면 `satisfies` 가 먼저 깨진다 — 이번 결함은 값을 검사해서는 잡히지 않았다. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XEHXspz4rv5pB5wiiSsVDu --- public/release-manifest.json | 10 +-- .../http/http-public-content-gateway.ts | 32 ++++++--- .../management/canonical-source.json | 2 +- .../contracts/public/canonical-source.json | 4 +- .../tech-log/contracts/public/generated.ts | 5 ++ .../contracts/public/public-api.openapi.yaml | 17 +++++ .../contracts/studio/canonical-source.json | 2 +- .../tech-log/public-reference-mapping.test.ts | 68 +++++++++++++++++++ 8 files changed, 121 insertions(+), 19 deletions(-) create mode 100644 tests/features/tech-log/public-reference-mapping.test.ts diff --git a/public/release-manifest.json b/public/release-manifest.json index e7984ca..7284e0e 100644 --- a/public/release-manifest.json +++ b/public/release-manifest.json @@ -38,28 +38,28 @@ }, "contractSet": { "setAlgorithm": "CA_CONTRACT_SET_V1", - "setDigest": "sha256:7832d43886cf12e6569ffd28e3f033d8a64a88e673e31449726d6dc450fa62ae", + "setDigest": "sha256:bafc39b2210dc792a763567b3a0f69da42004949899a8ec289c8c0ea9ad7a064", "packages": [ { "packageId": "@tech-log/management-contract", "version": "1.0.0", "digest": "sha256:72650735061fde627f5037571eb986cb758f44a546f065c88408399f8eec4a55", "runtimeProtocolVersion": 1, - "sourceRevision": "83148b2" + "sourceRevision": "ff0c12a" }, { "packageId": "@tech-log/public-contract", "version": "2.1.0", - "digest": "sha256:702d6666a8feba9899c7eb7c2a94a0880bcb23b178c7ed2009c6e69d9a1c848c", + "digest": "sha256:34efa8d2fdba959373081e5b2aace252be86bef243a80c238d8c4af50fa8eb0a", "runtimeProtocolVersion": 1, - "sourceRevision": "83148b2" + "sourceRevision": "ff0c12a" }, { "packageId": "@tech-log/studio-contract", "version": "3.1.0", "digest": "sha256:18dd46898be64b07f7e826409d19347512613ee2e22420028a4a0644f50f37dd", "runtimeProtocolVersion": 1, - "sourceRevision": "83148b2" + "sourceRevision": "ff0c12a" } ] } diff --git a/src/features/tech-log/adapters/http/http-public-content-gateway.ts b/src/features/tech-log/adapters/http/http-public-content-gateway.ts index f8bf206..ea70279 100644 --- a/src/features/tech-log/adapters/http/http-public-content-gateway.ts +++ b/src/features/tech-log/adapters/http/http-public-content-gateway.ts @@ -20,7 +20,6 @@ import { decisionItemToDecision, flattenRelations, knowledgeListItemToRecord, - markdownLines, markdownSections, questionListItemToRecord, releaseDetailToRelease, @@ -193,7 +192,7 @@ export function createHttpPublicContentGateway( return Object.freeze({ ...baseOf("REFERENCE", slug, { title: body.title as string, - summary: body.purposeSummary as string, + summary: body.scopeSummary as string, path: canonicalPath, primaryTopic: body.primaryTopic as never, primaryProject: body.primaryProject as never, @@ -205,16 +204,29 @@ export function createHttpPublicContentGateway( }), }), kind: "REFERENCE", - purpose: (body.purposeSummary as string) ?? "", + /* + 여기서 읽는 이름은 계약이 실제로 주는 이름이어야 한다. 한때 `purposeSummary`, + `applyWhenMarkdown`, `exceptionsMarkdown`, `examplesMarkdown` 을 읽었는데 계약에는 그런 + 칸이 없다 — 전부 undefined 로 떨어져 공개 Reference 화면이 통째로 비었다. Studio 에서는 + 같은 글이 다 보이므로 "공개 쪽만 안 나온다" 로 드러났다. + + 규칙과 예시는 `content` 마크다운을 잘라 만드는 것이 아니라 계약이 구조로 준다. Studio 의 + 편집기가 제목과 본문을 따로 받기 때문이다. + */ + purpose: (body.scopeSummary as string) ?? "", rules: Object.freeze( - markdownSections(body.content as string).map((section) => ({ - title: section.title, - body: section.paragraphs.join("\n"), - })), + ((body.rules as readonly Readonly>[] | undefined) ?? []).map( + (rule) => ({ + title: String(rule.title ?? ""), + body: String(rule.body ?? ""), + }), + ), ), - applyWhen: Object.freeze(markdownLines(body.applyWhenMarkdown as string)), - exceptions: Object.freeze(markdownLines(body.exceptionsMarkdown as string)), - examples: Object.freeze(markdownLines(body.examplesMarkdown as string)), + applyWhen: Object.freeze(((body.appliesTo as readonly string[] | undefined) ?? []).map(String)), + exceptions: Object.freeze( + ((body.excludedScope as readonly string[] | undefined) ?? []).map(String), + ), + examples: Object.freeze(((body.examples as readonly string[] | undefined) ?? []).map(String)), verifiedAt: dateLabel(body.lastVerifiedAt as string), }) as unknown as Extract; } diff --git a/src/features/tech-log/contracts/management/canonical-source.json b/src/features/tech-log/contracts/management/canonical-source.json index 5bf6ed9..3c1a4f3 100644 --- a/src/features/tech-log/contracts/management/canonical-source.json +++ b/src/features/tech-log/contracts/management/canonical-source.json @@ -2,7 +2,7 @@ "packageId": "@tech-log/management-contract", "version": "1.0.0", "digest": "sha256:72650735061fde627f5037571eb986cb758f44a546f065c88408399f8eec4a55", - "sourceRevision": "83148b2", + "sourceRevision": "ff0c12a", "operationIds": [ "createCaseDraft", "getCaseForEdit", diff --git a/src/features/tech-log/contracts/public/canonical-source.json b/src/features/tech-log/contracts/public/canonical-source.json index c150146..b2d328c 100644 --- a/src/features/tech-log/contracts/public/canonical-source.json +++ b/src/features/tech-log/contracts/public/canonical-source.json @@ -1,8 +1,8 @@ { "packageId": "@tech-log/public-contract", "version": "2.1.0", - "digest": "sha256:702d6666a8feba9899c7eb7c2a94a0880bcb23b178c7ed2009c6e69d9a1c848c", - "sourceRevision": "83148b2", + "digest": "sha256:34efa8d2fdba959373081e5b2aace252be86bef243a80c238d8c4af50fa8eb0a", + "sourceRevision": "ff0c12a", "operationIds": [ "getPublicSite", "getPublicHome", diff --git a/src/features/tech-log/contracts/public/generated.ts b/src/features/tech-log/contracts/public/generated.ts index 7dd55ad..2957253 100644 --- a/src/features/tech-log/contracts/public/generated.ts +++ b/src/features/tech-log/contracts/public/generated.ts @@ -574,6 +574,11 @@ export interface components { scopeSummary: string; appliesTo: string[]; excludedScope: string[]; + rules?: { + title: string; + body: string; + }[]; + examples?: string[]; /** @enum {string} */ freshnessStatus: "CURRENT" | "REVIEW_DUE" | "HISTORICAL"; content: string; diff --git a/src/features/tech-log/contracts/public/public-api.openapi.yaml b/src/features/tech-log/contracts/public/public-api.openapi.yaml index f40b592..bc34691 100644 --- a/src/features/tech-log/contracts/public/public-api.openapi.yaml +++ b/src/features/tech-log/contracts/public/public-api.openapi.yaml @@ -1335,6 +1335,23 @@ components: type: array items: type: string + # Reference 의 본문은 `content` 마크다운이 아니라 이 두 칸에 있다. Studio 의 Reference + # 편집기는 규칙(제목+본문)과 예시를 따로 받고 body_markdown 은 비워 두므로, 이것을 + # 내보내지 않으면 공개 화면에 판단 기준과 예시가 통째로 빠진다. + rules: + type: array + items: + type: object + required: [title, body] + properties: + title: + type: string + body: + type: string + examples: + type: array + items: + type: string freshnessStatus: type: string enum: diff --git a/src/features/tech-log/contracts/studio/canonical-source.json b/src/features/tech-log/contracts/studio/canonical-source.json index daf319a..bab32c8 100644 --- a/src/features/tech-log/contracts/studio/canonical-source.json +++ b/src/features/tech-log/contracts/studio/canonical-source.json @@ -2,7 +2,7 @@ "packageId": "@tech-log/studio-contract", "version": "3.1.0", "digest": "sha256:18dd46898be64b07f7e826409d19347512613ee2e22420028a4a0644f50f37dd", - "sourceRevision": "83148b2", + "sourceRevision": "ff0c12a", "operationIds": [ "getStudioSession", "getStudioDashboard", diff --git a/tests/features/tech-log/public-reference-mapping.test.ts b/tests/features/tech-log/public-reference-mapping.test.ts new file mode 100644 index 0000000..1499c18 --- /dev/null +++ b/tests/features/tech-log/public-reference-mapping.test.ts @@ -0,0 +1,68 @@ +import { strict as assert } from "node:assert"; +import { test } from "vitest"; + +import type { components } from "../../../src/features/tech-log/contracts/public/generated.ts"; + +type ReferenceBody = + components["schemas"]["ReferenceDetailResponse"]["reference"]; + +/* + 이 파일은 사고 하나에서 나왔다. + + 공개 Reference 화면이 통째로 비어 있었다. Studio 에서는 같은 글이 다 보였으므로 "공개 쪽만 안 + 나온다" 로 드러났다. 원인은 게이트웨이가 읽던 이름이 계약에 없는 것들이었다는 것이다 — + `purposeSummary`, `applyWhenMarkdown`, `exceptionsMarkdown`, `examplesMarkdown`. 전부 undefined 로 + 떨어졌고 타입 검사는 `as string` 단언 때문에 아무 말도 하지 않았다. + + 그래서 여기서 확인하는 것은 값이 아니라 **이름**이다. 계약이 그 칸을 갖고 있는가, 그리고 화면이 + 기대하는 모양인가. +*/ +test("the public Reference contract keeps the fields the screen reads", () => { + const reference = { + title: "", + scopeSummary: "", + appliesTo: [], + excludedScope: [], + rules: [{ title: "", body: "" }], + examples: [], + freshnessStatus: "CURRENT", + content: "", + contentFormat: "MARKDOWN", + contentFormatVersion: 1, + tags: [], + publishedAt: "", + updatedAt: "", + } satisfies ReferenceBody; + + // 화면이 읽는 이름 그대로. 하나라도 계약에서 사라지면 위의 `satisfies` 가 먼저 깨진다. + assert.deepEqual(Object.keys(reference).sort(), [ + "appliesTo", + "content", + "contentFormat", + "contentFormatVersion", + "examples", + "excludedScope", + "freshnessStatus", + "publishedAt", + "rules", + "scopeSummary", + "tags", + "title", + "updatedAt", + ]); +}); + +/* + Reference 의 본문은 `content` 마크다운이 아니라 규칙과 예시에 있다. Studio 의 편집기가 제목과 + 본문을 따로 받고 `body_markdown` 은 비워 두기 때문이다 — 그래서 `content` 를 잘라 규칙을 만들려 + 하면 언제나 빈 목록이 된다. +*/ +test("a rule carries its own title and body, not a slice of markdown", () => { + const rule: NonNullable[number] = { + title: "Authorization Endpoint에는 client_secret을 보내지 않는다", + body: "이 요청은 브라우저의 full-page navigation으로 나간다.", + }; + + assert.equal(typeof rule.title, "string"); + assert.equal(typeof rule.body, "string"); +});