fix: 공개 Reference 가 계약이 주는 이름을 읽게 한다
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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XEHXspz4rv5pB5wiiSsVDu
This commit is contained in:
co-authored by
Claude Opus 5
parent
3036b8d788
commit
7211dd1a92
@@ -38,28 +38,28 @@
|
|||||||
},
|
},
|
||||||
"contractSet": {
|
"contractSet": {
|
||||||
"setAlgorithm": "CA_CONTRACT_SET_V1",
|
"setAlgorithm": "CA_CONTRACT_SET_V1",
|
||||||
"setDigest": "sha256:7832d43886cf12e6569ffd28e3f033d8a64a88e673e31449726d6dc450fa62ae",
|
"setDigest": "sha256:bafc39b2210dc792a763567b3a0f69da42004949899a8ec289c8c0ea9ad7a064",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"packageId": "@tech-log/management-contract",
|
"packageId": "@tech-log/management-contract",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"digest": "sha256:72650735061fde627f5037571eb986cb758f44a546f065c88408399f8eec4a55",
|
"digest": "sha256:72650735061fde627f5037571eb986cb758f44a546f065c88408399f8eec4a55",
|
||||||
"runtimeProtocolVersion": 1,
|
"runtimeProtocolVersion": 1,
|
||||||
"sourceRevision": "83148b2"
|
"sourceRevision": "ff0c12a"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"packageId": "@tech-log/public-contract",
|
"packageId": "@tech-log/public-contract",
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"digest": "sha256:702d6666a8feba9899c7eb7c2a94a0880bcb23b178c7ed2009c6e69d9a1c848c",
|
"digest": "sha256:34efa8d2fdba959373081e5b2aace252be86bef243a80c238d8c4af50fa8eb0a",
|
||||||
"runtimeProtocolVersion": 1,
|
"runtimeProtocolVersion": 1,
|
||||||
"sourceRevision": "83148b2"
|
"sourceRevision": "ff0c12a"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"packageId": "@tech-log/studio-contract",
|
"packageId": "@tech-log/studio-contract",
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"digest": "sha256:18dd46898be64b07f7e826409d19347512613ee2e22420028a4a0644f50f37dd",
|
"digest": "sha256:18dd46898be64b07f7e826409d19347512613ee2e22420028a4a0644f50f37dd",
|
||||||
"runtimeProtocolVersion": 1,
|
"runtimeProtocolVersion": 1,
|
||||||
"sourceRevision": "83148b2"
|
"sourceRevision": "ff0c12a"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
decisionItemToDecision,
|
decisionItemToDecision,
|
||||||
flattenRelations,
|
flattenRelations,
|
||||||
knowledgeListItemToRecord,
|
knowledgeListItemToRecord,
|
||||||
markdownLines,
|
|
||||||
markdownSections,
|
markdownSections,
|
||||||
questionListItemToRecord,
|
questionListItemToRecord,
|
||||||
releaseDetailToRelease,
|
releaseDetailToRelease,
|
||||||
@@ -193,7 +192,7 @@ export function createHttpPublicContentGateway(
|
|||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
...baseOf("REFERENCE", slug, {
|
...baseOf("REFERENCE", slug, {
|
||||||
title: body.title as string,
|
title: body.title as string,
|
||||||
summary: body.purposeSummary as string,
|
summary: body.scopeSummary as string,
|
||||||
path: canonicalPath,
|
path: canonicalPath,
|
||||||
primaryTopic: body.primaryTopic as never,
|
primaryTopic: body.primaryTopic as never,
|
||||||
primaryProject: body.primaryProject as never,
|
primaryProject: body.primaryProject as never,
|
||||||
@@ -205,16 +204,29 @@ export function createHttpPublicContentGateway(
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
kind: "REFERENCE",
|
kind: "REFERENCE",
|
||||||
purpose: (body.purposeSummary as string) ?? "",
|
/*
|
||||||
|
여기서 읽는 이름은 계약이 실제로 주는 이름이어야 한다. 한때 `purposeSummary`,
|
||||||
|
`applyWhenMarkdown`, `exceptionsMarkdown`, `examplesMarkdown` 을 읽었는데 계약에는 그런
|
||||||
|
칸이 없다 — 전부 undefined 로 떨어져 공개 Reference 화면이 통째로 비었다. Studio 에서는
|
||||||
|
같은 글이 다 보이므로 "공개 쪽만 안 나온다" 로 드러났다.
|
||||||
|
|
||||||
|
규칙과 예시는 `content` 마크다운을 잘라 만드는 것이 아니라 계약이 구조로 준다. Studio 의
|
||||||
|
편집기가 제목과 본문을 따로 받기 때문이다.
|
||||||
|
*/
|
||||||
|
purpose: (body.scopeSummary as string) ?? "",
|
||||||
rules: Object.freeze(
|
rules: Object.freeze(
|
||||||
markdownSections(body.content as string).map((section) => ({
|
((body.rules as readonly Readonly<Record<string, unknown>>[] | undefined) ?? []).map(
|
||||||
title: section.title,
|
(rule) => ({
|
||||||
body: section.paragraphs.join("\n"),
|
title: String(rule.title ?? ""),
|
||||||
})),
|
body: String(rule.body ?? ""),
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
applyWhen: Object.freeze(markdownLines(body.applyWhenMarkdown as string)),
|
applyWhen: Object.freeze(((body.appliesTo as readonly string[] | undefined) ?? []).map(String)),
|
||||||
exceptions: Object.freeze(markdownLines(body.exceptionsMarkdown as string)),
|
exceptions: Object.freeze(
|
||||||
examples: Object.freeze(markdownLines(body.examplesMarkdown as string)),
|
((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),
|
verifiedAt: dateLabel(body.lastVerifiedAt as string),
|
||||||
}) as unknown as Extract<PublicRecord, { kind: K }>;
|
}) as unknown as Extract<PublicRecord, { kind: K }>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"packageId": "@tech-log/management-contract",
|
"packageId": "@tech-log/management-contract",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"digest": "sha256:72650735061fde627f5037571eb986cb758f44a546f065c88408399f8eec4a55",
|
"digest": "sha256:72650735061fde627f5037571eb986cb758f44a546f065c88408399f8eec4a55",
|
||||||
"sourceRevision": "83148b2",
|
"sourceRevision": "ff0c12a",
|
||||||
"operationIds": [
|
"operationIds": [
|
||||||
"createCaseDraft",
|
"createCaseDraft",
|
||||||
"getCaseForEdit",
|
"getCaseForEdit",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"packageId": "@tech-log/public-contract",
|
"packageId": "@tech-log/public-contract",
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"digest": "sha256:702d6666a8feba9899c7eb7c2a94a0880bcb23b178c7ed2009c6e69d9a1c848c",
|
"digest": "sha256:34efa8d2fdba959373081e5b2aace252be86bef243a80c238d8c4af50fa8eb0a",
|
||||||
"sourceRevision": "83148b2",
|
"sourceRevision": "ff0c12a",
|
||||||
"operationIds": [
|
"operationIds": [
|
||||||
"getPublicSite",
|
"getPublicSite",
|
||||||
"getPublicHome",
|
"getPublicHome",
|
||||||
|
|||||||
@@ -574,6 +574,11 @@ export interface components {
|
|||||||
scopeSummary: string;
|
scopeSummary: string;
|
||||||
appliesTo: string[];
|
appliesTo: string[];
|
||||||
excludedScope: string[];
|
excludedScope: string[];
|
||||||
|
rules?: {
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
}[];
|
||||||
|
examples?: string[];
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
freshnessStatus: "CURRENT" | "REVIEW_DUE" | "HISTORICAL";
|
freshnessStatus: "CURRENT" | "REVIEW_DUE" | "HISTORICAL";
|
||||||
content: string;
|
content: string;
|
||||||
|
|||||||
@@ -1335,6 +1335,23 @@ components:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
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:
|
freshnessStatus:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"packageId": "@tech-log/studio-contract",
|
"packageId": "@tech-log/studio-contract",
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"digest": "sha256:18dd46898be64b07f7e826409d19347512613ee2e22420028a4a0644f50f37dd",
|
"digest": "sha256:18dd46898be64b07f7e826409d19347512613ee2e22420028a4a0644f50f37dd",
|
||||||
"sourceRevision": "83148b2",
|
"sourceRevision": "ff0c12a",
|
||||||
"operationIds": [
|
"operationIds": [
|
||||||
"getStudioSession",
|
"getStudioSession",
|
||||||
"getStudioDashboard",
|
"getStudioDashboard",
|
||||||
|
|||||||
@@ -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<ReferenceBody["rules"]>[number] = {
|
||||||
|
title: "Authorization Endpoint에는 client_secret을 보내지 않는다",
|
||||||
|
body: "이 요청은 브라우저의 full-page navigation으로 나간다.",
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.equal(typeof rule.title, "string");
|
||||||
|
assert.equal(typeof rule.body, "string");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user