From 01ed1e9300653f39d27b8da4143c46058f0fd0ed Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Sat, 15 Aug 2026 18:07:12 +0900 Subject: [PATCH] feat: add TechLog feature contracts --- .../ports/public-content-queries.ts | 181 +++ .../application/ports/studio-gateway-error.ts | 32 + .../application/ports/studio-gateway.ts | 34 + .../application/tech-log-feature-input.ts | 15 + .../tech-log/contracts/studio/contract.ts | 27 + .../tech-log/contracts/studio/generated.ts | 1355 +++++++++++++++++ .../contracts/studio/studio-api.openapi.yaml | 984 ++++++++++++ tests/features/tech-log/feature-input.test.ts | 33 + .../features/tech-log/studio-contract.test.ts | 126 ++ 9 files changed, 2787 insertions(+) create mode 100644 src/features/tech-log/application/ports/public-content-queries.ts create mode 100644 src/features/tech-log/application/ports/studio-gateway-error.ts create mode 100644 src/features/tech-log/application/ports/studio-gateway.ts create mode 100644 src/features/tech-log/application/tech-log-feature-input.ts create mode 100644 src/features/tech-log/contracts/studio/contract.ts create mode 100644 src/features/tech-log/contracts/studio/generated.ts create mode 100644 src/features/tech-log/contracts/studio/studio-api.openapi.yaml create mode 100644 tests/features/tech-log/feature-input.test.ts create mode 100644 tests/features/tech-log/studio-contract.test.ts diff --git a/src/features/tech-log/application/ports/public-content-queries.ts b/src/features/tech-log/application/ports/public-content-queries.ts new file mode 100644 index 0000000..d6ca418 --- /dev/null +++ b/src/features/tech-log/application/ports/public-content-queries.ts @@ -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; + bullets?: ReadonlyArray; +}; + +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; +}; + +export type CaseRecord = PublicRecordBase & { + kind: "CASE"; + problem: string; + conclusion: string; + environment: string; + verification: string; + lastVerifiedLabel: string; + sections: ReadonlyArray; +}; + +export type ReferenceRule = { + title: string; + body: string; +}; + +export type ReferenceRecord = PublicRecordBase & { + kind: "REFERENCE"; + purpose: string; + rules: ReadonlyArray; + applyWhen: ReadonlyArray; + exceptions: ReadonlyArray; + examples: ReadonlyArray; + verifiedAt: string; +}; + +export type QuestionOption = { + title: string; + description: string; +}; + +export type QuestionRecord = PublicRecordBase & { + kind: "QUESTION"; + questionStatus: QuestionStatus; + facts: ReadonlyArray; + assumptions: ReadonlyArray; + unknowns: ReadonlyArray; + constraints: ReadonlyArray; + options: ReadonlyArray; + 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; + 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; + decisions: ReadonlyArray; + activity: ReadonlyArray; +}; + +export type Release = { + version: string; + path: string; + title: string; + summary: string; + publishedAt: string; + publishedLabel: string; + changes: ReadonlyArray; + reasons: ReadonlyArray; + impacts: ReadonlyArray; + 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; + 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( + kind: K, + slug: string, + ): Extract | 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[]; +}>; diff --git a/src/features/tech-log/application/ports/studio-gateway-error.ts b/src/features/tech-log/application/ports/studio-gateway-error.ts new file mode 100644 index 0000000..87db172 --- /dev/null +++ b/src/features/tech-log/application/ports/studio-gateway-error.ts @@ -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; + 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 + ); +} diff --git a/src/features/tech-log/application/ports/studio-gateway.ts b/src/features/tech-log/application/ports/studio-gateway.ts new file mode 100644 index 0000000..af37524 --- /dev/null +++ b/src/features/tech-log/application/ports/studio-gateway.ts @@ -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; + listDocuments(query: ListDocumentsQuery, options?: RequestOptions): Promise; + createDocument(input: CreateDocumentInput, options: IdempotentOptions): Promise; + getDocument(documentId: string, options?: RequestOptions): Promise; + saveDocument(documentId: string, command: SaveDocumentCommand, options: IdempotentOptions): Promise; + validateDocument(documentId: string, command: ValidateDocumentCommand, options: IdempotentOptions): Promise; + createPreview(documentId: string, command: CreatePreviewCommand, options: IdempotentOptions): Promise; + getCurrentPreview(documentId: string, options?: RequestOptions): Promise; + publishDocument(documentId: string, command: PublishDocumentCommand, options: IdempotentOptions): Promise; + unpublishPublication(publicationId: string, command: UnpublishCommand, options: IdempotentOptions): Promise; + listPublications(query: ListPublicationsQuery, options?: RequestOptions): Promise; + getPublicationSnapshot(publicationEventId: string, options?: RequestOptions): Promise; + getCatalog(query: CatalogQuery, options?: RequestOptions): Promise; +} diff --git a/src/features/tech-log/application/tech-log-feature-input.ts b/src/features/tech-log/application/tech-log-feature-input.ts new file mode 100644 index 0000000..9eb506e --- /dev/null +++ b/src/features/tech-log/application/tech-log-feature-input.ts @@ -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; + } +} diff --git a/src/features/tech-log/contracts/studio/contract.ts b/src/features/tech-log/contracts/studio/contract.ts new file mode 100644 index 0000000..2c6b64c --- /dev/null +++ b/src/features/tech-log/contracts/studio/contract.ts @@ -0,0 +1,27 @@ +import type { components } from "./generated.ts"; + +type Schemas = components["schemas"]; +export type RecordKind = Schemas["RecordKind"]; +export type WorkingCopy = Schemas["WorkingCopy"]; +export type WorkingCopyInput = Schemas["WorkingCopyInput"]; +export type WorkingCopyDetail = Schemas["WorkingCopyDetail"]; +export type StudioDashboard = Schemas["StudioDashboard"]; +export type DocumentPage = Schemas["DocumentPage"]; +export type CreateDocumentInput = Schemas["CreateDocumentInput"]; +export type SaveDocumentCommand = Schemas["SaveDocumentCommand"]; +export type ValidateDocumentCommand = Schemas["ValidateDocumentCommand"]; +export type ValidationReport = Schemas["ValidationReport"]; +export type CreatePreviewCommand = Schemas["CreatePreviewCommand"]; +export type PublicPreview = Schemas["PublicPreview"]; +export type PreviewDetail = Schemas["PreviewDetail"]; +export type PublishDocumentCommand = Schemas["PublishDocumentCommand"]; +export type PublishResult = Schemas["PublishResult"]; +export type UnpublishCommand = Schemas["UnpublishCommand"]; +export type PublicationAggregate = Schemas["PublicationAggregate"]; +export type PublicationEvent = Schemas["PublicationEvent"]; +export type PublicationListItem = Schemas["PublicationListItem"]; +export type PublicationPage = Schemas["PublicationPage"]; +export type PublicationSnapshot = Schemas["PublicationSnapshot"]; +export type CatalogPage = Schemas["CatalogPage"]; +export type ProblemDetails = Schemas["ProblemDetails"]; +export type PublicRenderModel = Schemas["PublicRenderModel"]; diff --git a/src/features/tech-log/contracts/studio/generated.ts b/src/features/tech-log/contracts/studio/generated.ts new file mode 100644 index 0000000..b419ce4 --- /dev/null +++ b/src/features/tech-log/contracts/studio/generated.ts @@ -0,0 +1,1355 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/api/v1/studio/dashboard": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get the Studio dashboard */ + get: operations["getStudioDashboard"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/studio/documents": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List working copies */ + get: operations["listStudioDocuments"]; + put?: never; + /** Create a working copy */ + post: operations["createStudioDocument"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/studio/documents/{documentId}": { + parameters: { + query?: never; + header?: never; + path: { + documentId: components["parameters"]["DocumentId"]; + }; + cookie?: never; + }; + /** Get a working copy and its current state */ + get: operations["getStudioDocument"]; + /** Save a full working copy */ + put: operations["saveStudioDocument"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/studio/documents/{documentId}/validate": { + parameters: { + query?: never; + header?: never; + path: { + documentId: components["parameters"]["DocumentId"]; + }; + cookie?: never; + }; + get?: never; + put?: never; + /** Validate a saved working copy */ + post: operations["validateStudioDocument"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/studio/documents/{documentId}/preview": { + parameters: { + query?: never; + header?: never; + path: { + documentId: components["parameters"]["DocumentId"]; + }; + cookie?: never; + }; + /** Get the latest preview and its computed state */ + get: operations["getCurrentStudioPreview"]; + put?: never; + /** Create a public-layout preview */ + post: operations["createStudioPreview"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/studio/documents/{documentId}/publish": { + parameters: { + query?: never; + header?: never; + path: { + documentId: components["parameters"]["DocumentId"]; + }; + cookie?: never; + }; + get?: never; + put?: never; + /** Publish or republish a validated preview */ + post: operations["publishStudioDocument"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/studio/publications/{publicationId}/unpublish": { + parameters: { + query?: never; + header?: never; + path: { + publicationId: components["parameters"]["PublicationId"]; + }; + cookie?: never; + }; + get?: never; + put?: never; + /** Unpublish the current publication */ + post: operations["unpublishStudioPublication"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/studio/publications": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List immutable publication events + * @description Events are ordered by occurredAt DESC, then publicationEventId. + */ + get: operations["listStudioPublications"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/studio/publications/{publicationEventId}/preview": { + parameters: { + query?: never; + header?: never; + path: { + publicationEventId: components["parameters"]["PublicationEventId"]; + }; + cookie?: never; + }; + /** Get an immutable publication snapshot */ + get: operations["getStudioPublicationSnapshot"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/studio/catalog": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List catalog entries for editor pickers */ + get: operations["listStudioCatalog"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** @enum {string} */ + RecordKind: "CASE" | "REFERENCE" | "QUESTION"; + /** @enum {string} */ + PublicationStatus: "NEVER_PUBLISHED" | "PUBLISHED" | "UNPUBLISHED"; + /** @enum {string} */ + NextAction: "CONTINUE_EDITING" | "VALIDATE" | "FIX_VALIDATION" | "CREATE_PREVIEW" | "PUBLISH" | "NONE"; + RelationInput: { + /** Format: uuid */ + id: string | null; + /** Format: uuid */ + targetId: string | null; + reason: string; + order: number; + }; + Relation: { + /** Format: uuid */ + id: string; + /** Format: uuid */ + targetId: string; + reason: string; + order: number; + }; + OrderedText: { + /** Format: uuid */ + id: string; + text: string; + order: number; + }; + ReferenceRule: { + /** Format: uuid */ + id: string; + title: string; + body: string; + order: number; + }; + QuestionOption: { + /** Format: uuid */ + id: string; + title: string; + description: string; + order: number; + }; + QuestionResolution: { + summary: string; + /** Format: uuid */ + evidenceTargetId: string | null; + linkLabel: string; + }; + WorkingCopyInputBase: { + kind: components["schemas"]["RecordKind"]; + title: string; + slug: "" | string; + summary: string; + /** Format: uuid */ + topicId: string | null; + /** Format: uuid */ + projectId: string | null; + relations: components["schemas"]["RelationInput"][]; + }; + CaseInput: components["schemas"]["WorkingCopyInputBase"] & { + /** @constant */ + kind: "CASE"; + problem: string; + conclusion: string; + environment: string; + reproduction: string; + /** Format: date */ + lastVerifiedOn: string | null; + bodyMarkdown: string; + } & { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + kind: "CASE"; + }; + ReferenceInput: components["schemas"]["WorkingCopyInputBase"] & { + /** @constant */ + kind: "REFERENCE"; + purpose: string; + rules: components["schemas"]["ReferenceRule"][]; + applyWhen: components["schemas"]["OrderedText"][]; + exceptions: components["schemas"]["OrderedText"][]; + examples: components["schemas"]["OrderedText"][]; + /** Format: date */ + verifiedOn: string | null; + } & { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + kind: "REFERENCE"; + }; + QuestionInput: components["schemas"]["WorkingCopyInputBase"] & { + /** @constant */ + kind: "QUESTION"; + /** @enum {string|null} */ + questionStatus: "OPEN" | "RESOLVED" | null; + facts: components["schemas"]["OrderedText"][]; + assumptions: components["schemas"]["OrderedText"][]; + unknowns: components["schemas"]["OrderedText"][]; + constraints: components["schemas"]["OrderedText"][]; + options: components["schemas"]["QuestionOption"][]; + nextValidation: string; + resolution: components["schemas"]["QuestionResolution"] | null; + } & { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + kind: "QUESTION"; + }; + WorkingCopyInput: components["schemas"]["CaseInput"] | components["schemas"]["ReferenceInput"] | components["schemas"]["QuestionInput"]; + WorkingCopyBase: components["schemas"]["WorkingCopyInputBase"] & { + /** Format: uuid */ + id: string; + version: number; + relations: components["schemas"]["Relation"][]; + /** Format: date-time */ + updatedAt: string; + }; + CaseWorkingCopy: components["schemas"]["WorkingCopyBase"] & { + /** @constant */ + kind: "CASE"; + problem: string; + conclusion: string; + environment: string; + reproduction: string; + /** Format: date */ + lastVerifiedOn: string | null; + bodyMarkdown: string; + } & { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + kind: "CASE"; + }; + ReferenceWorkingCopy: components["schemas"]["WorkingCopyBase"] & { + /** @constant */ + kind: "REFERENCE"; + purpose: string; + rules: components["schemas"]["ReferenceRule"][]; + applyWhen: components["schemas"]["OrderedText"][]; + exceptions: components["schemas"]["OrderedText"][]; + examples: components["schemas"]["OrderedText"][]; + /** Format: date */ + verifiedOn: string | null; + } & { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + kind: "REFERENCE"; + }; + QuestionWorkingCopy: components["schemas"]["WorkingCopyBase"] & { + /** @constant */ + kind: "QUESTION"; + /** @enum {string|null} */ + questionStatus: "OPEN" | "RESOLVED" | null; + facts: components["schemas"]["OrderedText"][]; + assumptions: components["schemas"]["OrderedText"][]; + unknowns: components["schemas"]["OrderedText"][]; + constraints: components["schemas"]["OrderedText"][]; + options: components["schemas"]["QuestionOption"][]; + nextValidation: string; + resolution: components["schemas"]["QuestionResolution"] | null; + } & { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + kind: "QUESTION"; + }; + WorkingCopy: components["schemas"]["CaseWorkingCopy"] | components["schemas"]["ReferenceWorkingCopy"] | components["schemas"]["QuestionWorkingCopy"]; + CreateDocumentInput: components["schemas"]["WorkingCopyInput"]; + SaveDocumentCommand: { + expectedVersion: number; + document: components["schemas"]["WorkingCopyInput"]; + }; + ValidateDocumentCommand: { + expectedVersion: number; + }; + ValidationIssue: { + code: string; + /** @enum {string} */ + severity: "ERROR" | "WARNING"; + /** @description JSON Pointer to the affected field */ + path: string; + message: string; + }; + ValidationReport: { + /** Format: uuid */ + validationId: string; + /** Format: uuid */ + documentId: string; + validatedVersion: number; + /** @enum {string} */ + status: "INVALID" | "WARNINGS" | "VALID"; + issues: components["schemas"]["ValidationIssue"][]; + /** Format: date-time */ + validatedAt: string; + /** Format: date-time */ + validUntil: string; + dependencyRevision: string; + }; + CreatePreviewCommand: { + expectedVersion: number; + /** Format: uuid */ + validationId: string; + }; + PublishDocumentCommand: { + expectedVersion: number; + /** Format: uuid */ + validationId: string; + /** Format: uuid */ + previewId: string; + acknowledgedWarningCodes: string[]; + }; + UnpublishCommand: { + expectedPublicationRevision: number; + }; + DisplayTarget: { + /** Format: uuid */ + id: string; + label: string; + publicPath: string | null; + }; + ResolvedRelation: { + /** Format: uuid */ + id: string; + /** Format: uuid */ + targetId: string; + /** @enum {string} */ + targetKind: "CASE" | "REFERENCE" | "QUESTION" | "PROJECT" | "PROJECT_DECISION"; + title: string; + publicPath: string | null; + reason: string; + order: number; + }; + RenderContext: { + /** Format: date-time */ + generatedAt: string; + dependencyRevision: string; + }; + PublicRenderModelBase: { + kind: components["schemas"]["RecordKind"]; + slug: string; + title: string; + summary: string; + publicPath: string; + topic: components["schemas"]["DisplayTarget"]; + project: components["schemas"]["DisplayTarget"] | null; + relations: components["schemas"]["ResolvedRelation"][]; + renderContext: components["schemas"]["RenderContext"]; + }; + InlineText: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "TEXT"; + text: string; + }; + InlineContainer: { + /** @enum {string} */ + type: "EMPHASIS" | "STRONG"; + children: components["schemas"]["Inline"][]; + }; + InlineCode: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "INLINE_CODE"; + code: string; + }; + InlineLink: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "LINK"; + label: string; + /** Format: uri */ + href: string; + }; + InlineStatus: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "STATUS"; + label: string; + /** @enum {string} */ + tone: "warning" | "evidence" | "neutral"; + }; + Inline: components["schemas"]["InlineText"] | components["schemas"]["InlineEmphasis"] | components["schemas"]["InlineStrong"] | components["schemas"]["InlineCode"] | components["schemas"]["InlineLink"] | components["schemas"]["InlineStatus"]; + InlineEmphasis: components["schemas"]["InlineContainer"] & { + /** @constant */ + type?: "EMPHASIS"; + } & { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "EMPHASIS"; + }; + InlineStrong: components["schemas"]["InlineContainer"] & { + /** @constant */ + type?: "STRONG"; + } & { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "STRONG"; + }; + HeadingBlock: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "HEADING"; + id: string; + level: number; + content: components["schemas"]["Inline"][]; + }; + ParagraphBlock: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "PARAGRAPH"; + content: components["schemas"]["Inline"][]; + }; + BlockquoteBlock: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "BLOCKQUOTE"; + content: components["schemas"]["Inline"][]; + }; + ListItem: { + id: string; + content: components["schemas"]["Inline"][]; + }; + ListBlockBase: { + /** @enum {string} */ + type: "UNORDERED_LIST" | "ORDERED_LIST"; + items: components["schemas"]["ListItem"][]; + }; + UnorderedListBlock: components["schemas"]["ListBlockBase"] & { + /** @constant */ + type?: "UNORDERED_LIST"; + } & { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "UNORDERED_LIST"; + }; + OrderedListBlock: components["schemas"]["ListBlockBase"] & { + /** @constant */ + type?: "ORDERED_LIST"; + } & { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "ORDERED_LIST"; + }; + CodeBlock: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "CODE_BLOCK"; + code: string; + language: string | null; + label: string | null; + }; + DataTableColumn: { + id: string; + label: string; + /** @enum {string} */ + alignment: "LEFT" | "CENTER" | "RIGHT"; + }; + DataTableCell: { + columnId: string; + content: components["schemas"]["Inline"][]; + }; + DataTableRow: { + id: string; + cells: components["schemas"]["DataTableCell"][]; + }; + DataTableBlock: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "DATA_TABLE"; + id: string; + caption: string; + rowHeaderColumn: number | null; + columns: components["schemas"]["DataTableColumn"][]; + rows: components["schemas"]["DataTableRow"][]; + }; + CalloutBlock: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "CALLOUT"; + /** @enum {string} */ + tone: "warning" | "info"; + label: string; + content: components["schemas"]["Inline"][]; + }; + EvidenceFigureBlock: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "EVIDENCE_FIGURE"; + key: string; + alt: string; + caption: string; + zoom: boolean; + }; + CaseRenderBlock: components["schemas"]["HeadingBlock"] | components["schemas"]["ParagraphBlock"] | components["schemas"]["BlockquoteBlock"] | components["schemas"]["UnorderedListBlock"] | components["schemas"]["OrderedListBlock"] | components["schemas"]["CodeBlock"] | components["schemas"]["DataTableBlock"] | components["schemas"]["CalloutBlock"] | components["schemas"]["EvidenceFigureBlock"]; + CasePublicRenderModel: components["schemas"]["PublicRenderModelBase"] & { + /** @constant */ + kind: "CASE"; + problem: string; + conclusion: string; + environment: string; + reproduction: string; + /** Format: date */ + lastVerifiedOn: string; + bodyBlocks: components["schemas"]["CaseRenderBlock"][]; + } & { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + kind: "CASE"; + }; + ReferencePublicRenderModel: components["schemas"]["PublicRenderModelBase"] & { + /** @constant */ + kind: "REFERENCE"; + purpose: string; + rules: components["schemas"]["ReferenceRule"][]; + applyWhen: components["schemas"]["OrderedText"][]; + exceptions: components["schemas"]["OrderedText"][]; + examples: components["schemas"]["OrderedText"][]; + /** Format: date */ + verifiedOn: string; + } & { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + kind: "REFERENCE"; + }; + ResolvedQuestionResolution: { + summary: string; + evidenceTarget: components["schemas"]["DisplayTarget"]; + linkLabel: string; + }; + QuestionPublicRenderModel: components["schemas"]["PublicRenderModelBase"] & { + /** @constant */ + kind: "QUESTION"; + /** @enum {string} */ + status: "OPEN" | "RESOLVED"; + facts: components["schemas"]["OrderedText"][]; + assumptions: components["schemas"]["OrderedText"][]; + unknowns: components["schemas"]["OrderedText"][]; + constraints: components["schemas"]["OrderedText"][]; + options: components["schemas"]["QuestionOption"][]; + nextValidation: string; + resolution: components["schemas"]["ResolvedQuestionResolution"] | null; + } & { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + kind: "QUESTION"; + }; + PublicRenderModel: components["schemas"]["CasePublicRenderModel"] | components["schemas"]["ReferencePublicRenderModel"] | components["schemas"]["QuestionPublicRenderModel"]; + PublicPreview: { + /** Format: uuid */ + previewId: string; + /** Format: uuid */ + documentId: string; + previewVersion: number; + /** Format: uuid */ + validationId: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + expiresAt: string; + renderModel: components["schemas"]["PublicRenderModel"]; + }; + PreviewDetail: { + preview: components["schemas"]["PublicPreview"]; + /** @enum {string} */ + state: "CURRENT" | "STALE" | "EXPIRED"; + currentDocumentVersion: number; + /** Format: uuid */ + currentValidationId: string | null; + }; + PublicationAggregate: { + /** Format: uuid */ + publicationId: string; + /** Format: uuid */ + documentId: string; + /** @enum {string} */ + status: "PUBLISHED" | "UNPUBLISHED"; + publishedVersion: number; + publicationRevision: number; + /** Format: uuid */ + latestEventId: string; + publicPath: string; + /** Format: date-time */ + updatedAt: string; + }; + /** @enum {string} */ + PublicationEventType: "PUBLISHED" | "REPUBLISHED" | "UNPUBLISHED"; + PublicationEvent: { + /** Format: uuid */ + publicationEventId: string; + /** Format: uuid */ + publicationId: string; + /** Format: uuid */ + documentId: string; + type: components["schemas"]["PublicationEventType"]; + /** Format: date-time */ + occurredAt: string; + publishedVersion: number; + /** Format: uuid */ + sourcePublishedEventId: string | null; + snapshotAvailable: boolean; + }; + PublicationSnapshot: { + event: components["schemas"]["PublicationEvent"]; + renderModel: components["schemas"]["PublicRenderModel"]; + }; + PublishResult: { + publication: components["schemas"]["PublicationAggregate"]; + event: components["schemas"]["PublicationEvent"]; + }; + DocumentSummary: { + /** Format: uuid */ + id: string; + title: string; + kind: components["schemas"]["RecordKind"]; + project: components["schemas"]["DisplayTarget"] | null; + /** Format: date-time */ + updatedAt: string; + publicationStatus: components["schemas"]["PublicationStatus"]; + publishedVersion: number | null; + hasUnpublishedChanges: boolean; + nextAction: components["schemas"]["NextAction"]; + }; + /** @enum {string} */ + PublicationAction: "VIEW_SNAPSHOT" | "VIEW_SOURCE_SNAPSHOT" | "UNPUBLISH"; + PublicationListItem: { + event: components["schemas"]["PublicationEvent"]; + publication: components["schemas"]["PublicationAggregate"]; + document: components["schemas"]["DocumentSummary"]; + availableActions: components["schemas"]["PublicationAction"][]; + }; + WorkingCopyDetail: { + document: components["schemas"]["WorkingCopy"]; + currentValidation: components["schemas"]["ValidationReport"] | null; + latestPreview: components["schemas"]["PublicPreview"] | null; + currentPublication: components["schemas"]["PublicationAggregate"] | null; + dependencyRevision: string; + }; + StudioDashboard: { + continueWriting: components["schemas"]["DocumentSummary"][]; + readyToPublish: components["schemas"]["DocumentSummary"][]; + recentPublications: components["schemas"]["PublicationListItem"][]; + totals: components["schemas"]["DashboardTotals"]; + }; + DashboardTotals: { + documents: number; + readyToPublish: number; + publications: number; + }; + DocumentPage: { + items: components["schemas"]["DocumentSummary"][]; + nextCursor: string | null; + }; + PublicationPage: { + items: components["schemas"]["PublicationListItem"][]; + nextCursor: string | null; + }; + /** @enum {string} */ + CatalogEntryType: "TOPIC" | "PROJECT" | "RELATION" | "EVIDENCE"; + CatalogEntry: { + /** Format: uuid */ + id: string; + type: components["schemas"]["CatalogEntryType"]; + label: string; + /** @enum {string} */ + kind?: "CASE" | "REFERENCE" | "QUESTION" | "PROJECT" | "PROJECT_DECISION"; + publicPath?: string; + dependencyRevision: string; + }; + CatalogPage: { + items: components["schemas"]["CatalogEntry"][]; + nextCursor: string | null; + }; + FieldError: { + /** @description JSON Pointer to the invalid field */ + path: string; + message: string; + }; + ProblemDetails: { + /** Format: uri-reference */ + type: string; + title: string; + status: number; + detail: string; + /** @enum {string} */ + code: "AUTHENTICATION_REQUIRED" | "STUDIO_ACCESS_DENIED" | "DOCUMENT_NOT_FOUND" | "PREVIEW_NOT_FOUND" | "PUBLICATION_NOT_FOUND" | "PUBLICATION_EVENT_NOT_FOUND" | "PUBLICATION_SNAPSHOT_NOT_FOUND" | "VERSION_CONFLICT" | "PUBLICATION_CONFLICT" | "VALIDATION_STALE" | "PREVIEW_STALE" | "PREVIEW_EXPIRED" | "IDEMPOTENCY_KEY_REUSED" | "REQUEST_VALIDATION_FAILED" | "STUDIO_UNAVAILABLE"; + /** Format: uri-reference */ + instance?: string; + traceId?: string; + fieldErrors?: components["schemas"]["FieldError"][]; + latestDocument?: components["schemas"]["WorkingCopyDetail"]; + latestPublication?: components["schemas"]["PublicationAggregate"]; + conflictingFields?: string[]; + retryable?: boolean; + } & { + [key: string]: unknown; + }; + }; + responses: { + /** @description Authentication required */ + AuthenticationRequired: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Studio access denied */ + AccessDenied: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Document not found */ + DocumentNotFound: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Document or preview not found */ + PreviewNotFound: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Publication not found */ + PublicationNotFound: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Publication event or snapshot not found */ + PublicationSnapshotNotFound: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Command conflicts with current state, freshness, or idempotency */ + CommandConflict: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Request validation failed */ + RequestValidationFailed: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Studio unavailable */ + StudioUnavailable: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetails"]; + }; + }; + }; + parameters: { + DocumentId: string; + PublicationId: string; + PublicationEventId: string; + IdempotencyKey: string; + /** @description Free-text query normalized as part of the opaque cursor */ + Query: string; + /** @description Opaque cursor bound to normalized filters and sort */ + Cursor: string; + Limit: number; + DocumentKind: components["schemas"]["RecordKind"]; + PublicationStatus: components["schemas"]["PublicationStatus"]; + NextAction: components["schemas"]["NextAction"]; + ProjectId: string; + DocumentSort: "UPDATED_DESC" | "UPDATED_ASC" | "TITLE_ASC"; + PublicationEventType: components["schemas"]["PublicationEventType"]; + CatalogType: components["schemas"]["CatalogEntryType"]; + }; + requestBodies: never; + headers: { + /** @description True when the original result was replayed */ + IdempotencyReplayed: boolean; + }; + pathItems: never; +} +export type $defs = Record; +export interface operations { + getStudioDashboard: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Dashboard lists and totals */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["StudioDashboard"]; + }; + }; + 401: components["responses"]["AuthenticationRequired"]; + 403: components["responses"]["AccessDenied"]; + 503: components["responses"]["StudioUnavailable"]; + }; + }; + listStudioDocuments: { + parameters: { + query?: { + /** @description Free-text query normalized as part of the opaque cursor */ + q?: components["parameters"]["Query"]; + kind?: components["parameters"]["DocumentKind"]; + publicationStatus?: components["parameters"]["PublicationStatus"]; + nextAction?: components["parameters"]["NextAction"]; + projectId?: components["parameters"]["ProjectId"]; + sort?: components["parameters"]["DocumentSort"]; + /** @description Opaque cursor bound to normalized filters and sort */ + cursor?: components["parameters"]["Cursor"]; + limit?: components["parameters"]["Limit"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Working-copy cursor page */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DocumentPage"]; + }; + }; + 401: components["responses"]["AuthenticationRequired"]; + 403: components["responses"]["AccessDenied"]; + 422: components["responses"]["RequestValidationFailed"]; + 503: components["responses"]["StudioUnavailable"]; + }; + }; + createStudioDocument: { + parameters: { + query?: never; + header: { + "Idempotency-Key": components["parameters"]["IdempotencyKey"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateDocumentInput"]; + }; + }; + responses: { + /** @description Created working copy */ + 201: { + headers: { + "Idempotency-Replayed": components["headers"]["IdempotencyReplayed"]; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WorkingCopy"]; + }; + }; + 401: components["responses"]["AuthenticationRequired"]; + 403: components["responses"]["AccessDenied"]; + 409: components["responses"]["CommandConflict"]; + 422: components["responses"]["RequestValidationFailed"]; + 503: components["responses"]["StudioUnavailable"]; + }; + }; + getStudioDocument: { + parameters: { + query?: never; + header?: never; + path: { + documentId: components["parameters"]["DocumentId"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Working-copy detail */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WorkingCopyDetail"]; + }; + }; + 401: components["responses"]["AuthenticationRequired"]; + 403: components["responses"]["AccessDenied"]; + 404: components["responses"]["DocumentNotFound"]; + 503: components["responses"]["StudioUnavailable"]; + }; + }; + saveStudioDocument: { + parameters: { + query?: never; + header: { + "Idempotency-Key": components["parameters"]["IdempotencyKey"]; + }; + path: { + documentId: components["parameters"]["DocumentId"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SaveDocumentCommand"]; + }; + }; + responses: { + /** @description Saved working-copy detail */ + 200: { + headers: { + "Idempotency-Replayed": components["headers"]["IdempotencyReplayed"]; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WorkingCopyDetail"]; + }; + }; + 401: components["responses"]["AuthenticationRequired"]; + 403: components["responses"]["AccessDenied"]; + 404: components["responses"]["DocumentNotFound"]; + 409: components["responses"]["CommandConflict"]; + 422: components["responses"]["RequestValidationFailed"]; + 503: components["responses"]["StudioUnavailable"]; + }; + }; + validateStudioDocument: { + parameters: { + query?: never; + header: { + "Idempotency-Key": components["parameters"]["IdempotencyKey"]; + }; + path: { + documentId: components["parameters"]["DocumentId"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ValidateDocumentCommand"]; + }; + }; + responses: { + /** @description Validation report */ + 200: { + headers: { + "Idempotency-Replayed": components["headers"]["IdempotencyReplayed"]; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ValidationReport"]; + }; + }; + 401: components["responses"]["AuthenticationRequired"]; + 403: components["responses"]["AccessDenied"]; + 404: components["responses"]["DocumentNotFound"]; + 409: components["responses"]["CommandConflict"]; + 422: components["responses"]["RequestValidationFailed"]; + 503: components["responses"]["StudioUnavailable"]; + }; + }; + getCurrentStudioPreview: { + parameters: { + query?: never; + header?: never; + path: { + documentId: components["parameters"]["DocumentId"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Preview detail */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PreviewDetail"]; + }; + }; + 401: components["responses"]["AuthenticationRequired"]; + 403: components["responses"]["AccessDenied"]; + 404: components["responses"]["PreviewNotFound"]; + 503: components["responses"]["StudioUnavailable"]; + }; + }; + createStudioPreview: { + parameters: { + query?: never; + header: { + "Idempotency-Key": components["parameters"]["IdempotencyKey"]; + }; + path: { + documentId: components["parameters"]["DocumentId"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreatePreviewCommand"]; + }; + }; + responses: { + /** @description Created preview */ + 201: { + headers: { + "Idempotency-Replayed": components["headers"]["IdempotencyReplayed"]; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PublicPreview"]; + }; + }; + 401: components["responses"]["AuthenticationRequired"]; + 403: components["responses"]["AccessDenied"]; + 404: components["responses"]["DocumentNotFound"]; + 409: components["responses"]["CommandConflict"]; + 422: components["responses"]["RequestValidationFailed"]; + 503: components["responses"]["StudioUnavailable"]; + }; + }; + publishStudioDocument: { + parameters: { + query?: never; + header: { + "Idempotency-Key": components["parameters"]["IdempotencyKey"]; + }; + path: { + documentId: components["parameters"]["DocumentId"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PublishDocumentCommand"]; + }; + }; + responses: { + /** @description Publication aggregate and immutable event */ + 200: { + headers: { + "Idempotency-Replayed": components["headers"]["IdempotencyReplayed"]; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PublishResult"]; + }; + }; + 401: components["responses"]["AuthenticationRequired"]; + 403: components["responses"]["AccessDenied"]; + 404: components["responses"]["DocumentNotFound"]; + 409: components["responses"]["CommandConflict"]; + 422: components["responses"]["RequestValidationFailed"]; + 503: components["responses"]["StudioUnavailable"]; + }; + }; + unpublishStudioPublication: { + parameters: { + query?: never; + header: { + "Idempotency-Key": components["parameters"]["IdempotencyKey"]; + }; + path: { + publicationId: components["parameters"]["PublicationId"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UnpublishCommand"]; + }; + }; + responses: { + /** @description Updated publication aggregate and event */ + 200: { + headers: { + "Idempotency-Replayed": components["headers"]["IdempotencyReplayed"]; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PublishResult"]; + }; + }; + 401: components["responses"]["AuthenticationRequired"]; + 403: components["responses"]["AccessDenied"]; + 404: components["responses"]["PublicationNotFound"]; + 409: components["responses"]["CommandConflict"]; + 422: components["responses"]["RequestValidationFailed"]; + 503: components["responses"]["StudioUnavailable"]; + }; + }; + listStudioPublications: { + parameters: { + query?: { + /** @description Free-text query normalized as part of the opaque cursor */ + q?: components["parameters"]["Query"]; + type?: components["parameters"]["PublicationEventType"]; + /** @description Opaque cursor bound to normalized filters and sort */ + cursor?: components["parameters"]["Cursor"]; + limit?: components["parameters"]["Limit"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Publication cursor page */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PublicationPage"]; + }; + }; + 401: components["responses"]["AuthenticationRequired"]; + 403: components["responses"]["AccessDenied"]; + 422: components["responses"]["RequestValidationFailed"]; + 503: components["responses"]["StudioUnavailable"]; + }; + }; + getStudioPublicationSnapshot: { + parameters: { + query?: never; + header?: never; + path: { + publicationEventId: components["parameters"]["PublicationEventId"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Publication snapshot */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PublicationSnapshot"]; + }; + }; + 401: components["responses"]["AuthenticationRequired"]; + 403: components["responses"]["AccessDenied"]; + 404: components["responses"]["PublicationSnapshotNotFound"]; + 503: components["responses"]["StudioUnavailable"]; + }; + }; + listStudioCatalog: { + parameters: { + query: { + type: components["parameters"]["CatalogType"]; + /** @description Free-text query normalized as part of the opaque cursor */ + q?: components["parameters"]["Query"]; + /** @description Opaque cursor bound to normalized filters and sort */ + cursor?: components["parameters"]["Cursor"]; + limit?: components["parameters"]["Limit"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Catalog cursor page */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CatalogPage"]; + }; + }; + 401: components["responses"]["AuthenticationRequired"]; + 403: components["responses"]["AccessDenied"]; + 422: components["responses"]["RequestValidationFailed"]; + 503: components["responses"]["StudioUnavailable"]; + }; + }; +} diff --git a/src/features/tech-log/contracts/studio/studio-api.openapi.yaml b/src/features/tech-log/contracts/studio/studio-api.openapi.yaml new file mode 100644 index 0000000..3e02f35 --- /dev/null +++ b/src/features/tech-log/contracts/studio/studio-api.openapi.yaml @@ -0,0 +1,984 @@ +openapi: 3.1.0 +info: + title: TechLog Studio API + version: 1.0.0 + description: Transport contract for the TechLog Studio authoring frontend. + license: { name: Proprietary, identifier: LicenseRef-Proprietary } +servers: + - url: / +security: [] +paths: + /api/v1/studio/dashboard: + get: + operationId: getStudioDashboard + summary: Get the Studio dashboard + responses: + "200": { description: Dashboard lists and totals, content: { application/json: { schema: { $ref: "#/components/schemas/StudioDashboard" } } } } + "401": { $ref: "#/components/responses/AuthenticationRequired" } + "403": { $ref: "#/components/responses/AccessDenied" } + "503": { $ref: "#/components/responses/StudioUnavailable" } + /api/v1/studio/documents: + get: + operationId: listStudioDocuments + summary: List working copies + parameters: + - { $ref: "#/components/parameters/Query" } + - { $ref: "#/components/parameters/DocumentKind" } + - { $ref: "#/components/parameters/PublicationStatus" } + - { $ref: "#/components/parameters/NextAction" } + - { $ref: "#/components/parameters/ProjectId" } + - { $ref: "#/components/parameters/DocumentSort" } + - { $ref: "#/components/parameters/Cursor" } + - { $ref: "#/components/parameters/Limit" } + responses: + "200": { description: Working-copy cursor page, content: { application/json: { schema: { $ref: "#/components/schemas/DocumentPage" } } } } + "401": { $ref: "#/components/responses/AuthenticationRequired" } + "403": { $ref: "#/components/responses/AccessDenied" } + "422": { $ref: "#/components/responses/RequestValidationFailed" } + "503": { $ref: "#/components/responses/StudioUnavailable" } + post: + operationId: createStudioDocument + summary: Create a working copy + parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }] + requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/CreateDocumentInput" } } } } + responses: + "201": + description: Created working copy + headers: { Idempotency-Replayed: { $ref: "#/components/headers/IdempotencyReplayed" } } + content: { application/json: { schema: { $ref: "#/components/schemas/WorkingCopy" } } } + "401": { $ref: "#/components/responses/AuthenticationRequired" } + "403": { $ref: "#/components/responses/AccessDenied" } + "409": { $ref: "#/components/responses/CommandConflict" } + "422": { $ref: "#/components/responses/RequestValidationFailed" } + "503": { $ref: "#/components/responses/StudioUnavailable" } + /api/v1/studio/documents/{documentId}: + parameters: [{ $ref: "#/components/parameters/DocumentId" }] + get: + operationId: getStudioDocument + summary: Get a working copy and its current state + responses: + "200": { description: Working-copy detail, content: { application/json: { schema: { $ref: "#/components/schemas/WorkingCopyDetail" } } } } + "401": { $ref: "#/components/responses/AuthenticationRequired" } + "403": { $ref: "#/components/responses/AccessDenied" } + "404": { $ref: "#/components/responses/DocumentNotFound" } + "503": { $ref: "#/components/responses/StudioUnavailable" } + put: + operationId: saveStudioDocument + summary: Save a full working copy + parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }] + requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/SaveDocumentCommand" } } } } + responses: + "200": + description: Saved working-copy detail + headers: { Idempotency-Replayed: { $ref: "#/components/headers/IdempotencyReplayed" } } + content: { application/json: { schema: { $ref: "#/components/schemas/WorkingCopyDetail" } } } + "401": { $ref: "#/components/responses/AuthenticationRequired" } + "403": { $ref: "#/components/responses/AccessDenied" } + "404": { $ref: "#/components/responses/DocumentNotFound" } + "409": { $ref: "#/components/responses/CommandConflict" } + "422": { $ref: "#/components/responses/RequestValidationFailed" } + "503": { $ref: "#/components/responses/StudioUnavailable" } + /api/v1/studio/documents/{documentId}/validate: + parameters: [{ $ref: "#/components/parameters/DocumentId" }] + post: + operationId: validateStudioDocument + summary: Validate a saved working copy + parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }] + requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/ValidateDocumentCommand" } } } } + responses: + "200": + description: Validation report + headers: { Idempotency-Replayed: { $ref: "#/components/headers/IdempotencyReplayed" } } + content: { application/json: { schema: { $ref: "#/components/schemas/ValidationReport" } } } + "401": { $ref: "#/components/responses/AuthenticationRequired" } + "403": { $ref: "#/components/responses/AccessDenied" } + "404": { $ref: "#/components/responses/DocumentNotFound" } + "409": { $ref: "#/components/responses/CommandConflict" } + "422": { $ref: "#/components/responses/RequestValidationFailed" } + "503": { $ref: "#/components/responses/StudioUnavailable" } + /api/v1/studio/documents/{documentId}/preview: + parameters: [{ $ref: "#/components/parameters/DocumentId" }] + get: + operationId: getCurrentStudioPreview + summary: Get the latest preview and its computed state + responses: + "200": { description: Preview detail, content: { application/json: { schema: { $ref: "#/components/schemas/PreviewDetail" } } } } + "401": { $ref: "#/components/responses/AuthenticationRequired" } + "403": { $ref: "#/components/responses/AccessDenied" } + "404": { $ref: "#/components/responses/PreviewNotFound" } + "503": { $ref: "#/components/responses/StudioUnavailable" } + post: + operationId: createStudioPreview + summary: Create a public-layout preview + parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }] + requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/CreatePreviewCommand" } } } } + responses: + "201": + description: Created preview + headers: { Idempotency-Replayed: { $ref: "#/components/headers/IdempotencyReplayed" } } + content: { application/json: { schema: { $ref: "#/components/schemas/PublicPreview" } } } + "401": { $ref: "#/components/responses/AuthenticationRequired" } + "403": { $ref: "#/components/responses/AccessDenied" } + "404": { $ref: "#/components/responses/DocumentNotFound" } + "409": { $ref: "#/components/responses/CommandConflict" } + "422": { $ref: "#/components/responses/RequestValidationFailed" } + "503": { $ref: "#/components/responses/StudioUnavailable" } + /api/v1/studio/documents/{documentId}/publish: + parameters: [{ $ref: "#/components/parameters/DocumentId" }] + post: + operationId: publishStudioDocument + summary: Publish or republish a validated preview + parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }] + requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/PublishDocumentCommand" } } } } + responses: + "200": + description: Publication aggregate and immutable event + headers: { Idempotency-Replayed: { $ref: "#/components/headers/IdempotencyReplayed" } } + content: { application/json: { schema: { $ref: "#/components/schemas/PublishResult" } } } + "401": { $ref: "#/components/responses/AuthenticationRequired" } + "403": { $ref: "#/components/responses/AccessDenied" } + "404": { $ref: "#/components/responses/DocumentNotFound" } + "409": { $ref: "#/components/responses/CommandConflict" } + "422": { $ref: "#/components/responses/RequestValidationFailed" } + "503": { $ref: "#/components/responses/StudioUnavailable" } + /api/v1/studio/publications/{publicationId}/unpublish: + parameters: [{ $ref: "#/components/parameters/PublicationId" }] + post: + operationId: unpublishStudioPublication + summary: Unpublish the current publication + parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }] + requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/UnpublishCommand" } } } } + responses: + "200": + description: Updated publication aggregate and event + headers: { Idempotency-Replayed: { $ref: "#/components/headers/IdempotencyReplayed" } } + content: { application/json: { schema: { $ref: "#/components/schemas/PublishResult" } } } + "401": { $ref: "#/components/responses/AuthenticationRequired" } + "403": { $ref: "#/components/responses/AccessDenied" } + "404": { $ref: "#/components/responses/PublicationNotFound" } + "409": { $ref: "#/components/responses/CommandConflict" } + "422": { $ref: "#/components/responses/RequestValidationFailed" } + "503": { $ref: "#/components/responses/StudioUnavailable" } + /api/v1/studio/publications: + get: + operationId: listStudioPublications + summary: List immutable publication events + description: Events are ordered by occurredAt DESC, then publicationEventId. + parameters: + - { $ref: "#/components/parameters/Query" } + - { $ref: "#/components/parameters/PublicationEventType" } + - { $ref: "#/components/parameters/Cursor" } + - { $ref: "#/components/parameters/Limit" } + responses: + "200": { description: Publication cursor page, content: { application/json: { schema: { $ref: "#/components/schemas/PublicationPage" } } } } + "401": { $ref: "#/components/responses/AuthenticationRequired" } + "403": { $ref: "#/components/responses/AccessDenied" } + "422": { $ref: "#/components/responses/RequestValidationFailed" } + "503": { $ref: "#/components/responses/StudioUnavailable" } + /api/v1/studio/publications/{publicationEventId}/preview: + parameters: [{ $ref: "#/components/parameters/PublicationEventId" }] + get: + operationId: getStudioPublicationSnapshot + summary: Get an immutable publication snapshot + responses: + "200": { description: Publication snapshot, content: { application/json: { schema: { $ref: "#/components/schemas/PublicationSnapshot" } } } } + "401": { $ref: "#/components/responses/AuthenticationRequired" } + "403": { $ref: "#/components/responses/AccessDenied" } + "404": { $ref: "#/components/responses/PublicationSnapshotNotFound" } + "503": { $ref: "#/components/responses/StudioUnavailable" } + /api/v1/studio/catalog: + get: + operationId: listStudioCatalog + summary: List catalog entries for editor pickers + parameters: + - { $ref: "#/components/parameters/CatalogType" } + - { $ref: "#/components/parameters/Query" } + - { $ref: "#/components/parameters/Cursor" } + - { $ref: "#/components/parameters/Limit" } + responses: + "200": { description: Catalog cursor page, content: { application/json: { schema: { $ref: "#/components/schemas/CatalogPage" } } } } + "401": { $ref: "#/components/responses/AuthenticationRequired" } + "403": { $ref: "#/components/responses/AccessDenied" } + "422": { $ref: "#/components/responses/RequestValidationFailed" } + "503": { $ref: "#/components/responses/StudioUnavailable" } +components: + parameters: + DocumentId: { name: documentId, in: path, required: true, schema: { type: string, format: uuid } } + PublicationId: { name: publicationId, in: path, required: true, schema: { type: string, format: uuid } } + PublicationEventId: { name: publicationEventId, in: path, required: true, schema: { type: string, format: uuid } } + IdempotencyKey: { name: Idempotency-Key, in: header, required: true, schema: { type: string, minLength: 1, maxLength: 200 } } + Query: { name: q, in: query, description: Free-text query normalized as part of the opaque cursor, schema: { type: string, maxLength: 100 } } + Cursor: { name: cursor, in: query, description: Opaque cursor bound to normalized filters and sort, schema: { type: string, minLength: 1, maxLength: 2000 } } + Limit: { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } } + DocumentKind: { name: kind, in: query, schema: { $ref: "#/components/schemas/RecordKind" } } + PublicationStatus: { name: publicationStatus, in: query, schema: { $ref: "#/components/schemas/PublicationStatus" } } + NextAction: { name: nextAction, in: query, schema: { $ref: "#/components/schemas/NextAction" } } + ProjectId: { name: projectId, in: query, schema: { type: string, format: uuid } } + DocumentSort: { name: sort, in: query, schema: { type: string, enum: [UPDATED_DESC, UPDATED_ASC, TITLE_ASC], default: UPDATED_DESC } } + PublicationEventType: { name: type, in: query, schema: { $ref: "#/components/schemas/PublicationEventType" } } + CatalogType: { name: type, in: query, required: true, schema: { $ref: "#/components/schemas/CatalogEntryType" } } + headers: + IdempotencyReplayed: { description: True when the original result was replayed, schema: { type: boolean } } + responses: + AuthenticationRequired: { description: Authentication required, x-error-codes: [AUTHENTICATION_REQUIRED], content: { application/problem+json: { schema: { $ref: "#/components/schemas/ProblemDetails" } } } } + AccessDenied: { description: Studio access denied, x-error-codes: [STUDIO_ACCESS_DENIED], content: { application/problem+json: { schema: { $ref: "#/components/schemas/ProblemDetails" } } } } + DocumentNotFound: { description: Document not found, x-error-codes: [DOCUMENT_NOT_FOUND], content: { application/problem+json: { schema: { $ref: "#/components/schemas/ProblemDetails" } } } } + PreviewNotFound: { description: Document or preview not found, x-error-codes: [DOCUMENT_NOT_FOUND, PREVIEW_NOT_FOUND], content: { application/problem+json: { schema: { $ref: "#/components/schemas/ProblemDetails" } } } } + PublicationNotFound: { description: Publication not found, x-error-codes: [PUBLICATION_NOT_FOUND], content: { application/problem+json: { schema: { $ref: "#/components/schemas/ProblemDetails" } } } } + PublicationSnapshotNotFound: { description: Publication event or snapshot not found, x-error-codes: [PUBLICATION_EVENT_NOT_FOUND, PUBLICATION_SNAPSHOT_NOT_FOUND], content: { application/problem+json: { schema: { $ref: "#/components/schemas/ProblemDetails" } } } } + CommandConflict: + description: Command conflicts with current state, freshness, or idempotency + x-error-codes: [VERSION_CONFLICT, PUBLICATION_CONFLICT, VALIDATION_STALE, PREVIEW_STALE, PREVIEW_EXPIRED, IDEMPOTENCY_KEY_REUSED] + content: { application/problem+json: { schema: { $ref: "#/components/schemas/ProblemDetails" } } } + RequestValidationFailed: { description: Request validation failed, x-error-codes: [REQUEST_VALIDATION_FAILED], content: { application/problem+json: { schema: { $ref: "#/components/schemas/ProblemDetails" } } } } + StudioUnavailable: { description: Studio unavailable, x-error-codes: [STUDIO_UNAVAILABLE], content: { application/problem+json: { schema: { $ref: "#/components/schemas/ProblemDetails" } } } } + schemas: + RecordKind: { type: string, enum: [CASE, REFERENCE, QUESTION] } + PublicationStatus: { type: string, enum: [NEVER_PUBLISHED, PUBLISHED, UNPUBLISHED] } + NextAction: { type: string, enum: [CONTINUE_EDITING, VALIDATE, FIX_VALIDATION, CREATE_PREVIEW, PUBLISH, NONE] } + RelationInput: + type: object + additionalProperties: false + required: [id, targetId, reason, order] + properties: + id: { type: [string, "null"], format: uuid } + targetId: { type: [string, "null"], format: uuid } + reason: { type: string, maxLength: 100000 } + order: { type: integer, minimum: 0 } + Relation: + type: object + additionalProperties: false + required: [id, targetId, reason, order] + properties: + id: { type: string, format: uuid } + targetId: { type: string, format: uuid } + reason: { type: string, maxLength: 100000 } + order: { type: integer, minimum: 0 } + OrderedText: + type: object + additionalProperties: false + required: [id, text, order] + properties: + id: { type: string, format: uuid } + text: { type: string, minLength: 1, maxLength: 100000 } + order: { type: integer, minimum: 0 } + ReferenceRule: + type: object + additionalProperties: false + required: [id, title, body, order] + properties: + id: { type: string, format: uuid } + title: { type: string, minLength: 1, maxLength: 120 } + body: { type: string, minLength: 1, maxLength: 100000 } + order: { type: integer, minimum: 0 } + QuestionOption: + type: object + additionalProperties: false + required: [id, title, description, order] + properties: + id: { type: string, format: uuid } + title: { type: string, minLength: 1, maxLength: 120 } + description: { type: string, maxLength: 100000 } + order: { type: integer, minimum: 0 } + QuestionResolution: + type: object + additionalProperties: false + required: [summary, evidenceTargetId, linkLabel] + properties: + summary: { type: string, maxLength: 100000 } + evidenceTargetId: { type: [string, "null"], format: uuid } + linkLabel: { type: string, maxLength: 120 } + WorkingCopyInputBase: + type: object + required: [kind, title, slug, summary, topicId, projectId, relations] + properties: + kind: { $ref: "#/components/schemas/RecordKind" } + title: { type: string, maxLength: 120 } + slug: + oneOf: + - { type: string, const: "" } + - { type: string, minLength: 3, maxLength: 100, pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" } + summary: { type: string, maxLength: 300 } + topicId: { type: [string, "null"], format: uuid } + projectId: { type: [string, "null"], format: uuid } + relations: { type: array, maxItems: 20, items: { $ref: "#/components/schemas/RelationInput" } } + CaseInput: + unevaluatedProperties: false + allOf: + - { $ref: "#/components/schemas/WorkingCopyInputBase" } + - type: object + required: [kind, problem, conclusion, environment, reproduction, lastVerifiedOn, bodyMarkdown] + properties: + kind: { type: string, const: CASE } + problem: { type: string, maxLength: 100000 } + conclusion: { type: string, maxLength: 100000 } + environment: { type: string, maxLength: 100000 } + reproduction: { type: string, maxLength: 100000 } + lastVerifiedOn: { type: [string, "null"], format: date } + bodyMarkdown: { type: string, maxLength: 100000 } + ReferenceInput: + unevaluatedProperties: false + allOf: + - { $ref: "#/components/schemas/WorkingCopyInputBase" } + - type: object + required: [kind, purpose, rules, applyWhen, exceptions, examples, verifiedOn] + properties: + kind: { type: string, const: REFERENCE } + purpose: { type: string, maxLength: 100000 } + rules: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/ReferenceRule" } } + applyWhen: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } + exceptions: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } + examples: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } + verifiedOn: { type: [string, "null"], format: date } + QuestionInput: + unevaluatedProperties: false + allOf: + - { $ref: "#/components/schemas/WorkingCopyInputBase" } + - type: object + required: [kind, questionStatus, facts, assumptions, unknowns, constraints, options, nextValidation, resolution] + properties: + kind: { type: string, const: QUESTION } + questionStatus: { type: [string, "null"], enum: [OPEN, RESOLVED, null] } + facts: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } + assumptions: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } + unknowns: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } + constraints: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } + options: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/QuestionOption" } } + nextValidation: { type: string, maxLength: 100000 } + resolution: + oneOf: + - { $ref: "#/components/schemas/QuestionResolution" } + - { type: "null" } + WorkingCopyInput: + oneOf: + - { $ref: "#/components/schemas/CaseInput" } + - { $ref: "#/components/schemas/ReferenceInput" } + - { $ref: "#/components/schemas/QuestionInput" } + discriminator: + propertyName: kind + mapping: + CASE: "#/components/schemas/CaseInput" + REFERENCE: "#/components/schemas/ReferenceInput" + QUESTION: "#/components/schemas/QuestionInput" + WorkingCopyBase: + allOf: + - { $ref: "#/components/schemas/WorkingCopyInputBase" } + - type: object + required: [id, version, relations, updatedAt] + properties: + id: { type: string, format: uuid } + version: { type: integer, minimum: 1 } + relations: { type: array, maxItems: 20, items: { $ref: "#/components/schemas/Relation" } } + updatedAt: { type: string, format: date-time } + CaseWorkingCopy: + unevaluatedProperties: false + allOf: + - { $ref: "#/components/schemas/WorkingCopyBase" } + - type: object + required: [kind, problem, conclusion, environment, reproduction, lastVerifiedOn, bodyMarkdown] + properties: + kind: { type: string, const: CASE } + problem: { type: string, maxLength: 100000 } + conclusion: { type: string, maxLength: 100000 } + environment: { type: string, maxLength: 100000 } + reproduction: { type: string, maxLength: 100000 } + lastVerifiedOn: { type: [string, "null"], format: date } + bodyMarkdown: { type: string, maxLength: 100000 } + ReferenceWorkingCopy: + unevaluatedProperties: false + allOf: + - { $ref: "#/components/schemas/WorkingCopyBase" } + - type: object + required: [kind, purpose, rules, applyWhen, exceptions, examples, verifiedOn] + properties: + kind: { type: string, const: REFERENCE } + purpose: { type: string, maxLength: 100000 } + rules: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/ReferenceRule" } } + applyWhen: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } + exceptions: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } + examples: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } + verifiedOn: { type: [string, "null"], format: date } + QuestionWorkingCopy: + unevaluatedProperties: false + allOf: + - { $ref: "#/components/schemas/WorkingCopyBase" } + - type: object + required: [kind, questionStatus, facts, assumptions, unknowns, constraints, options, nextValidation, resolution] + properties: + kind: { type: string, const: QUESTION } + questionStatus: { type: [string, "null"], enum: [OPEN, RESOLVED, null] } + facts: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } + assumptions: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } + unknowns: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } + constraints: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } + options: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/QuestionOption" } } + nextValidation: { type: string, maxLength: 100000 } + resolution: + oneOf: + - { $ref: "#/components/schemas/QuestionResolution" } + - { type: "null" } + WorkingCopy: + oneOf: + - { $ref: "#/components/schemas/CaseWorkingCopy" } + - { $ref: "#/components/schemas/ReferenceWorkingCopy" } + - { $ref: "#/components/schemas/QuestionWorkingCopy" } + discriminator: + propertyName: kind + mapping: + CASE: "#/components/schemas/CaseWorkingCopy" + REFERENCE: "#/components/schemas/ReferenceWorkingCopy" + QUESTION: "#/components/schemas/QuestionWorkingCopy" + CreateDocumentInput: { $ref: "#/components/schemas/WorkingCopyInput" } + SaveDocumentCommand: + type: object + additionalProperties: false + required: [expectedVersion, document] + properties: + expectedVersion: { type: integer, minimum: 1 } + document: { $ref: "#/components/schemas/WorkingCopyInput" } + ValidateDocumentCommand: + type: object + additionalProperties: false + required: [expectedVersion] + properties: { expectedVersion: { type: integer, minimum: 1 } } + ValidationIssue: + type: object + additionalProperties: false + required: [code, severity, path, message] + properties: + code: { type: string, minLength: 1, maxLength: 100 } + severity: { type: string, enum: [ERROR, WARNING] } + path: + type: string + pattern: "^(?:/(?:[^~/]|~0|~1)*)*$" + description: JSON Pointer to the affected field + message: { type: string, minLength: 1, maxLength: 1000 } + ValidationReport: + type: object + additionalProperties: false + required: [validationId, documentId, validatedVersion, status, issues, validatedAt, validUntil, dependencyRevision] + properties: + validationId: { type: string, format: uuid } + documentId: { type: string, format: uuid } + validatedVersion: { type: integer, minimum: 1 } + status: { type: string, enum: [INVALID, WARNINGS, VALID] } + issues: { type: array, maxItems: 200, items: { $ref: "#/components/schemas/ValidationIssue" } } + validatedAt: { type: string, format: date-time } + validUntil: { type: string, format: date-time } + dependencyRevision: { type: string, minLength: 1, maxLength: 200 } + CreatePreviewCommand: + type: object + additionalProperties: false + required: [expectedVersion, validationId] + properties: + expectedVersion: { type: integer, minimum: 1 } + validationId: { type: string, format: uuid } + PublishDocumentCommand: + type: object + additionalProperties: false + required: [expectedVersion, validationId, previewId, acknowledgedWarningCodes] + properties: + expectedVersion: { type: integer, minimum: 1 } + validationId: { type: string, format: uuid } + previewId: { type: string, format: uuid } + acknowledgedWarningCodes: { type: array, uniqueItems: true, maxItems: 200, items: { type: string, minLength: 1, maxLength: 100 } } + UnpublishCommand: + type: object + additionalProperties: false + required: [expectedPublicationRevision] + properties: { expectedPublicationRevision: { type: integer, minimum: 1 } } + DisplayTarget: + type: object + additionalProperties: false + required: [id, label, publicPath] + properties: + id: { type: string, format: uuid } + label: { type: string, minLength: 1, maxLength: 200 } + publicPath: { type: [string, "null"], maxLength: 500 } + ResolvedRelation: + type: object + additionalProperties: false + required: [id, targetId, targetKind, title, publicPath, reason, order] + properties: + id: { type: string, format: uuid } + targetId: { type: string, format: uuid } + targetKind: { type: string, enum: [CASE, REFERENCE, QUESTION, PROJECT, PROJECT_DECISION] } + title: { type: string, minLength: 1, maxLength: 200 } + publicPath: { type: [string, "null"], maxLength: 500 } + reason: { type: string, maxLength: 100000 } + order: { type: integer, minimum: 0 } + RenderContext: + type: object + additionalProperties: false + required: [generatedAt, dependencyRevision] + properties: + generatedAt: { type: string, format: date-time } + dependencyRevision: { type: string, minLength: 1, maxLength: 200 } + PublicRenderModelBase: + type: object + required: [kind, slug, title, summary, publicPath, topic, project, relations, renderContext] + properties: + kind: { $ref: "#/components/schemas/RecordKind" } + slug: { type: string, minLength: 3, maxLength: 100, pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" } + title: { type: string, minLength: 1, maxLength: 120 } + summary: { type: string, minLength: 1, maxLength: 300 } + publicPath: { type: string, minLength: 1, maxLength: 500 } + topic: { $ref: "#/components/schemas/DisplayTarget" } + project: + oneOf: + - { $ref: "#/components/schemas/DisplayTarget" } + - { type: "null" } + relations: { type: array, maxItems: 20, items: { $ref: "#/components/schemas/ResolvedRelation" } } + renderContext: { $ref: "#/components/schemas/RenderContext" } + InlineText: + type: object + additionalProperties: false + required: [type, text] + properties: + type: { type: string, const: TEXT } + text: { type: string, minLength: 1, maxLength: 100000 } + InlineContainer: + type: object + required: [type, children] + properties: + type: { type: string, enum: [EMPHASIS, STRONG] } + children: { type: array, maxItems: 1000, items: { $ref: "#/components/schemas/Inline" } } + InlineCode: + type: object + additionalProperties: false + required: [type, code] + properties: + type: { type: string, const: INLINE_CODE } + code: { type: string, minLength: 1, maxLength: 100000 } + InlineLink: + type: object + additionalProperties: false + required: [type, label, href] + properties: + type: { type: string, const: LINK } + label: { type: string, minLength: 1, maxLength: 100000 } + href: { type: string, format: uri, maxLength: 2000 } + InlineStatus: + type: object + additionalProperties: false + required: [type, label, tone] + properties: + type: { type: string, const: STATUS } + label: { type: string, minLength: 1, maxLength: 120 } + tone: { type: string, enum: [warning, evidence, neutral] } + Inline: + oneOf: + - { $ref: "#/components/schemas/InlineText" } + - { $ref: "#/components/schemas/InlineEmphasis" } + - { $ref: "#/components/schemas/InlineStrong" } + - { $ref: "#/components/schemas/InlineCode" } + - { $ref: "#/components/schemas/InlineLink" } + - { $ref: "#/components/schemas/InlineStatus" } + discriminator: + propertyName: type + mapping: + TEXT: "#/components/schemas/InlineText" + EMPHASIS: "#/components/schemas/InlineEmphasis" + STRONG: "#/components/schemas/InlineStrong" + INLINE_CODE: "#/components/schemas/InlineCode" + LINK: "#/components/schemas/InlineLink" + STATUS: "#/components/schemas/InlineStatus" + InlineEmphasis: + unevaluatedProperties: false + allOf: + - { $ref: "#/components/schemas/InlineContainer" } + - { type: object, properties: { type: { type: string, const: EMPHASIS } } } + InlineStrong: + unevaluatedProperties: false + allOf: + - { $ref: "#/components/schemas/InlineContainer" } + - { type: object, properties: { type: { type: string, const: STRONG } } } + HeadingBlock: + type: object + additionalProperties: false + required: [type, id, level, content] + properties: + type: { type: string, const: HEADING } + id: { type: string, minLength: 1, maxLength: 200 } + level: { type: integer, minimum: 2, maximum: 4 } + content: { type: array, maxItems: 1000, items: { $ref: "#/components/schemas/Inline" } } + ParagraphBlock: + type: object + additionalProperties: false + required: [type, content] + properties: + type: { type: string, const: PARAGRAPH } + content: { type: array, maxItems: 1000, items: { $ref: "#/components/schemas/Inline" } } + BlockquoteBlock: + type: object + additionalProperties: false + required: [type, content] + properties: + type: { type: string, const: BLOCKQUOTE } + content: { type: array, maxItems: 1000, items: { $ref: "#/components/schemas/Inline" } } + ListItem: + type: object + additionalProperties: false + required: [id, content] + properties: + id: { type: string, minLength: 1, maxLength: 200 } + content: { type: array, maxItems: 1000, items: { $ref: "#/components/schemas/Inline" } } + ListBlockBase: + type: object + required: [type, items] + properties: + type: { type: string, enum: [UNORDERED_LIST, ORDERED_LIST] } + items: { type: array, maxItems: 1000, items: { $ref: "#/components/schemas/ListItem" } } + UnorderedListBlock: + unevaluatedProperties: false + allOf: + - { $ref: "#/components/schemas/ListBlockBase" } + - { type: object, properties: { type: { type: string, const: UNORDERED_LIST } } } + OrderedListBlock: + unevaluatedProperties: false + allOf: + - { $ref: "#/components/schemas/ListBlockBase" } + - { type: object, properties: { type: { type: string, const: ORDERED_LIST } } } + CodeBlock: + type: object + additionalProperties: false + required: [type, code, language, label] + properties: + type: { type: string, const: CODE_BLOCK } + code: { type: string, maxLength: 100000 } + language: { type: [string, "null"], maxLength: 100 } + label: { type: [string, "null"], maxLength: 200 } + DataTableColumn: + type: object + additionalProperties: false + required: [id, label, alignment] + properties: + id: { type: string, minLength: 1, maxLength: 200 } + label: { type: string, minLength: 1, maxLength: 500 } + alignment: { type: string, enum: [LEFT, CENTER, RIGHT] } + DataTableCell: + type: object + additionalProperties: false + required: [columnId, content] + properties: + columnId: { type: string, minLength: 1, maxLength: 200 } + content: { type: array, maxItems: 1000, items: { $ref: "#/components/schemas/Inline" } } + DataTableRow: + type: object + additionalProperties: false + required: [id, cells] + properties: + id: { type: string, minLength: 1, maxLength: 200 } + cells: { type: array, maxItems: 100, items: { $ref: "#/components/schemas/DataTableCell" } } + DataTableBlock: + type: object + additionalProperties: false + required: [type, id, caption, rowHeaderColumn, columns, rows] + properties: + type: { type: string, const: DATA_TABLE } + id: { type: string, minLength: 1, maxLength: 200 } + caption: { type: string, maxLength: 1000 } + rowHeaderColumn: { type: [integer, "null"], minimum: 1 } + columns: { type: array, minItems: 1, maxItems: 100, items: { $ref: "#/components/schemas/DataTableColumn" } } + rows: { type: array, maxItems: 1000, items: { $ref: "#/components/schemas/DataTableRow" } } + CalloutBlock: + type: object + additionalProperties: false + required: [type, tone, label, content] + properties: + type: { type: string, const: CALLOUT } + tone: { type: string, enum: [warning, info] } + label: { type: string, maxLength: 200 } + content: { type: array, maxItems: 1000, items: { $ref: "#/components/schemas/Inline" } } + EvidenceFigureBlock: + type: object + additionalProperties: false + required: [type, key, alt, caption, zoom] + properties: + type: { type: string, const: EVIDENCE_FIGURE } + key: { type: string, minLength: 1, maxLength: 200 } + alt: { type: string, minLength: 1, maxLength: 1000 } + caption: { type: string, maxLength: 1000 } + zoom: { type: boolean } + CaseRenderBlock: + oneOf: + - { $ref: "#/components/schemas/HeadingBlock" } + - { $ref: "#/components/schemas/ParagraphBlock" } + - { $ref: "#/components/schemas/BlockquoteBlock" } + - { $ref: "#/components/schemas/UnorderedListBlock" } + - { $ref: "#/components/schemas/OrderedListBlock" } + - { $ref: "#/components/schemas/CodeBlock" } + - { $ref: "#/components/schemas/DataTableBlock" } + - { $ref: "#/components/schemas/CalloutBlock" } + - { $ref: "#/components/schemas/EvidenceFigureBlock" } + discriminator: + propertyName: type + mapping: + HEADING: "#/components/schemas/HeadingBlock" + PARAGRAPH: "#/components/schemas/ParagraphBlock" + BLOCKQUOTE: "#/components/schemas/BlockquoteBlock" + UNORDERED_LIST: "#/components/schemas/UnorderedListBlock" + ORDERED_LIST: "#/components/schemas/OrderedListBlock" + CODE_BLOCK: "#/components/schemas/CodeBlock" + DATA_TABLE: "#/components/schemas/DataTableBlock" + CALLOUT: "#/components/schemas/CalloutBlock" + EVIDENCE_FIGURE: "#/components/schemas/EvidenceFigureBlock" + CasePublicRenderModel: + unevaluatedProperties: false + allOf: + - { $ref: "#/components/schemas/PublicRenderModelBase" } + - type: object + required: [kind, problem, conclusion, environment, reproduction, lastVerifiedOn, bodyBlocks] + properties: + kind: { type: string, const: CASE } + problem: { type: string, minLength: 1, maxLength: 100000 } + conclusion: { type: string, minLength: 1, maxLength: 100000 } + environment: { type: string, maxLength: 100000 } + reproduction: { type: string, maxLength: 100000 } + lastVerifiedOn: { type: string, format: date } + bodyBlocks: { type: array, maxItems: 10000, items: { $ref: "#/components/schemas/CaseRenderBlock" } } + ReferencePublicRenderModel: + unevaluatedProperties: false + allOf: + - { $ref: "#/components/schemas/PublicRenderModelBase" } + - type: object + required: [kind, purpose, rules, applyWhen, exceptions, examples, verifiedOn] + properties: + kind: { type: string, const: REFERENCE } + purpose: { type: string, minLength: 1, maxLength: 100000 } + rules: { type: array, minItems: 1, maxItems: 50, items: { $ref: "#/components/schemas/ReferenceRule" } } + applyWhen: { type: array, minItems: 1, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } + exceptions: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } + examples: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } + verifiedOn: { type: string, format: date } + ResolvedQuestionResolution: + type: object + additionalProperties: false + required: [summary, evidenceTarget, linkLabel] + properties: + summary: { type: string, minLength: 1, maxLength: 100000 } + evidenceTarget: { $ref: "#/components/schemas/DisplayTarget" } + linkLabel: { type: string, minLength: 1, maxLength: 120 } + QuestionPublicRenderModel: + unevaluatedProperties: false + allOf: + - { $ref: "#/components/schemas/PublicRenderModelBase" } + - type: object + required: [kind, status, facts, assumptions, unknowns, constraints, options, nextValidation, resolution] + properties: + kind: { type: string, const: QUESTION } + status: { type: string, enum: [OPEN, RESOLVED] } + facts: { type: array, minItems: 1, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } + assumptions: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } + unknowns: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } + constraints: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } + options: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/QuestionOption" } } + nextValidation: { type: string, minLength: 1, maxLength: 100000 } + resolution: + oneOf: + - { $ref: "#/components/schemas/ResolvedQuestionResolution" } + - { type: "null" } + PublicRenderModel: + oneOf: + - { $ref: "#/components/schemas/CasePublicRenderModel" } + - { $ref: "#/components/schemas/ReferencePublicRenderModel" } + - { $ref: "#/components/schemas/QuestionPublicRenderModel" } + discriminator: + propertyName: kind + mapping: + CASE: "#/components/schemas/CasePublicRenderModel" + REFERENCE: "#/components/schemas/ReferencePublicRenderModel" + QUESTION: "#/components/schemas/QuestionPublicRenderModel" + PublicPreview: + type: object + additionalProperties: false + required: [previewId, documentId, previewVersion, validationId, createdAt, expiresAt, renderModel] + properties: + previewId: { type: string, format: uuid } + documentId: { type: string, format: uuid } + previewVersion: { type: integer, minimum: 1 } + validationId: { type: string, format: uuid } + createdAt: { type: string, format: date-time } + expiresAt: { type: string, format: date-time } + renderModel: { $ref: "#/components/schemas/PublicRenderModel" } + PreviewDetail: + type: object + additionalProperties: false + required: [preview, state, currentDocumentVersion, currentValidationId] + properties: + preview: { $ref: "#/components/schemas/PublicPreview" } + state: { type: string, enum: [CURRENT, STALE, EXPIRED] } + currentDocumentVersion: { type: integer, minimum: 1 } + currentValidationId: { type: [string, "null"], format: uuid } + PublicationAggregate: + type: object + additionalProperties: false + required: [publicationId, documentId, status, publishedVersion, publicationRevision, latestEventId, publicPath, updatedAt] + properties: + publicationId: { type: string, format: uuid } + documentId: { type: string, format: uuid } + status: { type: string, enum: [PUBLISHED, UNPUBLISHED] } + publishedVersion: { type: integer, minimum: 1 } + publicationRevision: { type: integer, minimum: 1 } + latestEventId: { type: string, format: uuid } + publicPath: { type: string, minLength: 1, maxLength: 500 } + updatedAt: { type: string, format: date-time } + PublicationEventType: { type: string, enum: [PUBLISHED, REPUBLISHED, UNPUBLISHED] } + PublicationEvent: + type: object + additionalProperties: false + required: [publicationEventId, publicationId, documentId, type, occurredAt, publishedVersion, sourcePublishedEventId, snapshotAvailable] + properties: + publicationEventId: { type: string, format: uuid } + publicationId: { type: string, format: uuid } + documentId: { type: string, format: uuid } + type: { $ref: "#/components/schemas/PublicationEventType" } + occurredAt: { type: string, format: date-time } + publishedVersion: { type: integer, minimum: 1 } + sourcePublishedEventId: { type: [string, "null"], format: uuid } + snapshotAvailable: { type: boolean } + PublicationSnapshot: + type: object + additionalProperties: false + required: [event, renderModel] + properties: + event: { $ref: "#/components/schemas/PublicationEvent" } + renderModel: { $ref: "#/components/schemas/PublicRenderModel" } + PublishResult: + type: object + additionalProperties: false + required: [publication, event] + properties: + publication: { $ref: "#/components/schemas/PublicationAggregate" } + event: { $ref: "#/components/schemas/PublicationEvent" } + DocumentSummary: + type: object + additionalProperties: false + required: [id, title, kind, project, updatedAt, publicationStatus, publishedVersion, hasUnpublishedChanges, nextAction] + properties: + id: { type: string, format: uuid } + title: { type: string, maxLength: 120 } + kind: { $ref: "#/components/schemas/RecordKind" } + project: + oneOf: + - { $ref: "#/components/schemas/DisplayTarget" } + - { type: "null" } + updatedAt: { type: string, format: date-time } + publicationStatus: { $ref: "#/components/schemas/PublicationStatus" } + publishedVersion: { type: [integer, "null"], minimum: 1 } + hasUnpublishedChanges: { type: boolean } + nextAction: { $ref: "#/components/schemas/NextAction" } + PublicationAction: { type: string, enum: [VIEW_SNAPSHOT, VIEW_SOURCE_SNAPSHOT, UNPUBLISH] } + PublicationListItem: + type: object + additionalProperties: false + required: [event, publication, document, availableActions] + properties: + event: { $ref: "#/components/schemas/PublicationEvent" } + publication: { $ref: "#/components/schemas/PublicationAggregate" } + document: { $ref: "#/components/schemas/DocumentSummary" } + availableActions: { type: array, uniqueItems: true, maxItems: 3, items: { $ref: "#/components/schemas/PublicationAction" } } + WorkingCopyDetail: + type: object + additionalProperties: false + required: [document, currentValidation, latestPreview, currentPublication, dependencyRevision] + properties: + document: { $ref: "#/components/schemas/WorkingCopy" } + currentValidation: + oneOf: + - { $ref: "#/components/schemas/ValidationReport" } + - { type: "null" } + latestPreview: + oneOf: + - { $ref: "#/components/schemas/PublicPreview" } + - { type: "null" } + currentPublication: + oneOf: + - { $ref: "#/components/schemas/PublicationAggregate" } + - { type: "null" } + dependencyRevision: { type: string, minLength: 1, maxLength: 200 } + StudioDashboard: + type: object + additionalProperties: false + required: [continueWriting, readyToPublish, recentPublications, totals] + properties: + continueWriting: { type: array, maxItems: 5, items: { $ref: "#/components/schemas/DocumentSummary" } } + readyToPublish: { type: array, maxItems: 5, items: { $ref: "#/components/schemas/DocumentSummary" } } + recentPublications: { type: array, maxItems: 5, items: { $ref: "#/components/schemas/PublicationListItem" } } + totals: { $ref: "#/components/schemas/DashboardTotals" } + DashboardTotals: + type: object + additionalProperties: false + required: [documents, readyToPublish, publications] + properties: + documents: { type: integer, minimum: 0 } + readyToPublish: { type: integer, minimum: 0 } + publications: { type: integer, minimum: 0 } + DocumentPage: + type: object + additionalProperties: false + required: [items, nextCursor] + properties: + items: { type: array, maxItems: 100, items: { $ref: "#/components/schemas/DocumentSummary" } } + nextCursor: { type: [string, "null"], maxLength: 2000 } + PublicationPage: + type: object + additionalProperties: false + required: [items, nextCursor] + properties: + items: { type: array, maxItems: 100, items: { $ref: "#/components/schemas/PublicationListItem" } } + nextCursor: { type: [string, "null"], maxLength: 2000 } + CatalogEntryType: { type: string, enum: [TOPIC, PROJECT, RELATION, EVIDENCE] } + CatalogEntry: + type: object + additionalProperties: false + required: [id, type, label, dependencyRevision] + properties: + id: { type: string, format: uuid } + type: { $ref: "#/components/schemas/CatalogEntryType" } + label: { type: string, minLength: 1, maxLength: 200 } + kind: { type: string, enum: [CASE, REFERENCE, QUESTION, PROJECT, PROJECT_DECISION] } + publicPath: { type: string, minLength: 1, maxLength: 500 } + dependencyRevision: { type: string, minLength: 1, maxLength: 200 } + CatalogPage: + type: object + additionalProperties: false + required: [items, nextCursor] + properties: + items: { type: array, maxItems: 100, items: { $ref: "#/components/schemas/CatalogEntry" } } + nextCursor: { type: [string, "null"], maxLength: 2000 } + FieldError: + type: object + additionalProperties: false + required: [path, message] + properties: + path: + type: string + pattern: "^(?:/(?:[^~/]|~0|~1)*)*$" + description: JSON Pointer to the invalid field + message: { type: string, minLength: 1, maxLength: 1000 } + ProblemDetails: + type: object + additionalProperties: true + required: [type, title, status, detail, code] + properties: + type: { type: string, format: uri-reference } + title: { type: string, minLength: 1, maxLength: 200 } + status: { type: integer, minimum: 400, maximum: 599 } + detail: { type: string, minLength: 1, maxLength: 5000 } + code: + type: string + enum: [AUTHENTICATION_REQUIRED, STUDIO_ACCESS_DENIED, DOCUMENT_NOT_FOUND, PREVIEW_NOT_FOUND, PUBLICATION_NOT_FOUND, PUBLICATION_EVENT_NOT_FOUND, PUBLICATION_SNAPSHOT_NOT_FOUND, VERSION_CONFLICT, PUBLICATION_CONFLICT, VALIDATION_STALE, PREVIEW_STALE, PREVIEW_EXPIRED, IDEMPOTENCY_KEY_REUSED, REQUEST_VALIDATION_FAILED, STUDIO_UNAVAILABLE] + instance: { type: string, format: uri-reference } + traceId: { type: string, maxLength: 200 } + fieldErrors: { type: array, maxItems: 200, items: { $ref: "#/components/schemas/FieldError" } } + latestDocument: { $ref: "#/components/schemas/WorkingCopyDetail" } + latestPublication: { $ref: "#/components/schemas/PublicationAggregate" } + conflictingFields: + type: array + uniqueItems: true + maxItems: 200 + items: + type: string + pattern: "^(?:/(?:[^~/]|~0|~1)*)*$" + retryable: { type: boolean } diff --git a/tests/features/tech-log/feature-input.test.ts b/tests/features/tech-log/feature-input.test.ts new file mode 100644 index 0000000..3e4432a --- /dev/null +++ b/tests/features/tech-log/feature-input.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; + +import type { ApplicationFeatureInputs } from "../../../src/application/ports/in/application-api.ts"; +import { + TECH_LOG_FEATURE_ID, + type TechLogFeatureInput, +} from "../../../src/features/tech-log/application/tech-log-feature-input.ts"; +import type { PublicContentQueries } from "../../../src/features/tech-log/application/ports/public-content-queries.ts"; +import type { StudioGateway } from "../../../src/features/tech-log/application/ports/studio-gateway.ts"; + +function acceptsApplicationFeatureInput( + input: ApplicationFeatureInputs["tech-log"], +): TechLogFeatureInput { + return input; +} + +describe("TechLog feature input", () => { + it("uses the fixed application feature identifier", () => { + expect(TECH_LOG_FEATURE_ID).toBe("tech-log"); + }); + + it("accepts public queries and a Studio gateway factory through the application registry", () => { + const publicContent = {} as PublicContentQueries; + const gateway = {} as StudioGateway; + const input = acceptsApplicationFeatureInput({ + publicContent, + createStudioGateway: () => gateway, + }); + + expect(input.publicContent).toBe(publicContent); + expect(input.createStudioGateway()).toBe(gateway); + }); +}); diff --git a/tests/features/tech-log/studio-contract.test.ts b/tests/features/tech-log/studio-contract.test.ts new file mode 100644 index 0000000..83239fe --- /dev/null +++ b/tests/features/tech-log/studio-contract.test.ts @@ -0,0 +1,126 @@ +import { describe, expect, it } from "vitest"; + +import type { components } from "../../../src/features/tech-log/contracts/studio/generated.ts"; +import { + isStudioGatewayError, + StudioGatewayError, +} from "../../../src/features/tech-log/application/ports/studio-gateway-error.ts"; +import type { + StudioGateway, +} from "../../../src/features/tech-log/application/ports/studio-gateway.ts"; + +type Inline = components["schemas"]["Inline"]; +type CaseRenderBlock = components["schemas"]["CaseRenderBlock"]; + +const inlines: Inline[] = [ + { type: "TEXT", text: "plain" }, + { type: "EMPHASIS", children: [{ type: "TEXT", text: "emphasis" }] }, + { type: "STRONG", children: [{ type: "TEXT", text: "strong" }] }, + { type: "INLINE_CODE", code: "findById" }, + { type: "LINK", label: "TechLog", href: "https://example.com" }, + { type: "STATUS", label: "검증 필요", tone: "warning" }, +]; + +const blocks: CaseRenderBlock[] = [ + { type: "HEADING", id: "problem", level: 2, content: [] }, + { type: "PARAGRAPH", content: [] }, + { type: "BLOCKQUOTE", content: [] }, + { type: "UNORDERED_LIST", items: [] }, + { type: "ORDERED_LIST", items: [] }, + { type: "CODE_BLOCK", code: "select 1", language: "sql", label: null }, + { + type: "DATA_TABLE", + id: "query-counts", + caption: "Query counts", + rowHeaderColumn: null, + columns: [{ id: "query-counts-column-1", label: "Case", alignment: "LEFT" }], + rows: [], + }, + { type: "CALLOUT", tone: "warning", label: "주의", content: [] }, + { type: "EVIDENCE_FIGURE", key: "fetch-plan", alt: "Fetch plan", caption: "Measured fetch plan", zoom: true }, +]; + +const gatewayMethodNames = [ + "getDashboard", + "listDocuments", + "createDocument", + "getDocument", + "saveDocument", + "validateDocument", + "createPreview", + "getCurrentPreview", + "publishDocument", + "unpublishPublication", + "listPublications", + "getPublicationSnapshot", + "getCatalog", +] as const; + +function gatewayWithEveryRequiredMethod(): StudioGateway { + return { + getDashboard: async () => undefined as never, + listDocuments: async () => undefined as never, + createDocument: async () => undefined as never, + getDocument: async () => undefined as never, + saveDocument: async () => undefined as never, + validateDocument: async () => undefined as never, + createPreview: async () => undefined as never, + getCurrentPreview: async () => undefined as never, + publishDocument: async () => undefined as never, + unpublishPublication: async () => undefined as never, + listPublications: async () => undefined as never, + getPublicationSnapshot: async () => undefined as never, + getCatalog: async () => undefined as never, + }; +} + +describe("TechLog Studio contracts", () => { + it("preserves every public wire discriminator exactly once", () => { + expect(inlines.map((inline) => inline.type)).toEqual([ + "TEXT", + "EMPHASIS", + "STRONG", + "INLINE_CODE", + "LINK", + "STATUS", + ]); + expect(blocks.map((block) => block.type)).toEqual([ + "HEADING", + "PARAGRAPH", + "BLOCKQUOTE", + "UNORDERED_LIST", + "ORDERED_LIST", + "CODE_BLOCK", + "DATA_TABLE", + "CALLOUT", + "EVIDENCE_FIGURE", + ]); + expect(new Set(blocks.map((block) => block.type)).size).toBe(blocks.length); + }); + + it("requires the exact Studio gateway operation set", () => { + expect(Object.keys(gatewayWithEveryRequiredMethod()).sort()).toEqual( + [...gatewayMethodNames].sort(), + ); + }); + + it("keeps RFC 9457-style gateway failures inspectable at the application boundary", () => { + const error = new StudioGatewayError({ + type: "https://techlog.dev/problems/conflict", + title: "Conflict", + status: 409, + detail: "The working copy has changed.", + code: "DOCUMENT_VERSION_CONFLICT", + retryable: false, + }); + + expect(error).toMatchObject({ + name: "StudioGatewayError", + status: 409, + code: "DOCUMENT_VERSION_CONFLICT", + retryable: false, + }); + expect(isStudioGatewayError(error)).toBe(true); + expect(isStudioGatewayError(new DOMException("Aborted", "AbortError"))).toBe(false); + }); +});