""" 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), }