feat: define closed portfolio input contract

This commit is contained in:
2026-09-02 00:31:43 +03:00
parent 51af112e21
commit 4a704d89f7
20 changed files with 819 additions and 0 deletions

12
src/content/asset-url.ts Normal file
View File

@@ -0,0 +1,12 @@
import type { PublicAssetManifest } from './types.ts';
export const assetUrl = (manifest: PublicAssetManifest, assetId: string, base = '/assets/'): string => {
if (!base.startsWith('/') || base.startsWith('//') || /[?#]/u.test(base)) {
throw new Error('Asset URL base must be an absolute path without a query or fragment.');
}
const asset = manifest.assets.find((candidate) => candidate.id === assetId);
if (asset === undefined) {
throw new Error(`Unknown asset ID: ${assetId}`);
}
return `${base.endsWith('/') ? base : `${base}/`}${encodeURIComponent(asset.fileName)}`;
};

View File

@@ -0,0 +1,170 @@
import { createHash } from 'node:crypto';
import { lstat, readFile, readdir } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { join } from 'node:path';
import { validateProductionInputs } from './schemas.ts';
import type { ProductionInputs } from './types.ts';
export interface ProductionInputLocations {
contentJson: URL;
assetManifestJson: URL;
preparedAssetRoot: URL;
siteConfigJson: URL;
}
const assertPlainFileUrl = (url: URL, label: string): string => {
if (url.protocol !== 'file:' || url.search || url.hash) {
throw new Error(`${label} must be a plain file URL.`);
}
return fileURLToPath(url);
};
const assertRegularFile = async (path: string, label: string): Promise<void> => {
const stats = await lstat(path);
if (stats.isSymbolicLink() || !stats.isFile() || stats.nlink !== 1) {
throw new Error(`${label} must be one regular, non-linked file.`);
}
};
const skipWhitespace = (text: string, index: number): number => {
let next = index;
while (/\s/u.test(text[next] ?? '')) next += 1;
return next;
};
const readJsonString = (text: string, index: number): { value: string; next: number } => {
if (text[index] !== '"') throw new Error('Invalid JSON string.');
let cursor = index + 1;
while (cursor < text.length) {
if (text[cursor] === '\\') {
cursor += 2;
continue;
}
if (text[cursor] === '"') {
const source = text.slice(index, cursor + 1);
return { value: JSON.parse(source) as string, next: cursor + 1 };
}
cursor += 1;
}
throw new Error('Unterminated JSON string.');
};
const findJsonValueEnd = (text: string, index: number): number => {
const cursor = skipWhitespace(text, index);
const character = text[cursor];
if (character === '"') return readJsonString(text, cursor).next;
if (character === '{') {
let next = skipWhitespace(text, cursor + 1);
const keys = new Set<string>();
if (text[next] === '}') return next + 1;
while (next < text.length) {
const key = readJsonString(text, next);
if (keys.has(key.value)) throw new Error(`Duplicate JSON key: ${key.value}`);
keys.add(key.value);
next = skipWhitespace(text, key.next);
if (text[next] !== ':') throw new Error('Invalid JSON object.');
next = skipWhitespace(text, findJsonValueEnd(text, next + 1));
if (text[next] === '}') return next + 1;
if (text[next] !== ',') throw new Error('Invalid JSON object.');
next = skipWhitespace(text, next + 1);
}
throw new Error('Unterminated JSON object.');
}
if (character === '[') {
let next = skipWhitespace(text, cursor + 1);
if (text[next] === ']') return next + 1;
while (next < text.length) {
next = skipWhitespace(text, findJsonValueEnd(text, next));
if (text[next] === ']') return next + 1;
if (text[next] !== ',') throw new Error('Invalid JSON array.');
next = skipWhitespace(text, next + 1);
}
throw new Error('Unterminated JSON array.');
}
let next = cursor;
while (next < text.length && !/[\s,}\]]/u.test(text[next] ?? '')) next += 1;
if (next === cursor) throw new Error('Invalid JSON value.');
return next;
};
const parseJsonWithoutDuplicateKeys = (source: string): unknown => {
const end = skipWhitespace(source, findJsonValueEnd(source, 0));
if (end !== source.length) throw new Error('Invalid JSON input.');
return JSON.parse(source) as unknown;
};
const readExplicitJson = async (url: URL, label: string): Promise<unknown> => {
const path = assertPlainFileUrl(url, label);
await assertRegularFile(path, label);
return parseJsonWithoutDuplicateKeys(await readFile(path, 'utf8'));
};
const sha256 = (bytes: Uint8Array): string => createHash('sha256').update(bytes).digest('hex');
const validatePreparedAssetSet = async (rootUrl: URL, inputs: ProductionInputs): Promise<void> => {
const rootPath = assertPlainFileUrl(rootUrl, 'Prepared asset root');
const rootStats = await lstat(rootPath);
if (rootStats.isSymbolicLink() || !rootStats.isDirectory()) {
throw new Error('Prepared asset root must be a non-symlink directory.');
}
const manifestByFileName = new Map(inputs.manifest.assets.map((asset) => [asset.fileName, asset]));
const expectedCaseFolded = new Set<string>();
for (const asset of inputs.manifest.assets) {
const folded = asset.fileName.toLocaleLowerCase('en-US');
if (expectedCaseFolded.has(folded)) throw new Error('Manifest has a case-folding collision.');
expectedCaseFolded.add(folded);
}
const preparedNames = new Set<string>();
for (const entry of await readdir(rootPath, { withFileTypes: true })) {
if (entry.isDirectory()) throw new Error(`Prepared asset set may not contain directories: ${entry.name}`);
if (entry.isSymbolicLink() || !entry.isFile()) throw new Error(`Prepared asset is a symlink or special file: ${entry.name}`);
const filePath = join(rootPath, entry.name);
const stats = await lstat(filePath);
if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1) {
throw new Error(`Prepared asset is a symlink, hard link, or special file: ${entry.name}`);
}
const foldedName = entry.name.toLocaleLowerCase('en-US');
if (preparedNames.has(foldedName)) throw new Error(`Prepared asset has a case-fold collision: ${entry.name}`);
preparedNames.add(foldedName);
const manifestAsset = manifestByFileName.get(entry.name);
if (manifestAsset === undefined) throw new Error(`Extra prepared asset: ${entry.name}`);
if (sha256(await readFile(filePath)) !== manifestAsset.sha256) {
throw new Error(`Prepared asset hash does not match manifest: ${entry.name}`);
}
}
for (const asset of inputs.manifest.assets) {
if (!preparedNames.has(asset.fileName.toLocaleLowerCase('en-US'))) {
throw new Error(`Missing prepared asset: ${asset.fileName}`);
}
}
};
const deepFreeze = <T>(value: T, seen = new WeakSet<object>()): T => {
if (value !== null && typeof value === 'object') {
const object = value as object;
if (!seen.has(object)) {
seen.add(object);
for (const key of Reflect.ownKeys(object)) {
deepFreeze(Reflect.get(object, key), seen);
}
Object.freeze(object);
}
}
return value;
};
export const loadProductionInputs = async (
locations: ProductionInputLocations,
): Promise<Readonly<ProductionInputs>> => {
const [content, manifest, site] = await Promise.all([
readExplicitJson(locations.contentJson, 'Portfolio content'),
readExplicitJson(locations.assetManifestJson, 'Public asset manifest'),
readExplicitJson(locations.siteConfigJson, 'Site config'),
]);
const inputs = validateProductionInputs(content, manifest, site, locations.preparedAssetRoot);
await validatePreparedAssetSet(locations.preparedAssetRoot, inputs);
return deepFreeze(inputs);
};

224
src/content/schemas.ts Normal file
View File

@@ -0,0 +1,224 @@
import { z } from 'zod';
import type { PortfolioContent, PublicAssetManifest, ProductionInputs, SiteConfig } from './types.ts';
const canonicalNavigationIds = [
'start', 'profile', 'experience', 'infrastructure', 'selected-builds', 'additional-work', 'toolkit', 'contact',
] as const;
const socialHosts = {
GitHub: 'github.com',
GitLab: 'gitlab.com',
LinkedIn: 'linkedin.com',
} as const;
const hasVisibleCopy = (value: string): boolean =>
value.trim().length > 0 && value === value.normalize('NFC') && !/[\u0000-\u001f\u007f]/u.test(value);
const visibleString = z.string().refine(hasVisibleCopy, 'Visible copy must be non-empty, NFC-normalized, and control-character free.');
const opaqueId = z.string().regex(/^[a-z][a-z0-9-]{0,63}$/, 'IDs must be generated safe identifiers.');
const httpUrl = z.string().refine((value) => {
try {
const url = new URL(value);
return url.protocol === 'https:' && !url.username && !url.password && !url.search && !url.hash;
} catch {
return false;
}
}, 'URL must be an absolute HTTPS URL without credentials, query, or fragment.');
const navigationItemSchema = z.object({
id: z.enum(canonicalNavigationIds),
label: visibleString,
}).strict();
const projectSchema = z.object({
id: opaqueId,
title: visibleString,
visibility: z.enum(['public', 'private']),
maturity: z.enum(['learning', 'experimental', 'prototype', 'work-in-progress', 'active']),
summary: visibleString,
problem: visibleString,
contribution: visibleString,
technicalDecisions: z.array(visibleString).min(1),
outcome: visibleString,
url: httpUrl.optional(),
assetIds: z.array(opaqueId),
}).strict().superRefine((project, context) => {
if (project.visibility === 'private' && project.url !== undefined) {
context.addIssue({ code: 'custom', message: 'A private project may not include a url.', path: ['url'] });
}
if (project.visibility === 'private' && project.title !== 'Private project') {
context.addIssue({ code: 'custom', message: 'Private project title must be generic.', path: ['title'] });
}
});
const portfolioContentSchema = z.object({
identity: z.object({
name: z.literal('Luca Pîslaru'),
role: z.literal('System Administrator & Full-Stack Developer'),
valueProposition: visibleString,
}).strict(),
navigation: z.array(navigationItemSchema).length(8),
profile: z.object({
introduction: visibleString,
screeningFacts: z.array(z.object({ label: visibleString, value: visibleString }).strict()),
}).strict(),
experience: z.array(z.object({
id: opaqueId,
role: visibleString,
organization: visibleString,
dates: visibleString,
location: visibleString.optional(),
highlights: z.array(visibleString).min(2).max(4),
}).strict()).min(1),
systemsOperations: z.object({
id: opaqueId,
title: z.literal('Professional systems operations'),
summary: visibleString,
evidence: z.array(visibleString).min(1),
}).strict(),
infrastructure: z.object({
id: opaqueId,
title: z.literal('Secure self-hosted platform and lifecycle'),
summary: visibleString,
bands: z.array(z.object({ id: opaqueId, title: visibleString, summary: visibleString }).strict()).length(4),
}).strict(),
projects: z.object({
selected: z.array(projectSchema).length(3),
additional: z.array(projectSchema).min(1),
}).strict(),
toolkit: z.array(z.object({
title: visibleString,
technologies: z.array(z.object({ name: visibleString, assetId: opaqueId }).strict()).min(1),
}).strict()).min(1),
contact: z.object({
email: z.email(),
social: z.array(z.object({
service: z.enum(['GitHub', 'GitLab', 'LinkedIn']),
url: httpUrl,
}).strict()).superRefine((links, context) => {
for (const [index, link] of links.entries()) {
const host = new URL(link.url).hostname;
if (host !== socialHosts[link.service]) {
context.addIssue({ code: 'custom', message: `Social URL must use the approved ${link.service} host.`, path: [index, 'url'] });
}
}
}),
cvMessage: visibleString,
}).strict(),
metadata: z.object({
title: visibleString,
description: visibleString,
socialPreviewAssetId: opaqueId,
}).strict(),
sceneDescription: visibleString,
}).strict().superRefine((content, context) => {
const navigationIds = content.navigation.map((item) => item.id);
if (new Set(navigationIds).size !== canonicalNavigationIds.length ||
canonicalNavigationIds.some((id) => !navigationIds.includes(id))) {
context.addIssue({ code: 'custom', message: 'Navigation must contain each canonical section ID exactly once.', path: ['navigation'] });
}
const projects = [...content.projects.selected, ...content.projects.additional];
const projectIds = projects.map((project) => project.id);
if (new Set(projectIds).size !== projectIds.length) {
context.addIssue({ code: 'custom', message: 'Project IDs must be unique.', path: ['projects'] });
}
if (content.projects.selected.filter((project) => project.maturity === 'experimental').length !== 1) {
context.addIssue({ code: 'custom', message: 'Selected builds must include exactly one supporting experimental research panel.', path: ['projects', 'selected'] });
}
const experienceIds = content.experience.map((record) => record.id);
if (new Set(experienceIds).size !== experienceIds.length) {
context.addIssue({ code: 'custom', message: 'Experience IDs must be unique.', path: ['experience'] });
}
const bandIds = content.infrastructure.bands.map((band) => band.id);
if (new Set(bandIds).size !== bandIds.length) {
context.addIssue({ code: 'custom', message: 'Infrastructure band IDs must be unique.', path: ['infrastructure', 'bands'] });
}
});
const mediaTypes = ['image/avif', 'image/jpeg', 'image/png', 'image/svg+xml', 'image/webp', 'video/mp4', 'video/webm'] as const;
const fileExtensionByMediaType: Record<(typeof mediaTypes)[number], string> = {
'image/avif': 'avif',
'image/jpeg': 'jpg',
'image/png': 'png',
'image/svg+xml': 'svg',
'image/webp': 'webp',
'video/mp4': 'mp4',
'video/webm': 'webm',
};
const publicAssetSchema = z.object({
id: opaqueId,
fileName: z.string().regex(/^[a-z0-9][a-z0-9-]*\.(avif|jpg|png|svg|webp|mp4|webm)$/, 'Asset filenames must be generated safe filenames.'),
sha256: z.string().regex(/^[a-f0-9]{64}$/, 'Asset sha256 must be lowercase hexadecimal.'),
mediaType: z.enum(mediaTypes),
width: z.number().int().positive().nullable(),
height: z.number().int().positive().nullable(),
durationMs: z.number().int().positive().nullable(),
rightsHolder: visibleString,
license: visibleString,
attribution: visibleString,
}).strict().superRefine((asset, context) => {
if (!asset.fileName.endsWith(`.${fileExtensionByMediaType[asset.mediaType]}`)) {
context.addIssue({ code: 'custom', message: 'Asset filename extension must match media type.', path: ['fileName'] });
}
const image = asset.mediaType.startsWith('image/');
if (image && (asset.width === null || asset.height === null || asset.durationMs !== null)) {
context.addIssue({ code: 'custom', message: 'Image assets require dimensions and no duration.', path: ['width'] });
}
if (!image && (asset.width === null || asset.height === null || asset.durationMs === null)) {
context.addIssue({ code: 'custom', message: 'Video assets require dimensions and duration.', path: ['durationMs'] });
}
});
const publicAssetManifestSchema = z.object({ assets: z.array(publicAssetSchema).min(1) }).strict().superRefine((manifest, context) => {
const ids = manifest.assets.map((asset) => asset.id);
if (new Set(ids).size !== ids.length) {
context.addIssue({ code: 'custom', message: 'Duplicate asset ID in manifest.', path: ['assets'] });
}
const filenames = manifest.assets.map((asset) => asset.fileName);
if (new Set(filenames).size !== filenames.length || new Set(filenames.map((name) => name.toLocaleLowerCase('en-US'))).size !== filenames.length) {
context.addIssue({ code: 'custom', message: 'Asset filenames must be unique without case-fold collisions.', path: ['assets'] });
}
});
const siteConfigSchema = z.object({
environment: z.enum(['preview', 'production']),
canonicalUrl: httpUrl,
buildId: z.string().regex(/^[a-z0-9][a-z0-9._-]{0,127}$/, 'Build ID must be a safe immutable identifier.'),
}).strict().superRefine((site, context) => {
const url = new URL(site.canonicalUrl);
if (url.pathname !== '/') {
context.addIssue({ code: 'custom', message: 'Canonical URL must not contain a path.', path: ['canonicalUrl'] });
}
const previewHost = url.hostname === 'localhost' || url.hostname.endsWith('.localhost') || url.hostname.endsWith('.test');
if (site.environment === 'preview' && !previewHost) {
context.addIssue({ code: 'custom', message: 'Preview canonical URL must use a localhost or .test host.', path: ['canonicalUrl'] });
}
if (site.environment === 'production' && previewHost) {
context.addIssue({ code: 'custom', message: 'Production canonical URL may not use a preview host.', path: ['canonicalUrl'] });
}
});
export const parsePortfolioContent = (input: unknown): PortfolioContent => portfolioContentSchema.parse(input) as PortfolioContent;
export const parsePublicAssetManifest = (input: unknown): PublicAssetManifest => publicAssetManifestSchema.parse(input) as PublicAssetManifest;
export const parseSiteConfig = (input: unknown): SiteConfig => siteConfigSchema.parse(input) as SiteConfig;
export const validateProductionInputs = (
contentInput: unknown,
manifestInput: unknown,
siteInput: unknown,
_preparedAssetRoot: URL,
): ProductionInputs => {
const content = parsePortfolioContent(contentInput);
const manifest = parsePublicAssetManifest(manifestInput);
const site = parseSiteConfig(siteInput);
const manifestIds = new Set(manifest.assets.map((asset) => asset.id));
const referencedAssetIds = [
...content.projects.selected.flatMap((project) => project.assetIds),
...content.projects.additional.flatMap((project) => project.assetIds),
...content.toolkit.flatMap((group) => group.technologies.map((technology) => technology.assetId)),
content.metadata.socialPreviewAssetId,
];
for (const assetId of referencedAssetIds) {
if (!manifestIds.has(assetId)) {
throw new Error(`Unmanifested asset reference: ${assetId}`);
}
}
return { content, manifest, site };
};

138
src/content/types.ts Normal file
View File

@@ -0,0 +1,138 @@
export type SectionId =
| 'start'
| 'profile'
| 'experience'
| 'infrastructure'
| 'selected-builds'
| 'additional-work'
| 'toolkit'
| 'contact';
export type ProjectVisibility = 'public' | 'private';
export type ProjectMaturity = 'learning' | 'experimental' | 'prototype' | 'work-in-progress' | 'active';
export interface ExperienceRecord {
id: string;
role: string;
organization: string;
dates: string;
location?: string;
highlights: readonly string[];
}
export interface SystemsCaseStudy {
id: string;
title: string;
summary: string;
evidence: readonly string[];
}
export interface InfrastructureEvidenceBand {
id: string;
title: string;
summary: string;
}
export interface InfrastructureCaseStudy {
id: string;
title: string;
summary: string;
bands: readonly InfrastructureEvidenceBand[];
}
export interface PublicProject {
id: string;
title: string;
visibility: ProjectVisibility;
maturity: ProjectMaturity;
summary: string;
problem: string;
contribution: string;
technicalDecisions: readonly string[];
outcome: string;
url?: string;
assetIds: readonly string[];
}
export interface ToolkitTechnology {
name: string;
assetId: string;
}
export interface ToolkitGroup {
title: string;
technologies: readonly ToolkitTechnology[];
}
export interface ContactSocialLink {
service: 'GitHub' | 'GitLab' | 'LinkedIn';
url: string;
}
export interface ContactContent {
email: string;
social: readonly ContactSocialLink[];
cvMessage: string;
}
export interface MetadataContent {
title: string;
description: string;
socialPreviewAssetId: string;
}
export interface PortfolioContent {
identity: {
name: 'Luca Pîslaru';
role: 'System Administrator & Full-Stack Developer';
valueProposition: string;
};
navigation: ReadonlyArray<{ id: SectionId; label: string }>;
profile: { introduction: string; screeningFacts: ReadonlyArray<{ label: string; value: string }> };
experience: readonly ExperienceRecord[];
systemsOperations: SystemsCaseStudy;
infrastructure: InfrastructureCaseStudy;
projects: { selected: readonly PublicProject[]; additional: readonly PublicProject[] };
toolkit: readonly ToolkitGroup[];
contact: ContactContent;
metadata: MetadataContent;
sceneDescription: string;
}
export type PublicAssetMediaType =
| 'image/avif'
| 'image/jpeg'
| 'image/png'
| 'image/svg+xml'
| 'image/webp'
| 'video/mp4'
| 'video/webm';
export interface PublicAsset {
id: string;
fileName: string;
sha256: string;
mediaType: PublicAssetMediaType;
width: number | null;
height: number | null;
durationMs: number | null;
rightsHolder: string;
license: string;
attribution: string;
}
export interface PublicAssetManifest {
assets: readonly PublicAsset[];
}
export interface SiteConfig {
environment: 'preview' | 'production';
canonicalUrl: string;
buildId: string;
}
export interface ProductionInputs {
content: PortfolioContent;
manifest: PublicAssetManifest;
site: SiteConfig;
}