193 lines
6.2 KiB
JavaScript
193 lines
6.2 KiB
JavaScript
export const CORE_SCENARIO_IDS = Object.freeze([
|
|
'REL-01',
|
|
'REL-02',
|
|
'REL-04',
|
|
'REL-05',
|
|
'REL-06',
|
|
'REL-09',
|
|
'REL-11',
|
|
'AUTH-01',
|
|
'AUTH-05',
|
|
'CHAT-01',
|
|
'CHAT-06',
|
|
'CHAT-07',
|
|
'AGENT-01',
|
|
'AGENT-03',
|
|
'MS-01',
|
|
'COMP-09',
|
|
]);
|
|
|
|
const CRITICAL_IMPACT_RULES = Object.freeze([
|
|
{
|
|
groups: ['AGENT', 'CFG', 'UI'],
|
|
pattern: /^(?:(?:package|npm-shrinkwrap).*\.json|pnpm-lock\.yaml|yarn\.lock)$/i,
|
|
},
|
|
{
|
|
groups: ['AGENT', 'AUTH', 'CFG', 'CHAT', 'DATA'],
|
|
pattern: /^(?:server\.mjs|schema\.sql|(?:migrations?|database)\/)/i,
|
|
},
|
|
{
|
|
groups: ['CFG', 'REL'],
|
|
pattern: /^(?:release-gate\/|scripts\/(?:build-portal-runtime|check-release-ready|release-|run-release-gate|verify-release-gate|verify-canary-))/i,
|
|
},
|
|
{
|
|
groups: ['AGENT', 'CFG', 'REL'],
|
|
pattern: /^scripts\/(?:run-memind-portal-prod|run-memind-portal-candidate|goosed.*compose)/i,
|
|
},
|
|
{
|
|
groups: ['CFG', 'REL'],
|
|
pattern: /^(?:docs\/(?:production-release-guardian|release-gate-automation|release-canary-103)\.md|(?:PRODUCTION_RELEASE_RULES|ENGINEERING_WORKFLOW_RULES)\.md|\.github\/workflows\/)/i,
|
|
},
|
|
{
|
|
groups: ['AGENT', 'AUTH', 'CFG', 'CHAT', 'WX'],
|
|
pattern: /^server\/portal-/i,
|
|
},
|
|
{
|
|
groups: ['AUTH', 'CFG', 'UI'],
|
|
pattern: /^src\/(?:api\/core|config)\.[cm]?[jt]sx?$/i,
|
|
},
|
|
{
|
|
groups: ['AGENT', 'CFG', 'CHAT'],
|
|
pattern: /^(?:tkmind-proxy|session-stream|session-stream-store|deepseek-no-think-proxy)\.mjs$/i,
|
|
},
|
|
]);
|
|
|
|
const NON_RUNTIME_PATHS = Object.freeze([
|
|
/^(?:AGENTS|README|CHANGELOG)\.md$/i,
|
|
/^\.runtime\//i,
|
|
/^docs\//i,
|
|
/^\.cursor\//i,
|
|
/^\.codex\//i,
|
|
/^scripts\/dev(?:-|\.|\/)/i,
|
|
/^scripts\/.*(?:test|fixture|mock).*\.[cm]?[jt]s$/i,
|
|
/\.(?:test|spec)\.[cm]?[jt]sx?$/i,
|
|
/^(?:eslint|prettier|tsconfig|components)\..*$/i,
|
|
/^openapi\.json$/i,
|
|
]);
|
|
|
|
const IMPACT_RULES = Object.freeze([
|
|
{ groups: ['MEM'], pattern: /(?:^|\/)(?:memory|episodic)[^/]*|memory-v2/i },
|
|
{ groups: ['DATA'], pattern: /(?:page-data|dataset|page-policy)/i },
|
|
{ groups: ['WX'], pattern: /(?:wechat|weixin|wx-)/i },
|
|
{ groups: ['BILL'], pattern: /(?:billing|payment|charge|balance|subscription)/i },
|
|
{ groups: ['PLAZA'], pattern: /(?:^|\/)plaza/i },
|
|
{ groups: ['SCHED'], pattern: /(?:schedule|scheduler|reminder|cron)/i },
|
|
{ groups: ['SEARCH'], pattern: /(?:search|weather|market|news-provider)/i },
|
|
{ groups: ['XLS'], pattern: /(?:excel|xlsx|spreadsheet)/i },
|
|
{ groups: ['IMGPG'], pattern: /(?:image|thumbnail|cover|imgproxy)/i },
|
|
{ groups: ['FILE'], pattern: /(?:file|attachment|upload|document|pdf|docx|csv)/i },
|
|
{ groups: ['MS'], pattern: /mindspace/i },
|
|
{ groups: ['PAGE'], pattern: /(?:public-(?:page|finish)|published-page|publication|page-delivery|mindspace-public)/i },
|
|
{ groups: ['AGENT'], pattern: /(?:agent|capabilit|goosed|worker|aider|mcp)/i },
|
|
{ groups: ['CHAT'], pattern: /(?:chat|conversation|message|reply|session|sse|routing|intent)/i },
|
|
{ groups: ['AUTH'], pattern: /(?:auth|access-policy|account|user-permission)/i },
|
|
{ groups: ['CFG'], pattern: /(?:config|provider|model-catalog|orchestrator|analytics|disclosure)/i },
|
|
{ groups: ['CFG'], pattern: /(?:^|\/)\.env(?:\.example)?$/i },
|
|
{ groups: ['UI'], pattern: /^(?:src\/|public\/)|\.(?:css|scss|tsx|vue)$/i },
|
|
]);
|
|
|
|
const GROUP_DEPENDENCIES = Object.freeze({
|
|
MEM: ['CHAT'],
|
|
DATA: ['AUTH', 'PAGE', 'MS'],
|
|
WX: ['AUTH', 'CHAT'],
|
|
BILL: ['AUTH', 'CHAT'],
|
|
PLAZA: ['AUTH'],
|
|
SEARCH: ['CHAT'],
|
|
XLS: ['FILE'],
|
|
IMGPG: ['FILE', 'PAGE'],
|
|
FILE: ['AUTH', 'CHAT'],
|
|
MS: ['PAGE'],
|
|
PAGE: ['FILE', 'MS'],
|
|
AGENT: ['CHAT', 'CFG'],
|
|
});
|
|
|
|
function normalizePaths(paths) {
|
|
return [...new Set(paths.map((item) => String(item).replaceAll('\\', '/')).filter(Boolean))].sort();
|
|
}
|
|
|
|
function matchesAny(patterns, relativePath) {
|
|
return patterns.some((pattern) => pattern.test(relativePath));
|
|
}
|
|
|
|
function closeGroupDependencies(initialGroups) {
|
|
const groups = new Set(initialGroups);
|
|
const pending = [...groups];
|
|
while (pending.length > 0) {
|
|
const group = pending.shift();
|
|
for (const dependency of GROUP_DEPENDENCIES[group] ?? []) {
|
|
if (groups.has(dependency)) continue;
|
|
groups.add(dependency);
|
|
pending.push(dependency);
|
|
}
|
|
}
|
|
return [...groups].sort();
|
|
}
|
|
|
|
export function selectImpactScenarios({
|
|
catalog,
|
|
changedPaths,
|
|
blockingReasons = [],
|
|
}) {
|
|
const normalizedPaths = normalizePaths(changedPaths);
|
|
const catalogIds = new Set(catalog.map((scenario) => scenario.id));
|
|
const missingCore = CORE_SCENARIO_IDS.filter((id) => !catalogIds.has(id));
|
|
if (missingCore.length > 0) {
|
|
throw new Error(`Core release scenarios are missing from the catalog: ${missingCore.join(',')}`);
|
|
}
|
|
|
|
if (blockingReasons.length > 0) {
|
|
throw new Error(`Impact selection is blocked: ${blockingReasons.join(',')}`);
|
|
}
|
|
const directGroups = new Set();
|
|
const unmappedPaths = [];
|
|
|
|
for (const relativePath of normalizedPaths) {
|
|
let matched = false;
|
|
for (const rule of CRITICAL_IMPACT_RULES) {
|
|
if (!rule.pattern.test(relativePath)) continue;
|
|
matched = true;
|
|
for (const group of rule.groups) directGroups.add(group);
|
|
}
|
|
if (matchesAny(NON_RUNTIME_PATHS, relativePath)) continue;
|
|
|
|
for (const rule of IMPACT_RULES) {
|
|
if (!rule.pattern.test(relativePath)) continue;
|
|
matched = true;
|
|
for (const group of rule.groups) directGroups.add(group);
|
|
}
|
|
if (matched) continue;
|
|
|
|
unmappedPaths.push(relativePath);
|
|
}
|
|
|
|
if (unmappedPaths.length > 0) {
|
|
throw new Error(
|
|
`Impact selection has unmapped runtime paths: ${unmappedPaths.join(',')}`,
|
|
);
|
|
}
|
|
const impactGroups = closeGroupDependencies(directGroups);
|
|
const strategy = impactGroups.length > 0 ? 'impact' : 'core';
|
|
const selected = new Set(CORE_SCENARIO_IDS);
|
|
|
|
for (const scenario of catalog) {
|
|
if (impactGroups.includes(scenario.group)) selected.add(scenario.id);
|
|
}
|
|
|
|
const selectedIds = catalog
|
|
.map((scenario) => scenario.id)
|
|
.filter((id) => selected.has(id));
|
|
|
|
return {
|
|
policy_version: 2,
|
|
strategy,
|
|
catalog_total: catalog.length,
|
|
core_ids: [...CORE_SCENARIO_IDS],
|
|
impact_groups: impactGroups,
|
|
changed_paths: normalizedPaths,
|
|
unmapped_paths: unmappedPaths,
|
|
full_gate_reasons: [],
|
|
selected_ids: selectedIds,
|
|
selected_total: selectedIds.length,
|
|
};
|
|
}
|