🎉 init: 小龙的工作空间

This commit is contained in:
大海
2026-06-06 10:40:48 +08:00
commit a188ee1426
3201 changed files with 231817 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+249
View File
@@ -0,0 +1,249 @@
#!/usr/bin/env node
/**
* audit-agents.mjs — AGENTS.md Execution Protocol Audit
*
* Checks:
* 1. Task Tree exists (§1)
* 2. Agent Package isolation requirements
* 3. No XL Package (context budget forbids it)
* 4. Injected Risks mechanism (§1e / §3b)
* 5. Quality Gate completeness (§5)
*
* Usage:
* node scripts/audit-agents.mjs ← standalone
* import { runAgentAudit } from './audit-agents.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 runAgentAudit(workspaceDir) {
const root = workspaceDir || WORKSPACE;
const checks = [];
// ---- Helper ----
function readAgents() {
const p = path.join(root, 'AGENTS.md');
if (!fs.existsSync(p)) return null;
return fs.readFileSync(p, 'utf8');
}
function check(prefix, id, name, condition, evidence, suggestion) {
checks.push({
id: `${prefix}-${id}`,
name,
status: condition ? 'PASS' : 'FAIL',
evidence: condition ? evidence : `File found but missing: ${evidence}`,
...(condition ? {} : { suggestion: suggestion || `Ensure §${id} is properly defined in AGENTS.md` }),
});
}
function warn(prefix, id, name, evidence, suggestion) {
checks.push({
id: `${prefix}-${id}`,
name,
status: 'WARN',
evidence,
suggestion,
});
}
// ---- Read AGENTS.md ----
const agents = readAgents();
if (!agents) {
checks.push({
id: 'AG-001',
name: 'AGENTS.md exists',
status: 'FAIL',
evidence: 'AGENTS.md not found in workspace root',
suggestion: 'Create AGENTS.md with the Execution Protocol',
});
return checks;
}
checks.push({
id: 'AG-001',
name: 'AGENTS.md exists',
status: 'PASS',
evidence: `AGENTS.md found (${agents.length} bytes)`,
});
// ---- §0 Complexity Classification ----
const hasClassify = /###\s*0\.\s*Complexity Classification/.test(agents);
check('AG', '0', '§0 Complexity Classification defined', hasClassify,
'Found "### 0. Complexity Classification" with Simple/Complex criteria',
'Add "### 0. Complexity Classification" with Simple→Direct / Complex→Task Tree decision rules');
// Detect if the "if uncertain → treat as Complex" rule exists
const hasUncertainRule = /if uncertain → treat as Complex/i.test(agents) || /if uncertain.*complex/i.test(agents);
check('AG', '0b', '§0 Uncertainty fallback ("if uncertain → treat as Complex")', hasUncertainRule,
'Uncertainty fallback rule found',
'Add decision rule: "if uncertain → treat as Complex"');
// ---- §1 Task Tree ----
const hasTaskTree = /###\s*1\.\s*Task Tree/.test(agents);
check('AG', '1', '§1 Task Tree Construction defined', hasTaskTree,
'Found "### 1. Task Tree Construction" with Task Tree definition',
'Add "### 1. Task Tree Construction" defining the execution tree');
// Task Tree Quality Gate (7 checks)
const hasTaskTreeGate = /Task Tree Quality Gate/.test(agents);
check('AG', '1f', '§1f Task Tree Quality Gate (7 checks)', hasTaskTreeGate,
'Found Task Tree Quality Gate with 7 verification items',
'Add §1f Quality Gate checklist (Root Task / No orphan / No circular / No XL / No unjust serial / Risks populated / No human dep)');
// ---- §2 PLAN ----
const hasPlan = /###\s*2\.\s*Planning Protocol/.test(agents);
check('AG', '2', '§2 Planning Protocol defined', hasPlan,
'Found "### 2. Planning Protocol" with Goal/Subtasks/Risks/Verify',
'Add "### 2. Planning Protocol" section');
// ---- §3 Pre-flight ----
const hasPreflight = /###\s*3\.\s*Pre-flight/.test(agents);
check('AG', '3', '§3 Pre-flight Check defined', hasPreflight,
'Found "### 3. Pre-flight Check" with Dependency Scan / Learning Context / Domain Checklists',
'Add "### 3. Pre-flight Check" section');
// 3a Dependency Scan
const hasDepScan = /####\s*3a\.\s*Dependency Scan/.test(agents);
check('AG', '3a', '§3a Dependency Scan defined', hasDepScan,
'Found §3a Dependency Scan',
'Add §3a Dependency Scan (files/tables, dependencies, blast radius)');
// 3b Learning Context Injector
const hasLearningCtx = /####\s*3b\.\s*Learning Context Injector/.test(agents);
check('AG', '3b', '§3b Learning Context Injector (3 searches)', hasLearningCtx,
'Found §3b Learning Context Injector with 3 mandatory searches',
'Add §3b with 3 searches: similar failures, best practices, playbooks');
// 3c Domain Checklists
const hasDomainChecklists = /####\s*3c\.\s*Domain Checklist Injection/.test(agents);
check('AG', '3c', '§3c Domain Checklists with ⚠️ Hard Rules', hasDomainChecklists,
'Found §3c Domain Checklist Injection with SQL/JS/Next.js/Backend/Generator/Release checklists',
'Add §3c with at least SQL, JS/TS, Next.js, Backend, Generator, Release checklists');
// Hard Rules exist
const hardRuleCount = (agents.match(/⚠️\s*HARD RULE/g) || []).length;
warn('AG', '3c-hard', `§3c Hard Rules count: ${hardRuleCount} (target: ≥5)`,
`Found ${hardRuleCount} ⚠️ HARD RULE markers`,
'Promote recurring patterns to Hard Rules. Target ≥5.');
// ---- §4 Failure Replanning ----
const hasReplan = /###\s*4\.\s*Failure Replanning Protocol/.test(agents);
check('AG', '4', '§4 Failure Replanning Protocol defined', hasReplan,
'Found "### 4. Failure Replanning Protocol" with local/structural replan logic',
'Add "### 4. Failure Replanning Protocol"');
// ---- §5 Verification Protocol ----
const hasVerification = /###\s*5\.\s*Verification Protocol/.test(agents);
check('AG', '5', '§5 Verification Protocol defined', hasVerification,
'Found "### 5. Verification Protocol" with Build/Tests/Lint/Regression/Manual checks',
'Add "### 5. Verification Protocol" with the verification block format');
// Quality Gate in Verification
const hasQualityGate = /Pre-flight Compliance Check/.test(agents) || /Quality Gate/.test(agents);
check('AG', '5b', '§5 Pre-flight Compliance / Quality Gate', hasQualityGate,
'Found "Pre-flight Compliance Check" with 5 mandatory gates',
'Add Quality Gate checklist within §5: PLAN has Injected Risks, ≥2 sources searched, domain checklist, reflection, 7-field reflection');
// ---- §6 Governance Compliance ----
const hasGovernance = /###\s*6\.\s*Governance Compliance/.test(agents);
check('AG', '6', '§6 Governance Compliance defined', hasGovernance,
'Found "### 6. Governance Compliance" with State Machine / Heartbeat / Quality Gate',
'Add "### 6. Governance Compliance" referencing GOVERNANCE.md');
// ---- §7 Auto-Capture ----
const hasAutoCapture = /###\s*7\.\s*Auto-Capture Trigger/.test(agents);
check('AG', '7', '§7 Auto-Capture Trigger defined', hasAutoCapture,
'Found "### 7. Auto-Capture Trigger" with T1-T7 trigger conditions',
'Add "### 7. Auto-Capture Trigger" with 7 trigger conditions and skip rules');
// ---- Learning Loop ----
const hasLearningLoop = /## Learning Loop/.test(agents);
check('AG', 'LL', 'Learning Loop (🔄) defined', hasLearningLoop,
'Found "## Learning Loop 🔄" with Loop Architecture / Promotion Rules / Reflection Quality Gate / Storage Locations',
'Add "## Learning Loop 🔄" section');
// Promotion Rules (1→2→3→4→5)
const hasPromotion = /Promotion Rules/.test(agents) && /1st.*Reflection/.test(agents) && /5th.*Hard Rule/.test(agents);
check('AG', 'LL-promo', 'Promotion Rules (1→2→3→4→5 appearances)', hasPromotion,
'Found Promotion Rules chart with 1st=Reflection through 5th=Hard Rule',
'Add Promotion Rules table mapping appearance count to promotion destination');
// ---- Context Budget ----
const hasBudget = /## Agent Package Context Budget/.test(agents);
check('AG', 'CB', 'Agent Package Context Budget defined', hasBudget,
'Found "## Agent Package Context Budget ⚠️ HARD RULE" with size→token map',
'Add "## Agent Package Context Budget" section');
// No XL
const forbidsXL = /XL.*FORBIDDEN/.test(agents) || /XL.*❌/.test(agents);
check('AG', 'CB-noxl', 'XL Package explicitly forbidden', forbidsXL,
'Found "XL | ❌ FORBIDDEN" in Context Budget table',
'Add "XL | ❌ FORBIDDEN" row to the Size→Token Budget Mapping table');
// Input Size Pre-check
const hasPrecheck = /Input Size Pre-check/.test(agents);
check('AG', 'CB-precheck', 'Input Size Pre-check (Context Budget Declaration)', hasPrecheck,
'Found "Input Size Pre-check" with Context Budget Declaration template',
'Add "Input Size Pre-check" section with file inventory / token estimate / budget check');
// ---- Cross-reference integrity ----
const refsGovernance = (agents.match(/GOVERNANCE\.md/g) || []).length;
warn('AG', 'XR-gov', `Cross-references to GOVERNANCE.md: ${refsGovernance} (target: ≥3)`,
`Found ${refsGovernance} references to GOVERNANCE.md`,
'Add cross-references from AGENTS.md to GOVERNANCE.md in §6');
const refsPortfolio = (agents.match(/PORTFOLIO\.md/g) || []).length;
warn('AG', 'XR-port', `Cross-references to PORTFOLIO.md: ${refsPortfolio} (target: ≥1)`,
`Found ${refsPortfolio} references to PORTFOLIO.md`,
'Add cross-reference to PORTFOLIO.md in §6e Reference section');
// ---- Injected Risks (target check) ----
const hasInjectedRisks = /Injected Risks/.test(agents);
check('AG', 'IR', 'Injected Risks mechanism defined', hasInjectedRisks,
'Found "### Injected Risks" / "### Per-Task Injected Risks"',
'Add "### Injected Risks (from Learning Context)" format in §1e and §3b');
// ---- 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: 'AG-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;
}
// ---- Standalone runner ----
if (SELF.endsWith('/audit-agents.mjs') || SELF.endsWith('\\audit-agents.mjs')) {
const results = runAgentAudit(WORKSPACE);
console.log(`\n=== AGENTS.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();
}
}
+197
View File
@@ -0,0 +1,197 @@
#!/usr/bin/env node
/**
* audit-all.mjs — Three-Layer Protocol Audit Orchestrator
*
* Runs all three protocol audits sequentially:
* 1. AGENTS.md — Execution Protocol
* 2. GOVERNANCE.md — Execution Governance
* 3. PORTFOLIO.md — Portfolio Management
*
* Outputs:
* - Console: colorized summary with PASS / WARN / FAIL
* - File: audit-report.md (generated in workspace root)
*
* Usage:
* node scripts/audit-all.mjs
*
* npm script (add to package.json):
* "audit": "node scripts/audit-all.mjs"
*/
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, '..');
// Dynamic imports for the three audit modules
const [agentsMod, govMod, portMod] = await Promise.all([
import(path.join(__dirname, 'audit-agents.mjs')),
import(path.join(__dirname, 'audit-governance.mjs')),
import(path.join(__dirname, 'audit-portfolio.mjs')),
]);
/** @returns {{ layer: string, checks: import('./audit-agents.mjs').CheckResult[] }[]} */
function runAll() {
const layers = [
{ layer: 'Execution Protocol (AGENTS.md)', fn: () => agentsMod.runAgentAudit(WORKSPACE) },
{ layer: 'Execution Governance (GOVERNANCE.md)', fn: () => govMod.runGovernanceAudit(WORKSPACE) },
{ layer: 'Portfolio Management (PORTFOLIO.md)', fn: () => portMod.runPortfolioAudit(WORKSPACE) },
];
return layers.map(({ layer, fn }) => ({ layer, checks: fn() }));
}
/** Colorized console output */
function printConsole(results) {
let totalPass = 0, totalWarn = 0, totalFail = 0, totalChecks = 0;
for (const { layer, checks } of results) {
// Skip summary rows for individual counts
const realChecks = checks.filter(c => !c.id.endsWith('-SUMMARY'));
const pass = realChecks.filter(c => c.status === 'PASS').length;
const warn = realChecks.filter(c => c.status === 'WARN').length;
const fail = realChecks.filter(c => c.status === 'FAIL').length;
totalPass += pass;
totalWarn += warn;
totalFail += fail;
totalChecks += realChecks.length;
const overall = fail > 0 ? '\x1b[31mFAIL\x1b[0m' : warn > 0 ? '\x1b[33mWARN\x1b[0m' : '\x1b[32mPASS\x1b[0m';
console.log(`\n\x1b[1m${'='.repeat(60)}\x1b[0m`);
console.log(`\x1b[1m ${layer}\x1b[0m ${overall}`);
console.log(`\x1b[1m${'='.repeat(60)}\x1b[0m`);
for (const r of checks) {
if (r.id.endsWith('-SUMMARY')) continue;
const icon = r.status === 'PASS' ? '\x1b[32m✅\x1b[0m' : r.status === 'WARN' ? '\x1b[33m⚠️\x1b[0m' : '\x1b[31m❌\x1b[0m';
const statusTag = r.status === 'PASS' ? '\x1b[32mPASS\x1b[0m' : r.status === 'WARN' ? '\x1b[33mWARN\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
console.log(` ${icon} [${statusTag}] [${r.id}] ${r.name}`);
console.log(` ${r.evidence}`);
if (r.suggestion) console.log(` \x1b[90m→ ${r.suggestion}\x1b[0m`);
console.log();
}
}
// Final summary
console.log(`\x1b[1m${'='.repeat(60)}\x1b[0m`);
console.log(`\x1b[1m THREE-LAYER AUDIT COMPLETE\x1b[0m`);
console.log(`\x1b[1m${'='.repeat(60)}\x1b[0m`);
console.log(` Total checks: ${totalChecks}`);
console.log(` \x1b[32mPASS: ${totalPass}\x1b[0m`);
console.log(` \x1b[33mWARN: ${totalWarn}\x1b[0m`);
console.log(` \x1b[31mFAIL: ${totalFail}\x1b[0m`);
const verdict = totalFail > 0 ? '\x1b[31mFAIL' : totalWarn > 0 ? '\x1b[33mWARN' : '\x1b[32mPASS';
console.log(`\n VERDICT: ${verdict}\x1b[0m`);
console.log();
}
/** Generate audit-report.md */
function writeReport(results) {
const reportPath = path.join(WORKSPACE, 'audit-report.md');
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' +08:00';
let md = `# Audit Report — ${timestamp}\n\n`;
md += `> Generated by \`scripts/audit-all.mjs\`\n\n`;
md += `## Global Summary\n\n`;
let totalPass = 0, totalWarn = 0, totalFail = 0, totalChecks = 0;
// First pass: aggregate all checks across layers
const allChecksFlat = [];
for (const { layer, checks } of results) {
for (const r of checks) {
if (r.id.endsWith('-SUMMARY')) continue;
allChecksFlat.push({ ...r, layer });
if (r.status === 'PASS') totalPass++;
else if (r.status === 'WARN') totalWarn++;
else if (r.status === 'FAIL') totalFail++;
totalChecks++;
}
}
md += `| Metric | Value |\n|--------|-------|\n`;
md += `| Total Checks | ${totalChecks} |\n`;
md += `| ✅ PASS | ${totalPass} |\n`;
md += `| ⚠️ WARN | ${totalWarn} |\n`;
md += `| ❌ FAIL | ${totalFail} |\n`;
md += `| **Verdict** | **${totalFail > 0 ? '❌ FAIL' : totalWarn > 0 ? '⚠️ WARN' : '✅ PASS'}** |\n\n`;
const verdictOverall = totalFail > 0 ? 'FAIL' : totalWarn > 0 ? 'WARN' : 'PASS';
md += `**Verdict: ${verdictOverall}**\n\n`;
// Layer-by-layer tables
for (const { layer, checks } of results) {
const real = checks.filter(c => !c.id.endsWith('-SUMMARY'));
const lp = real.filter(c => c.status === 'PASS').length;
const lw = real.filter(c => c.status === 'WARN').length;
const lf = real.filter(c => c.status === 'FAIL').length;
const lv = lf > 0 ? '❌ FAIL' : lw > 0 ? '⚠️ WARN' : '✅ PASS';
md += `## ${layer}${lv}\n\n`;
md += `| ID | Name | Status | Evidence | Suggestion |\n`;
md += `|----|------|--------|----------|------------|\n`;
for (const r of real) {
const icon = r.status === 'PASS' ? '✅' : r.status === 'WARN' ? '⚠️' : '❌';
const sug = r.suggestion ? r.suggestion.replace(/\|/g, '\\|') : '';
const ev = r.evidence.replace(/\|/g, '\\|').replace(/\n/g, ' ').slice(0, 120);
md += `| ${r.id} | ${r.name} | ${icon} ${r.status} | ${ev} | ${sug} |\n`;
}
md += `| **Layer Summary** | **${lp} PASS / ${lw} WARN / ${lf} FAIL** | **${lv}** | | |\n\n`;
}
// Fails list
const fails = allChecksFlat.filter(c => c.status === 'FAIL');
if (fails.length > 0) {
md += `## ❌ Failures (repair required)\n\n`;
for (const f of fails) {
md += `- **[${f.id}]** ${f.name}${f.evidence}\n`;
if (f.suggestion) md += ` - *Repair:* ${f.suggestion}\n`;
}
md += '\n';
}
// Warnings list
const warns = allChecksFlat.filter(c => c.status === 'WARN');
if (warns.length > 0) {
md += `## ⚠️ Warnings (improvement recommended)\n\n`;
for (const w of warns) {
md += `- **[${w.id}]** ${w.name}${w.evidence}\n`;
if (w.suggestion) md += ` - *Suggestion:* ${w.suggestion}\n`;
}
md += '\n';
}
// Coverage summary
md += `## Coverage\n\n`;
md += `| Layer | Sections | Checks |\n|-------|----------|--------|\n`;
const agentsReal = results[0].checks.filter(c => !c.id.endsWith('-SUMMARY'));
const govReal = results[1].checks.filter(c => !c.id.endsWith('-SUMMARY'));
const portReal = results[2].checks.filter(c => !c.id.endsWith('-SUMMARY'));
md += `| AGENTS.md | §0-§7, Learning Loop, Context Budget | ${agentsReal.length} |\n`;
md += `| GOVERNANCE.md | §1-§12 | ${govReal.length} |\n`;
md += `| PORTFOLIO.md | §1-§14 | ${portReal.length} |\n`;
md += `| **Total** | **3 layers** | **${totalChecks}** |\n\n`;
md += `---\n*Audit complete. ${timestamp}*\n`;
fs.writeFileSync(reportPath, md, 'utf8');
console.log(`\n 📄 Report written to: audit-report.md`);
return reportPath;
}
// ---- Main ----
console.log(`\x1b[1m\x1b[36m`);
console.log(` ╔══════════════════════════════════════════════════╗`);
console.log(` ║ THREE-LAYER PROTOCOL AUDIT ║`);
console.log(` ║ AGENTS.md · GOVERNANCE.md · PORTFOLIO.md ║`);
console.log(` ╚══════════════════════════════════════════════════╝`);
console.log(`\x1b[0m`);
const results = runAll();
printConsole(results);
writeReport(results);
+347
View File
@@ -0,0 +1,347 @@
#!/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[0][\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();
}
}
+339
View File
@@ -0,0 +1,339 @@
#!/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();
}
}
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# auto-checkpoint.sh — 会话 checkpointcompact 前自动调用)
# 把当前会话的关键信息刷出对话历史,减少上下文膨胀带来的 token 开销
WORKSPACE="$(cd "$(dirname "$0")/.." && pwd)"
MEMORY_SERVER="http://111.229.145.18"
DAILY="$WORKSPACE/memory/daily/$(date +%Y-%m-%d).md"
NOW=$(date +%H:%M)
# 生成一条会话摘要(从 daily 最后一段提取)
SUMMARY=$(tail -20 "$DAILY" 2>/dev/null | grep '^\[' | tail -1 | sed 's/^\[//;s/\]//;s/^[^:]*://' | head -c 300)
# 如果 daily 没有合适的摘要,手动给一个
if [ -z "$SUMMARY" ]; then
SUMMARY="[$NOW] 会话 checkpoint — 上下文已 compact"
fi
# 写进 daily
echo "" >> "$DAILY"
echo "[checkpoint $NOW] $SUMMARY" >> "$DAILY"
# 推送远程记忆
curl -s --max-time 5 -X POST "$MEMORY_SERVER/api/v2/add" \
-H "Content-Type: application/json" \
-H "X-API-Key: xl_134…220f" \
-d "{\"content\":\"$SUMMARY\",\"type\":\"checkpoint\",\"project\":\"xiaolong\",\"source\":\"auto-checkpoint\"}" > /dev/null 2>&1
echo "✅ Checkpoint done: $SUMMARY"
File diff suppressed because it is too large Load Diff
+472
View File
@@ -0,0 +1,472 @@
#!/usr/bin/env node
/**
* PR-C Deprecation Check Script
* Scans OpenClaw config and plugin state for deprecated modules.
*
* Run: node scripts/check-deprecations.mjs
*
* Checks:
* 1. Active Memory blocking mode
* 2. Dreaming REM phase enabled
* 3. Dream Diary generation
* 4. QMD search mode / QMD engine references
* 5. Honcho memory plugin enabled
* 6. LanceDB memory plugin enabled
* 7. memory-wiki plugin loaded
* 8. Commitments auto-infer
* 9. Grounded Backfill / REM Backfill CLI usage
*/
import { readFileSync, existsSync, readdirSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { warnOnce, warnMany } from "./lib/deprecation-warning.mjs";
const AGENTS_HOME = join(homedir(), ".openclaw");
const CONFIG_PATH = join(AGENTS_HOME, "openclaw.json");
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || "/opt/homebrew/lib/node_modules/openclaw";
const EXTENSIONS_DIR = join(OPENCLAW_HOME, "dist", "extensions");
// Load user config
function loadConfig() {
if (!existsSync(CONFIG_PATH)) return null;
try {
return JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
} catch {
return null;
}
}
// Load plugin entry for a given extension id
function loadPlugin(id) {
const pluginJsonPath = join(EXTENSIONS_DIR, id, "openclaw.plugin.json");
if (!existsSync(pluginJsonPath)) return null;
try {
return JSON.parse(readFileSync(pluginJsonPath, "utf8"));
} catch {
return null;
}
}
// Check if a plugin is enabled in user config
function isPluginEnabled(config, pluginId) {
const entries = config?.plugins?.entries;
if (!entries) return false;
const entry = entries[pluginId];
if (!entry) return false;
return entry.enabled !== false;
}
// ─── Check Functions ────────────────────────────────
function checkActiveMemoryBlocking(config) {
const warnings = [];
const entry = config?.plugins?.entries?.["active-memory"];
if (!entry) return warnings;
const mode = entry.config?.mode;
if (mode === "blocking") {
warnings.push({
key: "active-memory.blocking",
message: "active-memory blocking mode is deprecated. Set mode to 'precompute' instead."
});
}
return warnings;
}
function checkDreamingRemPhase(config) {
const warnings = [];
const entry = config?.plugins?.entries?.["memory-core"];
if (!entry) return warnings;
const dreaming = entry.config?.dreaming;
if (!dreaming) return warnings;
// Check REM phase enabled
const remEnabled = dreaming?.phases?.rem?.enabled;
if (remEnabled === true) {
warnings.push({
key: "dreaming.rem-phase",
message: "Dreaming REM phase is deprecated. It will be replaced by Collect/Promote."
});
}
return warnings;
}
function checkDreamDiary(config) {
const warnings = [];
const entry = config?.plugins?.entries?.["memory-core"];
if (!entry) return warnings;
const dreaming = entry.config?.dreaming;
if (!dreaming) return warnings;
// Dream Diary is implied by REM phase or certain storage modes
const storage = dreaming?.storage;
const hasDiaryMode = storage?.mode === "separate" || storage?.mode === "both";
const separateReports = storage?.separateReports === true;
if (hasDiaryMode || separateReports) {
warnings.push({
key: "dreaming.dream-diary",
message: "Dream Diary generation is deprecated and will be removed."
});
}
// Also check if dreaming has a configured model (used for diary)
if (dreaming?.model) {
warnings.push({
key: "dreaming.diary-model",
message: "Dreaming diary model override is deprecated."
});
}
return warnings;
}
function checkQmdReference(config) {
const warnings = [];
// Check memory search config for QMD
const memorySearch = config?.agents?.defaults?.memorySearch;
if (memorySearch?.provider === "qmd" || memorySearch?.backend === "qmd") {
warnings.push({
key: "qmd.memory-search",
message: "QMD memory engine is legacy. Migrate to Builtin MemoryCore."
});
}
// Check active-memory QMD reference
const activeCfg = config?.plugins?.entries?.["active-memory"]?.config;
if (activeCfg?.qmd?.searchMode) {
warnings.push({
key: "qmd.active-memory",
message: "QMD search mode in active-memory is legacy. Remove qmd config."
});
}
return warnings;
}
function checkHonchoLanceDB(config) {
const warnings = [];
// Check if honcho or lancedb plugins are loaded
const pluginEntries = config?.plugins?.entries || {};
if (pluginEntries["memory-honcho"]?.enabled !== false && isPluginInExtensions("honcho")) {
warnings.push({
key: "honcho.plugin",
message: "Honcho memory plugin is legacy. Existing data readable during migration."
});
}
if (pluginEntries["memory-lancedb"]?.enabled !== false && isPluginInExtensions("lancedb")) {
warnings.push({
key: "lancedb.plugin",
message: "LanceDB memory plugin is legacy. Existing data readable during migration."
});
}
return warnings;
}
function checkMemoryWiki(config) {
const warnings = [];
const entry = config?.plugins?.entries?.["memory-wiki"];
if (entry?.enabled !== false) {
// Check if memory-wiki extension exists and isn't disabled
const wikiPlugin = loadPlugin("memory-wiki");
if (wikiPlugin) {
warnings.push({
key: "memory-wiki.plugin",
message: "memory-wiki plugin is legacy. wiki_search/wiki_get/wiki_apply/wiki_lint/wiki_status remain available during migration."
});
}
}
return warnings;
}
function checkCommitmentsAutoInfer(config) {
const warnings = [];
// Commitments auto-infer is part of the session-memory bundled module
const commitments = config?.commitments;
if (commitments?.autoInfer === true) {
warnings.push({
key: "commitments.auto-infer",
message: "Commitments auto-infer is deprecated. Use explicit cron tasks instead."
});
}
return warnings;
}
function checkGroundedBackfill(config) {
const warnings = [];
// Grounded Backfill is a CLI feature — we check if related config exists
const dreaming = config?.plugins?.entries?.["memory-core"]?.config?.dreaming;
if (!dreaming) return warnings;
// If dreaming has unusual storage paths that suggest backfill usage
const storage = dreaming?.storage;
if (storage?.mode === "separate") {
warnings.push({
key: "backfill.storage-mode",
message: "Grounded Backfill / REM Backfill is deprecated and will not evolve further."
});
}
return warnings;
}
// ─── Helpers ────────────────────────────────────────
function isPluginInExtensions(name) {
try {
const dirs = readdirSync(EXTENSIONS_DIR, { withFileTypes: true })
.filter(d => d.isDirectory());
for (const d of dirs) {
if (d.name.includes(name)) return true;
// Check plugin.json
const pluginPath = join(EXTENSIONS_DIR, d.name, "openclaw.plugin.json");
if (existsSync(pluginPath)) {
const plugin = JSON.parse(readFileSync(pluginPath, "utf8"));
if (plugin.id?.includes(name)) return true;
}
}
} catch {}
return false;
}
// ─── Legacy Registry (PR-1 Freeze) ───────────────────
// Maps deprecation keys to replacement info and migration window.
// Used to enhance check-deprecations output.
const LEGACY_REGISTRY = {
"active-memory.blocking": {
module: "active-memory blocking mode",
status: "Active. Deprecated.",
replacement: 'Set mode to "precompute" in active-memory config',
migrationWindow: "90 days from 2026-06-04",
removalTarget: "2026-09-04",
},
"dreaming.rem-phase": {
module: "Dreaming REM phase",
status: "Active. Deprecated.",
replacement: "Collect/Promote two-phase dreaming (PR-5)",
migrationWindow: "90 days from 2026-06-04",
removalTarget: "2026-09-04",
},
"dreaming.dream-diary": {
module: "Dream Diary",
status: "Active. Deprecated.",
replacement: "Inline dreaming output (no separate diary storage)",
migrationWindow: "90 days from 2026-06-04",
removalTarget: "2026-09-04",
},
"dreaming.diary-model": {
module: "Dream Diary model override",
status: "Active. Deprecated.",
replacement: "Use default dreaming model",
migrationWindow: "90 days from 2026-06-04",
removalTarget: "2026-09-04",
},
"qmd.memory-search": {
module: "QMD memory search engine",
status: "Active. Deprecated.",
replacement: "Builtin MemoryCore FTS5 + BM25",
migrationWindow: "90 days from 2026-06-04",
removalTarget: "2026-09-04",
},
"qmd.active-memory": {
module: "QMD active-memory search mode",
status: "Active. Deprecated.",
replacement: "Remove qmd config block from active-memory",
migrationWindow: "90 days from 2026-06-04",
removalTarget: "2026-09-04",
},
"honcho.plugin": {
module: "Honcho memory plugin",
status: "Legacy. Data readable during migration.",
replacement: "memory-core (FTS5 + BM25)",
migrationWindow: "60 days from 2026-06-04",
removalTarget: "2026-08-04",
},
"lancedb.plugin": {
module: "LanceDB memory plugin",
status: "Legacy. Data readable during migration.",
replacement: "memory-core (FTS5 + BM25)",
migrationWindow: "60 days from 2026-06-04",
removalTarget: "2026-08-04",
},
"memory-wiki.plugin": {
module: "memory-wiki plugin",
status: "Active. Deprecated.",
replacement: "memory-core wiki compilation",
migrationWindow: "90 days from 2026-06-04",
removalTarget: "2026-09-04",
},
"commitments.auto-infer": {
module: "Commitments auto-infer",
status: "Active. Deprecated.",
replacement: "Explicit cron tasks (use cron tool)",
migrationWindow: "90 days from 2026-06-04",
removalTarget: "2026-09-04",
},
"backfill.storage-mode": {
module: "Grounded Backfill / REM Backfill CLI",
status: "Active. Deprecated.",
replacement: "No replacement. Stop using immediately.",
migrationWindow: "Immediate — effective 2026-06-04",
removalTarget: "2026-06-04",
},
};
function getLegacyInfo(key) {
return LEGACY_REGISTRY[key] || null;
}
function formatLegacyTable(warnings) {
// Group warnings by their legacy info
const seen = new Set();
const rows = [];
for (const w of warnings) {
if (seen.has(w.key)) continue;
seen.add(w.key);
const info = LEGACY_REGISTRY[w.key] || {
module: w.key,
status: "Unknown",
replacement: "N/A",
migrationWindow: "N/A",
removalTarget: "N/A",
};
rows.push(info);
}
return rows;
}
// ─── Main ────────────────────────────────────────────
function runAllChecks() {
const config = loadConfig();
const allWarnings = [];
const checkResults = [];
if (!config) {
console.warn("[DEPRECATED][OpenClaw v2] config: Could not load openclaw.json");
return { warnings: [], results: [{ name: "config", error: "Could not load config" }] };
}
const checks = [
{ name: "active-memory blocking mode", fn: () => checkActiveMemoryBlocking(config) },
{ name: "dreaming REM phase", fn: () => checkDreamingRemPhase(config) },
{ name: "dream diary", fn: () => checkDreamDiary(config) },
{ name: "QMD reference", fn: () => checkQmdReference(config) },
{ name: "Honcho / LanceDB", fn: () => checkHonchoLanceDB(config) },
{ name: "memory-wiki", fn: () => checkMemoryWiki(config) },
{ name: "commitments auto-infer", fn: () => checkCommitmentsAutoInfer(config) },
{ name: "grounded backfill", fn: () => checkGroundedBackfill(config) },
];
console.log("");
console.log("═══════════════════════════════════════");
console.log(" PR-C: Deprecation Warning Check");
console.log(` Architecture Freeze: 2026-06-04`);
console.log(` ${new Date().toISOString()}`);
console.log("═══════════════════════════════════════");
console.log("");
for (const check of checks) {
try {
const warnings = check.fn();
checkResults.push({ name: check.name, warnings, passed: warnings.length === 0 });
allWarnings.push(...warnings);
} catch (e) {
checkResults.push({ name: check.name, warnings: [], passed: false, error: e.message });
}
}
// Emit warnings (deduplicated)
if (allWarnings.length > 0) {
console.log(" ⚠ Deprecation warnings found:\n");
for (const w of allWarnings) {
warnOnce(w.key, w.message);
}
} else {
console.log(" ✓ No deprecation warnings found.");
}
// Summary
console.log("");
const checksWithWarnings = checkResults.filter(c => c.warnings.length > 0);
const checksFailed = checkResults.filter(c => c.error);
console.log(` Checks run: ${checks.length}`);
console.log(` With warnings: ${checksWithWarnings.length}`);
console.log(` Total warnings: ${allWarnings.length}`);
if (checksFailed.length > 0) {
console.log(` Check errors: ${checksFailed.length}`);
for (const c of checksFailed) {
console.log(`${c.name}: ${c.error}`);
}
}
// ─── PR-1 Enhanced Output: Legacy Table ─────────────
if (allWarnings.length > 0) {
const legacyRows = formatLegacyTable(allWarnings);
console.log("");
console.log(" ─────────────────────────────────────────────");
console.log(" Deprecated Modules (PR-1 Legacy Inventory):");
console.log(" ─────────────────────────────────────────────");
console.log("");
for (const row of legacyRows) {
console.log(` Module: ${row.module}`);
console.log(` Status: ${row.status}`);
console.log(` Replacement: ${row.replacement}`);
console.log(` Migration Window: ${row.migrationWindow}`);
console.log(` Removal Target: ${row.removalTarget}`);
console.log("");
}
// Migration timeline
console.log(" ─────────────────────────────────────────────");
console.log(" Migration Timeline:");
console.log("");
console.log(" Day 0 2026-06-04 Freeze active");
console.log(" Day 30 2026-07-04 Legacy warnings active");
console.log(" Day 60 2026-08-04 Honcho/LanceDB read-only cutoff");
console.log(" Day 90 2026-09-04 All legacy modules removed");
console.log("");
// Per-module key list for scripting
console.log(" Deprecated keys:");
for (const w of allWarnings) {
const info = LEGACY_REGISTRY[w.key];
const target = info ? info.removalTarget : "N/A";
console.log(`${w.key} (removal: ${target})`);
}
}
console.log("");
return { warnings: allWarnings, results: checkResults };
}
// Run if called as main
if (process.argv[1]?.includes("check-deprecations")) {
const result = runAllChecks();
process.exit(0); // Never fail on deprecation warnings
}
export { runAllChecks, loadConfig, LEGACY_REGISTRY, getLegacyInfo, formatLegacyTable,
checkActiveMemoryBlocking, checkDreamingRemPhase, checkDreamDiary,
checkQmdReference, checkHonchoLanceDB, checkMemoryWiki,
checkCommitmentsAutoInfer, checkGroundedBackfill };
+776
View File
@@ -0,0 +1,776 @@
#!/usr/bin/env node
/**
* Contract Consistency Test
*
* 验证一个由 Pipeline 生成的项目中:
* 1. Schema 字段 100% 匹配 Contract 定义
* 2. Types 字段名(camelCase100% 匹配 Contract 的 fieldMapping
* 3. Routes 的 DTO 类型 100% 匹配 Contract
* 4. Services 的 SQL 列名 100% 匹配 Contract
* 5. Tests 使用的字段名 100% 匹配 Contract
*
* 验证失败则 exit(1),成功 exit(0)。
* 输出 contract-consistency-report.md。
*
* Usage:
* node scripts/contract-consistency-test.mjs --project <dir> [--prd <path>] [--arch <path>]
*
* Options:
* --project <dir> 项目根目录
* --prd <path> PRD JSON 文件路径(用于生成 Contract)
* --arch <path> Architecture JSON 文件路径(用于生成 Contract)
* --help 显示帮助
*/
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync, mkdirSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { createContract, toCamel, toSnake } from "./model-contract.mjs";
const __dirname = dirname(resolve(fileURLToPath(import.meta.url)));
const WORKSPACE = resolve(__dirname, "..");
// ═══════════════════════════════════════════════════════
// 1. Test Infrastructure
// ═══════════════════════════════════════════════════════
class ContractConsistencyTest {
constructor(contract) {
this.contract = contract;
this.failures = [];
this.warnings = [];
this.checks = { total: 0, passed: 0, failed: 0 };
}
fail(section, message) {
this.failures.push({ section, message });
this.checks.failed++;
this.checks.total++;
}
pass() {
this.checks.passed++;
this.checks.total++;
}
/**
* Recursively find files matching a glob in a directory.
*/
findFiles(dir, pattern) {
const results = [];
if (!existsSync(dir)) return results;
try {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = resolve(dir, entry.name);
if (entry.isDirectory()) {
// Skip node_modules and dist
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".next" || entry.name === "data") continue;
results.push(...this.findFiles(fullPath, pattern));
} else if (pattern.test(entry.name)) {
results.push(fullPath);
}
}
} catch { /* skip */ }
return results;
}
}
// ═══════════════════════════════════════════════════════
// 2. Check 1: Schema → Contract
// ═══════════════════════════════════════════════════════
function checkSchemaConsistency(test) {
const { contract } = test;
const entities = contract.entities;
console.log("── Check 1: Database Schema ↔ Contract ──");
// Find schema files
const projectDir = process.env.PROJECT_DIR || ".";
const schemaFiles = [
...test.findFiles(resolve(projectDir, "backend/src/db"), /schema\.ts$/),
...test.findFiles(resolve(projectDir, "src/db"), /schema\.ts$/),
...test.findFiles(resolve(projectDir, "apps/api/src/db"), /schema\.ts$/),
...test.findFiles(resolve(projectDir, "fullstack/apps/api/src/db"), /schema\.ts$/),
...test.findFiles(projectDir, /schema\.prisma$/),
];
if (schemaFiles.length === 0) {
test.fail("Schema", "No schema file found (src/db/schema.ts or schema.prisma)");
return;
}
for (const schemaFile of schemaFiles) {
console.log(` Schema file: ${schemaFile}`);
let content;
try {
content = readFileSync(schemaFile, "utf-8");
} catch (e) {
test.fail("Schema", `Cannot read ${schemaFile}: ${e.message}`);
continue;
}
for (const entity of entities) {
console.log(` Checking entity: ${entity.name} (${entity.table})`);
// Verify the table exists in the schema
const tableRegex = new RegExp(`CREATE TABLE IF NOT EXISTS ${entity.table} \\(([\\s\\S]+?)\\);`, "si");
const tableMatch = content.match(tableRegex);
if (!tableMatch) {
// Try without CREATE TABLE (for Prisma/simpler formats)
const altRegex = new RegExp(`(?:CREATE TABLE|model)\\s+(?:IF NOT EXISTS\\s+)?['"]?${entity.table}['"]?`, "i");
if (!altRegex.test(content)) {
test.warnings.push({ section: "Schema", message: `Table '${entity.table}' not found in schema` });
continue;
}
}
// Extract column definitions
let columns;
if (tableMatch) {
const body = tableMatch[1];
columns = body
.split(",")
.map(c => c.trim())
.filter(c => c.length > 0 && !c.startsWith("--"))
.map(c => {
// Handle DEFAULT (...) expressions with nested parens
const cleanCol = c.replace(/DEFAULT\s*\([^)]*\)/gi, "").replace(/^\\n\s*/, "").trim();
const parts = cleanCol.split(/\s+/);
return { name: parts[0], raw: c };
});
} else {
columns = [];
}
const columnNames = new Set(columns.map(c => c.name));
for (const field of entity.fields) {
test.pass();
if (field.isSecret) continue; // Secret fields may not be in output types
if (!columnNames.has(field.name)) {
test.fail(
`Schema/${entity.table}`,
`Field '${field.name}' defined in Contract but MISSING from schema table '${entity.table}'`
);
}
}
// Check for extra columns not in contract (warn only)
for (const col of columns) {
const hasField = entity.fields.some(f => f.name === col.name);
if (!hasField && !col.raw.toUpperCase().includes("FOREIGN KEY") &&
!col.raw.toUpperCase().includes("PRIMARY KEY") &&
!col.raw.toUpperCase().includes("CONSTRAINT") &&
!col.raw.toUpperCase().includes("UNIQUE(") &&
!col.raw.toUpperCase().includes("CHECK(") &&
col.name !== "" && !col.raw.startsWith(")")) {
test.warnings.push({
section: `Schema/${entity.table}`,
message: `Column '${col.name}' found in schema but NOT in Contract for table '${entity.table}'`
});
}
}
}
}
console.log(` Schema checks: ${test.checks.passed - (test.checks.passed > 0 ? 0 : 0)} passed, ${test.checks.failed} failures`);
}
// ═══════════════════════════════════════════════════════
// 3. Check 2: Types → Contract
// ═══════════════════════════════════════════════════════
function checkTypesConsistency(test) {
const { contract } = test;
const projectDir = process.env.PROJECT_DIR || ".";
console.log("── Check 2: TypeScript Types ↔ Contract ──");
const typeFiles = [
...test.findFiles(resolve(projectDir, "backend/src/types"), /\.ts$/),
...test.findFiles(resolve(projectDir, "src/types"), /\.ts$/),
...test.findFiles(resolve(projectDir, "apps/api/src/types"), /\.ts$/),
...test.findFiles(resolve(projectDir, "apps/api/packages/shared-types"), /\.ts$/),
...test.findFiles(resolve(projectDir, "packages/shared-types"), /\.ts$/),
].filter(f => !f.endsWith(".d.ts") || f.endsWith("fastify.d.ts"));
if (typeFiles.length === 0) {
test.fail("Types", "No TypeScript type files found");
return;
}
// Build a set of all expected camelCase field names from the contract
const expectedFields = new Map(); // entityName → Set of field names
for (const entity of contract.entities) {
const fieldSet = new Set();
for (const field of entity.fields) {
if (field.isSecret) continue;
const camel = toCamel(contract, field.name);
fieldSet.add(camel);
}
expectedFields.set(entity.name, fieldSet);
}
for (const typeFile of typeFiles) {
console.log(` Types file: ${typeFile}`);
let content;
try {
content = readFileSync(typeFile, "utf-8");
} catch (e) {
test.fail("Types", `Cannot read ${typeFile}: ${e.message}`);
continue;
}
// For each entity, check its interface exists and has correct fields
for (const entity of contract.entities) {
const expected = expectedFields.get(entity.name);
if (!expected) continue;
// Find the interface declaration
const ifaceRegex = new RegExp(`(?:export\\s+)?interface\\s+${entity.name}\\s*(?:extends\\s+[^{]+)?\\s*\\{([^}]*)\\}`, "s");
const match = content.match(ifaceRegex);
if (!match) {
// Entity types might not be directly in this file (e.g. in shared-types)
continue;
}
const body = match[1];
const fieldLines = body
.split("\n")
.map(l => l.trim())
.filter(l => l.length > 0 && !l.startsWith("//"));
const foundFields = new Set();
for (const line of fieldLines) {
// Extract field name: " username: string;" or " username?: string;"
const fieldMatch = line.match(/^\s*(\w+)(\?)?\s*:/);
if (fieldMatch) {
foundFields.add(fieldMatch[1]);
}
}
// Check each expected field exists
for (const fieldName of expected) {
test.pass();
if (!foundFields.has(fieldName)) {
test.fail(
`Types/${entity.name}`,
`Field '${fieldName}' (camelCase) expected in interface '${entity.name}' but MISSING from types file`
);
}
}
// Warn about extra fields
for (const found of foundFields) {
if (!expected.has(found)) {
test.warnings.push({
section: `Types/${entity.name}`,
message: `Field '${found}' found in interface '${entity.name}' but NOT in Contract`
});
}
}
}
}
// Also check that all entities exist as interfaces in the types
// Find ALL interface names declared
for (const typeFile of typeFiles) {
let content;
try { content = readFileSync(typeFile, "utf-8"); } catch { continue; }
for (const entity of contract.entities) {
const ifaceRegex = new RegExp(`interface\\s+${entity.name}\\s*(?:extends|\\{)`, "s");
if (!ifaceRegex.test(content)) {
// Check if entity's table is users — User is a special type
if (entity.table !== "users") {
// It might be in another type file; we accumulate all
continue;
}
}
}
}
}
// ═══════════════════════════════════════════════════════
// 4. Check 3: Routes → Contract
// ═══════════════════════════════════════════════════════
function checkRoutesConsistency(test) {
const { contract } = test;
const projectDir = process.env.PROJECT_DIR || ".";
console.log("── Check 3: Route DTO Types ↔ Contract ──");
const routeFiles = [
...test.findFiles(resolve(projectDir, "src/routes"), /\.ts$/),
...test.findFiles(resolve(projectDir, "apps/api/src/routes"), /\.ts$/),
];
if (routeFiles.length === 0) {
test.warnings.push({ section: "Routes", message: "No route files found, skipping route checks" });
return;
}
for (const routeFile of routeFiles) {
console.log(` Route file: ${routeFile}`);
let content;
try {
content = readFileSync(routeFile, "utf-8");
} catch (e) { continue; }
// Check auth routes use contract auth config
// Check CRUD routes use correct entity field names
for (const entity of contract.entities) {
if (entity.table === "users") continue; // Skip User — handled by auth routes
const routeName = entity.table;
if (routeFile.includes(routeName) || routeFile.endsWith(`${routeName}.ts`)) {
// Verify CreateInput and UpdateInput types are used
const pascalName = entity.name;
const hasCreateInput = new RegExp(`Create${pascalName}Input`).test(content);
const hasUpdateInput = new RegExp(`Update${pascalName}Input`).test(content);
if (!hasCreateInput) {
test.fail(
`Routes/${routeName}`,
`Route for '${routeName}' should use Create${pascalName}Input type`
);
} else { test.pass(); }
if (!hasUpdateInput) {
test.fail(
`Routes/${routeName}`,
`Route for '${routeName}' should use Update${pascalName}Input type`
);
} else { test.pass(); }
}
}
}
}
// ═══════════════════════════════════════════════════════
// 5. Check 4: Services → Contract
// ═══════════════════════════════════════════════════════
function checkServicesConsistency(test) {
const { contract } = test;
const projectDir = process.env.PROJECT_DIR || ".";
console.log("── Check 4: Service SQL Columns ↔ Contract ──");
const serviceFiles = [
...test.findFiles(resolve(projectDir, "src/services"), /\.ts$/),
...test.findFiles(resolve(projectDir, "apps/api/src/services"), /\.ts$/),
];
if (serviceFiles.length === 0) {
test.warnings.push({ section: "Services", message: "No service files found, skipping service checks" });
return;
}
for (const serviceFile of serviceFiles) {
console.log(` Service file: ${serviceFile}`);
let content;
try {
content = readFileSync(serviceFile, "utf-8");
} catch (e) { continue; }
// Extract SQL column names from INSERT/SELECT statements
const insertMatches = content.matchAll(/INSERT INTO\s+(\w+)\s*\(([^)]+)\)/gi);
for (const match of insertMatches) {
const tableName = match[1];
const colStr = match[2];
const columns = colStr.split(",").map(c => c.trim().replace(/"/g, ""));
// Find the entity for this table
const entity = contract.entities.find(e => e.table === tableName);
if (!entity) continue;
const contractCols = new Set(entity.fields.filter(f => !f.isSecret).map(f => f.name));
for (const col of columns) {
test.pass();
if (!contractCols.has(col)) {
test.fail(
`Services/${tableName}`,
`SQL column '${col}' in INSERT statement not found in Contract for table '${tableName}'`
);
}
}
}
// Check SELECT statements too
const selectMatches = content.matchAll(/SELECT\s+(.+?)\s+FROM\s+(\w+)/gi);
for (const match of selectMatches) {
const selectStr = match[1];
const tableName = match[2];
const entity = contract.entities.find(e => e.table === tableName);
if (!entity || selectStr === "*") continue;
const selectCols = selectStr
.split(",")
.map(c => c.trim().split(/\s+as\s+/i)[0].trim())
.filter(c => c.length > 0 && c !== "*");
const contractCols = new Set(entity.fields.filter(f => !f.isSecret).map(f => f.name));
for (const col of selectCols) {
// Column might be aliased or use table prefix
const bareCol = col.split(".").pop().replace(/"/g, "");
if (!contractCols.has(bareCol)) {
test.warnings.push({
section: `Services/${tableName}`,
message: `SQL selected column '${bareCol}' not found in Contract for table '${tableName}'`
});
}
}
}
}
}
// ═══════════════════════════════════════════════════════
// 6. Check 5: Tests → Contract
// ═══════════════════════════════════════════════════════
function checkTestsConsistency(test) {
const { contract } = test;
const projectDir = process.env.PROJECT_DIR || ".";
console.log("── Check 5: Test Payloads ↔ Contract ──");
const testFiles = [
...test.findFiles(resolve(projectDir, "src/__tests__"), /\.test\.ts$/),
...test.findFiles(resolve(projectDir, "apps/api/src/__tests__"), /\.test\.ts$/),
...test.findFiles(projectDir, /\.test\.ts$/),
];
if (testFiles.length === 0) {
test.warnings.push({ section: "Tests", message: "No test files found, skipping test checks" });
return;
}
for (const testFile of testFiles) {
console.log(` Test file: ${testFile}`);
let content;
try {
content = readFileSync(testFile, "utf-8");
} catch (e) { continue; }
// Extract payload objects from .inject() calls
const payloadRegex = /payload:\s*({[^}]+})/gs;
const matches = [...content.matchAll(payloadRegex)];
for (const match of matches) {
let payloadStr = match[1];
try {
// Try to parse as JSON (might have template literals, so this is approximate)
// Extract field names using regex
const fieldRegex = /(\w+)\s*:/g;
const payloadFields = [];
let fm;
while ((fm = fieldRegex.exec(payloadStr)) !== null) {
payloadFields.push(fm[1]);
}
// Check if these fields are valid according to some entity
const allContractFields = new Set();
for (const entity of contract.entities) {
for (const field of entity.fields) {
if (!field.isSecret && !field.isAuto && !field.isPrimary) {
allContractFields.add(toCamel(contract, field.name));
allContractFields.add(field.name); // snake_case
}
}
}
for (const pf of payloadFields) {
if (!allContractFields.has(pf)) {
// Allow special test fields
if (["username", "password", "nickname", "token", "authorization"].includes(pf)) {
continue;
}
test.warnings.push({
section: `Tests/${testFile}`,
message: `Test payload field '${pf}' not found in any Contract entity`
});
}
}
} catch {
// Non-JSON payload, skip
}
}
// Check for hardcoded fake userId values
const fakeIdMatch = content.match(/"00000000-0000-0000-0000-000000000001"/);
if (fakeIdMatch) {
test.warnings.push({
section: `Tests/${testFile}`,
message: "Hardcoded fake userId found in test. Consider using a real registered user ID."
});
}
}
}
// ═══════════════════════════════════════════════════════
// 7. Check 6: Auth Configuration
// ═══════════════════════════════════════════════════════
function checkAuthConsistency(test) {
const { contract } = test;
const projectDir = process.env.PROJECT_DIR || ".";
console.log("── Check 7: Auth Config ↔ Contract ──");
const authFiles = [
...test.findFiles(resolve(projectDir, "src/routes"), /auth\.ts$/),
...test.findFiles(resolve(projectDir, "apps/api/src/routes"), /auth\.ts$/),
];
if (authFiles.length === 0) {
test.warnings.push({ section: "Auth", message: "No auth routes found" });
return;
}
for (const authFile of authFiles) {
let content;
try {
content = readFileSync(authFile, "utf-8");
} catch (e) { continue; }
// Check registration fields
for (const field of contract.auth.registrationFields) {
test.pass();
if (!content.includes(field)) {
test.fail("Auth", `Registration field '${field}' from contract.auth.registrationFields not found in auth routes`);
}
}
// Check JWT payload fields
for (const field of contract.auth.jwtPayload) {
test.pass();
if (!content.includes(field)) {
test.fail("Auth", `JWT payload field '${field}' from contract.auth.jwtPayload not found in auth routes`);
}
}
// Check password policy (minLength)
const pwPolicy = contract.auth.passwordPolicy;
if (pwPolicy.minLength) {
const minLenRegex = new RegExp(`password\\.length\\s*<\\s*${pwPolicy.minLength}`);
if (minLenRegex.test(content)) {
test.pass();
} else {
test.fail("Auth", `Password minLength policy (${pwPolicy.minLength}) not enforced in auth routes`);
}
}
// Check password_hash column is used (not a plain password column)
test.pass();
if (!content.includes("password_hash") && !content.includes("passwordHash")) {
test.fail("Auth", "Auth routes should use 'password_hash' column (not plain password)");
}
}
}
// ═══════════════════════════════════════════════════════
// 8. Main Runner
// ═══════════════════════════════════════════════════════
function generateReport(test, contract) {
const lines = [];
lines.push("# Contract Consistency Report");
lines.push("");
lines.push(`**Generated**: ${new Date().toISOString()}`);
lines.push(`**Project**: ${contract.projectName}`);
lines.push(`**Domain**: ${contract.domain}`);
lines.push(`**Entities**: ${contract.entities.length}`);
lines.push("");
lines.push("## Summary");
lines.push("");
lines.push(`| Metric | Value |`);
lines.push(`|--------|-------|`);
lines.push(`| Total Checks | ${test.checks.total} |`);
lines.push(`| Passed | ${test.checks.passed} |`);
lines.push(`| Failed | ${test.checks.failed} |`);
lines.push(`| Warnings | ${test.warnings.length} |`);
lines.push(`| **Result** | **${test.checks.failed === 0 ? "✅ PASS" : "❌ FAIL"}** |`);
lines.push("");
if (test.failures.length > 0) {
lines.push("## ❌ Failures");
lines.push("");
for (const f of test.failures) {
lines.push(`- **${f.section}**: ${f.message}`);
}
lines.push("");
}
if (test.warnings.length > 0) {
lines.push("## ⚠️ Warnings");
lines.push("");
for (const w of test.warnings) {
lines.push(`- **${w.section}**: ${w.message}`);
}
lines.push("");
}
lines.push("## Contract Entities");
lines.push("");
for (const entity of contract.entities) {
lines.push(`### ${entity.name} (\`${entity.table}\`)`);
lines.push(`*${entity.description}*`);
lines.push("");
lines.push("| Field | Type | Required | Notes |");
lines.push("|-------|------|----------|-------|");
for (const field of entity.fields) {
const notes = [];
if (field.isPrimary) notes.push("PK");
if (field.isAuto) notes.push("auto");
if (field.isSecret) notes.push("secret");
if (field.unique) notes.push("unique");
if (field.enum) notes.push(`enum: ${field.enum.join("|")}`);
if (field.defaultValue !== undefined) notes.push(`default: ${field.defaultValue}`);
if (field.fkEntity) notes.push(`FK→${field.fkEntity}.${field.fkColumn}`);
lines.push(`| \`${field.name}\` | ${field.type} | ${field.required ? "✓" : ""} | ${notes.join(", ")} |`);
}
lines.push("");
}
// Field mapping reference
lines.push("## Field Mapping");
lines.push("");
lines.push("| snake_case | camelCase |");
lines.push("|------------|-----------|");
for (const [snake, camel] of Object.entries(contract.fieldMapping.snakeToCamel)) {
if (snake !== camel) {
lines.push(`| \`${snake}\` | \`${camel}\` |`);
}
}
lines.push("");
const report = lines.join("\n");
// Write report
const reportPath = resolve(WORKSPACE, "contract-consistency-report.md");
mkdirSync(dirname(reportPath), { recursive: true });
writeFileSync(reportPath, report);
console.log(`\nReport written to: ${reportPath}`);
return report;
}
async function main() {
const args = process.argv.slice(2);
let projectDir = null, prdPath = null, archPath = null;
for (let i = 0; i < args.length; i++) {
if (args[i] === "--project" && args[i + 1]) {
projectDir = resolve(args[++i]);
} else if (args[i] === "--prd" && args[i + 1]) {
prdPath = resolve(args[++i]);
} else if (args[i] === "--arch" && args[i + 1]) {
archPath = resolve(args[++i]);
} else if (args[i] === "--help" || args[i] === "-h") {
console.log(`
Contract Consistency Test
Usage:
node scripts/contract-consistency-test.mjs --project <dir> [--prd <path>] [--arch <path>]
Options:
--project <dir> 项目根目录
--prd <path> PRD JSON 文件路径
--arch <path> Architecture JSON 文件路径
--help 显示帮助
`);
process.exit(0);
}
}
if (!projectDir) {
console.error("Error: --project <dir> is required. Use --help.");
process.exit(1);
}
// Set global PROJECT_DIR for check functions
process.env.PROJECT_DIR = projectDir;
// Load PRD and Architecture if available, or create a minimal contract
let prd = null, arch = null;
if (prdPath && existsSync(prdPath)) {
try {
prd = JSON.parse(readFileSync(prdPath, "utf-8"));
} catch (e) {
console.error(`Warning: Failed to load PRD: ${e.message}`);
}
}
if (archPath && existsSync(archPath)) {
try {
arch = JSON.parse(readFileSync(archPath, "utf-8"));
} catch (e) {
console.error(`Warning: Failed to load Architecture: ${e.message}`);
}
}
// If no PRD/arch, try to derive contract from generated project
if (!prd && !arch) {
// Check if project has a prd.json or arch.json
const projectPrd = resolve(projectDir, "prd.json");
const projectArch = resolve(projectDir, "arch.json");
if (existsSync(projectPrd)) {
try { prd = JSON.parse(readFileSync(projectPrd, "utf-8")); } catch {}
}
if (existsSync(projectArch)) {
try { arch = JSON.parse(readFileSync(projectArch, "utf-8")); } catch {}
}
}
// If still no PRD, try to detect domain from project structure
if (!prd) {
prd = { projectName: "ContractMgmt", domain: "enterprise", features: [], pages: [], apiRequirements: [] };
}
// Create contract
const contract = createContract(prd, arch);
console.log(`\n═══ Contract Consistency Test ═══`);
console.log(`Project: ${contract.projectName}`);
console.log(`Domain: ${contract.domain}`);
console.log(`Entities: ${contract.entities.map(e => e.name).join(", ")}`);
console.log(`Project Dir: ${projectDir}\n`);
// Initialize test
const test = new ContractConsistencyTest(contract);
// Run checks
try { checkSchemaConsistency(test); } catch (e) { test.fail("Schema", `Error: ${e.message}`); }
try { checkTypesConsistency(test); } catch (e) { test.fail("Types", `Error: ${e.message}`); }
try { checkRoutesConsistency(test); } catch (e) { test.fail("Routes", `Error: ${e.message}`); }
try { checkServicesConsistency(test); } catch (e) { test.fail("Services", `Error: ${e.message}`); }
try { checkTestsConsistency(test); } catch (e) { test.fail("Tests", `Error: ${e.message}`); }
try { checkAuthConsistency(test); } catch (e) { test.fail("Auth", `Error: ${e.message}`); }
// Generate report
const report = generateReport(test, contract);
// Print summary
console.log(`\n═══ Results ═══`);
console.log(`Total: ${test.checks.total} | Passed: ${test.checks.passed} | Failed: ${test.checks.failed} | Warnings: ${test.warnings.length}`);
console.log(`Result: ${test.checks.failed === 0 ? "✅ PASS" : "❌ FAIL"}`);
// Exit with appropriate code
process.exit(test.checks.failed > 0 ? 1 : 0);
}
main();
+206
View File
@@ -0,0 +1,206 @@
#!/usr/bin/env node
/**
* 合同管理工具 — 完整 E2E 生成 + 验证
* 使用 Certified Software Generator v1.1 的标准 Pipeline
* 不修改任何 Generator 源码
*/
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
import { resolve, dirname, join } from "node:path";
import { execSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const __dirname = dirname(resolve(fileURLToPath(import.meta.url)));
const WORKSPACE = resolve(__dirname, "..");
const SCRIPTS = resolve(WORKSPACE, "scripts");
const B = resolve(WORKSPACE, ".benchmark", "contract-mgmt");
const INPUT = "开发一个合同管理桌面工具,支持合同创建、合同审批、合同到期提醒、客户管理、合同金额统计、操作日志";
mkdirSync(B, { recursive: true });
function cmd(c, opts = {}) {
try {
return execSync(c, { cwd: WORKSPACE, encoding: "utf8", ...opts, stdio: opts.stdio ?? "pipe" });
} catch (e) {
return { error: e.message, stderr: e.stderr?.toString() || "", stdout: e.stdout?.toString() || "" };
}
}
function runAgent(script, args) {
const t0 = Date.now();
const result = cmd(`node ${join(SCRIPTS, script)} ${args}`);
const ms = Date.now() - t0;
if (result.error) {
try { return { ms, ...JSON.parse(result.stdout?.trim() || "{}") }; } catch { return { ms, error: result.error }; }
}
try { return { ms, ...JSON.parse(result.trim()) }; } catch { return { ms, raw: result }; }
}
function countFiles(dir) {
try {
return parseInt(execSync(`find ${dir} -type f 2>/dev/null | wc -l`, { encoding: "utf8", cwd: WORKSPACE }).trim(), 10);
} catch { return 0; }
}
// Validation functions (same as e2e-benchmark-v1.mjs)
function validateFrontend(dir) {
if (!existsSync(dir)) return { pass: false, reason: "dir missing" };
const checks = {
packageJson: existsSync(join(dir, "package.json")),
appDir: existsSync(join(dir, "app")),
services: existsSync(join(dir, "services")) || existsSync(join(dir, "src", "services")),
types: existsSync(join(dir, "types")) || existsSync(join(dir, "src", "types")),
};
const fileCount = countFiles(dir);
return { pass: checks.packageJson && checks.appDir && fileCount > 10, fileCount, ...checks };
}
function validateBackend(dir) {
if (!existsSync(dir)) return { pass: false, reason: "dir missing" };
const checks = {
packageJson: existsSync(join(dir, "package.json")),
routes: existsSync(join(dir, "src", "routes")),
services: existsSync(join(dir, "src", "services")),
auth: existsSync(join(dir, "src", "routes", "auth.ts")) || existsSync(join(dir, "src", "routes", "auth-route.ts")),
db: existsSync(join(dir, "src", "db")) || existsSync(join(dir, "src", "schema")),
};
const fileCount = countFiles(dir);
return { pass: checks.packageJson && checks.routes && checks.services && fileCount > 10, fileCount, ...checks };
}
function validateFullstack(dir) {
if (!existsSync(dir)) return { pass: false, reason: "dir missing" };
const checks = {
packageJson: existsSync(join(dir, "package.json")),
web: existsSync(join(dir, "apps", "web")),
api: existsSync(join(dir, "apps", "api")),
shared: existsSync(join(dir, "packages")),
};
const fileCount = countFiles(dir);
return { pass: checks.packageJson && checks.web && checks.api && fileCount > 30, fileCount, ...checks };
}
function validateElectron(dir) {
if (!existsSync(dir)) return { pass: false, reason: "dir missing" };
const checks = {
packageJson: existsSync(join(dir, "package.json")),
main: existsSync(join(dir, "electron", "main.ts")),
preload: existsSync(join(dir, "electron", "preload.ts")),
ipc: existsSync(join(dir, "electron", "ipc.ts")),
yml: existsSync(join(dir, "electron-builder.yml")),
};
const fileCount = countFiles(dir);
return { pass: checks.packageJson && checks.main && checks.preload && fileCount >= 8, fileCount, ...checks };
}
function validateRelease(dir) {
if (!existsSync(dir)) return { pass: false, reason: "dir missing" };
const rDir = join(dir, "release");
const checks = {
versionJson: existsSync(join(rDir, "version.json")),
manifest: existsSync(join(rDir, "manifests", "manifest.json")),
checksums: existsSync(join(rDir, "checksums", "checksums.txt")),
releaseNotes: existsSync(join(rDir, "release-notes", "release-notes.md")),
buildInfo: existsSync(join(rDir, "build-info.json")),
};
if (checks.checksums) {
const lines = readFileSync(join(rDir, "checksums", "checksums.txt"), "utf8").trim().split("\n").filter(l => l.length > 0);
checks.checksumCount = lines.length;
checks.checksumsValid = lines.every(l => l.split(/\s+/)[0]?.length === 64);
}
if (checks.buildInfo) {
try {
const bi = JSON.parse(readFileSync(join(rDir, "build-info.json"), "utf8"));
checks.buildInfoValid = !!(bi.nodeVersion && bi.generatorVersion);
} catch { checks.buildInfoValid = false; }
}
const fileCount = countFiles(dir);
return { pass: checks.versionJson && checks.manifest && checks.checksums && checks.releaseNotes && checks.buildInfo, fileCount, ...checks };
}
// ═══════════════════════════════════════════════════════
// MAIN
// ═══════════════════════════════════════════════════════
console.log("═".repeat(60));
console.log(" 合同管理工具 — Certified Generator v1.1 E2E 验证");
console.log("═".repeat(60));
console.log(`\n需求: ${INPUT}\n`);
// SF-01: PRD
console.log("▸ SF-01 Project Intake...");
const s1 = runAgent("project-intake-agent.mjs", `--input "${INPUT}" --output "${join(B, "prd.json")}"`);
console.log(` ✓ PRD: ${s1.projectName || "?"} (${s1.ms}ms) ${s1.error ? "❌ " + s1.error : "✅"}`);
// SF-02: Architecture
console.log("▸ SF-02 Architecture...");
const s2 = runAgent("architecture-agent.mjs", `--input "${join(B, "prd.json")}" --output "${join(B, "arch.json")}"`);
console.log(` ✓ Architecture: ${s2.moduleCount || "?"} modules (${s2.ms}ms) ${s2.error ? "❌ " + s2.error : "✅"}`);
// SF-03: Frontend
console.log("▸ SF-03 Frontend Builder...");
const feOut = join(B, "frontend");
const s3 = runAgent("frontend-builder-agent.mjs", `--prd "${join(B, "prd.json")}" --arch "${join(B, "arch.json")}" --output "${feOut}"`);
const feVal = validateFrontend(feOut);
console.log(` ✓ Frontend: ${feVal.fileCount} files (${s3.ms}ms) ${feVal.pass ? "✅" : "❌"}`);
// SF-04: Backend
console.log("▸ SF-04 Backend Builder...");
const beOut = join(B, "backend");
const s4 = runAgent("backend-builder-agent.mjs", `--prd "${join(B, "prd.json")}" --arch "${join(B, "arch.json")}" --output "${beOut}"`);
const beVal = validateBackend(beOut);
console.log(` ✓ Backend: ${beVal.fileCount} files (${s4.ms}ms) ${beVal.pass ? "✅" : "❌"}`);
// SF-05: Fullstack
console.log("▸ SF-05 Fullstack Composer...");
const fsOut = join(B, "fullstack");
const s5 = runAgent("fullstack-composer-agent.mjs", `--prd "${join(B, "prd.json")}" --arch "${join(B, "arch.json")}" --output "${fsOut}"`);
const fsVal = validateFullstack(fsOut);
console.log(` ✓ Fullstack: ${fsVal.fileCount} files (${s5.ms}ms) ${fsVal.pass ? "✅" : "❌"}`);
// SF-06: Electron
console.log("▸ SF-06 Electron Builder...");
const elOut = join(B, "electron");
const s6 = runAgent("electron-builder-agent.mjs", `--input "${fsOut}" --output "${elOut}" --prd "${join(B, "prd.json")}"`);
const elVal = validateElectron(elOut);
console.log(` ✓ Electron: ${elVal.fileCount} files (${s6.ms}ms) ${elVal.pass ? "✅" : "❌"}`);
// SF-07: Release
console.log("▸ SF-07 Release Builder...");
const rlOut = join(B, "release");
const s7 = runAgent("release-builder-agent.mjs", `--input "${fsOut}" --output "${rlOut}"`);
const rlVal = validateRelease(rlOut);
console.log(` ✓ Release: ${rlVal.fileCount} files (${s7.ms}ms) ${rlVal.pass ? "✅" : "❌"}`);
// Summary
const totalMs = s1.ms + s2.ms + s3.ms + s4.ms + s5.ms + s6.ms + s7.ms;
const totalFiles = (s3.stats?.totalFiles || feVal.fileCount) + (s4.stats?.totalFiles || beVal.fileCount) + (s5.stats?.totalFiles || fsVal.fileCount) + (s6.stats?.totalFiles || elVal.fileCount) + (s7.stats?.totalFiles || rlVal.fileCount);
const allPass = feVal.pass && beVal.pass && fsVal.pass && elVal.pass && rlVal.pass;
console.log("\n" + "═".repeat(60));
console.log(` 总计: ${totalFiles} files | ${totalMs}ms | ${allPass ? "✅ ALL PASS" : "❌ HAS FAILURES"}`);
console.log("═".repeat(60));
// Output summary as JSON
const summary = {
input: INPUT,
projectName: s1.projectName,
domain: s1.domain,
stages: {
prd: { ms: s1.ms, error: s1.error },
arch: { ms: s2.ms, modules: s2.moduleCount, error: s2.error },
frontend: { ms: s3.ms, files: feVal.fileCount, pass: feVal.pass, error: s3.error },
backend: { ms: s4.ms, files: beVal.fileCount, pass: beVal.pass, error: s4.error },
fullstack: { ms: s5.ms, files: fsVal.fileCount, pass: fsVal.pass, error: s5.error },
electron: { ms: s6.ms, files: elVal.fileCount, pass: elVal.pass, error: s6.error },
release: { ms: s7.ms, files: rlVal.fileCount, pass: rlVal.pass, error: s7.error },
},
totalMs,
totalFiles,
allPass,
outputDir: B,
};
writeFileSync(join(B, "summary.json"), JSON.stringify(summary, null, 2));
console.log(`\nSummary: ${join(B, "summary.json")}`);
+873
View File
@@ -0,0 +1,873 @@
#!/usr/bin/env node
/**
* Domain Benchmark Suite
*
* 验证现有系统的泛化能力:
* project-intake-agent → architecture-agent
* → frontend-builder-agent → backend-builder-agent → fullstack-composer-agent
*
* 对 10 个领域生成完整项目,验证 Build + API,输出 benchmark-report.md
*
* Usage:
* node scripts/domain-benchmark.mjs [--domain <name>] [--skip-build] [--skip-api]
*/
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
import { resolve, dirname, join } from "node:path";
import { execSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const __dirname = dirname(resolve(fileURLToPath(import.meta.url)));
const WORKSPACE = resolve(__dirname, "..");
const BENCH_ROOT = resolve(WORKSPACE, ".benchmark");
const SCRIPTS = resolve(WORKSPACE, "scripts");
const REPORT_PATH = resolve(WORKSPACE, "benchmark-report.md");
// ═══════════════════════════════════════════════════════
// Domain Definitions
// ═══════════════════════════════════════════════════════
const DOMAINS = [
{ id: "petcare", input: "做一个宠物护理管理平台,宠物主人可以管理宠物档案、健康日程、日常记录和成长相册" },
{ id: "crm", input: "做一个客户关系管理系统,支持客户管理、销售漏斗、跟进记录和数据分析仪表盘" },
{ id: "inventory", input: "做一个库存管理系统,支持商品入库出库、库存盘点、供应商管理和库存预警" },
{ id: "ticket", input: "做一个工单系统,支持工单创建、分配、处理流程、优先级管理和工单归档" },
{ id: "blog-cms", input: "做一个博客内容管理系统,支持文章发布、分类标签、评论管理和媒体库" },
{ id: "project-mgmt", input: "做一个项目管理系统,支持项目看板、任务分配、甘特图和团队协作" },
{ id: "hr", input: "做一个人力资源管理系统,支持员工档案、考勤管理、招聘流程和绩效评估" },
{ id: "asset", input: "做一个固定资产管理系统,支持资产登记、领用归还、折旧计算和盘点统计" },
{ id: "course", input: "做一个在线课程管理系统,支持课程发布、章节管理、学员进度和作业批改" },
{ id: "appointment", input: "做一个预约管理系统,支持服务项目、时间段预约、客户通知和预约统计" },
];
// ═══════════════════════════════════════════════════════
// Helpers
// ═══════════════════════════════════════════════════════
function cmd(c, opts = {}) {
try {
return execSync(c, { cwd: WORKSPACE, encoding: "utf8", ...opts, stdio: opts.stdio ?? "pipe" });
} catch (e) {
return { error: e.message, stderr: e.stderr?.toString() || "", stdout: e.stdout?.toString() || "" };
}
}
function runAgent(agentPath, args) {
const result = cmd(`node ${agentPath} ${args}`);
if (result.error) {
// Try to extract JSON from stdout even on error
try { return JSON.parse(result.stdout?.trim() || "{}"); } catch { return { error: result.error, stderr: result.stderr }; }
}
try { return JSON.parse(result.trim()); } catch { return { raw: result }; }
}
function rmDirSafe(dir) {
try { if (existsSync(dir)) rmSync(dir, { recursive: true, force: true }); } catch {}
}
function listFilesRecursive(dir, acc = []) {
if (!existsSync(dir)) return acc;
const entries = readFileSync(dir)?.toString?.(); // not for dirs
const { readdirSync, statSync } = require("node:fs") || {};
// Use shell for simplicity
const result = execSync(`find ${dir} -type f | head -500`, { encoding: "utf8", cwd: WORKSPACE });
return result.trim().split("\n").filter(Boolean);
}
function countFiles(dir) {
try {
const r = execSync(`find ${dir} -type f 2>/dev/null | wc -l`, { encoding: "utf8", cwd: WORKSPACE });
return parseInt(r.trim(), 10);
} catch { return 0; }
}
function fileExists(dir, pattern) {
try {
const r = execSync(`find ${dir} -path "*${pattern}*" -type f 2>/dev/null | head -1`, { encoding: "utf8", cwd: WORKSPACE });
return r.trim().length > 0;
} catch { return false; }
}
// ═══════════════════════════════════════════════════════
// Step 1: RequirementPackage (SF-01 + SF-02)
// ═══════════════════════════════════════════════════════
function loadJSON(path) {
try { return JSON.parse(readFileSync(path, "utf8")); } catch { return {}; }
}
function generateRequirementPackage(domain) {
console.log(`\n📋 [${domain.id}] Generating RequirementPackage...`);
const t0 = Date.now();
// SF-01: PRD
const prdOut = join(BENCH_ROOT, domain.id, "prd.json");
mkdirSync(dirname(prdOut), { recursive: true });
runAgent(
join(SCRIPTS, "project-intake-agent.mjs"),
`--input "${domain.input}" --output "${prdOut}"`
);
const prd = loadJSON(prdOut);
// SF-02: Architecture
const archOut = join(BENCH_ROOT, domain.id, "arch.json");
runAgent(
join(SCRIPTS, "architecture-agent.mjs"),
`--input "${prdOut}" --output "${archOut}"`
);
const arch = loadJSON(archOut);
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
console.log(` ✅ PRD + Architecture generated (${elapsed}s)`);
console.log(` Project: ${prd.projectName || "?"}, Domain: ${prd.domain || "?"}`);
console.log(` Features: ${prd.features?.length || 0}, Pages: ${prd.pages?.length || 0}, APIs: ${prd.apiRequirements?.length || 0}`);
return { prd, arch, elapsed: parseFloat(elapsed), prdPath: prdOut, archPath: archOut };
}
// ═══════════════════════════════════════════════════════
// Step 2: Frontend Builder
// ═══════════════════════════════════════════════════════
function generateFrontend(domain, prdPath, archPath) {
console.log(` 🎨 Generating Frontend...`);
const t0 = Date.now();
const outDir = join(BENCH_ROOT, domain.id, "frontend");
const result = runAgent(
join(SCRIPTS, "frontend-builder-agent.mjs"),
`--prd "${prdPath}" --arch "${archPath}" --output "${outDir}"`
);
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
const fileCount = countFiles(outDir);
// Validation checks
const checks = {
hasPackageJson: fileExists(outDir, "package.json"),
hasNextConfig: fileExists(outDir, "next.config.*"),
hasAppPage: fileExists(outDir, "app/page.*"),
hasEntityPages: fileExists(outDir, "app/") && countFiles(join(outDir, "app")) > 2,
hasServices: fileExists(outDir, "services/"),
hasTypes: fileExists(outDir, "types/"),
fileCount,
};
const allPassed = checks.hasPackageJson && checks.hasNextConfig && checks.hasAppPage && checks.hasEntityPages;
console.log(` ${allPassed ? "✅" : "❌"} Frontend generated (${elapsed}s, ${fileCount} files)`);
if (!allPassed) {
console.log(` Missing: ${Object.entries(checks).filter(([,v]) => !v).map(([k]) => k).join(", ")}`);
}
return { ...checks, elapsed: parseFloat(elapsed), allPassed, result };
}
// ═══════════════════════════════════════════════════════
// Step 3: Backend Builder
// ═══════════════════════════════════════════════════════
function generateBackend(domain, prdPath, archPath) {
console.log(` ⚙️ Generating Backend...`);
const t0 = Date.now();
const outDir = join(BENCH_ROOT, domain.id, "backend");
const result = runAgent(
join(SCRIPTS, "backend-builder-agent.mjs"),
`--prd "${prdPath}" --arch "${archPath}" --output "${outDir}"`
);
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
const fileCount = countFiles(outDir);
const checks = {
hasPackageJson: fileExists(outDir, "package.json"),
hasDbSchema: fileExists(outDir, "schema") || fileExists(outDir, "db"),
hasRoutes: fileExists(outDir, "routes/"),
hasServices: fileExists(outDir, "services/"),
hasAuth: fileExists(outDir, "auth"),
hasMiddleware: fileExists(outDir, "middleware"),
hasTypes: fileExists(outDir, "types/"),
fileCount,
};
const allPassed = checks.hasPackageJson && checks.hasRoutes && checks.hasServices && checks.hasAuth;
console.log(` ${allPassed ? "✅" : "❌"} Backend generated (${elapsed}s, ${fileCount} files)`);
if (!allPassed) {
console.log(` Missing: ${Object.entries(checks).filter(([,v]) => !v).map(([k]) => k).join(", ")}`);
}
return { ...checks, elapsed: parseFloat(elapsed), allPassed, result };
}
// ═══════════════════════════════════════════════════════
// Step 4: Fullstack Composer
// ═══════════════════════════════════════════════════════
function generateFullstack(domain, prdPath, archPath) {
console.log(` 🔗 Composing Fullstack...`);
const t0 = Date.now();
const outDir = join(BENCH_ROOT, domain.id, "fullstack");
const result = runAgent(
join(SCRIPTS, "fullstack-composer-agent.mjs"),
`--prd "${prdPath}" --arch "${archPath}" --output "${outDir}"`
);
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
const fileCount = countFiles(outDir);
const checks = {
hasWeb: fileExists(outDir, "apps/web/"),
hasApi: fileExists(outDir, "apps/api/"),
hasSharedTypes: fileExists(outDir, "packages/shared-types/"),
hasSharedConfig: fileExists(outDir, "packages/shared-config/"),
hasRootPackageJson: fileExists(outDir, "package.json"),
hasReadme: fileExists(outDir, "README.md"),
fileCount,
};
const allPassed = checks.hasWeb && checks.hasApi && checks.hasSharedTypes && checks.hasRootPackageJson;
console.log(` ${allPassed ? "✅" : "❌"} Fullstack composed (${elapsed}s, ${fileCount} files)`);
if (!allPassed) {
console.log(` Missing: ${Object.entries(checks).filter(([,v]) => !v).map(([k]) => k).join(", ")}`);
}
return { ...checks, elapsed: parseFloat(elapsed), allPassed, result };
}
// ═══════════════════════════════════════════════════════
// Step 5: Build Validation
// ═══════════════════════════════════════════════════════
function validateBuild(domain) {
console.log(` 🔨 Build Validation...`);
const results = { frontend: null, backend: null, fullstack: null };
// Frontend build
const feDir = join(BENCH_ROOT, domain.id, "frontend");
if (existsSync(join(feDir, "package.json"))) {
console.log(` Frontend: npm install...`);
const install = cmd("npm install --legacy-peer-deps 2>&1", { cwd: feDir, timeout: 120_000 });
if (install.error) {
results.frontend = { build: "FAIL", reason: `npm install failed: ${install.stderr?.slice(0, 200) || install.error}` };
console.log(` ❌ npm install failed`);
} else {
console.log(` Frontend: npm run build...`);
// Next.js build may need specific config; try build, if no build script, try next build
const buildResult = cmd("npm run build 2>&1 || npx next build 2>&1", { cwd: feDir, timeout: 180_000 });
const buildFailed = buildResult.error || (typeof buildResult === 'string' && buildResult.includes('error'));
const buildErrText = typeof buildResult === 'string' ? buildResult.split('\n').filter(l => l.includes('error') || l.includes('Error') || l.includes('')).slice(0,5).join(' || ') : (buildResult.stderr?.slice(0, 500) || buildResult.error);
results.frontend = {
build: buildFailed ? "FAIL" : "PASS",
error: buildFailed ? buildErrText : null
};
console.log(` ${buildFailed ? "❌" : "✅"} Frontend build ${results.frontend.build}`);
}
} else {
results.frontend = { build: "SKIP", reason: "No package.json" };
}
// Backend build
const beDir = join(BENCH_ROOT, domain.id, "backend");
if (existsSync(join(beDir, "package.json"))) {
console.log(` Backend: npm install...`);
const install = cmd("npm install --legacy-peer-deps 2>&1", { cwd: beDir, timeout: 120_000 });
if (install.error) {
results.backend = { build: "FAIL", reason: `npm install failed: ${install.stderr?.slice(0, 200) || install.error}` };
console.log(` ❌ npm install failed`);
} else {
console.log(` Backend: tsc --noEmit...`);
const buildResult = cmd("npx tsc --noEmit 2>&1", { cwd: beDir, timeout: 120_000 });
const buildFailed = buildResult.error || (typeof buildResult === 'string' && buildResult.includes('error TS'));
const buildErrText = typeof buildResult === 'string' ? buildResult.split('\n').filter(l => l.includes('error TS')).slice(0,5).join(' || ') : (buildResult.stderr?.slice(0, 500) || buildResult.error);
results.backend = {
build: buildFailed ? "FAIL" : "PASS",
error: buildFailed ? buildErrText : null
};
console.log(` ${buildFailed ? "❌" : "✅"} Backend typecheck ${results.backend.build}`);
}
} else {
results.backend = { build: "SKIP", reason: "No package.json" };
}
// Fullstack build (just verify structure, don't run full monorepo build)
const fsDir = join(BENCH_ROOT, domain.id, "fullstack");
if (existsSync(join(fsDir, "package.json"))) {
results.fullstack = { build: "PASS", reason: "Structure validated (monorepo)" };
console.log(` ✅ Fullstack structure validated`);
} else {
results.fullstack = { build: "FAIL", reason: "No root package.json" };
}
return results;
}
// ═══════════════════════════════════════════════════════
// Step 6: API Validation
// ═══════════════════════════════════════════════════════
function validateAPI(domain) {
console.log(` 🧪 API Validation...`);
const beDir = join(BENCH_ROOT, domain.id, "backend");
const results = {};
if (!existsSync(join(beDir, "package.json"))) {
return { error: "No backend package.json" };
}
// Check route files for expected endpoints
const routesDir = join(beDir, "src", "routes");
const routeFiles = [];
try {
const find = execSync(`find ${routesDir} -name "*.ts" -type f 2>/dev/null`, { encoding: "utf8", cwd: WORKSPACE });
routeFiles.push(...find.trim().split("\n").filter(Boolean));
} catch {}
// Check for auth routes
const hasAuthRoute = routeFiles.some(f => f.includes("auth"));
const hasCRUDRoutes = routeFiles.filter(f => !f.includes("auth")).length;
// Check for health endpoint
const indexFile = join(beDir, "src", "index.ts");
let hasHealth = false;
try {
hasHealth = readFileSync(indexFile, "utf8").includes("health");
} catch {}
// Check CRUD route content
const crudOps = { create: false, read: false, update: false, delete: false };
for (const f of routeFiles) {
try {
const content = readFileSync(f, "utf8");
if (content.includes(".post(") || content.includes("app.post") || content.includes("router.post")) crudOps.create = true;
if (content.includes(".get(") || content.includes("app.get") || content.includes("router.get")) crudOps.read = true;
if (content.includes(".put(") || content.includes("app.put") || content.includes("router.put") || content.includes(".patch(") || content.includes("app.patch") || content.includes("router.patch")) crudOps.update = true;
if (content.includes(".delete(") || content.includes("app.delete") || content.includes("router.delete")) crudOps.delete = true;
} catch {}
}
// Check auth route content
let authOps = { register: false, login: false, me: false };
const authFile = routeFiles.find(f => f.includes("auth"));
if (authFile) {
try {
const content = readFileSync(authFile, "utf8");
authOps.register = content.includes("register") || content.includes("Register");
authOps.login = content.includes("login") || content.includes("Login");
authOps.me = content.includes("/me") || content.includes("'me'") || content.includes('"me"');
} catch {}
}
const crudPass = Object.values(crudOps).every(Boolean);
const authPass = authOps.register && authOps.login;
results.health = hasHealth;
results.register = authOps.register;
results.login = authOps.login;
results.me = authOps.me;
results.create = crudOps.create;
results.read = crudOps.read;
results.update = crudOps.update;
results.delete = crudOps.delete;
results.crudAllPass = crudPass;
results.authAllPass = authPass;
results.routeCount = routeFiles.length;
console.log(` Health: ${hasHealth ? "✅" : "❌"} | Auth: ${authPass ? "✅" : "❌"} | CRUD: ${crudPass ? "✅" : "❌"} | Routes: ${routeFiles.length}`);
return results;
}
// ═══════════════════════════════════════════════════════
// Step 7: Report Generation
// ═══════════════════════════════════════════════════════
function generateReport(allResults) {
console.log("\n\n📊 Generating Benchmark Report...\n");
const lines = [];
const h = (text) => { lines.push(text); return lines; };
h(`# Domain Benchmark Report`);
h(``);
h(`> Generated: ${new Date().toISOString()}`);
h(`> System: project-intake-agent → architecture-agent → frontend-builder → backend-builder → fullstack-composer`);
h(``);
// Summary metrics
const total = allResults.length;
const fePass = allResults.filter(r => r.frontend?.allPassed).length;
const bePass = allResults.filter(r => r.backend?.allPassed).length;
const fsPass = allResults.filter(r => r.fullstack?.allPassed).length;
const buildPass = allResults.filter(r => r.build?.frontend?.build === "PASS").length;
const typecheckPass = allResults.filter(r => r.build?.backend?.build === "PASS").length;
const crudPass = allResults.filter(r => r.api?.crudAllPass).length;
const authPass = allResults.filter(r => r.api?.authAllPass).length;
h(`## Summary`);
h(``);
h(`| Metric | Value |`);
h(`|--------|-------|`);
h(`| Total Domains | ${total} |`);
h(`| Frontend Generated | ${fePass}/${total} |`);
h(`| Backend Generated | ${bePass}/${total} |`);
h(`| Fullstack Composed | ${fsPass}/${total} |`);
h(`| Frontend Build PASS | ${buildPass}/${total} |`);
h(`| Backend Typecheck PASS | ${typecheckPass}/${total} |`);
h(`| CRUD PASS | ${crudPass}/${total} |`);
h(`| Auth PASS | ${authPass}/${total} |`);
h(``);
const avgFrontendFiles = allResults.reduce((s, r) => s + (r.frontend?.fileCount || 0), 0) / total;
const avgBackendFiles = allResults.reduce((s, r) => s + (r.backend?.fileCount || 0), 0) / total;
const avgFullstackFiles = allResults.reduce((s, r) => s + (r.fullstack?.fileCount || 0), 0) / total;
const avgReqTime = allResults.reduce((s, r) => s + (r.reqTime || 0), 0) / total;
const avgFeTime = allResults.reduce((s, r) => s + (r.frontend?.elapsed || 0), 0) / total;
const avgBeTime = allResults.reduce((s, r) => s + (r.backend?.elapsed || 0), 0) / total;
const avgFsTime = allResults.reduce((s, r) => s + (r.fullstack?.elapsed || 0), 0) / total;
const avgTotalTime = allResults.reduce((s, r) => s + (r.totalTime || 0), 0) / total;
h(`## Timing`);
h(``);
h(`| Phase | Avg Time |`);
h(`|-------|----------|`);
h(`| RequirementPackage | ${avgReqTime.toFixed(1)}s |`);
h(`| Frontend Builder | ${avgFeTime.toFixed(1)}s |`);
h(`| Backend Builder | ${avgBeTime.toFixed(1)}s |`);
h(`| Fullstack Composer | ${avgFsTime.toFixed(1)}s |`);
h(`| **Total Pipeline** | **${avgTotalTime.toFixed(1)}s** |`);
h(``);
h(`## Files Generated`);
h(``);
h(`| Layer | Avg Files |`);
h(`|-------|-----------|`);
h(`| Frontend | ${avgFrontendFiles.toFixed(0)} |`);
h(`| Backend | ${avgBackendFiles.toFixed(0)} |`);
h(`| Fullstack | ${avgFullstackFiles.toFixed(0)} |`);
h(``);
// Domain Results Table
h(`## Domain Results`);
h(``);
h(`| Domain | FE Gen | BE Gen | FS Gen | FE Build | BE Typecheck | CRUD | Auth | Total Files | Time |`);
h(`|--------|--------|--------|--------|----------|-------------|------|------|-------------|------|`);
for (const r of allResults) {
const feOk = r.frontend?.allPassed ? "✅" : "❌";
const beOk = r.backend?.allPassed ? "✅" : "❌";
const fsOk = r.fullstack?.allPassed ? "✅" : "❌";
const feBuild = r.build?.frontend?.build === "PASS" ? "✅" : r.build?.frontend?.build === "SKIP" ? "—" : "❌";
const beTC = r.build?.backend?.build === "PASS" ? "✅" : r.build?.backend?.build === "SKIP" ? "—" : "❌";
const crudOk = r.api?.crudAllPass ? "✅" : "❌";
const authOk = r.api?.authAllPass ? "✅" : "❌";
const totalFiles = (r.frontend?.fileCount || 0) + (r.backend?.fileCount || 0) + (r.fullstack?.fileCount || 0);
h(`| ${r.id} | ${feOk} | ${beOk} | ${fsOk} | ${feBuild} | ${beTC} | ${crudOk} | ${authOk} | ${totalFiles} | ${(r.totalTime || 0).toFixed(1)}s |`);
}
h(``);
// Detailed Domain Results
h(`## Detailed Results`);
h(``);
for (const r of allResults) {
h(`### ${r.id}`);
h(``);
h(`**Input:** "${r.input}"`);
h(`**Project:** ${r.projectName || "N/A"} | **Domain:** ${r.domain || "N/A"}`);
h(``);
h(`#### Frontend`);
if (r.frontend?.allPassed === false) {
const missing = Object.entries(r.frontend || {})
.filter(([k, v]) => k.startsWith("has") && !v)
.map(([k]) => k.replace("has", ""));
h(`❌ Missing: ${missing.join(", ")}`);
} else {
h(`${r.frontend?.fileCount || 0} files generated`);
}
h(``);
h(`#### Backend`);
if (r.backend?.allPassed === false) {
const missing = Object.entries(r.backend || {})
.filter(([k, v]) => k.startsWith("has") && !v)
.map(([k]) => k.replace("has", ""));
h(`❌ Missing: ${missing.join(", ")}`);
} else {
h(`${r.backend?.fileCount || 0} files generated`);
}
h(``);
h(`#### Fullstack`);
if (r.fullstack?.allPassed === false) {
const missing = Object.entries(r.fullstack || {})
.filter(([k, v]) => k.startsWith("has") && !v)
.map(([k]) => k.replace("has", ""));
h(`❌ Missing: ${missing.join(", ")}`);
} else {
h(`${r.fullstack?.fileCount || 0} files generated`);
}
h(``);
h(`#### Build`);
if (r.build?.frontend?.build === "FAIL") {
h(`❌ Frontend build failed: ${r.build.frontend.reason || r.build.frontend.error || ""}`);
} else {
h(`✅ Frontend build: ${r.build?.frontend?.build || "SKIP"}`);
}
if (r.build?.backend?.build === "FAIL") {
h(`❌ Backend typecheck failed: ${r.build.backend.reason || r.build.backend.error || ""}`);
} else {
h(`✅ Backend typecheck: ${r.build?.backend?.build || "SKIP"}`);
}
h(``);
h(`#### API`);
if (r.api) {
h(`| Endpoint | Status |`);
h(`|----------|--------|`);
h(`| Health | ${r.api.health ? "✅" : "❌"} |`);
h(`| Register | ${r.api.register ? "✅" : "❌"} |`);
h(`| Login | ${r.api.login ? "✅" : "❌"} |`);
h(`| Me | ${r.api.me ? "✅" : "❌"} |`);
h(`| Create | ${r.api.create ? "✅" : "❌"} |`);
h(`| Read | ${r.api.read ? "✅" : "❌"} |`);
h(`| Update | ${r.api.update ? "✅" : "❌"} |`);
h(`| Delete | ${r.api.delete ? "✅" : "❌"} |`);
}
h(``);
}
// Failure Analysis
h(`## Failure Analysis`);
h(``);
const failures = allResults.filter(r =>
!r.frontend?.allPassed || !r.backend?.allPassed || !r.fullstack?.allPassed ||
r.build?.frontend?.build === "FAIL" || r.build?.backend?.build === "FAIL" ||
!r.api?.crudAllPass || !r.api?.authAllPass
);
if (failures.length === 0) {
h(`✅ **All domains passed all checks!**`);
h(``);
} else {
h(`### Failure Categories`);
h(``);
// Categorize failures
const feGenFails = allResults.filter(r => !r.frontend?.allPassed);
const beGenFails = allResults.filter(r => !r.backend?.allPassed);
const fsGenFails = allResults.filter(r => !r.fullstack?.allPassed);
const feBuildFails = allResults.filter(r => r.build?.frontend?.build === "FAIL");
const beBuildFails = allResults.filter(r => r.build?.backend?.build === "FAIL");
const crudFails = allResults.filter(r => !r.api?.crudAllPass);
const authFails = allResults.filter(r => !r.api?.authAllPass);
h(`| Category | Count | Domains |`);
h(`|----------|-------|---------|`);
h(`| Frontend Generation | ${feGenFails.length} | ${feGenFails.map(r => r.id).join(", ") || "—"} |`);
h(`| Backend Generation | ${beGenFails.length} | ${beGenFails.map(r => r.id).join(", ") || "—"} |`);
h(`| Fullstack Composition | ${fsGenFails.length} | ${fsGenFails.map(r => r.id).join(", ") || "—"} |`);
h(`| Frontend Build | ${feBuildFails.length} | ${feBuildFails.map(r => r.id).join(", ") || "—"} |`);
h(`| Backend Typecheck | ${beBuildFails.length} | ${beBuildFails.map(r => r.id).join(", ") || "—"} |`);
h(`| CRUD Missing | ${crudFails.length} | ${crudFails.map(r => r.id).join(", ") || "—"} |`);
h(`| Auth Missing | ${authFails.length} | ${authFails.map(r => r.id).join(", ") || "—"} |`);
h(``);
// Detailed failure reasons
h(`### Failure Details`);
h(``);
for (const r of allResults) {
const issues = [];
if (!r.frontend?.allPassed) {
const missing = Object.entries(r.frontend || {}).filter(([k, v]) => k.startsWith("has") && !v).map(([k]) => k.replace("has", ""));
issues.push(`Frontend: missing ${missing.join(", ")}`);
}
if (!r.backend?.allPassed) {
const missing = Object.entries(r.backend || {}).filter(([k, v]) => k.startsWith("has") && !v).map(([k]) => k.replace("has", ""));
issues.push(`Backend: missing ${missing.join(", ")}`);
}
if (!r.fullstack?.allPassed) {
const missing = Object.entries(r.fullstack || {}).filter(([k, v]) => k.startsWith("has") && !v).map(([k]) => k.replace("has", ""));
issues.push(`Fullstack: missing ${missing.join(", ")}`);
}
if (r.build?.frontend?.build === "FAIL") issues.push(`Frontend build: ${r.build.frontend.error || r.build.frontend.reason}`);
if (r.build?.backend?.build === "FAIL") issues.push(`Backend typecheck: ${r.build.backend.error || r.build.backend.reason}`);
if (!r.api?.crudAllPass) {
const missing = Object.entries({create: r.api?.create, read: r.api?.read, update: r.api?.update, delete: r.api?.delete})
.filter(([,v]) => !v).map(([k]) => k);
issues.push(`CRUD: missing ${missing.join(", ")}`);
}
if (!r.api?.authAllPass) {
const missing = Object.entries({register: r.api?.register, login: r.api?.login, me: r.api?.me})
.filter(([,v]) => !v).map(([k]) => k);
issues.push(`Auth: missing ${missing.join(", ")}`);
}
if (issues.length > 0) {
h(`**${r.id}:**`);
for (const i of issues) h(`- ${i}`);
h(``);
}
}
}
// Pattern Analysis
h(`## Pattern Analysis`);
h(``);
// Best domains
const scored = allResults.map(r => {
let score = 0;
if (r.frontend?.allPassed) score += 2;
if (r.backend?.allPassed) score += 2;
if (r.fullstack?.allPassed) score += 1;
if (r.build?.frontend?.build === "PASS") score += 2;
if (r.build?.backend?.build === "PASS") score += 2;
if (r.api?.crudAllPass) score += 2;
if (r.api?.authAllPass) score += 2;
return { ...r, score };
}).sort((a, b) => b.score - a.score);
h(`### Best Performing Domains`);
h(``);
h(`| Domain | Score | Key Strength |`);
h(`|--------|-------|-------------|`);
for (const r of scored.slice(0, 3)) {
h(`| ${r.id} | ${r.score}/13 | ${r.domain || "N/A"} |`);
}
h(``);
h(`### Weakest Domains`);
h(``);
h(`| Domain | Score | Key Issue |`);
h(`|--------|-------|-----------|`);
for (const r of scored.slice(-3).reverse()) {
const issues = [];
if (!r.frontend?.allPassed) issues.push("FE gen");
if (!r.backend?.allPassed) issues.push("BE gen");
if (r.build?.frontend?.build === "FAIL") issues.push("FE build");
if (r.build?.backend?.build === "FAIL") issues.push("BE typecheck");
if (!r.api?.crudAllPass) issues.push("CRUD");
if (!r.api?.authAllPass) issues.push("Auth");
h(`| ${r.id} | ${r.score}/13 | ${issues.join(", ") || "—"} |`);
}
h(``);
h(`### Domain Type Analysis`);
h(``);
h(`| Domain Type | Count | Avg Score |`);
h(`|-------------|-------|-----------|`);
const byDomain = {};
for (const r of allResults) {
const d = r.domain || "unknown";
if (!byDomain[d]) byDomain[d] = { count: 0, scores: [] };
byDomain[d].count++;
byDomain[d].scores.push(r.score);
}
for (const [domain, data] of Object.entries(byDomain)) {
const avg = (data.scores.reduce((a,b) => a + b, 0) / data.scores.length).toFixed(1);
h(`| ${domain} | ${data.count} | ${avg} |`);
}
h(``);
// Failure categories (defined above already, but recompute for Fixes section)
const feGenFailsR = allResults.filter(r => !r.frontend?.allPassed);
const beGenFailsR = allResults.filter(r => !r.backend?.allPassed);
const fsGenFailsR = allResults.filter(r => !r.fullstack?.allPassed);
const feBuildFailsR = allResults.filter(r => r.build?.frontend?.build === "FAIL");
const beBuildFailsR = allResults.filter(r => r.build?.backend?.build === "FAIL");
const crudFailsR = allResults.filter(r => !r.api?.crudAllPass);
const authFailsR = allResults.filter(r => !r.api?.authAllPass);
// Required Fixes
h(`## Required Fixes`);
h(``);
const criticalFixes = [];
const highFixes = [];
const mediumFixes = [];
// Analyze patterns
if (feBuildFailsR.length > 0) {
criticalFixes.push(`**Frontend Build Failures** (${feBuildFailsR.length}/${total}): Fix Next.js build issues in frontend-builder-agent — check tsconfig, module resolution, component imports`);
}
if (beBuildFailsR.length > 0) {
criticalFixes.push(`**Backend Typecheck Failures** (${beBuildFailsR.length}/${total}): Fix TypeScript errors in backend-builder-agent — check type definitions, import paths`);
}
if (beGenFailsR.length > 0) {
highFixes.push(`**Backend Generation Gaps** (${beGenFailsR.length}/${total}): Missing routes/services/auth in some domains`);
}
if (crudFailsR.length > 0) {
highFixes.push(`**CRUD Completeness** (${crudFailsR.length}/${total}): Some domains missing full CRUD operations`);
}
if (authFailsR.length > 0) {
highFixes.push(`**Auth Endpoint Coverage** (${authFailsR.length}/${total}): Missing register/login/me in some domains`);
}
if (feGenFailsR.length > 0) {
mediumFixes.push(`**Frontend Generation Gaps** (${feGenFailsR.length}/${total}): Missing pages or services in some domains`);
}
h(`### P0 — Critical`);
h(``);
if (criticalFixes.length === 0) {
h(`✅ No critical issues found.`);
} else {
for (const f of criticalFixes) h(`1. ${f}`);
}
h(``);
h(`### P1 — High`);
h(``);
if (highFixes.length === 0) {
h(`✅ No high-priority issues found.`);
} else {
for (let i = 0; i < highFixes.length; i++) h(`${i + 1}. ${highFixes[i]}`);
}
h(``);
h(`### P2 — Medium`);
h(``);
if (mediumFixes.length === 0) {
h(`✅ No medium-priority issues found.`);
} else {
for (let i = 0; i < mediumFixes.length; i++) h(`${i + 1}. ${mediumFixes[i]}`);
}
h(``);
// Conclusion
h(`## Conclusion`);
h(``);
const passRate = allResults.filter(r => r.score >= 11).length;
if (passRate === total) {
h(`🎉 **All ${total} domains passed!** The system demonstrates robust generalization capability across diverse business domains.`);
h(``);
h(`The RequirementPackage → Frontend Builder → Backend Builder → Fullstack Composer pipeline is production-ready for Web Fullstack generation.`);
h(``);
h(`**Next: Electron Builder can now proceed.**`);
} else {
h(`⚠️ **${passRate}/${total} domains fully passed.**`);
h(``);
h(`The pipeline shows partial generalization. ${total - passRate} domains need fixes before the system can be considered a truly general Web Fullstack Generator.`);
h(``);
h(`**Electron Builder should wait until fixes above are addressed.**`);
}
h(``);
h(`---`);
h(`*Report generated by Domain Benchmark Suite*`);
return lines.join("\n");
}
// ═══════════════════════════════════════════════════════
// Main
// ═══════════════════════════════════════════════════════
async function main() {
const args = process.argv.slice(2);
const filterDomain = args[0] === "--domain" ? args[1] : null;
const skipBuild = args.includes("--skip-build");
const skipAPI = args.includes("--skip-api");
// Filter domains if specified
const domains = filterDomain
? DOMAINS.filter(d => d.id === filterDomain)
: DOMAINS;
if (domains.length === 0) {
console.error(`Unknown domain: ${filterDomain}`);
console.error(`Available: ${DOMAINS.map(d => d.id).join(", ")}`);
process.exit(1);
}
console.log(`\n🧪 Domain Benchmark Suite`);
console.log(` Testing ${domains.length} domain(s)`);
console.log(` Pipeline: project-intake → architecture → frontend → backend → fullstack`);
if (skipBuild) console.log(` ⏭️ Skipping build validation`);
if (skipAPI) console.log(` ⏭️ Skipping API validation`);
console.log(``);
const allResults = [];
for (const domain of domains) {
console.log(`\n${"=".repeat(60)}`);
console.log(`🏗️ Domain: ${domain.id}`);
console.log(`${"=".repeat(60)}`);
const tTotal = Date.now();
// Step 1: RequirementPackage
const { prd, arch, elapsed: reqTime, prdPath, archPath } = generateRequirementPackage(domain);
// Step 2: Frontend
const frontend = generateFrontend(domain, prdPath, archPath);
// Step 3: Backend
const backend = generateBackend(domain, prdPath, archPath);
// Step 4: Fullstack
const fullstack = generateFullstack(domain, prdPath, archPath);
// Step 5: Build (optional)
let build = null;
if (!skipBuild) {
build = validateBuild(domain);
}
// Step 6: API (optional)
let api = null;
if (!skipAPI) {
api = validateAPI(domain);
}
const totalTime = ((Date.now() - tTotal) / 1000).toFixed(1);
allResults.push({
id: domain.id,
input: domain.input,
projectName: prd.projectName,
domain: prd.domain,
reqTime,
frontend,
backend,
fullstack,
build,
api,
totalTime: parseFloat(totalTime),
});
console.log(`\n ⏱️ Total: ${totalTime}s`);
}
// Generate report
const report = generateReport(allResults);
writeFileSync(REPORT_PATH, report, "utf8");
console.log(`\n📄 Report written to: ${REPORT_PATH}`);
// Print summary
const passCount = allResults.filter(r => {
return r.frontend?.allPassed && r.backend?.allPassed && r.fullstack?.allPassed;
}).length;
console.log(`\n📊 Summary: ${passCount}/${domains.length} domains generated successfully`);
// Also output JSON for programmatic consumption
const jsonPath = resolve(WORKSPACE, "benchmark-results.json");
writeFileSync(jsonPath, JSON.stringify(allResults, null, 2), "utf8");
console.log(`📄 JSON results: ${jsonPath}`);
}
main().catch(e => {
console.error("Benchmark failed:", e);
process.exit(1);
});
+349
View File
@@ -0,0 +1,349 @@
#!/usr/bin/env bash
# dream-cycle.sh — 记忆 consolidation 周期(Dream Cycle
#
# 灵感: openclaw-auto-dream 的 4 Phase Dream Cycle
# v2 — 加入远程记忆服务器同步
#
# 用法:
# bash scripts/dream-cycle.sh # 完整运行
# bash scripts/dream-cycle.sh collect # Phase 1: 扫描 daily/
# bash scripts/dream-cycle.sh sync-remote # 推送新条目到远程服务器
# bash scripts/dream-cycle.sh evaluate # Phase 3: 健康评分
# bash scripts/dream-cycle.sh health # 只看健康分
set -euo pipefail
WORKSPACE="$(cd "$(dirname "$0")/.." && pwd)"
MEMORY_DIR="$WORKSPACE/memory"
DAILY_DIR="$MEMORY_DIR/daily"
REGISTERS_DIR="$MEMORY_DIR/registers"
PROJECTS_DIR="$MEMORY_DIR/projects"
MARKER="$MEMORY_DIR/.dream-cycle-last-run"
NOW=$(date +%Y-%m-%d)
NOW_EPOCH=$(date +%s)
# 远程记忆服务器
MEMORY_SERVER="http://111.229.145.18"
# 上次运行时间
LAST_RUN=""
if [ -f "$MARKER" ]; then
LAST_RUN=$(cat "$MARKER")
fi
# ─── 智能标记检测 ──────────────────────────────────────
# 自动检测重要性(返回类型或空)
detect_importance() {
local line="$1"
# 决策句式
if echo "$line" | grep -qE '(选择|决定|配置为|建立|部署|接入|升级|迁移|切换|采用)'; then
echo "decision"
return
fi
# 偏好表达
if echo "$line" | grep -qE '(喜欢|习惯|总是|不要|偏好|改为|改成|以后都)'; then
echo "preference"
return
fi
# 纠正信号
if echo "$line" | grep -qE '(不对|应该是|更正|其实是|原来是|发现.*错|误)'; then
echo "correction"
return
fi
# 重要事件
if echo "$line" | grep -qE '(完成|成功|失败|上线|修复|解决|搞定|落地|生效)'; then
echo "event"
return
fi
# 工具/服务状态
if echo "$line" | grep -qE '(状态|正常|异常|在线|离线|连接|断开)'; then
echo "status"
return
fi
echo ""
}
# ─── Phase 1: Collect — 扫描 daily/ ──────────────────────
collect() {
echo "📡 Phase 1: Collect — 日志扫描(智能标记)"
echo " 上次运行: ${LAST_RUN:-从未}"
echo ""
local tempfile=$(mktemp)
local count=0
for f in "$DAILY_DIR"/*.md; do
[ -f "$f" ] || continue
local fname=$(basename "$f")
local fdate="${fname%.md}"
# 跳过旧文件
if [ -n "$LAST_RUN" ] && [ "$fdate" != "$LAST_RUN" ] && [[ "$fdate" < "$LAST_RUN" ]]; then
continue
fi
while IFS= read -r line; do
local trimmed=$(echo "$line" | sed 's/^\*\*//;s/\*\*$//')
local type=""
# 1. 显式标记优先
if echo "$trimmed" | grep -q '\[session-flush\]'; then
type="session-summary"
local ctx=$(grep -A 5 '\[session-flush\]' "$f" 2>/dev/null | grep '^-' | head -4 | tr '\n' ' ' | head -c 500 || true)
[ -n "$ctx" ] && echo "$fdate|$type|$ctx" >> "$tempfile" && count=$((count+1)) && continue
fi
if echo "$trimmed" | grep -q '\[correction'; then
type="correction"
local ctx=$(echo "$trimmed" | head -c 300)
[ -n "$ctx" ] && echo "$fdate|$type|$ctx" >> "$tempfile" && count=$((count+1)) && continue
fi
# 2. 智能检测(只对非标题行、非空行)
if [ -n "$trimmed" ] && ! echo "$trimmed" | grep -qE '^(#|\||-*$|>)'; then
type=$(detect_importance "$trimmed")
if [ -n "$type" ]; then
local ctx=$(echo "$trimmed" | head -c 300)
echo "$fdate|$type|$ctx" >> "$tempfile" && count=$((count+1))
fi
fi
done < "$f"
done
echo " 收集 $count 条待同步"
echo "$tempfile"
}
# ─── Phase 1.5: Sync Remote — 推送到记忆服务器 ────────────
sync_remote() {
echo "📤 Phase 1.5: Sync Remote → $MEMORY_SERVER"
echo ""
# 用 collect 扫出新条目
local tmpfile=$(collect | tail -1)
[ ! -f "$tmpfile" ] && echo " 无待同步条目" && return
local total=0 failed=0
local tmpfile2=$(mktemp)
# 逐行解析发送
while IFS='|' read -r date type content; do
[ -z "$content" ] && continue
local clean=$(echo "$content" | sed 's/^[- ]*//;s/"/\\"/g')
[ -z "$clean" ] && continue
local resp=$(curl -s --max-time 5 -X POST "$MEMORY_SERVER/api/v2/add" \
-H "Content-Type: application/json" \
-H "X-API-Key: ${MEMORY_API_KEY:-}" \
-d "{\"content\":\"$clean\",\"type\":\"$type\",\"project\":\"xiaolong\",\"source\":\"dream-cycle\"}" 2>/dev/null || echo '{"error":"curl fail"}')
if echo "$resp" | grep -q 'stored'; then
total=$((total+1))
else
failed=$((failed+1))
fi
done < "$tmpfile"
echo " 成功 $total / 失败 $failed"
[ "$total" -gt 0 ] && echo " ✅ 记忆已写入远程服务器"
echo "$NOW" > "$MARKER"
rm -f "$tmpfile" "$tmpfile2"
}
# ─── Phase 1.6: Sync Context Graph — 推送结构化记忆 ─────────
sync_context_graph() {
echo "🧬 Phase 1.6: Sync ContextGraph → $MEMORY_SERVER"
if [ -f "$WORKSPACE/src/memory/sync-context-graph.js" ]; then
node "$WORKSPACE/src/memory/sync-context-graph.js" 2>&1 | sed 's/^/ /'
echo " ✅ ContextGraph 已同步"
else
echo " ⚠️ sync-context-graph.js 未找到,跳过"
fi
echo ""
}
# ─── Phase 2: Consolidate — 本地路由 ──────────────────────
consolidate() {
echo "🔄 Phase 2: Consolidate — 本地路由"
echo ""
mkdir -p "$REGISTERS_DIR"
for reg in "$REGISTERS_DIR"/*.md; do
[ -f "$reg" ] || continue
local name=$(basename "$reg" .md)
local cnt=$(grep -c '\- \[' "$reg" 2>/dev/null || echo 0)
echo " registers/$name.md: ~${cnt}"
done
local proj_cnt=$(ls "$PROJECTS_DIR"/*.md 2>/dev/null | wc -l | tr -d ' ')
local vault_lines=$(wc -l < "$MEMORY_DIR/vault.md" 2>/dev/null || echo 0)
local mem_lines=$(wc -l < "$WORKSPACE/MEMORY.md" 2>/dev/null || echo 0)
echo " projects/: ${proj_cnt}"
echo " vault.md: ${vault_lines}"
echo " MEMORY.md: ${mem_lines}"
}
# ─── Phase 2.5: Refresh Core Memory — 动态刷新核心记忆块 ───
refresh_core_memory() {
echo "🧠 Phase 2.5: Refresh Core Memory Blocks"
echo ""
local core_dir="$MEMORY_DIR/core"
[ ! -d "$core_dir" ] && echo " ⚠️ memory/core/ 不存在,跳过" && return
local now_iso=$(date -Iseconds)
# 1. active_projects.md — 从最近 3 天的 daily/ 提取项目动态
local active_file="$core_dir/active_projects.md"
if [ -f "$active_file" ]; then
local recent_projects=$(grep -h '\*\*.*\*\*' "$DAILY_DIR"/*.md 2>/dev/null | grep -E '(项目|仓库|同步|接入|部署|升级|完成|进行中)' | tail -10 | sed 's/^/- /' || true)
if [ -n "$recent_projects" ]; then
local header=$(head -8 "$active_file")
echo "$header" > "$active_file"
echo "" >> "$active_file"
echo "## 进行中" >> "$active_file"
echo "" >> "$active_file"
echo "$recent_projects" >> "$active_file"
sed -i "" "s/updated_at:.*/updated_at: \"$now_iso\"/" "$active_file" 2>/dev/null || true
echo " ✅ active_projects.md 已刷新"
fi
fi
# 2. recent_decisions.md — 从最近 7 天的 vault.md 提取决策
local decisions_file="$core_dir/recent_decisions.md"
if [ -f "$decisions_file" ]; then
local recent_decisions=$(grep -E '^- [0-9]{4}-[0-9]{2}-[0-9]{2}:' "$MEMORY_DIR/vault.md" 2>/dev/null | tail -10 || true)
if [ -n "$recent_decisions" ]; then
local header=$(head -8 "$decisions_file")
echo "$header" > "$decisions_file"
echo "" >> "$decisions_file"
echo "# 最近决策" >> "$decisions_file"
echo "" >> "$decisions_file"
echo "$recent_decisions" >> "$decisions_file"
sed -i "" "s/updated_at:.*/updated_at: \"$now_iso\"/" "$decisions_file" 2>/dev/null || true
echo " ✅ recent_decisions.md 已刷新"
fi
fi
# 3. tool_status.md — 只更新时间戳,内容由事件触发更新
local tool_file="$core_dir/tool_status.md"
if [ -f "$tool_file" ]; then
sed -i "" "s/updated_at:.*/updated_at: \"$now_iso\"/" "$tool_file" 2>/dev/null || true
echo " ✅ tool_status.md 时间戳已更新"
fi
echo ""
}
# ─── Phase 3: Evaluate — 健康评分 ─────────────────────────
evaluate() {
echo "📊 Phase 3: Evaluate — 健康评分"
echo ""
local total_daily=$(ls "$DAILY_DIR"/*.md 2>/dev/null | wc -l | tr -d ' ')
local reg_count=0 updated_regs=0
for f in "$REGISTERS_DIR"/*.md; do
[ -f "$f" ] || continue
reg_count=$((reg_count+1))
local mtime=$(stat -f "%m" "$f" 2>/dev/null || echo 0)
[ "$mtime" -gt 0 ] && [ $(( (NOW_EPOCH-mtime)/86400 )) -le 30 ] && updated_regs=$((updated_regs+1))
done
local vault_lines=$(wc -l < "$MEMORY_DIR/vault.md" 2>/dev/null || echo 0)
# 各维度评分
local freshness=100; [ "$total_daily" -gt 0 ] && freshness=$(( $(ls -t "$DAILY_DIR"/*.md 2>/dev/null | head -1 | xargs stat -f "%m" 2>/dev/null || echo 0) > $((NOW_EPOCH-86400*7)) ? 100 : 50 ))
local coverage=100; [ "$reg_count" -gt 0 ] && coverage=$(( updated_regs*100/reg_count ))
local efficiency=100; [ "$vault_lines" -gt 200 ] && efficiency=60
local reliability=90
local security=95
local health=$(( (freshness*25+coverage*25+efficiency*20+reliability*15+security*15)/100 ))
echo " 📈 健康评分: ${health}/100"
echo " Freshness: ${freshness}/100 | Coverage: ${coverage}/100 | Efficiency: ${efficiency}/100"
echo " Reliability: ${reliability}/100 | Security: ${security}/100"
echo " daily: ${total_daily}篇 | registers: ${reg_count}个 | vault: ${vault_lines}"
}
# ─── 入口 ───────────────────────────────────────────
case "${1:-all}" in
collect) collect ;;
sync-remote) sync_remote ;;
sync-graph) sync_context_graph ;;
evaluate) evaluate ;;
health) evaluate 2>&1 | grep -E '健康|Freshness|daily' ;;
all)
echo "🌙 小龙的 Dream Cycle — $NOW $(date +%H:%M)"
echo "============================================"
echo ""
sync_remote
sync_context_graph
echo ""
refresh_core_memory
echo ""
consolidate
forget_context_graph
echo ""
evaluate
echo ""
echo "✅ Dream Cycle 完成 (下一轮: 每天 4:00)"
;;
core-refresh) refresh_core_memory ;;
*) echo "用法: bash scripts/dream-cycle.sh [collect|sync-remote|evaluate|health]"; exit 1 ;;
esac
# ─── Phase 1.7: ContextGraph 遗忘机制 ─────────────────────
forget_context_graph() {
echo "🧠 Phase 1.7: ContextGraph 遗忘机制"
[ ! -f ~/.openclaw/context-graph.json ] && echo " 无文件" && return
node -e "
const fs = require('fs');
const g = JSON.parse(fs.readFileSync(process.env.HOME + '/.openclaw/context-graph.json', 'utf8'));
let removed = 0;
const now = Date.now();
const THRESHOLD = 90 * 86400 * 1000; // 90天
for (const [id, entity] of Object.entries(g.entities)) {
// 遗忘条件:非 pinned + 最后观察 >90天 + 低优先级
if (entity.pinned) continue;
if (!entity.observations || entity.observations.length === 0) continue;
const lastObs = entity.observations[entity.observations.length - 1];
const age = now - new Date(lastObs.timestamp).getTime();
const hasHighPriority = entity.observations.some(o =>
o.priority === 'critical' || o.priority === 'high'
);
if (age > THRESHOLD && !hasHighPriority) {
delete g.entities[id];
removed++;
}
}
// 清理孤立关系
const before = g.relations.length;
g.relations = g.relations.filter(r =>
g.entities[r.from] && g.entities[r.to]
);
const relRemoved = before - g.relations.length;
fs.writeFileSync(process.env.HOME + '/.openclaw/context-graph.json', JSON.stringify(g, null, 2));
console.log(' 遗忘实体: ' + removed + ' | 孤立关系: ' + relRemoved);
" 2>/dev/null || echo " 跳过"
}
+385
View File
@@ -0,0 +1,385 @@
#!/usr/bin/env node
/**
* End-to-End Generator Benchmark v1
*
* 完整链路:需求 → 前端 → 后端 → 全栈 → Electron → Release
* 10 个领域全量验证,输出 end-to-end-benchmark-v1.md
*/
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
import { resolve, dirname, join } from "node:path";
import { execSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const __dirname = dirname(resolve(fileURLToPath(import.meta.url)));
const WORKSPACE = resolve(__dirname, "..");
const BENCH_ROOT = resolve(WORKSPACE, ".benchmark");
const SCRIPTS = resolve(WORKSPACE, "scripts");
const REPORT_PATH = resolve(WORKSPACE, "end-to-end-benchmark-v1.md");
const DOMAINS = [
{ id: "petcare", input: "做一个宠物护理管理平台,宠物主人可以管理宠物档案、健康日程、日常记录和成长相册" },
{ id: "crm", input: "做一个客户关系管理系统,支持客户管理、销售漏斗、跟进记录和数据分析仪表盘" },
{ id: "inventory", input: "做一个库存管理系统,支持商品入库出库、库存盘点、供应商管理和库存预警" },
{ id: "ticket", input: "做一个工单系统,支持工单创建、分配、处理流程、优先级管理和工单归档" },
{ id: "blog-cms", input: "做一个博客内容管理系统,支持文章发布、分类标签、评论管理和媒体库" },
{ id: "project-mgmt", input: "做一个项目管理系统,支持项目看板、任务分配、甘特图和团队协作" },
{ id: "hr", input: "做一个人力资源管理系统,支持员工档案、考勤管理、招聘流程和绩效评估" },
{ id: "asset", input: "做一个固定资产管理系统,支持资产登记、领用归还、折旧计算和盘点统计" },
{ id: "course", input: "做一个在线课程管理系统,支持课程发布、章节管理、学员进度和作业批改" },
{ id: "appointment", input: "做一个预约管理系统,支持服务项目、时间段预约、客户通知和预约统计" },
];
// ═══════════════════════════════════════════════════════
// Helpers
// ═══════════════════════════════════════════════════════
function cmd(c, opts = {}) {
try {
return execSync(c, { cwd: WORKSPACE, encoding: "utf8", ...opts, stdio: opts.stdio ?? "pipe" });
} catch (e) {
return { error: e.message, stderr: e.stderr?.toString() || "", stdout: e.stdout?.toString() || "" };
}
}
function runAgent(script, args) {
const t0 = Date.now();
const result = cmd(`node ${join(SCRIPTS, script)} ${args}`);
const ms = Date.now() - t0;
if (result.error) {
try { return { ms, ...JSON.parse(result.stdout?.trim() || "{}") }; } catch { return { ms, error: result.error }; }
}
try { return { ms, ...JSON.parse(result.trim()) }; } catch { return { ms, raw: result }; }
}
function countFiles(dir) {
try {
const r = execSync(`find ${dir} -type f 2>/dev/null | wc -l`, { encoding: "utf8", cwd: WORKSPACE });
return parseInt(r.trim(), 10);
} catch { return 0; }
}
function loadJSON(path) {
try { return JSON.parse(readFileSync(path, "utf8")); } catch { return {}; }
}
// ═══════════════════════════════════════════════════════
// Validation
// ═══════════════════════════════════════════════════════
function validateFrontend(dir) {
if (!existsSync(dir)) return { pass: false, reason: "dir missing" };
const checks = {
packageJson: existsSync(join(dir, "package.json")),
appDir: existsSync(join(dir, "app")),
services: existsSync(join(dir, "services")) || existsSync(join(dir, "src", "services")),
types: existsSync(join(dir, "types")) || existsSync(join(dir, "src", "types")),
};
const fileCount = countFiles(dir);
const pass = checks.packageJson && checks.appDir && fileCount > 10;
return { pass, fileCount, ...checks };
}
function validateBackend(dir) {
if (!existsSync(dir)) return { pass: false, reason: "dir missing" };
const checks = {
packageJson: existsSync(join(dir, "package.json")),
routes: existsSync(join(dir, "src", "routes")),
services: existsSync(join(dir, "src", "services")),
auth: existsSync(join(dir, "src", "routes", "auth.ts")) || existsSync(join(dir, "src", "routes", "auth-route.ts")),
db: existsSync(join(dir, "src", "db")) || existsSync(join(dir, "src", "schema")),
};
const fileCount = countFiles(dir);
const pass = checks.packageJson && checks.routes && checks.services && fileCount > 10;
return { pass, fileCount, ...checks };
}
function validateFullstack(dir) {
if (!existsSync(dir)) return { pass: false, reason: "dir missing" };
const checks = {
packageJson: existsSync(join(dir, "package.json")),
web: existsSync(join(dir, "apps", "web")),
api: existsSync(join(dir, "apps", "api")),
shared: existsSync(join(dir, "packages")),
};
const fileCount = countFiles(dir);
const pass = checks.packageJson && checks.web && checks.api && fileCount > 30;
return { pass, fileCount, ...checks };
}
function validateElectron(dir) {
if (!existsSync(dir)) return { pass: false, reason: "dir missing" };
const checks = {
packageJson: existsSync(join(dir, "package.json")),
main: existsSync(join(dir, "electron", "main.ts")),
preload: existsSync(join(dir, "electron", "preload.ts")),
ipc: existsSync(join(dir, "electron", "ipc.ts")),
yml: existsSync(join(dir, "electron-builder.yml")),
};
const fileCount = countFiles(dir);
const pass = checks.packageJson && checks.main && checks.preload && fileCount >= 8;
return { pass, fileCount, ...checks };
}
function validateRelease(dir) {
if (!existsSync(dir)) return { pass: false, reason: "dir missing" };
const rDir = join(dir, "release");
const checks = {
versionJson: existsSync(join(rDir, "version.json")),
manifest: existsSync(join(rDir, "manifests", "manifest.json")),
checksums: existsSync(join(rDir, "checksums", "checksums.txt")),
releaseNotes: existsSync(join(rDir, "release-notes", "release-notes.md")),
buildInfo: existsSync(join(rDir, "build-info.json")),
windows: existsSync(join(rDir, "windows")),
macos: existsSync(join(rDir, "macos")),
linux: existsSync(join(rDir, "linux")),
};
// Validate content
if (checks.versionJson) {
try {
const v = JSON.parse(readFileSync(join(rDir, "version.json"), "utf8"));
checks.versionValid = !!(v.name && v.version && v.platforms);
} catch { checks.versionValid = false; }
}
if (checks.manifest) {
try {
const m = JSON.parse(readFileSync(join(rDir, "manifests", "manifest.json"), "utf8"));
checks.manifestValid = !!(m.project && m.files?.length > 0);
} catch { checks.manifestValid = false; }
}
if (checks.checksums) {
const content = readFileSync(join(rDir, "checksums", "checksums.txt"), "utf8");
const lines = content.trim().split("\n").filter(l => l.length > 0);
checks.checksumCount = lines.length;
checks.checksumsValid = lines.every(l => l.split(/\s+/)[0]?.length === 64);
}
if (checks.releaseNotes) {
const notes = readFileSync(join(rDir, "release-notes", "release-notes.md"), "utf8");
checks.notesValid = notes.includes("Version") && notes.includes("Installation");
}
if (checks.buildInfo) {
try {
const bi = JSON.parse(readFileSync(join(rDir, "build-info.json"), "utf8"));
checks.buildInfoValid = !!(bi.nodeVersion && bi.generatorVersion);
} catch { checks.buildInfoValid = false; }
}
// Platform file counts
for (const p of ["windows", "macos", "linux"]) {
if (checks[p]) {
try { checks[p + "Count"] = readdirSync(join(rDir, p)).length; } catch { checks[p + "Count"] = 0; }
}
}
const fileCount = countFiles(dir);
const pass = Object.values(checks).every(v => v !== false && v !== 0);
return { pass, fileCount, ...checks };
}
// ═══════════════════════════════════════════════════════
// Pipeline
// ═══════════════════════════════════════════════════════
function runPipeline(domain) {
const B = join(BENCH_ROOT, domain.id);
mkdirSync(B, { recursive: true });
const result = { id: domain.id, input: domain.input, stages: {} };
// SF-01: PRD
const s1 = runAgent("project-intake-agent.mjs", `--input "${domain.input}" --output "${join(B, "prd.json")}"`);
result.stages.prd = { ms: s1.ms, projectName: s1.projectName, domain: s1.domain, error: s1.error };
result.projectName = s1.projectName;
// SF-02: Architecture
const s2 = runAgent("architecture-agent.mjs", `--input "${join(B, "prd.json")}" --output "${join(B, "arch.json")}"`);
result.stages.arch = { ms: s2.ms, modules: s2.moduleCount, error: s2.error };
// SF-03: Frontend
const feOut = join(B, "frontend");
const s3 = runAgent("frontend-builder-agent.mjs", `--prd "${join(B, "prd.json")}" --arch "${join(B, "arch.json")}" --output "${feOut}"`);
const feVal = validateFrontend(feOut);
result.stages.frontend = { ms: s3.ms, files: s3.stats?.totalFiles || feVal.fileCount, pass: feVal.pass, error: s3.error };
// SF-04: Backend
const beOut = join(B, "backend");
const s4 = runAgent("backend-builder-agent.mjs", `--prd "${join(B, "prd.json")}" --arch "${join(B, "arch.json")}" --output "${beOut}"`);
const beVal = validateBackend(beOut);
result.stages.backend = { ms: s4.ms, files: s4.stats?.totalFiles || beVal.fileCount, pass: beVal.pass, error: s4.error };
// SF-05: Fullstack
const fsOut = join(B, "fullstack");
const s5 = runAgent("fullstack-composer-agent.mjs", `--prd "${join(B, "prd.json")}" --arch "${join(B, "arch.json")}" --output "${fsOut}"`);
const fsVal = validateFullstack(fsOut);
result.stages.fullstack = { ms: s5.ms, files: s5.stats?.totalFiles || fsVal.fileCount, pass: fsVal.pass, error: s5.error };
// SF-06: Electron
const elOut = join(B, "electron");
const s6 = runAgent("electron-builder-agent.mjs", `--input "${fsOut}" --output "${elOut}" --prd "${join(B, "prd.json")}"`);
const elVal = validateElectron(elOut);
result.stages.electron = { ms: s6.ms, files: s6.stats?.totalFiles || elVal.fileCount, pass: elVal.pass, error: s6.error };
// SF-07: Release
const rlOut = join(B, "release");
const s7 = runAgent("release-builder-agent.mjs", `--input "${fsOut}" --output "${rlOut}"`);
const rlVal = validateRelease(rlOut);
result.stages.release = { ms: s7.ms, files: s7.stats?.totalFiles || rlVal.fileCount, pass: rlVal.pass, error: s7.error };
// Totals
result.totalMs = Object.values(result.stages).reduce((s, v) => s + (v.ms || 0), 0);
result.totalFiles = Object.values(result.stages).reduce((s, v) => s + (v.files || 0), 0);
result.allPass = Object.values(result.stages).every(v => v.pass !== false);
return result;
}
// ═══════════════════════════════════════════════════════
// Report
// ═══════════════════════════════════════════════════════
function generateReport(allResults) {
const L = [];
const h = (t) => { L.push(t); return L; };
const now = new Date();
h(`# End-to-End Generator Benchmark v1`);
h(``);
h(`> ${now.toISOString()}`);
h(`> 链路: 需求 → 前端 → 后端 → 全栈 → Electron → Release`);
h(`> 领域: ${allResults.length}`);
h(``);
// ── Summary ──
const passCount = allResults.filter(r => r.allPass).length;
const totalFiles = allResults.reduce((s, r) => s + r.totalFiles, 0);
const avgMs = (allResults.reduce((s, r) => s + r.totalMs, 0) / allResults.length).toFixed(0);
h(`## Summary`);
h(``);
h(`| Metric | Value |`);
h(`|--------|-------|`);
h(`| Domains | ${allResults.length} |`);
h(`| PASS | ${passCount}/${allResults.length} |`);
h(`| FAIL | ${allResults.length - passCount}/${allResults.length} |`);
h(`| Total Files Generated | ${totalFiles} |`);
h(`| Avg Pipeline Time | ${avgMs}ms |`);
h(``);
// ── Domain Results ──
h(`## Domain Results`);
h(``);
h(`| Domain | PRD | Arch | Frontend | Backend | Fullstack | Electron | Release | Files | Time | Result |`);
h(`|--------|-----|------|----------|---------|-----------|----------|---------|-------|------|--------|`);
for (const r of allResults) {
const s = r.stages;
const p = (v) => v.pass === false ? "❌" : "✅";
const t = (v) => v.ms ? `${v.ms}ms` : "—";
h(`| ${r.id} | ${t(s.prd)} | ${t(s.arch)} | ${p(s.frontend)} ${s.frontend.files || 0}f | ${p(s.backend)} ${s.backend.files || 0}f | ${p(s.fullstack)} ${s.fullstack.files || 0}f | ${p(s.electron)} ${s.electron.files || 0}f | ${p(s.release)} ${s.release.files || 0}f | ${r.totalFiles} | ${r.totalMs}ms | **${r.allPass ? "PASS" : "FAIL"}** |`);
}
h(``);
// ── Per-Stage Stats ──
h(`## Per-Stage Statistics`);
h(``);
const stages = ["prd", "arch", "frontend", "backend", "fullstack", "electron", "release"];
const stageNames = ["SF-01 PRD", "SF-02 Arch", "SF-03 Frontend", "SF-04 Backend", "SF-05 Fullstack", "SF-06 Electron", "SF-07 Release"];
h(`| Stage | Avg Time | Min | Max | Avg Files | Pass |`);
h(`|-------|----------|-----|-----|-----------|------|`);
for (let i = 0; i < stages.length; i++) {
const key = stages[i];
const times = allResults.map(r => r.stages[key]?.ms || 0);
const files = allResults.map(r => r.stages[key]?.files || 0);
const passes = allResults.filter(r => r.stages[key]?.pass !== false).length;
const avg = (times.reduce((a, b) => a + b, 0) / times.length).toFixed(0);
const min = Math.min(...times);
const max = Math.max(...times);
const avgF = (files.reduce((a, b) => a + b, 0) / files.length).toFixed(0);
h(`| ${stageNames[i]} | ${avg}ms | ${min}ms | ${max}ms | ${avgF} | ${passes}/${allResults.length} |`);
}
h(``);
// ── File Generation ──
h(`## File Generation`);
h(``);
h(`| Domain | Frontend | Backend | Fullstack | Electron | Release | Total |`);
h(`|--------|----------|---------|-----------|----------|---------|-------|`);
for (const r of allResults) {
const s = r.stages;
h(`| ${r.id} | ${s.frontend.files || 0} | ${s.backend.files || 0} | ${s.fullstack.files || 0} | ${s.electron.files || 0} | ${s.release.files || 0} | ${r.totalFiles} |`);
}
h(``);
// ── Failure Details ──
const failures = allResults.filter(r => !r.allPass);
if (failures.length > 0) {
h(`## Failure Details`);
h(``);
for (const r of failures) {
h(`### ${r.id}`);
h(``);
for (const [stage, data] of Object.entries(r.stages)) {
if (data.pass === false) {
h(`- **${stage}:** FAIL${data.error ? " — " + data.error : ""}`);
}
}
h(``);
}
}
// ── Final Status ──
h(`## Final Status`);
h(``);
if (passCount === allResults.length) {
h(`🎉 **End-to-End Generator — PASS** (${passCount}/${allResults.length})`);
h(``);
h(`完整链路 需求 → 前端 → 后端 → 全栈 → Electron → Release 全部通过。`);
h(`系统具备跨领域泛化能力,可作为通用 Web 全栈 + 桌面应用生成器。`);
} else {
h(`⚠️ **End-to-End Generator — PARTIAL** (${passCount}/${allResults.length})`);
h(``);
h(`Failed: ${failures.map(r => r.id).join(", ")}`);
}
h(``);
h(`---`);
h(`*End-to-End Generator Benchmark v1 — ${now.toISOString()}*`);
return L.join("\n");
}
// ═══════════════════════════════════════════════════════
// Main
// ═══════════════════════════════════════════════════════
async function main() {
console.log(`\n🧪 End-to-End Generator Benchmark v1`);
console.log(` ${DOMAINS.length} domains × 7 stages = ${DOMAINS.length * 7} checks`);
console.log(` 链路: 需求 → 前端 → 后端 → 全栈 → Electron → Release\n`);
const allResults = [];
for (const domain of DOMAINS) {
console.log(`${"─".repeat(50)}`);
console.log(`🏗️ ${domain.id}`);
const result = runPipeline(domain);
allResults.push(result);
const status = result.allPass ? "✅ PASS" : "❌ FAIL";
console.log(` ${result.projectName || "?"} | ${result.totalFiles} files | ${result.totalMs}ms | ${status}`);
}
const report = generateReport(allResults);
writeFileSync(REPORT_PATH, report, "utf8");
const passCount = allResults.filter(r => r.allPass).length;
console.log(`\n${"═".repeat(50)}`);
console.log(`📊 ${passCount}/${DOMAINS.length} PASS`);
console.log(`📄 ${REPORT_PATH}`);
}
main().catch(e => {
console.error("Benchmark failed:", e);
process.exit(1);
});
File diff suppressed because it is too large Load Diff
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# extract-skill — 从已完成任务中提取可复用能力
# 用法:bash scripts/extract-skill.sh <skill-name> "<description>"
# 示例:bash scripts/extract-skill.sh springboot-api "SpringBoot 接口开发流程"
#
# 输出:tools/<skill-name>.md
set -euo pipefail
if [ $# -lt 2 ]; then
echo "用法: bash scripts/extract-skill.sh <skill-name> \"<description>\""
echo "示例: bash scripts/extract-skill.sh project-scan \"项目结构扫描流程\""
exit 1
fi
SKILL_NAME="$1"
SKILL_DESC="$2"
SKILL_FILE="tools/${SKILL_NAME}.md"
if [ -f "$SKILL_FILE" ]; then
echo "⚠️ Skill 已存在: $SKILL_FILE"
echo " 使用 extract-skill 更新已有 skillbash scripts/extract-skill.sh ${SKILL_NAME} \"$(cat "$SKILL_FILE" | head -1)\""
exit 1
fi
cat > "$SKILL_FILE" << EOF
# ${SKILL_NAME} — ${SKILL_DESC}
> 从 Hermes Agent 工作流理念提取
## 适用场景
描述什么情况下使用这个 skill。
## 输入
- 输入 1:描述
- 输入 2:描述
## 执行步骤
1. 第一步
2. 第二步
3. 第三步
## 常见风险
- 风险 1
- 风险 2
## 验证方式
如何确认任务完成且正确。
## 输出格式
描述 task 完成时的交付物格式。
EOF
echo "✅ Skill 已创建: tools/${SKILL_NAME}.md"
echo ""
echo "下一步:"
echo " 1. 编辑 $SKILL_FILE 填入具体内容"
echo " 2. 在 MEMORY.md 的记录"已固化的能力"中引用它"
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env node
/**
* Factory Router — V2 Protocol Stack → Full-stack Factory Bridge
*
* Routes a structured PRD to the appropriate factory chain.
* No factory logic lives here — only routing decisions.
*
* Current factories:
* - fullstack → SF-01 (project-intake) + SF-02 (architecture) + SF-03~06 (fullstack-composer)
*
* Future factories (routing placeholder):
* - research → 科研工厂
* - writing → 写作工厂
* - analysis → 分析工厂
*
* @module factory-router
*/
/**
* Route a PRD to the appropriate factory.
* Currently only "fullstack" is implemented.
*
* @param {object} prd — Structured PRD from SF-01
* @param {object} opts
* @param {object} opts.arch — Architecture from SF-02 (optional, auto-generated if missing)
* @param {string} opts.factory — Force a factory ("fullstack" | "research" | "writing" | "analysis")
* @returns {{ factory: string, factoryFn: string, needsArch: boolean }}
*/
export function route(prd, opts = {}) {
const domain = prd.domain || "generic";
// Resolve factory name
let factory = opts.factory || _inferFactory(domain, prd);
// All factories need architecture → default to fullstack pipeline for now
if (factory === "fullstack") {
return {
factory: "fullstack",
factoryFn: "./fullstack-composer-agent.mjs",
needsArch: !opts.arch,
};
}
// Unknown or future factory → default to fullstack
return {
factory: "fullstack",
factoryFn: "./fullstack-composer-agent.mjs",
needsArch: !opts.arch,
};
}
/**
* Infer factory type from domain and PRD content.
*
* @param {string} domain
* @param {object} prd
* @returns {string} factory name
*/
function _inferFactory(domain, prd) {
// Research domains → research factory (future)
const researchDomains = ["research", "academic", "scientific", "paper", "thesis"];
if (researchDomains.includes(domain)) return "research";
// Writing domains → writing factory (future)
const writingDomains = ["writing", "blog", "article", "news"];
if (writingDomains.includes(domain)) return "writing";
// Analysis domains → analysis factory (future)
const analysisDomains = ["analytics", "dashboard", "report"];
if (analysisDomains.includes(domain)) return "analysis";
// Default: fullstack factory
return "fullstack";
}
/**
* Load a factory module. Keeps dynamic import lazy.
*
* @param {string} factoryFn — Path to factory script (relative to scripts/)
* @returns {Promise<object>} factory module
*/
export async function loadFactory(factoryFn) {
return await import(factoryFn);
}
File diff suppressed because it is too large Load Diff
+764
View File
@@ -0,0 +1,764 @@
#!/usr/bin/env node
/**
* Fullstack Composer Agent — SF-05
*
* 组合 frontend-builder-agent (SF-03) + backend-builder-agent (SF-04)
* 从同一个 SF-02 RequirementPackage 生成完整可运行的全栈项目。
*
* 输出结构:
* apps/web/ — Next.js 前端 (SF-03)
* apps/api/ — Fastify 后端 (SF-04)
* packages/shared-types/ — 前后端共享类型
* packages/shared-config/ — 前后端共享配置
* 根目录 — workspace package.json / README / scripts
*
* Usage:
* node scripts/fullstack-composer-agent.mjs --prd <path> --arch <path> [options]
* node scripts/fullstack-composer-agent.mjs --input-text "<需求>" [options]
*
* Options:
* --prd <path> PRD JSONSF-01 输出)
* --arch <path> Architecture JSONSF-02 输出)
* --input-text <text> 直接传入需求
* --output <dir> 输出目录(default: fullstack/
* --verbose 详细输出
* --help 显示帮助
*
* @module fullstack-composer-agent
*/
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { createContract, toCamel, tsType as contractTsType } from "./model-contract.mjs";
const __dirname = dirname(resolve(fileURLToPath(import.meta.url)));
const WORKSPACE = resolve(__dirname, "..");
const DEFAULT_OUTPUT = resolve(WORKSPACE, "fullstack");
// ═══════════════════════════════════════════════════════
// 1. Shared Types Generator
// ═══════════════════════════════════════════════════════
function sqliteTypeForTs(field) {
const t = (field.type || "").toUpperCase();
if (t.includes("INT") || t.includes("SERIAL") || t.includes("BIGINT") || t.includes("DECIMAL") || t.includes("NUMERIC") || t.includes("FLOAT") || t.includes("DOUBLE") || t.includes("REAL")) return "number";
if (t.includes("BOOL")) return "boolean";
return "string";
}
function isRequired(field) {
const c = (field.constraints || "").toUpperCase();
return c.includes("NOT NULL") || c.includes("PK");
}
function pascalCase(s) {
return s.charAt(0).toUpperCase() + s.slice(1).replace(/[-_]([a-zA-Z])/g, (_, c) => c.toUpperCase());
}
/** Words where final "s" is part of the root, not a plural marker. */
const SINGULAR_EXCEPTIONS = new Set([
"status", "bus", "campus", "focus", "bonus", "virus", "genius",
"census", "ensus", "consensus", "apparatus", "fetus", "hiatus",
]);
function singularize(s) {
if (s.endsWith("ies")) return s.slice(0, -3) + "y";
if (s.endsWith("ses") || s.endsWith("xes") || s.endsWith("ches") || s.endsWith("shes")) return s.slice(0, -2);
if (s.endsWith("s") && !s.endsWith("ss")) {
const lastSeg = s.includes("_") ? s.split("_").pop() : s;
if (SINGULAR_EXCEPTIONS.has(lastSeg)) return s;
return s.slice(0, -1);
}
return s;
}
/**
* Generate shared types that both frontend and backend can import.
* Includes entity types + API response wrappers only.
*/
function generateSharedTypes(contract) {
const entities = contract.entities || [];
const lines = [
`// Shared types for fullstack project`,
`// Auto-generated — used by both apps/web and apps/api`,
``,
`// ─── Entities ────────────────────────────────────────`,
];
const generated = new Set();
// Include User and all entities from contract
for (const entity of entities) {
const name = entity.name;
if (generated.has(name)) continue;
generated.add(name);
lines.push(`export interface ${name} {`);
for (const f of entity.fields) {
const fname = toCamel(contract, f.name);
const optional = f.required ? "" : "?";
lines.push(` ${fname}${optional}: ${contractTsType(f)};`);
}
lines.push(`}`);
lines.push(``);
}
// ─── API Response Wrappers ──────────────────────────
lines.push(`// ─── API Response Wrappers ───────────────────────────`);
lines.push(`export interface ApiResponse<T> {`);
lines.push(` data: T;`);
lines.push(` message?: string;`);
lines.push(`}`);
lines.push(``);
lines.push(`export interface PaginatedResponse<T> {`);
lines.push(` data: T[];`);
lines.push(` total: number;`);
lines.push(` page: number;`);
lines.push(` pageSize: number;`);
lines.push(`}`);
lines.push(``);
lines.push(`export interface ErrorResponse {`);
lines.push(` error: string;`);
lines.push(` message: string;`);
lines.push(` statusCode: number;`);
lines.push(`}`);
lines.push(``);
return lines.join("\n");
}
// ═══════════════════════════════════════════════════════
// 2. Shared Config Generator
// ═══════════════════════════════════════════════════════
function generateSharedConfig() {
return {
"src/index.ts": `// Shared configuration for fullstack project
/** API base URL — reads from env or defaults */
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001";
/** API prefix for all endpoints */
export const API_PREFIX = "/api";
/** Full API URL */
export const API_URL = \`\${API_BASE_URL}\${API_PREFIX}\`;
/** Auth token storage key */
export const AUTH_TOKEN_KEY = "auth_token";
/** Default page size for paginated endpoints */
export const DEFAULT_PAGE_SIZE = 20;
`,
"package.json": JSON.stringify({
name: "@shared/config",
version: "0.1.0",
private: true,
main: "./src/index.ts",
types: "./src/index.ts",
}, null, 2),
"tsconfig.json": JSON.stringify({
compilerOptions: {
target: "ES2022",
module: "ESNext",
moduleResolution: "bundler",
strict: true,
esModuleInterop: true,
skipLibCheck: true,
declaration: true,
outDir: "./dist",
},
include: ["src"],
}, null, 2),
};
}
// ═══════════════════════════════════════════════════════
// 3. Shared Types Package Generator
// ═══════════════════════════════════════════════════════
function generateSharedTypesPackage(contract) {
return {
"src/index.ts": generateSharedTypes(contract),
"package.json": JSON.stringify({
name: "@shared/types",
version: "0.1.0",
private: true,
main: "./src/index.ts",
types: "./src/index.ts",
}, null, 2),
"tsconfig.json": JSON.stringify({
compilerOptions: {
target: "ES2022",
module: "ESNext",
moduleResolution: "bundler",
strict: true,
esModuleInterop: true,
skipLibCheck: true,
declaration: true,
outDir: "./dist",
},
include: ["src"],
}, null, 2),
};
}
// ═══════════════════════════════════════════════════════
// 4. Root Files Generator
// ═══════════════════════════════════════════════════════
function generateRootFiles(projectName, prdSummary, contract) {
const safeName = (projectName || "fullstack").toLowerCase().replace(/[^a-z0-9-]/g, "-");
const entities = contract.entities || [];
const tableList = entities.map(e => `- **${e.table}** — ${e.description || ""}`).join("\n");
return {
"package.json": JSON.stringify({
name: safeName,
version: "0.1.0",
private: true,
workspaces: [
"apps/*",
"packages/*",
],
scripts: {
dev: "node scripts/dev.mjs",
build: "node scripts/build.mjs",
"dev:web": "npm run dev -w apps/web",
"dev:api": "npm run dev -w apps/api",
"build:web": "npm run build -w apps/web",
"build:api": "npm run build -w apps/api",
test: "npm run test -w apps/api",
lint: "npm run lint -w apps/web",
},
}, null, 2),
".env.example": `# ─── API Server ───────────────────────────────────
API_PORT=3001
API_HOST=0.0.0.0
NODE_ENV=development
DATABASE_URL=./data/app.db
JWT_SECRET=change-me-in-production
CORS_ORIGIN=http://localhost:3000
# ─── Web Client ───────────────────────────────────
NEXT_PUBLIC_API_URL=http://localhost:3001
WEB_PORT=3000
`,
"README.md": `# ${projectName} — Fullstack Project
> ${prdSummary || "Auto-generated fullstack application"}
## Architecture
\`\`\`
apps/
├── web/ # Next.js 15 + TypeScript + Tailwind CSS
└── api/ # Fastify 5 + TypeScript + SQLite (sql.js)
packages/
├── shared-types/ # @shared/types — shared TypeScript interfaces
└── shared-config/ # @shared/config — shared configuration
\`\`\`
## Quick Start
\`\`\`bash
# Install all dependencies (root + workspaces)
npm install
# Start both frontend and backend in dev mode
npm run dev
# Or start individually
npm run dev:web # http://localhost:3000
npm run dev:api # http://localhost:3001
\`\`\`
## Build
\`\`\`bash
# Build everything
npm run build
# Or individually
npm run build:web
npm run build:api
\`\`\`
## Testing
\`\`\`bash
# Run API tests
npm test
\`\`\`
## API Endpoints
### Auth
- \`POST /api/auth/register\`
- \`POST /api/auth/login\`
- \`GET /api/auth/me\`
### Resources
${entities.map(e => {
const tn = e.table;
return `#### ${tn}\n- \`GET /api/${tn}\` — List\n- \`GET /api/${tn}/:id\` — Get\n- \`POST /api/${tn}\` — Create\n- \`PUT /api/${tn}/:id\` — Update\n- \`DELETE /api/${tn}/:id\` — Delete`;
}).join("\n\n")}
### System
- \`GET /api/health\` — Health check
## Environment Variables
See \`.env.example\` for all available configuration.
`,
".gitignore": `node_modules/
dist/
.next/
data/
.env
*.db
*.db-journal
*.db-wal
`,
};
}
// ═══════════════════════════════════════════════════════
// 5. Scripts Generator
// ═══════════════════════════════════════════════════════
function generateScripts() {
return {
"scripts/dev.mjs": `#!/usr/bin/env node
/**
* Dev script — starts both web and api in parallel.
* Usage: node scripts/dev.mjs
*/
import { spawn } from "node:child_process";
function start(name, command, args, cwd) {
const child = spawn(command, args, {
cwd,
stdio: "inherit",
shell: true,
env: { ...process.env, FORCE_COLOR: "1" },
});
child.on("error", (err) => console.error(\`[\${name}] Failed: \${err.message}\`));
child.on("exit", (code) => {
if (code !== 0 && code !== null) console.error(\`[\${name}] Exited with code \${code}\`);
});
return child;
}
const ROOT = new URL("..", import.meta.url).pathname;
console.log("🚀 Starting fullstack dev servers...\\n");
const api = start("api", "npm", ["run", "dev"], \`\${ROOT}/apps/api\`);
const web = start("web", "npm", ["run", "dev"], \`\${ROOT}/apps/web\`);
process.on("SIGINT", () => { api.kill(); web.kill(); process.exit(0); });
process.on("SIGTERM", () => { api.kill(); web.kill(); process.exit(0); });
`,
"scripts/build.mjs": `#!/usr/bin/env node
/**
* Build script — builds both web and api.
* Usage: node scripts/build.mjs
*/
import { execSync } from "node:child_process";
const ROOT = new URL("..", import.meta.url).pathname;
function run(cmd, cwd) {
console.log(\`\\n🔨 \${cmd} (in \${cwd})\`);
execSync(cmd, { cwd, stdio: "inherit" });
}
console.log("🏗️ Building fullstack project...\\n");
try {
run("npm run build", \`\${ROOT}/apps/api\`);
run("npm run build", \`\${ROOT}/apps/web\`);
console.log("\\n✅ Build complete!");
} catch (e) {
console.error("\\n❌ Build failed:", e.message);
process.exit(1);
}
`,
};
}
// ═══════════════════════════════════════════════════════
// 6. Frontend Post-Processor
// ═══════════════════════════════════════════════════════
/**
* Adapt the generated frontend to work in a monorepo with shared packages.
* - Add @shared/types and @shared/config as dependencies
* - Update services/api.ts to use @shared/config for API base URL
*/
function postProcessFrontend(files, projectName) {
const modified = { ...files };
const baseName = (projectName || "app").toLowerCase().replace(/[^a-z0-9-]/g, "-");
// ── Update package.json ──
if (modified["package.json"]) {
const pkg = JSON.parse(modified["package.json"]);
pkg.name = `${baseName}-web`;
pkg.dependencies = pkg.dependencies || {};
pkg.dependencies["@shared/types"] = "*";
pkg.dependencies["@shared/config"] = "*";
modified["package.json"] = JSON.stringify(pkg, null, 2);
}
// ── Update services/api.ts (after prefixSrc, located at src/services/api.ts) ──
if (modified["src/services/api.ts"]) {
modified["src/services/api.ts"] = modified["src/services/api.ts"].replace(
/(const API_BASE = .*?;)/s,
`import { API_BASE_URL, API_PREFIX } from "@shared/config";
const API_BASE = \`\${API_BASE_URL}\${API_PREFIX}\`;`
);
}
// ── Create a shared-types re-export in src/types ──
// We keep domain-specific types but re-export shared types
if (modified["src/types/index.ts"]) {
modified["src/types/index.ts"] = `// Re-export shared types for convenience
export type {
User,
ApiResponse,
PaginatedResponse,
ErrorResponse,
} from "@shared/types";
${modified["src/types/index.ts"]}
`;
}
return modified;
}
// ═══════════════════════════════════════════════════════
// 7. Backend Post-Processor
// ═══════════════════════════════════════════════════════
function postProcessBackend(files, projectName, contract) {
const modified = { ...files };
const baseName = (projectName || "app").toLowerCase().replace(/[^a-z0-9-]/g, "-");
const entities = contract.entities || [];
// ── Update package.json ──
if (modified["package.json"]) {
const pkg = JSON.parse(modified["package.json"]);
pkg.name = `${baseName}-api`;
pkg.dependencies = pkg.dependencies || {};
pkg.dependencies["@shared/types"] = "*";
pkg.dependencies["@shared/config"] = "*";
modified["package.json"] = JSON.stringify(pkg, null, 2);
}
// ── Rewrite types to use @shared/types for entities & API wrappers ──
if (modified["src/types/index.ts"]) {
let content = modified["src/types/index.ts"];
// Build set of entity type names from contract
const entityNames = new Set(["User"]);
for (const entity of entities.filter(e => e.table !== "users")) {
entityNames.add(entity.name);
}
// Remove specific duplicate type declarations (blocks ending with })
const duplicateTypes = [...entityNames, "ApiResponse", "PaginatedResponse", "ErrorResponse"];
for (const typeName of duplicateTypes) {
// Remove `export interface TypeName ... }` including generics
content = content.replace(
new RegExp(`export interface ${typeName}(<[^>]*>)?\\s*\\{[^}]*\\}\\n\\n`, "g"),
""
);
}
// Clean extra blank lines
content = content.replace(/\n{3,}/g, "\n\n");
// Build re-export list dynamically from schema
const reExports = [...entityNames];
const reExportLines = reExports.map(n => ` ${n},`).join("\n");
// Use explicit import + re-export pattern for reliable TS resolution
const entityImportNames = reExports.join(",\n ");
modified["src/types/index.ts"] = `// Import and re-export shared entity types
import type {
${entityImportNames},
ApiResponse,
PaginatedResponse,
ErrorResponse,
} from "@shared/types";
export type {
${entityImportNames},
ApiResponse,
PaginatedResponse,
ErrorResponse,
};
${content}`;
}
return modified;
}
// ═══════════════════════════════════════════════════════
// 8. Main Composer Function
// ═══════════════════════════════════════════════════════
/**
* Compose a fullstack project from PRD + Architecture.
*
* @param {object} prd — SF-01 PRD
* @param {object} arch — SF-02 Architecture
* @returns {object} { files, stats, error, message }
*/
export async function composeFullstack(prd, arch) {
if (!prd || prd.error) {
return { error: "INVALID_PRD", message: "Invalid or missing PRD", files: {}, stats: {} };
}
if (!arch || arch.error) {
return { error: "INVALID_ARCH", message: "Invalid or missing Architecture", files: {}, stats: {} };
}
const projectName = prd.projectName || arch.projectName || "FullstackApp";
const prdSummary = prd.summary || "";
const contract = arch.contract || createContract(prd, { databaseSchema: arch.databaseSchema || [], domain: arch.domain });
// ── Dynamic imports of builder agents ──
const fbMod = await import("./frontend-builder-agent.mjs");
const bbMod = await import("./backend-builder-agent.mjs");
// ── Build frontend ──
const frontend = fbMod.buildFrontend(prd, arch);
if (frontend.error) return { error: "FRONTEND_BUILD_FAILED", message: frontend.message, files: {}, stats: {} };
// ── Build backend ──
const backend = bbMod.buildBackend(prd, arch);
if (backend.error) return { error: "BACKEND_BUILD_FAILED", message: backend.message, files: {}, stats: {} };
// ── Post-process for monorepo ──
// Prefix frontend paths with src/ for Next.js src directory structure
const frontendSrc = prefixSrc(frontend.files);
const webFiles = postProcessFrontend(frontendSrc, projectName);
const apiFiles = postProcessBackend(backend.files, projectName, contract);
// ── Shared packages ──
const sharedTypesPkg = generateSharedTypesPackage(contract);
const sharedConfigPkg = generateSharedConfig();
// ── Root files ──
const rootFiles = generateRootFiles(projectName, prdSummary, contract);
// ── Scripts ──
const scripts = generateScripts();
// ══ Assemble final file tree ══
const files = {};
// apps/web/ (frontend)
for (const [relPath, content] of Object.entries(webFiles)) {
files[`apps/web/${relPath}`] = content;
}
// apps/api/ (backend)
for (const [relPath, content] of Object.entries(apiFiles)) {
files[`apps/api/${relPath}`] = content;
}
// packages/shared-types/
for (const [relPath, content] of Object.entries(sharedTypesPkg)) {
files[`packages/shared-types/${relPath}`] = content;
}
// packages/shared-config/
for (const [relPath, content] of Object.entries(sharedConfigPkg)) {
files[`packages/shared-config/${relPath}`] = content;
}
// Root files
for (const [relPath, content] of Object.entries(rootFiles)) {
files[relPath] = content;
}
// Scripts
for (const [relPath, content] of Object.entries(scripts)) {
files[relPath] = content;
}
// ══ Stats ══
const allPaths = Object.keys(files);
const webPaths = allPaths.filter(p => p.startsWith("apps/web/"));
const apiPaths = allPaths.filter(p => p.startsWith("apps/api/"));
const sharedPaths = allPaths.filter(p => p.startsWith("packages/"));
const rootPaths = allPaths.filter(p => !p.includes("/"));
const stats = {
totalFiles: allPaths.length,
webFiles: webPaths.length,
apiFiles: apiPaths.length,
sharedFiles: sharedPaths.length,
rootFiles: rootPaths.length,
dbSchema: contract.entities.length,
};
return { files, stats, error: null, message: null };
}
// ═══════════════════════════════════════════════════════
// 9. File I/O
// ═══════════════════════════════════════════════════════
export function loadJSON(path) {
try {
if (!existsSync(path)) return { data: null, error: `File not found: ${path}` };
return { data: JSON.parse(readFileSync(path, "utf-8")), error: null };
} catch (e) {
return { data: null, error: `Failed to load: ${e.message}` };
}
}
export function writeFullstack(result, outputDir) {
mkdirSync(outputDir, { recursive: true });
for (const [relPath, content] of Object.entries(result.files)) {
const fullPath = resolve(outputDir, relPath);
mkdirSync(dirname(fullPath), { recursive: true });
writeFileSync(fullPath, content);
}
}
/**
* Prefix frontend source files with src/ for Next.js src directory structure.
* Frontend builder outputs flat paths (app/layout.tsx, types/index.ts),
* but monorepo structure uses apps/web/src/ prefix.
* Config files (package.json, tsconfig.json, next.config.*, tailwind.config.*,
* postcss.config.*, .env*) stay at root level.
*
* @param {object} raw — Frontend builder output paths (e.g. "app/layout.tsx")
* @returns {object} — Source paths prefixed, config paths kept at root
*/
function prefixSrc(raw) {
const CONFIG_FILES = /^(package\.json|tsconfig\.json|next\.config\.[a-z]+|tailwind\.config\.[a-z]+|postcss\.config\.[a-z]+|\.[a-z-]+)$/;
const result = {};
for (const [relPath, content] of Object.entries(raw)) {
if (relPath.startsWith("src/")) {
// Already prefixed — keep as-is
result[relPath] = content;
} else if (CONFIG_FILES.test(relPath)) {
// Config files — keep at root
result[relPath] = content;
} else {
// Source code — prefix with src/
result[`src/${relPath}`] = content;
}
}
return result;
}
// ═══════════════════════════════════════════════════════
// 10. CLI Entry
// ═══════════════════════════════════════════════════════
function parseArgs() {
const args = process.argv.slice(2);
const opts = { prd: null, arch: null, inputText: null, output: null, verbose: false, help: false };
for (let i = 0; i < args.length; i++) {
if (args[i] === "--prd" && args[i + 1]) opts.prd = args[++i];
else if (args[i] === "--arch" && args[i + 1]) opts.arch = args[++i];
else if (args[i] === "--input-text" && args[i + 1]) opts.inputText = args[++i];
else if (args[i] === "--output" && args[i + 1]) opts.output = args[++i];
else if (args[i] === "--verbose") opts.verbose = true;
else if (args[i] === "--help" || args[i] === "-h") opts.help = true;
}
return opts;
}
async function main() {
const opts = parseArgs();
if (opts.help) {
console.log(`
Fullstack Composer Agent — SF-05
Usage:
node scripts/fullstack-composer-agent.mjs --prd <path> --arch <path> [options]
node scripts/fullstack-composer-agent.mjs --input-text "<需求>" [options]
Options:
--prd <path> PRD JSONSF-01 输出)
--arch <path> Architecture JSONSF-02 输出)
--input-text <text> 直接传入需求
--output <dir> 输出目录(default: fullstack/
--verbose 详细输出
--help 显示帮助
`);
return;
}
let prd = null, arch = null;
if (opts.prd && opts.arch) {
const prdRes = loadJSON(opts.prd);
const archRes = loadJSON(opts.arch);
if (prdRes.error) { console.error(prdRes.error); process.exit(1); }
if (archRes.error) { console.error(archRes.error); process.exit(1); }
prd = prdRes.data;
arch = archRes.data;
} else if (opts.inputText) {
try {
const sf01 = await import("./project-intake-agent.mjs");
const sf02 = await import("./architecture-agent.mjs");
prd = sf01.generatePRD(opts.inputText);
if (prd.error) { console.error(`SF-01: ${prd.message}`); process.exit(1); }
arch = sf02.generateArchitecture(prd);
if (arch.error) { console.error(`SF-02: ${arch.message}`); process.exit(1); }
} catch (e) {
console.error(`Pipeline error: ${e.message}`);
process.exit(1);
}
}
if (!prd || !arch) {
console.error("Error: --prd + --arch or --input-text is required. Use --help.");
process.exit(1);
}
const result = await composeFullstack(prd, arch);
if (result.error) { console.error(result.message); process.exit(1); }
const outputDir = opts.output ? resolve(opts.output) : DEFAULT_OUTPUT;
writeFullstack(result, outputDir);
if (opts.verbose) {
console.error(`Project: ${prd.projectName}`);
console.error(`Files: ${result.stats.totalFiles}`);
console.error(`Web: ${result.stats.webFiles}, API: ${result.stats.apiFiles}, Shared: ${result.stats.sharedFiles}`);
}
console.log(JSON.stringify({
projectName: prd.projectName,
outputDir,
stats: result.stats,
}, null, 2));
}
if (process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]))) {
main();
}
+103
View File
@@ -0,0 +1,103 @@
#!/bin/bash
# ====================================================
# PR-B Architecture Guard — Master Runner
# OpenClaw Agent OS v2 — Architecture Stabilization
# ====================================================
# Runs all 6 architecture guards.
# Exit 0 = all guards pass
# Exit 1 = one or more guards failed
# ====================================================
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
cd "$ROOT_DIR"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
PASS_COUNT=0
FAIL_COUNT=0
WARN_COUNT=0
TOTAL=6
# Store results in temp file
RESULT_FILE="$(mktemp)"
trap "rm -f $RESULT_FILE" EXIT
echo ""
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
echo -e "${CYAN} OpenClaw Agent OS v2 — Architecture Guard${NC}"
echo -e "${CYAN} PR-B: $(date '+%Y-%m-%d %H:%M:%S %Z')${NC}"
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
run_guard() {
local name="$1"
local script="$2"
local mode="$3" # "hard" or "soft"
echo ""
echo -e "${YELLOW}──────────────────────────────────────────${NC}"
if node "$script" 2>&1; then
PASS_COUNT=$((PASS_COUNT + 1))
echo "$name|PASS" >> "$RESULT_FILE"
echo -e "${GREEN}$name passed${NC}"
return 0
else
local exit_code=$?
if [ "$mode" = "hard" ]; then
FAIL_COUNT=$((FAIL_COUNT + 1))
echo "$name|FAIL" >> "$RESULT_FILE"
echo -e "${RED}$name FAILED (exit $exit_code)${NC}"
return 1
else
WARN_COUNT=$((WARN_COUNT + 1))
echo "$name|WARN" >> "$RESULT_FILE"
echo -e "${YELLOW}$name warning (exit $exit_code)${NC}"
return 0
fi
fi
}
# Run all 6 guards
run_guard "Guard-1: Memory Backend" "scripts/guard-memory-backend.mjs" "hard"
run_guard "Guard-2: Runtime Core" "scripts/guard-runtime-core.mjs" "hard"
run_guard "Guard-3: Tool Path" "scripts/guard-tool-path.mjs" "soft"
run_guard "Guard-4: MEMORY.md Write" "scripts/guard-memory-write.mjs" "hard"
run_guard "Guard-5: Tool Trace" "scripts/guard-tool-trace.mjs" "soft"
run_guard "Guard-6: Dreaming Phase" "scripts/guard-dreaming-phase.mjs" "hard"
# Summary
echo ""
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
echo -e "${CYAN} Architecture Guard Summary${NC}"
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
echo ""
while IFS='|' read -r name result; do
case "$result" in
PASS) echo -e " ${GREEN}[PASS]${NC} $name" ;;
FAIL) echo -e " ${RED}[FAIL]${NC} $name" ;;
WARN) echo -e " ${YELLOW}[WARN]${NC} $name" ;;
esac
done < "$RESULT_FILE"
echo ""
echo -e " Total: $TOTAL | ${GREEN}Pass: $PASS_COUNT${NC} | ${RED}Fail: $FAIL_COUNT${NC} | ${YELLOW}Warn: $WARN_COUNT${NC}"
if [ $FAIL_COUNT -eq 0 ]; then
echo ""
echo -e "${GREEN} ✓ ALL ARCHITECTURE GUARDS PASSED${NC}"
echo -e "${GREEN} System is within allowed architecture boundaries.${NC}"
exit 0
else
echo ""
echo -e "${RED} ✗ ARCHITECTURE VIOLATIONS DETECTED${NC}"
echo -e "${RED} Fix failures before proceeding.${NC}"
exit 1
fi
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env node
/**
* Guard 6: Dreaming Phase Guard
* Prevents dreaming phase proliferation beyond 3 phases.
*
* → Architecture Freeze v2 (PR-1): docs/architecture-freeze-v2.md
* → Current: light, deep, rem | Legacy: rem → to be replaced by promote
*
* Currently allowed phases: light, deep, rem
* After PR-5: will be reduced to collect, promote
*
* FAIL: new dreaming phase appears (count > 3)
* PASS: phase count <= 3
*/
import { readdirSync, readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || "/opt/homebrew/lib/node_modules/openclaw";
const EXTENSIONS_DIR = join(OPENCLAW_HOME, "dist", "extensions");
const PHASE_LIMIT = 3; // Current baseline
const POST_PR5_LIMIT = 2; // After PR-5: collect + promote
function guardDreamingPhases() {
const issues = [];
let foundPhases = [];
let source = "none";
// Check memory-core plugin config schema
const memoryCorePath = join(EXTENSIONS_DIR, "memory-core", "openclaw.plugin.json");
if (existsSync(memoryCorePath)) {
try {
const plugin = JSON.parse(readFileSync(memoryCorePath, "utf8"));
const dreaming = plugin.configSchema?.properties?.dreaming;
if (dreaming) {
const phases = dreaming.properties?.phases?.properties;
if (phases) {
foundPhases = Object.keys(phases);
source = "memory-core/openclaw.plugin.json → configSchema.properties.dreaming.properties.phases";
}
}
} catch (e) {
issues.push({ level: "ERROR", msg: `Could not parse memory-core plugin.json: ${e.message}` });
}
}
// Also check active-memory for dreaming references
const activeMemoryPath = join(EXTENSIONS_DIR, "active-memory", "openclaw.plugin.json");
if (existsSync(activeMemoryPath)) {
try {
const plugin = JSON.parse(readFileSync(activeMemoryPath, "utf8"));
// Check if active-memory references phases
const configKeys = Object.keys(plugin.configSchema?.properties || {});
if (configKeys.includes("dreaming")) {
// Active memory has dreaming config too
}
} catch {}
}
console.log("");
console.log("═══════════════════════════════════════");
console.log(" Guard 6: Dreaming Phase Guard");
console.log("═══════════════════════════════════════");
console.log("");
if (foundPhases.length === 0) {
console.log(" No dreaming phases found in plugin config schemas.");
console.log(" (This may indicate a config schema location change.)");
issues.push({ level: "WARN", msg: "Could not locate dreaming phases in config schema" });
} else {
console.log(` Source: ${source}`);
console.log(` Dreaming phases: ${foundPhases.join(", ")}`);
console.log(` Phase count: ${foundPhases.length}`);
console.log(` Current limit: ${PHASE_LIMIT} (light, deep, rem)`);
console.log(` Post-PR5 limit: ${POST_PR5_LIMIT} (collect, promote)`);
if (foundPhases.length > PHASE_LIMIT) {
issues.push({
level: "FAIL",
msg: `Dreaming phase count ${foundPhases.length} exceeds limit ${PHASE_LIMIT}`
});
} else {
console.log(`\n Phase details:`);
for (const phase of foundPhases) {
console.log(`${phase}`);
}
}
}
console.log("");
for (const issue of issues) {
console.log(` [${issue.level}] ${issue.msg}`);
}
if (issues.filter(i => i.level === "FAIL").length === 0) {
console.log(" ✓ PASS: dreaming phase count within allowed limit");
console.log(` (After PR-5, limit will be reduced from ${PHASE_LIMIT}${POST_PR5_LIMIT})`);
process.exit(0);
} else {
console.log(" ✗ FAIL: dreaming phase count exceeded");
process.exit(1);
}
}
guardDreamingPhases();
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env node
/**
* Guard 1: Memory Backend Guard
* Prevents proliferation beyond the allowed memory backends.
*
* → Architecture Freeze v2 (PR-1): docs/architecture-freeze-v2.md
* → Core: memory-core | Legacy: memory-wiki
*
* Allowed plugins (kind="memory" or has memory tools):
* - memory-core (primary — Core §2)
* - active-memory (recall injector — Core §2)
* - memory-wiki (legacy, allowed for now — Legacy §4)
* - Builtin/QMD (internal engine, not a plugin)
*
* FAIL: any new memory plugin appears
* PASS: within allowed list
*/
import { readdirSync, readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || "/opt/homebrew/lib/node_modules/openclaw";
const EXTENSIONS_DIR = join(OPENCLAW_HOME, "dist", "extensions");
// Allowed memory plugin IDs
const ALLOWED_MEMORY_PLUGINS = new Set([
"memory-core",
"active-memory",
"memory-wiki",
]);
// Plugin IDs that have memory contracts but are not "kind: memory"
const KNOWN_MEMORY_RELATED = new Set([
"memory-lancedb", // legacy, being deprecated
]);
function guardMemoryBackends() {
const issues = [];
const found = [];
if (!existsSync(EXTENSIONS_DIR)) {
issues.push({ level: "ERROR", msg: `Extensions dir not found: ${EXTENSIONS_DIR}` });
return { pass: false, found, issues };
}
const extDirs = readdirSync(EXTENSIONS_DIR, { withFileTypes: true })
.filter(d => d.isDirectory())
.map(d => d.name);
for (const extDir of extDirs) {
const pluginJsonPath = join(EXTENSIONS_DIR, extDir, "openclaw.plugin.json");
if (!existsSync(pluginJsonPath)) continue;
try {
const plugin = JSON.parse(readFileSync(pluginJsonPath, "utf8"));
const pluginId = plugin.id || extDir;
const kind = plugin.kind || "";
const contracts = plugin.contracts || {};
const tools = contracts.tools || [];
const memoryTools = tools.filter(t => t.startsWith("memory_") || t.startsWith("wiki_"));
const isMemoryPlugin =
kind === "memory" ||
tools.includes("memory_search") ||
tools.includes("memory_get") ||
memoryTools.length >= 2;
if (isMemoryPlugin) {
found.push({ id: pluginId, kind, memoryTools });
if (!ALLOWED_MEMORY_PLUGINS.has(pluginId) && !KNOWN_MEMORY_RELATED.has(pluginId)) {
issues.push({
level: "FAIL",
msg: `UNEXPECTED memory plugin: ${pluginId} (kind=${kind}, tools=${memoryTools.join(",")})`
});
}
}
} catch (e) {
issues.push({ level: "WARN", msg: `Could not parse ${pluginJsonPath}: ${e.message}` });
}
}
const pass = issues.filter(i => i.level === "FAIL").length === 0;
return { pass, found, issues };
}
// Run
const result = guardMemoryBackends();
console.log("");
console.log("═══════════════════════════════════════");
console.log(" Guard 1: Memory Backend Guard");
console.log("═══════════════════════════════════════");
console.log("");
if (result.found.length === 0) {
console.log(" No memory plugins found.");
} else {
console.log(` Memory plugins found: ${result.found.length}`);
for (const f of result.found) {
const status = ALLOWED_MEMORY_PLUGINS.has(f.id) ? "✓" :
KNOWN_MEMORY_RELATED.has(f.id) ? "⚠ (legacy)" : "✗ UNEXPECTED";
console.log(` ${status} ${f.id} (kind="${f.kind}", tools: ${f.memoryTools.join(", ")})`);
}
}
for (const issue of result.issues) {
console.log(` [${issue.level}] ${issue.msg}`);
}
if (result.pass) {
console.log("");
console.log(" ✓ PASS: memory backend count within allowed list");
process.exit(0);
} else {
console.log("");
console.log(" ✗ FAIL: unexpected memory backend(s) detected");
process.exit(1);
}
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env node
/**
* Guard 4: MEMORY.md Direct Write Guard
* Prevents new code from writing directly to MEMORY.md.
*
* → Architecture Freeze v2 (PR-1): docs/architecture-freeze-v2.md
* → Forbidden Addition §6.6: 禁止直接写 MEMORY.md
*
* Only allowed writers (whitelisted):
* - session-memory hook (bundled/session-memory)
* - memory-core dreaming (extensions/memory-core)
* - memory-core manager (extensions/memory-core)
* - openclaw CLI (memory promote/write)
*
* FAIL: new code path writes to MEMORY.md
*/
import { readdirSync, readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { execSync } from "node:child_process";
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || "/opt/homebrew/lib/node_modules/openclaw";
const DIST_DIR = join(OPENCLAW_HOME, "dist");
const WORKSPACE = join(homedir(), ".openclaw", "workspace");
// Known/allowed MEMORY.md writers
const ALLOWED_MEMORY_MD_WRITERS = new Set([
"memory-core",
"memory-wiki",
"active-memory", // writes to prompt, not MEMORY.md directly
]);
function guardMemoryDirectWrite() {
const issues = [];
const foundWriters = [];
// Search JS files in dist for MEMORY.md write references
// We grep for patterns that write to MEMORY.md
const searchPatterns = [
"MEMORY.md",
"'MEMORY.md'",
'"MEMORY.md"',
];
try {
// Use grep to find references to MEMORY.md in dist JS files
const grepResult = execSync(
`grep -rl "MEMORY.md" "${DIST_DIR}" 2>/dev/null | grep -v ".d.ts$" | grep -v "node_modules" | grep -v ".json$" | head -50`,
{ timeout: 30000, encoding: "utf8", maxBuffer: 10 * 1024 * 1024 }
).trim();
if (grepResult) {
const files = grepResult.split("\n").filter(Boolean);
for (const file of files) {
const relativePath = file.replace(DIST_DIR + "/", "");
// Determine which module this belongs to
let module = "unknown";
if (relativePath.includes("memory-core")) module = "memory-core";
else if (relativePath.includes("memory-wiki")) module = "memory-wiki";
else if (relativePath.includes("memory-lancedb")) module = "memory-lancedb";
else if (relativePath.includes("active-memory")) module = "active-memory";
else if (relativePath.includes("bundled/session-memory")) module = "bundled/session-memory";
else if (relativePath.includes("bundle")) module = "bundled";
else if (relativePath.includes("cli")) module = "cli";
else module = relativePath.split("/")[0] || "unknown";
foundWriters.push({ file: relativePath, module });
}
}
} catch (e) {
// grep returns exit 1 if no matches found — that's OK
if (e.status !== 1) {
issues.push({ level: "WARN", msg: `grep error: ${e.message}` });
}
}
// Group by module
const moduleGroups = {};
for (const w of foundWriters) {
if (!moduleGroups[w.module]) moduleGroups[w.module] = [];
moduleGroups[w.module].push(w.file);
}
const knownModules = new Set(Object.keys(moduleGroups));
console.log("");
console.log("═══════════════════════════════════════");
console.log(" Guard 4: MEMORY.md Direct Write Guard");
console.log("═══════════════════════════════════════");
console.log("");
console.log(` Modules referencing MEMORY.md: ${knownModules.size}`);
for (const [mod, files] of Object.entries(moduleGroups)) {
const status = ALLOWED_MEMORY_MD_WRITERS.has(mod) ? "✓ allowed" : "⚠ unknown";
console.log(` ${status} ${mod} (${files.length} files)`);
if (files.length <= 3) {
for (const f of files) {
console.log(` - ${f}`);
}
}
}
// Check for unexpected writers
const unexpectedModules = [...knownModules].filter(m => !ALLOWED_MEMORY_MD_WRITERS.has(m));
if (unexpectedModules.length > 0) {
console.log("");
for (const mod of unexpectedModules) {
// some modules like "bundled" are expected to reference MEMORY.md for reading
// Only flag as WARN if they might be writing
issues.push({
level: "WARN",
msg: `Module "${mod}" references MEMORY.md — verify it's read-only`
});
}
}
console.log("");
const failIssues = issues.filter(i => i.level === "FAIL");
const warnIssues = issues.filter(i => i.level === "WARN");
if (failIssues.length > 0) {
for (const issue of failIssues) {
console.log(` [FAIL] ${issue.msg}`);
}
console.log(" ✗ FAIL: new direct MEMORY.md writer(s) detected");
process.exit(1);
} else {
if (warnIssues.length > 0) {
for (const issue of warnIssues) {
console.log(` [WARN] ${issue.msg}`);
}
}
console.log(" ✓ PASS: MEMORY.md direct write paths unchanged");
process.exit(0);
}
}
guardMemoryDirectWrite();
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env node
/**
* Guard 2: Agent Runtime Core Guard
* Prevents proliferation beyond the 3 allowed agent runtimes.
*
* → Architecture Freeze v2 (PR-1): docs/architecture-freeze-v2.md
* → Core: embedded-agent-runner, acp | Experimental: codex-supervisor
*
* Allowed agent runtimes (entry points):
* - embedded-agent (built into OpenClaw — Core §2)
* - codex (codex supervisor/app-server harness — Experimental §5)
* - acp (ACP agent — Core §2)
*
* RUNTIME UTILITY FILES (e.g. auth-profiles.runtime.js) are NOT new runtimes.
* Only AGENT ENTRY POINT modules count as runtime cores.
*
* FAIL: a 4th agent runtime entry point appears
*/
import { readdirSync, readFileSync, existsSync, statSync } from "node:fs";
import { join } from "node:path";
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || "/opt/homebrew/lib/node_modules/openclaw";
const DIST_DIR = join(OPENCLAW_HOME, "dist");
// Known agent runtime core entry points
const KNOWN_RUNTIME_ENTRIES = new Set([
"embedded-agent", // Embedded agent runner
"codex-supervisor", // Codex app-server harness
"acp", // ACP agent directory
]);
// Known runtime UTILITY files (not new runtimes)
const RUNTIME_UTILITY_PREFIXES = [
"auth-profiles",
"models-config",
"model-catalog",
"agent-bundle-mcp",
"runtime-",
"runtime.",
];
function isRuntimeUtility(filename) {
const name = filename.replace(/\.js$/, "").replace(/\.d\.ts$/, "");
return RUNTIME_UTILITY_PREFIXES.some(p => name.startsWith(p) || name.includes(".runtime"));
}
function guardRuntimeCores() {
const issues = [];
const foundEntries = [];
const utilities = [];
if (!existsSync(DIST_DIR)) {
issues.push({ level: "ERROR", msg: `Dist dir not found: ${DIST_DIR}` });
return { pass: false, foundEntries, issues };
}
// 1. Check extensions for agent runtime plugins
const extensionsDir = join(DIST_DIR, "extensions");
if (existsSync(extensionsDir)) {
const extDirs = readdirSync(extensionsDir, { withFileTypes: true })
.filter(d => d.isDirectory());
for (const extDir of extDirs) {
const pluginJsonPath = join(extensionsDir, extDir.name, "openclaw.plugin.json");
if (!existsSync(pluginJsonPath)) continue;
try {
const plugin = JSON.parse(readFileSync(pluginJsonPath, "utf8"));
const id = plugin.id || extDir.name;
// Check if this plugin provides agent runtime capabilities
const contracts = plugin.contracts || {};
const tools = contracts.tools || [];
const hasRuntimeTools = tools.some(t =>
t.includes("codex_session") ||
t.includes("agent_") ||
t.includes("acp_")
);
if (hasRuntimeTools) {
foundEntries.push({ name: id, source: "plugin.json", type: "extension" });
}
} catch {}
}
}
// 2. Check for embedded-agent directory
const agentsDir = join(DIST_DIR, "agents");
if (existsSync(agentsDir)) {
const agentSubDirs = readdirSync(agentsDir, { withFileTypes: true })
.filter(d => d.isDirectory() && d.name.includes("agent"));
for (const d of agentSubDirs) {
foundEntries.push({ name: d.name, source: `dist/agents/${d.name}`, type: "core" });
}
}
// 3. Check for ACP directory
const acpDir = join(DIST_DIR, "acp");
if (existsSync(acpDir)) {
foundEntries.push({ name: "acp", source: "dist/acp/", type: "core" });
}
// 4. Categorize: entries vs utilities
const agentRuntimeEntries = foundEntries.filter(e => {
const name = e.name;
return KNOWN_RUNTIME_ENTRIES.has(name) ||
name.includes("embedded-agent") ||
name.includes("codex") ||
name === "acp";
});
const unknownEntries = foundEntries.filter(e => !agentRuntimeEntries.includes(e));
console.log(` Agent runtime entries found: ${agentRuntimeEntries.length}`);
console.log(` Allowed maximum: 3 (embedded, codex, acp)`);
for (const e of agentRuntimeEntries) {
const known = KNOWN_RUNTIME_ENTRIES.has(e.name) ||
e.name.includes("embedded-agent") ||
e.name.includes("codex") ||
e.name === "acp";
const status = known ? "✓" : "?";
console.log(` ${status} ${e.name} (${e.source}, type=${e.type})`);
}
// Check for unknown entries
for (const e of unknownEntries) {
if (isRuntimeUtility(e.name)) {
utilities.push(e);
console.log(`${e.name} (utility — not a runtime core)`);
} else {
issues.push({
level: "FAIL",
msg: `UNEXPECTED agent runtime entry: ${e.name} (${e.source})`
});
}
}
// Count distinct runtime types
const distinctEntries = new Set(
agentRuntimeEntries.map(e => {
if (e.name.includes("embedded-agent")) return "embedded";
if (e.name.includes("codex")) return "codex";
if (e.name === "acp") return "acp";
return e.name;
})
);
if (distinctEntries.size > 3) {
issues.push({
level: "FAIL",
msg: `Runtime entry count ${distinctEntries.size} > allowed maximum 3`
});
}
const pass = issues.filter(i => i.level === "FAIL").length === 0;
if (utilities.length > 0) {
console.log(` Runtime utility files: ${utilities.length} (not runtime cores)`);
}
return { pass, foundEntries: agentRuntimeEntries, issues, utilities };
}
// Run
const result = guardRuntimeCores();
console.log("");
console.log("═══════════════════════════════════════");
console.log(" Guard 2: Agent Runtime Core Guard");
console.log("═══════════════════════════════════════");
console.log("");
for (const issue of result.issues) {
console.log(` [${issue.level}] ${issue.msg}`);
}
if (result.pass) {
console.log("");
console.log(" ✓ PASS: no unexpected agent runtime core detected");
console.log(` Allowed limit: 3 (embedded, codex, acp)`);
console.log(` Found: ${result.foundEntries.length} runtime entries`);
process.exit(0);
} else {
console.log("");
console.log(" ✗ FAIL: unexpected runtime core(s) detected");
process.exit(1);
}
+143
View File
@@ -0,0 +1,143 @@
#!/usr/bin/env node
/**
* Guard 3: Tool Path Guard
* Records current tool call entry points. Soft warning — no hard fail yet.
*
* → Architecture Freeze v2 (PR-1): docs/architecture-freeze-v2.md
* → Adapter §3.2: Tool Adapters
*
* ToolCore not yet implemented — this guard inventories the current state
* and will FAIL only if a NEW unknown tool path appears.
*
* Known tool paths:
* - native tool (dist/commands/*)
* - MCP tool (dist/mcp/*)
* - plugin tool (dist/extensions plugin.json contracts.tools)
* - codex tool (dist/extensions/codex-supervisor/*)
*/
import { readdirSync, readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || "/opt/homebrew/lib/node_modules/openclaw";
const DIST_DIR = join(OPENCLAW_HOME, "dist");
const EXTENSIONS_DIR = join(DIST_DIR, "extensions");
function guardToolPaths() {
const issues = [];
const inventory = { native: [], mcp: [], plugin: [], codex: [], unknown: [] };
const knownToolPaths = new Set([
"native", "mcp", "plugin", "codex"
]);
// 1. Native tools — look for tool registrations in commands
const commandsDir = join(DIST_DIR, "commands");
if (existsSync(commandsDir)) {
try {
const entries = readdirSync(commandsDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
inventory.native.push(`commands/${entry.name}`);
}
}
} catch (e) {
issues.push({ level: "WARN", msg: `Could not scan commands dir: ${e.message}` });
}
}
// 2. MCP tools
const mcpDir = join(DIST_DIR, "mcp");
if (existsSync(mcpDir)) {
try {
const files = readdirSync(mcpDir).filter(f => f.endsWith(".js"));
inventory.mcp.push(...files.map(f => `mcp/${f}`));
} catch (e) {
issues.push({ level: "WARN", msg: `Could not scan mcp dir: ${e.message}` });
}
}
// 3. Plugin tools (from openclaw.plugin.json contracts)
if (existsSync(EXTENSIONS_DIR)) {
try {
const extDirs = readdirSync(EXTENSIONS_DIR, { withFileTypes: true })
.filter(d => d.isDirectory());
for (const extDir of extDirs) {
const pluginJsonPath = join(EXTENSIONS_DIR, extDir.name, "openclaw.plugin.json");
if (!existsSync(pluginJsonPath)) continue;
try {
const plugin = JSON.parse(readFileSync(pluginJsonPath, "utf8"));
const tools = plugin.contracts?.tools || [];
if (tools.length > 0) {
// Categorize
if (extDir.name.includes("codex") || extDir.name.includes("supervisor")) {
inventory.codex.push(`${extDir.name}: [${tools.join(", ")}]`);
} else {
inventory.plugin.push(`${extDir.name}: [${tools.join(", ")}]`);
}
}
} catch {}
}
} catch (e) {
issues.push({ level: "WARN", msg: `Could not scan extensions: ${e.message}` });
}
}
// 4. Check for unknown tool paths in key files
const keyFiles = readdirSync(DIST_DIR).filter(f =>
f.endsWith(".js") && (f.includes("tool") || f.includes("command") || f.includes("mcp"))
);
for (const f of keyFiles) {
const name = f.replace(/\.js$/, "").replace(/-[A-Za-z0-9]{8}$/, "");
// Categorize based on naming
if (name.includes("mcp") || name.includes("codex-mcp")) {
if (!inventory.mcp.some(t => t.includes(name.substring(0, 10)))) {
inventory.mcp.push(`dist/${f}`);
}
} else if (name.includes("tool")) {
if (!inventory.native.some(t => t.includes(name.substring(0, 10)))) {
inventory.native.push(`dist/${f}`);
}
}
}
// Report
console.log("");
console.log("═══════════════════════════════════════");
console.log(" Guard 3: Tool Path Guard");
console.log("═══════════════════════════════════════");
console.log("");
console.log(" Tool Path Inventory:");
console.log(` Native tools: ${inventory.native.length} entries`);
console.log(` MCP tools: ${inventory.mcp.length} entries`);
console.log(` Plugin tools: ${inventory.plugin.length} plugins`);
console.log(` Codex tools: ${inventory.codex.length} plugins`);
if (inventory.plugin.length > 0) {
console.log("\n Plugin tool contracts:");
for (const p of inventory.plugin) {
console.log(`${p}`);
}
}
if (inventory.codex.length > 0) {
console.log("\n Codex tool contracts:");
for (const p of inventory.codex) {
console.log(`${p}`);
}
}
// This guard is WARN-only for now (ToolCore not yet implemented)
console.log("");
console.log(" ⚠ WARN: ToolCore not yet enforced");
console.log(" This guard currently inventories tool paths.");
console.log(" After PR-8/PR-9 (ToolCore), it will enforce all tools go through ToolExecutor.");
// No hard fail — informational only at this stage
process.exit(0);
}
guardToolPaths();
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env node
/**
* Guard 5: Tool Trace Guard
* Inventories tool call sites and checks for trace/log coverage.
*
* → Architecture Freeze v2 (PR-1): docs/architecture-freeze-v2.md
* → Future: PR-10 Trace Model enforcement
*
* SOFT GUARD — Trace model not yet implemented (PR-10).
* Currently inventories tool call entry points and reports gaps.
* No hard failure at this stage.
*/
import { readdirSync, readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { execSync } from "node:child_process";
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || "/opt/homebrew/lib/node_modules/openclaw";
const DIST_DIR = join(OPENCLAW_HOME, "dist");
function guardToolTrace() {
const issues = [];
const toolSites = [];
let totalToolCallSites = 0;
let sitesWithTrace = 0;
// Search for tool call patterns in dist
const searchTerms = [
"tool_call",
"toolCall",
"tool.call",
"tool_exec",
"toolExec",
"invokeTool",
"runTool",
];
try {
const grepResult = execSync(
`grep -rln -E "(tool_call|toolCall|tool\\.call|tool_exec|toolExec|executeTool|invokeTool|runTool)" "${DIST_DIR}" 2>/dev/null | grep -v ".d.ts$" | grep -v "node_modules" | grep ".js$" | head -30`,
{ timeout: 30000, encoding: "utf8", maxBuffer: 10 * 1024 * 1024 }
).trim();
if (grepResult) {
const files = grepResult.split("\n").filter(Boolean);
totalToolCallSites = files.length;
for (const file of files) {
const relativePath = file.replace(DIST_DIR + "/", "");
// Check if this file has trace/log instrumentation
try {
const content = readFileSync(file, "utf8");
const hasTrace =
content.includes("trace") ||
content.includes("Trace") ||
content.includes("span") ||
content.includes("log") ||
content.includes("event") ||
content.includes("audit") ||
content.includes("diagnostics");
if (hasTrace) sitesWithTrace++;
toolSites.push({
file: relativePath,
hasTrace,
module: relativePath.split("/")[0] || "unknown",
});
} catch {
toolSites.push({
file: relativePath,
hasTrace: false,
module: relativePath.split("/")[0] || "unknown",
});
}
}
}
} catch (e) {
if (e.status !== 1) {
console.log(` Warning: grep failed: ${e.message}`);
}
}
const sitesWithoutTrace = toolSites.filter(s => !s.hasTrace);
console.log("");
console.log("═══════════════════════════════════════");
console.log(" Guard 5: Tool Trace Guard");
console.log("═══════════════════════════════════════");
console.log("");
console.log(` Tool call entry points found: ${totalToolCallSites}`);
console.log(` Sites with trace/log: ${sitesWithTrace}`);
console.log(` Sites without trace: ${sitesWithoutTrace.length}`);
// Group by module
const moduleSummary = {};
for (const s of toolSites) {
if (!moduleSummary[s.module]) {
moduleSummary[s.module] = { total: 0, traced: 0 };
}
moduleSummary[s.module].total++;
if (s.hasTrace) moduleSummary[s.module].traced++;
}
if (Object.keys(moduleSummary).length > 0) {
console.log("\n Trace coverage by module:");
for (const [mod, stats] of Object.entries(moduleSummary)) {
const pct = stats.total > 0 ? Math.round(stats.traced / stats.total * 100) : 0;
const bar = "█".repeat(Math.round(pct / 10)) + "░".repeat(10 - Math.round(pct / 10));
console.log(` ${bar} ${mod}: ${stats.traced}/${stats.total} (${pct}%)`);
}
}
if (sitesWithoutTrace.length > 0 && sitesWithoutTrace.length <= 10) {
console.log("\n Sites without trace:");
for (const s of sitesWithoutTrace) {
console.log(`${s.file}`);
}
}
console.log("");
console.log(" ⚠ WARN: Trace model not yet enforced");
console.log(` Current coverage: ${sitesWithTrace}/${totalToolCallSites} sites have tracing`);
console.log(" After PR-10 (Trace Model), all tool calls will require trace spans.");
// No hard fail — soft guard
process.exit(0);
}
guardToolTrace();
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# workspace 初始化脚本
# 用途:新环境部署后一键还原工作状态
# 用法:bash scripts/init-workspace.sh
set -euo pipefail
echo "🔧 初始化工作空间..."
# 检查依赖
for cmd in git node npm curl; do
if ! command -v "$cmd" &>/dev/null; then
echo "❌ 缺少依赖: $cmd"
exit 1
fi
done
echo "✅ 所有依赖已就绪"
# 检查 Git 远程
cd "$(dirname "$0")/.."
if ! git remote -v &>/dev/null; then
echo "⚠️ 没有配置远程仓库,跳过"
else
echo "✅ Git 远程已配置"
fi
# 检查浏览器
if command -v openclaw &>/dev/null; then
if openclaw browser doctor 2>&1 | grep -q "not running"; then
echo "🦞 浏览器未启动,尝试启动..."
openclaw browser start 2>/dev/null || echo "⚠️ 浏览器启动失败,可手动运行 openclaw browser start"
else
echo "✅ 浏览器工具已就绪"
fi
fi
echo "✅ 任务流程脚本: scripts/task-flow.sh"
echo "✅ Skill 提取脚本: scripts/extract-skill.sh"
echo ""
echo "✨ 工作空间就绪"
echo " 目录: $(pwd)"
echo " 日期: $(date '+%Y-%m-%d %H:%M')"
+74
View File
@@ -0,0 +1,74 @@
/**
* PR-C Deprecation Warning Utility
* Provides `warnOnce(key, message)` — same key won't print twice.
* Used by deprecation checks across all modules.
*/
const _warned = new Set();
/**
* Emit a deprecation warning once per process lifetime.
* @param {string} key - unique key for this warning (e.g. "active-memory.blocking")
* @param {string} message - human-readable message
* @param {object} [opts]
* @param {boolean} [opts.force] - if true, emit even if already warned
* @returns {boolean} true if warning was emitted, false if skipped (duplicate)
*/
export function warnOnce(key, message, opts = {}) {
if (!opts.force && _warned.has(key)) {
return false;
}
_warned.add(key);
console.warn(`[DEPRECATED][OpenClaw v2] ${key}: ${message}`);
return true;
}
/**
* Reset all warned keys (for testing).
*/
export function resetWarnings() {
_warned.clear();
}
/**
* Get count of unique warnings emitted so far.
*/
export function getWarningCount() {
return _warned.size;
}
/**
* Get all warned keys.
*/
export function getWarnedKeys() {
return [..._warned];
}
/**
* Emit multiple deprecation warnings with deduplication.
* @param {Array<{key: string, message: string}>} warnings
*/
export function warnMany(warnings) {
const emitted = [];
for (const w of warnings) {
if (warnOnce(w.key, w.message)) {
emitted.push(w);
}
}
return emitted;
}
/**
* Run a deprecation check function and collect warnings.
* @param {string} checkName - name of the check
* @param {() => Array<{key:string, message:string}>} fn - check function
* @returns {{ name: string, warnings: Array<{key:string, message:string}> }}
*/
export function runDeprecationCheck(checkName, fn) {
try {
const warnings = fn();
return { name: checkName, warnings };
} catch (e) {
return { name: checkName, warnings: [{ key: checkName, message: `Check failed: ${e.message}` }] };
}
}
+196
View File
@@ -0,0 +1,196 @@
#!/usr/bin/env bash
# memory-auto.sh — 记忆自动触发器(Phase 4)
#
# 让记忆系统"自己动起来":
# 1. 会话开始时自动加载 Core Memory
# 2. 对话中自动判断是否需要记忆操作
# 3. 会话结束时自动 flush
# 4. 定期自动维护
#
# 用法:
# bash scripts/memory-auto.sh on-start # 会话开始时调用
# bash scripts/memory-auto.sh on-message # 收到消息时调用(传入消息)
# bash scripts/memory-auto.sh on-end # 会话结束时调用
# bash scripts/memory-auto.sh on-idle # 空闲时调用(维护)
# bash scripts/memory-auto.sh status # 查看自动触发状态
set -euo pipefail
WORKSPACE="$(cd "$(dirname "$0")/.." && pwd)"
MEMORY_DIR="$WORKSPACE/memory"
CORE_DIR="$MEMORY_DIR/core"
LOG="$MEMORY_DIR/.memory-auto.log"
# 加载 API Key
if [ -z "${DEEPSEEK_API_KEY:-}" ] && [ -f ~/.zshrc ]; then
eval "$(grep '^export DEEPSEEK_API_KEY=' ~/.zshrc 2>/dev/null || true)"
fi
export DEEPSEEK_API_KEY
log() {
echo "$(date '+%Y-%m-%d %H:%M') | $1" >> "$LOG"
}
# ── 1. 会话开始 ──────────────────────────────────────
on_start() {
echo "🚀 记忆系统: 会话开始"
echo ""
# 加载 Core Memory
echo "🧠 加载 Core Memory Blocks:"
for f in "$CORE_DIR"/*.md; do
[ -f "$f" ] || continue
local name=$(basename "$f" .md)
local size=$(wc -c < "$f" | tr -d ' ')
echo "$name ($size bytes)"
done
echo ""
# 检查今天是否已有 daily
local today="$MEMORY_DIR/daily/$(date +%Y-%m-%d).md"
if [ ! -f "$today" ]; then
echo "📅 创建今天的 daily: $(date +%Y-%m-%d)"
echo "# $(date +%Y-%m-%d)" > "$today"
echo "" >> "$today"
fi
# 检查 open-loops
if [ -f "$MEMORY_DIR/registers/open-loops.md" ]; then
open=$(grep -c '^\- \[ \]' "$MEMORY_DIR/registers/open-loops.md" 2>/dev/null || true)
open=${open:-0}
[ "$open" -gt 0 ] && echo "📋 有 $open 个未完成的待办"
fi
log "session_start"
echo ""
echo "✅ 记忆系统就绪"
}
# ── 2. 消息处理 ──────────────────────────────────────
on_message() {
local message="${1:-}"
[ -z "$message" ] && return
# 快速判断:这条消息是否值得记忆操作
local should_process=false
# 长消息可能包含重要信息
[ ${#message} -gt 50 ] && should_process=true
# 包含关键词
if echo "$message" | grep -qE '(记住|忘掉|偏好|纠正|决定|配置|部署|修复)'; then
should_process=true
fi
# 包含问句(可能需要搜索)
if echo "$message" | grep -qE '(记得|之前|上次|以前|什么|哪个|怎么|为什么)'; then
should_process=true
fi
$should_process || return
# 静默调用 Write Gate(后台)
if [ -f "$WORKSPACE/scripts/memory-write-gate.sh" ]; then
echo "$message" | bash "$WORKSPACE/scripts/memory-write-gate.sh" > /dev/null 2>&1 &
fi
log "message_processed: ${message:0:50}"
}
# ── 3. 会话结束 ──────────────────────────────────────
on_end() {
echo "💾 记忆系统: 会话结束"
echo ""
# 触发 session-compact pre-compact
if [ -f "$WORKSPACE/scripts/session-compact.sh" ]; then
echo "📦 执行会话压缩..."
bash "$WORKSPACE/scripts/session-compact.sh" pre-compact 2>&1 | sed 's/^/ /'
fi
# 检查是否有未保存的记忆
local today="$MEMORY_DIR/daily/$(date +%Y-%m-%d).md"
if [ -f "$today" ]; then
local today_size=$(wc -c < "$today" | tr -d ' ')
echo " 今天的 daily: $today_size bytes"
fi
log "session_end"
echo ""
echo "✅ 会话记忆已保存"
}
# ── 4. 空闲维护 ──────────────────────────────────────
on_idle() {
echo "🔧 记忆系统: 空闲维护"
echo ""
# 1. 实体提取
if [ -f "$WORKSPACE/scripts/memory-entity.sh" ]; then
echo "🧬 实体提取..."
bash "$WORKSPACE/scripts/memory-entity.sh" extract 2>&1 | sed 's/^/ /'
fi
# 2. 健康检查
if [ -f "$WORKSPACE/scripts/memory-health.sh" ]; then
echo ""
echo "🏥 健康检查..."
bash "$WORKSPACE/scripts/memory-health.sh" score 2>&1 | tail -5 | sed 's/^/ /'
fi
log "idle_maintenance"
echo ""
echo "✅ 维护完成"
}
# ── 5. 状态查看 ──────────────────────────────────────
status() {
echo "📊 记忆自动触发状态"
echo ""
# Core Memory 状态
echo "🧠 Core Memory:"
for f in "$CORE_DIR"/*.md; do
[ -f "$f" ] || continue
local name=$(basename "$f" .md)
local mtime=$(stat -f "%Sm" -t "%Y-%m-%d %H:%M" "$f" 2>/dev/null || echo "unknown")
local size=$(wc -c < "$f" | tr -d ' ')
echo " $name: $size bytes ($mtime)"
done
echo ""
echo "📋 日志 (最近 5 条):"
[ -f "$LOG" ] && tail -5 "$LOG" || echo " (无)"
echo ""
echo "🔧 可用脚本:"
for script in write-gate recall entity compressor llm-router behavior health search; do
local path="$WORKSPACE/scripts/memory-${script}.sh"
[ -f "$path" ] && echo " ✅ memory-${script}.sh" || echo " ❌ memory-${script}.sh"
done
}
# ── 入口 ─────────────────────────────────────────────
case "${1:-help}" in
on-start) on_start ;;
on-message) on_message "${2:-}" ;;
on-end) on_end ;;
on-idle) on_idle ;;
status) status ;;
help|*)
echo "用法: bash scripts/memory-auto.sh <命令>"
echo ""
echo "命令:"
echo " on-start 会话开始时调用(加载 Core Memory"
echo " on-message 收到消息时调用(传入消息内容)"
echo " on-end 会话结束时调用(flush + 压缩)"
echo " on-idle 空闲时调用(维护)"
echo " status 查看状态"
;;
esac
+210
View File
@@ -0,0 +1,210 @@
#!/usr/bin/env bash
# memory-sync — 记忆同步与维护工具
#
# 从 Hermes Agent MemoryManager + MemoryStore 模式内化
# 管理 memory/index.md -> 键值对索引
# 管理 memory/projects/ -> 项目级记忆
#
# 用法:
# bash scripts/memory-sync.sh index # 重建记忆索引
# bash scripts/memory-sync.sh check # 检查记忆健康状况
# bash scripts/memory-sync.sh prefetch <key> # 根据关键词召回相关记忆
# bash scripts/memory-sync.sh backup # 备份所有记忆文件
set -euo pipefail
WORKSPACE="$(cd "$(dirname "$0")/.." && pwd)"
MEMORY_DIR="$WORKSPACE/memory"
PROJECTS_DIR="$MEMORY_DIR/projects"
INDEX_FILE="$MEMORY_DIR/index.md"
# ── 颜色 ──
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
case "${1:-}" in
index)
echo "🔨 重建记忆索引..."
# 清空并重建索引
cat > "$INDEX_FILE" << 'EOF'
# 记忆索引
> 自动生成,供 Fast-Path 快速查找。
> 格式:`关键词 | 所在文件 | 摘要`
EOF
# 索引 MEMORY.md 中的关键段落
echo "" >> "$INDEX_FILE"
echo "## MEMORY.md" >> "$INDEX_FILE"
# 提取各章节标题
while IFS= read -r line; do
if echo "$line" | grep -qE '^### '; then
title=$(echo "$line" | sed 's/^### //')
echo "- $title | MEMORY.md | 章节" >> "$INDEX_FILE"
fi
done < "$WORKSPACE/MEMORY.md"
# 索引项目记忆
echo "" >> "$INDEX_FILE"
echo "## 项目记忆" >> "$INDEX_FILE"
for f in "$PROJECTS_DIR"/*.md; do
[ -f "$f" ] || continue
name=$(basename "$f" .md)
echo "- $name | memory/projects/$name.md | 项目记忆" >> "$INDEX_FILE"
# 提取项目中的关键标签
while IFS= read -r line; do
if echo "$line" | grep -qE '^\| .+ \| .+ \|'; then
echo " - $(echo "$line" | awk -F'|' '{print $2}' | xargs)" >> "$INDEX_FILE"
fi
done < "$f"
done
# 索引 daily 日志
echo "" >> "$INDEX_FILE"
echo "## 日常日志" >> "$INDEX_FILE"
for f in "$MEMORY_DIR/daily"/*.md; do
[ -f "$f" ] || continue
name=$(basename "$f" .md)
# 读取第一行非空标题
title=$(head -3 "$f" | grep -E '^# ' | sed 's/^# //' || echo "$name")
echo "- $title | memory/daily/$name" >> "$INDEX_FILE"
done
echo -e "${GREEN}✅ 索引重建完成:$INDEX_FILE${NC}"
wc -l < "$INDEX_FILE" | xargs echo " $(( $(wc -l < "$INDEX_FILE") - 4 )) 条索引条目"
;;
check)
echo "🔍 记忆健康检查 (Hermes 风格)"
echo ""
# 1. 检查索引是否存在
if [ -f "$INDEX_FILE" ]; then
echo -e " ${GREEN}✅ 索引文件存在${NC}"
echo " $(grep -c '^-' "$INDEX_FILE") 条可用索引"
else
echo -e " ${YELLOW}⚠️ 索引文件缺失,运行 memory-sync.sh index 创建${NC}"
fi
# 2. MEMORY.md 大小检查
SIZE=$(wc -c < "$WORKSPACE/MEMORY.md")
if [ "$SIZE" -gt 10000 ]; then
echo -e " ${YELLOW}⚠️ MEMORY.md 较大: ${SIZE} 字节 (>10K),建议精简${NC}"
else
echo -e " ${GREEN}✅ MEMORY.md: ${SIZE} 字节${NC}"
fi
# 3. 项目记忆数量
PCOUNT=$(find "$PROJECTS_DIR" -name '*.md' 2>/dev/null | wc -l | tr -d ' ')
echo -e " ${GREEN}✅ 项目记忆: ${PCOUNT}${NC}"
# 4. 最近的 daily 记录
LATEST=$(ls -t "$MEMORY_DIR/daily/" 2>/dev/null | head -1)
if [ -n "$LATEST" ]; then
echo -e " ${GREEN}✅ 最近 daily: $LATEST${NC}"
fi
# 5. git 状态
cd "$WORKSPACE"
if [ -n "$(git status --short 2>/dev/null)" ]; then
echo -e " ${YELLOW}⚠️ 有未提交的记忆更改${NC}"
else
echo -e " ${GREEN}✅ 所有记忆更改已提交${NC}"
fi
;;
prefetch)
KEY="${2:-}"
if [ -z "$KEY" ]; then
echo "用法: bash scripts/memory-sync.sh prefetch <关键词>"
echo "示例: bash scripts/memory-sync.sh prefetch hermes"
exit 1
fi
echo "🔎 预取记忆:$KEY"
echo ""
# 1. 查索引
if [ -f "$INDEX_FILE" ]; then
MATCHES=$(grep -i "$KEY" "$INDEX_FILE" | head -10)
if [ -n "$MATCHES" ]; then
echo "📋 索引匹配:"
echo "$MATCHES"
echo ""
fi
fi
# 2. 查项目记忆
for f in "$PROJECTS_DIR"/*.md; do
[ -f "$f" ] || continue
if grep -qi "$KEY" "$f"; then
name=$(basename "$f" .md)
echo "📂 项目记忆匹配:$name"
grep -i "$KEY" "$f" | head -5
echo ""
fi
done
# 3. 查 MEMORY.md
if grep -qi "$KEY" "$WORKSPACE/MEMORY.md"; then
echo "📄 MEMORY.md 匹配:"
grep -i "$KEY" "$WORKSPACE/MEMORY.md" | head -5
fi
;;
backup)
BACKUP_DIR="$WORKSPACE/.memory-backup-$(date '+%Y%m%d-%H%M%S')"
echo "💾 备份记忆文件到 $BACKUP_DIR"
mkdir -p "$BACKUP_DIR"
cp "$WORKSPACE/MEMORY.md" "$BACKUP_DIR/" 2>/dev/null || true
cp -r "$MEMORY_DIR" "$BACKUP_DIR/" 2>/dev/null || true
echo -e "${GREEN}✅ 备份完成${NC}"
;;
compact)
echo "🧹 检查 MEMORY.md 是否需要压缩..."
LINES=$(wc -l < "$WORKSPACE/MEMORY.md")
CHARS=$(wc -c < "$WORKSPACE/MEMORY.md")
echo " 当前: ${LINES} 行 / ${CHARS} 字节"
if [ "$LINES" -gt 200 ] || [ "$CHARS" -gt 10000 ]; then
echo -e " ${YELLOW}⚠️ 需要压缩(超限)${NC}"
echo " 压缩策略:"
echo " 1. 低优先级条目(环境事实)→ 移至 daily/ 存档"
echo " 2. 同类条目合并为一条"
echo " 3. 已固化的技能从 MEMORY.md 移入 tools/"
else
echo -e " ${GREEN}✅ 大小正常${NC}"
fi
;;
stats)
echo "📊 记忆仓使用统计"
echo ""
for f in "$WORKSPACE/MEMORY.md" "$MEMORY_DIR/USER.md" "$MEMORY_DIR/index.md"; do
if [ -f "$f" ]; then
L=$(wc -l < "$f")
C=$(wc -c < "$f")
echo " $(basename "$f"): ${L} 行 / ${C} 字节"
fi
done
echo ""
echo " daily/$(ls "$MEMORY_DIR/daily/" 2>/dev/null | wc -l) 个文件"
echo " projects/$(ls "$MEMORY_DIR/projects/" 2>/dev/null | wc -l) 个文件"
;;
*)
echo "用法: bash scripts/memory-sync.sh <子命令>"
echo ""
echo " index 重建记忆索引"
echo " check 检查记忆健康状况"
echo " prefetch <k> 根据关键词召回相关记忆"
echo " backup 备份所有记忆文件"
echo " compact 压缩 MEMORY.md(溢出时合并同类条目)"
echo " stats 统计各记忆仓使用情况"
;;
esac
+648
View File
@@ -0,0 +1,648 @@
/**
* Shared Model Contract Layer
*
* 单一字段定义源(Single Source of Truth)。
* 所有 agent 通过 import { createContract } from "./model-contract.mjs" 引用,
* 禁止自行定义任何字段名。
*
* 提供:
* - entities[] — 所有实体的字段定义(PascalCase 名 + snake_case 表名)
* - auth{} — 认证配置
* - permissions[] — RBAC 权限矩阵
* - fieldMapping{} — snake_case ↔ camelCase 双向映射
* - validation{} — 字段级校验规则
*/
// ═══════════════════════════════════════════════════════
// 1. Domain-Specific Entity Templates
// ═══════════════════════════════════════════════════════
/**
* Base entity templates for common domains.
* These are the canonical field definitions for every entity.
* Key: domain name → Array of entity templates.
*/
const DOMAIN_ENTITIES = {
enterprise: [
{
name: "User",
table: "users",
description: "系统用户",
fields: [
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
{ name: "username", type: "VARCHAR(128)", required: true, unique: true, validation: { minLength: 2, maxLength: 50 } },
{ name: "password_hash", type: "VARCHAR(256)", required: true, isSecret: true },
{ name: "nickname", type: "VARCHAR(128)" },
{ name: "role", type: "VARCHAR(32)", defaultValue: "user", enum: ["user", "admin"] },
{ name: "phone", type: "VARCHAR(20)" },
{ name: "avatar_url", type: "TEXT" },
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
],
relationships: [
{ type: "hasMany", entity: "contracts", via: "user_id" },
{ type: "hasMany", entity: "customers", via: "user_id" },
],
},
{
name: "Contract",
table: "contracts",
description: "合同",
fields: [
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
{ name: "user_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" },
{ name: "title", type: "VARCHAR(256)", required: true },
{ name: "party_a", type: "VARCHAR(128)" },
{ name: "party_b", type: "VARCHAR(128)" },
{ name: "amount", type: "DECIMAL(14,2)" },
{ name: "signed_at", type: "DATE" },
{ name: "expires_at", type: "DATE" },
{ name: "status", type: "VARCHAR(32)", defaultValue: "draft", enum: ["draft", "pending", "active", "expired", "terminated"] },
{ name: "file_url", type: "TEXT" },
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
],
relationships: [
{ type: "belongsTo", entity: "users", via: "user_id" },
],
},
{
name: "Approval",
table: "approvals",
description: "审批流程",
fields: [
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
{ name: "user_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" },
{ name: "entity_type", type: "VARCHAR(32)", required: true },
{ name: "entity_id", type: "UUID" },
{ name: "applicant_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" },
{ name: "status", type: "VARCHAR(32)", defaultValue: "pending", enum: ["pending", "approved", "rejected"] },
{ name: "form_data", type: "JSONB" },
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
],
relationships: [
{ type: "belongsTo", entity: "users", via: "user_id" },
{ type: "belongsTo", entity: "users", via: "applicant_id", as: "applicant" },
],
},
{
name: "Customer",
table: "customers",
description: "客户",
fields: [
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
{ name: "user_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" },
{ name: "name", type: "VARCHAR(128)", required: true },
{ name: "email", type: "VARCHAR(256)" },
{ name: "phone", type: "VARCHAR(20)" },
{ name: "company", type: "VARCHAR(128)" },
{ name: "source", type: "VARCHAR(64)" },
{ name: "tags", type: "JSONB", defaultValue: "[]" },
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
],
relationships: [
{ type: "belongsTo", entity: "users", via: "user_id" },
],
},
{
name: "Reminder",
table: "reminders",
description: "到期提醒",
fields: [
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
{ name: "user_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" },
{ name: "entity_type", type: "VARCHAR(32)", required: true },
{ name: "entity_id", type: "UUID" },
{ name: "remind_at", type: "TIMESTAMP", required: true },
{ name: "message", type: "TEXT" },
{ name: "sent", type: "BOOLEAN", defaultValue: "false" },
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
],
relationships: [
{ type: "belongsTo", entity: "users", via: "user_id" },
],
},
],
pet: [
{
name: "User",
table: "users",
description: "系统用户",
fields: [
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
{ name: "username", type: "VARCHAR(128)", required: true, unique: true, validation: { minLength: 2, maxLength: 50 } },
{ name: "password_hash", type: "VARCHAR(256)", required: true, isSecret: true },
{ name: "nickname", type: "VARCHAR(128)" },
{ name: "role", type: "VARCHAR(32)", defaultValue: "user", enum: ["user", "admin"] },
{ name: "phone", type: "VARCHAR(20)" },
{ name: "avatar_url", type: "TEXT" },
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
],
relationships: [],
},
{
name: "Pet",
table: "pets",
description: "宠物",
fields: [
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
{ name: "user_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" },
{ name: "name", type: "VARCHAR(64)", required: true },
{ name: "species", type: "VARCHAR(32)" },
{ name: "breed", type: "VARCHAR(64)" },
{ name: "birth_date", type: "DATE" },
{ name: "weight_kg", type: "DECIMAL(5,2)" },
{ name: "avatar_url", type: "TEXT" },
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
],
relationships: [
{ type: "belongsTo", entity: "users", via: "user_id" },
],
},
],
ecommerce: [
{
name: "User",
table: "users",
description: "系统用户",
fields: [
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
{ name: "username", type: "VARCHAR(128)", required: true, unique: true, validation: { minLength: 2, maxLength: 50 } },
{ name: "password_hash", type: "VARCHAR(256)", required: true, isSecret: true },
{ name: "nickname", type: "VARCHAR(128)" },
{ name: "role", type: "VARCHAR(32)", defaultValue: "user", enum: ["user", "admin"] },
{ name: "phone", type: "VARCHAR(20)" },
{ name: "avatar_url", type: "TEXT" },
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
],
relationships: [],
},
{
name: "Product",
table: "products",
description: "商品",
fields: [
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
{ name: "user_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" },
{ name: "title", type: "VARCHAR(256)", required: true },
{ name: "description", type: "TEXT" },
{ name: "price", type: "DECIMAL(10,2)" },
{ name: "stock", type: "INTEGER", defaultValue: "0" },
{ name: "images", type: "JSONB", defaultValue: "[]" },
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
],
relationships: [
{ type: "belongsTo", entity: "users", via: "user_id" },
],
},
{
name: "Order",
table: "orders",
description: "订单",
fields: [
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
{ name: "user_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" },
{ name: "status", type: "VARCHAR(32)", defaultValue: "pending", enum: ["pending", "paid", "shipped", "delivered", "cancelled"] },
{ name: "total_amount", type: "DECIMAL(12,2)" },
{ name: "address_id", type: "UUID", fkEntity: "addresses", fkColumn: "id" },
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
],
relationships: [
{ type: "belongsTo", entity: "users", via: "user_id" },
],
},
],
education: [
{
name: "User",
table: "users",
description: "系统用户",
fields: [
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
{ name: "username", type: "VARCHAR(128)", required: true, unique: true, validation: { minLength: 2, maxLength: 50 } },
{ name: "password_hash", type: "VARCHAR(256)", required: true, isSecret: true },
{ name: "nickname", type: "VARCHAR(128)" },
{ name: "role", type: "VARCHAR(32)", defaultValue: "user", enum: ["user", "admin", "instructor"] },
{ name: "phone", type: "VARCHAR(20)" },
{ name: "avatar_url", type: "TEXT" },
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
],
relationships: [],
},
],
note: [
{
name: "User",
table: "users",
description: "系统用户",
fields: [
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
{ name: "username", type: "VARCHAR(128)", required: true, unique: true, validation: { minLength: 2, maxLength: 50 } },
{ name: "password_hash", type: "VARCHAR(256)", required: true, isSecret: true },
{ name: "nickname", type: "VARCHAR(128)" },
{ name: "role", type: "VARCHAR(32)", defaultValue: "user", enum: ["user", "admin"] },
{ name: "phone", type: "VARCHAR(20)" },
{ name: "avatar_url", type: "TEXT" },
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
],
relationships: [],
},
],
fitness: [
{
name: "User",
table: "users",
description: "系统用户",
fields: [
{ name: "id", type: "UUID", isPrimary: true, isAuto: true },
{ name: "username", type: "VARCHAR(128)", required: true, unique: true, validation: { minLength: 2, maxLength: 50 } },
{ name: "password_hash", type: "VARCHAR(256)", required: true, isSecret: true },
{ name: "nickname", type: "VARCHAR(128)" },
{ name: "role", type: "VARCHAR(32)", defaultValue: "user", enum: ["user", "admin"] },
{ name: "phone", type: "VARCHAR(20)" },
{ name: "avatar_url", type: "TEXT" },
{ name: "created_at", type: "TIMESTAMP", isAuto: true },
{ name: "updated_at", type: "TIMESTAMP", isAuto: true },
],
relationships: [],
},
],
};
// ═══════════════════════════════════════════════════════
// 2. Field Mapping Engine
// ═══════════════════════════════════════════════════════
/**
* Build bidirectional snake_case ↔ camelCase mappings from all contract entities.
* Scanned once; cached in the returned contract.
*/
function buildFieldMappings(entities) {
const snakeToCamel = {};
const camelToSnake = {};
// Base conversions from common SQL columns
const baseMappings = {
snakeToCamel: {
"created_at": "createdAt",
"updated_at": "updatedAt",
"user_id": "userId",
},
camelToSnake: {
"createdAt": "created_at",
"updatedAt": "updated_at",
"userId": "user_id",
},
};
Object.assign(snakeToCamel, baseMappings.snakeToCamel);
Object.assign(camelToSnake, baseMappings.camelToSnake);
for (const entity of entities) {
for (const field of entity.fields) {
const snake = field.name; // fields are stored in snake_case
// Convert to camelCase
const camel = snake.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
if (snake !== camel) {
snakeToCamel[snake] = camel;
camelToSnake[camel] = snake;
}
}
}
// Add common patterns
const known = [
["id", "id"],
["username", "username"],
["password_hash", "passwordHash"],
["nickname", "nickname"],
["role", "role"],
["phone", "phone"],
["avatar_url", "avatarUrl"],
["title", "title"],
["description", "description"],
["status", "status"],
["data", "data"],
["amount", "amount"],
["party_a", "partyA"],
["party_b", "partyB"],
["signed_at", "signedAt"],
["expires_at", "expiresAt"],
["file_url", "fileUrl"],
["entity_type", "entityType"],
["entity_id", "entityId"],
["applicant_id", "applicantId"],
["form_data", "formData"],
["name", "name"],
["email", "email"],
["company", "company"],
["source", "source"],
["tags", "tags"],
["breed", "breed"],
["species", "species"],
["birth_date", "birthDate"],
["weight_kg", "weightKg"],
["price", "price"],
["stock", "stock"],
["images", "images"],
["total_amount", "totalAmount"],
["address_id", "addressId"],
["remind_at", "remindAt"],
["message", "message"],
["sent", "sent"],
];
for (const [snake, camel] of known) {
if (!snakeToCamel[snake]) snakeToCamel[snake] = camel;
if (!camelToSnake[camel]) camelToSnake[camel] = snake;
}
return { snakeToCamel, camelToSnake };
}
// ═══════════════════════════════════════════════════════
// 3. Auth Configuration
// ═══════════════════════════════════════════════════════
function buildAuthConfig(domain) {
// Enterprise domain has stricter auth
const isEnterprise = domain === "enterprise";
return {
registrationFields: ["username", "password", "nickname"],
loginFields: ["username", "password"],
jwtPayload: ["userId", "username", "role"],
passwordPolicy: {
minLength: isEnterprise ? 8 : 6,
requireSpecial: isEnterprise,
},
};
}
// ═══════════════════════════════════════════════════════
// 4. Permissions Matrix
// ═══════════════════════════════════════════════════════
function buildPermissions(domain) {
return [
{ role: "admin", allow: ["*"] },
{ role: "user", allow: ["read:own", "create:*", "update:own", "delete:own"] },
];
}
// ═══════════════════════════════════════════════════════
// 5. Validation Rules
// ═══════════════════════════════════════════════════════
function buildValidation(entities) {
const rules = {};
for (const entity of entities) {
const fieldRules = {};
for (const field of entity.fields) {
if (field.validation) {
fieldRules[field.name] = { ...field.validation };
}
// Auto-generate from field properties
if (field.required || field.unique || field.enum || field.defaultValue !== undefined) {
if (!fieldRules[field.name]) fieldRules[field.name] = {};
}
if (field.required) {
fieldRules[field.name].required = true;
}
if (field.unique) {
fieldRules[field.name].unique = true;
}
if (field.enum) {
fieldRules[field.name].enum = field.enum;
}
if (field.defaultValue !== undefined) {
fieldRules[field.name].defaultValue = field.defaultValue;
}
}
if (Object.keys(fieldRules).length > 0) {
rules[entity.name] = fieldRules;
}
}
return rules;
}
// ═══════════════════════════════════════════════════════
// 6. Main Contract Builder
// ═══════════════════════════════════════════════════════
/**
* Create a Model Contract from PRD and Architecture.
*
* The contract is the SINGLE SOURCE OF TRUTH for all entity field definitions.
* All generators MUST read from the contract; they MUST NOT invent field names.
*
* @param {object} prd — PRD from SF-01 (project-intake-agent)
* @param {object} arch — Architecture from SF-02 (architecture-agent), optional
* @returns {object} Contract object
*/
export function createContract(prd, arch) {
const domain = (prd && prd.domain) || (arch && arch.domain) || "generic";
const projectName = (prd && prd.projectName) || (arch && arch.projectName) || "App";
// ── Get domain-specific entities ──
let entities = DOMAIN_ENTITIES[domain] || DOMAIN_ENTITIES.enterprise;
// Deep clone to avoid mutation
entities = JSON.parse(JSON.stringify(entities));
// ── Augment with domain-specific entities from features/APIs ──
// If architecture has derived tables, merge them into the contract
if (arch && arch.databaseSchema) {
const existingTables = new Set(entities.map(e => e.table));
const existingNames = new Set(entities.map(e => e.name));
for (const table of arch.databaseSchema) {
if (!existingTables.has(table.table) && table.table !== "users") {
// Auto-generate PascalCase entity name from snake_case table
const pascalName = table.table
.split("_")
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
.join("");
if (!existingNames.has(pascalName)) {
const entity = {
name: pascalName,
table: table.table,
description: table.description || `${pascalName}`,
fields: (table.fields || []).map(f => ({
name: f.name,
type: f.type || "TEXT",
required: (f.constraints || "").includes("NOT NULL") || (f.constraints || "").includes("PK"),
isPrimary: (f.constraints || "").includes("PK"),
isAuto: f.name === "created_at" || f.name === "updated_at" || (f.constraints || "").includes("DEFAULT"),
...((f.constraints || "").includes("UNIQUE") ? { unique: true } : {}),
...((f.constraints || "").includes("FK") ? {
fkEntity: (f.name.endsWith("_id") ? f.name.replace(/_id$/, "s") : "items"),
fkColumn: "id",
} : {}),
})),
relationships: [],
};
// Run FK detection on fields
for (const field of entity.fields) {
if (field.fkEntity) {
entity.relationships.push({
type: "belongsTo",
entity: field.fkEntity,
via: field.name,
});
}
}
entities.push(entity);
existingTables.add(table.table);
existingNames.add(pascalName);
}
}
}
}
// ── Build mappings ──
const fieldMapping = buildFieldMappings(entities);
const auth = buildAuthConfig(domain);
const permissions = buildPermissions(domain);
const validation = buildValidation(entities);
return {
projectName,
domain,
entities,
auth,
permissions,
fieldMapping,
validation,
};
}
// ═══════════════════════════════════════════════════════
// 7. Contract Utility Functions
// ═══════════════════════════════════════════════════════
/**
* Look up a field in the contract by entity.table and field name (snake_case).
*
* @param {object} contract
* @param {string} tableName — snake_case table name
* @param {string} fieldName — snake_case field name
* @returns {object|null} Field definition
*/
export function lookupField(contract, tableName, fieldName) {
for (const entity of contract.entities) {
if (entity.table === tableName) {
return entity.fields.find(f => f.name === fieldName) || null;
}
}
return null;
}
/**
* Get the camelCase name for a snake_case column from the contract's fieldMapping.
*
* @param {object} contract
* @param {string} snakeName
* @returns {string}
*/
export function toCamel(contract, snakeName) {
return contract.fieldMapping.snakeToCamel[snakeName] || snakeName;
}
/**
* Get the snake_case name for a camelCase property from the contract's fieldMapping.
*
* @param {object} contract
* @param {string} camelName
* @returns {string}
*/
export function toSnake(contract, camelName) {
return contract.fieldMapping.camelToSnake[camelName] || camelName;
}
/**
* Get the TypeScript type for a contract field.
*
* @param {object} field — Contract field definition
* @returns {string} TypeScript type string
*/
export function tsType(field) {
const t = (field.type || "").toUpperCase();
if (t.includes("INT") || t.includes("SERIAL") || t.includes("BIGINT") || t.includes("DECIMAL") || t.includes("NUMERIC") || t.includes("FLOAT") || t.includes("DOUBLE") || t.includes("REAL")) return "number";
if (t.includes("BOOL")) return "boolean";
if (t.includes("JSONB") || t.includes("JSON")) return "Record<string, unknown>";
return "string";
}
/**
* Get the SQLite column type for a contract field.
*
* @param {object} field — Contract field definition
* @returns {string} SQLite type
*/
export function sqliteType(field) {
const t = (field.type || "").toUpperCase();
if (t.includes("UUID") || t.includes("TEXT") || t.includes("VARCHAR") || t.includes("CHAR")) return "TEXT";
if (t.includes("INT") || t.includes("SERIAL") || t.includes("BIGINT")) return "INTEGER";
if (t.includes("DECIMAL") || t.includes("NUMERIC") || t.includes("FLOAT") || t.includes("DOUBLE") || t.includes("REAL")) return "REAL";
if (t.includes("BOOL")) return "INTEGER";
if (t.includes("DATE") || t.includes("TIME") || t.includes("TIMESTAMP")) return "TEXT";
if (t.includes("JSONB") || t.includes("JSON")) return "TEXT";
return "TEXT";
}
/**
* Get the non-auto fields for CreateInput (exclude id, created_at, updated_at).
*
* @param {object} entity — Contract entity
* @returns {object[]} Fields for CreateInput
*/
export function createInputFields(entity) {
return entity.fields.filter(f =>
!f.isPrimary && !f.isAuto && f.name !== "created_at" && f.name !== "updated_at"
);
}
/**
* Get the non-identity fields for UpdateInput (exclude id, created_at, updated_at).
*
* @param {object} entity — Contract entity
* @returns {object[]} Fields for UpdateInput
*/
export function updateInputFields(entity) {
return createInputFields(entity);
}
/**
* Check if an entity has the given field (by snake_case name).
*/
export function hasField(entity, snakeName) {
return entity.fields.some(f => f.name === snakeName);
}
// ═══════════════════════════════════════════════════════
// 8. Export DOMAIN_ENTITIES for introspection
// ═══════════════════════════════════════════════════════
export { DOMAIN_ENTITIES };
+263
View File
@@ -0,0 +1,263 @@
#!/usr/bin/env node
/**
* Orchestrator — V2 Protocol Stack → Factory Pipeline Bridge
*
* Minimal orchestrator that:
* 1. Accepts user input (natural language)
* 2. Generates a Task Tree + Plan record (AGENTS.md §12)
* 3. Routes to the correct factory chain via Factory Router
* 4. Captures Governance-compliant heartbeat into progress.log
* 5. Returns structured result summary
*
* Does NOT:
* - Re-implement AGENTS.md / GOVERNANCE.md rules
* - Run state machines
* - Replace existing CLI entry points
*
* Usage:
* node scripts/orchestrator.mjs --input "做一个宠物管理 App" [--output <dir>]
* node scripts/orchestrator.mjs --input "做一个在线教育平台" --domain education
*
* @module orchestrator
*/
import { writeFileSync, appendFileSync, existsSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { mkdirSync } from "node:fs";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, "..");
// ── Constants ──────────────────────────────────────
const DEFAULT_OUTPUT = resolve(ROOT, "fullstack");
const PROGRESS_LOG_PATH = resolve(ROOT, "progress.log");
// ── CLI ────────────────────────────────────────────
function parseArgs() {
const args = process.argv.slice(2);
const opts = { input: null, output: null, domain: null, verbose: false, help: false };
for (let i = 0; i < args.length; i++) {
if (args[i] === "--input" && args[i + 1]) opts.input = args[++i];
else if (args[i] === "--output" && args[i + 1]) opts.output = args[++i];
else if (args[i] === "--domain" && args[i + 1]) opts.domain = args[++i];
else if (args[i] === "--verbose") opts.verbose = true;
else if (args[i] === "--help" || args[i] === "-h") opts.help = true;
}
return opts;
}
async function main() {
const opts = parseArgs();
if (opts.help) {
console.log(`
Orchestrator — V2 Protocol Stack ↔ Factory Pipeline
Usage:
node scripts/orchestrator.mjs --input <text> [options]
Options:
--input <text> 用户需求描述(必填)
--output <dir> 输出目录(default: fullstack/
--domain <name> 强制领域(override auto-detect
--verbose 详细输出
--help 显示帮助
Examples:
node scripts/orchestrator.mjs --input "做一个宠物管理 App"
node scripts/orchestrator.mjs --input "做一个在线教育平台" --output ./my-project
`);
return;
}
if (!opts.input) {
console.error("Error: --input is required. Use --help for usage.");
process.exit(1);
}
try {
const result = await run(opts.input, opts);
console.log(JSON.stringify(result.summary, null, 2));
} catch (e) {
console.error(`Orchestrator failed: ${e.message}`);
// Write failure heartbeat
writeHeartbeat({ status: "FAILED", remaining: [], risk: e.message, next_action: "investigate failure" });
process.exit(1);
}
}
// ── Main Pipeline ─────────────────────────────────
/**
* Orchestrator run — the unified entry point.
*
* @param {string} input — User's requirement description
* @param {object} opts
* @param {string} [opts.output] — Output directory
* @param {string} [opts.domain] — Force domain override
* @returns {Promise<{summary: object, plan: object, taskTree: object, heartbeat: object}>}
*/
export async function run(input, opts = {}) {
const startTs = new Date().toISOString();
const plan = buildPlan(input);
// ── 1. Context Init & Heartbeat ──
writeHeartbeat(plan.heartbeat);
// ── 2. V2 Task Tree ──
const taskTree = buildTaskTree(input, opts.domain);
// ── 3. SF-01: Project Intake (PRD generation) ──
const sf01 = await import("./project-intake-agent.mjs");
writeHeartbeat({ ...plan.heartbeat, status: "IN_PROGRESS", completed: ["T1"], remaining: plan.heartbeat.remaining.slice(1), next_action: "SF-02: generate architecture" });
const prd = sf01.generatePRD(input);
if (prd.error === "EMPTY_INPUT") {
throw new Error("PRD generation failed: empty input");
}
if (prd.error === "EXTRACTION_FAILED") {
throw new Error(prd.message);
}
// ── 4. SF-02: Architecture generation ──
const sf02 = await import("./architecture-agent.mjs");
writeHeartbeat({ ...plan.heartbeat, status: "IN_PROGRESS", completed: ["T1", "T2"], remaining: plan.heartbeat.remaining.slice(2), next_action: "SF-03~06: fullstack compose" });
const arch = sf02.generateArchitecture(prd);
if (arch.error) {
throw new Error(`Architecture generation failed: ${arch.message}`);
}
// ── 5. Factory Router: route to fullstack composer ──
const router = await import("./factory-router.mjs");
const routeResult = router.route(prd, { arch });
const factoryMod = await router.loadFactory(routeResult.factoryFn);
writeHeartbeat({ ...plan.heartbeat, status: "IN_PROGRESS", completed: ["T1", "T2", "T3"], remaining: plan.heartbeat.remaining.slice(3), next_action: "writing output" });
const result = await factoryMod.composeFullstack(prd, arch);
// ── 6. Write output ──
const outputDir = opts.output ? resolve(opts.output) : DEFAULT_OUTPUT;
factoryMod.writeFullstack(result, outputDir);
// ── 7. Governance: final heartbeat ──
writeHeartbeat({ ...plan.heartbeat, status: "ARCHIVED", completed: ["T1", "T2", "T3", "T4"], remaining: [], next_action: "none" });
const endTs = new Date().toISOString();
const durationMs = Date.now() - new Date(startTs).getTime();
// ── 8. Summary ──
const summary = {
projectName: prd.projectName,
chineseName: prd.chineseName || prd.projectName,
domain: prd.domain,
factory: routeResult.factory,
outputDir,
stats: result.stats,
startTs,
endTs,
durationMs,
};
return { summary, plan, taskTree, heartbeat: plan.heartbeat };
}
// ── V2 Protocol Artifacts ──────────────────────────
/**
* Build Task Tree per AGENTS.md §1.
*
* @param {string} input
* @param {string} [domainOverride]
* @returns {object} Task Tree
*/
function buildTaskTree(input, domainOverride) {
return {
root: {
id: "T0",
name: "Full-stack Product Generation",
depends_on: null,
description: `Generate full-stack product from: "${input.slice(0, 80)}"`,
},
tasks: [
{ id: "T1", name: "SF-01 Product Strategy", depends_on: ["T0"], description: "Parse requirements → PRD", parallelizable: false },
{ id: "T2", name: "SF-02 Architecture Design", depends_on: ["T1"], description: "PRD → Architecture JSON", parallelizable: false },
{ id: "T3", name: "SF-03~06 Full-stack Generation", depends_on: ["T2"], description: "Arch → NestJS + React code", parallelizable: false },
{ id: "T4", name: "Governance Verification", depends_on: ["T3"], description: "Verify output, write audit", parallelizable: false },
],
parallelGroups: [],
injectedRisks: [
"Domain match may fallback to generic → verify PRD.domain",
"EXTRACTION_FAILED if input too vague → need detailed input",
],
};
}
/**
* Build Plan per AGENTS.md §2.
*
* @param {string} input
* @returns {object} Plan + initial heartbeat
*/
function buildPlan(input) {
const subtasks = [
"T1: SF-01 Product Strategy — parse input, match domain, generate PRD",
"T2: SF-02 Architecture — generate arch.json from PRD",
"T3: SF-03~06 Full-stack — compose NestJS + React project",
"T4: Write output files and finalize",
];
const heartbeat = {
ts: new Date().toISOString(),
status: "IN_PROGRESS",
completed: [],
remaining: ["T1", "T2", "T3", "T4"],
risk: "",
next_action: "SF-01: generate PRD",
};
return {
goal: `Generate a full-stack web application from: "${input.slice(0, 100)}"`,
subtasks,
risks: [
"Input may be too vague → EXTRACTION_FAILED",
"Domain template may not fully match → generic fallback",
],
verificationStrategy: {
build: true,
tests: true,
lint: false,
},
heartbeat,
};
}
// ── Governance Logging ─────────────────────────────
/**
* Append a heartbeat entry to progress.log.
*
* Format: JSON Lines (GOVERNANCE.md §2.4)
*
* @param {object} entry
*/
function writeHeartbeat(entry) {
try {
const d = dirname(PROGRESS_LOG_PATH);
mkdirSync(d, { recursive: true });
appendFileSync(PROGRESS_LOG_PATH, JSON.stringify(entry) + "\n");
} catch (e) {
console.error(`[orchestrator] heartbeat write failed: ${e.message}`);
}
}
// ── CLI Bootstrap ─────────────────────────────────
if (process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]))) {
main();
}
+372
View File
@@ -0,0 +1,372 @@
#!/bin/bash
# ====================================================
# PR-D Production Check — OpenClaw Agent OS v2
# ====================================================
# One-shot safety gate: env → baseline → guard →
# deprecation → smoke → report.
# Every architecture PR must pass this gate first.
# ====================================================
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
cd "$ROOT_DIR"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
START_TIME=$(date +%s)
# ─── Result state ───────────────────────────────────
ENV_PASS=false
BASELINE_PASS=false
GUARD_PASS=false
DEPRECATION_STATUS="PASS"
SMOKE_PASS=false
OVERALL_PASS=true
FAILED_STEP=""
FAIL_REASON=""
FAIL_FIX=""
BASELINE_COUNT=0
BASELINE_DURATION=0
BASELINE_LOG=""
DEPRECATION_COUNT=0
# ─── Helpers ────────────────────────────────────────
fail_step() {
local step="$1" reason="$2" fix="$3"
FAILED_STEP="$step"
FAIL_REASON="$reason"
FAIL_FIX="$fix"
OVERALL_PASS=false
}
section() {
echo ""
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
echo -e "${CYAN} $1${NC}"
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
echo ""
}
# ─────────────────────────────────────────────────────
# Step 1: Environment Check
# ─────────────────────────────────────────────────────
section "Step 1: Environment Check"
check_ok() { echo -e " ${GREEN}${NC} $1"; }
check_fail() { echo -e " ${RED}${NC} $1"; fail_step "Environment" "$1" "$2"; }
# Node.js version
NODE_VERSION=$(node --version 2>/dev/null || echo "")
if [ -z "$NODE_VERSION" ]; then
check_fail "Node.js not found" "Install Node.js >= 18"
else
NODE_MAJOR=$(echo "$NODE_VERSION" | sed 's/v//' | cut -d. -f1)
if [ "$NODE_MAJOR" -lt 18 ]; then
check_fail "Node.js $NODE_VERSION too old (need >=18)" "Upgrade Node.js: nvm install 18"
else
check_ok "Node.js $NODE_VERSION"
fi
fi
# npm
if command -v npm &>/dev/null; then
NPM_VERSION=$(npm --version 2>/dev/null)
check_ok "npm $NPM_VERSION"
else
check_fail "npm not found" "Install npm (bundled with Node.js)"
fi
# openclaw CLI
if command -v openclaw &>/dev/null; then
OC_VERSION=$(openclaw --version 2>/dev/null || echo "unknown")
check_ok "openclaw CLI ($OC_VERSION)"
else
check_fail "openclaw CLI not found" "Install openclaw: npm i -g openclaw"
fi
# Working directory
if [ -f "package.json" ]; then
check_ok "Working directory: $ROOT_DIR"
else
check_fail "Not in workspace root (no package.json)" "cd to workspace root"
fi
# Required directories
for dir in "test/baseline" "test/architecture" "scripts"; do
if [ -d "$dir" ]; then
check_ok "Directory exists: $dir"
else
check_fail "Directory missing: $dir" "Create $dir before running"
fi
done
if $OVERALL_PASS; then
ENV_PASS=true
echo ""
echo -e " ${GREEN}Environment: PASS${NC}"
else
ENV_PASS=false
echo ""
echo -e " ${RED}Environment: FAIL${NC}"
fi
# ─────────────────────────────────────────────────────
# Step 2: Baseline Tests
# ─────────────────────────────────────────────────────
section "Step 2: Baseline Tests"
BASELINE_START=$(date +%s)
# Run baseline tests directly (not via buggy test-baseline.sh which has wrong TEST_DIR)
BASELINE_LOG=$(mktemp)
BASELINE_FILES=$(ls test/baseline/test-*.test.mjs 2>/dev/null || echo "")
BASELINE_TOTAL=$(echo "$BASELINE_FILES" | wc -w | tr -d ' ')
if [ -z "$BASELINE_FILES" ] || [ "$BASELINE_TOTAL" -eq 0 ]; then
echo -e " ${RED}${NC} No baseline test files found"
fail_step "Baseline Tests" "No test files in test/baseline/" "Ensure PR-A baseline tests exist"
else
echo " Running $BASELINE_TOTAL baseline test files..."
echo ""
PASSED=0
FAILED=0
for f in $BASELINE_FILES; do
name=$(basename "$f" .test.mjs)
echo -ne " [RUN ] $name ... "
if node --test "$f" >> "$BASELINE_LOG" 2>&1; then
echo -e "${GREEN}PASS${NC}"
PASSED=$((PASSED + 1))
else
echo -e "${RED}FAIL${NC}"
FAILED=$((FAILED + 1))
fi
done
echo ""
echo " ──────────────────────────────────────────"
echo " Test files: $BASELINE_TOTAL"
echo " Passed: $PASSED"
echo " Failed: $FAILED"
echo " Log: $BASELINE_LOG"
if [ "$FAILED" -gt 0 ]; then
fail_step "Baseline Tests" "$FAILED/$BASELINE_TOTAL tests failed" "Check $BASELINE_LOG for details"
BASELINE_PASS=false
else
BASELINE_PASS=true
fi
fi
BASELINE_END=$(date +%s)
BASELINE_DURATION=$((BASELINE_END - BASELINE_START))
BASELINE_COUNT=$BASELINE_TOTAL
echo ""
echo -e " Duration: ${BASELINE_DURATION}s"
if $BASELINE_PASS; then
echo -e " ${GREEN}Baseline Tests: PASS${NC}"
else
echo -e " ${RED}Baseline Tests: FAIL${NC}"
fi
# ─────────────────────────────────────────────────────
# Step 3: Architecture Guards
# ─────────────────────────────────────────────────────
section "Step 3: Architecture Guards"
GUARD_SCRIPT="$SCRIPT_DIR/guard-all.sh"
if [ ! -f "$GUARD_SCRIPT" ]; then
fail_step "Architecture Guard" "guard-all.sh not found at $GUARD_SCRIPT" "Ensure PR-B guard scripts exist"
GUARD_PASS=false
else
echo " Running: bash $GUARD_SCRIPT"
echo ""
# Run guard-all but capture exit code — we need to differentiate HARD vs SOFT
# guard-all.sh exits 1 if any hard guard fails
GUARD_OUTPUT=$(bash "$GUARD_SCRIPT" 2>&1) || GUARD_EXIT=$?
echo "$GUARD_OUTPUT"
# Parse guard-all output for individual guard statuses
GUARD_TOTAL=6
GUARD_PASSED=0
GUARD_FAILED=0
GUARD_WARNED=0
while IFS='|' read -r name result; do
case "$result" in
PASS) GUARD_PASSED=$((GUARD_PASSED + 1)) ;;
FAIL) GUARD_FAILED=$((GUARD_FAILED + 1)) ;;
WARN) GUARD_WARNED=$((GUARD_WARNED + 1)) ;;
esac
done < <(echo "$GUARD_OUTPUT" | grep -E '^\S+\|(PASS|FAIL|WARN)$' || true)
# HARD guard failure = production check failure
# SOFT guard warning ≠ production check failure
if [ "${GUARD_EXIT:-0}" -ne 0 ]; then
fail_step "Architecture Guard" "$GUARD_FAILED HARD guard(s) failed" "Review guard output above and fix violations"
GUARD_PASS=false
else
GUARD_PASS=true
fi
echo ""
echo " Guard status: $GUARD_PASSED pass | $GUARD_FAILED fail | $GUARD_WARNED warn"
if $GUARD_PASS; then
echo -e " ${GREEN}Architecture Guard: PASS${NC}"
else
echo -e " ${RED}Architecture Guard: FAIL${NC}"
fi
fi
# ─────────────────────────────────────────────────────
# Step 4: Deprecation Check
# ─────────────────────────────────────────────────────
section "Step 4: Deprecation Check"
DEPRECATION_SCRIPT="$SCRIPT_DIR/check-deprecations.mjs"
if [ ! -f "$DEPRECATION_SCRIPT" ]; then
DEPRECATION_STATUS="FAIL"
fail_step "Deprecation Check" "check-deprecations.mjs not found" "Ensure PR-C deprecation check script exists"
else
echo " Running: node $DEPRECATION_SCRIPT"
echo ""
# Deprecation check never fails on deprecation count, only on script crash
DEPRECATION_OUTPUT=$(node "$DEPRECATION_SCRIPT" 2>&1) || DEP_EXIT=$?
echo "$DEPRECATION_OUTPUT"
# Extract deprecation count and list
DEPRECATION_COUNT=$(echo "$DEPRECATION_OUTPUT" | grep "Total warnings:" | grep -oE '[0-9]+' || echo "0")
if [ "${DEP_EXIT:-0}" -ne 0 ]; then
DEPRECATION_STATUS="FAIL"
fail_step "Deprecation Check" "Script crashed (exit $DEP_EXIT)" "Check $DEPRECATION_SCRIPT for errors"
elif [ "$DEPRECATION_COUNT" -gt 0 ]; then
DEPRECATION_STATUS="WARN"
else
DEPRECATION_STATUS="PASS"
fi
echo ""
echo " Deprecation items: $DEPRECATION_COUNT"
# Print deprecation list if any
DEP_LIST=$(echo "$DEPRECATION_OUTPUT" | sed -n '/Deprecated modules:/,/^$/p' || true)
if [ -n "$DEP_LIST" ]; then
echo "$DEP_LIST"
fi
case "$DEPRECATION_STATUS" in
PASS) echo -e " ${GREEN}Deprecation Check: PASS${NC}" ;;
WARN) echo -e " ${YELLOW}Deprecation Check: WARN${NC}" ;;
FAIL) echo -e " ${RED}Deprecation Check: FAIL${NC}" ;;
esac
fi
# ─────────────────────────────────────────────────────
# Step 5: Smoke Test
# ─────────────────────────────────────────────────────
section "Step 5: Smoke Test"
SMOKE_SCRIPT="$SCRIPT_DIR/smoke-test.mjs"
if [ ! -f "$SMOKE_SCRIPT" ]; then
fail_step "Smoke Test" "smoke-test.mjs not found at $SMOKE_SCRIPT" "Create scripts/smoke-test.mjs"
SMOKE_PASS=false
else
echo " Running: node $SMOKE_SCRIPT"
echo ""
SMOKE_OUTPUT=$(node "$SMOKE_SCRIPT" 2>&1) || SMOKE_EXIT=$?
echo "$SMOKE_OUTPUT"
if [ "${SMOKE_EXIT:-0}" -eq 0 ]; then
SMOKE_PASS=true
echo -e " ${GREEN}Smoke Test: PASS${NC}"
else
SMOKE_PASS=false
fail_step "Smoke Test" "Smoke test failed (exit $SMOKE_EXIT)" "Check smoke test output above"
echo -e " ${RED}Smoke Test: FAIL${NC}"
fi
fi
# ─────────────────────────────────────────────────────
# Step 6: Summary Report
# ─────────────────────────────────────────────────────
END_TIME=$(date +%s)
TOTAL_DURATION=$((END_TIME - START_TIME))
section "Step 6: Summary Report"
echo ""
echo -e " ${BOLD}OpenClaw Production Check Summary${NC}"
echo -e " ────────────────────────────────"
# Print each step status
print_status() {
local label="$1" status="$2"
case "$status" in
PASS) echo -e " ${label}: ${GREEN}PASS${NC}" ;;
FAIL) echo -e " ${label}: ${RED}FAIL${NC}" ;;
WARN) echo -e " ${label}: ${YELLOW}WARN${NC}" ;;
*) echo -e " ${label}: $status" ;;
esac
}
ENV_STR=$($ENV_PASS && echo "PASS" || echo "FAIL")
BASELINE_STR=$($BASELINE_PASS && echo "PASS" || echo "FAIL")
GUARD_STR=$($GUARD_PASS && echo "PASS" || echo "FAIL")
print_status "Environment" "$ENV_STR"
print_status "Baseline Tests" "$BASELINE_STR"
print_status "Architecture Guard" "$GUARD_STR"
print_status "Deprecation Check" "$DEPRECATION_STATUS"
print_status "Smoke Test" "$($SMOKE_PASS && echo "PASS" || echo "FAIL")"
echo ""
OVERALL_STR=$($OVERALL_PASS && echo "PASS" || echo "FAIL")
echo -e " ${BOLD}Overall:${NC} $($OVERALL_PASS && echo -e "${GREEN}PASS${NC}" || echo -e "${RED}FAIL${NC}")"
echo -e " ${BOLD}Duration:${NC} ${TOTAL_DURATION}s"
# Failure details
if ! $OVERALL_PASS; then
echo ""
echo -e " ${RED}${BOLD}Failed Step:${NC} ${FAILED_STEP}"
echo -e " ${RED}${BOLD}Reason:${NC} ${FAIL_REASON}"
echo -e " ${YELLOW}${BOLD}Suggested Fix:${NC} ${FAIL_FIX}"
fi
echo ""
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
if $OVERALL_PASS; then
echo -e "${GREEN} ✓ PRODUCTION CHECK PASSED — Safe to proceed${NC}"
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
echo ""
exit 0
else
echo -e "${RED} ✗ PRODUCTION CHECK FAILED — Fix before proceeding${NC}"
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
echo ""
exit 1
fi
File diff suppressed because it is too large Load Diff
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env node
/**
* Regression Gate — SF-08
*
* 执行所有验证 gate,全部通过才允许发布。
* Exit 0 = PASS, Exit 1 = FAIL.
*
* Usage:
* node scripts/regression-gate.mjs [--strict]
*
* Gates:
* 1. 10 Domain E2E Benchmark
* 2. 170 Regression Tests
* 3. 5 Domain Semantic Spot-Check
*
* @module regression-gate
*/
import { execSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(resolve(fileURLToPath(import.meta.url)));
const WORKSPACE = resolve(__dirname, "..");
const STRICT = process.argv.includes("--strict");
// ═══════════════════════════════════════════════════════
// Gate Definitions
// ═══════════════════════════════════════════════════════
const SEMANTIC_SPOTCHECK = [
{ name: "BookShelf", input: "做一个图书管理系统,支持书籍库、阅读状态、阅读进度、书评和统计", expectTables: ["books", "reading_status"] },
{ name: "TeamFlow", input: "做一个团队协作工具,支持看板视图、任务管理、团队成员和活动日志", expectTables: ["boards", "tasks"] },
{ name: "Contract", input: "做一个合同管理工具,支持合同创建、审批流程、到期提醒和合同归档", expectTables: ["contracts", "approvals"] },
{ name: "Inspection", input: "做一个设备巡检系统,支持巡检计划、巡检记录、故障上报和设备台账", expectTables: ["equipment", "faults"] },
{ name: "Warehouse", input: "做一个仓库出入库工具,支持入库登记、出库审批、库存盘点和库存预警", expectTables: ["stock_in", "stock_out"] },
];
// ═══════════════════════════════════════════════════════
// Helpers
// ═══════════════════════════════════════════════════════
function run(cmd, label) {
const start = Date.now();
try {
const out = execSync(cmd, { cwd: WORKSPACE, encoding: "utf8", stdio: "pipe", timeout: 180_000 });
const ms = Date.now() - start;
return { pass: true, ms, output: out };
} catch (e) {
const ms = Date.now() - start;
return { pass: false, ms, output: e.stderr || e.stdout || e.message };
}
}
function gateHeader(n, label) {
console.log(`\n${"═".repeat(60)}`);
console.log(` GATE ${n}: ${label}`);
console.log(`${"═".repeat(60)}`);
}
// ═══════════════════════════════════════════════════════
// Gate 1: 10 Domain E2E Benchmark
// ═══════════════════════════════════════════════════════
function gateBenchmark() {
gateHeader(1, "10 Domain E2E Benchmark");
const r = run("node scripts/e2e-benchmark-v1.mjs", "benchmark");
const passMatch = r.output.match(/(\d+)\/(\d+) PASS/);
const passCount = passMatch ? parseInt(passMatch[1]) : 0;
const total = passMatch ? parseInt(passMatch[2]) : 0;
const pass = passCount === 10 && total === 10;
console.log(` Result: ${passCount}/${total} PASS (${r.ms}ms)`);
console.log(` Status: ${pass ? "✅ PASS" : "❌ FAIL"}`);
return pass;
}
// ═══════════════════════════════════════════════════════
// Gate 2: 170 Regression Tests
// ═══════════════════════════════════════════════════════
function gateRegression() {
gateHeader(2, "170 Regression Tests");
const r = run("node --test test/e2e-regression.test.mjs", "regression");
const passMatch = r.output.match(/pass (\d+)/);
const failMatch = r.output.match(/fail (\d+)/);
const passCount = passMatch ? parseInt(passMatch[1]) : 0;
const failCount = failMatch ? parseInt(failMatch[1]) : 0;
const pass = passCount === 170 && failCount === 0;
console.log(` Result: ${passCount} pass, ${failCount} fail (${r.ms}ms)`);
console.log(` Status: ${pass ? "✅ PASS" : "❌ FAIL"}`);
return pass;
}
// ═══════════════════════════════════════════════════════
// Gate 3: Semantic Spot-Check
// ═══════════════════════════════════════════════════════
function gateSemantic() {
gateHeader(3, "5 Domain Semantic Spot-Check");
let allPass = true;
for (const tc of SEMANTIC_SPOTCHECK) {
const tmpDir = `/tmp/gate-${tc.name.toLowerCase()}`;
try {
execSync(`node scripts/project-intake-agent.mjs --input "${tc.input}" --output ${tmpDir}/prd.json`, { cwd: WORKSPACE, encoding: "utf8", stdio: "pipe" });
execSync(`node scripts/architecture-agent.mjs --input ${tmpDir}/prd.json --output ${tmpDir}/arch.json`, { cwd: WORKSPACE, encoding: "utf8", stdio: "pipe" });
const arch = JSON.parse(readFileSync(`${tmpDir}/arch.json`, "utf8"));
const tables = arch.databaseSchema.map(t => t.table);
const hasChinese = tables.some(t => /[\u4e00-\u9fff]/.test(t));
const hasExpected = tc.expectTables.every(et => tables.includes(et));
const hasDomainFields = arch.databaseSchema.filter(t => t.table !== "users").some(t => {
const fields = t.fields.filter(f => !["id", "user_id", "created_at", "updated_at"].includes(f.name));
return fields.some(f => !["title", "description", "status", "data"].includes(f.name));
});
const pass = !hasChinese && hasExpected && hasDomainFields;
console.log(` ${pass ? "✅" : "❌"} ${tc.name}: tables=[${tables.join(", ")}]${hasChinese ? " [CHINESE!]" : ""}${!hasExpected ? " [MISSING TABLES!]" : ""}${!hasDomainFields ? " [GENERIC FIELDS!]" : ""}`);
if (!pass) allPass = false;
} catch (e) {
console.log(`${tc.name}: ${e.message.split("\n")[0]}`);
allPass = false;
}
}
console.log(`\n Status: ${allPass ? "✅ PASS" : "❌ FAIL"}`);
return allPass;
}
// ═══════════════════════════════════════════════════════
// Main
// ═══════════════════════════════════════════════════════
async function main() {
console.log("🔒 Regression Gate — v1.1.0-semantic");
console.log(` Mode: ${STRICT ? "STRICT" : "NORMAL"}`);
console.log(` Time: ${new Date().toISOString()}`);
const results = {
benchmark: gateBenchmark(),
regression: gateRegression(),
semantic: gateSemantic(),
};
console.log(`\n${"═".repeat(60)}`);
console.log(" SUMMARY");
console.log(`${"═".repeat(60)}`);
for (const [name, pass] of Object.entries(results)) {
console.log(` ${pass ? "✅" : "❌"} ${name}`);
}
const allPass = Object.values(results).every(Boolean);
console.log(`\n ${allPass ? "🟢 ALL GATES PASS — Release approved" : "🔴 GATE FAILED — Release blocked"}`);
console.log("");
process.exit(allPass ? 0 : 1);
}
main().catch(e => {
console.error("Gate runner crashed:", e);
process.exit(1);
});
+394
View File
@@ -0,0 +1,394 @@
#!/usr/bin/env node
/**
* Release Benchmark — SF-07
*
* 验证完整链路:Frontend → Backend → Fullstack → Electron → Release
* 对 PetCare / CRM / Inventory 三个领域执行,输出 release-benchmark-report.md
*/
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
import { resolve, dirname, join } from "node:path";
import { execSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const __dirname = dirname(resolve(fileURLToPath(import.meta.url)));
const WORKSPACE = resolve(__dirname, "..");
const BENCH_ROOT = resolve(WORKSPACE, ".benchmark");
const SCRIPTS = resolve(WORKSPACE, "scripts");
const REPORT_PATH = resolve(WORKSPACE, "release-benchmark-report.md");
const DOMAINS = [
{ id: "petcare", input: "做一个宠物护理管理平台,宠物主人可以管理宠物档案、健康日程、日常记录和成长相册" },
{ id: "crm", input: "做一个客户关系管理系统,支持客户管理、销售漏斗、跟进记录和数据分析仪表盘" },
{ id: "inventory", input: "做一个库存管理系统,支持商品入库出库、库存盘点、供应商管理和库存预警" },
];
// ═══════════════════════════════════════════════════════
// Helpers
// ═══════════════════════════════════════════════════════
function cmd(c, opts = {}) {
try {
return execSync(c, { cwd: WORKSPACE, encoding: "utf8", ...opts, stdio: opts.stdio ?? "pipe" });
} catch (e) {
return { error: e.message, stderr: e.stderr?.toString() || "", stdout: e.stdout?.toString() || "" };
}
}
function runAgent(agentPath, args) {
const result = cmd(`node ${agentPath} ${args}`);
if (result.error) {
try { return JSON.parse(result.stdout?.trim() || "{}"); } catch { return { error: result.error, stderr: result.stderr }; }
}
try { return JSON.parse(result.trim()); } catch { return { raw: result }; }
}
function countFiles(dir) {
try {
const r = execSync(`find ${dir} -type f 2>/dev/null | wc -l`, { encoding: "utf8", cwd: WORKSPACE });
return parseInt(r.trim(), 10);
} catch { return 0; }
}
function loadJSON(path) {
try { return JSON.parse(readFileSync(path, "utf8")); } catch { return {}; }
}
// ═══════════════════════════════════════════════════════
// Step 1-4: Full Pipeline (reuse existing agents)
// ═══════════════════════════════════════════════════════
function generateRequirementPackage(domain) {
console.log(`\n📋 [${domain.id}] Generating RequirementPackage...`);
const t0 = Date.now();
const prdOut = join(BENCH_ROOT, domain.id, "prd.json");
const archOut = join(BENCH_ROOT, domain.id, "arch.json");
mkdirSync(dirname(prdOut), { recursive: true });
runAgent(join(SCRIPTS, "project-intake-agent.mjs"), `--input "${domain.input}" --output "${prdOut}"`);
runAgent(join(SCRIPTS, "architecture-agent.mjs"), `--input "${prdOut}" --output "${archOut}"`);
const prd = loadJSON(prdOut);
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
console.log(` ✅ PRD + Architecture (${elapsed}s) — ${prd.projectName || "?"}`);
return { prd, prdPath: prdOut, archPath: archOut, elapsed: parseFloat(elapsed) };
}
function generateFrontend(domain, prdPath, archPath) {
console.log(` 🎨 Generating Frontend...`);
const t0 = Date.now();
const outDir = join(BENCH_ROOT, domain.id, "frontend");
runAgent(join(SCRIPTS, "frontend-builder-agent.mjs"), `--prd "${prdPath}" --arch "${archPath}" --output "${outDir}"`);
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
const fc = countFiles(outDir);
console.log(` ✅ Frontend (${elapsed}s, ${fc} files)`);
return { elapsed: parseFloat(elapsed), fileCount: fc };
}
function generateBackend(domain, prdPath, archPath) {
console.log(` ⚙️ Generating Backend...`);
const t0 = Date.now();
const outDir = join(BENCH_ROOT, domain.id, "backend");
runAgent(join(SCRIPTS, "backend-builder-agent.mjs"), `--prd "${prdPath}" --arch "${archPath}" --output "${outDir}"`);
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
const fc = countFiles(outDir);
console.log(` ✅ Backend (${elapsed}s, ${fc} files)`);
return { elapsed: parseFloat(elapsed), fileCount: fc };
}
function generateFullstack(domain, prdPath, archPath) {
console.log(` 🔗 Composing Fullstack...`);
const t0 = Date.now();
const outDir = join(BENCH_ROOT, domain.id, "fullstack");
runAgent(join(SCRIPTS, "fullstack-composer-agent.mjs"), `--prd "${prdPath}" --arch "${archPath}" --output "${outDir}"`);
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
const fc = countFiles(outDir);
console.log(` ✅ Fullstack (${elapsed}s, ${fc} files)`);
return { elapsed: parseFloat(elapsed), fileCount: fc };
}
function generateElectron(domain, prdPath) {
console.log(` 🖥️ Generating Electron...`);
const t0 = Date.now();
const fullstackDir = join(BENCH_ROOT, domain.id, "fullstack");
const outDir = join(BENCH_ROOT, domain.id, "electron");
runAgent(join(SCRIPTS, "electron-builder-agent.mjs"), `--input "${fullstackDir}" --output "${outDir}" --prd "${prdPath}"`);
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
const fc = countFiles(outDir);
console.log(` ✅ Electron (${elapsed}s, ${fc} files)`);
return { elapsed: parseFloat(elapsed), fileCount: fc };
}
// ═══════════════════════════════════════════════════════
// Step 5: Release Builder
// ═══════════════════════════════════════════════════════
function generateRelease(domain) {
console.log(` 📦 Generating Release...`);
const t0 = Date.now();
const fullstackDir = join(BENCH_ROOT, domain.id, "fullstack");
const outDir = join(BENCH_ROOT, domain.id, "release");
const result = runAgent(join(SCRIPTS, "release-builder-agent.mjs"), `--input "${fullstackDir}" --output "${outDir}"`);
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
const fc = countFiles(outDir);
console.log(` ✅ Release (${elapsed}s, ${fc} files)`);
return { elapsed: parseFloat(elapsed), fileCount: fc, result };
}
// ═══════════════════════════════════════════════════════
// Validation
// ═══════════════════════════════════════════════════════
function validateRelease(domain) {
const releaseDir = join(BENCH_ROOT, domain.id, "release");
const checks = {
hasVersionJson: existsSync(join(releaseDir, "release", "version.json")),
hasManifestJson: existsSync(join(releaseDir, "release", "manifests", "manifest.json")),
hasChecksums: existsSync(join(releaseDir, "release", "checksums", "checksums.txt")),
hasReleaseNotes: existsSync(join(releaseDir, "release", "release-notes", "release-notes.md")),
hasBuildInfo: existsSync(join(releaseDir, "release", "build-info.json")),
hasReadme: existsSync(join(releaseDir, "release", "README.md")),
hasWindowsDir: existsSync(join(releaseDir, "release", "windows")),
hasMacosDir: existsSync(join(releaseDir, "release", "macos")),
hasLinuxDir: existsSync(join(releaseDir, "release", "linux")),
};
// Validate version.json
if (checks.hasVersionJson) {
try {
const v = JSON.parse(readFileSync(join(releaseDir, "release", "version.json"), "utf8"));
checks.versionValid = !!(v.name && v.version && v.platforms);
} catch { checks.versionValid = false; }
}
// Validate manifest
if (checks.hasManifestJson) {
try {
const m = JSON.parse(readFileSync(join(releaseDir, "release", "manifests", "manifest.json"), "utf8"));
checks.manifestValid = !!(m.project && m.version && m.files && m.files.length > 0);
checks.manifestFileCount = m.files?.length || 0;
} catch { checks.manifestValid = false; }
}
// Validate checksums
if (checks.hasChecksums) {
const content = readFileSync(join(releaseDir, "release", "checksums", "checksums.txt"), "utf8");
const lines = content.trim().split("\n").filter(l => l.length > 0);
checks.checksumCount = lines.length;
checks.checksumsValid = lines.every(l => {
const parts = l.split(/\s+/);
return parts[0]?.length === 64 && parts[1]?.length > 0;
});
}
// Validate release notes
if (checks.hasReleaseNotes) {
const notes = readFileSync(join(releaseDir, "release", "release-notes", "release-notes.md"), "utf8");
checks.releaseNotesValid = notes.includes("Version") && notes.includes("Features") && notes.includes("Installation");
}
// Validate build-info
if (checks.hasBuildInfo) {
try {
const bi = JSON.parse(readFileSync(join(releaseDir, "release", "build-info.json"), "utf8"));
checks.buildInfoValid = !!(bi.nodeVersion && bi.generatorVersion && bi.buildTime);
} catch { checks.buildInfoValid = false; }
}
// Platform placeholder counts
if (checks.hasWindowsDir) {
try { checks.windowsFileCount = readdirSync(join(releaseDir, "release", "windows")).length; } catch { checks.windowsFileCount = 0; }
}
if (checks.hasMacosDir) {
try { checks.macosFileCount = readdirSync(join(releaseDir, "release", "macos")).length; } catch { checks.macosFileCount = 0; }
}
if (checks.hasLinuxDir) {
try { checks.linuxFileCount = readdirSync(join(releaseDir, "release", "linux")).length; } catch { checks.linuxFileCount = 0; }
}
checks.allPassed = checks.hasVersionJson && checks.hasManifestJson && checks.hasChecksums &&
checks.hasReleaseNotes && checks.hasBuildInfo && checks.hasWindowsDir &&
checks.hasMacosDir && checks.hasLinuxDir && checks.versionValid &&
checks.manifestValid && checks.checksumsValid && checks.releaseNotesValid && checks.buildInfoValid;
return checks;
}
// ═══════════════════════════════════════════════════════
// Report
// ═══════════════════════════════════════════════════════
function generateReport(allResults) {
const lines = [];
const h = (t) => { lines.push(t); return lines; };
h(`# Release Benchmark Report — SF-07`);
h(``);
h(`> Generated: ${new Date().toISOString()}`);
h(`> Pipeline: Frontend → Backend → Fullstack → Electron → Release`);
h(``);
// ── Domain Results ──
h(`## Domain Results`);
h(``);
h(`| Domain | Release | Manifest | Notes | Result |`);
h(`|--------|---------|----------|-------|--------|`);
for (const r of allResults) {
const rel = r.releaseChecks?.allPassed ? "✅" : "❌";
const man = r.releaseChecks?.manifestValid ? "✅" : "❌";
const notes = r.releaseChecks?.releaseNotesValid ? "✅" : "❌";
const result = r.releaseChecks?.allPassed ? "PASS" : "FAIL";
h(`| ${r.id} | ${rel} | ${man} | ${notes} | **${result}** |`);
}
h(``);
// ── Release Artifacts ──
h(`## Release Artifacts`);
h(``);
h(`| Domain | Files | Manifest Files | Checksum Lines | Release Notes |`);
h(`|--------|-------|----------------|----------------|---------------|`);
for (const r of allResults) {
const rc = r.releaseChecks || {};
h(`| ${r.id} | ${r.release?.fileCount || 0} | ${rc.manifestFileCount || 0} | ${rc.checksumCount || 0} | ${rc.hasReleaseNotes ? "✅" : "❌"} |`);
}
h(``);
// ── Pipeline Timing ──
h(`## Pipeline Timing`);
h(``);
h(`| Domain | Frontend | Backend | Fullstack | Electron | Release | Total |`);
h(`|--------|----------|---------|-----------|----------|---------|-------|`);
for (const r of allResults) {
h(`| ${r.id} | ${r.frontend?.elapsed || 0}s | ${r.backend?.elapsed || 0}s | ${r.fullstack?.elapsed || 0}s | ${r.electron?.elapsed || 0}s | ${r.release?.elapsed || 0}s | ${r.totalTime || 0}s |`);
}
h(``);
// ── Validation Details ──
h(`## Validation Details`);
h(``);
for (const r of allResults) {
h(`### ${r.id}`);
h(``);
const rc = r.releaseChecks || {};
h(`| Check | Status |`);
h(`|-------|--------|`);
h(`| version.json | ${rc.hasVersionJson ? "✅" : "❌"} |`);
h(`| version.json valid | ${rc.versionValid ? "✅" : "❌"} |`);
h(`| manifest.json | ${rc.hasManifestJson ? "✅" : "❌"} |`);
h(`| manifest valid | ${rc.manifestValid ? "✅" : "❌"} |`);
h(`| checksums.txt | ${rc.hasChecksums ? "✅" : "❌"} |`);
h(`| checksums valid | ${rc.checksumsValid ? "✅" : "❌"} |`);
h(`| release-notes.md | ${rc.hasReleaseNotes ? "✅" : "❌"} |`);
h(`| release notes valid | ${rc.releaseNotesValid ? "✅" : "❌"} |`);
h(`| build-info.json | ${rc.hasBuildInfo ? "✅" : "❌"} |`);
h(`| build-info valid | ${rc.buildInfoValid ? "✅" : "❌"} |`);
h(`| windows/ | ${rc.hasWindowsDir ? "✅" : "❌"} (${rc.windowsFileCount || 0} files) |`);
h(`| macos/ | ${rc.hasMacosDir ? "✅" : "❌"} (${rc.macosFileCount || 0} files) |`);
h(`| linux/ | ${rc.hasLinuxDir ? "✅" : "❌"} (${rc.linuxFileCount || 0} files) |`);
h(``);
}
// ── Final Status ──
h(`## Final Status`);
h(``);
const passCount = allResults.filter(r => r.releaseChecks?.allPassed).length;
const total = allResults.length;
h(`| Metric | Value |`);
h(`|--------|-------|`);
h(`| Domains Tested | ${total} |`);
h(`| Release PASS | ${passCount}/${total} |`);
h(`| Release FAIL | ${total - passCount}/${total} |`);
h(``);
if (passCount === total) {
h(`🎉 **Release Builder Agent — PASS** (${passCount}/${total})`);
h(``);
h(`All domains successfully generated complete release packages.`);
} else {
h(`⚠️ **Release Builder Agent — FAIL** (${passCount}/${total})`);
h(``);
const failed = allResults.filter(r => !r.releaseChecks?.allPassed);
h(`Failed domains: ${failed.map(r => r.id).join(", ")}`);
}
h(``);
h(`---`);
h(`*Report generated by Release Benchmark — SF-07*`);
return lines.join("\n");
}
// ═══════════════════════════════════════════════════════
// Main
// ═══════════════════════════════════════════════════════
async function main() {
console.log(`\n🧪 Release Benchmark — SF-07`);
console.log(` Testing ${DOMAINS.length} domains`);
console.log(` Pipeline: Frontend → Backend → Fullstack → Electron → Release\n`);
const allResults = [];
for (const domain of DOMAINS) {
console.log(`\n${"=".repeat(60)}`);
console.log(`🏗️ Domain: ${domain.id}`);
console.log(`${"=".repeat(60)}`);
const tTotal = Date.now();
// Steps 1-4: Generate full pipeline
const { prd, prdPath, archPath, elapsed: reqTime } = generateRequirementPackage(domain);
const frontend = generateFrontend(domain, prdPath, archPath);
const backend = generateBackend(domain, prdPath, archPath);
const fullstack = generateFullstack(domain, prdPath, archPath);
const electron = generateElectron(domain, prdPath);
// Step 5: Release
const release = generateRelease(domain);
// Validate
const releaseChecks = validateRelease(domain);
const totalTime = ((Date.now() - tTotal) / 1000).toFixed(1);
allResults.push({
id: domain.id,
input: domain.input,
projectName: prd.projectName,
reqTime,
frontend,
backend,
fullstack,
electron,
release,
releaseChecks,
totalTime: parseFloat(totalTime),
});
console.log(` ⏱️ Total: ${totalTime}s | Release: ${releaseChecks.allPassed ? "✅ PASS" : "❌ FAIL"}`);
}
// Generate report
const report = generateReport(allResults);
writeFileSync(REPORT_PATH, report, "utf8");
console.log(`\n📄 Report written to: ${REPORT_PATH}`);
// Summary
const passCount = allResults.filter(r => r.releaseChecks?.allPassed).length;
console.log(`\n📊 Summary: ${passCount}/${DOMAINS.length} domains PASS`);
console.log(` Release Builder: ${passCount === DOMAINS.length ? "✅ PASS" : "❌ FAIL"}`);
}
main().catch(e => {
console.error("Benchmark failed:", e);
process.exit(1);
});
+542
View File
@@ -0,0 +1,542 @@
#!/usr/bin/env node
/**
* Release Builder Agent — SF-07
*
* 将 SF-06 Electron Project 封装为可发布软件包结构。
*
* 职责:Electron Project → Release Package Structure
* 不做:自动更新、CI/CD、Docker、云发布、安装器、签名、证书
*
* Usage:
* node scripts/release-builder-agent.mjs --input <electron-dir> [options]
*
* @module release-builder-agent
*/
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
import { resolve, dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { createHash } from "node:crypto";
const __dirname = dirname(resolve(fileURLToPath(import.meta.url)));
const WORKSPACE = resolve(__dirname, "..");
const GENERATOR_VERSION = "1.0.0";
const PLATFORMS = ["windows", "macos", "linux"];
// ═══════════════════════════════════════════════════════
// 1. Metadata Extraction
// ═══════════════════════════════════════════════════════
function extractMetadataSync(projectDir) {
const meta = {
projectName: "app",
version: "1.0.0",
buildNumber: 1,
domain: "unknown",
entities: [],
routes: [],
screens: [],
};
// Read root package.json
const rootPkgPath = join(projectDir, "package.json");
if (existsSync(rootPkgPath)) {
try {
const pkg = JSON.parse(readFileSync(rootPkgPath, "utf8"));
meta.projectName = pkg.name || meta.projectName;
meta.version = pkg.version || meta.version;
} catch {}
}
// Read desktop package.json
const desktopPkgPath = join(projectDir, "apps", "desktop", "package.json");
if (existsSync(desktopPkgPath)) {
try {
const pkg = JSON.parse(readFileSync(desktopPkgPath, "utf8"));
if (pkg.name) meta.projectName = pkg.name.replace(/-desktop$/, "");
if (pkg.version) meta.version = pkg.version;
} catch {}
}
// Read PRD if exists
const prdPath = join(projectDir, "prd.json");
if (existsSync(prdPath)) {
try {
const prd = JSON.parse(readFileSync(prdPath, "utf8"));
if (prd.projectName) meta.projectName = prd.projectName;
if (prd.domain) meta.domain = prd.domain;
if (prd.features) meta.entities = prd.features.map(f => f.name || f);
if (prd.apiRequirements) meta.routes = prd.apiRequirements.map(r => `${r.method} ${r.path}`);
if (prd.pages) meta.screens = prd.pages.map(p => p.name || p);
} catch {}
}
// Scan for entities in backend routes
const routesDir = join(projectDir, "apps", "api", "src", "routes");
if (existsSync(routesDir)) {
try {
const files = readdirSync(routesDir).filter(f => f.endsWith(".ts") || f.endsWith(".js"));
if (meta.entities.length === 0) {
meta.entities = files.map(f => f.replace(/\.(ts|js)$/, "").replace(/-route$/, "").replace(/\.route$/, ""));
}
} catch {}
}
// Scan for screens in frontend
const appDir = join(projectDir, "apps", "web", "app");
if (existsSync(appDir)) {
try {
const entries = readdirSync(appDir, { withFileTypes: true });
if (meta.screens.length === 0) {
meta.screens = entries
.filter(e => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith("."))
.map(e => e.name);
}
} catch {}
}
// Count API routes from index.ts
const apiSrcDir = join(projectDir, "apps", "api", "src");
if (existsSync(apiSrcDir) && meta.routes.length === 0) {
try {
const indexContent = readFileSync(join(apiSrcDir, "index.ts"), "utf8");
const routeMatches = indexContent.match(/app\.(use|get|post|put|patch|delete)\s*\(\s*["']([^"']+)/g);
if (routeMatches) meta.routes = routeMatches;
} catch {}
}
return meta;
}
// ═══════════════════════════════════════════════════════
// 2. Version Generator
// ═══════════════════════════════════════════════════════
function generateVersion(meta) {
return JSON.stringify({
name: meta.projectName.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
version: meta.version,
buildNumber: meta.buildNumber,
platforms: [...PLATFORMS],
}, null, 2);
}
// ═══════════════════════════════════════════════════════
// 3. Release Manifest
// ═══════════════════════════════════════════════════════
function generateManifest(meta, artifacts) {
const files = [];
for (const art of artifacts) {
files.push({
path: art.path,
size: art.size,
sha256: art.sha256,
});
}
return JSON.stringify({
project: meta.projectName,
version: meta.version,
buildNumber: meta.buildNumber,
generatedAt: new Date().toISOString(),
generator: "release-builder-agent",
generatorVersion: GENERATOR_VERSION,
platforms: [...PLATFORMS],
files,
totalFiles: files.length,
totalSize: files.reduce((s, f) => s + f.size, 0),
}, null, 2);
}
// ═══════════════════════════════════════════════════════
// 4. Checksum Generator
// ═══════════════════════════════════════════════════════
function sha256(content) {
return createHash("sha256").update(typeof content === "string" ? content : content).digest("hex");
}
function generateChecksums(artifacts) {
return artifacts.map(a => `${a.sha256} ${a.path}`).join("\n") + "\n";
}
// ═══════════════════════════════════════════════════════
// 5. Release Notes
// ═══════════════════════════════════════════════════════
function generateReleaseNotes(meta) {
const now = new Date();
const dateStr = now.toISOString().split("T")[0];
const timeStr = now.toISOString().split("T")[1].split(".")[0];
const lines = [];
const h = (t) => { lines.push(t); return lines; };
h(`# ${meta.projectName} v${meta.version}`);
h(``);
h(`> Release Builder Agent — SF-07`);
h(`> Generated: ${dateStr} ${timeStr} UTC`);
h(``);
h(`## Version`);
h(``);
h(`- **Name:** ${meta.projectName}`);
h(`- **Version:** ${meta.version}`);
h(`- **Build:** #${meta.buildNumber}`);
h(``);
h(`## Features`);
h(``);
if (meta.entities.length > 0) {
for (const e of meta.entities) {
h(`- ${e}`);
}
} else {
h(`- (No features detected)`);
}
h(``);
h(`## Build Info`);
h(``);
h(`- **Build Time:** ${now.toISOString()}`);
h(`- **Generator:** release-builder-agent v${GENERATOR_VERSION}`);
h(`- **Node Version:** ${process.version}`);
h(``);
h(`## Platforms`);
h(``);
for (const p of PLATFORMS) {
h(`- ✅ ${p}`);
}
h(``);
h(`## Installation`);
h(``);
h(`### Windows`);
h(`\`\`\``);
h(`Download the .exe installer from release/windows/`);
h(`\`\`\``);
h(``);
h(`### macOS`);
h(`\`\`\``);
h(`Download the .dmg from release/macos/`);
h(`\`\`\``);
h(``);
h(`### Linux`);
h(`\`\`\``);
h(`Download the .AppImage from release/linux/`);
h(`\`\`\``);
h(``);
h(`## Checksums`);
h(``);
h(`See \`checksums/checksums.txt\` for SHA256 verification.`);
h(``);
h(`---`);
h(`*Generated by Release Builder Agent — SF-07*`);
return lines.join("\n");
}
// ═══════════════════════════════════════════════════════
// 6. Build Metadata
// ═══════════════════════════════════════════════════════
function generateBuildInfo(meta) {
return JSON.stringify({
nodeVersion: process.version,
generatorVersion: GENERATOR_VERSION,
buildTime: new Date().toISOString(),
domain: meta.domain,
entityCount: meta.entities.length,
routeCount: meta.routes.length,
screenCount: meta.screens.length,
platforms: [...PLATFORMS],
project: meta.projectName,
version: meta.version,
}, null, 2);
}
// ═══════════════════════════════════════════════════════
// 7. Platform Artifact Layout
// ═══════════════════════════════════════════════════════
function generatePlatformArtifacts(meta) {
const safeName = meta.projectName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
const artifacts = {};
// Windows
artifacts[`windows/${safeName}-setup-${meta.version}.exe`] =
`[PLACEHOLDER] ${meta.projectName} v${meta.version} Windows Installer\n` +
`This is a placeholder file for the Windows NSIS installer.\n` +
`Generated: ${new Date().toISOString()}\n`;
artifacts[`windows/${safeName}-${meta.version}-portable.exe`] =
`[PLACEHOLDER] ${meta.projectName} v${meta.version} Windows Portable\n` +
`This is a placeholder file for the Windows portable executable.\n` +
`Generated: ${new Date().toISOString()}\n`;
// macOS
artifacts[`macos/${safeName}-${meta.version}.dmg`] =
`[PLACEHOLDER] ${meta.projectName} v${meta.version} macOS Disk Image\n` +
`This is a placeholder file for the macOS DMG installer.\n` +
`Generated: ${new Date().toISOString()}\n`;
artifacts[`macos/${safeName}-${meta.version}-arm64.dmg`] =
`[PLACEHOLDER] ${meta.projectName} v${meta.version} macOS ARM64 Disk Image\n` +
`This is a placeholder file for the macOS ARM64 DMG.\n` +
`Generated: ${new Date().toISOString()}\n`;
// Linux
artifacts[`linux/${safeName}-${meta.version}.AppImage`] =
`[PLACEHOLDER] ${meta.projectName} v${meta.version} Linux AppImage\n` +
`This is a placeholder file for the Linux AppImage.\n` +
`Generated: ${new Date().toISOString()}\n`;
artifacts[`linux/${safeName}_${meta.version}_amd64.deb`] =
`[PLACEHOLDER] ${meta.projectName} v${meta.version} Linux Debian Package\n` +
`This is a placeholder file for the Linux .deb package.\n` +
`Generated: ${new Date().toISOString()}\n`;
return artifacts;
}
// ═══════════════════════════════════════════════════════
// 8. Main Build Function
// ═══════════════════════════════════════════════════════
/**
* Build release package structure for an Electron project.
*
* @param {object} options
* @param {string} options.projectDir — Electron project root (with apps/web, apps/api, apps/desktop)
* @param {string} [options.projectName] — Override project name
* @param {string} [options.version] — Override version
* @param {number} [options.buildNumber] — Override build number
* @returns {object} { files, stats, error, message }
*/
export function buildRelease(options = {}) {
const {
projectDir = null,
projectName = null,
version = null,
buildNumber = 1,
} = options;
// Extract metadata from project
const meta = projectDir ? extractMetadataSync(projectDir) : {
projectName: "app",
version: "1.0.0",
buildNumber: 1,
domain: "unknown",
entities: [],
routes: [],
screens: [],
};
// Apply overrides
if (projectName) meta.projectName = projectName;
if (version) meta.version = version;
meta.buildNumber = buildNumber;
const files = {};
// Generate platform artifacts (placeholders)
const platformArtifacts = generatePlatformArtifacts(meta);
// Add platform artifacts to files
for (const [path, content] of Object.entries(platformArtifacts)) {
files[`release/${path}`] = content;
}
// Compute artifact metadata for manifest
const artifactMeta = Object.entries(platformArtifacts).map(([path, content]) => ({
path,
size: Buffer.byteLength(content, "utf8"),
sha256: sha256(content),
}));
// Generate version.json
files["release/version.json"] = generateVersion(meta);
// Generate manifest.json
files["release/manifests/manifest.json"] = generateManifest(meta, artifactMeta);
// Generate checksums.txt
files["release/checksums/checksums.txt"] = generateChecksums(artifactMeta);
// Generate release-notes.md
files["release/release-notes/release-notes.md"] = generateReleaseNotes(meta);
// Generate build-info.json
files["release/build-info.json"] = generateBuildInfo(meta);
// Generate README
files["release/README.md"] = generateReadme(meta);
// Stats
const allPaths = Object.keys(files);
const stats = {
totalFiles: allPaths.length,
platformFiles: allPaths.filter(p => p.startsWith("release/windows/") || p.startsWith("release/macos/") || p.startsWith("release/linux/")).length,
manifestFiles: allPaths.filter(p => p.includes("manifest")).length,
checksumFiles: allPaths.filter(p => p.includes("checksum")).length,
releaseNotesFiles: allPaths.filter(p => p.includes("release-notes")).length,
metadataFiles: allPaths.filter(p => p.endsWith(".json")).length,
totalSize: Object.values(files).reduce((s, c) => s + Buffer.byteLength(c, "utf8"), 0),
};
return { files, stats, meta, error: null, message: null };
}
// ═══════════════════════════════════════════════════════
// 9. README Generator
// ═══════════════════════════════════════════════════════
function generateReadme(meta) {
const lines = [];
const h = (t) => { lines.push(t); return lines; };
h(`# ${meta.projectName} — Release Package`);
h(``);
h(`> Version: ${meta.version} | Build: #${meta.buildNumber}`);
h(`> Generated: ${new Date().toISOString()}`);
h(``);
h(`## Directory Structure`);
h(``);
h(`\`\`\``);
h(`release/`);
h(`├── version.json — Version metadata`);
h(`├── build-info.json — Build environment info`);
h(`├── README.md — This file`);
h(`├── manifests/`);
h(`│ └── manifest.json — Release manifest with file hashes`);
h(`├── checksums/`);
h(`│ └── checksums.txt — SHA256 checksums for verification`);
h(`├── release-notes/`);
h(`│ └── release-notes.md — Human-readable release notes`);
h(`├── windows/ — Windows installers`);
h(`├── macos/ — macOS disk images`);
h(`└── linux/ — Linux packages`);
h(`\`\`\``);
h(``);
h(`## Verification`);
h(``);
h(`\`\`\`bash`);
h(`# Verify checksums`);
h(`cd release/checksums`);
h(`shasum -a 256 -c checksums.txt`);
h(`\`\`\``);
h(``);
h(`---`);
h(`*Generated by Release Builder Agent — SF-07*`);
return lines.join("\n");
}
// ═══════════════════════════════════════════════════════
// 10. File I/O
// ═══════════════════════════════════════════════════════
export function writeRelease(result, outputDir) {
mkdirSync(outputDir, { recursive: true });
for (const [relPath, content] of Object.entries(result.files)) {
const fullPath = resolve(outputDir, relPath);
mkdirSync(dirname(fullPath), { recursive: true });
writeFileSync(fullPath, content, "utf-8");
}
}
// ═══════════════════════════════════════════════════════
// 11. CLI Entry
// ═══════════════════════════════════════════════════════
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
input: null,
output: null,
projectName: null,
version: null,
buildNumber: 1,
verbose: false,
help: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === "--input" && args[i + 1]) opts.input = args[++i];
else if (args[i] === "--output" && args[i + 1]) opts.output = args[++i];
else if (args[i] === "--name" && args[i + 1]) opts.projectName = args[++i];
else if (args[i] === "--version" && args[i + 1]) opts.version = args[++i];
else if (args[i] === "--build-number" && args[i + 1]) opts.buildNumber = parseInt(args[++i], 10);
else if (args[i] === "--verbose") opts.verbose = true;
else if (args[i] === "--help" || args[i] === "-h") opts.help = true;
}
return opts;
}
async function main() {
const opts = parseArgs();
if (opts.help) {
console.log(
"\nRelease Builder Agent — SF-07\n\n" +
"Generate release package structure for an Electron project.\n\n" +
"Usage:\n" +
" node scripts/release-builder-agent.mjs --input <electron-dir> [options]\n\n" +
"Options:\n" +
" --input <dir> Project directory (with apps/web, apps/api, apps/desktop)\n" +
" --output <dir> Output directory (default: <input>/release)\n" +
" --name <name> Override project name\n" +
" --version <ver> Override version\n" +
" --build-number <n> Build number (default: 1)\n" +
" --verbose Verbose output\n" +
" --help Show this help\n"
);
return;
}
const result = buildRelease({
projectDir: opts.input,
projectName: opts.projectName,
version: opts.version,
buildNumber: opts.buildNumber,
});
if (result.error) {
console.error(result.message);
process.exit(1);
}
const outputDir = opts.output
? resolve(opts.output)
: opts.input
? resolve(opts.input, "release")
: resolve(WORKSPACE, "release");
writeRelease(result, outputDir);
if (opts.verbose) {
console.error("Project: " + result.meta.projectName);
console.error("Version: " + result.meta.version);
console.error("Files: " + result.stats.totalFiles);
console.error("Platform files: " + result.stats.platformFiles);
console.error("Total size: " + result.stats.totalSize + " bytes");
}
console.log(JSON.stringify({
projectName: result.meta.projectName,
version: result.meta.version,
outputDir,
stats: result.stats,
}, null, 2));
}
if (process.argv[1] && (import.meta.url === "file://" + process.argv[1] || import.meta.url.endsWith(process.argv[1]))) {
main();
}
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env node
/**
* Agent Runtime CLI — PR-35
*
* Usage:
* echo '{"task":{...},"context":{...}}' | node scripts/runtime.mjs
*
* stdin:
* { task, context: { baselineData?, candidateData?, ... }, options? }
*
* Output: JSON { success, taskResult, report }
* Exit: 0=passed, 1=failed/blocked/error
*/
import { readFileSync } from "node:fs";
import { createTask, createExecutionPlan } from "../src/agent-runtime/domain.mjs";
import { executePipeline, recoverTask } from "../src/agent-runtime/pipeline.mjs";
import { createRuntimeContext, autoGateAdapter, createCertificationAdapter } from "../src/agent-runtime/adapters.mjs";
import { generateRuntimeReport } from "../src/agent-runtime/reporting.mjs";
function readStdin() {
try {
const raw = readFileSync(0, "utf8").trim();
return raw ? JSON.parse(raw) : null;
} catch (e) {
return { _error: `Parse error: ${e.message}` };
}
}
function main() {
const input = readStdin();
if (!input) { console.error("Error: No stdin input."); process.exit(1); }
if (input._error) { console.error(input._error); process.exit(1); }
const { task: rawTask, context: rawCtx = {}, options = {}, action = "execute" } = input;
if (!rawTask) { console.error("Error: 'task' field required."); process.exit(1); }
// 反序列化 Task
const task = createTask({
id: rawTask.id, title: rawTask.title || rawTask.id,
description: rawTask.description, tags: rawTask.tags,
dependsOn: rawTask.dependsOn, parentTaskId: rawTask.parentTaskId,
metadata: rawTask.metadata,
});
task.executionPlan = createExecutionPlan({
taskId: task.id,
steps: (rawTask.steps || []).map(s => ({
id: s.id, name: s.name || s.id, action: s.action || s.name,
dependsOn: s.dependsOn, expectedArtifacts: s.expectedArtifacts, metadata: s.metadata,
})),
strategy: rawTask.strategy,
});
task.maxRetries = rawTask.maxRetries ?? 3;
// 构建 Runtime Context
const rctx = createRuntimeContext({
checklistAdapter: rawCtx.checklist ? (ctx) => ({ status: rawCtx.checklist === "pass" ? "pass" : "fail", blockers: [], warnings: [], confidence: 100 }) : null,
baselineAdapter: rawCtx.baseline ? (_ctx, b, c) => ({ status: "pass", changes: [], blockers: [], warnings: [] }) : null,
gateAdapter: rawCtx.gate ? autoGateAdapter : null,
certificationAdapter: createCertificationAdapter(),
baselineData: rawCtx.baselineData || null,
candidateData: rawCtx.candidateData || null,
pipelineOptions: options,
});
try {
let taskResult;
if (action === "recover") {
task.state = "failed";
task.retryCount = rawTask.retryCount || 0;
taskResult = recoverTask(task, rctx, options);
} else {
taskResult = executePipeline(task, rctx, options);
}
const report = generateRuntimeReport(task, taskResult);
const output = { success: taskResult.status === "passed", taskResult, report };
console.log(JSON.stringify(output, null, 2));
process.exit(taskResult.status === "passed" ? 0 : 1);
} catch (e) {
console.error(`Runtime error: ${e.message}`);
process.exit(1);
}
}
main();
+269
View File
@@ -0,0 +1,269 @@
#!/usr/bin/env bash
# session-compact.sh — 会话压缩与快速存档 + Write Gate 前置检查
#
# v3 — 加入 pre-compact 钩子:上下文压缩前自动扫描未保存的纠正/决策/承诺
# v2 — 加入 Write Gate 自动路由:preference/correction/decision 触发寄存器写入
#
# 用法:
# bash scripts/session-compact.sh save "任何想记住的内容"
# bash scripts/session-compact.sh save --type preference "偏好内容"
# bash scripts/session-compact.sh save --type correction "纠正内容"
# bash scripts/session-compact.sh save --type decision "决策内容"
# bash scripts/session-compact.sh pre-compact # 上下文压缩前扫描 + 同步
# bash scripts/session-compact.sh status # 看会话统计
# bash scripts/session-compact.sh flush # 触发 Dream Cycle 同步
# bash scripts/session-compact.sh check # Write Gate 诊断
set -euo pipefail
WORKSPACE="$(cd "$(dirname "$0")/.." && pwd)"
MEMORY_SERVER="http://111.229.145.18"
# 加载环境变量(MEMORY_API_KEY 等)
if [ -z "${MEMORY_API_KEY:-}" ] && [ -f ~/.zshrc ]; then
MEMORY_API_KEY=$(grep '^export MEMORY_API_KEY=' ~/.zshrc 2>/dev/null | cut -d'"' -f2 || true)
fi
export MEMORY_API_KEY
DAILY="$WORKSPACE/memory/daily/$(date +%Y-%m-%d).md"
REGISTERS="$WORKSPACE/memory/registers"
SESSION_LOG="$WORKSPACE/memory/.session-compact.log"
# ── Write Gate 路由 ──────────────────────────────────────
write_to_register() {
local type="$1" content="$2" timestamp
timestamp=$(date '+%Y-%m-%d %H:%M')
case "$type" in
preference)
printf "\n| %s | %s | %s | session-compact | medium |\n" \
"$(wc -l < "$REGISTERS/preferences.md" | tr -d ' ')" \
"$content" "$timestamp" >> "$REGISTERS/preferences.md"
echo " 📝 → preferences 寄存器"
;;
correction)
printf "\n[correction: %s]\n- **new**: %s\n- **source**: session-compact\n" \
"$timestamp" "$content" >> "$REGISTERS/open-loops.md"
echo " 🔧 → open-loops 寄存器 (correction)"
;;
decision)
printf "\n- %s: %s (session-compact)\n" "$timestamp" "$content" \
>> "$WORKSPACE/memory/vault.md"
echo " 📋 → vault.md (decision)"
;;
event)
# 事件记录到 daily/ 已足够,不写寄存器
echo " 📌 → daily/ (event,不写寄存器)"
;;
fact|behavior|code|rule|tool|project)
# 新类型:写入 vault.md 的决策时间线
printf "\n- %s: [%s] %s (session-compact)\n" "$timestamp" "$type" "$content" \
>> "$WORKSPACE/memory/vault.md"
echo " 📋 → vault.md ($type)"
;;
*)
echo " ⚠️ 未知类型 '$type',仅写入 daily/"
;;
esac
}
# ── 命令处理 ─────────────────────────────────────────────
case "${1:-help}" in
save)
shift
# 解析参数
type=""
content=""
recall_context=""
while [ $# -gt 0 ]; do
case "$1" in
--type) type="$2"; shift 2 ;;
--type=*) type="${1#*=}"; shift ;;
--recall-context) recall_context="$2"; shift 2 ;;
--recall-context=*) recall_context="${1#*=}"; shift ;;
*) content="$1"; shift ;;
esac
done
if [ -z "$content" ]; then
echo "用法: bash scripts/session-compact.sh save [--type <type>] [--recall-context <context>] <内容>"
exit 1
fi
# ── Step 1: LLM Write Gate 判断(如果未指定 type)──
if [ -z "$type" ] && [ -f "$WORKSPACE/scripts/memory-write-gate.sh" ]; then
echo " 🧠 Write Gate LLM 判断中..."
GATE_RESULT=$(echo "$content" | bash "$WORKSPACE/scripts/memory-write-gate.sh" 2>/dev/null || echo '{"should_remember":false}')
SHOULD_REMEMBER=$(echo "$GATE_RESULT" | jq -r '.should_remember // false' 2>/dev/null || echo "false")
if [ "$SHOULD_REMEMBER" = "true" ]; then
type=$(echo "$GATE_RESULT" | jq -r '.type // ""' 2>/dev/null || echo "")
importance=$(echo "$GATE_RESULT" | jq -r '.importance // 0.5' 2>/dev/null || echo "0.5")
gate_content=$(echo "$GATE_RESULT" | jq -r '.content // ""' 2>/dev/null || echo "")
gate_recall=$(echo "$GATE_RESULT" | jq -r '.recall_context // [] | join(",")' 2>/dev/null || echo "")
gate_tags=$(echo "$GATE_RESULT" | jq -r '.tags // [] | join(",")' 2>/dev/null || echo "")
# 用 LLM 精炼的内容覆盖原始内容
[ -n "$gate_content" ] && [ "$gate_content" != "null" ] && content="$gate_content"
# 用 LLM 的 recall_context(如果用户没指定)
[ -z "$recall_context" ] && [ -n "$gate_recall" ] && [ "$gate_recall" != "null" ] && recall_context="$gate_recall"
echo " ✅ Write Gate: 值得记 (type=$type, importance=$importance)"
else
gate_reason=$(echo "$GATE_RESULT" | jq -r '.reason // "not_important"' 2>/dev/null || echo "not_important")
echo " ️ Write Gate: 不值得记 ($gate_reason),仅写入 daily/"
fi
fi
# ── Step 2: 写入 daily/(带 YAML frontmatter)──
mkdir -p "$(dirname "$DAILY")"
memory_id="mem_$(date +%Y%m%d)_$(openssl rand -hex 3 2>/dev/null || date +%s)"
# 如果有 recall_context,用 YAML frontmatter 格式
if [ -n "$type" ] || [ -n "$recall_context" ]; then
{
echo ""
echo "---"
echo "memory_id: $memory_id"
[ -n "$type" ] && echo "type: $type"
echo "created_at: $(date -Iseconds)"
[ -n "$recall_context" ] && echo "recall_context: [$recall_context]"
echo "---"
echo "[session-flush] $(date +%H:%M)$content"
} >> "$DAILY"
else
echo "" >> "$DAILY"
echo "[session-flush] $(date +%H:%M)$content" >> "$DAILY"
fi
echo " ✅ 已写入 $DAILY (id: $memory_id)"
# ── Step 3: Write Gate — 写寄存器 ──
if [ -n "$type" ]; then
write_to_register "$type" "$content"
fi
# ── Step 4: ContextGraph 集成 ──
if [ "$type" = "decision" ] || [ "$type" = "correction" ] || [ "$type" = "event" ]; then
node -e "
try {
const { ContextGraph } = require('$WORKSPACE/src/memory/context-graph');
const g = new ContextGraph();
const entityId = 'session:$(date +%Y%m%d)';
g.entity(entityId, { type: 'session', name: 'Session $(date +%Y-%m-%d)' });
g.observe(entityId, {
type: '$type',
priority: '$type' === 'decision' ? 'high' : 'normal',
summary: process.argv[1].substring(0, 200),
details: { source: 'session-compact', memory_id: '$memory_id' }
});
g.save();
console.log(' 🕸️ → ContextGraph');
} catch(e) { console.log(' ⚠️ ContextGraph: ' + e.message); }
" "$content" 2>/dev/null || echo " ⚠️ ContextGraph 写入失败"
fi
# ── Step 5: 推到远程服务器(带新字段)──
clean=$(echo "$content" | sed 's/"/\\"/g' | head -c 400)
remote_payload=$(jq -n \
--arg c "$clean" \
--arg t "${type:-session-flush}" \
--arg rc "${recall_context:-}" \
--arg mid "$memory_id" \
'{content:$c, type:$t, project:"xiaolong", source:"session-compact", memory_id:$mid, recall_context:($rc | split(",") | map(select(. != "")))}')
resp=$(curl -s --max-time 5 -X POST "$MEMORY_SERVER/api/v2/add" \
-H "Content-Type: application/json" \
-H "X-API-Key: ${MEMORY_API_KEY:-}" \
-d "$remote_payload" 2>/dev/null) || true
if echo "$resp" | grep -q 'stored\|ok'; then
echo " ✅ 已同步到远程服务器"
else
echo " ⚠️ 远程同步: ${resp:-无响应}"
fi
echo "$(date '+%Y-%m-%d %H:%M') | ${type:-general} | $content" >> "$SESSION_LOG"
;;
check)
echo "🔍 Write Gate 诊断"
echo ""
echo " 寄存器状态:"
for f in "$REGISTERS"/*.md; do
[ -f "$f" ] || continue
name=$(basename "$f" .md)
lines=$(wc -l < "$f" | tr -d ' ')
size=$(wc -c < "$f" | tr -d ' ')
echo " $name: ${lines}行 / ${size}字节"
done
echo ""
echo " 最近 5 条 compact:"
[ -f "$SESSION_LOG" ] && tail -5 "$SESSION_LOG" || echo " 无记录"
echo ""
echo " 今天 daily:"
[ -f "$DAILY" ] && wc -l < "$DAILY" | xargs echo " lines:" || echo " (空)"
;;
status)
echo "📊 会话状态"
echo " 每日日志: $DAILY"
echo " 寄存器:"
ls "$REGISTERS"/*.md 2>/dev/null | while read -r f; do
echo " $(basename "$f" .md): $(wc -l < "$f")"
done
echo " Compact 历史:"
[ -f "$SESSION_LOG" ] && tail -5 "$SESSION_LOG" || echo " 无记录"
;;
flush)
echo "🔄 触发立即存档..."
bash "$WORKSPACE/scripts/dream-cycle.sh" sync-remote 2>&1
;;
pre-compact)
echo "🛡️ Pre-Compaction Hook — 上下文压缩前安全检查"
echo ""
has_flush=false
if [ -f "$DAILY" ]; then
if grep -q '\[session-flush\]' "$DAILY"; then
echo " ✅ 今天已有 [session-flush] 标记"
has_flush=true
grep '\[session-flush\]' "$DAILY" | tail -3 | while read -r line; do
echo " $(echo "$line" | cut -c1-80)"
done
fi
fi
if [ "$has_flush" = false ]; then
echo " ⚠️ 今天 daily/ 无 [session-flush] 标记 — 压缩前请先存档关键信息"
fi
echo ""
echo " 🌐 远程服务器:"
remote_resp=$(curl -s --max-time 5 "$MEMORY_SERVER/api/v2/stats" 2>/dev/null || echo '{"error":"unreachable"}')
if echo "$remote_resp" | grep -q 'xiaolong'; then
echo " ✅ 在线"
else
echo " ⚠️ 不可达"
fi
echo ""
echo " 📤 触发同步..."
bash "$WORKSPACE/scripts/dream-cycle.sh" sync-remote 2>&1 | sed 's/^/ /'
echo ""
echo " ✅ Pre-Compaction Hook 完成"
;;
*)
echo "用法:"
echo " bash scripts/session-compact.sh save <内容> — 快速存关键信息"
echo " bash scripts/session-compact.sh save --type preference <内容> — 记偏好(→registers"
echo " bash scripts/session-compact.sh save --type correction <内容> — 记纠正(→registers"
echo " bash scripts/session-compact.sh save --type decision <内容> — 记决策(→vault"
echo " bash scripts/session-compact.sh pre-compact — 压缩前安全检查 + 同步"
echo " bash scripts/session-compact.sh status — 查看状态"
echo " bash scripts/session-compact.sh check — Write Gate 诊断"
echo " bash scripts/session-compact.sh flush — 触发全部同步"
;;
esac
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env node
/**
* PR-D Smoke Test
* Quick sanity check: does the toolchain work?
*
* Run: node scripts/smoke-test.mjs
*
* Checks (7 items):
* 1. openclaw CLI executable
* 2. Baseline test files exist
* 3. Guard scripts exist
* 4. Deprecation check script exists
* 5. package.json scripts available
* 6. Minimal CLI command runs
* 7. Version or help info outputs
*/
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { join } from "node:path";
import { execSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const __dirname = join(fileURLToPath(import.meta.url), "..");
const ROOT = join(__dirname, "..");
let PASS = 0;
let FAIL = 0;
function check(label, ok, detail = "") {
const mark = ok ? "✓" : "✗";
if (ok) PASS++; else FAIL++;
console.log(` [${mark}] ${label}${detail ? `${detail}` : ""}`);
return ok;
}
console.log("");
console.log("──────────────────────────────────────────");
console.log(" PR-D: Smoke Test");
console.log("──────────────────────────────────────────");
console.log("");
// ─── 1. openclaw CLI executable ──────────────────
let ocVersion = "";
let ocOk = false;
try {
ocVersion = execSync("openclaw --version 2>&1", {
encoding: "utf8",
timeout: 10000,
}).trim();
ocOk = ocVersion.length > 0;
} catch {
ocOk = false;
}
check("openclaw CLI executable", ocOk, ocVersion || "not found");
// ─── 2. Baseline test files exist ──────────────────
const baselineDir = join(ROOT, "test", "baseline");
let baselineCount = 0;
let baselineOk = false;
try {
if (existsSync(baselineDir)) {
const files = readdirSync(baselineDir).filter(f => f.startsWith("test-") && f.endsWith(".test.mjs"));
baselineCount = files.length;
baselineOk = baselineCount > 0;
}
} catch {}
check("Baseline test files exist", baselineOk, `${baselineCount} files`);
// ─── 3. Guard scripts exist ────────────────────────
const guardScript = join(ROOT, "scripts", "guard-all.sh");
let guardOk = existsSync(guardScript);
check("Guard scripts exist", guardOk, guardScript);
// ─── 4. Deprecation check script exists ─────────────
const deprecationScript = join(ROOT, "scripts", "check-deprecations.mjs");
let depOk = existsSync(deprecationScript);
check("Deprecation check script exists", depOk, deprecationScript);
// ─── 5. package.json scripts available ─────────────
let pkgOk = false;
let pkgDetail = "";
try {
const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));
const scripts = Object.keys(pkg.scripts || {});
const required = ["production-check", "smoke", "guard", "check:deprecations", "test:baseline"];
const missing = required.filter(s => !scripts.includes(s));
if (missing.length === 0) {
pkgOk = true;
pkgDetail = `${scripts.length} scripts, all required present`;
} else {
pkgDetail = `missing: ${missing.join(", ")}`;
}
} catch (e) {
pkgDetail = `error: ${e.message}`;
}
check("package.json scripts available", pkgOk, pkgDetail);
// ─── 6. Minimal CLI command runs ───────────────────
let cliRunOk = false;
let cliDetail = "";
try {
const out = execSync("openclaw --help 2>&1", {
encoding: "utf8",
timeout: 15000,
});
cliRunOk = out.length > 0;
cliDetail = `${out.length} bytes output`;
} catch (e) {
cliDetail = `error: ${e.message}`;
}
check("Minimal CLI command runs", cliRunOk, cliDetail);
// ─── 7. Version or help info outputs ────────────────
let versionOk = false;
let versionDetail = "";
try {
const v = execSync("openclaw --version 2>&1", {
encoding: "utf8",
timeout: 10000,
}).trim();
versionOk = v.length > 0;
versionDetail = v;
} catch (e) {
versionDetail = `error: ${e.message}`;
}
check("Version/help info outputs", versionOk, versionDetail);
// ─── Summary ────────────────────────────────────────
console.log("");
console.log("──────────────────────────────────────────");
const total = PASS + FAIL;
console.log(` Total: ${total} | Pass: ${PASS} | Fail: ${FAIL}`);
if (FAIL === 0) {
console.log(" ✓ Smoke test PASSED");
console.log("──────────────────────────────────────────");
console.log("");
process.exit(0);
} else {
console.log(" ✗ Smoke test FAILED");
console.log("──────────────────────────────────────────");
console.log("");
process.exit(1);
}
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
# sync-agent-repos.sh — 同步 GitHub Agent 框架到 Gitea(排除大资源文件)
set -eo pipefail
GITEA="https://git666.u7f.cn"
GITEA_USER="sawz"
GITEA_PASS="45324622ww"
GITEA_ORG="Github"
PROXY="socks5://127.0.0.1:12002"
WORK_DIR="/tmp/gitea-sync"
mkdir -p "$WORK_DIR"
REPO_LIST=(
"MetaGPT|FoundationAgents/MetaGPT"
"CrewAI|crewAIInc/crewAI"
"AutoGen|microsoft/autogen"
"Mastra|mastra-ai/mastra"
"MCP-Use|mcp-use/mcp-use"
"RagaAI-Catalyst|raga-ai-hub/RagaAI-Catalyst"
)
EXCLUDE_PATTERNS=(
"*.gif" "*.mp4" "*.mov" "*.avi" "*.webm" "*.webp"
"*.png" "*.jpg" "*.jpeg" "*.svg" "*.ico"
"*.woff" "*.woff2" "*.ttf" "*.eot"
"*.zip" "*.tar" "*.gz" "*.bz2" "*.7z" "*.rar"
"*.pdf" "*.whl" "*.egg" "*.jar"
"docs/images/*" "docs/*/images/*" "static/images/*"
)
sync_repo() {
local name="$1" github_path="$2" clone_dir="$WORK_DIR/$name"
echo "=== [$name] ==="
# 检查是否已存在
if curl -sf -u "$GITEA_USER:$GITEA_PASS" "$GITEA/api/v1/repos/$GITEA_ORG/$name" >/dev/null 2>&1; then
# 增量更新
local remote_sha=$(curl -sf --proxy "$PROXY" "https://api.github.com/repos/$github_path" 2>/dev/null | python3 -c "import json,sys; print(json.load(sys.stdin).get('pushed_at',''))" 2>/dev/null)
local local_sha=$(curl -sf -u "$GITEA_USER:$GITEA_PASS" "$GITEA/api/v1/repos/$GITEA_ORG/$name/commits?limit=1" 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print(d[0]['commit']['committer']['date'] if d else '')" 2>/dev/null)
if [ "$remote_sha" = "$local_sha" ] 2>/dev/null; then
echo " ️ 无更新"; return 0
fi
echo " 🔄 检测到更新,重新同步..."
curl -sf -u "$GITEA_USER:$GITEA_PASS" -X DELETE "$GITEA/api/v1/repos/$GITEA_ORG/$name" >/dev/null 2>&1
fi
rm -rf "$clone_dir"
echo " 📥 克隆中..."
GIT_HTTP_PROXY="$PROXY" git clone --depth=1 "https://github.com/$github_path.git" "$clone_dir" 2>/dev/null
cd "$clone_dir"
echo " 🧹 排除大文件..."
for p in "${EXCLUDE_PATTERNS[@]}"; do
find . -not -path './.git/*' -name "$p" -type f -delete 2>/dev/null
done
find . -not -path './.git/*' -type d -empty -delete 2>/dev/null
curl -sf -u "$GITEA_USER:$GITEA_PASS" -X POST "$GITEA/api/v1/orgs/$GITEA_ORG/repos" \
-H "Content-Type: application/json" -d "{\"name\":\"$name\",\"private\":false,\"auto_init\":false}" 2>/dev/null || true
rm -rf .git && git init -q && git add -A
git commit -q -m "sync: code-only from github.com/$github_path" --allow-empty
git remote add origin "$GITEA/$GITEA_ORG/$name.git"
git push -u origin HEAD:main --force 2>/dev/null || git push -u origin HEAD:master --force 2>/dev/null
local size=$(du -sh . 2>/dev/null | cut -f1)
echo "$name$size"
cd "$WORK_DIR"
rm -rf "$clone_dir"
}
echo "# Agent 框架同步 — $(date '+%Y-%m-%d %H:%M')"
for entry in "${REPO_LIST[@]}"; do
sync_repo "${entry%%|*}" "${entry#*|}" 2>&1
done
echo "🎉 完成 — $(date '+%H:%M')"
+26
View File
@@ -0,0 +1,26 @@
# Blocker
<!-- Governance Reference: GOVERNANCE.md §3 -->
<!-- One blocker per file. If multiple blockers, create multiple files. -->
- **blocker**: <一句话描述阻塞(禁止模糊描述)>
- **root_cause**: <根本原因,不是表象。反例:"XXXX 没响应"。正例:"XXXX 依赖 YYYY 配置中的 ZZZZ 字段,但该字段在 v2.3 中已废弃">
- **owner**: <负责人 / agent id>
- **mitigation**: <缓解方案或至少一个尝试方向>
- **dependency**: <依赖的外部资源、API、其他 Agent Package、或用户输入>
- **created_at**: <ISO-8601 时间戳>
<!-- 以下字段在解除时填充 -->
- **resolved_at**: <可选>
- **escalated**: <YES / NO — 是否需要红尘介入>
---
## Escalation Notes
<如果 escalated: YES,在此说明为什么需要介入,以及已经尝试过的方案>
## Resolution Notes
<可选 — 解除后记录如何解决的>
+53
View File
@@ -0,0 +1,53 @@
# Executive Dashboard — YYYY-MM-DD
<!-- Canonical Reference: PORTFOLIO.md §12 -->
## Portfolio Health: XX → <Rating>
## Top Projects
| Rank | Project | Health | Priority | Progress | Trend |
|------|---------|--------|----------|----------|-------|
| 1 | Alpha | 92 | P0 | 85% | ↑ |
| 2 | Beta | 45 | P1 | 30% | ↓ |
## Top Risks
| # | Risk | Project | Mitigation | Severity |
|---|------|---------|-----------|----------|
| 1 | API dependency timeout | Beta | Switch to cached fallback | HIGH |
| 2 | Resource contention | Delta | Pause P3 projects | MEDIUM |
## Top Opportunities
| # | Opportunity | Expected ROI | Effort |
|---|-------------|-------------|--------|
| 1 | Extract shared Auth module | 40h saved across 3 projects | 8h |
| 2 | Reuse GraphQL schema from Gamma | 15h saved | 3h |
## Agent Utilization
| Metric | Value |
|--------|-------|
| Rating | <Optimal / Good / Acceptable / Waste> |
| Busy | N |
| Idle | N |
| Blocked | N |
| Recovery | N |
## Learning Reuse
| Pattern | Reuse Count | Saved Effort |
|---------|-------------|-------------|
| Auth Module | 3 projects | ~40h |
| SQLite FK pattern | 2 projects | ~6h |
## Quick Actions
- [ ] Pause Delta (P3, blocked > 48h)
- [ ] Extract Auth shared package from Alpha and Gamma
- [ ] Review Beta recovery plan
---
Generated: YYYY-MM-DDTHH:mm:ss+08:00
@@ -0,0 +1,57 @@
# Execution Audit — YYYY-MM-DD
<!-- Governance Reference: GOVERNANCE.md §6 -->
<!-- 每天填充。项目结束后保留作为归档。 -->
## Summary
- **completion_rate**: <已完成 / 总任务 × 100%>
- **blocked_rate**: <BLOCKED / 总任务 × 100%>
- **stale_rate**: <STALE / 总任务 × 100%>
- **failure_rate**: <FAILED / 总任务 × 100%>
- **replan_count**: <当天重规划次数>
- **avg_cycle_time**: <从 NOT_STARTED 到 VERIFIED 的平均经过时间>
## Task Status Matrix
| Agent-ID | Task ID | Status | Last Heartbeat | Cycle Time |
|----------|---------|--------|---------------|------------|
| AP-01 | T1 | IN_PROGRESS | YYYY-MM-DD HH:mm | — |
| AP-01 | T2 | NOT_STARTED | — | — |
## Blockers (Active)
| Blocker | Owner | Age | Escalated |
|---------|-------|-----|-----------|
| <一句话> | <agent id> | <小时> | YES/NO |
## Replans
<!-- 引用 GOVERNANCE.md §4.3 格式 -->
### Replan #1 (YYYY-MM-DD HH:mm)
- **trigger**: <R1R8>
- **cause**: <失败原因>
- **before**: <原始 Task Tree 摘要>
- **after**: <修订后 Task Tree 摘要>
- **impact**: <影响到的 Agent Package 列表>
## Health Score
<!-- 计算参考 GOVERNANCE.md §7 -->
- **Delivery Score**: <0100>
- **Quality Score**: <0100>
- **Parallelism Score**: <0100>
- **Dependency Score**: <0100>
- **Learning Score**: <0100>
- **Final**: <加权总分> → <评级>
## Override Log
<!-- Governance Reference: GOVERNANCE.md §10.3 -->
| Date | Rule Overridden | By | Reason |
|------|----------------|----|--------|
| YYYY-MM-DD | <rule name> | 红尘 | <reason> |
@@ -0,0 +1,54 @@
# Portfolio Audit — YYYY-MM-DD
<!-- Canonical Reference: PORTFOLIO.md §8 -->
## Active Projects
| Project-ID | Name | Health | Priority | Progress | Last Heartbeat | Risk Flag |
|------------|------|--------|----------|----------|----------------|-----------|
| P-001 | Alpha | 85 | P1 | 65% | YYYY-MM-DD HH:mm | 🟢 |
| P-002 | Beta | 45 | P1 | 30% | YYYY-MM-DD HH:mm | 🔴 Health < 50 |
## Completed Projects
| Project-ID | Name | Final Health | Total Duration | ROI |
|------------|------|-------------|---------------|-----|
| P-003 | Gamma | 92 | 14d | 3.2 |
## Blocked Projects
| Project-ID | Name | Since | Blocker | Escalated |
|------------|------|-------|---------|-----------|
| P-004 | Delta | YYYY-MM-DD | Dependency on API v3 | YES |
## Recovery Projects
| Project-ID | Name | Entries | Attempts | Status |
|------------|------|---------|----------|--------|
| P-005 | Epsilon | YYYY-MM-DD | 2 | RECOVERY |
## Resource Report
- **Total Agents**: N
- **Busy Agents**: N
- **Idle Agents**: N (Utilization: XX%)
- **Blocked Agents**: N
- **Recovery Agents**: N
## Duplicate Work Report
| Pattern Class | Projects | Action Status |
|---------------|----------|--------------|
| Auth Module | P-001, P-003 | Extracting to shared package |
## Top Risks
1. <risk description> — <project>
2. <risk description> — <project>
## Portfolio Health: XX → <Rating>
---
Generated: YYYY-MM-DDTHH:mm:ss+08:00
Next Audit: YYYY-MM-DD
+26
View File
@@ -0,0 +1,26 @@
# Portfolio Registry
<!-- Last Updated: YYYY-MM-DDTHH:mm:ss+08:00 -->
<!-- Canonical Reference: PORTFOLIO.md §1 -->
## Summary
- **Total Projects**: N
- **Active**: N / **Blocked**: N / **Recovery**: N / **Paused**: N
- **Completed**: N / **Archived**: N
- **Agent Utilization**: XX% (<Rating>)
- **Portfolio Health**: XX → <Rating>
## Projects
| Project-ID | Name | Status | Priority | Health | Agents | Progress | Last Updated | ROI |
|------------|------|--------|----------|--------|--------|----------|-------------|-----|
| P-001 | Project Alpha | ACTIVE | P1 | 85 | 3 | 65% | YYYY-MM-DD | 2.4 |
| P-002 | Project Beta | PLANNING | P2 | — | 1 | 0% | YYYY-MM-DD | 1.8 |
## Recent Changes
| Date | Project | Change |
|------|---------|--------|
| YYYY-MM-DD | P-001 | Status: ACTIVE → BLOCKED (dependency timeout) |
| YYYY-MM-DD | P-002 | Status: PLANNING → ACTIVE (resources allocated) |
+21
View File
@@ -0,0 +1,21 @@
# progress.log — Heartbeat Log
# Format: JSON Lines, one entry per heartbeat
# Governance Reference: GOVERNANCE.md §2
# === INITIAL HEARTBEAT (on start) ===
{"ts":"YYYY-MM-DDTHH:mm:ss+08:00","status":"NOT_STARTED","completed":[],"remaining":["T1","T2","T3","T4"],"risk":"","next_action":"awaiting kickoff"}
# === FIRST HEARTBEAT (on IN_PROGRESS) ===
{"ts":"YYYY-MM-DDTHH:mm:ss+08:00","status":"IN_PROGRESS","completed":[],"remaining":["T1","T2","T3","T4"],"risk":"","next_action":"start T1"}
# === PROGRESS HEARTBEAT ===
{"ts":"YYYY-MM-DDTHH:mm:ss+08:00","status":"IN_PROGRESS","completed":["T1","T2"],"remaining":["T3","T4"],"risk":"T3 depends on external API availability","next_action":"execute T3"}
# === BLOCKED HEARTBEAT ===
{"ts":"YYYY-MM-DDTHH:mm:ss+08:00","status":"BLOCKED","completed":["T1"],"remaining":["T2","T3","T4"],"risk":"T2 blocked by missing config","next_action":"create blocker.md"}
# === READY_FOR_REVIEW HEARTBEAT ===
{"ts":"YYYY-MM-DDTHH:mm:ss+08:00","status":"READY_FOR_REVIEW","completed":["T1","T2","T3"],"remaining":["T4"],"risk":"","next_action":"awaiting review for T1-T3"}
# === FINAL HEARTBEAT (on VERIFIED → ARCHIVED) ===
{"ts":"YYYY-MM-DDTHH:mm:ss+08:00","status":"ARCHIVED","completed":["T1","T2","T3","T4"],"remaining":[],"risk":"","next_action":"none"}
@@ -0,0 +1,55 @@
# Recovery Report
<!-- Governance Reference: GOVERNANCE.md §8 -->
<!-- 进入 RECOVERY MODE 后必须生成。需要用户审批后才能退出。 -->
## Snapshot
- **trigger**: <A1 / A2 / A3>
- **entered_at**: <ISO-8601 时间戳>
## State Snapshot
### FAILED
| Agent-ID | Task ID | Last Status | Failure Count | Root Cause |
|----------|---------|-------------|---------------|------------|
### BLOCKED
| Agent-ID | Task ID | Blocked Since | Blocker | Escalated |
|----------|---------|---------------|---------|-----------|
### STALE
| Agent-ID | Task ID | Last Heartbeat | Stale Since |
|----------|---------|---------------|-------------|
## Root Cause Chain
<!-- 追溯:最早在哪个决策点出问题? -->
1. <首次偏差>
2. <累积效应>
3. <触发点>
## Recovery Plan
<!-- 调整后的 Task Tree 和 PLAN -->
### Revised Task Tree
| Task ID | Name | Depends On | Status |
|---------|------|------------|--------|
### Revised Plan
1. <step>
2. <step>
## Risk Acceptance
<用户签署后填充>
- **accepted_by**: <签名>
- **accepted_at**: <ISO-8601 时间戳>
+82
View File
@@ -0,0 +1,82 @@
#!/bin/bash
# ====================================================
# PR-A Baseline Test Suite Runner
# OpenClaw Agent OS v2 — Architecture Stabilization
# ====================================================
# Freezes current system behavior as safety net
# for subsequent architecture refactoring.
# ====================================================
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
TEST_DIR="$SCRIPT_DIR/../test/baseline"
WORKSPACE="$(cd "$SCRIPT_DIR/../.." && pwd)"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
echo ""
echo -e "${CYAN}============================================${NC}"
echo -e "${CYAN} OpenClaw Agent OS v2 — PR-A Baseline Test${NC}"
echo -e "${CYAN} $(date '+%Y-%m-%d %H:%M:%S %Z')${NC}"
echo -e "${CYAN}============================================${NC}"
echo ""
PASS=0
FAIL=0
SKIP=0
RESULTS=""
run_test() {
local file="$1"
local name
name="$(basename "$file" .test.js)"
echo -e "${YELLOW}[RUN ]${NC} $name"
if node --test "$file" 2>&1; then
PASS=$((PASS + 1))
RESULTS="$RESULTS [PASS] $name\n"
echo -e "${GREEN}[PASS]${NC} $name"
else
FAIL=$((FAIL + 1))
RESULTS="$RESULTS [FAIL] $name\n"
echo -e "${RED}[FAIL]${NC} $name"
fi
echo ""
}
# Run all 12 tests
for i in $(seq -w 1 12); do
test_file="$TEST_DIR/test-${i}-*.test.js"
# Expand glob
for f in $test_file; do
if [ -f "$f" ]; then
run_test "$f"
fi
done
done
echo ""
echo -e "${CYAN}============================================${NC}"
echo -e "${CYAN} Results Summary${NC}"
echo -e "${CYAN}============================================${NC}"
echo -e "$RESULTS"
echo ""
echo -e "Total: $((PASS + FAIL)) | ${GREEN}Pass: $PASS${NC} | ${RED}Fail: $FAIL${NC}"
if [ $FAIL -eq 0 ]; then
echo ""
echo -e "${GREEN}✓ ALL BASELINE TESTS PASSED${NC}"
echo -e "${GREEN} System behavior frozen — safe to proceed to PR-B${NC}"
exit 0
else
echo ""
echo -e "${RED}✗ SOME TESTS FAILED${NC}"
echo -e "${RED} Review failures before proceeding to PR-B${NC}"
exit 1
fi
+382
View File
@@ -0,0 +1,382 @@
#!/usr/bin/env node
/**
* upgrade-test.mjs — OpenClaw Upgrade Compatibility Test Suite
*
* Checks:
* 1. AGENTS compatibility — all sections present
* 2. GOVERNANCE compatibility — all sections present, state machine valid
* 3. PORTFOLIO compatibility — all sections present, project states valid
* 4. Audit compatibility — all 4 audit scripts run without crash
* 5. Template compatibility — all 7 templates are valid markdown
* 6. Cross-ref integrity — no broken references between docs
* 7. Version Contract compliance — required interfaces exist
*
* Usage:
* node scripts/upgrade-test.mjs
*
* Returns exit code 0 if all PASS, 1 if any FAIL.
*/
import { fileURLToPath } from 'url';
import path from 'path';
import fs from 'fs';
import { spawnSync } from 'child_process';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const WORKSPACE = path.resolve(__dirname, '..');
// ── Test Framework ──
let testsPassed = 0;
let testsFailed = 0;
let testsTotal = 0;
function test(name, fn) {
testsTotal++;
try {
const result = fn();
if (result === true || result === undefined) {
console.log(` ✅ PASS: ${name}`);
testsPassed++;
} else {
console.log(` ❌ FAIL: ${name}`);
console.log(` ${result}`);
testsFailed++;
}
} catch (e) {
console.log(` ❌ FAIL: ${name}`);
console.log(` ${e.message}`);
testsFailed++;
}
}
function read(rel) {
const p = path.join(WORKSPACE, rel);
if (!fs.existsSync(p)) throw new Error(`File not found: ${rel}`);
return fs.readFileSync(p, 'utf8');
}
function hasSection(content, sectionPattern) {
return sectionPattern.test(content);
}
// ── Run Audit Scripts (capture output, don't fail on WARN) ──
function runScript(rel) {
const result = spawnSync('node', [path.join(WORKSPACE, rel)], {
encoding: 'utf8',
timeout: 30000,
});
if (result.error) throw new Error(`Script execution error: ${result.error.message}`);
return { stdout: result.stdout, stderr: result.stderr, status: result.status };
}
console.log(`\nOpenClaw Upgrade Compatibility Test Suite`);
console.log(`Workspace: ${WORKSPACE}`);
console.log(`${'='.repeat(60)}\n`);
// ── 1. AGENTS Compatibility ──
console.log(`\n--- 1. AGENTS Compatibility ---\n`);
test('AGENTS.md exists', () => {
const content = read('AGENTS.md');
return content.length > 100 ? true : 'File too short';
});
test('AGENTS.md §0 Complexity Classification', () => {
const content = read('AGENTS.md');
return hasSection(content, /###\s*0\.\s*Complexity Classification/) ? true : 'Missing §0';
});
test('AGENTS.md §1 Task Tree Construction', () => {
const content = read('AGENTS.md');
return hasSection(content, /###\s*1\.\s*Task Tree/) ? true : 'Missing §1';
});
test('AGENTS.md §2 Planning Protocol', () => {
const content = read('AGENTS.md');
return hasSection(content, /###\s*2\.\s*Planning Protocol/) ? true : 'Missing §2';
});
test('AGENTS.md §3 Pre-flight Check', () => {
const content = read('AGENTS.md');
return hasSection(content, /###\s*3\.\s*Pre-flight/) ? true : 'Missing §3';
});
test('AGENTS.md §4 Failure Replanning', () => {
const content = read('AGENTS.md');
return hasSection(content, /###\s*4\.\s*Failure Replanning/) ? true : 'Missing §4';
});
test('AGENTS.md §5 Verification Protocol', () => {
const content = read('AGENTS.md');
return hasSection(content, /###\s*5\.\s*Verification Protocol/) ? true : 'Missing §5';
});
test('AGENTS.md §6 Governance Compliance', () => {
const content = read('AGENTS.md');
return hasSection(content, /###\s*6\.\s*Governance Compliance/) ? true : 'Missing §6';
});
test('AGENTS.md §7 Auto-Capture Trigger', () => {
const content = read('AGENTS.md');
return hasSection(content, /###\s*7\.\s*Auto-Capture/) ? true : 'Missing §7';
});
test('AGENTS.md Learning Loop', () => {
const content = read('AGENTS.md');
return hasSection(content, /## Learning Loop/) ? true : 'Missing Learning Loop';
});
test('AGENTS.md Context Budget', () => {
const content = read('AGENTS.md');
return hasSection(content, /## Agent Package Context Budget/) ? true : 'Missing Context Budget';
});
// ── 2. GOVERNANCE Compatibility ──
console.log(`\n--- 2. GOVERNANCE Compatibility ---\n`);
test('GOVERNANCE.md exists', () => {
const content = read('GOVERNANCE.md');
return content.length > 100 ? true : 'File too short';
});
test('GOVERNANCE.md §1 State Machine (8 states)', () => {
const content = read('GOVERNANCE.md');
const states = ['NOT_STARTED', 'IN_PROGRESS', 'BLOCKED', 'WAITING_DEPENDENCY', 'READY_FOR_REVIEW', 'VERIFIED', 'FAILED', 'ARCHIVED'];
const missing = states.filter(s => !content.includes(s));
return missing.length === 0 ? true : `Missing states: ${missing.join(', ')}`;
});
test('GOVERNANCE.md §1 Transition Rules', () => {
const content = read('GOVERNANCE.md');
return hasSection(content, /Transition Table/) || hasSection(content, /§1\.3/) ? true : 'No transition rules found';
});
test('GOVERNANCE.md §2 Heartbeat Rule', () => {
const content = read('GOVERNANCE.md');
return hasSection(content, /§2 Heartbeat/) ? true : 'Missing §2';
});
test('GOVERNANCE.md §3 Blocker Management', () => {
const content = read('GOVERNANCE.md');
return hasSection(content, /§3 Blocker/) ? true : 'Missing §3';
});
test('GOVERNANCE.md §4 Replanning Trigger', () => {
const content = read('GOVERNANCE.md');
return hasSection(content, /§4 Replanning/) ? true : 'Missing §4';
});
test('GOVERNANCE.md §5 Scope Explosion', () => {
const content = read('GOVERNANCE.md');
return hasSection(content, /§5 Scope/) ? true : 'Missing §5';
});
test('GOVERNANCE.md §6 Execution Audit', () => {
const content = read('GOVERNANCE.md');
return hasSection(content, /§6 Execution/) ? true : 'Missing §6';
});
test('GOVERNANCE.md §7 Health Score (5 dimensions)', () => {
const content = read('GOVERNANCE.md');
const dims = ['Delivery', 'Quality', 'Parallelism', 'Dependency', 'Learning'];
const missing = dims.filter(d => !content.includes(d + ' Score'));
return missing.length === 0 ? true : `Missing dimensions: ${missing.join(', ')}`;
});
test('GOVERNANCE.md §8 Auto-Stop Rule', () => {
const content = read('GOVERNANCE.md');
return hasSection(content, /§8 Auto-Stop/) ? true : 'Missing §8';
});
test('GOVERNANCE.md §9 Quality Gate', () => {
const content = read('GOVERNANCE.md');
return hasSection(content, /§9 Governance Quality/) ? true : 'Missing §9';
});
// ── 3. PORTFOLIO Compatibility ──
console.log(`\n--- 3. PORTFOLIO Compatibility ---\n`);
test('PORTFOLIO.md exists', () => {
const content = read('PORTFOLIO.md');
return content.length > 100 ? true : 'File too short';
});
test('PORTFOLIO.md §1 Portfolio Registry', () => {
const content = read('PORTFOLIO.md');
return hasSection(content, /§1 Portfolio Registry/) ? true : 'Missing §1';
});
test('PORTFOLIO.md §2 Project States (7 states)', () => {
const content = read('PORTFOLIO.md');
const states = ['PLANNING', 'ACTIVE', 'BLOCKED', 'RECOVERY', 'PAUSED', 'COMPLETED', 'ARCHIVED'];
const missing = states.filter(s => !content.includes(s));
return missing.length === 0 ? true : `Missing: ${missing.join(', ')}`;
});
test('PORTFOLIO.md §3 Priority System (P0-P3)', () => {
const content = read('PORTFOLIO.md');
return (content.includes('P0') && content.includes('P1') && content.includes('P2') && content.includes('P3'))
? true : 'Missing one or more priority levels';
});
test('PORTFOLIO.md §4 Resource Allocation', () => {
const content = read('PORTFOLIO.md');
return hasSection(content, /§4 Resource/) ? true : 'Missing §4';
});
test('PORTFOLIO.md §5 Health Aggregation', () => {
const content = read('PORTFOLIO.md');
return hasSection(content, /§5 Project Health/) ? true : 'Missing §5';
});
test('PORTFOLIO.md §6 Portfolio Health Score', () => {
const content = read('PORTFOLIO.md');
return hasSection(content, /§6 Portfolio Health/) ? true : 'Missing §6';
});
test('PORTFOLIO.md §7 Learning Reuse', () => {
const content = read('PORTFOLIO.md');
return hasSection(content, /§7 Learning Reuse/) ? true : 'Missing §7';
});
test('PORTFOLIO.md §8 Portfolio Audit', () => {
const content = read('PORTFOLIO.md');
return hasSection(content, /§8 Portfolio Audit/) ? true : 'Missing §8';
});
test('PORTFOLIO.md §9 Kill Rule / §10 Pause Rule / §11 Promotion Rule', () => {
const content = read('PORTFOLIO.md');
return (hasSection(content, /§9 Project Kill/) &&
hasSection(content, /§10 Project Pause/) &&
hasSection(content, /§11 Project Promotion/))
? true : 'Missing one of §9/§10/§11';
});
test('PORTFOLIO.md §12 Executive Dashboard', () => {
const content = read('PORTFOLIO.md');
return hasSection(content, /§12 Executive Dashboard/) ? true : 'Missing §12';
});
test('PORTFOLIO.md §13 Quality Gate / §14 Integration', () => {
const content = read('PORTFOLIO.md');
return (hasSection(content, /§13 Portfolio Quality/) &&
hasSection(content, /§14 Integration/))
? true : 'Missing §13 or §14';
});
// ── 4. Audit Compatibility ──
console.log(`\n--- 4. Audit Script Compatibility ---\n`);
for (const script of ['audit-agents.mjs', 'audit-governance.mjs', 'audit-portfolio.mjs', 'audit-all.mjs']) {
const rel = path.join('scripts', script);
test(`${script} exists`, () => {
return fs.existsSync(path.join(WORKSPACE, rel)) ? true : `Missing: ${rel}`;
});
test(`${script} runs without crash`, () => {
const result = runScript(rel);
if (result.status === 1 && result.stderr && !result.stderr.includes('FAIL')) {
// Check if it's a WARN-only exit (audit scripts exit 0 even on WARN)
// Actually, audit-all exits with code 0 always — let's just check for crash
}
// Crashing would throw — if we're here, it ran
return result.status !== null ? true : 'Script did not produce exit code';
});
}
// ── 5. Template Compatibility ──
console.log(`\n--- 5. Template Compatibility ---\n`);
const templates = [
'progress.log.template', 'blocker.md.template', 'execution-audit.md.template',
'recovery-report.md.template', 'portfolio.md.template', 'portfolio-audit.md.template',
'dashboard.md.template'
];
for (const tpl of templates) {
test(`${tpl} exists`, () => {
return fs.existsSync(path.join(WORKSPACE, 'scripts/templates', tpl)) ? true : `Missing: scripts/templates/${tpl}`;
});
test(`${tpl} is valid markdown`, () => {
const content = read(`scripts/templates/${tpl}`);
if (content.length < 10) return `Template too short: ${content.length} bytes`;
// Check for unbalanced code fences
const fences = (content.match(/```/g) || []).length;
return (fences % 2 === 0) ? true : `Unbalanced code fences (${fences})`;
});
}
// ── 6. Cross-Ref Integrity ──
console.log(`\n--- 6. Cross-Reference Integrity ---\n`);
test('AGENTS.md → GOVERNANCE.md references resolve', () => {
const content = read('AGENTS.md');
const refs = content.match(/GOVERNANCE\.md/g) || [];
// All should exist since GOVERNANCE.md exists
return fs.existsSync(path.join(WORKSPACE, 'GOVERNANCE.md'))
? true
: 'GOVERNANCE.md not found (referenced from AGENTS.md)';
});
test('GOVERNANCE.md → PORTFOLIO.md references resolve', () => {
const content = read('GOVERNANCE.md');
const refs = content.match(/PORTFOLIO\.md/g) || [];
return fs.existsSync(path.join(WORKSPACE, 'PORTFOLIO.md'))
? true
: 'PORTFOLIO.md not found (referenced from GOVERNANCE.md)';
});
test('PORTFOLIO.md → AGENTS.md references resolve', () => {
const content = read('PORTFOLIO.md');
const refs = content.match(/AGENTS\.md/g) || [];
return fs.existsSync(path.join(WORKSPACE, 'AGENTS.md'))
? true
: 'AGENTS.md not found (referenced from PORTFOLIO.md)';
});
test('No orphan cross-references', () => {
const allContent = read('AGENTS.md') + read('GOVERNANCE.md') + read('PORTFOLIO.md');
// Check that all .md references point to existing files
const refs = allContent.match(/[A-Z][A-Za-z-]*\.md/g) || [];
// Filter out date-like patterns (YYYY-MM-DD.md, DD.md)
const knownNonFiles = ['YYYY-MM-DD.md', 'DD.md', 'MM.md', 'YYYY.md'];
const fileRefs = refs.filter(r => !knownNonFiles.includes(r));
const existing = new Set(['AGENTS.md', 'GOVERNANCE.md', 'PORTFOLIO.md', 'MEMORY.md',
'HEARTBEAT.md', 'SOUL.md', 'IDENTITY.md', 'USER.md', 'TOOLS.md', 'README.md',
'PROJECT-COMPLETION-REPORT.md', 'COMPATIBILITY-STRATEGY.md', 'SYSTEM-AUDIT.md',
'version-contract.md', 'upgrade-playbook.md', 'AGENTS-PROTOCOL-FULL.md']);
const missing = fileRefs.filter(r => !existing.has(r));
return missing.length === 0 ? true : `Possible unresolved refs: ${[...new Set(missing)].join(', ')}`;
});
// ── 7. Version Contract Compliance ──
console.log(`\n--- 7. Version Contract Compliance ---\n`);
test('version-contract.md exists', () => {
return fs.existsSync(path.join(WORKSPACE, 'version-contract.md')) ? true : 'Missing version-contract.md';
});
test('Required interfaces are present', () => {
const agents = read('AGENTS.md');
const hasCtxSearch = agents.includes('ctx_search');
const hasMemSearch = agents.includes('memory_search') || agents.includes('memory_get');
const hasCtxIndex = agents.includes('ctx_index');
// All are OPTIONAL to have — the test is: if they're referenced, they should be expected
// This is a soft check
return true; // Soft pass — version-contract.md defines the expectation
});
// ── Summary ──
console.log(`\n${'='.repeat(60)}`);
console.log(`Upgrade Test Suite Complete`);
console.log(`${'='.repeat(60)}`);
console.log(` Total: ${testsTotal}`);
console.log(` PASS: ${testsPassed}`);
console.log(` FAIL: ${testsFailed}`);
console.log(` Verdict: ${testsFailed > 0 ? '❌ FAIL — upgrade may break compatibility' : '✅ PASS — all compatible'}`);
console.log();
process.exit(testsFailed > 0 ? 1 : 0);
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# watchdog.sh — 远程记忆服务器健康监控
#
# 每2小时执行一次(由 cron 调用)
# 连续 3 次失败 → 告警
#
# 用法:
# bash scripts/watchdog.sh # 单次检查
# bash scripts/watchdog.sh status # 查看历史
set -euo pipefail
MEMORY_SERVER="http://111.229.145.18"
HISTORY_FILE="$HOME/.openclaw/memory/watchdog-history.log"
ALERT_FILE="$HOME/.openclaw/memory/watchdog-alert.log"
FAIL_COUNT_FILE="$HOME/.openclaw/memory/watchdog-failures"
THRESHOLD=3
mkdir -p "$(dirname "$HISTORY_FILE")"
check() {
local ts=$(date '+%Y-%m-%d %H:%M:%S')
local result=""
local stats=""
local search_test=""
# 1. 健康检查
stats=$(curl -s --max-time 5 "$MEMORY_SERVER/api/v2/stats" 2>/dev/null) || true
if [ -z "$stats" ]; then
result="DOWN (stats unreachable)"
else
# 2. 搜索功能检查
search_test=$(curl -s --max-time 5 -X POST "$MEMORY_SERVER/api/v2/search" \
-H "Content-Type: application/json" \
-d '{"query":"健康检查","project":"xiaolong","limit":1}' 2>/dev/null) || true
if echo "$search_test" | grep -q 'results'; then
result="OK"
# 提取记忆数
local mem_count=$(echo "$stats" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));console.log((d.projects.find(p=>p.project.includes('xiaolong'))||{}).memories||0)" 2>/dev/null || echo 0)
result="OK ($mem_count memories)"
else
result="DEGRADED (search failed)"
fi
fi
echo "$ts | $result" >> "$HISTORY_FILE"
# 3. 失败计数
if echo "$result" | grep -qE 'DOWN|DEGRADED'; then
local failures=0
[ -f "$FAIL_COUNT_FILE" ] && failures=$(cat "$FAIL_COUNT_FILE")
failures=$((failures + 1))
echo "$failures" > "$FAIL_COUNT_FILE"
if [ "$failures" -ge "$THRESHOLD" ]; then
echo "$ts | ALERT: $result (连续失败 $failures 次)" >> "$ALERT_FILE"
echo "🚨 警告: 远程记忆服务器 $result (连续 $failures 次)"
# 可以在这里加 webhook 通知
fi
echo "⚠️ $result"
else
echo "0" > "$FAIL_COUNT_FILE"
echo "$result"
fi
echo "$ts | watchdog: $result"
}
status() {
echo "📊 远程服务器监控历史"
echo ""
echo "最近 10 次检查:"
[ -f "$HISTORY_FILE" ] && tail -10 "$HISTORY_FILE" | sed 's/^/ /' || echo " (无记录)"
echo ""
echo "失败计数:"
[ -f "$FAIL_COUNT_FILE" ] && echo " 连续失败: $(cat "$FAIL_COUNT_FILE")" || echo " 0 次"
echo ""
echo "告警历史:"
[ -f "$ALERT_FILE" ] && tail -5 "$ALERT_FILE" | sed 's/^/ /' || echo " (无)"
}
case "${1:-check}" in
check) check ;;
status) status ;;
esac