Files
memind/mindspace-conversation-package-audit.mjs

251 lines
6.5 KiB
JavaScript

import crypto from 'node:crypto';
import path from 'node:path';
function text(value) {
return String(value ?? '').trim();
}
function normalizeRelativePath(value) {
return text(value).replace(/\\/g, '/').replace(/^\/+/, '');
}
function isPublicHtmlPath(value) {
const normalized = normalizeRelativePath(value);
return (
normalized.startsWith('public/') &&
normalized.toLowerCase().endsWith('.html')
);
}
function issue(code, message, details = {}) {
return { code, message, ...details };
}
export function buildPublicHtmlArtifactId({
userId,
sessionId,
relativePath,
}) {
const hash = crypto
.createHash('sha256')
.update(
`${text(userId)}:${text(sessionId)}:${normalizeRelativePath(relativePath)}`,
)
.digest('hex')
.slice(0, 16);
return `ca_public_html_${hash}`;
}
function artifactHasReadableReference(artifact) {
return Boolean(
text(artifact?.storage_key) ||
text(artifact?.storageKey) ||
text(artifact?.canonical_url) ||
text(artifact?.canonicalUrl),
);
}
function artifactAssetId(artifact) {
return text(artifact?.asset_id ?? artifact?.assetId);
}
function artifactKind(artifact) {
return text(artifact?.artifact_kind ?? artifact?.artifactKind);
}
function artifactDisplayName(artifact) {
return text(artifact?.display_name ?? artifact?.displayName);
}
function assetRelativePath(asset) {
return normalizeRelativePath(
asset?.workspace_relative_path ??
asset?.workspaceRelativePath,
);
}
function assetSize(asset) {
const value = Number(asset?.size_bytes ?? asset?.sizeBytes);
return Number.isFinite(value) ? value : null;
}
function assetUpdatedAt(asset, fallback) {
const value = Number(asset?.updated_at ?? asset?.updatedAt);
return Number.isFinite(value) ? value : fallback;
}
function packageUserId(packageRecord) {
return text(packageRecord?.user_id ?? packageRecord?.userId);
}
function packageSessionId(packageRecord) {
return text(packageRecord?.session_id ?? packageRecord?.sessionId);
}
function packageId(packageRecord) {
return text(packageRecord?.id ?? packageRecord?.packageId);
}
function publicHtmlKey(relativePath) {
return path.posix.basename(normalizeRelativePath(relativePath));
}
function canonicalPublicUrl({
publicBaseUrl,
userId,
relativePath,
}) {
const base = text(publicBaseUrl).replace(/\/+$/, '');
const owner = encodeURIComponent(text(userId));
const encodedRelative = normalizeRelativePath(relativePath)
.split('/')
.map(encodeURIComponent)
.join('/');
return `${base}/MindSpace/${owner}/${encodedRelative}`;
}
export function auditConversationPackages({
packages = [],
artifacts = [],
assets = [],
publicBaseUrl = 'http://127.0.0.1:5173',
} = {}) {
const issues = [];
const repairs = [];
const artifactsByPackage = new Map();
const assetsById = new Map(
assets.map((asset) => [text(asset?.id), asset]),
);
for (const artifact of artifacts) {
const id = text(artifact?.package_id ?? artifact?.packageId);
if (!id) continue;
const list = artifactsByPackage.get(id) ?? [];
list.push(artifact);
artifactsByPackage.set(id, list);
}
for (const packageRecord of packages) {
const id = packageId(packageRecord);
const userId = packageUserId(packageRecord);
const sessionId = packageSessionId(packageRecord);
const packageArtifacts = artifactsByPackage.get(id) ?? [];
if (packageArtifacts.length === 0) {
issues.push(
issue(
'package_without_artifacts',
`package has no artifacts: ${id}`,
{ packageId: id, userId, sessionId },
),
);
continue;
}
const publicHtmlArtifactNames = new Set(
packageArtifacts
.filter((artifact) => artifactKind(artifact) === 'public_html')
.map((artifact) => artifactDisplayName(artifact))
.filter(Boolean),
);
for (const artifact of packageArtifacts) {
const artifactId = text(artifact?.id);
if (!artifactHasReadableReference(artifact)) {
issues.push(
issue(
'artifact_without_backing_reference',
`artifact has no storageKey or canonicalUrl: ${artifactId}`,
{ packageId: id, artifactId },
),
);
}
const assetId = artifactAssetId(artifact);
const asset = assetId ? assetsById.get(assetId) : null;
if (assetId && !asset) {
issues.push(
issue(
'artifact_asset_missing',
`artifact points to a missing asset: ${assetId}`,
{ packageId: id, artifactId, assetId },
),
);
continue;
}
const relativePath = asset ? assetRelativePath(asset) : '';
if (
artifactKind(artifact) === 'generated_file' &&
isPublicHtmlPath(relativePath)
) {
const displayName = publicHtmlKey(relativePath);
if (!publicHtmlArtifactNames.has(displayName)) {
const repair = {
action: 'insert_public_html_artifact',
packageId: id,
userId,
sessionId,
sourceArtifactId: artifactId,
sourceAssetId: assetId,
relativePath,
artifactId: buildPublicHtmlArtifactId({
userId,
sessionId,
relativePath,
}),
displayName,
mimeType: 'text/html',
sizeBytes:
assetSize(asset) ??
(Number(artifact?.size_bytes ?? 0) || null),
canonicalUrl: canonicalPublicUrl({
publicBaseUrl,
userId,
relativePath,
}),
sortOrder: assetUpdatedAt(
asset,
Number(artifact?.created_at ?? Date.now()),
),
};
issues.push(
issue(
'missing_public_html_artifact',
`generated public HTML is missing public_html artifact: ${relativePath}`,
{
packageId: id,
artifactId,
assetId,
relativePath,
repair,
},
),
);
repairs.push(repair);
}
}
}
}
return {
ok: issues.length === 0,
issues,
repairs,
summary: {
packageCount: packages.length,
artifactCount: artifacts.length,
assetCount: assets.length,
issueCount: issues.length,
repairCount: repairs.length,
},
};
}
export const conversationPackageAuditInternals = {
artifactHasReadableReference,
canonicalPublicUrl,
isPublicHtmlPath,
normalizeRelativePath,
publicHtmlKey,
};