Files
openclaw-workspace-2026/architecture/format-locker.js
T

349 lines
8.8 KiB
JavaScript

#!/usr/bin/env node
/**
* Format Locker - Enforce Structured Output Templates
*
* Ensures responses follow prescribed formats based on workflow type
*/
class FormatLocker {
constructor(workflow) {
this.workflow = workflow;
this.templates = this.loadTemplates();
}
/**
* Load format templates for each workflow type
*/
loadTemplates() {
return {
coding: {
requiredSections: ['Summary', 'Files Modified', 'Verification', 'Decisions Made'],
optionalSections: ['Still Open', 'Testing Notes'],
format: `
## Summary
[What changed - 1-2 sentences]
## Files Modified
| File | Change |
|------|--------|
| file1.py | [description] |
| file2.js | [description] |
## Verification
- [ ] Tests pass: \`command\`
- [ ] Lint passes: \`command\`
- [ ] Manual verification: [how]
## Decisions Made
- [Decision 1]
- [Decision 2]
## Still Open
- [ ] [if any]
`.trim(),
validators: {
'Files Modified': (content) => content.includes('|') && content.includes('File'),
'Verification': (content) => content.includes('- [ ]') || content.includes('- [x]'),
'Decisions Made': (content) => content.startsWith('- ')
}
},
debug: {
requiredSections: ['Problem', 'Cause', 'Fix', 'Validation'],
optionalSections: ['Prevention', 'Impact'],
format: `
## Problem
[Symptom - what was observed]
## Cause
[Root cause - why it happened]
## Fix
[What was changed to resolve it]
## Validation
[How it was verified fixed]
`.trim(),
validators: {
'Problem': (content) => content.length > 10,
'Cause': (content) => content.length > 10,
'Fix': (content) => content.length > 10,
'Validation': (content) => content.length > 10
}
},
deploy: {
requiredSections: ['Deployment Summary', 'Pre-Deployment', 'Changes Applied', 'Verification'],
optionalSections: ['Rollback', 'Post-Deployment Notes'],
format: `
## Deployment Summary
[What and where]
## Pre-Deployment
- [x] Backup created
- [x] Tests pass
- [x] Rollback plan documented
## Changes Applied
| Step | Command | Status |
|------|---------|--------|
| 1 | [command] | ✅ |
| 2 | [command] | ✅ |
## Verification
- [x] Service responding: [url]
- [x] Logs normal: [check]
- [x] Smoke test: [result]
`.trim(),
validators: {
'Pre-Deployment': (content) => content.includes('- [x]') || content.includes('- [ ]'),
'Changes Applied': (content) => content.includes('|'),
'Verification': (content) => content.includes('- [x]') || content.includes('- [ ]')
}
},
audit: {
requiredSections: ['Scope', 'Findings', 'Recommendations'],
optionalSections: ['Risk Assessment', 'Priority Matrix', 'Action Items'],
format: `
## Scope
[What was audited]
## Findings
| Item | Status | Severity |
|------|--------|----------|
| Finding 1 | ✅/⚠️/❌ | Low/Med/High |
| Finding 2 | ✅/⚠️/❌ | Low/Med/High |
## Recommendations
1. **[Priority]** [Action to take]
2. **[Priority]** [Action to take]
`.trim(),
validators: {
'Findings': (content) => content.includes('|'),
'Recommendations': (content) => /^\d+\./.test(content)
}
},
planning: {
requiredSections: ['Goal', 'Approach', 'Breakdown', 'Timeline'],
optionalSections: ['Dependencies', 'Risks', 'Acceptance Criteria'],
format: `
## Goal
[What we're trying to achieve]
## Approach
[High-level strategy]
## Breakdown
| Task | Owner | Estimate |
|------|-------|----------|
| Task 1 | [agent] | [time] |
| Task 2 | [agent] | [time] |
## Timeline
- Phase 1: [duration] - [deliverable]
- Phase 2: [duration] - [deliverable]
`.trim(),
validators: {
'Breakdown': (content) => content.includes('|'),
'Timeline': (content) => content.includes('- Phase')
}
}
};
}
/**
* Get template for current workflow
*/
getTemplate() {
return this.templates[this.workflow] || this.templates['coding'];
}
/**
* Validate that response follows required format
*/
validateFormat(response) {
const template = this.getTemplate();
const results = {
passed: true,
missingSections: [],
malformedSections: [],
suggestions: []
};
// Check for required sections
for (const section of template.requiredSections) {
const sectionPattern = new RegExp(`##\\s*${section.replace(/\s+/g, '\\s+')}`, 'i');
if (!sectionPattern.test(response)) {
results.missingSections.push(section);
results.passed = false;
}
}
// Extract and validate section contents
const sections = this.extractSections(response);
for (const [name, content] of Object.entries(sections)) {
if (template.validators[name]) {
const isValid = template.validators[name](content);
if (!isValid) {
results.malformedSections.push(name);
results.passed = false;
}
}
}
// Generate suggestions
if (results.missingSections.length > 0) {
results.suggestions.push(`Add missing sections: ${results.missingSections.join(', ')}`);
}
if (results.malformedSections.length > 0) {
results.suggestions.push(`Fix format in sections: ${results.malformedSections.join(', ')}`);
}
return results;
}
/**
* Extract sections from markdown response
*/
extractSections(response) {
const sections = {};
const sectionRegex = /^##\s+(.+)$/gm;
let match;
const sectionNames = [];
const sectionPositions = [];
while ((match = sectionRegex.exec(response)) !== null) {
sectionNames.push(match[1].trim());
sectionPositions.push(match.index);
}
// Extract content between sections
for (let i = 0; i < sectionNames.length; i++) {
const startPos = sectionPositions[i] + sectionNames[i].length + 3; // +3 for "## "
const endPos = i < sectionNames.length - 1 ? sectionPositions[i + 1] : response.length;
sections[sectionNames[i]] = response.substring(startPos, endPos).trim();
}
return sections;
}
/**
* Apply format template to content
*/
applyTemplate(content, customData = {}) {
const template = this.getTemplate();
let formatted = template.format;
// Replace placeholders with actual content
for (const [key, value] of Object.entries(content)) {
const placeholder = `[${key}]`;
if (formatted.includes(placeholder)) {
formatted = formatted.replace(placeholder, value);
}
}
// Replace custom data
for (const [key, value] of Object.entries(customData)) {
const placeholder = `{${key}}`;
if (formatted.includes(placeholder)) {
formatted = formatted.replace(new RegExp(`\\{${key}\\}`, 'g'), value);
}
}
return formatted;
}
/**
* Wrap a response to ensure format compliance
*/
enforceFormat(response, autoFix = true) {
const validation = this.validateFormat(response);
if (validation.passed) {
return {
response: response,
status: 'passed',
fixes: []
};
}
if (!autoFix) {
return {
response: response,
status: 'failed',
errors: validation,
suggestion: this.getTemplate().format
};
}
// Auto-fix: add missing sections
let fixed = response;
const sections = this.extractSections(response);
const template = this.getTemplate();
for (const section of validation.missingSections) {
// Add missing section at end
fixed += `\n\n## ${section}\n[To be completed]`;
}
return {
response: fixed,
status: 'fixed',
fixes: validation.suggestions
};
}
/**
* Get format requirements as system prompt addition
*/
getFormatPrompt() {
const template = this.getTemplate();
return `
## Output Format Requirements
You MUST follow this structure exactly:
${template.format}
### Required Sections
${template.requiredSections.map(s => `- ${s}`).join('\n')}
### Rules
- All required sections must be present
- Use proper markdown tables where shown
- Include checkboxes for verification items
- Be specific, not vague
`;
}
}
// Export for use
module.exports = FormatLocker;
// CLI usage
if (require.main === module) {
const workflow = process.argv[2] || 'coding';
const responseFile = process.argv[3];
const locker = new FormatLocker(workflow);
console.log('=== Format Template ===');
console.log(locker.getTemplate().format);
if (responseFile) {
const fs = require('fs');
const response = fs.readFileSync(responseFile, 'utf8');
console.log('\n=== Validation ===');
const result = locker.enforceFormat(response, true);
console.log('Status:', result.status);
if (result.fixes) {
console.log('Fixes:', result.fixes);
}
}
}