feat: compose TechLog static and mock adapters

This commit is contained in:
DongHyeonka
2026-08-15 19:36:46 +09:00
parent 708680b28e
commit 27dda3e0e3
18 changed files with 3107 additions and 1 deletions
@@ -0,0 +1,132 @@
import type { components } from "../../contracts/studio/generated.ts";
import type {
PublicationAggregate,
PublicPreview,
ValidationReport,
WorkingCopy,
WorkingCopyDetail,
} from "../../contracts/studio/contract.ts";
export type ValidationResultState = "NOT_RUN" | "INVALID" | "WARNINGS" | "VALID";
export type ValidationFreshness = "NONE" | "CURRENT" | "STALE";
export type PreviewState = "NONE" | "CURRENT" | "STALE" | "EXPIRED";
export type NextAction = components["schemas"]["NextAction"];
export type ValidationState = {
result: ValidationResultState;
freshness: ValidationFreshness;
};
export type StudioDocumentState = {
validation: ValidationState;
preview: PreviewState;
nextAction: NextAction;
};
export type DocumentStateInput = {
// eslint-disable-next-line no-restricted-globals -- Exact Studio contract field.
document: WorkingCopy;
validation: ValidationReport | null;
preview: PublicPreview | null;
publication: PublicationAggregate | null;
dependencyRevision: string;
now: Date | string;
};
export type WorkingCopyDetailStateInput = WorkingCopyDetail & {
now: Date | string;
};
type StateInput = DocumentStateInput | WorkingCopyDetailStateInput;
function normalize(input: StateInput): DocumentStateInput {
if ("validation" in input) return input;
return {
document: input.document,
validation: input.currentValidation,
preview: input.latestPreview,
publication: input.currentPublication,
dependencyRevision: input.dependencyRevision,
now: input.now,
};
}
function timestamp(value: Date | string): number {
return value instanceof Date ? value.getTime() : Date.parse(value);
}
function isStrictlyFuture(value: string, now: Date | string): boolean {
const expiry = timestamp(value);
const current = timestamp(now);
return Number.isFinite(expiry) && Number.isFinite(current) && current < expiry;
}
function isExpired(value: string, now: Date | string): boolean {
const expiry = timestamp(value);
const current = timestamp(now);
return Number.isFinite(expiry) && Number.isFinite(current) && current >= expiry;
}
export function deriveValidationState(input: StateInput): ValidationState {
const { document, validation, dependencyRevision, now } = normalize(input);
if (validation === null) {
return { result: "NOT_RUN", freshness: "NONE" };
}
const freshness: ValidationFreshness =
validation.validatedVersion === document.version &&
isStrictlyFuture(validation.validUntil, now) &&
validation.dependencyRevision === dependencyRevision
? "CURRENT"
: "STALE";
return { result: validation.status, freshness };
}
export function derivePreviewState(input: StateInput): PreviewState {
const normalized = normalize(input);
const { document, validation, preview, now } = normalized;
if (preview === null) return "NONE";
if (isExpired(preview.expiresAt, now)) return "EXPIRED";
return preview.previewVersion === document.version &&
validation !== null &&
preview.validationId === validation.validationId &&
deriveValidationState(normalized).freshness === "CURRENT"
? "CURRENT"
: "STALE";
}
function isCompletelyEmpty(document: WorkingCopy): boolean {
return [document.title, document.slug, document.summary].every(
(value) => value.trim().length === 0,
);
}
export function deriveNextAction(input: StateInput): NextAction {
const normalized = normalize(input);
const { document, publication } = normalized;
if (publication?.status === "PUBLISHED" && publication.publishedVersion === document.version) {
return "NONE";
}
if (isCompletelyEmpty(document)) return "CONTINUE_EDITING";
const validation = deriveValidationState(normalized);
if (validation.freshness !== "CURRENT") return "VALIDATE";
if (validation.result === "INVALID") return "FIX_VALIDATION";
const preview = derivePreviewState(normalized);
if (preview !== "CURRENT") return "CREATE_PREVIEW";
return "PUBLISH";
}
export function deriveDocumentState(input: StateInput): StudioDocumentState {
const normalized = normalize(input);
return {
validation: deriveValidationState(normalized),
preview: derivePreviewState(normalized),
nextAction: deriveNextAction(normalized),
};
}