Files
memind/memory-v2-backend-contract.mjs
2026-07-03 09:46:03 +08:00

125 lines
3.6 KiB
JavaScript

const ALLOWED_OPERATIONS = ['resolve', 'write', 'compact'];
const ALLOWED_PLUGIN_CATEGORIES = new Set([
'semantic',
'extraction',
'lifecycle',
'behavior',
'policy',
]);
function check(name, ok, message = null, details = {}) {
return { name, ok: Boolean(ok), ...(message ? { message } : {}), ...details };
}
function isValidName(value) {
return /^[a-z][a-z0-9-]*$/.test(String(value ?? ''));
}
function supports(backend, operation) {
return typeof backend?.[operation] === 'function';
}
function safeAvailable(backend) {
try {
return backend?.isAvailable?.() !== false;
} catch {
return false;
}
}
export function inspectMemoryV2Backend(backend) {
const name = String(backend?.name ?? '').trim();
const available = safeAvailable(backend);
const support = Object.fromEntries(
ALLOWED_OPERATIONS.map((operation) => [operation, supports(backend, operation)]),
);
const checks = [
check('name_present', Boolean(name), 'Backend name is required'),
check('name_format', isValidName(name), 'Backend name must be lowercase kebab-case', {
backendName: name,
}),
check(
'has_operation',
Object.values(support).some(Boolean),
'Backend must implement at least one Memory V2 operation',
{ supports: support },
),
];
if (!available) {
const reason = typeof backend?.getUnavailableReason === 'function'
? backend.getUnavailableReason()
: backend?.unavailableReason;
checks.push(check(
'unavailable_reason',
Boolean(reason),
'Unavailable backend must expose a reason',
{ reason: reason ?? null },
));
}
if (backend?.category != null) {
checks.push(check(
'category_known',
ALLOWED_PLUGIN_CATEGORIES.has(String(backend.category)),
'Plugin backend category is unknown',
{ category: String(backend.category) },
));
}
if (backend?.flag != null) {
checks.push(check(
'flag_format',
/^[A-Z0-9_]+$/.test(String(backend.flag)),
'Backend feature flag must be SCREAMING_SNAKE_CASE',
{ flag: String(backend.flag) },
));
}
return {
ok: checks.every((item) => item.ok),
name,
available,
supports: support,
category: backend?.category ?? null,
role: backend?.role ?? null,
flag: backend?.flag ?? null,
checks,
};
}
export function validateMemoryV2BackendSet(backends = []) {
const inspected = backends.map((backend) => inspectMemoryV2Backend(backend));
const names = inspected.map((item) => item.name).filter(Boolean);
const duplicateNames = names.filter((name, index) => names.indexOf(name) !== index);
const checks = [
check('backend_list_present', Array.isArray(backends), 'Backends must be an array'),
check('backend_list_non_empty', inspected.length > 0, 'At least one backend is required'),
check('backend_names_unique', duplicateNames.length === 0, 'Backend names must be unique', {
duplicateNames: [...new Set(duplicateNames)],
}),
check(
'legacy_backend_present',
names.includes('legacy-conversation-memory'),
'Legacy conversation memory backend must remain registered',
),
];
return {
ok: checks.every((item) => item.ok) && inspected.every((item) => item.ok),
checkedAt: new Date().toISOString(),
summary: {
backendCount: inspected.length,
availableBackends: inspected.filter((item) => item.available).map((item) => item.name),
operationSupport: Object.fromEntries(
ALLOWED_OPERATIONS.map((operation) => [
operation,
inspected.filter((item) => item.supports[operation]).map((item) => item.name),
]),
),
},
checks,
backends: inspected,
};
}