fix: harden release gate and page delivery
Memind CI / Test, build, and release guards (push) Failing after 12m3s

This commit is contained in:
john
2026-07-26 14:32:01 +08:00
parent d3141a3e91
commit b1577a16e9
61 changed files with 6017 additions and 89 deletions
+157
View File
@@ -0,0 +1,157 @@
import fs from 'node:fs/promises';
import path from 'node:path';
export const SCENARIO_GROUP_COUNTS = Object.freeze({
REL: 11,
AUTH: 8,
CHAT: 15,
MEM: 16,
FILE: 8,
IMGPG: 8,
XLS: 12,
PAGE: 13,
DATA: 13,
SEARCH: 8,
AGENT: 11,
SCHED: 5,
WX: 13,
BILL: 7,
MS: 9,
PLAZA: 5,
CFG: 8,
UI: 8,
COMP: 9,
});
export const SCENARIO_MODE_BY_GROUP = Object.freeze({
REL: 'upgrade',
AUTH: 'scenarios',
CHAT: 'scenarios',
MEM: 'scenarios',
FILE: 'scenarios',
IMGPG: 'scenarios',
XLS: 'scenarios',
PAGE: 'scenarios',
DATA: 'scenarios',
SEARCH: 'providers',
AGENT: 'scenarios',
SCHED: 'scenarios',
WX: 'scenarios',
BILL: 'scenarios',
MS: 'scenarios',
PLAZA: 'scenarios',
CFG: 'deterministic',
UI: 'browser',
COMP: 'upgrade',
});
const ALWAYS_REQUIRED = new Set([
...range('REL', 1, 11),
'AUTH-01',
'AUTH-04',
'AUTH-05',
'AUTH-06',
'CHAT-01',
'CHAT-02',
'CHAT-06',
'CHAT-07',
'CHAT-08',
'CHAT-09',
'CHAT-11',
'CHAT-12',
'CHAT-13',
'CHAT-15',
'MEM-08',
'MEM-13',
'MEM-14',
'MEM-15',
'MEM-16',
'FILE-07',
'PAGE-08',
'PAGE-09',
'PAGE-10',
'PAGE-12',
'DATA-06',
'DATA-10',
'DATA-12',
'AGENT-10',
'AGENT-11',
...range('WX', 9, 13),
'BILL-02',
'BILL-03',
'BILL-04',
'MS-01',
'MS-03',
'MS-04',
'CFG-08',
...range('COMP', 1, 4),
'COMP-08',
'COMP-09',
]);
function range(prefix, start, end) {
return Array.from(
{ length: end - start + 1 },
(_, index) => `${prefix}-${String(start + index).padStart(2, '0')}`,
);
}
export function expectedScenarioIds() {
return Object.entries(SCENARIO_GROUP_COUNTS).flatMap(
([prefix, count]) => range(prefix, 1, count),
);
}
export function isScenarioExemptable(id) {
return !ALWAYS_REQUIRED.has(id);
}
export function validateScenarioCatalog(catalog) {
const expected = expectedScenarioIds();
const expectedSet = new Set(expected);
const ids = catalog.map((scenario) => scenario.id);
const unique = new Set(ids);
const missing = expected.filter((id) => !unique.has(id));
const unexpected = ids.filter((id) => !expectedSet.has(id));
const duplicates = ids.filter((id, index) => ids.indexOf(id) !== index);
if (catalog.length !== expected.length || unique.size !== expected.length) {
throw new Error(
`Release gate catalog must contain exactly ${expected.length} unique scenarios `
+ `(found ${catalog.length}/${unique.size})`,
);
}
if (missing.length || unexpected.length || duplicates.length) {
throw new Error(
`Release gate catalog mismatch: missing=${missing.join(',') || '-'} `
+ `unexpected=${unexpected.join(',') || '-'} duplicates=${[...new Set(duplicates)].join(',') || '-'}`,
);
}
for (const scenario of catalog) {
if (!scenario.name || !scenario.group || !scenario.mode) {
throw new Error(`Scenario ${scenario.id} is missing name, group, or mode`);
}
}
return catalog;
}
export async function loadScenarioCatalog({
root = path.resolve(new URL('..', import.meta.url).pathname),
guardianPath = path.join(root, 'docs', 'production-release-guardian.md'),
} = {}) {
const source = await fs.readFile(guardianPath, 'utf8');
const catalog = [];
const scenarioPattern = /^- \*\*([A-Z]+-\d{2})\*\*(.+)$/gm;
for (const match of source.matchAll(scenarioPattern)) {
const id = match[1];
const group = id.split('-')[0];
catalog.push({
id,
name: match[2].trim(),
group,
mode: SCENARIO_MODE_BY_GROUP[group],
exemptable: isScenarioExemptable(id),
});
}
return validateScenarioCatalog(catalog);
}