348 lines
15 KiB
JavaScript
348 lines
15 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
/**
|
||
* audit-governance.mjs — GOVERNANCE.md Execution Governance Audit
|
||
*
|
||
* Checks:
|
||
* 1. All task states are legal (7-state machine)
|
||
* 2. State transitions are legal (no skipping)
|
||
* 3. Stale task detection rules exist
|
||
* 4. Blocker management (blocker.md template + schema)
|
||
* 5. Execution audit exists (execution-audit.md template)
|
||
* 6. Project Health Score formula defined
|
||
* 7. Auto-Stop Rule defined
|
||
* 8. Governance Quality Gate defined
|
||
*
|
||
* Usage:
|
||
* node scripts/audit-governance.mjs ← standalone
|
||
* import { runGovernanceAudit } from './audit-governance.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 runGovernanceAudit(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 GOVERNANCE.md` }),
|
||
});
|
||
}
|
||
|
||
function warn(prefix, id, name, evidence, suggestion) {
|
||
checks.push({ id: `${prefix}-${id}`, name, status: 'WARN', evidence, suggestion });
|
||
}
|
||
|
||
// ---- Read protocol file ----
|
||
const gov = readFile('GOVERNANCE.md');
|
||
if (!gov) {
|
||
checks.push({
|
||
id: 'GV-001',
|
||
name: 'GOVERNANCE.md exists',
|
||
status: 'FAIL',
|
||
evidence: 'GOVERNANCE.md not found in workspace root',
|
||
suggestion: 'Create GOVERNANCE.md with the Execution Governance Protocol',
|
||
});
|
||
return checks;
|
||
}
|
||
checks.push({
|
||
id: 'GV-001',
|
||
name: 'GOVERNANCE.md exists',
|
||
status: 'PASS',
|
||
evidence: `GOVERNANCE.md found (${gov.length} bytes)`,
|
||
});
|
||
|
||
// ---- §1 State Machine ----
|
||
const hasStateMachine = /§1 State Machine/.test(gov);
|
||
check('GV', '1', '§1 State Machine defined', hasStateMachine,
|
||
'Found "§1 State Machine" section',
|
||
'Add §1 State Machine section');
|
||
|
||
// 7 states
|
||
const states = ['NOT_STARTED', 'IN_PROGRESS', 'BLOCKED', 'WAITING_DEPENDENCY', 'READY_FOR_REVIEW', 'VERIFIED', 'FAILED', 'ARCHIVED'];
|
||
const foundStates = states.filter(s => gov.includes(s));
|
||
check('GV', '1a', `§1a ${states.length} states defined (found ${foundStates.length})`, foundStates.length >= states.length,
|
||
`All ${states.length} states present: ${foundStates.join(', ')}`,
|
||
`Missing states: ${states.filter(s => !gov.includes(s)).join(', ')}`,
|
||
'Add missing states to §1.1 Allowed States');
|
||
|
||
// Mermaid diagram
|
||
const hasMermaid = /stateDiagram-v2/.test(gov);
|
||
check('GV', '1b', '§1b State transition diagram (mermaid)', hasMermaid,
|
||
'Found mermaid stateDiagram-v2 with all legal transitions',
|
||
'Add a mermaid stateDiagram-v2 for the state machine');
|
||
|
||
// Transition table
|
||
const hasTransitionTable = /Transition Table/.test(gov) || /§1\.3/.test(gov);
|
||
check('GV', '1c', '§1c Transition table (From/To/Condition)', hasTransitionTable,
|
||
'Found Transition Table with From/To/Condition columns',
|
||
'Add Transition Table (From | To | Condition)');
|
||
|
||
// Prohibited transitions
|
||
const hasProhibited = /Prohibited Transitions/.test(gov) || /§1\.4/.test(gov);
|
||
check('GV', '1d', '§1d Prohibited transitions defined', hasProhibited,
|
||
'Found "Prohibited Transitions" with ❌ markers for illegal jumps',
|
||
'Add "Prohibited Transitions" with explicit ❌ transition violations');
|
||
|
||
// ---- §2 Heartbeat Rule ----
|
||
const hasHeartbeat = /§2 Heartbeat Rule/.test(gov);
|
||
check('GV', '2', '§2 Heartbeat Rule defined', hasHeartbeat,
|
||
'Found "§2 Heartbeat Rule" section',
|
||
'Add §2 Heartbeat Rule section');
|
||
|
||
// Heartbeat format defined
|
||
const hasHeartbeatFormat = /"ts".*"status".*"completed".*"remaining".*"next_action"/.test(gov);
|
||
check('GV', '2a', '§2a Heartbeat JSON format defined (ts/status/completed/remaining/next_action)',
|
||
hasHeartbeatFormat,
|
||
'Heartbeat JSON format found with ts, status, completed, remaining, next_action fields',
|
||
'Define heartbeat format as JSON with ts/status/completed/remaining/next_action');
|
||
|
||
// Stale thresholds
|
||
const hasStaleThresholds = /Small[\s\S]*3[00][\s\S]*Medium[\s\S]*2[\s\S]*Large[\s\S]*8/.test(gov);
|
||
check('GV', '2b', '§2b Stale thresholds defined (Small=30m, Medium=2h, Large=8h)',
|
||
hasStaleThresholds,
|
||
'Found stale thresholds: Small=30min/30分钟, Medium=2h/2小时, Large=8h/8小时',
|
||
'Add Stale thresholds: Small=30min, Medium=2h, Large=8h');
|
||
|
||
// progress.log template exists
|
||
const progressTmpl = readFile('scripts/templates/progress.log.template');
|
||
check('GV', '2c', 'progress.log.template exists', progressTmpl !== null,
|
||
'Found scripts/templates/progress.log.template',
|
||
'Create scripts/templates/progress.log.template');
|
||
|
||
// ---- §3 Blocker Management ----
|
||
const hasBlockerMgmt = /§3 Blocker Management/.test(gov);
|
||
check('GV', '3', '§3 Blocker Management defined', hasBlockerMgmt,
|
||
'Found "§3 Blocker Management" section',
|
||
'Add §3 Blocker Management section');
|
||
|
||
// Blocker schema
|
||
const hasBlockerSchema = /blocker[\s\S]*root_cause[\s\S]*owner[\s\S]*mitigation[\s\S]*dependency[\s\S]*created_at/.test(gov) || /protocol-reference\/templates\.md[\s\S]*blocker/.test(gov);
|
||
check('GV', '3a', '§3a Blocker schema (blocker/root_cause/owner/mitigation/dependency/created_at)',
|
||
hasBlockerSchema,
|
||
'Blocker schema found in GOVERNANCE.md or protocol-reference/templates.md',
|
||
'Define blocker md schema in protocol-reference/templates.md');
|
||
|
||
// Blocker escalation
|
||
const hasBlockerEsc = /Blocker Escalation/.test(gov) || /escalated/.test(gov);
|
||
check('GV', '3b', '§3b Blocker escalation defined', hasBlockerEsc,
|
||
'Found Blocker Escalation mechanism with escalated: YES/NO',
|
||
'Add Blocker Escalation: if escalated=YES, notify user and stop AP');
|
||
|
||
// blocker.md template exists
|
||
const blockerTmpl = readFile('scripts/templates/blocker.md.template');
|
||
check('GV', '3c', 'blocker.md.template exists', blockerTmpl !== null,
|
||
'Found scripts/templates/blocker.md.template',
|
||
'Create scripts/templates/blocker.md.template');
|
||
|
||
// ---- §4 Replanning Trigger ----
|
||
const hasReplan = /§4 Replanning Trigger/.test(gov);
|
||
check('GV', '4', '§4 Replanning Trigger defined', hasReplan,
|
||
'Found "§4 Replanning Trigger" with R1-R8 conditions',
|
||
'Add §4 Replanning Trigger with R1-R8 conditions');
|
||
|
||
// 8 trigger conditions
|
||
const triggerCount = (gov.match(/R\d /g) || []).length;
|
||
warn('GV', '4a', `§4a Replan triggers: ${triggerCount} (target: ≥8)`,
|
||
`Found ${triggerCount} replan trigger conditions (R1-R? )`,
|
||
'Ensure all 8 replan triggers (R1-R8) are defined');
|
||
|
||
// Replan log entry format
|
||
const hasReplanLog = /Replan.*#N/.test(gov) || /replan.*evidence/.test(gov);
|
||
check('GV', '4b', '§4b Replan log entry format defined', hasReplanLog,
|
||
'Found Replan log entry format with trigger/cause/before/after/impact',
|
||
'Define Replan log entry format (trigger, cause, before, after, impact)');
|
||
|
||
// ---- §5 Scope Explosion ----
|
||
const hasScope = /§5 Scope Explosion/.test(gov);
|
||
check('GV', '5', '§5 Scope Explosion Detector defined', hasScope,
|
||
'Found "§5 Scope Explosion Detector" with S1-S4 detection rules',
|
||
'Add §5 Scope Explosion Detector with S1-S4 detection rules');
|
||
|
||
// ---- §6 Execution Audit ----
|
||
const hasAudit = /§6 Execution Audit/.test(gov);
|
||
check('GV', '6', '§6 Execution Audit defined', hasAudit,
|
||
'Found "§6 Execution Audit" section',
|
||
'Add §6 Execution Audit section');
|
||
|
||
// execution-audit.md template exists
|
||
const auditTmpl = readFile('scripts/templates/execution-audit.md.template');
|
||
check('GV', '6a', 'execution-audit.md.template exists', auditTmpl !== null,
|
||
'Found scripts/templates/execution-audit.md.template',
|
||
'Create scripts/templates/execution-audit.md.template');
|
||
|
||
// ---- §7 Health Score ----
|
||
const hasHealth = /§7 Project Health Score/.test(gov);
|
||
check('GV', '7', '§7 Project Health Score defined', hasHealth,
|
||
'Found "§7 Project Health Score" with 5 dimensions (Delivery/Quality/Parallelism/Dependency/Learning)',
|
||
'Add §7 Project Health Score with 5 dimensions');
|
||
|
||
// All 5 sub-scores
|
||
const hasDelivery = /Delivery Score/.test(gov);
|
||
const hasQuality = /Quality Score/.test(gov);
|
||
const hasParallelism = /Parallelism Score/.test(gov);
|
||
const hasDependency = /Dependency Score/.test(gov);
|
||
const hasLearning = /Learning Score/.test(gov);
|
||
const all5 = hasDelivery && hasQuality && hasParallelism && hasDependency && hasLearning;
|
||
check('GV', '7a', '§7a All 5 Health Score dimensions defined (Delivery/Quality/Parallelism/Dependency/Learning)',
|
||
all5,
|
||
'All 5 dimensions found with formulas',
|
||
'Ensure all 5 dimensions: Delivery, Quality, Parallelism, Dependency, Learning');
|
||
|
||
// Weighted formula
|
||
const hasWeighted = /Health Score\s?=\s?Delivery\s*[×x]\s*0\.\d+/.test(gov);
|
||
check('GV', '7b', '§7b Weighted final formula defined', hasWeighted,
|
||
'Found weighted formula: Health Score = Delivery×0.30 + Quality×0.25 + Parallelism×0.15 + Dependency×0.15 + Learning×0.15',
|
||
'Define the weighted sum formula for final Health Score');
|
||
|
||
// Rating
|
||
const hasRating = /EXCELLENT[\s\S]*GOOD[\s\S]*ACCEPTABLE[\s\S]*AT RISK[\s\S]*CRITICAL/.test(gov) || /protocol-reference\/metrics\.md/.test(gov);
|
||
check('GV', '7c', '§7c Health Score rating defined (EXCELLENT/GOOD/ACCEPTABLE/AT RISK/CRITICAL)',
|
||
hasRating,
|
||
'Found 5-level rating in GOVERNANCE.md or protocol-reference/metrics.md',
|
||
'Add 5-level rating to protocol-reference/metrics.md');
|
||
|
||
// ---- §8 Auto-Stop ----
|
||
const hasAutoStop = /§8 Auto-Stop/.test(gov);
|
||
check('GV', '8', '§8 Auto-Stop Rule defined', hasAutoStop,
|
||
'Found "§8 Auto-Stop Rule" with A1-A3 conditions and RECOVERY MODE',
|
||
'Add §8 Auto-Stop Rule with A1(>3 failures), A2(Health<60), A3(Critical dep failed)');
|
||
|
||
// Recovery mode
|
||
const hasRecoveryMode = /RECOVERY MODE/.test(gov);
|
||
check('GV', '8a', '§8a RECOVERY MODE defined', hasRecoveryMode,
|
||
'Found RECOVERY MODE with recovery steps and user approval requirement',
|
||
'Add RECOVERY MODE procedure (stop, analyze, plan, user approval)');
|
||
|
||
// ---- §9 Quality Gate ----
|
||
const hasGovGate = /§9 Governance Quality Gate/.test(gov);
|
||
check('GV', '9', '§9 Governance Quality Gate defined', hasGovGate,
|
||
'Found "§9 Governance Quality Gate" with 8-item checklist',
|
||
'Add §9 Governance Quality Gate with 8 pre-output checks');
|
||
|
||
// ---- §10 State Initialization ----
|
||
const hasInit = /§10 Governance State Initialization/.test(gov);
|
||
check('GV', '10', '§10 Governance State Initialization defined', hasInit,
|
||
'Found "§10 Governance State Initialization" with project start/handoff/override',
|
||
'Add §10 Governance State Initialization section');
|
||
|
||
// ---- §11 Integration ----
|
||
const hasIntegration = /§11 Integration Summary/.test(gov);
|
||
check('GV', '11', '§11 Integration Summary defined', hasIntegration,
|
||
'Found "§11 Integration Summary" with raw diagram and lifecycle',
|
||
'Add §11 Integration Summary');
|
||
|
||
// ---- §11 Integration Summary (re-numbered — §12 was deleted) ----
|
||
const hasInt11 = /§11 Integration Summary/.test(gov);
|
||
check('GV', '11b', '§11 Integration Summary includes Portfolio ref', hasInt11,
|
||
'Found §11 Integration Summary (with governance→portfolio data flow)',
|
||
'Portfolio Integration reference is covered by §11 Integration Summary');
|
||
|
||
// ---- Runtime artifacts scan ----
|
||
// Scan for actual progress.log files
|
||
const progressLogs = findFiles(root, 'progress.log');
|
||
const blockerFiles = findFiles(root, 'blocker.md');
|
||
const execAudits = findFiles(root, 'execution-audit.md');
|
||
|
||
warn('GV', 'RA-progress', `Progress logs found: ${progressLogs.length}`,
|
||
progressLogs.length > 0
|
||
? `Found ${progressLogs.length} progress.log file(s): ${progressLogs.join(', ')}`
|
||
: 'No progress.log files found (expected during active project execution)',
|
||
progressLogs.length === 0 ? 'Active projects should have progress.log heartbeat entries' : undefined);
|
||
|
||
warn('GV', 'RA-blocker', `Blocker files found: ${blockerFiles.length}`,
|
||
blockerFiles.length > 0
|
||
? `Found ${blockerFiles.length} blocker.md file(s): ${blockerFiles.join(', ')}`
|
||
: 'No blocker.md files found',
|
||
blockerFiles.length === 0 ? '(no active blockers — good)' : undefined);
|
||
|
||
warn('GV', 'RA-audit', `Execution audits found: ${execAudits.length}`,
|
||
execAudits.length > 0
|
||
? `Found ${execAudits.length} execution-audit.md file(s): ${execAudits.join(', ')}`
|
||
: 'No execution-audit.md found (expected after first project execution)',
|
||
execAudits.length === 0 ? 'Run first project and generate execution-audit.md' : undefined);
|
||
|
||
// ---- Health Score calculation (from available data) ----
|
||
// Without actual project data, report N/A
|
||
checks.push({
|
||
id: 'GV-HS',
|
||
name: 'Project Health Score calculated',
|
||
status: 'WARN',
|
||
evidence: 'No active project data available for health score calculation. Formula is defined in GOVERNANCE.md §7.',
|
||
suggestion: 'Health Score will be calculated during/after project execution',
|
||
});
|
||
|
||
// ---- 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: 'GV-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;
|
||
}
|
||
|
||
/** Recursively find files by name (limited depth) */
|
||
function findFiles(dir, filename, depth = 3) {
|
||
const results = [];
|
||
if (depth <= 0) return results;
|
||
try {
|
||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||
for (const entry of entries) {
|
||
if (entry.name === filename) {
|
||
results.push(path.relative(WORKSPACE, path.join(dir, entry.name)));
|
||
}
|
||
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
||
results.push(...findFiles(path.join(dir, entry.name), filename, depth - 1));
|
||
}
|
||
}
|
||
} catch { /* skip */ }
|
||
return results;
|
||
}
|
||
|
||
// ---- Standalone runner ----
|
||
if (SELF.endsWith('/audit-governance.mjs') || SELF.endsWith('\\audit-governance.mjs')) {
|
||
const results = runGovernanceAudit(WORKSPACE);
|
||
console.log(`\n=== GOVERNANCE.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();
|
||
}
|
||
}
|