Files
16gagent/scripts/audit-portfolio.mjs
2026-06-06 10:40:48 +08:00

340 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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(ROI<threshold), K4(Scope+P3)');
// ---- §10 Pause Rule ----
const hasPause = /§10 Project Pause/.test(port);
check('PF', '10', '§10 Project Pause Rule defined', hasPause,
'Found "§10 Project Pause Rule" with P1-P3 conditions and resume decision',
'Add §10 Project Pause Rule with P1(Priority), P2(Resources), P3(Dependency)');
// ---- §11 Promotion Rule ----
const hasPromotion = /§11 Project Promotion/.test(port);
check('PF', '11', '§11 Project Promotion Rule defined', hasPromotion,
'Found "§11 Project Promotion Rule" with F1(Health>90), 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();
}
}