orchestrated-discussions/src/discussions/markers.py

257 lines
6.4 KiB
Python

"""
Marker parsing for Orchestrated Discussions.
This module handles parsing of structured markers in discussion content:
- VOTE: READY|CHANGES|REJECT
- Q: / QUESTION: - Questions
- TODO: / ACTION: - Action items
- DECISION: - Decisions
- ASSIGNED: - Claimed tasks
- DONE: - Completed tasks
- CONCERN: - Raised concerns
- DIAGRAM: - Diagram references (path to .puml file)
- @alias - Mentions
See docs/DESIGN.md for full marker specification.
"""
import re
from dataclasses import dataclass
from typing import Optional
# Regex patterns for marker extraction
VOTE_PATTERN = re.compile(r'^VOTE:\s*(READY|CHANGES|REJECT)\s*$', re.IGNORECASE | re.MULTILINE)
QUESTION_PATTERN = re.compile(r'^(?:Q|QUESTION):\s*(.+)$', re.IGNORECASE | re.MULTILINE)
TODO_PATTERN = re.compile(r'^(?:TODO|ACTION):\s*(.+)$', re.IGNORECASE | re.MULTILINE)
DECISION_PATTERN = re.compile(r'^DECISION:\s*(.+)$', re.IGNORECASE | re.MULTILINE)
ASSIGNED_PATTERN = re.compile(r'^ASSIGNED:\s*(.+)$', re.IGNORECASE | re.MULTILINE)
DONE_PATTERN = re.compile(r'^DONE:\s*(.+)$', re.IGNORECASE | re.MULTILINE)
CONCERN_PATTERN = re.compile(r'^CONCERN:\s*(.+)$', re.IGNORECASE | re.MULTILINE)
DIAGRAM_PATTERN = re.compile(r'^DIAGRAM:\s*(.+)$', re.IGNORECASE | re.MULTILINE)
MENTION_PATTERN = re.compile(r'@(\w+)')
@dataclass
class Question:
"""A question raised in the discussion."""
text: str
author: str
status: str = "open" # open, answered, deferred
@dataclass
class ActionItem:
"""An action item or TODO."""
text: str
author: str
assignee: Optional[str] = None
status: str = "todo" # todo, assigned, done
@dataclass
class Decision:
"""A decision made in the discussion."""
text: str
author: str
supporters: list[str] = None
def __post_init__(self):
if self.supporters is None:
self.supporters = []
@dataclass
class Concern:
"""A concern raised by a participant."""
text: str
author: str
addressed: bool = False
@dataclass
class Diagram:
"""A diagram reference in the discussion."""
path: str # Path to the diagram file
author: str
@dataclass
class Mention:
"""An @mention in the discussion."""
target: str # The alias mentioned
author: str
context: str # Surrounding text
def extract_vote(text: str) -> Optional[str]:
"""
Extract vote from text.
Args:
text: Text to search for vote
Returns:
Vote value (READY, CHANGES, REJECT) or None
"""
match = VOTE_PATTERN.search(text)
if match:
return match.group(1).upper()
return None
def extract_questions(text: str, author: str = "unknown") -> list[Question]:
"""
Extract all questions from text.
Args:
text: Text to search
author: Author to attribute questions to
Returns:
List of Question objects
"""
questions = []
for match in QUESTION_PATTERN.finditer(text):
questions.append(Question(
text=match.group(1).strip(),
author=author
))
return questions
def extract_action_items(text: str, author: str = "unknown") -> list[ActionItem]:
"""
Extract all action items/TODOs from text.
Args:
text: Text to search
author: Author to attribute items to
Returns:
List of ActionItem objects
"""
items = []
for match in TODO_PATTERN.finditer(text):
item_text = match.group(1).strip()
# Check for @mention to determine assignee
mention = MENTION_PATTERN.search(item_text)
assignee = mention.group(1) if mention else None
items.append(ActionItem(
text=item_text,
author=author,
assignee=assignee
))
return items
def extract_decisions(text: str, author: str = "unknown") -> list[Decision]:
"""
Extract all decisions from text.
Args:
text: Text to search
author: Author to attribute decisions to
Returns:
List of Decision objects
"""
decisions = []
for match in DECISION_PATTERN.finditer(text):
decisions.append(Decision(
text=match.group(1).strip(),
author=author
))
return decisions
def extract_concerns(text: str, author: str = "unknown") -> list[Concern]:
"""
Extract all concerns from text.
Args:
text: Text to search
author: Author to attribute concerns to
Returns:
List of Concern objects
"""
concerns = []
for match in CONCERN_PATTERN.finditer(text):
concerns.append(Concern(
text=match.group(1).strip(),
author=author
))
return concerns
def extract_diagrams(text: str, author: str = "unknown") -> list[Diagram]:
"""
Extract all diagram references from text.
Args:
text: Text to search
author: Author to attribute diagrams to
Returns:
List of Diagram objects
"""
diagrams = []
for match in DIAGRAM_PATTERN.finditer(text):
diagrams.append(Diagram(
path=match.group(1).strip(),
author=author
))
return diagrams
def extract_mentions(text: str, author: str = "unknown") -> list[Mention]:
"""
Extract all @mentions from text.
Args:
text: Text to search
author: Author making the mentions
Returns:
List of Mention objects
"""
mentions = []
for match in MENTION_PATTERN.finditer(text):
# Get surrounding context (the line containing the mention)
start = text.rfind('\n', 0, match.start()) + 1
end = text.find('\n', match.end())
if end == -1:
end = len(text)
context = text[start:end].strip()
mentions.append(Mention(
target=match.group(1),
author=author,
context=context
))
return mentions
def extract_all_markers(text: str, author: str = "unknown") -> dict:
"""
Extract all markers from text.
Args:
text: Text to search
author: Author to attribute markers to
Returns:
Dict with keys: vote, questions, action_items, decisions, concerns, mentions
"""
return {
"vote": extract_vote(text),
"questions": extract_questions(text, author),
"action_items": extract_action_items(text, author),
"decisions": extract_decisions(text, author),
"concerns": extract_concerns(text, author),
"diagrams": extract_diagrams(text, author),
"mentions": extract_mentions(text, author),
}