#!/usr/bin/env node /** * audit-portfolio.mjs — PORTFOLIO.md Portfolio Management Audit * * Checks: * 1. portfolio.md exists (template or actual) * 2. Project states are legal (7-state portfolio states) * 3. Priority / Health / ROI / Agent Allocation fields are complete * 4. Duplicate work detection mechanism exists * 5. Portfolio Health Score formula defined * 6. Kill / Pause / Promotion rules defined * 7. Executive Dashboard schema defined * * Usage: * node scripts/audit-portfolio.mjs ← standalone * import { runPortfolioAudit } from './audit-portfolio.mjs' ← via orchestrator */ import { fileURLToPath } from 'url'; import path from 'path'; import fs from 'fs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const WORKSPACE = path.resolve(__dirname, '..'); const SELF = process.argv[1].replace(/\\/g, '/'); /** * @typedef {Object} CheckResult * @property {string} id * @property {string} name * @property {'PASS'|'WARN'|'FAIL'} status * @property {string} evidence * @property {string} [suggestion] */ /** @returns {CheckResult[]} */ export function runPortfolioAudit(workspaceDir) { const root = workspaceDir || WORKSPACE; const checks = []; // ---- Helpers ---- function readFile(rel) { const p = path.join(root, rel); if (!fs.existsSync(p)) return null; return fs.readFileSync(p, 'utf8'); } function check(prefix, id, name, ok, passEv, failEv, suggestion) { checks.push({ id: `${prefix}-${id}`, name, status: ok ? 'PASS' : 'FAIL', evidence: ok ? passEv : failEv, ...(ok ? {} : { suggestion: suggestion || `Implement ${id} in PORTFOLIO.md` }), }); } function warn(prefix, id, name, evidence, suggestion) { checks.push({ id: `${prefix}-${id}`, name, status: 'WARN', evidence, suggestion }); } // ---- Read protocol file ---- const port = readFile('PORTFOLIO.md'); if (!port) { checks.push({ id: 'PF-001', name: 'PORTFOLIO.md exists', status: 'FAIL', evidence: 'PORTFOLIO.md not found in workspace root', suggestion: 'Create PORTFOLIO.md with the Portfolio Management Protocol', }); return checks; } checks.push({ id: 'PF-001', name: 'PORTFOLIO.md exists', status: 'PASS', evidence: `PORTFOLIO.md found (${port.length} bytes)`, }); // ---- §1 Portfolio Registry ---- const hasReg = /§1 Portfolio Registry/.test(port); check('PF', '1', '§1 Portfolio Registry defined', hasReg, 'Found "§1 Portfolio Registry" with schema and field definitions', 'Add §1 Portfolio Registry section'); // Registry schema has all required fields const hasFields = /Project-ID.*Name.*Status.*Priority.*Health.*Agents.*Progress.*Last Updated.*ROI/.test(port); check('PF', '1a', '§1a Registry schema: all 9 fields (ID/Name/Status/Priority/Health/Agents/Progress/Updated/ROI)', hasFields, 'Registry schema found with all 9 fields', 'Ensure registry schema includes: Project-ID, Name, Status, Priority, Health, Agents, Progress, Last Updated, ROI'); // portfolio.md template exists const portTmpl = readFile('scripts/templates/portfolio.md.template'); check('PF', '1b', 'portfolio.md.template exists', portTmpl !== null, 'Found scripts/templates/portfolio.md.template', 'Create scripts/templates/portfolio.md.template'); // Actual portfolio.md in workspace? const actualPortfolio = readFile('portfolio.md'); warn('PF', '1c', 'Actual portfolio.md in workspace root', actualPortfolio ? `Found portfolio.md (${actualPortfolio.length} bytes)` : 'No portfolio.md found — expected when projects are active', actualPortfolio ? undefined : 'Create portfolio.md when first project becomes active'); // ---- §2 Project States ---- const hasStates = /§2 Project States/.test(port); check('PF', '2', '§2 Project States defined', hasStates, 'Found "§2 Project States" with 7 portfolio states and transition rules', 'Add §2 Project States with PLANNING/ACTIVE/BLOCKED/RECOVERY/PAUSED/COMPLETED/ARCHIVED'); // 7 states const states = ['PLANNING', 'ACTIVE', 'BLOCKED', 'RECOVERY', 'PAUSED', 'COMPLETED', 'ARCHIVED']; const foundStates = states.filter(s => port.includes(s)); check('PF', '2a', `§2a ${states.length} portfolio states defined (found ${foundStates.length})`, foundStates.length >= states.length, `All ${states.length} states present: ${foundStates.join(', ')}`, `Missing: ${states.filter(s => !port.includes(s)).join(', ')}`, 'Add missing portfolio states'); // State diagram const hasDiagram = /stateDiagram-v2/.test(port); check('PF', '2b', '§2b Portfolio state transition diagram (mermaid)', hasDiagram, 'Found mermaid stateDiagram-v2 for portfolio states', 'Add mermaid stateDiagram-v2 for portfolio state transitions'); // Prohibited transitions const hasProhibited = /Prohibited Transitions[\s\S]*❌/.test(port); check('PF', '2c', '§2c Prohibited portfolio transitions defined', hasProhibited, 'Found Prohibited Transitions with ❌ markers', 'Add Prohibited Transitions section for portfolio states'); // ---- §3 Priority System ---- const hasPriority = /§3 Priority System/.test(port); check('PF', '3', '§3 Priority System defined (P0/P1/P2/P3)', hasPriority, 'Found "§3 Priority System" with P0(P1(P2(P3 levels)', 'Add §3 Priority System with P0(P1(P2(P3 levels'); const hasP0 = port.includes('P0'); const hasP1 = port.includes('P1'); const hasP2 = port.includes('P2'); const hasP3 = port.includes('P3'); check('PF', '3a', `§3a All priority levels: P0/P1/P2/P3`, hasP0 && hasP1 && hasP2 && hasP3, `P0=${hasP0}, P1=${hasP1}, P2=${hasP2}, P3=${hasP3}`, 'Define all 4 priority levels: P0(Mission Critical), P1(High Value), P2(Normal), P3(Low Priority)'); // Collision resolution const hasCollision = /Priority Collision/.test(port); check('PF', '3b', '§3b Priority collision resolution defined', hasCollision, 'Found Priority Collision Resolution: ROI sort → dependency sort → user decision', 'Add Priority Collision Resolution for multiple P0 conflicts'); // ---- §4 Resource Allocation ---- const hasResources = /§4 Resource Allocation/.test(port); check('PF', '4', '§4 Resource Allocation defined', hasResources, 'Found "§4 Resource Allocation" with Agent Inventory / Utilization Score / Rebalancing', 'Add §4 Resource Allocation section'); // Agent inventory = 5 metrics const hasTotal = port.includes('Total Agents'); const hasBusy = port.includes('Busy Agents'); const hasIdle = port.includes('Idle Agents'); const hasBlocked = port.includes('Blocked Agents'); const hasRecovery = port.includes('Recovery Agents'); const all5inv = hasTotal && hasBusy && hasIdle && hasBlocked && hasRecovery; check('PF', '4a', `§4a Agent inventory: all 5 metrics (Total/Busy/Idle/Blocked/Recovery)`, all5inv, `Total=${hasTotal}, Busy=${hasBusy}, Idle=${hasIdle}, Blocked=${hasBlocked}, Recovery=${hasRecovery}`, 'Define Agent Inventory with Total/Busy/Idle/Blocked/Recovery metrics'); // Utilization formula const hasUtilFormula = /Utilization\s*=\s*Busy_Agents\s*\/\s*Total_Agents/.test(port); check('PF', '4b', '§4b Agent Utilization formula defined', hasUtilFormula, 'Found Utilization = Busy_Agents / Total_Agents with 4-level rating', 'Define Agent Utilization Score formula and rating'); // ---- §5 Project Health Aggregation ---- const hasAgg = /§5 Project Health Aggregation/.test(port); check('PF', '5', '§5 Project Health Aggregation defined', hasAgg, 'Found "§5 Project Health Aggregation" with 5 dimensions and weights', 'Add §5 Project Health Aggregation with Delivery/Quality/Learning/Execution/Risk dimensions'); // ---- §6 Portfolio Health Score ---- const hasPortHealth = /§6 Portfolio Health Score/.test(port); check('PF', '6', '§6 Portfolio Health Score defined', hasPortHealth, 'Found "§6 Portfolio Health Score" with weighted average formula', 'Add §6 Portfolio Health Score with weighted average calculation'); // Weighted formula const hasWeighted = /加权[\s\S]*平均/.test(port) || /weighted/i.test(port) || /Σ\(Project_Health[\s\S]*Agent_Count\)/.test(port); check('PF', '6a', '§6a Weighted portfolio formula defined', hasWeighted, 'Found weighted portfolio formula (Project_Health × Agent_Count / Σ Agent_Count)', 'Define Portfolio_Health = Σ(Project_Health × Agent_Count) / Σ(Agent_Count)'); // ---- §7 Learning Reuse ---- const hasReuse = /§7 Learning Reuse/.test(port); check('PF', '7', '§7 Learning Reuse Detector defined', hasReuse, 'Found "§7 Learning Reuse Detector" with detection criteria and action plan', 'Add §7 Learning Reuse Detector with duplicate work detection'); // Duplicate work criteria const hasDupCriteria = /Duplicate Work/.test(port); check('PF', '7a', '§7a Duplicate Work criteria defined', hasDupCriteria, 'Found Duplicate Work criteria (30% overlap, same Pattern Class, same deps, same domain)', 'Add Duplicate Work criteria with actionable thresholds'); // ---- §8 Portfolio Audit ---- const hasAudit = /§8 Portfolio Audit/.test(port); check('PF', '8', '§8 Portfolio Audit defined', hasAudit, 'Found "§8 Portfolio Audit" with daily generation and audit schema', 'Add §8 Portfolio Audit with daily generation and schema'); // portfolio-audit.md template exists const auditTmpl = readFile('scripts/templates/portfolio-audit.md.template'); check('PF', '8a', 'portfolio-audit.md.template exists', auditTmpl !== null, 'Found scripts/templates/portfolio-audit.md.template', 'Create scripts/templates/portfolio-audit.md.template'); // ---- §9 Kill Rule ---- const hasKill = /§9 Project Kill/.test(port); check('PF', '9', '§9 Project Kill Rule defined', hasKill, 'Found "§9 Project Kill Rule" with K1-K4 conditions and graceful shutdown', 'Add §9 Project Kill Rule with K1(Health<50), K2(>3 recovery fails), K3(ROI90), F2(Progress>80%), F3(Risk<20)', 'Add §11 Project Promotion Rule with Fast Track criteria'); // ---- §12 Executive Dashboard ---- const hasDash = /§12 Executive Dashboard/.test(port); check('PF', '12', '§12 Executive Dashboard defined', hasDash, 'Found "§12 Executive Dashboard" with Portfolio Health / Top Projects / Risks / Opportunities / Utilization / Learning Reuse / Quick Actions', 'Add §12 Executive Dashboard schema'); // dashboard.md template exists const dashTmpl = readFile('scripts/templates/dashboard.md.template'); check('PF', '12a', 'dashboard.md.template exists', dashTmpl !== null, 'Found scripts/templates/dashboard.md.template', 'Create scripts/templates/dashboard.md.template'); // ---- §13 Quality Gate ---- const hasQualityGate = /§13 Portfolio Quality Gate/.test(port); check('PF', '13', '§13 Portfolio Quality Gate defined', hasQualityGate, 'Found "§13 Portfolio Quality Gate" with 8-item checklist', 'Add §13 Portfolio Quality Gate with 8 pre-output checks'); // ---- §14 Integration ---- const hasIntegration = /§14 Integration Summary/.test(port); check('PF', '14', '§14 Integration Summary defined', hasIntegration, 'Found "§14 Integration Summary" with data flow and lifecycle', 'Add §14 Integration Summary with data flow and lifecycle diagram'); // ---- Cross-refs ---- const refsGovernance = (port.match(/GOVERNANCE\.md/g) || []).length; warn('PF', 'XR-gov', `Cross-references to GOVERNANCE.md: ${refsGovernance} (target: ≥5)`, `Found ${refsGovernance} references to GOVERNANCE.md`, 'PORTFOLIO.md should reference GOVERNANCE.md for project-level health/blockers'); const refsAgents = (port.match(/AGENTS\.md/g) || []).length; warn('PF', 'XR-ag', `Cross-references to AGENTS.md: ${refsAgents} (target: ≥2)`, `Found ${refsAgents} references to AGENTS.md`, 'PORTFOLIO.md should reference AGENTS.md for execution protocol'); // ---- Duplicate work scan ---- const projectDirs = findProjectDirs(root); warn('PF', 'DW-scan', `Potential duplicate work scan: ${projectDirs.length} project directories found`, projectDirs.length > 0 ? `Found ${projectDirs.length} project directories. Manual review recommended for duplicate patterns: ${projectDirs.join(', ')}` : 'No project directories found for duplicate work analysis', projectDirs.length >= 2 ? 'Check for duplicate code across project directories' : undefined); // ---- Portfolio Health Score (from available data) ---- checks.push({ id: 'PF-HS', name: 'Portfolio Health Score calculated', status: 'WARN', evidence: 'No active project data available for portfolio health calculation. Formula is defined in PORTFOLIO.md §6.', suggestion: 'Portfolio Health will be calculated after at least one project has a Health Score', }); // ---- Summary ---- const passCount = checks.filter(c => c.status === 'PASS').length; const warnCount = checks.filter(c => c.status === 'WARN').length; const failCount = checks.filter(c => c.status === 'FAIL').length; checks.push({ id: 'PF-SUMMARY', name: `Summary — ${passCount} PASS / ${warnCount} WARN / ${failCount} FAIL`, status: failCount > 0 ? 'FAIL' : warnCount > 0 ? 'WARN' : 'PASS', evidence: `${passCount} passed, ${warnCount} warnings, ${failCount} failures`, }); return checks; } /** Find all directories that look like projects (have progress.log or AGENTS.md ref) */ function findProjectDirs(root) { const candidates = []; try { const entries = fs.readdirSync(root, { withFileTypes: true }); for (const e of entries) { if (e.isDirectory() && !e.name.startsWith('.') && !['node_modules', 'scripts', 'memory', 'repos'].includes(e.name)) { // Check if this dir has project-like files const sub = path.join(root, e.name); const hasProgress = fs.existsSync(path.join(sub, 'progress.log')); const hasReadme = fs.existsSync(path.join(sub, 'README.md')); const hasPkg = fs.existsSync(path.join(sub, 'package.json')); if (hasProgress || hasReadme || hasPkg) { candidates.push(e.name); } } } } catch { /* skip */ } return candidates; } // ---- Standalone runner ---- if (SELF.endsWith('/audit-portfolio.mjs') || SELF.endsWith('\\audit-portfolio.mjs')) { const results = runPortfolioAudit(WORKSPACE); console.log(`\n=== PORTFOLIO.md Audit ===\n`); for (const r of results) { const icon = r.status === 'PASS' ? '✅' : r.status === 'WARN' ? '⚠️' : '❌'; console.log(` ${icon} [${r.id}] ${r.name}`); console.log(` ${r.evidence}`); if (r.suggestion) console.log(` → ${r.suggestion}`); console.log(); } }