595 lines
20 KiB
JavaScript
Executable file
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,
|
|
};
|