openpetswithchatandmcp/website/scripts/run-ep07-release-readiness-sweep.mjs
OpenPets Dev 65ac83849e Add 'website/' from commit '966aea480ffe1c340553aa878def30131d2af827'
git-subtree-dir: website
git-subtree-mainline: 8d3849caea
git-subtree-split: 966aea480f
2026-06-17 20:48:39 +00:00

595 lines
20 KiB
JavaScript
Executable file

#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const DEFAULT_ITERATIONS = 1;
const DEFAULT_ADVANCED_ITERATIONS = 1;
const DEFAULT_REPORT_BASENAME = 'ep07-release-readiness-last';
const DEFAULT_DOD_BASENAME = 'ep07-dod-checklist-release-last';
const DEFAULT_GIT_CONTEXT_BASENAME = 'ep07-git-context-release-last';
const VALID_STEPS = ['base', 'advanced', 'git_context', 'dod'];
function parsePositiveInt(value, fallback) {
const parsed = Number.parseInt(String(value || ''), 10);
if (Number.isFinite(parsed) && parsed > 0) return parsed;
return fallback;
}
function parseFailOn(value) {
const normalized = String(value || '').trim().toLowerCase();
if (['never', 'warning', 'critical'].includes(normalized)) return normalized;
return 'critical';
}
function parseArgs(argv) {
const args = {
iterations: DEFAULT_ITERATIONS,
advancedIterations: DEFAULT_ADVANCED_ITERATIONS,
reportBasename: DEFAULT_REPORT_BASENAME,
dodBasename: DEFAULT_DOD_BASENAME,
gitContextBasename: DEFAULT_GIT_CONTEXT_BASENAME,
baseUrl: process.env.EP07_RELEASE_BASE_URL || process.env.SCRIPTORIUM_API_URL || 'http://127.0.0.1:3000',
failOn: 'critical',
steps: [...VALID_STEPS],
writeReport: true,
};
for (const token of argv) {
if (token.startsWith('--iterations=')) {
args.iterations = parsePositiveInt(token.slice('--iterations='.length), DEFAULT_ITERATIONS);
continue;
}
if (token.startsWith('--advanced-iterations=')) {
args.advancedIterations = parsePositiveInt(
token.slice('--advanced-iterations='.length),
DEFAULT_ADVANCED_ITERATIONS,
);
continue;
}
if (token.startsWith('--report-basename=')) {
const value = token.slice('--report-basename='.length).trim();
if (value) args.reportBasename = value;
continue;
}
if (token.startsWith('--dod-basename=')) {
const value = token.slice('--dod-basename='.length).trim();
if (value) args.dodBasename = value;
continue;
}
if (token.startsWith('--git-context-basename=')) {
const value = token.slice('--git-context-basename='.length).trim();
if (value) args.gitContextBasename = value;
continue;
}
if (token.startsWith('--base-url=')) {
const value = token.slice('--base-url='.length).trim();
if (value) args.baseUrl = value;
continue;
}
if (token.startsWith('--fail-on=')) {
args.failOn = parseFailOn(token.slice('--fail-on='.length));
continue;
}
if (token.startsWith('--steps=')) {
const stepTokens = token
.slice('--steps='.length)
.split(',')
.map((entry) => entry.trim().toLowerCase())
.filter(Boolean);
if (stepTokens.length === 0) continue;
const invalid = stepTokens.filter((entry) => !VALID_STEPS.includes(entry));
if (invalid.length > 0) {
throw new Error(`Unknown --steps entries: ${invalid.join(', ')}. Allowed: ${VALID_STEPS.join(', ')}`);
}
args.steps = Array.from(new Set(stepTokens));
continue;
}
if (token === '--no-report') {
args.writeReport = false;
}
}
return args;
}
function formatMs(ms) {
return `${(ms / 1000).toFixed(2)}s`;
}
function buildCommandString(command, commandArgs) {
return [command, ...commandArgs].join(' ');
}
function runCommand(label, command, commandArgs, options = {}) {
const startedAt = Date.now();
const startedAtIso = new Date(startedAt).toISOString();
console.log(`\n[EP-07 release sweep] ${label}`);
const child = spawnSync(command, commandArgs, {
cwd: options.cwd,
stdio: 'inherit',
shell: false,
});
const endedAt = Date.now();
const durationMs = endedAt - startedAt;
const exitCode = child.status ?? 1;
const ok = exitCode === 0;
if (ok) {
console.log(`[EP-07 release sweep] ${label} passed in ${formatMs(durationMs)}.`);
} else {
console.error(`[EP-07 release sweep] ${label} failed in ${formatMs(durationMs)} (exit=${exitCode}).`);
}
return {
label,
command,
args: commandArgs,
commandLine: buildCommandString(command, commandArgs),
ok,
exitCode,
startedAtIso,
endedAtIso: new Date(endedAt).toISOString(),
durationMs,
};
}
function readJsonSafe(filePath) {
try {
return JSON.parse(readFileSync(filePath, 'utf8'));
} catch (_) {
return null;
}
}
function severityValue(severity) {
if (severity === 'critical') return 2;
if (severity === 'warning') return 1;
return 0;
}
function computeOverallSeverity(steps) {
let severity = 'none';
for (const step of steps) {
if (!step.ok) {
severity = 'critical';
break;
}
const reportSeverity = String(step.report?.severity || '').trim().toLowerCase();
if (reportSeverity === 'critical') {
severity = 'critical';
break;
}
if (reportSeverity === 'warning' && severity !== 'critical') {
severity = 'warning';
}
}
return severity;
}
function shouldFailReport(severity, failOn) {
if (failOn === 'never') return false;
if (failOn === 'warning') return severityValue(severity) >= severityValue('warning');
return severityValue(severity) >= severityValue('critical');
}
function actionPriorityWeight(priority) {
const normalized = String(priority || '').trim().toUpperCase();
if (/^P\d+$/.test(normalized)) {
return Number.parseInt(normalized.slice(1), 10);
}
if (normalized === 'CRITICAL') return 0;
if (normalized === 'HIGH') return 1;
if (normalized === 'MEDIUM') return 2;
if (normalized === 'LOW') return 3;
return 99;
}
function normalizeActionPriority(priority) {
const normalized = String(priority || '').trim().toUpperCase();
if (normalized.length === 0) return 'P1';
if (/^P\d+$/.test(normalized)) return normalized;
if (normalized === 'CRITICAL') return 'P0';
if (normalized === 'HIGH') return 'P1';
if (normalized === 'MEDIUM') return 'P2';
if (normalized === 'LOW') return 'P3';
return normalized;
}
function collectActionQueue(steps) {
const queue = [];
for (const step of steps) {
const stepQueue = Array.isArray(step.report?.action_queue) ? step.report.action_queue : [];
for (const entry of stepQueue) {
if (!entry || typeof entry !== 'object') continue;
const action = String(entry.action || '').trim();
if (!action) continue;
const checkId = String(entry.check_id || entry.checkId || '').trim() || 'unknown_check';
const priority = normalizeActionPriority(entry.priority);
queue.push({
stepId: step.id,
priority,
checkId,
action,
});
}
}
return queue.sort((a, b) => {
const p = actionPriorityWeight(a.priority) - actionPriorityWeight(b.priority);
if (p !== 0) return p;
if (a.stepId !== b.stepId) return a.stepId.localeCompare(b.stepId);
if (a.checkId !== b.checkId) return a.checkId.localeCompare(b.checkId);
return a.action.localeCompare(b.action);
});
}
function summarizeActionQueue(actionQueue) {
const byPriority = {};
for (const item of actionQueue) {
byPriority[item.priority] = (byPriority[item.priority] || 0) + 1;
}
const orderedPriorities = Object.keys(byPriority).sort((a, b) => actionPriorityWeight(a) - actionPriorityWeight(b));
return {
total: actionQueue.length,
byPriority,
orderedPriorities,
};
}
function normalizeCheckSeverity(severity) {
const normalized = String(severity || '').trim().toLowerCase();
if (normalized === 'critical') return 'critical';
if (normalized === 'warning') return 'warning';
return 'none';
}
function checkSeveritySortWeight(severity) {
const normalized = normalizeCheckSeverity(severity);
if (normalized === 'critical') return 0;
if (normalized === 'warning') return 1;
return 2;
}
function collectGateDecision(steps) {
const stepReports = {};
const blockingChecks = [];
for (const step of steps) {
if (!step.report || typeof step.report !== 'object') continue;
const reportStatus = String(step.report.status || '').trim().toLowerCase() || 'unknown';
const reportSeverity = normalizeCheckSeverity(step.report.severity);
const reportRecommendation = String(step.report.recommendation || '').trim() || null;
const reportApproved = step.report.approved === true;
stepReports[step.id] = {
status: reportStatus,
severity: reportSeverity,
recommendation: reportRecommendation,
approved: reportApproved,
};
const checks = Array.isArray(step.report.checks) ? step.report.checks : [];
for (const check of checks) {
if (!check || check.passed === true) continue;
const checkId = String(check.id || check.check_id || '').trim() || 'unknown_check';
blockingChecks.push({
stepId: step.id,
checkId,
severity: normalizeCheckSeverity(check.severity),
details: String(check.details || '').trim() || '',
remediation: String(check.remediation || '').trim() || '',
});
}
}
blockingChecks.sort((a, b) => {
const severityDelta = checkSeveritySortWeight(a.severity) - checkSeveritySortWeight(b.severity);
if (severityDelta !== 0) return severityDelta;
if (a.stepId !== b.stepId) return a.stepId.localeCompare(b.stepId);
return a.checkId.localeCompare(b.checkId);
});
const entries = Object.values(stepReports);
const hasCriticalBlocking = blockingChecks.some((entry) => entry.severity === 'critical');
const hasWarningBlocking = blockingChecks.some((entry) => entry.severity === 'warning');
const hasHoldStep = entries.some((entry) => entry.status === 'hold' || entry.status === 'failed');
const hasWarningStep = entries.some((entry) => entry.status === 'warning');
const allApproved = entries.length > 0 && entries.every((entry) => entry.approved === true);
const overallStatus = hasCriticalBlocking || hasHoldStep
? 'hold'
: (hasWarningBlocking || hasWarningStep ? 'warning' : (allApproved ? 'approved' : 'unknown'));
const overallSeverity = hasCriticalBlocking
? 'critical'
: (hasWarningBlocking || entries.some((entry) => entry.severity === 'warning') ? 'warning' : 'none');
const overallRecommendation = overallStatus === 'hold'
? 'gate_f_hold'
: (overallStatus === 'warning' ? 'gate_f_warning' : (overallStatus === 'approved' ? 'gate_f_ready' : 'gate_f_unknown'));
const overallApproved = overallStatus === 'approved';
return {
overallStatus,
overallSeverity,
overallRecommendation,
overallApproved,
stepCount: entries.length,
stepReports,
blockingChecks,
};
}
function renderMarkdown(report) {
const lines = [];
lines.push('# EP-07 Release Readiness Sweep');
lines.push('');
lines.push(`- Generated at: ${report.generatedAtIso}`);
lines.push(`- Status: ${report.status}`);
lines.push(`- Severity: ${report.severity}`);
lines.push(`- Fail policy: ${report.failOn}`);
lines.push(`- Total duration: ${formatMs(report.totalDurationMs)}`);
lines.push('');
lines.push('## Steps');
lines.push('');
lines.push('| Step | Status | Duration | Command |');
lines.push('| --- | --- | --- | --- |');
for (const step of report.steps) {
lines.push(`| ${step.id} | ${step.ok ? 'passed' : `failed (${step.exitCode})`} | ${formatMs(step.durationMs)} | \`${step.commandLine}\` |`);
}
lines.push('');
if (report.steps.some((step) => step.id === 'dod' && step.report)) {
const dodStep = report.steps.find((step) => step.id === 'dod');
const dodReport = dodStep?.report || {};
lines.push('## DoD Summary');
lines.push('');
lines.push(`- status: ${dodReport.status ?? 'n/a'}`);
lines.push(`- severity: ${dodReport.severity ?? 'n/a'}`);
lines.push(`- approved: ${dodReport.approved ?? 'n/a'}`);
lines.push(`- recommendation: ${dodReport.recommendation ?? 'n/a'}`);
lines.push('');
}
if (report.gateDecision && typeof report.gateDecision === 'object') {
lines.push('## Gate Decision');
lines.push('');
lines.push(`- status: ${report.gateDecision.overallStatus ?? 'unknown'}`);
lines.push(`- severity: ${report.gateDecision.overallSeverity ?? 'none'}`);
lines.push(`- recommendation: ${report.gateDecision.overallRecommendation ?? 'gate_f_unknown'}`);
lines.push(`- approved: ${report.gateDecision.overallApproved ?? false}`);
lines.push(`- step_count: ${report.gateDecision.stepCount ?? 0}`);
lines.push('');
const stepReports = report.gateDecision.stepReports && typeof report.gateDecision.stepReports === 'object'
? report.gateDecision.stepReports
: {};
const stepEntries = Object.entries(stepReports);
if (stepEntries.length > 0) {
lines.push('### Gate Step Reports');
lines.push('');
lines.push('| Step | Status | Severity | Recommendation | Approved |');
lines.push('| --- | --- | --- | --- | --- |');
for (const [stepId, stepReport] of stepEntries) {
lines.push(`| ${stepId} | ${stepReport.status ?? 'unknown'} | ${stepReport.severity ?? 'none'} | ${stepReport.recommendation ?? 'n/a'} | ${stepReport.approved ?? false} |`);
}
lines.push('');
}
const blockingChecks = Array.isArray(report.gateDecision.blockingChecks) ? report.gateDecision.blockingChecks : [];
if (blockingChecks.length > 0) {
lines.push('### Blocking Checks');
lines.push('');
lines.push('| Severity | Step | Check | Details | Remediation |');
lines.push('| --- | --- | --- | --- | --- |');
for (const check of blockingChecks) {
lines.push(`| ${check.severity ?? 'none'} | ${check.stepId ?? 'unknown'} | ${check.checkId ?? 'unknown_check'} | ${check.details || 'n/a'} | ${check.remediation || 'n/a'} |`);
}
lines.push('');
}
}
if (Array.isArray(report.actionQueue) && report.actionQueue.length > 0) {
lines.push('## Action Queue');
lines.push('');
lines.push(`- total: ${report.actionQueueSummary?.total ?? report.actionQueue.length}`);
if (Array.isArray(report.actionQueueSummary?.orderedPriorities) && report.actionQueueSummary.orderedPriorities.length > 0) {
const prioritySummary = report.actionQueueSummary.orderedPriorities
.map((priority) => `${priority}:${report.actionQueueSummary.byPriority?.[priority] ?? 0}`)
.join(', ');
lines.push(`- by_priority: ${prioritySummary}`);
}
lines.push('');
lines.push('| Priority | Step | Check | Action |');
lines.push('| --- | --- | --- | --- |');
for (const item of report.actionQueue) {
lines.push(`| ${item.priority} | ${item.stepId} | ${item.checkId} | ${item.action} |`);
}
lines.push('');
}
if (report.failure) {
lines.push('## Failure');
lines.push('');
lines.push(`- step: ${report.failure.stepId}`);
lines.push(`- exit_code: ${report.failure.exitCode}`);
lines.push('');
}
return `${lines.join('\n')}\n`;
}
function writeEvidenceReport(repoRoot, basename, report) {
const evidenceDir = path.join(repoRoot, 'docs', 'evidence', 'cross-corpus');
mkdirSync(evidenceDir, { recursive: true });
const jsonPath = path.join(evidenceDir, `${basename}.json`);
const mdPath = path.join(evidenceDir, `${basename}.md`);
writeFileSync(jsonPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
writeFileSync(mdPath, renderMarkdown(report), 'utf8');
console.log(`[EP-07 release sweep] evidence report written: ${jsonPath}`);
console.log(`[EP-07 release sweep] evidence report written: ${mdPath}`);
}
function main() {
const args = parseArgs(process.argv.slice(2));
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const uiRoot = path.resolve(scriptDir, '..');
const repoRoot = path.resolve(uiRoot, '..');
const serverRoot = path.join(repoRoot, 'ScriptoriumAI-Server');
const startedAt = Date.now();
const baseSweepBasename = `${args.reportBasename}-base`;
const advancedSweepBasename = `${args.reportBasename}-advanced`;
const gitContextJsonPath = path.join(
repoRoot,
'docs',
'evidence',
'cross-corpus',
`${args.gitContextBasename}.json`,
);
const gitContextMdPath = path.join(
repoRoot,
'docs',
'evidence',
'cross-corpus',
`${args.gitContextBasename}.md`,
);
const dodJsonPath = path.join(repoRoot, 'docs', 'evidence', 'cross-corpus', `${args.dodBasename}.json`);
const dodMdPath = path.join(repoRoot, 'docs', 'evidence', 'cross-corpus', `${args.dodBasename}.md`);
const stepRecords = [];
let failure = null;
if (args.steps.includes('base')) {
const record = runCommand(
'base EP-07 validation sweep',
process.execPath,
[
path.join(scriptDir, 'run-ep07-validation-sweep.mjs'),
`--iterations=${args.iterations}`,
`--report-basename=${baseSweepBasename}`,
],
{ cwd: uiRoot },
);
stepRecords.push({ id: 'base', ...record });
if (!record.ok) failure = { stepId: 'base', exitCode: record.exitCode };
}
if (!failure && args.steps.includes('advanced')) {
const record = runCommand(
'advanced EP-07 validation sweep',
process.execPath,
[
path.join(scriptDir, 'run-ep07-advanced-validation-sweep.mjs'),
`--iterations=${args.advancedIterations}`,
`--report-basename=${advancedSweepBasename}`,
],
{ cwd: uiRoot },
);
stepRecords.push({ id: 'advanced', ...record });
if (!record.ok) failure = { stepId: 'advanced', exitCode: record.exitCode };
}
if (!failure && args.steps.includes('git_context')) {
const record = runCommand(
'EP-07 git-context preflight',
process.execPath,
[
path.join(serverRoot, 'scripts', 'corpus-engine-git-context.cjs'),
'--base-url',
args.baseUrl,
'--label',
'gate_f_engine_git_context_release',
'--out',
gitContextJsonPath,
'--out-md',
gitContextMdPath,
'--fail-on',
'never',
],
{ cwd: serverRoot },
);
const gitContextReport = readJsonSafe(gitContextJsonPath);
stepRecords.push({ id: 'git_context', ...record, report: gitContextReport });
if (!record.ok) failure = { stepId: 'git_context', exitCode: record.exitCode };
}
if (!failure && args.steps.includes('dod')) {
const record = runCommand(
'EP-07 Gate F DoD checklist',
process.execPath,
[
path.join(serverRoot, 'scripts', 'corpus-engine-dod-checklist.cjs'),
'--base-url',
args.baseUrl,
'--label',
'gate_f_engine_dod_release',
'--out',
dodJsonPath,
'--out-md',
dodMdPath,
'--fail-on',
'never',
],
{ cwd: serverRoot },
);
const dodReport = readJsonSafe(dodJsonPath);
stepRecords.push({ id: 'dod', ...record, report: dodReport });
if (!record.ok) failure = { stepId: 'dod', exitCode: record.exitCode };
}
const totalDurationMs = Date.now() - startedAt;
const severity = computeOverallSeverity(stepRecords);
const actionQueue = collectActionQueue(stepRecords);
const actionQueueSummary = summarizeActionQueue(actionQueue);
const gateDecision = collectGateDecision(stepRecords);
const status = failure || severity === 'critical'
? 'failed'
: severity === 'warning'
? 'warning'
: 'passed';
const report = {
generatedAtIso: new Date().toISOString(),
status,
severity,
failOn: args.failOn,
target: {
baseUrl: args.baseUrl,
steps: args.steps,
iterations: args.iterations,
advancedIterations: args.advancedIterations,
},
totalDurationMs,
steps: stepRecords,
gateDecision,
actionQueue,
actionQueueSummary,
failure,
};
if (args.writeReport) {
writeEvidenceReport(repoRoot, args.reportBasename, report);
}
if (shouldFailReport(severity, args.failOn) || failure) {
process.exit(failure?.exitCode || 1);
}
}
const isDirectExecution = (() => {
if (!process.argv[1]) return false;
try {
return import.meta.url === pathToFileURL(process.argv[1]).href;
} catch (_) {
return false;
}
})();
if (isDirectExecution) {
main();
}
export {
parseArgs,
computeOverallSeverity,
shouldFailReport,
collectActionQueue,
summarizeActionQueue,
collectGateDecision,
renderMarkdown,
};