90 lines
3.4 KiB
YAML
90 lines
3.4 KiB
YAML
# discussion-validator - Validate discussion format and check for issues
|
|
# Usage: cat discussion.md | discussion-validator | jq .
|
|
|
|
name: discussion-validator
|
|
description: Validate discussion format and check for issues
|
|
category: Discussion
|
|
|
|
steps:
|
|
- type: code
|
|
code: |
|
|
import re
|
|
import json
|
|
|
|
issues = []
|
|
warnings = []
|
|
|
|
# Check for DISCUSSION marker
|
|
if not re.search(r'<!--\s*DISCUSSION\s*-->', input, re.IGNORECASE):
|
|
issues.append("Missing <!-- DISCUSSION --> marker")
|
|
|
|
# Check for required headers
|
|
required_headers = ['Title', 'Phase', 'Status']
|
|
for header in required_headers:
|
|
if not re.search(rf'<!--\s*{header}:\s*.+?\s*-->', input, re.IGNORECASE):
|
|
issues.append(f"Missing required header: {header}")
|
|
|
|
# Check for valid phase
|
|
phase_match = re.search(r'<!--\s*Phase:\s*(\w+)\s*-->', input, re.IGNORECASE)
|
|
if phase_match:
|
|
valid_phases = [
|
|
'initial_feedback', 'detailed_review', 'consensus_vote',
|
|
'final_vote', 'signoff', 'completed', 'proposal', 'review'
|
|
]
|
|
phase = phase_match.group(1)
|
|
# Build lowercase list explicitly to avoid exec() scope issues
|
|
valid_phases_lower = []
|
|
for p in valid_phases:
|
|
valid_phases_lower.append(p.lower())
|
|
if phase.lower() not in valid_phases_lower:
|
|
warnings.append(f"Unknown phase: {phase}")
|
|
|
|
# Check for valid status
|
|
status_match = re.search(r'<!--\s*Status:\s*(\w+)\s*-->', input, re.IGNORECASE)
|
|
if status_match:
|
|
valid_statuses = [
|
|
'OPEN', 'PROPOSED', 'READY_FOR_DESIGN', 'READY_FOR_IMPLEMENTATION',
|
|
'READY_FOR_TESTING', 'CLOSED', 'REJECTED'
|
|
]
|
|
status = status_match.group(1).upper()
|
|
if status not in valid_statuses:
|
|
warnings.append(f"Unknown status: {status}")
|
|
|
|
# Check for orphaned mentions (mentioned but no response)
|
|
mentions = set(re.findall(r'@(\w+)', input))
|
|
# Get responders from Name: lines
|
|
responders = set()
|
|
for match in re.finditer(r'^Name:\s*(?:AI-)?(\w+)', input, re.MULTILINE | re.IGNORECASE):
|
|
responders.add(match.group(1).lower())
|
|
|
|
pending = mentions - responders - {'all'}
|
|
if pending:
|
|
warnings.append(f"Pending responses from: {', '.join(sorted(pending))}")
|
|
|
|
# Check for empty comment blocks
|
|
for match in re.finditer(r'^---\s*\n\s*Name:\s*(.+?)\n\s*(?=^---|\Z)', input, re.MULTILINE):
|
|
author = match.group(1).strip()
|
|
warnings.append(f"Empty comment block from: {author}")
|
|
|
|
# Check for duplicate votes from same author
|
|
votes = {}
|
|
for match in re.finditer(r'^---\s*\nName:\s*(.+?)\n.*?VOTE:\s*(READY|CHANGES|REJECT)', input, re.MULTILINE | re.DOTALL | re.IGNORECASE):
|
|
author = match.group(1).strip()
|
|
vote = match.group(2).upper()
|
|
if author in votes and votes[author] != vote:
|
|
warnings.append(f"Multiple different votes from {author}: {votes[author]} -> {vote}")
|
|
votes[author] = vote
|
|
|
|
validation = json.dumps({
|
|
"valid": len(issues) == 0,
|
|
"issues": issues,
|
|
"warnings": warnings,
|
|
"metadata_found": {
|
|
"phase": phase_match.group(1) if phase_match else None,
|
|
"status": status_match.group(1) if status_match else None
|
|
}
|
|
}, indent=2)
|
|
output_var: validation
|
|
|
|
output: "{validation}"
|