Files
memind/release-gate/regression-corpus.mjs
john b1577a16e9
Memind CI / Test, build, and release guards (push) Failing after 12m3s
fix: harden release gate and page delivery
2026-07-26 14:32:01 +08:00

155 lines
4.9 KiB
JavaScript

import fs from 'node:fs/promises';
import path from 'node:path';
const FIXTURE_ID_PATTERN = /^PRC-[A-Z]+-\d{3}$/;
const SHA256_PATTERN = /^[0-9a-f]{64}$/i;
const FORBIDDEN_KEYS = new Set([
'raw_chat',
'raw_message',
'raw_memory',
'attachment_content',
'email',
'phone',
'openid',
'cookie',
'token',
'secret',
'user_id',
'workspace_path',
'production_url',
]);
function collectForbiddenKeys(value, prefix = '') {
if (!value || typeof value !== 'object') return [];
const found = [];
for (const [key, child] of Object.entries(value)) {
const field = prefix ? `${prefix}.${key}` : key;
if (FORBIDDEN_KEYS.has(key.toLowerCase())) found.push(field);
found.push(...collectForbiddenKeys(child, field));
}
return found;
}
export function validateRegressionFixture(fixture, {
catalogIds,
} = {}) {
const errors = [];
if (!FIXTURE_ID_PATTERN.test(fixture?.id ?? '')) errors.push('id must match PRC-<GROUP>-NNN');
if (!Array.isArray(fixture?.scenario_ids) || fixture.scenario_ids.length === 0) {
errors.push('scenario_ids must be a non-empty array');
} else if (catalogIds) {
for (const scenarioId of fixture.scenario_ids) {
if (!catalogIds.has(scenarioId)) errors.push(`unknown scenario_id ${scenarioId}`);
}
}
for (const field of ['failure_signature', 'synthetic_intent', 'source_batch_id']) {
if (!String(fixture?.[field] ?? '').trim()) errors.push(`${field} is required`);
}
if (!SHA256_PATTERN.test(fixture?.evidence_digest ?? '')) {
errors.push('evidence_digest must be an irreversible SHA256');
}
if (fixture?.contains_personal_data !== false) {
errors.push('contains_personal_data must be false');
}
if (!fixture?.privacy_review
|| !String(fixture.privacy_review.reviewed_by ?? '').trim()
|| Number.isNaN(Date.parse(fixture.privacy_review.reviewed_at ?? ''))) {
errors.push('privacy_review with reviewer and timestamp is required');
}
if (!fixture?.automation
|| !Array.isArray(fixture.automation.command)
|| fixture.automation.command.length < 2) {
errors.push('automation.command must be executable');
}
const forbidden = collectForbiddenKeys(fixture);
if (forbidden.length) errors.push(`forbidden personal-data fields: ${forbidden.join(',')}`);
return { valid: errors.length === 0, errors };
}
export async function loadActiveRegressionCorpus({
root,
catalog,
} = {}) {
const corpusRoot = path.join(root, 'scenarios', 'production-regressions');
const manifestPath = path.join(corpusRoot, 'manifests', 'active.json');
let manifest;
try {
manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
} catch (error) {
if (error.code === 'ENOENT') {
return {
status: 'missing',
manifestPath,
fixtures: [],
errors: ['active production regression manifest is missing'],
};
}
return {
status: 'invalid',
manifestPath,
fixtures: [],
errors: [`active production regression manifest is invalid: ${error.message}`],
};
}
const fixtureIds = manifest?.fixture_ids;
if (!Array.isArray(fixtureIds) || fixtureIds.length === 0) {
return {
status: 'invalid',
manifestPath,
fixtures: [],
errors: ['active manifest must contain at least one fixture_id'],
};
}
if (new Set(fixtureIds).size !== fixtureIds.length) {
return {
status: 'invalid',
manifestPath,
fixtures: [],
errors: ['active manifest contains duplicate fixture_ids'],
};
}
const fixtures = [];
const errors = [];
const catalogIds = new Set(catalog.map((scenario) => scenario.id));
for (const fixtureId of fixtureIds) {
if (!FIXTURE_ID_PATTERN.test(fixtureId)) {
errors.push(`invalid fixture id ${fixtureId}`);
continue;
}
const matches = [];
for (const bucket of ['agent-run', 'page-data', 'memory', 'wechat', 'files']) {
const candidate = path.join(corpusRoot, bucket, `${fixtureId}.json`);
try {
await fs.access(candidate);
matches.push(candidate);
} catch {
// The fixture belongs to exactly one approved regression bucket.
}
}
if (matches.length !== 1) {
errors.push(`${fixtureId} must exist in exactly one regression bucket`);
continue;
}
try {
const fixture = JSON.parse(await fs.readFile(matches[0], 'utf8'));
const validation = validateRegressionFixture(fixture, { catalogIds });
if (fixture.id !== fixtureId) validation.errors.push(`fixture id does not match ${fixtureId}`);
if (validation.errors.length) {
errors.push(`${fixtureId}: ${validation.errors.join('; ')}`);
} else {
fixtures.push({ ...fixture, file: path.relative(root, matches[0]) });
}
} catch (error) {
errors.push(`${fixtureId}: invalid JSON (${error.message})`);
}
}
return {
status: errors.length ? 'invalid' : 'ready',
manifestPath,
fixtures,
errors,
};
}