feat: add TechLog feature contracts
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user