mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-18 21:31:15 +08:00
Add repeatable operator readiness dashboard
This commit is contained in:
parent
bfffc33869
commit
50f375bc2c
@ -76,6 +76,7 @@
|
|||||||
"scripts/harness-adapter-compliance.js",
|
"scripts/harness-adapter-compliance.js",
|
||||||
"scripts/harness-audit.js",
|
"scripts/harness-audit.js",
|
||||||
"scripts/observability-readiness.js",
|
"scripts/observability-readiness.js",
|
||||||
|
"scripts/operator-readiness-dashboard.js",
|
||||||
"scripts/platform-audit.js",
|
"scripts/platform-audit.js",
|
||||||
"scripts/hooks/",
|
"scripts/hooks/",
|
||||||
"scripts/install-apply.js",
|
"scripts/install-apply.js",
|
||||||
@ -296,6 +297,7 @@
|
|||||||
"harness:adapters": "node scripts/harness-adapter-compliance.js",
|
"harness:adapters": "node scripts/harness-adapter-compliance.js",
|
||||||
"harness:audit": "node scripts/harness-audit.js",
|
"harness:audit": "node scripts/harness-audit.js",
|
||||||
"observability:ready": "node scripts/observability-readiness.js",
|
"observability:ready": "node scripts/observability-readiness.js",
|
||||||
|
"operator:dashboard": "node scripts/operator-readiness-dashboard.js",
|
||||||
"platform:audit": "node scripts/platform-audit.js",
|
"platform:audit": "node scripts/platform-audit.js",
|
||||||
"discussion:audit": "node scripts/discussion-audit.js",
|
"discussion:audit": "node scripts/discussion-audit.js",
|
||||||
"security:ioc-scan": "node scripts/ci/scan-supply-chain-iocs.js",
|
"security:ioc-scan": "node scripts/ci/scan-supply-chain-iocs.js",
|
||||||
|
|||||||
655
scripts/operator-readiness-dashboard.js
Normal file
655
scripts/operator-readiness-dashboard.js
Normal file
@ -0,0 +1,655 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { spawnSync } = require('child_process');
|
||||||
|
const { buildReport: buildPlatformReport } = require('./platform-audit');
|
||||||
|
|
||||||
|
const SCHEMA_VERSION = 'ecc.operator-readiness-dashboard.v1';
|
||||||
|
const DEFAULT_THRESHOLDS = Object.freeze({
|
||||||
|
maxOpenPrs: 20,
|
||||||
|
maxOpenIssues: 20,
|
||||||
|
maxDirtyFiles: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
function usage() {
|
||||||
|
console.log([
|
||||||
|
'Usage: node scripts/operator-readiness-dashboard.js [options]',
|
||||||
|
'',
|
||||||
|
'Generate the ECC operator readiness dashboard and prompt-to-artifact audit.',
|
||||||
|
'',
|
||||||
|
'Options:',
|
||||||
|
' --format <text|json|markdown>',
|
||||||
|
' Output format (default: markdown)',
|
||||||
|
' --json Alias for --format json',
|
||||||
|
' --markdown Alias for --format markdown',
|
||||||
|
' --write <path> Write json or markdown output to a file',
|
||||||
|
' --root <dir> Repository root to inspect (default: cwd)',
|
||||||
|
' --repo <owner/repo> GitHub repo to inspect; repeatable',
|
||||||
|
' --skip-github Skip live GitHub queue/discussion checks',
|
||||||
|
' --max-open-prs <n> PR budget passed through to platform:audit',
|
||||||
|
' --max-open-issues <n> Issue budget passed through to platform:audit',
|
||||||
|
' --max-dirty-files <n> Dirty-file budget passed through to platform:audit',
|
||||||
|
' --allow-untracked <path> Ignore untracked files under path; repeatable',
|
||||||
|
' --use-env-github-token Keep GITHUB_TOKEN when invoking gh',
|
||||||
|
' --generated-at <iso> Override generatedAt for deterministic tests',
|
||||||
|
' --exit-code Return 2 when the objective is not ready',
|
||||||
|
' --help, -h Show this help',
|
||||||
|
].join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function readValue(args, index, flagName) {
|
||||||
|
const value = args[index + 1];
|
||||||
|
if (!value || value.startsWith('--')) {
|
||||||
|
throw new Error(`${flagName} requires a value`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseIntegerFlag(value, flagName) {
|
||||||
|
const parsed = Number.parseInt(value, 10);
|
||||||
|
if (!Number.isFinite(parsed) || parsed < 0) {
|
||||||
|
throw new Error(`Invalid ${flagName}: ${value}`);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRelativePrefix(value) {
|
||||||
|
const normalized = String(value || '')
|
||||||
|
.replace(/\\/g, '/')
|
||||||
|
.replace(/^\.\/+/, '')
|
||||||
|
.replace(/\/+$/, '');
|
||||||
|
return normalized ? `${normalized}/` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const args = argv.slice(2);
|
||||||
|
const parsed = {
|
||||||
|
allowUntracked: [],
|
||||||
|
exitCode: false,
|
||||||
|
format: 'markdown',
|
||||||
|
generatedAt: null,
|
||||||
|
help: false,
|
||||||
|
repos: [],
|
||||||
|
root: path.resolve(process.cwd()),
|
||||||
|
skipGithub: false,
|
||||||
|
thresholds: { ...DEFAULT_THRESHOLDS },
|
||||||
|
useEnvGithubToken: false,
|
||||||
|
writePath: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let index = 0; index < args.length; index += 1) {
|
||||||
|
const arg = args[index];
|
||||||
|
|
||||||
|
if (arg === '--help' || arg === '-h') {
|
||||||
|
parsed.help = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--format') {
|
||||||
|
parsed.format = readValue(args, index, arg).toLowerCase();
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith('--format=')) {
|
||||||
|
parsed.format = arg.slice('--format='.length).toLowerCase();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--json') {
|
||||||
|
parsed.format = 'json';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--markdown') {
|
||||||
|
parsed.format = 'markdown';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--write') {
|
||||||
|
parsed.writePath = path.resolve(readValue(args, index, arg));
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith('--write=')) {
|
||||||
|
parsed.writePath = path.resolve(arg.slice('--write='.length));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--root') {
|
||||||
|
parsed.root = path.resolve(readValue(args, index, arg));
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith('--root=')) {
|
||||||
|
parsed.root = path.resolve(arg.slice('--root='.length));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--repo') {
|
||||||
|
parsed.repos.push(readValue(args, index, arg));
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith('--repo=')) {
|
||||||
|
parsed.repos.push(arg.slice('--repo='.length));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--skip-github') {
|
||||||
|
parsed.skipGithub = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--allow-untracked') {
|
||||||
|
parsed.allowUntracked.push(readValue(args, index, arg));
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith('--allow-untracked=')) {
|
||||||
|
parsed.allowUntracked.push(arg.slice('--allow-untracked='.length));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--max-open-prs') {
|
||||||
|
parsed.thresholds.maxOpenPrs = parseIntegerFlag(readValue(args, index, arg), arg);
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith('--max-open-prs=')) {
|
||||||
|
parsed.thresholds.maxOpenPrs = parseIntegerFlag(arg.slice('--max-open-prs='.length), '--max-open-prs');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--max-open-issues') {
|
||||||
|
parsed.thresholds.maxOpenIssues = parseIntegerFlag(readValue(args, index, arg), arg);
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith('--max-open-issues=')) {
|
||||||
|
parsed.thresholds.maxOpenIssues = parseIntegerFlag(arg.slice('--max-open-issues='.length), '--max-open-issues');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--max-dirty-files') {
|
||||||
|
parsed.thresholds.maxDirtyFiles = parseIntegerFlag(readValue(args, index, arg), arg);
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith('--max-dirty-files=')) {
|
||||||
|
parsed.thresholds.maxDirtyFiles = parseIntegerFlag(arg.slice('--max-dirty-files='.length), '--max-dirty-files');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--use-env-github-token') {
|
||||||
|
parsed.useEnvGithubToken = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--generated-at') {
|
||||||
|
parsed.generatedAt = readValue(args, index, arg);
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith('--generated-at=')) {
|
||||||
|
parsed.generatedAt = arg.slice('--generated-at='.length);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--exit-code') {
|
||||||
|
parsed.exitCode = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown argument: ${arg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!['text', 'json', 'markdown'].includes(parsed.format)) {
|
||||||
|
throw new Error(`Invalid format: ${parsed.format}. Use text, json, or markdown.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.writePath && parsed.format === 'text') {
|
||||||
|
throw new Error('--write requires --json, --markdown, or --format json|markdown');
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed.allowUntracked = parsed.allowUntracked.map(normalizeRelativePrefix).filter(Boolean);
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readText(rootDir, relativePath) {
|
||||||
|
try {
|
||||||
|
return fs.readFileSync(path.join(rootDir, relativePath), 'utf8');
|
||||||
|
} catch (_error) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileExists(rootDir, relativePath) {
|
||||||
|
return fs.existsSync(path.join(rootDir, relativePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
function includesAll(text, needles) {
|
||||||
|
return needles.every(needle => text.includes(needle));
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCommand(command, args, options = {}) {
|
||||||
|
const result = spawnSync(command, args, {
|
||||||
|
cwd: options.cwd,
|
||||||
|
encoding: 'utf8',
|
||||||
|
maxBuffer: 10 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error || result.status !== 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (result.stdout || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function readPackage(rootDir) {
|
||||||
|
const text = readText(rootDir, 'package.json');
|
||||||
|
if (!text.trim()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(text);
|
||||||
|
} catch (_error) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRequirement(id, requirement, artifact, status, evidence, gap) {
|
||||||
|
return { id, requirement, artifact, status, evidence, gap };
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCurrentOrComplete(status) {
|
||||||
|
return status === 'current' || status === 'complete';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRequirements(rootDir, platformReport) {
|
||||||
|
const roadmap = readText(rootDir, 'docs/ECC-2.0-GA-ROADMAP.md');
|
||||||
|
const publicationReadiness = readText(rootDir, 'docs/releases/2.0.0-rc.1/publication-readiness.md');
|
||||||
|
const namingMatrix = readText(rootDir, 'docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md');
|
||||||
|
const previewManifest = readText(rootDir, 'docs/releases/2.0.0-rc.1/preview-pack-manifest.md');
|
||||||
|
const progressSync = readText(rootDir, 'docs/architecture/progress-sync-contract.md');
|
||||||
|
const observabilityReadiness = readText(rootDir, 'docs/architecture/observability-readiness.md');
|
||||||
|
const stalePrSalvage = readText(rootDir, 'docs/stale-pr-salvage-ledger.md');
|
||||||
|
const supplyChainRunbook = readText(rootDir, 'docs/security/supply-chain-incident-response.md');
|
||||||
|
const packageJson = readPackage(rootDir);
|
||||||
|
const scripts = packageJson.scripts || {};
|
||||||
|
|
||||||
|
const githubLive = !platformReport.github.skipped && platformReport.github.totals.errors === 0;
|
||||||
|
const queuesCurrent = githubLive
|
||||||
|
&& platformReport.github.totals.openPrs <= platformReport.thresholds.maxOpenPrs
|
||||||
|
&& platformReport.github.totals.openIssues <= platformReport.thresholds.maxOpenIssues;
|
||||||
|
const discussionsCurrent = githubLive
|
||||||
|
&& platformReport.github.totals.discussionsNeedingMaintainerTouch === 0
|
||||||
|
&& platformReport.github.totals.discussionsMissingAcceptedAnswer === 0;
|
||||||
|
|
||||||
|
return [
|
||||||
|
buildRequirement(
|
||||||
|
'public-pr-budget',
|
||||||
|
'Keep public PRs below 20',
|
||||||
|
'scripts/platform-audit.js live GitHub sweep',
|
||||||
|
queuesCurrent ? 'current' : 'in_progress',
|
||||||
|
githubLive
|
||||||
|
? `${platformReport.github.totals.openPrs} open PRs across ${platformReport.github.repos.length} tracked repos`
|
||||||
|
: 'live GitHub queue readback was skipped or failed',
|
||||||
|
queuesCurrent ? 'repeat before release' : 'run live platform:audit and drain PR queue'
|
||||||
|
),
|
||||||
|
buildRequirement(
|
||||||
|
'public-issue-budget',
|
||||||
|
'Keep public issues below 20',
|
||||||
|
'scripts/platform-audit.js live GitHub sweep',
|
||||||
|
queuesCurrent ? 'current' : 'in_progress',
|
||||||
|
githubLive
|
||||||
|
? `${platformReport.github.totals.openIssues} open issues across ${platformReport.github.repos.length} tracked repos`
|
||||||
|
: 'live GitHub queue readback was skipped or failed',
|
||||||
|
queuesCurrent ? 'repeat before release' : 'run live platform:audit and drain issue queue'
|
||||||
|
),
|
||||||
|
buildRequirement(
|
||||||
|
'repository-discussions',
|
||||||
|
'Respond and manage repository discussions',
|
||||||
|
'scripts/platform-audit.js discussion summary',
|
||||||
|
discussionsCurrent ? 'current' : 'in_progress',
|
||||||
|
githubLive
|
||||||
|
? `${platformReport.github.totals.discussionsNeedingMaintainerTouch} need maintainer touch; ${platformReport.github.totals.discussionsMissingAcceptedAnswer} answerable discussions missing accepted answer`
|
||||||
|
: 'live discussion readback was skipped or failed',
|
||||||
|
discussionsCurrent ? 'repeat before release' : 'respond, answer, or route remaining discussions'
|
||||||
|
),
|
||||||
|
buildRequirement(
|
||||||
|
'completion-dashboard',
|
||||||
|
'Build ITO-44 completion dashboard into a repeatable command',
|
||||||
|
'npm run operator:dashboard',
|
||||||
|
scripts['operator:dashboard'] === 'node scripts/operator-readiness-dashboard.js'
|
||||||
|
&& fileExists(rootDir, 'scripts/operator-readiness-dashboard.js')
|
||||||
|
? 'complete'
|
||||||
|
: 'in_progress',
|
||||||
|
scripts['operator:dashboard'] === 'node scripts/operator-readiness-dashboard.js'
|
||||||
|
? 'operator:dashboard package script exists'
|
||||||
|
: 'operator:dashboard package script missing',
|
||||||
|
'keep generated dashboard attached to publication evidence'
|
||||||
|
),
|
||||||
|
buildRequirement(
|
||||||
|
'ecc-preview-pack',
|
||||||
|
'ECC 2.0 preview pack ready',
|
||||||
|
'docs/releases/2.0.0-rc.1/preview-pack-manifest.md',
|
||||||
|
includesAll(previewManifest, ['publication-readiness.md', 'release-notes.md', 'quickstart.md']) ? 'in_progress' : 'not_complete',
|
||||||
|
includesAll(previewManifest, ['publication-readiness.md', 'release-notes.md', 'quickstart.md'])
|
||||||
|
? 'preview pack manifest is in-tree'
|
||||||
|
: 'preview pack manifest is incomplete',
|
||||||
|
'final clean-checkout release approval and publish evidence still pending'
|
||||||
|
),
|
||||||
|
buildRequirement(
|
||||||
|
'hermes-specialized-skills',
|
||||||
|
'Include Hermes specialized skills safely',
|
||||||
|
'docs/HERMES-SETUP.md and skills/hermes-imports/SKILL.md',
|
||||||
|
fileExists(rootDir, 'docs/HERMES-SETUP.md') && fileExists(rootDir, 'skills/hermes-imports/SKILL.md')
|
||||||
|
? 'in_progress'
|
||||||
|
: 'not_complete',
|
||||||
|
fileExists(rootDir, 'docs/HERMES-SETUP.md') && fileExists(rootDir, 'skills/hermes-imports/SKILL.md')
|
||||||
|
? 'Hermes setup and import skill are present'
|
||||||
|
: 'Hermes setup/import artifacts missing',
|
||||||
|
'final preview-pack smoke and release review pending'
|
||||||
|
),
|
||||||
|
buildRequirement(
|
||||||
|
'naming-and-plugin-publication',
|
||||||
|
'Prepare name-change, Claude plugin, and Codex plugin paths',
|
||||||
|
'naming-and-publication-matrix plus publication-readiness',
|
||||||
|
includesAll(namingMatrix, ['Claude plugin', 'Codex plugin', 'npm package', 'Publication Paths'])
|
||||||
|
&& includesAll(publicationReadiness, ['Claude plugin', 'Codex plugin'])
|
||||||
|
? 'in_progress'
|
||||||
|
: 'not_complete',
|
||||||
|
'naming matrix and plugin readiness gates exist',
|
||||||
|
'real tag/push, marketplace submission, and final channel choice remain approval-gated'
|
||||||
|
),
|
||||||
|
buildRequirement(
|
||||||
|
'release-notes-and-notifications',
|
||||||
|
'Prepare release notes, articles, tweets, and push notifications',
|
||||||
|
'docs/releases/2.0.0-rc.1 social and release-copy files',
|
||||||
|
fileExists(rootDir, 'docs/releases/2.0.0-rc.1/release-notes.md')
|
||||||
|
&& fileExists(rootDir, 'docs/releases/2.0.0-rc.1/x-thread.md')
|
||||||
|
&& fileExists(rootDir, 'docs/releases/2.0.0-rc.1/linkedin-post.md')
|
||||||
|
? 'in_progress'
|
||||||
|
: 'not_complete',
|
||||||
|
'release notes, X thread, and LinkedIn draft are present',
|
||||||
|
'URL-backed refresh and publish approval still pending'
|
||||||
|
),
|
||||||
|
buildRequirement(
|
||||||
|
'agentshield-enterprise-iteration',
|
||||||
|
'Advance AgentShield enterprise iteration',
|
||||||
|
'AgentShield PR evidence plus enterprise roadmap',
|
||||||
|
roadmap.includes('AgentShield Enterprise Iteration') && roadmap.includes('#78-#86')
|
||||||
|
? 'in_progress'
|
||||||
|
: 'not_complete',
|
||||||
|
'AgentShield enterprise PR evidence is mirrored in the GA roadmap',
|
||||||
|
'live IOC update loop and cross-harness depth remain pending'
|
||||||
|
),
|
||||||
|
buildRequirement(
|
||||||
|
'ecc-tools-next-level',
|
||||||
|
'Advance ECC Tools native payments and AI-native harness-agnostic app',
|
||||||
|
'ECC Tools PR evidence, billing gate, hosted analysis lanes',
|
||||||
|
includesAll(roadmap, ['ECC-Tools PR #73', 'hosted promotion', 'announcementGate'])
|
||||||
|
? 'in_progress'
|
||||||
|
: 'not_complete',
|
||||||
|
'billing announcement gate and hosted analysis lanes are mirrored in the GA roadmap',
|
||||||
|
'live Marketplace test-account readback, hosted promotion telemetry, and operator review UX pending'
|
||||||
|
),
|
||||||
|
buildRequirement(
|
||||||
|
'legacy-salvage',
|
||||||
|
'Audit, prune, or attach legacy work',
|
||||||
|
'docs/stale-pr-salvage-ledger.md and legacy inventory',
|
||||||
|
stalePrSalvage.includes('Manual review tail') || roadmap.includes('ITO-55')
|
||||||
|
? 'in_progress'
|
||||||
|
: 'not_complete',
|
||||||
|
'legacy salvage ledger and ITO-55 tracking are present',
|
||||||
|
'final translation/manual-review tail remains'
|
||||||
|
),
|
||||||
|
buildRequirement(
|
||||||
|
'linear-roadmap-and-progress',
|
||||||
|
'Keep Linear roadmap detailed and progress tracking synchronized',
|
||||||
|
'Linear project mirror plus progress-sync contract',
|
||||||
|
includesAll(roadmap, ['ITO-44', 'ITO-59', 'Linear']) && includesAll(progressSync, ['GitHub', 'Linear', 'handoff', 'repo roadmap'])
|
||||||
|
? 'in_progress'
|
||||||
|
: 'not_complete',
|
||||||
|
'repo mirror and progress-sync contract are present',
|
||||||
|
'recurring Linear status sync and productized realtime sync remain pending'
|
||||||
|
),
|
||||||
|
buildRequirement(
|
||||||
|
'observability-for-self-use',
|
||||||
|
'Provide ECC 2.0 observability for self-use',
|
||||||
|
'observability readiness gate',
|
||||||
|
scripts['observability:ready'] === 'node scripts/observability-readiness.js'
|
||||||
|
&& includesAll(observabilityReadiness, ['observability-readiness.js'])
|
||||||
|
? 'complete'
|
||||||
|
: 'in_progress',
|
||||||
|
scripts['observability:ready'] === 'node scripts/observability-readiness.js'
|
||||||
|
? 'observability:ready command and readiness doc exist'
|
||||||
|
: 'observability readiness command missing',
|
||||||
|
'runtime/dashboard implementation can continue after release gates'
|
||||||
|
),
|
||||||
|
buildRequirement(
|
||||||
|
'supply-chain-local-protection',
|
||||||
|
'Keep Mini Shai-Hulud/TanStack protection loop current',
|
||||||
|
'supply-chain watch plus runbook',
|
||||||
|
includesAll(supplyChainRunbook, ['TanStack', 'Mini Shai-Hulud', 'scan-supply-chain-iocs.js'])
|
||||||
|
&& fileExists(rootDir, '.github/workflows/supply-chain-watch.yml')
|
||||||
|
? 'current'
|
||||||
|
: 'in_progress',
|
||||||
|
'scheduled supply-chain watch and runbook are present',
|
||||||
|
'advisory-source refresh automation and Linear status synchronization remain ITO-57 follow-up'
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildReport(options) {
|
||||||
|
const rootDir = path.resolve(options.root);
|
||||||
|
const generatedAt = options.generatedAt || new Date().toISOString();
|
||||||
|
const platformReport = buildPlatformReport({
|
||||||
|
allowUntracked: options.allowUntracked,
|
||||||
|
exitCode: false,
|
||||||
|
format: 'json',
|
||||||
|
help: false,
|
||||||
|
repos: options.repos,
|
||||||
|
root: rootDir,
|
||||||
|
skipGithub: options.skipGithub,
|
||||||
|
thresholds: options.thresholds,
|
||||||
|
useEnvGithubToken: options.useEnvGithubToken,
|
||||||
|
writePath: null,
|
||||||
|
});
|
||||||
|
const requirements = buildRequirements(rootDir, platformReport);
|
||||||
|
const incompleteRequirements = requirements.filter(item => !isCurrentOrComplete(item.status));
|
||||||
|
const topActions = incompleteRequirements.map(item => ({
|
||||||
|
id: item.id,
|
||||||
|
summary: item.requirement,
|
||||||
|
fix: item.gap,
|
||||||
|
}));
|
||||||
|
const head = runCommand('git', ['rev-parse', 'HEAD'], { cwd: rootDir });
|
||||||
|
|
||||||
|
return {
|
||||||
|
schema_version: SCHEMA_VERSION,
|
||||||
|
generatedAt,
|
||||||
|
root: rootDir,
|
||||||
|
head,
|
||||||
|
ready: incompleteRequirements.length === 0,
|
||||||
|
dashboardReady: platformReport.ready,
|
||||||
|
publicationReady: false,
|
||||||
|
platform: {
|
||||||
|
ready: platformReport.ready,
|
||||||
|
branch: platformReport.git.branch,
|
||||||
|
blockingDirtyCount: platformReport.git.blockingDirtyCount,
|
||||||
|
ignoredDirtyCount: platformReport.git.ignoredDirty.length,
|
||||||
|
openPrs: platformReport.github.totals.openPrs,
|
||||||
|
openIssues: platformReport.github.totals.openIssues,
|
||||||
|
discussionsNeedingMaintainerTouch: platformReport.github.totals.discussionsNeedingMaintainerTouch,
|
||||||
|
discussionsMissingAcceptedAnswer: platformReport.github.totals.discussionsMissingAcceptedAnswer,
|
||||||
|
githubErrors: platformReport.github.totals.errors,
|
||||||
|
githubSkipped: platformReport.github.skipped,
|
||||||
|
},
|
||||||
|
requirements,
|
||||||
|
top_actions: topActions,
|
||||||
|
next_work_order: [
|
||||||
|
'Regenerate this dashboard from the final release commit before publication evidence is recorded.',
|
||||||
|
'Continue ITO-57 with advisory-source refresh automation and Linear status synchronization for the scheduled supply-chain watch.',
|
||||||
|
'Advance ECC Tools live Marketplace test-account readback before publishing native-payments announcement copy.',
|
||||||
|
'Resume ITO-45, ITO-46, and ITO-56 only after the generated dashboard and final release gates are refreshed.',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function markdownEscape(value) {
|
||||||
|
return String(value === undefined || value === null ? '' : value)
|
||||||
|
.replace(/\|/g, '\\|')
|
||||||
|
.replace(/\r?\n/g, '<br>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderText(report) {
|
||||||
|
const lines = [
|
||||||
|
`ECC Operator Readiness Dashboard: ${report.ready ? 'objective ready' : 'work remaining'}`,
|
||||||
|
`Generated: ${report.generatedAt}`,
|
||||||
|
`Commit: ${report.head || 'unknown'}`,
|
||||||
|
`Dashboard ready: ${report.dashboardReady}`,
|
||||||
|
`Publication ready: ${report.publicationReady}`,
|
||||||
|
'',
|
||||||
|
'Platform:',
|
||||||
|
` PRs: ${report.platform.openPrs}`,
|
||||||
|
` Issues: ${report.platform.openIssues}`,
|
||||||
|
` Discussions needing touch: ${report.platform.discussionsNeedingMaintainerTouch}`,
|
||||||
|
` Missing accepted answers: ${report.platform.discussionsMissingAcceptedAnswer}`,
|
||||||
|
` Blocking dirty files: ${report.platform.blockingDirtyCount}`,
|
||||||
|
'',
|
||||||
|
'Requirements:',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const item of report.requirements) {
|
||||||
|
lines.push(` ${item.status.toUpperCase()} ${item.id}: ${item.requirement}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('', 'Top actions:');
|
||||||
|
if (report.top_actions.length === 0) {
|
||||||
|
lines.push(' none');
|
||||||
|
} else {
|
||||||
|
for (const action of report.top_actions) {
|
||||||
|
lines.push(` - ${action.id}: ${action.fix}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${lines.join('\n')}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMarkdown(report) {
|
||||||
|
const lines = [
|
||||||
|
'# ECC Operator Readiness Dashboard',
|
||||||
|
'',
|
||||||
|
'This dashboard is generated by `npm run operator:dashboard`. It is an operator snapshot, not release approval.',
|
||||||
|
'',
|
||||||
|
`Generated: ${report.generatedAt}`,
|
||||||
|
`Commit: ${report.head || 'unknown'}`,
|
||||||
|
`Status: ${report.ready ? 'objective ready' : 'work remaining'}`,
|
||||||
|
'',
|
||||||
|
'## Current Status',
|
||||||
|
'',
|
||||||
|
'| Area | Status | Evidence |',
|
||||||
|
'| --- | --- | --- |',
|
||||||
|
`| PR queue | ${report.platform.openPrs < 20 && !report.platform.githubSkipped ? 'Current' : 'Needs work'} | ${report.platform.openPrs} open PRs across tracked repos |`,
|
||||||
|
`| Issue queue | ${report.platform.openIssues < 20 && !report.platform.githubSkipped ? 'Current' : 'Needs work'} | ${report.platform.openIssues} open issues across tracked repos |`,
|
||||||
|
`| Discussions | ${report.platform.discussionsNeedingMaintainerTouch === 0 && report.platform.discussionsMissingAcceptedAnswer === 0 && !report.platform.githubSkipped ? 'Current' : 'Needs work'} | ${report.platform.discussionsNeedingMaintainerTouch} need maintainer touch; ${report.platform.discussionsMissingAcceptedAnswer} missing accepted answer |`,
|
||||||
|
`| Local worktree | ${report.platform.blockingDirtyCount === 0 ? 'Current' : 'Needs work'} | ${report.platform.blockingDirtyCount} blocking dirty files; ${report.platform.ignoredDirtyCount} ignored dirty entries |`,
|
||||||
|
`| Dashboard generation | ${report.dashboardReady ? 'Current' : 'Needs work'} | platform audit ready: ${report.platform.ready}; GitHub skipped: ${report.platform.githubSkipped} |`,
|
||||||
|
`| Publication | ${report.publicationReady ? 'Ready' : 'Not complete'} | release, npm, plugin, billing, and announcement gates are tracked below |`,
|
||||||
|
'',
|
||||||
|
'## Prompt-To-Artifact Checklist',
|
||||||
|
'',
|
||||||
|
'| Objective requirement | Artifact or gate | Status | Evidence | Gap |',
|
||||||
|
'| --- | --- | --- | --- | --- |',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const item of report.requirements) {
|
||||||
|
lines.push(`| ${markdownEscape(item.requirement)} | ${markdownEscape(item.artifact)} | ${markdownEscape(item.status)} | ${markdownEscape(item.evidence)} | ${markdownEscape(item.gap)} |`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('', '## Top Actions', '');
|
||||||
|
if (report.top_actions.length === 0) {
|
||||||
|
lines.push('- none');
|
||||||
|
} else {
|
||||||
|
for (const action of report.top_actions) {
|
||||||
|
lines.push(`- \`${markdownEscape(action.id)}\`: ${markdownEscape(action.fix)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('', '## Next Work Order', '');
|
||||||
|
report.next_work_order.forEach((item, index) => {
|
||||||
|
lines.push(`${index + 1}. ${item}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return `${lines.join('\n')}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderReport(report, format) {
|
||||||
|
if (format === 'json') {
|
||||||
|
return `${JSON.stringify(report, null, 2)}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format === 'text') {
|
||||||
|
return renderText(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderMarkdown(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeOutput(writePath, output) {
|
||||||
|
fs.mkdirSync(path.dirname(writePath), { recursive: true });
|
||||||
|
fs.writeFileSync(writePath, output, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
let options;
|
||||||
|
try {
|
||||||
|
options = parseArgs(process.argv);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error: ${error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.help) {
|
||||||
|
usage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const report = buildReport(options);
|
||||||
|
const output = renderReport(report, options.format);
|
||||||
|
|
||||||
|
if (options.writePath) {
|
||||||
|
writeOutput(options.writePath, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdout.write(output);
|
||||||
|
|
||||||
|
if (options.exitCode && !report.ready) {
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
buildReport,
|
||||||
|
parseArgs,
|
||||||
|
renderMarkdown,
|
||||||
|
renderReport,
|
||||||
|
renderText,
|
||||||
|
};
|
||||||
@ -268,6 +268,10 @@ function readText(rootDir, relativePath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fileExists(rootDir, relativePath) {
|
||||||
|
return fs.existsSync(path.join(rootDir, relativePath));
|
||||||
|
}
|
||||||
|
|
||||||
function safeParseJson(text) {
|
function safeParseJson(text) {
|
||||||
if (!text || !text.trim()) {
|
if (!text || !text.trim()) {
|
||||||
return null;
|
return null;
|
||||||
@ -431,10 +435,20 @@ function buildLocalEvidenceChecks(rootDir) {
|
|||||||
'platform-audit-cli-surface',
|
'platform-audit-cli-surface',
|
||||||
packageScripts['platform:audit'] === 'node scripts/platform-audit.js'
|
packageScripts['platform:audit'] === 'node scripts/platform-audit.js'
|
||||||
&& packageScripts['discussion:audit'] === 'node scripts/discussion-audit.js'
|
&& packageScripts['discussion:audit'] === 'node scripts/discussion-audit.js'
|
||||||
|
&& packageScripts['operator:dashboard'] === 'node scripts/operator-readiness-dashboard.js'
|
||||||
? 'pass'
|
? 'pass'
|
||||||
: 'fail',
|
: 'fail',
|
||||||
'package.json exposes the platform and discussion audit commands',
|
'package.json exposes platform, discussion, and operator dashboard audit commands',
|
||||||
{ fix: 'Add platform:audit and discussion:audit commands to package.json.' }
|
{ fix: 'Add platform:audit, discussion:audit, and operator:dashboard commands to package.json.' }
|
||||||
|
),
|
||||||
|
buildCheck(
|
||||||
|
'operator-dashboard-command',
|
||||||
|
fileExists(rootDir, 'scripts/operator-readiness-dashboard.js')
|
||||||
|
&& packageScripts['operator:dashboard'] === 'node scripts/operator-readiness-dashboard.js'
|
||||||
|
? 'pass'
|
||||||
|
: 'fail',
|
||||||
|
'operator dashboard is generated by the repeatable ITO-44 command',
|
||||||
|
{ path: 'scripts/operator-readiness-dashboard.js' }
|
||||||
),
|
),
|
||||||
buildCheck(
|
buildCheck(
|
||||||
'roadmap-linear-mirror',
|
'roadmap-linear-mirror',
|
||||||
|
|||||||
250
tests/scripts/operator-readiness-dashboard.test.js
Normal file
250
tests/scripts/operator-readiness-dashboard.test.js
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
/**
|
||||||
|
* Tests for scripts/operator-readiness-dashboard.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
|
const path = require('path');
|
||||||
|
const { execFileSync, spawnSync } = require('child_process');
|
||||||
|
|
||||||
|
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'operator-readiness-dashboard.js');
|
||||||
|
const { buildReport, parseArgs } = require(SCRIPT);
|
||||||
|
|
||||||
|
function createTempDir(prefix) {
|
||||||
|
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup(dirPath) {
|
||||||
|
fs.rmSync(dirPath, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeFile(rootDir, relativePath, content) {
|
||||||
|
const targetPath = path.join(rootDir, relativePath);
|
||||||
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||||||
|
fs.writeFileSync(targetPath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
function seedRepo(rootDir, overrides = {}) {
|
||||||
|
const files = {
|
||||||
|
'package.json': JSON.stringify({
|
||||||
|
name: 'everything-claude-code',
|
||||||
|
files: [
|
||||||
|
'scripts/observability-readiness.js',
|
||||||
|
'scripts/operator-readiness-dashboard.js',
|
||||||
|
'scripts/platform-audit.js'
|
||||||
|
],
|
||||||
|
scripts: {
|
||||||
|
'discussion:audit': 'node scripts/discussion-audit.js',
|
||||||
|
'observability:ready': 'node scripts/observability-readiness.js',
|
||||||
|
'operator:dashboard': 'node scripts/operator-readiness-dashboard.js',
|
||||||
|
'platform:audit': 'node scripts/platform-audit.js',
|
||||||
|
'security:ioc-scan': 'node scripts/ci/scan-supply-chain-iocs.js'
|
||||||
|
}
|
||||||
|
}, null, 2),
|
||||||
|
'scripts/operator-readiness-dashboard.js': 'operator dashboard generator',
|
||||||
|
'docs/ECC-2.0-GA-ROADMAP.md': [
|
||||||
|
'https://linear.app/itomarkets/project/ecc-platform-roadmap-52b328ee03e1',
|
||||||
|
'Linear ITO-44 ITO-59',
|
||||||
|
'AgentShield PR #86 #78-#86',
|
||||||
|
'AgentShield Enterprise Iteration',
|
||||||
|
'ECC-Tools PR #73',
|
||||||
|
'hosted promotion',
|
||||||
|
'announcementGate',
|
||||||
|
'ITO-55'
|
||||||
|
].join('\n'),
|
||||||
|
'docs/releases/2.0.0-rc.1/publication-readiness.md': 'Claude plugin Codex plugin',
|
||||||
|
'docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md': 'Claude plugin Codex plugin npm package Publication Paths',
|
||||||
|
'docs/releases/2.0.0-rc.1/preview-pack-manifest.md': 'publication-readiness.md release-notes.md quickstart.md',
|
||||||
|
'docs/releases/2.0.0-rc.1/release-notes.md': 'release notes',
|
||||||
|
'docs/releases/2.0.0-rc.1/x-thread.md': 'x thread',
|
||||||
|
'docs/releases/2.0.0-rc.1/linkedin-post.md': 'linkedin post',
|
||||||
|
'docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-15.md': [
|
||||||
|
'operator:dashboard',
|
||||||
|
'Prompt-To-Artifact Checklist',
|
||||||
|
'Next Work Order',
|
||||||
|
'ITO-44',
|
||||||
|
'ITO-59',
|
||||||
|
'PR queue',
|
||||||
|
'Not complete'
|
||||||
|
].join('\n'),
|
||||||
|
'docs/HERMES-SETUP.md': 'Hermes setup',
|
||||||
|
'skills/hermes-imports/SKILL.md': 'Hermes imports',
|
||||||
|
'docs/stale-pr-salvage-ledger.md': 'Manual review tail',
|
||||||
|
'docs/architecture/progress-sync-contract.md': 'GitHub PRs/issues/discussions Linear project local handoff repo roadmap scripts/work-items.js',
|
||||||
|
'docs/architecture/observability-readiness.md': 'observability-readiness.js',
|
||||||
|
'docs/security/supply-chain-incident-response.md': 'TanStack Mini Shai-Hulud node-ipc scan-supply-chain-iocs.js',
|
||||||
|
'docs/releases/2.0.0-rc.1/publication-evidence-2026-05-15.md': 'TanStack Mini Shai-Hulud Node IPC follow-up node-ipc IOC scan',
|
||||||
|
'.github/workflows/supply-chain-watch.yml': 'name: Supply-Chain Watch'
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [relativePath, content] of Object.entries({ ...files, ...overrides })) {
|
||||||
|
if (content === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
writeFile(rootDir, relativePath, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(args = [], options = {}) {
|
||||||
|
return execFileSync('node', [SCRIPT, ...args], {
|
||||||
|
cwd: options.cwd || path.join(__dirname, '..', '..'),
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
timeout: 10000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function runProcess(args = [], options = {}) {
|
||||||
|
return spawnSync('node', [SCRIPT, ...args], {
|
||||||
|
cwd: options.cwd || path.join(__dirname, '..', '..'),
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
timeout: 10000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function test(name, fn) {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
console.log(` PASS ${name}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(` FAIL ${name}`);
|
||||||
|
console.log(` Error: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTests() {
|
||||||
|
console.log('\n=== Testing operator-readiness-dashboard.js ===\n');
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
if (test('parseArgs accepts dashboard flags and rejects invalid values', () => {
|
||||||
|
const rootDir = createTempDir('operator-dashboard-args-');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = parseArgs([
|
||||||
|
'node',
|
||||||
|
'script',
|
||||||
|
'--format=json',
|
||||||
|
`--root=${rootDir}`,
|
||||||
|
'--skip-github',
|
||||||
|
'--allow-untracked',
|
||||||
|
'docs/drafts/',
|
||||||
|
'--repo',
|
||||||
|
'affaan-m/everything-claude-code',
|
||||||
|
'--generated-at',
|
||||||
|
'2026-05-15T00:00:00.000Z'
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.strictEqual(parsed.format, 'json');
|
||||||
|
assert.strictEqual(parsed.root, path.resolve(rootDir));
|
||||||
|
assert.strictEqual(parsed.skipGithub, true);
|
||||||
|
assert.deepStrictEqual(parsed.allowUntracked, ['docs/drafts/']);
|
||||||
|
assert.deepStrictEqual(parsed.repos, ['affaan-m/everything-claude-code']);
|
||||||
|
assert.strictEqual(parsed.generatedAt, '2026-05-15T00:00:00.000Z');
|
||||||
|
|
||||||
|
assert.throws(() => parseArgs(['node', 'script', '--format', 'xml']), /Invalid format/);
|
||||||
|
assert.throws(() => parseArgs(['node', 'script', '--write', 'dashboard.md', '--format', 'text']), /--write requires/);
|
||||||
|
assert.throws(() => parseArgs(['node', 'script', '--max-open-prs', 'x']), /Invalid --max-open-prs/);
|
||||||
|
assert.throws(() => parseArgs(['node', 'script', '--unknown']), /Unknown argument/);
|
||||||
|
} finally {
|
||||||
|
cleanup(rootDir);
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('seeded repo emits an objective audit with remaining work', () => {
|
||||||
|
const rootDir = createTempDir('operator-dashboard-report-');
|
||||||
|
|
||||||
|
try {
|
||||||
|
seedRepo(rootDir);
|
||||||
|
const report = buildReport({
|
||||||
|
allowUntracked: [],
|
||||||
|
exitCode: false,
|
||||||
|
format: 'json',
|
||||||
|
generatedAt: '2026-05-15T00:00:00.000Z',
|
||||||
|
help: false,
|
||||||
|
repos: [],
|
||||||
|
root: rootDir,
|
||||||
|
skipGithub: true,
|
||||||
|
thresholds: { maxOpenPrs: 20, maxOpenIssues: 20, maxDirtyFiles: 0 },
|
||||||
|
useEnvGithubToken: false,
|
||||||
|
writePath: null
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(report.schema_version, 'ecc.operator-readiness-dashboard.v1');
|
||||||
|
assert.strictEqual(report.generatedAt, '2026-05-15T00:00:00.000Z');
|
||||||
|
assert.strictEqual(report.dashboardReady, true);
|
||||||
|
assert.strictEqual(report.ready, false);
|
||||||
|
assert.strictEqual(report.publicationReady, false);
|
||||||
|
assert.ok(report.requirements.some(item => item.id === 'completion-dashboard' && item.status === 'complete'));
|
||||||
|
assert.ok(report.requirements.some(item => item.id === 'ecc-tools-next-level' && item.status === 'in_progress'));
|
||||||
|
assert.ok(report.top_actions.some(item => item.id === 'naming-and-plugin-publication'));
|
||||||
|
} finally {
|
||||||
|
cleanup(rootDir);
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('markdown output can be written as the dashboard artifact', () => {
|
||||||
|
const rootDir = createTempDir('operator-dashboard-markdown-');
|
||||||
|
const outputPath = path.join(rootDir, 'artifacts', 'dashboard.md');
|
||||||
|
|
||||||
|
try {
|
||||||
|
seedRepo(rootDir);
|
||||||
|
const stdout = run([
|
||||||
|
'--markdown',
|
||||||
|
'--skip-github',
|
||||||
|
`--root=${rootDir}`,
|
||||||
|
'--generated-at=2026-05-15T00:00:00.000Z',
|
||||||
|
'--write',
|
||||||
|
outputPath
|
||||||
|
], { cwd: rootDir });
|
||||||
|
const written = fs.readFileSync(outputPath, 'utf8');
|
||||||
|
|
||||||
|
assert.strictEqual(stdout, written);
|
||||||
|
assert.ok(written.includes('# ECC Operator Readiness Dashboard'));
|
||||||
|
assert.ok(written.includes('Generated: 2026-05-15T00:00:00.000Z'));
|
||||||
|
assert.ok(written.includes('## Prompt-To-Artifact Checklist'));
|
||||||
|
assert.ok(written.includes('Build ITO-44 completion dashboard into a repeatable command'));
|
||||||
|
assert.ok(written.includes('## Next Work Order'));
|
||||||
|
} finally {
|
||||||
|
cleanup(rootDir);
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('exit-code mode fails closed while macro objective has gaps', () => {
|
||||||
|
const rootDir = createTempDir('operator-dashboard-exit-');
|
||||||
|
|
||||||
|
try {
|
||||||
|
seedRepo(rootDir);
|
||||||
|
const result = runProcess([
|
||||||
|
'--json',
|
||||||
|
'--skip-github',
|
||||||
|
`--root=${rootDir}`,
|
||||||
|
'--generated-at=2026-05-15T00:00:00.000Z',
|
||||||
|
'--exit-code'
|
||||||
|
], { cwd: rootDir });
|
||||||
|
|
||||||
|
assert.strictEqual(result.status, 2);
|
||||||
|
assert.strictEqual(result.stderr, '');
|
||||||
|
assert.ok(result.stdout.includes('"ready": false'));
|
||||||
|
assert.ok(result.stdout.includes('"publicationReady": false'));
|
||||||
|
} finally {
|
||||||
|
cleanup(rootDir);
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
console.log(`\nPassed: ${passed}`);
|
||||||
|
console.log(`Failed: ${failed}`);
|
||||||
|
|
||||||
|
if (failed > 0) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
runTests();
|
||||||
|
}
|
||||||
@ -32,6 +32,7 @@ function seedRepo(rootDir, overrides = {}) {
|
|||||||
scripts: {
|
scripts: {
|
||||||
'platform:audit': 'node scripts/platform-audit.js',
|
'platform:audit': 'node scripts/platform-audit.js',
|
||||||
'discussion:audit': 'node scripts/discussion-audit.js',
|
'discussion:audit': 'node scripts/discussion-audit.js',
|
||||||
|
'operator:dashboard': 'node scripts/operator-readiness-dashboard.js',
|
||||||
'observability:ready': 'node scripts/observability-readiness.js',
|
'observability:ready': 'node scripts/observability-readiness.js',
|
||||||
'security:ioc-scan': 'node scripts/ci/scan-supply-chain-iocs.js',
|
'security:ioc-scan': 'node scripts/ci/scan-supply-chain-iocs.js',
|
||||||
'harness:audit': 'node scripts/harness-audit.js'
|
'harness:audit': 'node scripts/harness-audit.js'
|
||||||
@ -68,8 +69,11 @@ function seedRepo(rootDir, overrides = {}) {
|
|||||||
'ITO-44',
|
'ITO-44',
|
||||||
'ITO-59',
|
'ITO-59',
|
||||||
'PR queue',
|
'PR queue',
|
||||||
'Not complete'
|
'Not complete',
|
||||||
].join('\n')
|
'operator:dashboard',
|
||||||
|
'Next Work Order'
|
||||||
|
].join('\n'),
|
||||||
|
'scripts/operator-readiness-dashboard.js': 'operator dashboard generator'
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [relativePath, content] of Object.entries({ ...files, ...overrides })) {
|
for (const [relativePath, content] of Object.entries({ ...files, ...overrides })) {
|
||||||
@ -201,6 +205,7 @@ function runTests() {
|
|||||||
assert.strictEqual(parsed.github.skipped, true);
|
assert.strictEqual(parsed.github.skipped, true);
|
||||||
assert.ok(parsed.checks.some(check => check.id === 'roadmap-linear-mirror' && check.status === 'pass'));
|
assert.ok(parsed.checks.some(check => check.id === 'roadmap-linear-mirror' && check.status === 'pass'));
|
||||||
assert.ok(parsed.checks.some(check => check.id === 'supply-chain-runbook' && check.status === 'pass'));
|
assert.ok(parsed.checks.some(check => check.id === 'supply-chain-runbook' && check.status === 'pass'));
|
||||||
|
assert.ok(parsed.checks.some(check => check.id === 'operator-dashboard-command' && check.status === 'pass'));
|
||||||
assert.ok(parsed.checks.some(check => check.id === 'operator-readiness-dashboard' && check.status === 'pass'));
|
assert.ok(parsed.checks.some(check => check.id === 'operator-readiness-dashboard' && check.status === 'pass'));
|
||||||
assert.deepStrictEqual(parsed.top_actions, []);
|
assert.deepStrictEqual(parsed.top_actions, []);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user