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