feat: add TechLog feature contracts

This commit is contained in:
DongHyeonka
2026-08-15 18:07:12 +09:00
parent 5479101c8c
commit 01ed1e9300
9 changed files with 2787 additions and 0 deletions
@@ -0,0 +1,181 @@
export type RecordKind = "CASE" | "REFERENCE" | "QUESTION";
export type QuestionStatus =
| "OPEN"
| "INVESTIGATING"
| "PAUSED"
| "RESOLVED"
| "ARCHIVED";
export type PublicRelation = {
reason: string;
title: string;
path: string;
};
export type RecordSection = {
id: string;
title: string;
paragraphs: ReadonlyArray<string>;
bullets?: ReadonlyArray<string>;
};
type PublicRecordBase = {
kind: RecordKind;
slug: string;
title: string;
summary: string;
path: string;
topic: string;
topicSlug: string;
projectSlug: string;
projectTitle: string;
publishedAt: string;
publishedLabel: string;
visibility: "PUBLIC";
relations: ReadonlyArray<PublicRelation>;
};
export type CaseRecord = PublicRecordBase & {
kind: "CASE";
problem: string;
conclusion: string;
environment: string;
verification: string;
lastVerifiedLabel: string;
sections: ReadonlyArray<RecordSection>;
};
export type ReferenceRule = {
title: string;
body: string;
};
export type ReferenceRecord = PublicRecordBase & {
kind: "REFERENCE";
purpose: string;
rules: ReadonlyArray<ReferenceRule>;
applyWhen: ReadonlyArray<string>;
exceptions: ReadonlyArray<string>;
examples: ReadonlyArray<string>;
verifiedAt: string;
};
export type QuestionOption = {
title: string;
description: string;
};
export type QuestionRecord = PublicRecordBase & {
kind: "QUESTION";
questionStatus: QuestionStatus;
facts: ReadonlyArray<string>;
assumptions: ReadonlyArray<string>;
unknowns: ReadonlyArray<string>;
constraints: ReadonlyArray<string>;
options: ReadonlyArray<QuestionOption>;
nextValidation: string;
resolution?: {
summary: string;
path: string;
linkLabel: string;
};
};
export type PublicRecord = CaseRecord | ReferenceRecord | QuestionRecord;
export type ProjectDecision = {
id: string;
status: "ADOPTED" | "PROPOSED";
date: string;
title: string;
statement: string;
rationale: string;
consequences: ReadonlyArray<string>;
evidence: ReadonlyArray<{ title: string; path: string }>;
};
export type ProjectActivity = {
id: string;
date: string;
dateTime: string;
type: "PUBLICATION" | "PROJECT UPDATE" | "QUESTION";
title: string;
summary: string;
path: string;
recordPath?: string;
};
export type Project = {
slug: string;
title: string;
summary: string;
thesis: string;
stage: "DESIGN" | "VALIDATION";
currentGoal: string;
nextStep: string;
topics: ReadonlyArray<string>;
decisions: ReadonlyArray<ProjectDecision>;
activity: ReadonlyArray<ProjectActivity>;
};
export type Release = {
version: string;
path: string;
title: string;
summary: string;
publishedAt: string;
publishedLabel: string;
changes: ReadonlyArray<string>;
reasons: ReadonlyArray<string>;
impacts: ReadonlyArray<string>;
related: ReadonlyArray<{ title: string; path: string }>;
};
export type FocusKey = "current" | "question" | "decision";
export type HomeFocusItem = {
key: FocusKey;
label: string;
title: string;
summary: string;
details: ReadonlyArray<{ label: string; value: string }>;
targetPath: string;
};
export type RecordFilters = {
kind?: RecordKind;
topic?: string;
project?: string;
openQuestionsOnly?: boolean;
};
export type SearchablePublicEntity = {
contentType: RecordKind | "PROJECT" | "RELEASE";
title: string;
summary: string;
path: string;
topic?: string;
topics?: ReadonlyArray<string>;
project?: string;
publishedAt?: string;
};
/**
* The application-facing boundary for the immutable source Public catalog.
* Method signatures intentionally retain the source query argument and return shapes.
*/
export type PublicContentQueries = Readonly<{
listRecords(filters?: RecordFilters): PublicRecord[];
getRecord<K extends RecordKind>(
kind: K,
slug: string,
): Extract<PublicRecord, { kind: K }> | undefined;
getProject(slug: string): Project | undefined;
getRelease(version: string): Release | undefined;
getProjectRecords(projectSlug: string): PublicRecord[];
getProjectDecisions(projectSlug: string): ProjectDecision[];
getProjectActivity(projectSlug: string): ProjectActivity[];
getHomeFocusItems(): HomeFocusItem[];
searchPublicContent(query: string): SearchablePublicEntity[];
}>;
@@ -0,0 +1,32 @@
import type { ProblemDetails } from "../../contracts/studio/contract.ts";
export class StudioGatewayError extends Error {
readonly problem: ProblemDetails;
readonly status: number;
readonly code: ProblemDetails["code"];
readonly retryable: boolean;
constructor(problem: ProblemDetails, options?: ErrorOptions) {
super(problem.detail, options);
this.name = "StudioGatewayError";
this.problem = problem;
this.status = problem.status;
this.code = problem.code;
this.retryable = problem.retryable ?? false;
}
}
export function isStudioGatewayError(value: unknown): value is StudioGatewayError {
if (typeof value !== "object" || value === null) return false;
const candidate = value as Partial<StudioGatewayError>;
return (
typeof candidate.message === "string" &&
typeof candidate.status === "number" &&
typeof candidate.code === "string" &&
typeof candidate.retryable === "boolean" &&
typeof candidate.problem === "object" &&
candidate.problem !== null &&
candidate.problem.status === candidate.status &&
candidate.problem.code === candidate.code
);
}
@@ -0,0 +1,34 @@
import type {
CatalogPage, CreateDocumentInput, CreatePreviewCommand, DocumentPage,
PublicationPage, PublicationSnapshot, PublicPreview, PublishDocumentCommand,
PreviewDetail, PublishResult, SaveDocumentCommand, StudioDashboard, UnpublishCommand,
ValidationReport, ValidateDocumentCommand, WorkingCopy, WorkingCopyDetail,
} from "../../contracts/studio/contract.ts";
export type RequestOptions = { signal?: AbortSignal };
export type IdempotentOptions = RequestOptions & { idempotencyKey: string };
export type ListDocumentsQuery = {
q?: string; kind?: "CASE" | "REFERENCE" | "QUESTION";
publicationStatus?: "NEVER_PUBLISHED" | "PUBLISHED" | "UNPUBLISHED";
nextAction?: "CONTINUE_EDITING" | "VALIDATE" | "FIX_VALIDATION" | "CREATE_PREVIEW" | "PUBLISH" | "NONE";
projectId?: string; sort?: "UPDATED_DESC" | "UPDATED_ASC" | "TITLE_ASC";
cursor?: string; limit?: number;
};
export type ListPublicationsQuery = { q?: string; type?: "PUBLISHED" | "REPUBLISHED" | "UNPUBLISHED"; cursor?: string; limit?: number };
export type CatalogQuery = { type: "TOPIC" | "PROJECT" | "RELATION" | "EVIDENCE"; q?: string; cursor?: string; limit?: number };
export interface StudioGateway {
getDashboard(options?: RequestOptions): Promise<StudioDashboard>;
listDocuments(query: ListDocumentsQuery, options?: RequestOptions): Promise<DocumentPage>;
createDocument(input: CreateDocumentInput, options: IdempotentOptions): Promise<WorkingCopy>;
getDocument(documentId: string, options?: RequestOptions): Promise<WorkingCopyDetail>;
saveDocument(documentId: string, command: SaveDocumentCommand, options: IdempotentOptions): Promise<WorkingCopyDetail>;
validateDocument(documentId: string, command: ValidateDocumentCommand, options: IdempotentOptions): Promise<ValidationReport>;
createPreview(documentId: string, command: CreatePreviewCommand, options: IdempotentOptions): Promise<PublicPreview>;
getCurrentPreview(documentId: string, options?: RequestOptions): Promise<PreviewDetail>;
publishDocument(documentId: string, command: PublishDocumentCommand, options: IdempotentOptions): Promise<PublishResult>;
unpublishPublication(publicationId: string, command: UnpublishCommand, options: IdempotentOptions): Promise<PublishResult>;
listPublications(query: ListPublicationsQuery, options?: RequestOptions): Promise<PublicationPage>;
getPublicationSnapshot(publicationEventId: string, options?: RequestOptions): Promise<PublicationSnapshot>;
getCatalog(query: CatalogQuery, options?: RequestOptions): Promise<CatalogPage>;
}
@@ -0,0 +1,15 @@
import type { PublicContentQueries } from "./ports/public-content-queries.ts";
import type { StudioGateway } from "./ports/studio-gateway.ts";
export const TECH_LOG_FEATURE_ID = "tech-log" as const;
export type TechLogFeatureInput = Readonly<{
publicContent: PublicContentQueries;
createStudioGateway(): StudioGateway;
}>;
declare module "../../../application/ports/in/application-api.ts" {
interface ApplicationFeatureInputs {
"tech-log": TechLogFeatureInput;
}
}