Files
DongHyeonkaandClaude Opus 5 b3aa304975 feat: 프로젝트를 공개할 수 있게 하고, 홈이 무엇을 앞에 둘지 고를 수 있게 한다
공개 화면 다섯 곳이 조용히 비어 있었다. 원인은 하나씩 달랐지만 모두 "값을 채울
방법이 없었다"는 같은 모양이었다.

홈의 "지금 집중하는 것" — `home_focus_config` 는 마이그레이션이 빈 행 하나만
넣어 두었고, 계약에 선언된 `getHomeFocus`/`updateHomeFocus` 는 구현이 없었다.
세 슬롯이 모두 비면 홈은 그 영역을 아예 그리지 않으므로, 운영에서는 한 번도
나타난 적이 없다. Studio 대시보드에 고르는 화면을 둔다.

홈의 "최근 기록" — 화면이 공개된 프로젝트를 하나씩 돌며 타임라인을 조립했다.
그래서 게시한 문서라도 그 프로젝트가 공개되어 있지 않으면 목록에서 통째로
빠졌고, 실제로 릴리스 한 줄만 남았다. 무엇이 최근인지는 공개 투영이 이미 알고
있으므로 그것을 그대로 읽는다. 프로젝트마다 요청을 보내던 N+1 도 사라진다.

프로젝트 공개 — 프로젝트는 `RecordKind` 에 없어 문서 게시 파이프라인을 타지
못하는데, 공개 화면들(프로젝트 목록·프로필의 "현재 프로젝트"·홈 focus)은 전부
`public_resource_projection` 의 PROJECT 행을 가시성 관문으로 쓴다. 그 행을
세우는 경로가 없었으므로 프로젝트는 영원히 비공개였다. 계약에 이미 있던
`publishProject`/`unpublishProject` 를 구현하고 주제·프로젝트 화면에 버튼을 둔다.

문서 사이 관계 연결 — `JdbcCatalogQueryAdapter` 의 RELATION/EVIDENCE 가
`List.of()` 스텁이라 어떤 기록도 연결 대상 목록을 채울 수 없었다. RELATION 은
작성 중에 고르는 것이므로 작업본까지 포함하고, EVIDENCE 는 읽는 사람이 따라갈
수 있어야 하므로 공개된 것만 포함한다.

본문 너비 — 문서 한 편이 세 폭으로 갈라져 있었다. 머리말 920px, 유형·프로젝트
줄은 shell 전체 1180px, 본문은 672px 를 가운데 정렬. 셋을 같은 폭·같은 왼쪽
끝에 세우고 읽는 단을 56rem 으로 넓힌다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEHXspz4rv5pB5wiiSsVDu
2026-08-23 17:38:49 +09:00

67 lines
3.2 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { createHash } from "node:crypto";
import { test } from "vitest";
import canonicalSource from "../../../src/features/tech-log/contracts/studio/canonical-source.json" with { type: "json" };
import ciGates from "../../../config/ci/gates.json" with { type: "json" };
import packageDocument from "../../../package.json" with { type: "json" };
const YAML_PATH = "src/features/tech-log/contracts/studio/studio-api.openapi.yaml";
const DRIFT_GATE_SCRIPT = "check:tech-log-contract";
test("vendored contract matches the recorded canonical digest", () => {
const bytes = readFileSync(YAML_PATH);
const digest = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
assert.equal(digest, canonicalSource.digest);
});
test("canonical source records the pinned revision and version", () => {
assert.equal(canonicalSource.packageId, "@tech-log/studio-contract");
assert.equal(canonicalSource.version, "3.1.0");
// revision은 생성 시점에 기록된다. canonical 저장소는 활발히 편집 중이므로
// 특정 값을 박아두면 계약이 그대로인데도 테스트가 깨진다. 형식만 고정한다.
assert.match(canonicalSource.sourceRevision, /^[0-9a-f]{7,64}$/);
assert.match(canonicalSource.digest, /^sha256:[0-9a-f]{64}$/);
});
test("canonical source lists all 19 operationIds", () => {
assert.equal(canonicalSource.operationIds.length, 19);
assert.ok(canonicalSource.operationIds.includes("uploadStudioAsset"));
assert.ok(canonicalSource.operationIds.includes("getStudioSession"));
});
test("vendored contract declares the CSRF header", () => {
const yaml = readFileSync(YAML_PATH, "utf8");
assert.ok(yaml.includes("X-CSRF-TOKEN"));
});
// 이 브랜치의 중심 산출물은 "canonical 계약이 다시 갈라지지 못하게 빌드로 막는다"
// (§선택한 접근 A)이다. 스크립트가 존재하는 것만으로는 그 약속이 지켜지지 않는다.
// 누군가 손으로 vendor yaml이나 generated.ts를 고쳐도, 실제로 실행되는 게이트가
// 하나도 그것을 보지 않으면 digest 고정은 의미를 잃는다. 아래 두 테스트가
// "실행 경로에 실제로 연결돼 있는가"를 검증한다.
test("the contract drift check is a real CI gate command, not just a package script", () => {
const command = ciGates.commands.find((entry) => entry.script === DRIFT_GATE_SCRIPT);
assert.ok(command, `config/ci/gates.json declares no ${DRIFT_GATE_SCRIPT} command`);
assert.equal(command.expect, "pass");
const owningGates = ciGates.gates.filter((gate) =>
(gate.commandIds as readonly string[]).includes(command.id),
);
assert.equal(
owningGates.length,
1,
`${command.id} must be referenced by exactly one gate; found ${owningGates.length}`,
);
});
test("test:all runs the contract drift check alongside the TechLog suite", () => {
const segments = packageDocument.scripts["test:all"].split(" && ").map((value) => value.trim());
assert.ok(
segments.includes(`corepack pnpm ${DRIFT_GATE_SCRIPT}`),
`test:all does not run ${DRIFT_GATE_SCRIPT}: ${packageDocument.scripts["test:all"]}`,
);
assert.ok(segments.includes("corepack pnpm test:tech-log"), packageDocument.scripts["test:all"]);
});