refactor: 리펙토링
This commit is contained in:
@@ -0,0 +1,856 @@
|
||||
# 플랫폼 구성 화면 완성 구현 계획
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** `/examples/platform` 화면이 정적 선택뿐 아니라 런타임 해석 결과까지 보여주고, 진입 경로와 브라우저 회귀 커버리지를 갖춘다.
|
||||
|
||||
**Architecture:** 능력 상태 파생은 `src/contracts`의 순수 함수가 소유한다. 합성 루트가 런타임 오버라이드를 그 함수에 넣어 경계된 스냅샷을 만들고, 애플리케이션 포트를 통해 표현 계층에 전달한다. 표현 계층은 bootstrap을 알지 못한 채 해석 결과를 읽는다.
|
||||
|
||||
**Tech Stack:** TypeScript 7 (strict), React 19, Zod 4, Vitest 4, Playwright 1.62
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- `src/presentation`은 `src/adapters`와 `src/bootstrap`을 import 할 수 없다 (`presentation-does-not-know-adapters`).
|
||||
- `src/application`은 `src/presentation`, `src/adapters`, `src/bootstrap`, `react`, `@tanstack`을 import 할 수 없다.
|
||||
- 모든 소스는 `.ts` / `.tsx`. `allowJs: false`.
|
||||
- `src/presentation/{boundaries,components,design-system,forms,layouts,routes,templates}`에는 한글 리터럴을 둘 수 없다. `src/presentation/examples`는 대상이 아니다.
|
||||
- i18n 카탈로그는 ko-KR / en-US / pseudo 키 집합이 완전히 일치해야 한다.
|
||||
- 화면은 레지스트리에서 파생한 값만 렌더한다. 샘플 기능 이름을 하드코딩하면 `test:sample-removal`의 잔재 스캔이 실패한다.
|
||||
- 각 태스크는 `corepack pnpm check:types`, `corepack pnpm lint`, `corepack pnpm check:architecture`를 통과해야 한다.
|
||||
|
||||
## 배경: 왜 이 작업이 남았나
|
||||
|
||||
`docs/superpowers/specs/2026-07-31-platform-overview-page-design.md` 5절이 런타임 해석 결과를 의도적으로 제외했다. 해석은 `createRuntimeComposition`에서 일어나고 그 결과가 애플리케이션 계층으로 전달되지 않기 때문이다. 그래서 화면은 "정적 선택 기준"이라고만 말할 수 있고, 운영자가 `CAPABILITY_OVERRIDES`로 능력을 껐는지는 화면에서 알 수 없다.
|
||||
|
||||
이 계획은 그 경로를 만든다. 오버라이드는 이미 선택된 능력을 끄는 것만 가능하므로, 스냅샷은 선택 수와 활성 수를 함께 실어 둘의 차이가 곧 오버라이드 효과가 되게 한다.
|
||||
|
||||
## 파일 구조
|
||||
|
||||
| 파일 | 책임 |
|
||||
| --- | --- |
|
||||
| `src/contracts/runtime-capabilities.ts` (수정) | 능력 스냅샷 파생 순수 함수를 추가한다. 기존 `resolveRuntimeCapabilities`와 같은 입력을 받는다. |
|
||||
| `src/application/ports/runtime-capabilities-port.ts` (생성) | 애플리케이션이 능력 상태를 얻는 출력 포트. |
|
||||
| `src/application/create-application.ts` (수정) | `runtime.getCapabilitySnapshot()`을 인바운드 API에 노출한다. |
|
||||
| `src/application/ports/in/application-api.ts` (수정) | 인바운드 계약에 스냅샷 접근자를 선언한다. |
|
||||
| `src/bootstrap/runtime-adapters.ts` (수정) | 런타임 설정의 오버라이드로 포트를 합성한다. |
|
||||
| `src/presentation/examples/platform-overview-page.tsx` (수정) | 5번 섹션이 선택과 활성을 함께 표시한다. |
|
||||
| `src/presentation/pages/home-page.tsx` (수정) | 플랫폼 구성 화면 진입점. |
|
||||
| `tests/unit/runtime-capability-snapshot.test.ts` (생성) | 순수 함수 검증. |
|
||||
| `tests/unit/runtime-adapters.test.ts` (수정) | 포트 합성 검증. |
|
||||
| `tests/component/platform-overview-page.test.tsx` (수정) | 화면이 해석 결과를 표시하는지 검증. |
|
||||
| `tests/e2e/platform-overview.spec.ts` (생성) | 실제 번들에서 파생 불변식 검증. |
|
||||
| `tests/visual/platform.visual.spec.ts` (수정) | 시각 회귀 기준선. |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 능력 스냅샷 파생 계약
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/contracts/runtime-capabilities.ts`
|
||||
- Test: `tests/unit/runtime-capability-snapshot.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: 같은 파일의 `InstalledRuntimeCapabilities`, `CapabilityOverrideMap`, `RuntimeCapabilityOverride`
|
||||
- Produces: `RuntimeCapabilityId`, `RuntimeCapabilityStatus`, `RuntimeCapabilitySnapshot`, `describeRuntimeCapabilities(installed, overrides)`
|
||||
|
||||
- [ ] **Step 1: 실패하는 테스트 작성**
|
||||
|
||||
`tests/unit/runtime-capability-snapshot.test.ts`:
|
||||
|
||||
```ts
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
describeRuntimeCapabilities,
|
||||
type CapabilityOverrideMap,
|
||||
type InstalledRuntimeCapabilities,
|
||||
} from "../../src/contracts/runtime-capabilities.ts";
|
||||
|
||||
const DEFAULTS: CapabilityOverrideMap = Object.freeze({
|
||||
REALTIME: "DEFAULT",
|
||||
WEB_WORKER: "DEFAULT",
|
||||
SERVICE_WORKER: "DEFAULT",
|
||||
OFFLINE_COMMANDS: "DEFAULT",
|
||||
});
|
||||
|
||||
const EMPTY: InstalledRuntimeCapabilities = Object.freeze({
|
||||
realtime: Object.freeze([]),
|
||||
webWorkers: Object.freeze([]),
|
||||
serviceWorker: null,
|
||||
offlineCommands: null,
|
||||
});
|
||||
|
||||
const SELECTED: InstalledRuntimeCapabilities = Object.freeze({
|
||||
realtime: Object.freeze([]),
|
||||
webWorkers: Object.freeze([]),
|
||||
serviceWorker: Object.freeze({ mode: "ACTIVE" as const }),
|
||||
offlineCommands: null,
|
||||
});
|
||||
|
||||
describe("runtime capability snapshot", () => {
|
||||
it("describes every capability id in a fixed order", () => {
|
||||
const snapshot = describeRuntimeCapabilities(EMPTY, DEFAULTS);
|
||||
|
||||
expect(snapshot.map((status) => status.capabilityId)).toEqual([
|
||||
"REALTIME",
|
||||
"WEB_WORKER",
|
||||
"SERVICE_WORKER",
|
||||
"OFFLINE_COMMANDS",
|
||||
]);
|
||||
});
|
||||
|
||||
it("reports nothing selected and nothing active for an empty selection", () => {
|
||||
const snapshot = describeRuntimeCapabilities(EMPTY, DEFAULTS);
|
||||
|
||||
for (const status of snapshot) {
|
||||
expect(status.selected).toBe(0);
|
||||
expect(status.active).toBe(0);
|
||||
expect(status.override).toBe("DEFAULT");
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps a selected capability active while the override is DEFAULT", () => {
|
||||
const snapshot = describeRuntimeCapabilities(SELECTED, DEFAULTS);
|
||||
const serviceWorker = snapshot.find(
|
||||
(status) => status.capabilityId === "SERVICE_WORKER",
|
||||
);
|
||||
|
||||
expect(serviceWorker).toMatchObject({
|
||||
selected: 1,
|
||||
active: 1,
|
||||
override: "DEFAULT",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a disabled capability selected but not active", () => {
|
||||
const snapshot = describeRuntimeCapabilities(SELECTED, {
|
||||
...DEFAULTS,
|
||||
SERVICE_WORKER: "DISABLED",
|
||||
});
|
||||
const serviceWorker = snapshot.find(
|
||||
(status) => status.capabilityId === "SERVICE_WORKER",
|
||||
);
|
||||
|
||||
expect(serviceWorker).toMatchObject({
|
||||
selected: 1,
|
||||
active: 0,
|
||||
override: "DISABLED",
|
||||
});
|
||||
});
|
||||
|
||||
it("cannot activate a capability that was never selected", () => {
|
||||
const snapshot = describeRuntimeCapabilities(EMPTY, {
|
||||
...DEFAULTS,
|
||||
REALTIME: "DEFAULT",
|
||||
});
|
||||
const realtime = snapshot.find(
|
||||
(status) => status.capabilityId === "REALTIME",
|
||||
);
|
||||
|
||||
expect(realtime).toMatchObject({ selected: 0, active: 0 });
|
||||
});
|
||||
|
||||
it("returns a frozen snapshot and frozen entries", () => {
|
||||
const snapshot = describeRuntimeCapabilities(EMPTY, DEFAULTS);
|
||||
|
||||
expect(Object.isFrozen(snapshot)).toBe(true);
|
||||
expect(snapshot.every((status) => Object.isFrozen(status))).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 실패 확인**
|
||||
|
||||
실행: `corepack pnpm exec vitest run tests/unit/runtime-capability-snapshot.test.ts`
|
||||
기대: `describeRuntimeCapabilities` is not exported 로 FAIL
|
||||
|
||||
- [ ] **Step 3: 최소 구현**
|
||||
|
||||
`src/contracts/runtime-capabilities.ts` 끝에 추가한다. `resolveRuntimeCapabilities`를 재사용해 활성 판정 규칙이 한 곳에만 존재하게 한다.
|
||||
|
||||
```ts
|
||||
export type RuntimeCapabilityId =
|
||||
| "REALTIME"
|
||||
| "WEB_WORKER"
|
||||
| "SERVICE_WORKER"
|
||||
| "OFFLINE_COMMANDS";
|
||||
|
||||
/**
|
||||
* §3.5. A bounded, serialisable view of what a capability is. `selected` is the
|
||||
* static SSOT count and `active` is what survived the runtime override, so the
|
||||
* difference between the two is exactly the operator's effect.
|
||||
*/
|
||||
export type RuntimeCapabilityStatus = Readonly<{
|
||||
capabilityId: RuntimeCapabilityId;
|
||||
selected: number;
|
||||
active: number;
|
||||
override: RuntimeCapabilityOverride;
|
||||
}>;
|
||||
|
||||
export type RuntimeCapabilitySnapshot = readonly RuntimeCapabilityStatus[];
|
||||
|
||||
export function describeRuntimeCapabilities(
|
||||
installed: InstalledRuntimeCapabilities,
|
||||
overrides: CapabilityOverrideMap,
|
||||
): RuntimeCapabilitySnapshot {
|
||||
const resolved = resolveRuntimeCapabilities(installed, overrides);
|
||||
const counts: Readonly<
|
||||
Record<RuntimeCapabilityId, Readonly<{ selected: number; active: number }>>
|
||||
> = {
|
||||
REALTIME: {
|
||||
selected: installed.realtime.length,
|
||||
active: resolved.realtime.length,
|
||||
},
|
||||
WEB_WORKER: {
|
||||
selected: installed.webWorkers.length,
|
||||
active: resolved.webWorkers.length,
|
||||
},
|
||||
SERVICE_WORKER: {
|
||||
selected: installed.serviceWorker === null ? 0 : 1,
|
||||
active: resolved.serviceWorker === null ? 0 : 1,
|
||||
},
|
||||
OFFLINE_COMMANDS: {
|
||||
selected: installed.offlineCommands === null ? 0 : 1,
|
||||
active: resolved.offlineCommands === null ? 0 : 1,
|
||||
},
|
||||
};
|
||||
|
||||
return Object.freeze(
|
||||
(
|
||||
[
|
||||
"REALTIME",
|
||||
"WEB_WORKER",
|
||||
"SERVICE_WORKER",
|
||||
"OFFLINE_COMMANDS",
|
||||
] as const
|
||||
).map((capabilityId) =>
|
||||
Object.freeze({
|
||||
capabilityId,
|
||||
selected: counts[capabilityId].selected,
|
||||
active: counts[capabilityId].active,
|
||||
override: overrides[capabilityId],
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 통과 확인**
|
||||
|
||||
실행: `corepack pnpm exec vitest run tests/unit/runtime-capability-snapshot.test.ts`
|
||||
기대: 6 passed
|
||||
|
||||
- [ ] **Step 5: 게이트와 커밋**
|
||||
|
||||
```bash
|
||||
corepack pnpm check:types && corepack pnpm lint && corepack pnpm check:architecture
|
||||
git add src/contracts/runtime-capabilities.ts tests/unit/runtime-capability-snapshot.test.ts
|
||||
git commit -m "feat: derive a bounded runtime capability snapshot"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 능력 스냅샷을 애플리케이션 포트로 노출
|
||||
|
||||
**Files:**
|
||||
- Create: `src/application/ports/runtime-capabilities-port.ts`
|
||||
- Modify: `src/application/create-application.ts`
|
||||
- Modify: `src/application/ports/in/application-api.ts`
|
||||
- Modify: `src/bootstrap/runtime-adapters.ts`
|
||||
- Modify: `tests/helpers/create-test-application.ts`
|
||||
- Test: `tests/unit/runtime-adapters.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1의 `describeRuntimeCapabilities`, `RuntimeCapabilitySnapshot`
|
||||
- Produces: `RuntimeCapabilitiesPort` (`getSnapshot(): RuntimeCapabilitySnapshot`), `ApplicationApi.runtime.getCapabilitySnapshot(): RuntimeCapabilitySnapshot`
|
||||
|
||||
- [ ] **Step 1: 실패하는 테스트 작성**
|
||||
|
||||
`tests/unit/runtime-adapters.test.ts`의 `describe` 블록 안에 추가한다.
|
||||
|
||||
```ts
|
||||
it("reports the static selection when no capability override disables it", async () => {
|
||||
const adapters = await createRuntimeAdapters({ runtime, release, host: {} });
|
||||
|
||||
const snapshot = adapters.outputPorts.runtimeCapabilities.getSnapshot();
|
||||
|
||||
expect(snapshot.map((status) => status.capabilityId)).toEqual([
|
||||
"REALTIME",
|
||||
"WEB_WORKER",
|
||||
"SERVICE_WORKER",
|
||||
"OFFLINE_COMMANDS",
|
||||
]);
|
||||
expect(snapshot.every((status) => status.override === "DEFAULT")).toBe(true);
|
||||
});
|
||||
|
||||
it("carries a disabling override into the capability snapshot", async () => {
|
||||
const adapters = await createRuntimeAdapters({
|
||||
runtime: {
|
||||
...runtime,
|
||||
config: {
|
||||
...runtime.config,
|
||||
CAPABILITY_OVERRIDES: {
|
||||
...runtime.config.CAPABILITY_OVERRIDES,
|
||||
SERVICE_WORKER: "DISABLED",
|
||||
},
|
||||
},
|
||||
},
|
||||
release,
|
||||
host: {},
|
||||
});
|
||||
|
||||
const serviceWorker = adapters.outputPorts.runtimeCapabilities
|
||||
.getSnapshot()
|
||||
.find((status) => status.capabilityId === "SERVICE_WORKER");
|
||||
|
||||
expect(serviceWorker?.override).toBe("DISABLED");
|
||||
expect(serviceWorker?.active).toBe(0);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 실패 확인**
|
||||
|
||||
실행: `corepack pnpm exec vitest run tests/unit/runtime-adapters.test.ts`
|
||||
기대: `runtimeCapabilities` 가 `outputPorts`에 없어 타입/런타임 FAIL
|
||||
|
||||
- [ ] **Step 3: 포트 정의**
|
||||
|
||||
`src/application/ports/runtime-capabilities-port.ts` 생성:
|
||||
|
||||
```ts
|
||||
import type { RuntimeCapabilitySnapshot } from "../../contracts/runtime-capabilities.ts";
|
||||
|
||||
export type { RuntimeCapabilitySnapshot };
|
||||
|
||||
/**
|
||||
* §3.5. The application reads capability state; it never resolves it. Only the
|
||||
* composition root knows the runtime overrides.
|
||||
*/
|
||||
export type RuntimeCapabilitiesPort = Readonly<{
|
||||
getSnapshot(): RuntimeCapabilitySnapshot;
|
||||
}>;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 출력 포트 등록과 인바운드 노출**
|
||||
|
||||
`src/application/create-application.ts`의 `ApplicationOutputPorts`에 필드를 더하고, `runtime` 그룹에 접근자를 추가한다.
|
||||
|
||||
```ts
|
||||
import type { RuntimeCapabilitiesPort } from "./ports/runtime-capabilities-port.ts";
|
||||
```
|
||||
|
||||
`ApplicationOutputPorts` 안:
|
||||
|
||||
```ts
|
||||
runtimeCapabilities: RuntimeCapabilitiesPort;
|
||||
```
|
||||
|
||||
`runtime` 그룹 안, `getReleaseSummary` 다음:
|
||||
|
||||
```ts
|
||||
getCapabilitySnapshot() {
|
||||
return ports.runtimeCapabilities.getSnapshot();
|
||||
},
|
||||
```
|
||||
|
||||
`src/application/ports/in/application-api.ts`의 `runtime` 그룹에 선언을 추가하고 타입을 import 한다.
|
||||
|
||||
```ts
|
||||
import type { RuntimeCapabilitySnapshot } from "../runtime-capabilities-port.ts";
|
||||
|
||||
export type { RuntimeCapabilitySnapshot };
|
||||
```
|
||||
|
||||
```ts
|
||||
runtime: Readonly<{
|
||||
getReleaseSummary(): Promise<ReleaseSummary>;
|
||||
getCapabilitySnapshot(): RuntimeCapabilitySnapshot;
|
||||
}>;
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 합성 루트에서 포트 생성**
|
||||
|
||||
`src/bootstrap/runtime-adapters.ts`에 import를 추가한다.
|
||||
|
||||
```ts
|
||||
import {
|
||||
describeRuntimeCapabilities,
|
||||
} from "../contracts/runtime-capabilities.ts";
|
||||
import { INSTALLED_RUNTIME_CAPABILITIES } from "../features/installed-runtime-capabilities.ts";
|
||||
```
|
||||
|
||||
`releaseInfo` 정의 다음에 포트를 만들고 `outputPorts`에 넣는다.
|
||||
|
||||
```ts
|
||||
const runtimeCapabilities = Object.freeze({
|
||||
getSnapshot() {
|
||||
return describeRuntimeCapabilities(
|
||||
INSTALLED_RUNTIME_CAPABILITIES,
|
||||
config.CAPABILITY_OVERRIDES,
|
||||
);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 6: 테스트 헬퍼 기본값**
|
||||
|
||||
`tests/helpers/create-test-application.ts`의 `createApplication` 첫 인자에 추가한다.
|
||||
|
||||
```ts
|
||||
runtimeCapabilities:
|
||||
overrides.runtimeCapabilities ??
|
||||
{
|
||||
getSnapshot: () =>
|
||||
Object.freeze([
|
||||
Object.freeze({
|
||||
capabilityId: "REALTIME" as const,
|
||||
selected: 0,
|
||||
active: 0,
|
||||
override: "DEFAULT" as const,
|
||||
}),
|
||||
Object.freeze({
|
||||
capabilityId: "WEB_WORKER" as const,
|
||||
selected: 0,
|
||||
active: 0,
|
||||
override: "DEFAULT" as const,
|
||||
}),
|
||||
Object.freeze({
|
||||
capabilityId: "SERVICE_WORKER" as const,
|
||||
selected: 0,
|
||||
active: 0,
|
||||
override: "DEFAULT" as const,
|
||||
}),
|
||||
Object.freeze({
|
||||
capabilityId: "OFFLINE_COMMANDS" as const,
|
||||
selected: 0,
|
||||
active: 0,
|
||||
override: "DEFAULT" as const,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
```
|
||||
|
||||
- [ ] **Step 7: 통과 확인**
|
||||
|
||||
실행: `corepack pnpm exec vitest run tests/unit/runtime-adapters.test.ts`
|
||||
기대: 9 passed
|
||||
|
||||
- [ ] **Step 8: 게이트와 커밋**
|
||||
|
||||
```bash
|
||||
corepack pnpm check:types && corepack pnpm lint && corepack pnpm check:architecture && corepack pnpm test:all
|
||||
git add src/application src/bootstrap/runtime-adapters.ts tests/helpers/create-test-application.ts tests/unit/runtime-adapters.test.ts
|
||||
git commit -m "feat: expose the resolved runtime capability snapshot through the application port"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 화면이 선택과 활성을 함께 보여준다
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/presentation/examples/platform-overview-page.tsx`
|
||||
- Test: `tests/component/platform-overview-page.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 2의 `useApplication().runtime.getCapabilitySnapshot()`
|
||||
- Produces: 없음 (화면 종단)
|
||||
|
||||
- [ ] **Step 1: 실패하는 테스트 작성**
|
||||
|
||||
`tests/component/platform-overview-page.test.tsx`의 기존 능력 테스트를 다음 두 개로 교체한다.
|
||||
|
||||
```ts
|
||||
it("labels a capability that was never selected as unselected", () => {
|
||||
renderPage();
|
||||
|
||||
const region = screen.getByRole("region", { name: "선택적 런타임 능력" });
|
||||
|
||||
expect(within(region).getAllByText("미선택")).toHaveLength(4);
|
||||
expect(within(region).queryByText("운영자가 비활성화함")).toBeNull();
|
||||
});
|
||||
|
||||
it("separates an operator disable from a capability that was never selected", () => {
|
||||
render(
|
||||
<ApplicationProvider
|
||||
application={createTestApplication({
|
||||
runtimeCapabilities: {
|
||||
getSnapshot: () =>
|
||||
Object.freeze([
|
||||
Object.freeze({
|
||||
capabilityId: "REALTIME" as const,
|
||||
selected: 0,
|
||||
active: 0,
|
||||
override: "DEFAULT" as const,
|
||||
}),
|
||||
Object.freeze({
|
||||
capabilityId: "WEB_WORKER" as const,
|
||||
selected: 0,
|
||||
active: 0,
|
||||
override: "DEFAULT" as const,
|
||||
}),
|
||||
Object.freeze({
|
||||
capabilityId: "SERVICE_WORKER" as const,
|
||||
selected: 1,
|
||||
active: 0,
|
||||
override: "DISABLED" as const,
|
||||
}),
|
||||
Object.freeze({
|
||||
capabilityId: "OFFLINE_COMMANDS" as const,
|
||||
selected: 1,
|
||||
active: 1,
|
||||
override: "DEFAULT" as const,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
})}
|
||||
>
|
||||
<LocaleProvider>
|
||||
<PlatformOverviewPage />
|
||||
</LocaleProvider>
|
||||
</ApplicationProvider>,
|
||||
);
|
||||
|
||||
const region = screen.getByRole("region", { name: "선택적 런타임 능력" });
|
||||
|
||||
expect(within(region).getByText("운영자가 비활성화함")).toBeVisible();
|
||||
expect(within(region).getByText("활성 (1)")).toBeVisible();
|
||||
expect(within(region).getAllByText("미선택")).toHaveLength(2);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 실패 확인**
|
||||
|
||||
실행: `corepack pnpm exec vitest run tests/component/platform-overview-page.test.tsx`
|
||||
기대: "운영자가 비활성화함" 을 찾지 못해 FAIL
|
||||
|
||||
- [ ] **Step 3: 화면 구현**
|
||||
|
||||
`src/presentation/examples/platform-overview-page.tsx`에서 `INSTALLED_RUNTIME_CAPABILITIES` import를 제거하고 스냅샷 소비로 바꾼다.
|
||||
|
||||
능력 설명은 정적 표이므로 id로 찾는다.
|
||||
|
||||
```ts
|
||||
const CAPABILITY_COPY: Readonly<Record<RuntimeCapabilityId, Readonly<{
|
||||
label: string;
|
||||
description: string;
|
||||
}>>> = Object.freeze({
|
||||
REALTIME: Object.freeze({
|
||||
label: "실시간 수신",
|
||||
description:
|
||||
"WebSocket, SSE, 경계 폴링 런타임은 구현되어 있습니다. 제품 기여물이 엔드포인트와 이벤트 서술자를 제공해야 설치됩니다.",
|
||||
}),
|
||||
WEB_WORKER: Object.freeze({
|
||||
label: "웹 워커",
|
||||
description:
|
||||
"워커 실행 계약과 전용 타입 프로젝트가 준비되어 있습니다. 프로파일링으로 확인된 CPU 작업이 있어야 설치됩니다.",
|
||||
}),
|
||||
SERVICE_WORKER: Object.freeze({
|
||||
label: "서비스 워커",
|
||||
description:
|
||||
"참조 런타임과 두 단계 빌드가 준비되어 있습니다. 설치하면 검증된 정적 자산 캐시와 등록 해제 경로가 함께 켜집니다.",
|
||||
}),
|
||||
OFFLINE_COMMANDS: Object.freeze({
|
||||
label: "오프라인 명령",
|
||||
description:
|
||||
"명령 큐 상태 기계가 준비되어 있습니다. 복구 서술자를 가진 KEYED 오퍼레이션이 있어야 설치됩니다.",
|
||||
}),
|
||||
});
|
||||
|
||||
function capabilityBadge(
|
||||
status: RuntimeCapabilityStatus,
|
||||
): Readonly<{ text: string; variant: "success" | "warning" | "neutral" }> {
|
||||
if (status.selected === 0) {
|
||||
return { text: "미선택", variant: "neutral" };
|
||||
}
|
||||
if (status.active === 0) {
|
||||
return { text: "운영자가 비활성화함", variant: "warning" };
|
||||
}
|
||||
return { text: `활성 (${status.active})`, variant: "success" };
|
||||
}
|
||||
```
|
||||
|
||||
컴포넌트 본문에서:
|
||||
|
||||
```ts
|
||||
const capabilities = runtime.getCapabilitySnapshot();
|
||||
const activeCapabilityCount = capabilities.filter(
|
||||
(status) => status.active > 0,
|
||||
).length;
|
||||
```
|
||||
|
||||
카드 렌더링을 바꾼다.
|
||||
|
||||
```tsx
|
||||
{capabilities.map((status) => {
|
||||
const copy = CAPABILITY_COPY[status.capabilityId];
|
||||
const badge = capabilityBadge(status);
|
||||
return (
|
||||
<Card
|
||||
key={status.capabilityId}
|
||||
title={copy.label}
|
||||
footer={<Badge variant={badge.variant}>{badge.text}</Badge>}
|
||||
>
|
||||
<p>{copy.description}</p>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
```
|
||||
|
||||
설치 요약의 능력 지표 힌트를 `"런타임 오버라이드 반영"`으로 바꾸고, 5번 섹션 설명 마지막 문장을 다음으로 교체한다.
|
||||
|
||||
```
|
||||
여기 표시되는 상태는 정적 선택에 런타임 오버라이드를 적용한 결과입니다.
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 통과 확인**
|
||||
|
||||
실행: `corepack pnpm exec vitest run tests/component/platform-overview-page.test.tsx`
|
||||
기대: 7 passed
|
||||
|
||||
- [ ] **Step 5: 게이트와 커밋**
|
||||
|
||||
```bash
|
||||
corepack pnpm check:types && corepack pnpm lint && corepack pnpm check:i18n && corepack pnpm check:design-system && corepack pnpm test:all
|
||||
git add src/presentation/examples/platform-overview-page.tsx tests/component/platform-overview-page.test.tsx
|
||||
git commit -m "feat: show resolved capability state on the platform overview"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 홈에서 플랫폼 구성 화면으로 가는 진입점
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/presentation/pages/home-page.tsx`
|
||||
- Test: `tests/component/router.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `routePath("EXAMPLES_PLATFORM")`
|
||||
- Produces: 없음
|
||||
|
||||
- [ ] **Step 1: 실패하는 테스트 작성**
|
||||
|
||||
`tests/component/router.test.tsx`에 추가한다.
|
||||
|
||||
```ts
|
||||
it("reaches the platform overview from the home starter actions", async () => {
|
||||
const user = userEvent.setup();
|
||||
window.history.pushState({}, "", "/");
|
||||
renderRouter();
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole("link", { name: "플랫폼 구성 보기" }),
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "플랫폼 구성", level: 1 }),
|
||||
).toHaveFocus(),
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 실패 확인**
|
||||
|
||||
실행: `corepack pnpm exec vitest run tests/component/router.test.tsx`
|
||||
기대: "플랫폼 구성 보기" 링크가 없어 FAIL
|
||||
|
||||
- [ ] **Step 3: 구현**
|
||||
|
||||
`src/presentation/pages/home-page.tsx`의 `button-row`에 첫 번째 링크로 추가한다.
|
||||
|
||||
```tsx
|
||||
<Link className="ui-button" to={routePath("EXAMPLES_PLATFORM")}>
|
||||
플랫폼 구성 보기
|
||||
</Link>
|
||||
```
|
||||
|
||||
기존 "UI 구성요소 보기" 링크의 클래스를 `ui-button ui-button--secondary`로 바꾸어 주 행동이 하나가 되게 한다.
|
||||
|
||||
- [ ] **Step 4: 통과 확인**
|
||||
|
||||
실행: `corepack pnpm exec vitest run tests/component/router.test.tsx`
|
||||
기대: 4 passed
|
||||
|
||||
`tests/e2e/accessibility.spec.ts`가 홈의 첫 탭 대상을 `"UI 구성요소 보기"`로 단언하므로 함께 바꾼다.
|
||||
|
||||
```ts
|
||||
const action = page.getByRole("link", { name: "플랫폼 구성 보기" });
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 게이트와 커밋**
|
||||
|
||||
```bash
|
||||
corepack pnpm check:types && corepack pnpm lint && corepack pnpm test:component
|
||||
git add src/presentation/pages/home-page.tsx tests/component/router.test.tsx tests/e2e/accessibility.spec.ts
|
||||
git commit -m "feat: link the platform overview from the home starter actions"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 브라우저 회귀 커버리지
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/e2e/platform-overview.spec.ts`
|
||||
- Modify: `tests/visual/platform.visual.spec.ts`
|
||||
- Create: `tests/visual/__snapshots__/platform.visual.spec.ts-snapshots/platform-overview-light-chromium-visual-linux.png`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: 빌드된 번들의 `/examples/platform`
|
||||
- Produces: 없음
|
||||
|
||||
**전제:** Playwright 브라우저가 설치되어 있어야 한다. 없으면 `corepack pnpm exec playwright install chromium`을 먼저 실행한다. 설치가 불가능한 환경이면 이 태스크를 건너뛰고 그 사실을 보고한다 — 기준선 없이 스냅샷 테스트를 커밋하면 안 된다.
|
||||
|
||||
- [ ] **Step 1: e2e 스펙 작성**
|
||||
|
||||
`tests/e2e/platform-overview.spec.ts`:
|
||||
|
||||
```ts
|
||||
import { expect, test } from "../support/browser/strict-browser-test.ts";
|
||||
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.ts";
|
||||
import { SERVER_STATE_PROFILES } from "../../src/contracts/server-state.ts";
|
||||
|
||||
test("projects the installed route registry into the shipped bundle", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/examples/platform");
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "플랫폼 구성", level: 1 }),
|
||||
).toBeVisible();
|
||||
|
||||
const table = page.getByRole("table", { name: "설치된 라우트 목록" });
|
||||
await expect(table.locator("tbody tr")).toHaveCount(
|
||||
Object.keys(ROUTE_REGISTRY).length,
|
||||
);
|
||||
for (const routeId of Object.keys(ROUTE_REGISTRY)) {
|
||||
await expect(table.getByText(routeId, { exact: true })).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test("shows every fixed server-state profile with its budget", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/examples/platform");
|
||||
|
||||
const table = page.getByRole("table", { name: "서버 상태 프로파일" });
|
||||
await expect(table.locator("tbody tr")).toHaveCount(
|
||||
Object.keys(SERVER_STATE_PROFILES).length,
|
||||
);
|
||||
});
|
||||
|
||||
test("states the release contract identity verified at boot", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/examples/platform");
|
||||
|
||||
const release = page.locator(
|
||||
"section[aria-labelledby='platform-release-title']",
|
||||
);
|
||||
await expect(release.getByText(/^sha256:/)).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: e2e 실행**
|
||||
|
||||
실행: `corepack pnpm exec playwright test tests/e2e/platform-overview.spec.ts --project=chromium`
|
||||
기대: 3 passed
|
||||
|
||||
- [ ] **Step 3: 시각 회귀 케이스 추가**
|
||||
|
||||
`tests/visual/platform.visual.spec.ts` 끝에 추가한다.
|
||||
|
||||
```ts
|
||||
test("platform composition overview visual contract", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1280, height: 1100 });
|
||||
await page.goto("/examples/platform");
|
||||
await expect(
|
||||
page.locator("section[aria-labelledby='platform-capabilities-title']"),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole("main")).toHaveScreenshot(
|
||||
"platform-overview-light.png",
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 기준선 생성과 검증**
|
||||
|
||||
```bash
|
||||
corepack pnpm test:visual:update
|
||||
corepack pnpm test:visual
|
||||
```
|
||||
|
||||
기대: 5 passed, 새 PNG 기준선이 `tests/visual/__snapshots__/platform.visual.spec.ts-snapshots/`에 생긴다.
|
||||
|
||||
- [ ] **Step 5: 커밋**
|
||||
|
||||
```bash
|
||||
git add tests/e2e/platform-overview.spec.ts tests/visual
|
||||
git commit -m "test: cover the platform overview with browser and visual regression"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 최종 검증과 문서 갱신
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/superpowers/specs/2026-07-31-platform-overview-page-design.md`
|
||||
|
||||
- [ ] **Step 1: 설계 문서의 제외 항목 갱신**
|
||||
|
||||
5절 "의도적 제외"를 다음으로 교체한다.
|
||||
|
||||
```markdown
|
||||
## 5. 런타임 해석 결과
|
||||
|
||||
초판은 런타임 오버라이드 해석 결과를 제외했다. 해석이 bootstrap에서 일어나고 표현 계층이 그 계층을
|
||||
import 할 수 없었기 때문이다.
|
||||
|
||||
이후 `RuntimeCapabilitiesPort`가 추가되어 합성 루트가 경계된 스냅샷을 애플리케이션으로 전달한다.
|
||||
화면은 정적 선택 수와 활성 수를 함께 표시하므로, 운영자가 능력을 껐다는 사실과 애초에 선택되지
|
||||
않았다는 사실이 구분된다.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 전체 게이트**
|
||||
|
||||
```bash
|
||||
corepack pnpm check:types
|
||||
corepack pnpm lint
|
||||
corepack pnpm check:architecture
|
||||
corepack pnpm test:all
|
||||
corepack pnpm test:coverage
|
||||
corepack pnpm check:registries
|
||||
corepack pnpm check:i18n
|
||||
corepack pnpm check:design-system
|
||||
corepack pnpm check:diagnostics
|
||||
corepack pnpm check:ci
|
||||
corepack pnpm check:test-evidence:source
|
||||
corepack pnpm verify:compatibility
|
||||
corepack pnpm build
|
||||
```
|
||||
|
||||
기대: 전부 PASS
|
||||
|
||||
- [ ] **Step 3: 제거 하네스 4종**
|
||||
|
||||
```bash
|
||||
corepack pnpm test:sample-removal
|
||||
corepack pnpm test:optional-recipe-removal
|
||||
corepack pnpm test:browser-file-storage-removal
|
||||
corepack pnpm test:realtime-removal
|
||||
```
|
||||
|
||||
기대: 전부 PASS. 특히 `test:sample-removal`은 화면이 여전히 파생 전용인지 확인한다.
|
||||
|
||||
- [ ] **Step 4: 커밋**
|
||||
|
||||
```bash
|
||||
git add docs/superpowers/specs/2026-07-31-platform-overview-page-design.md
|
||||
git commit -m "docs: record the resolved capability path in the overview design"
|
||||
```
|
||||
|
||||
## Self-Review
|
||||
|
||||
**Spec coverage:** 설계 문서 5절(의도적 제외)이 Task 1–3으로 닫힌다. 6절 변경 파일 목록은 이미 반영됐다. 7절 검증은 Task 6이 수행한다. 진입 경로와 브라우저 커버리지는 초판 범위 밖이었으므로 Task 4–5로 추가했다.
|
||||
|
||||
**Placeholder scan:** TBD 없음. 모든 코드 단계에 실제 코드가 있다.
|
||||
|
||||
**Type consistency:** `describeRuntimeCapabilities`(Task 1) → `RuntimeCapabilitiesPort.getSnapshot`(Task 2) → `runtime.getCapabilitySnapshot()`(Task 3)로 이름과 반환 타입이 일치한다. `RuntimeCapabilityStatus`의 필드명 `capabilityId` / `selected` / `active` / `override`가 세 태스크에서 동일하다.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Scope-Owned Server State Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Prevent previous-account query and optimistic state from remaining renderable or writable after an authentication scope transition.
|
||||
|
||||
**Architecture:** The scope runtime is a fail-closed lifecycle authority. A bootstrap generation store owns a QueryClient and its concrete invalidation coordinator, swaps them only after the previous generation is closed, and exposes a stable invalidation facade plus a subscribable generation snapshot to React.
|
||||
|
||||
**Tech Stack:** TypeScript 7, React 19, TanStack Query 5, Vitest 4, Testing Library.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- FENCED is synchronous and no previous-account UI may render after it.
|
||||
- Mandatory participant or cache cleanup failure never publishes READY.
|
||||
- Snapshot subscriber defects never prevent reset scheduling.
|
||||
- Every READY generation owns a different QueryClient identity.
|
||||
- Late work from an old generation cannot write through the stable coordinator.
|
||||
- Preserve existing public application ports and dirty-worktree changes.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Fail-closed scope lifecycle
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/contracts/server-state-scope.ts`
|
||||
- Modify: `src/adapters/query-cache/server-state-scope-runtime.ts`
|
||||
- Modify: `tests/unit/server-state-scope-runtime.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Adds phase `FAILED` and lifecycle event `{ kind: "FAILED"; generation: number }`.
|
||||
- Adds dependency callback `activateNextGeneration(): void | Promise<void>` executed after old local reset and before READY.
|
||||
|
||||
- [ ] Add failing tests for a throwing snapshot listener, participant failure, reset failure, and next-generation activation failure.
|
||||
- [ ] Run `corepack pnpm exec vitest run tests/unit/server-state-scope-runtime.test.ts` and confirm each new behavior fails for the intended reason.
|
||||
- [ ] Notify listeners from a stable snapshot with per-listener exception isolation.
|
||||
- [ ] Continue all cleanup participants, remember the first mandatory failure, and always attempt local reset.
|
||||
- [ ] Close old identities after cleanup, then publish FAILED if any mandatory step failed.
|
||||
- [ ] Call `activateNextGeneration` only after successful reset and publish READY only after it succeeds.
|
||||
- [ ] Re-run the focused unit test and confirm all lifecycle cases pass.
|
||||
|
||||
### Task 2: Render fence surface synchronously
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/presentation/adapters/query/server-state-scope-provider.tsx`
|
||||
- Create: `tests/component/server-state-scope-provider.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- `ServerStateScopeProvider` subscribes to both snapshot and phase through one store notification.
|
||||
- Props add optional `transitionFallback?: ReactNode`; default is `null`.
|
||||
- Existing `useServerStateScope()` continues returning `CacheScopeSnapshot` for feature hooks.
|
||||
|
||||
- [ ] Add a failing component test that renders private child data, triggers a deferred reset, and expects the child to disappear in the same `act()` turn.
|
||||
- [ ] Add a failing test that FAILED never remounts children.
|
||||
- [ ] Run the new component test and confirm old children remain with the current provider.
|
||||
- [ ] Subscribe in the provider and render children only for READY; render the supplied fallback for FENCED/FAILED and nothing after DISPOSED.
|
||||
- [ ] Re-run the test and confirm no old-data frame is observable.
|
||||
|
||||
### Task 3: QueryClient generation store and stable coordinator facade
|
||||
|
||||
**Files:**
|
||||
- Create: `src/bootstrap/server-state-generation-store.ts`
|
||||
- Modify: `src/bootstrap/runtime-adapters.ts`
|
||||
- Modify: `src/bootstrap/runtime-application.tsx`
|
||||
- Modify: `tests/unit/runtime-adapters.test.ts`
|
||||
- Modify: `tests/component/runtime-application.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- `ServerStateGenerationSnapshot = { generation: number; queryClient: QueryClient; coordinator: QueryInvalidationCoordinator; crossContextStatus(): CrossContextInvalidationStatus }`.
|
||||
- Store methods: `getSnapshot()`, `subscribe(listener)`, `activateNext()`, `resetCurrent()`, `dispose()`.
|
||||
- A stable `QueryInvalidationCoordinator` facade delegates only to the current snapshot and fences delegates by generation.
|
||||
|
||||
- [ ] Add a failing unit test that captures the initial QueryClient, triggers session transition, completes reset, and expects a different current QueryClient.
|
||||
- [ ] Add a failing test that an old captured coordinator cannot invalidate after activation.
|
||||
- [ ] Add a component test that QueryClientProvider receives and renders against the new generation.
|
||||
- [ ] Implement generation factory ownership in bootstrap; create cross-context transport and concrete coordinator per generation.
|
||||
- [ ] Pass `resetCurrent` and `activateNext` to the scope runtime in the required order.
|
||||
- [ ] Make `RuntimeApplication` subscribe to the generation store and key the QueryClient provider by generation.
|
||||
- [ ] Dispose the current generation and store exactly once during application shutdown.
|
||||
- [ ] Run runtime-adapter and runtime-application tests until all pass.
|
||||
|
||||
### Task 4: Conditional validator and cleanup ordering
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/bootstrap/runtime-adapters.ts`
|
||||
- Modify: `tests/unit/runtime-adapters.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Conditional validator clearing becomes an ordered mandatory scope participant rather than a general snapshot subscriber.
|
||||
|
||||
- [ ] Add a failing order test proving validator clear occurs after admission fence and before cache disposal.
|
||||
- [ ] Register the validator closer with an explicit order and remove the generic subscription.
|
||||
- [ ] Re-run runtime-adapter tests and verify each closer executes once.
|
||||
|
||||
### Task 5: Verification
|
||||
|
||||
- [ ] Run scope runtime, provider, runtime-adapter, runtime-application, and application-query focused suites.
|
||||
- [ ] Run `corepack pnpm check:types` and `corepack pnpm lint`.
|
||||
- [ ] Run the complete non-browser suite.
|
||||
- [ ] Record browser-only follow-up separately; do not claim it passed without Playwright evidence.
|
||||
@@ -0,0 +1,82 @@
|
||||
# 플랫폼 구성 화면 (`EXAMPLES_PLATFORM`) 설계
|
||||
|
||||
작성일: 2026-07-31
|
||||
|
||||
## 1. 문제
|
||||
|
||||
템플릿이 무엇을 설치해 두었는지 확인할 방법이 없다. 홈 화면은 "실행 계약 / 교체 가능한 연동 /
|
||||
접근 가능한 화면"이라는 세 문장으로만 요약하고, 실제로 어떤 라우트·계약·런타임 능력이 설치되어
|
||||
있는지는 소스를 직접 읽어야만 알 수 있다.
|
||||
|
||||
## 2. 해결 방향
|
||||
|
||||
설치 상태를 **레지스트리에서 파생해서만** 렌더하는 화면을 하나 추가한다. 수기 서술을 두지 않으므로
|
||||
코드가 바뀌면 화면이 따라 바뀌고, 문서가 낡는 문제가 발생하지 않는다.
|
||||
|
||||
이 선택에는 부수 효과가 있다. reference feature를 삭제하면 관련 행이 자동으로 사라지므로
|
||||
`test:sample-removal` harness의 잔재 스캔과 충돌하지 않는다. 반대로 수기 목록이었다면 샘플 이름이
|
||||
페이지에 박혀 harness가 실패했을 것이다.
|
||||
|
||||
## 3. 배치
|
||||
|
||||
| 항목 | 값 | 근거 |
|
||||
| --- | --- | --- |
|
||||
| `routeId` | `EXAMPLES_PLATFORM` | |
|
||||
| `path` | `/examples/platform` | 제품 개발 시 통째로 삭제 가능한 `examples/` 옥 |
|
||||
| `access` | `public` | 세션 연동 없이 확인 가능해야 함 |
|
||||
| `loadingSurface` | `example-page` | governance `allowedValues`에 이미 존재 — 신규 값 추가 없음 |
|
||||
| `errorSurface` | `route-boundary` | 동일 |
|
||||
| `chunkId` | `route-examples-platform` | |
|
||||
| `navigationOrder` | `15` | 홈(10)과 UI(20) 사이. 기존 값 재번호 불필요 |
|
||||
| 구현 파일 | `src/presentation/examples/platform-overview-page.tsx` | `examples/`는 `check:i18n` 한글 리터럴 스캔 대상이 아님 |
|
||||
|
||||
## 4. 섹션 구성
|
||||
|
||||
전부 파생 데이터다.
|
||||
|
||||
| # | 섹션 | 출처 | 표현 대상 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 릴리스 신원 | `ApplicationApi.runtime.getReleaseSummary()` | buildId, releaseId, configSchemaVersion, contractSet digest |
|
||||
| 2 | 설치된 라우트 | `ROUTE_REGISTRY` | path, access, chunkId, params/search 스키마 |
|
||||
| 3 | 계약과 HTTP 오퍼레이션 | `COMPOSED_CONTRACT_CONTRIBUTIONS`, `EXPECTED_CONTRACT_SET_PACKAGES` | 외부 계약 패키지 수, 오퍼레이션별 재시도 의미·예산·바이트 한도·deadline·효과 확정성 |
|
||||
| 4 | 서버 상태와 실행 상한 | `SERVER_STATE_PROFILES`, `HTTP_EXECUTION_CEILINGS` | 4개 프로파일의 staleTime·gcTime·결과 예산, 강제되는 실행 상한 |
|
||||
| 5 | 선택적 런타임 능력 | `INSTALLED_RUNTIME_CAPABILITIES` | realtime / webWorker / serviceWorker / offlineCommands 선택 여부 |
|
||||
|
||||
섹션 3의 "외부 계약 패키지 0개"와 섹션 5의 "4개 능력 전부 미선택"이 "어디까지 제공하는가"에 대한
|
||||
답이다. 공통 런타임은 구현·검증되어 있으나 제품 기여물이 없어 선택되지 않은 상태임을 드러낸다.
|
||||
|
||||
## 5. 런타임 해석 결과
|
||||
|
||||
초판은 `CAPABILITY_OVERRIDES` 해석 결과를 제외했다. 해석이 bootstrap에서 일어나고
|
||||
`presentation-does-not-know-adapters` 규칙이 presentation → bootstrap import를 금지했기 때문이다.
|
||||
|
||||
이후 `docs/superpowers/plans/2026-07-31-platform-overview-completion.md`가 그 경로를 만들었다.
|
||||
`describeRuntimeCapabilities`가 정적 선택과 해석 결과를 경계된 스냅샷으로 축약하고,
|
||||
`RuntimeCapabilitiesPort`를 통해 합성 루트가 그 스냅샷을 애플리케이션에 전달한다. 표현 계층은
|
||||
`runtime.getCapabilitySnapshot()`만 호출하므로 bootstrap을 여전히 알지 못한다.
|
||||
|
||||
스냅샷이 `selected`와 `active`를 함께 실으므로 화면은 세 상태를 구분한다.
|
||||
|
||||
| 상태 | 조건 | 표시 |
|
||||
| --- | --- | --- |
|
||||
| 미선택 | `selected === 0` | 애초에 설치되지 않았다 |
|
||||
| 운영자가 비활성화함 | `selected > 0 && active === 0` | 설치됐으나 런타임 설정이 껐다 |
|
||||
| 활성 | `active > 0` | 지금 동작한다 |
|
||||
|
||||
## 6. 함께 변경되는 파일
|
||||
|
||||
1. `src/contracts/routes.ts` — 레지스트리 항목
|
||||
2. `src/contracts/route-runtime-contract.ts` — 런타임 계약 항목
|
||||
3. `src/presentation/routes/route-runtime.tsx` — lazy import
|
||||
4. `src/presentation/i18n/catalog.ts` — ko/en `route.EXAMPLES_PLATFORM.{title,navigation}`
|
||||
5. `public/release-manifest.json` — `routeChunks` 항목
|
||||
6. `config/contracts/registry-baseline.json` 및 승인·증거 파일
|
||||
7. `src/presentation/styles/` — 요약 그리드 스타일
|
||||
8. `tests/component/platform-overview-page.test.tsx` — 컴포넌트 테스트
|
||||
|
||||
## 7. 검증
|
||||
|
||||
- 타입, lint, 아키텍처, i18n, 디자인 시스템, 레지스트리 게이트
|
||||
- `test:all`
|
||||
- removal harness 4종. 특히 `test:sample-removal` 이후에도 페이지가 빈 상태로 정상 렌더되어야 한다.
|
||||
- 실제 브라우저 렌더 확인
|
||||
Reference in New Issue
Block a user