Makelore 2.0 initial clean snapshot

This commit is contained in:
inman
2026-07-29 17:22:35 +08:00
commit b8ca3f8eea
694 changed files with 139782 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
import { AppError } from '@/lib/error-model';
export type GameAssetCandidate = {
id: string;
name: string;
category: string;
status: string;
purpose: string;
source: string;
license: string;
localPath?: string;
mediaKind: 'image' | 'audio' | 'bundle' | 'unavailable';
previewDataUrl?: string;
audioDataUrl?: string;
manifest: string[];
unavailableReason?: string;
};
export type GameAssetReviewSnapshot = {
invocationId: string;
candidateIds: string[];
decisions: Record<string, 'approved' | 'discarded' | 'replace-requested'>;
status: 'pending' | 'resolved';
pendingAssetIds: string[];
approvedAssetIds: string[];
discardedAssetIds: string[];
};
export type GameAssetReviewResponse = {
review: GameAssetReviewSnapshot;
assets: GameAssetCandidate[];
};
export type GameAssetReviewLoadFailure = 'stale' | 'retryable';
const STALE_HTTP_STATUSES = new Set([400, 403, 404, 409, 410, 422]);
const MEDIA_KINDS = new Set(['image', 'audio', 'bundle', 'unavailable']);
const REVIEW_DECISIONS = new Set(['approved', 'discarded', 'replace-requested']);
function record(value: unknown): Record<string, unknown> | null {
return value !== null && typeof value === 'object'
? value as Record<string, unknown>
: null;
}
function stringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((item) => typeof item === 'string');
}
function optionalString(value: unknown): boolean {
return value === undefined || typeof value === 'string';
}
function validAsset(value: unknown): value is GameAssetCandidate {
const item = record(value);
return Boolean(
item
&& typeof item.id === 'string'
&& typeof item.name === 'string'
&& typeof item.category === 'string'
&& typeof item.status === 'string'
&& typeof item.purpose === 'string'
&& typeof item.source === 'string'
&& typeof item.license === 'string'
&& typeof item.mediaKind === 'string'
&& MEDIA_KINDS.has(item.mediaKind)
&& stringArray(item.manifest)
&& optionalString(item.localPath)
&& optionalString(item.previewDataUrl)
&& optionalString(item.audioDataUrl)
&& optionalString(item.unavailableReason),
);
}
export function isGameAssetReviewResponse(value: unknown): value is GameAssetReviewResponse {
const response = record(value);
const review = record(response?.review);
const decisions = record(review?.decisions);
return Boolean(
response
&& review
&& typeof review.invocationId === 'string'
&& stringArray(review.candidateIds)
&& decisions
&& !Array.isArray(review.decisions)
&& Object.values(decisions).every((decision) => typeof decision === 'string' && REVIEW_DECISIONS.has(decision))
&& (review.status === 'pending' || review.status === 'resolved')
&& stringArray(review.pendingAssetIds)
&& stringArray(review.approvedAssetIds)
&& stringArray(review.discardedAssetIds)
&& Array.isArray(response.assets)
&& response.assets.every(validAsset),
);
}
export function classifyGameAssetReviewLoadFailure(error: unknown): GameAssetReviewLoadFailure {
const status = error instanceof AppError && typeof error.details?.status === 'number'
? error.details.status
: null;
return status !== null && STALE_HTTP_STATUSES.has(status) ? 'stale' : 'retryable';
}