349 lines
8.8 KiB
JavaScript
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);
|
|
}
|
|
}
|
|
}
|