orchestrata_ultimate.orchestrator.nlp
Reference › orchestrata_ultimate › orchestrator › nlp
The text-processing layer under the natural-language intent path. Its only importer is intent_classifier, and it is traced to spec 003 story US-001. It has no external dependencies beyond the traceability decorator; everything is regular-expression based.
Tokenizer.tokenize splits text into Token records classified by TokenType (word, number, path, operator, punctuation, whitespace, string, glob, url, flag); tokenize_sentences splits sentences and get_token_types groups tokens by type. NamedEntityRecognizer.recognize finds typed NamedEntity spans (EntityType: file path, directory, package, command, agent, number, duration, size, percentage, url, git ref, variable) with a confidence score, running URL, file path, agent alias (the U: volk aliases), git ref, measurement and variable recognizers in turn and skipping any span that overlaps one already found. extract_by_type filters by entity type.
Context handling is in SessionContext, a dataclass holding the session id, current directory, recent files and commands (each capped at 20), active agent, variables and a bounded history (50 by default), and ContextAwareParser. Its parse_with_context tokenizes, recognizes entities, and replaces pronoun references such as "it" with the most recent file or command from the session, returning a dictionary with the original and resolved text, the tokens, the entities, the references resolved and a context_used flag. has_context_references tells a caller whether the text needs that step.
At a glance
| Source | src/orchestrata_ultimate/orchestrator/nlp.py |
| Lines of code | 604 |
| Classes | 8 |
| Functions | 0 |
| Imports (internal) | orchestrata_ultimate.core.base.traces |
| Imported by | orchestrata_ultimate.orchestrator.intent_classifier |
| Tests that import it | tests/generated_orchestrator/test_nlp_generated.py, tests/orchestrator/test_coverage_boost.py, tests/orchestrator/test_intent_parser_behavioral.py, tests/orchestrator/test_nlp_coverage.py, tests/test_orchestrator.py |
Class diagram
Dependency diagram
API
Classes
TokenType (enum)
Bases: Enum · Decorators: traces(spec='003', story='US-001', reqs=['REQ-003-US-001-012']) · Line 24
Token classification types.
| Member | Value |
|---|---|
WORD |
'word' |
NUMBER |
'number' |
PATH |
'path' |
OPERATOR |
'operator' |
PUNCTUATION |
'punctuation' |
WHITESPACE |
'whitespace' |
STRING |
'string' |
GLOB |
'glob' |
URL |
'url' |
FLAG |
'flag' |
Token (dataclass)
Decorators: dataclass, traces(spec='003', story='US-001', reqs=['REQ-003-US-001-012']) · Line 41
A single token from text input.
| Attribute | Type | Default |
|---|---|---|
text |
str |
|
token_type |
TokenType |
|
start |
int |
|
end |
int |
|
normalized |
str |
'' |
| Method | Signature | Summary |
|---|---|---|
__post_init__ |
__post_init__() -> None |
Tokenizer
Decorators: traces(spec='003', story='US-001', reqs=['REQ-003-US-001-008', 'REQ-003-US-001-012', 'REQ-003-US-001-019', 'REQ-003-US-… · Line 66
Tokenizes natural language input for intent parsing.
| Method | Signature | Summary |
|---|---|---|
tokenize |
tokenize(text: str) -> list[Token] |
Tokenize text into classified tokens. |
tokenize_sentences |
tokenize_sentences(text: str) -> list[str] |
Split text into sentences. |
get_token_types |
get_token_types(text: str) -> dict[TokenType, list[str]] |
Group tokens by their type. |
EntityType (enum)
Bases: Enum · Decorators: traces(spec='003', story='US-001', reqs=['REQ-003-US-001-015']) · Line 160
Named entity types for intent extraction.
| Member | Value |
|---|---|
FILE_PATH |
'file_path' |
DIRECTORY |
'directory' |
PACKAGE |
'package' |
COMMAND |
'command' |
AGENT |
'agent' |
NUMBER |
'number' |
DURATION |
'duration' |
SIZE |
'size' |
PERCENTAGE |
'percentage' |
URL |
'url' |
GIT_REF |
'git_ref' |
VARIABLE |
'variable' |
NamedEntity (dataclass)
Decorators: dataclass, traces(spec='003', story='US-001', reqs=['REQ-003-US-001-015']) · Line 179
A recognized named entity from text.
| Attribute | Type | Default |
|---|---|---|
text |
str |
|
entity_type |
EntityType |
|
start |
int |
|
end |
int |
|
confidence |
float |
1.0 |
metadata |
dict[str, Any] |
field(default_factory=dict) |
NamedEntityRecognizer
Decorators: traces(spec='003', story='US-001', reqs=['REQ-003-US-001-015', 'REQ-003-US-001-024']) · Line 195
Extracts named entities from natural language intent text.
| Method | Signature | Summary |
|---|---|---|
recognize |
recognize(text: str) -> list[NamedEntity] |
Extract all named entities from text. |
_recognize_urls |
_recognize_urls(text: str, entities: list[NamedEntity]) -> None |
Recognize URLs (matched first to shadow paths inside them). |
_recognize_file_paths |
_recognize_file_paths(text: str, entities: list[NamedEntity]) -> None |
Recognize file paths, skipping spans already captured (e.g. |
_recognize_agents |
_recognize_agents(text: str, entities: list[NamedEntity]) -> None |
Recognize known agent aliases (e.g. |
_recognize_git_refs |
_recognize_git_refs(text: str, entities: list[NamedEntity]) -> None |
Recognize git refs not already captured as another entity. |
_recognize_measurements |
_recognize_measurements(text: str, entities: list[NamedEntity]) -> None |
Recognize durations, sizes, and percentages with numeric metadata. |
_recognize_variables |
_recognize_variables(text: str, entities: list[NamedEntity]) -> None |
Recognize UPPER_SNAKE_CASE variables not already captured. |
_overlaps |
_overlaps(entities: list[NamedEntity], start: int, end: int) -> bool |
Check if a span overlaps with any existing entity. |
extract_by_type |
extract_by_type(text: str, entity_type: EntityType) -> list[NamedEntity] |
Extract entities of a specific type. |
SessionContext (dataclass)
Decorators: dataclass, traces(spec='003', story='US-001', reqs=['REQ-003-US-001-009', 'REQ-003-US-001-020']) · Line 423
Session state for context-aware parsing.
| Attribute | Type | Default |
|---|---|---|
session_id |
str |
'' |
current_directory |
str |
'' |
recent_files |
list[str] |
field(default_factory=list) |
recent_commands |
list[str] |
field(default_factory=list) |
active_agent |
str |
'' |
variables |
dict[str, Any] |
field(default_factory=dict) |
history |
list[str] |
field(default_factory=list) |
max_history |
int |
50 |
| Method | Signature | Summary |
|---|---|---|
add_to_history |
add_to_history(text: str) -> None |
|
add_recent_file |
add_recent_file(path: str) -> None |
|
add_recent_command |
add_recent_command(cmd: str) -> None |
ContextAwareParser
Decorators: traces(spec='003', story='US-001', reqs=['REQ-003-US-001-009', 'REQ-003-US-001-020']) · Line 460
Enhances intent parsing with session state context.
| Method | Signature | Summary |
|---|---|---|
__init__ |
__init__() -> None |
|
parse_with_context |
parse_with_context(text: str, context: SessionContext) -> dict[str, Any] |
Parse text with session context for disambiguation. |
_resolve_pronoun |
_resolve_pronoun(pattern: re.Pattern, text: str, context: SessionContext) -> str | None |
Resolve a pronoun reference using session context. |
has_context_references |
has_context_references(text: str) -> bool |
Check if text contains references that need context resolution. |
Source
"""NLP processing for intent parsing.
Provides tokenization, named entity recognition, and context-aware
parsing for natural language intent extraction.
Spec: 003-intent / US-001
"""
import logging
import re
from dataclasses import dataclass, field
from enum import Enum
from typing import Any
from orchestrata_ultimate.core.base.traces import traces
logger = logging.getLogger(__name__)
# ── Tokenization (REQ-003-US-001-012) ────────────────────────────────────
@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-012"])
class TokenType(Enum):
"""Token classification types."""
WORD = "word"
NUMBER = "number"
PATH = "path"
OPERATOR = "operator"
PUNCTUATION = "punctuation"
WHITESPACE = "whitespace"
STRING = "string"
GLOB = "glob"
URL = "url"
FLAG = "flag"
@dataclass
@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-012"])
class Token:
"""A single token from text input."""
text: str
token_type: TokenType
start: int
end: int
normalized: str = ""
@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-012"])
def __post_init__(self) -> None:
if not self.normalized:
self.normalized = self.text.lower().strip()
@traces(
spec="003",
story="US-001",
reqs=[
"REQ-003-US-001-008",
"REQ-003-US-001-012",
"REQ-003-US-001-019",
"REQ-003-US-001-024",
],
)
class Tokenizer:
"""Tokenizes natural language input for intent parsing.
Recognizes domain-specific tokens: file paths, glob patterns,
URLs, CLI flags, numbers with units, and natural language words.
"""
# Ordered patterns — first match wins
_PATTERNS = [
(TokenType.URL, re.compile(r"https?://[^\s]+")),
(
TokenType.PATH,
re.compile(
r"(?:~|\.{1,2})?/(?:[a-zA-Z0-9_.\-]+/)*[a-zA-Z0-9_.\-]+(?:\.[a-zA-Z0-9]+)?"
),
),
(TokenType.GLOB, re.compile(r"(?:\*\*?/[^\s]+|[^\s]*\*[^\s]*\.[a-zA-Z0-9]+)")),
(TokenType.FLAG, re.compile(r"--?[a-zA-Z][a-zA-Z0-9\-]*(?:=[^\s]+)?")),
(TokenType.STRING, re.compile(r'"[^"]*"|\'[^\']*\'')),
(
TokenType.NUMBER,
re.compile(
r"\d+(?:\.\d+)?(?:\s*(?:ms|seconds?|secs?|minutes?|mins?|hours?|hrs?|days?|bytes?|[kKmMgGtT][bB]|%))?"
),
),
(TokenType.OPERATOR, re.compile(r"[=<>!]=?|&&|\|\||[|&;]")),
(TokenType.PUNCTUATION, re.compile(r"[.,;:!?(){}\[\]]")),
(TokenType.WORD, re.compile(r"[a-zA-Z_][a-zA-Z0-9_]*")),
(TokenType.WHITESPACE, re.compile(r"\s+")),
]
@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-012"])
def tokenize(self, text: str) -> list[Token]:
"""Tokenize text into classified tokens.
Args:
text: Raw input text to tokenize.
Returns:
List of Token objects (whitespace tokens excluded).
"""
if not text:
return []
tokens = []
pos = 0
while pos < len(text):
matched = False
for token_type, pattern in self._PATTERNS:
m = pattern.match(text, pos)
if m:
if token_type != TokenType.WHITESPACE:
tokens.append(
Token(
text=m.group(),
token_type=token_type,
start=m.start(),
end=m.end(),
)
)
pos = m.end()
matched = True
break
if not matched:
# Skip unrecognized character
pos += 1
return tokens
@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-012"])
def tokenize_sentences(self, text: str) -> list[str]:
"""Split text into sentences."""
if not text:
return []
# Split on sentence-ending punctuation followed by space or end
sentences = re.split(r"(?<=[.!?])\s+", text.strip())
return [s.strip() for s in sentences if s.strip()]
@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-012"])
def get_token_types(self, text: str) -> dict[TokenType, list[str]]:
"""Group tokens by their type."""
tokens = self.tokenize(text)
groups: dict[TokenType, list[str]] = {}
for token in tokens:
groups.setdefault(token.token_type, []).append(token.text)
return groups
# ── Named Entity Recognition (REQ-003-US-001-015) ───────────────────
@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-015"])
class EntityType(Enum):
"""Named entity types for intent extraction."""
FILE_PATH = "file_path"
DIRECTORY = "directory"
PACKAGE = "package"
COMMAND = "command"
AGENT = "agent"
NUMBER = "number"
DURATION = "duration"
SIZE = "size"
PERCENTAGE = "percentage"
URL = "url"
GIT_REF = "git_ref"
VARIABLE = "variable"
@dataclass
@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-015"])
class NamedEntity:
"""A recognized named entity from text."""
text: str
entity_type: EntityType
start: int
end: int
confidence: float = 1.0
metadata: dict[str, Any] = field(default_factory=dict)
@traces(
spec="003",
story="US-001",
reqs=["REQ-003-US-001-015", "REQ-003-US-001-024"],
)
class NamedEntityRecognizer:
"""Extracts named entities from natural language intent text.
Uses pattern-based recognition for domain-specific entities:
file paths, packages, commands, agents, and value types.
"""
# Agent aliases
_AGENT_PATTERN = re.compile(r"\bU:[A-Z][a-z]+\b")
_AGENT_NAMES = frozenset(
{"U:Ox", "U:PS", "U:Cd", "U:Ux", "U:PM", "U:Ar", "U:Qa", "U:Cx"}
)
# Git refs
_GIT_REF_PATTERN = re.compile(
r"\b(?:main|master|develop|HEAD|[a-f0-9]{7,40}|v\d+\.\d+(?:\.\d+)?)\b"
)
# Package names (PyPI-style)
_PACKAGE_PATTERN = re.compile(r"\b[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*\b")
# Command patterns
_COMMAND_KEYWORDS = frozenset(
{
"run",
"execute",
"start",
"stop",
"restart",
"install",
"uninstall",
"build",
"test",
"deploy",
"lint",
"format",
"check",
"migrate",
"create",
"delete",
"update",
"list",
"show",
"describe",
}
)
# Duration patterns
_DURATION_RE = re.compile(
r"\b(\d+(?:\.\d+)?)\s*(ms|seconds?|secs?|minutes?|mins?|hours?|hrs?|days?)\b",
re.IGNORECASE,
)
# Size patterns
_SIZE_RE = re.compile(
r"\b(\d+(?:\.\d+)?)\s*([kKmMgGtT][bB]|bytes?)\b", re.IGNORECASE
)
# Percentage
_PERCENT_RE = re.compile(r"\b(\d+(?:\.\d+)?)\s*%")
# URL
_URL_RE = re.compile(r"https?://[^\s]+")
# File path
_FILE_PATH_RE = re.compile(
r"(?:~|\.{1,2})?/(?:[a-zA-Z0-9_.\-]+/)*[a-zA-Z0-9_.\-]+(?:\.[a-zA-Z0-9]+)?"
)
# Variable/identifier (UPPER_SNAKE_CASE)
_VARIABLE_RE = re.compile(r"\b[A-Z][A-Z0-9_]{2,}\b")
@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-015"])
def recognize(self, text: str) -> list[NamedEntity]:
"""Extract all named entities from text.
Args:
text: Natural language text to analyze.
Returns:
List of NamedEntity objects sorted by position.
"""
if not text:
return []
entities: list[NamedEntity] = []
# URLs first so later file-path matches inside a URL are skipped.
self._recognize_urls(text, entities)
self._recognize_file_paths(text, entities)
self._recognize_agents(text, entities)
self._recognize_git_refs(text, entities)
self._recognize_measurements(text, entities)
self._recognize_variables(text, entities)
entities.sort(key=lambda e: e.start)
return entities
@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-015"])
def _recognize_urls(self, text: str, entities: list[NamedEntity]) -> None:
"""Recognize URLs (matched first to shadow paths inside them)."""
for m in self._URL_RE.finditer(text):
entities.append(
NamedEntity(
text=m.group(),
entity_type=EntityType.URL,
start=m.start(),
end=m.end(),
)
)
@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-015"])
def _recognize_file_paths(self, text: str, entities: list[NamedEntity]) -> None:
"""Recognize file paths, skipping spans already captured (e.g. URLs)."""
for m in self._FILE_PATH_RE.finditer(text):
if not self._overlaps(entities, m.start(), m.end()):
entities.append(
NamedEntity(
text=m.group(),
entity_type=EntityType.FILE_PATH,
start=m.start(),
end=m.end(),
)
)
@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-015"])
def _recognize_agents(self, text: str, entities: list[NamedEntity]) -> None:
"""Recognize known agent aliases (e.g. U:Ox)."""
for m in self._AGENT_PATTERN.finditer(text):
if m.group() in self._AGENT_NAMES:
entities.append(
NamedEntity(
text=m.group(),
entity_type=EntityType.AGENT,
start=m.start(),
end=m.end(),
)
)
@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-015"])
def _recognize_git_refs(self, text: str, entities: list[NamedEntity]) -> None:
"""Recognize git refs not already captured as another entity."""
for m in self._GIT_REF_PATTERN.finditer(text):
if not self._overlaps(entities, m.start(), m.end()):
entities.append(
NamedEntity(
text=m.group(),
entity_type=EntityType.GIT_REF,
start=m.start(),
end=m.end(),
confidence=0.8,
)
)
@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-015"])
def _recognize_measurements(self, text: str, entities: list[NamedEntity]) -> None:
"""Recognize durations, sizes, and percentages with numeric metadata."""
for m in self._DURATION_RE.finditer(text):
entities.append(
NamedEntity(
text=m.group(),
entity_type=EntityType.DURATION,
start=m.start(),
end=m.end(),
metadata={"value": float(m.group(1)), "unit": m.group(2)},
)
)
for m in self._SIZE_RE.finditer(text):
entities.append(
NamedEntity(
text=m.group(),
entity_type=EntityType.SIZE,
start=m.start(),
end=m.end(),
metadata={"value": float(m.group(1)), "unit": m.group(2)},
)
)
for m in self._PERCENT_RE.finditer(text):
entities.append(
NamedEntity(
text=m.group(),
entity_type=EntityType.PERCENTAGE,
start=m.start(),
end=m.end(),
metadata={"value": float(m.group(1))},
)
)
@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-015"])
def _recognize_variables(self, text: str, entities: list[NamedEntity]) -> None:
"""Recognize UPPER_SNAKE_CASE variables not already captured."""
for m in self._VARIABLE_RE.finditer(text):
if not self._overlaps(entities, m.start(), m.end()):
entities.append(
NamedEntity(
text=m.group(),
entity_type=EntityType.VARIABLE,
start=m.start(),
end=m.end(),
confidence=0.7,
)
)
@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-015"])
def _overlaps(self, entities: list[NamedEntity], start: int, end: int) -> bool:
"""Check if a span overlaps with any existing entity."""
for e in entities:
if start < e.end and end > e.start:
return True
return False
@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-015"])
def extract_by_type(self, text: str, entity_type: EntityType) -> list[NamedEntity]:
"""Extract entities of a specific type."""
return [e for e in self.recognize(text) if e.entity_type == entity_type]
# ── Context-Aware Parsing (REQ-003-US-001-020) ──────────────────────
@dataclass
@traces(
spec="003",
story="US-001",
reqs=["REQ-003-US-001-009", "REQ-003-US-001-020"],
)
class SessionContext:
"""Session state for context-aware parsing."""
session_id: str = ""
current_directory: str = ""
recent_files: list[str] = field(default_factory=list)
recent_commands: list[str] = field(default_factory=list)
active_agent: str = ""
variables: dict[str, Any] = field(default_factory=dict)
history: list[str] = field(default_factory=list)
max_history: int = 50
@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-020"])
def add_to_history(self, text: str) -> None:
self.history.append(text)
if len(self.history) > self.max_history:
self.history = self.history[-self.max_history :]
@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-020"])
def add_recent_file(self, path: str) -> None:
if path not in self.recent_files:
self.recent_files.append(path)
if len(self.recent_files) > 20:
self.recent_files = self.recent_files[-20:]
@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-020"])
def add_recent_command(self, cmd: str) -> None:
self.recent_commands.append(cmd)
if len(self.recent_commands) > 20:
self.recent_commands = self.recent_commands[-20:]
@traces(
spec="003",
story="US-001",
reqs=["REQ-003-US-001-009", "REQ-003-US-001-020"],
)
class ContextAwareParser:
"""Enhances intent parsing with session state context.
Resolves ambiguous references using session history, recent files,
and current working directory. Expands pronouns like "it", "that file",
"the same" to concrete references.
"""
# Pronoun patterns that reference previous context
_PRONOUN_PATTERNS = [
re.compile(r"\b(?:it|this|that)\b", re.IGNORECASE),
re.compile(r"\bthat\s+file\b", re.IGNORECASE),
re.compile(r"\bthe\s+same\s+(?:file|command|thing)\b", re.IGNORECASE),
re.compile(r"\bthere\b", re.IGNORECASE),
re.compile(r"\blast\s+(?:file|command|result)\b", re.IGNORECASE),
]
# Relative path indicators
_RELATIVE_PATTERNS = [
re.compile(r"\bhere\b", re.IGNORECASE),
re.compile(r"\bcurrent\s+(?:dir|directory|folder)\b", re.IGNORECASE),
re.compile(r"\bthis\s+(?:dir|directory|folder)\b", re.IGNORECASE),
]
@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-020"])
def __init__(self) -> None:
self._tokenizer = Tokenizer()
self._ner = NamedEntityRecognizer()
@traces(
spec="003",
story="US-001",
reqs=["REQ-003-US-001-009", "REQ-003-US-001-020"],
)
def parse_with_context(
self,
text: str,
context: SessionContext,
) -> dict[str, Any]:
"""Parse text with session context for disambiguation.
Args:
text: Natural language input.
context: Current session state.
Returns:
Dict with resolved references and extracted info.
"""
result: dict[str, Any] = {
"original_text": text,
"resolved_text": text,
"tokens": [],
"entities": [],
"resolved_references": [],
"context_used": False,
}
# Tokenize
tokens = self._tokenizer.tokenize(text)
result["tokens"] = [
{"text": t.text, "type": t.token_type.value} for t in tokens
]
# NER
entities = self._ner.recognize(text)
result["entities"] = [
{"text": e.text, "type": e.entity_type.value, "confidence": e.confidence}
for e in entities
]
# Resolve pronoun references
resolved_text = text
for pattern in self._PRONOUN_PATTERNS:
if pattern.search(resolved_text):
replacement = self._resolve_pronoun(pattern, resolved_text, context)
if replacement:
resolved_text = pattern.sub(replacement, resolved_text, count=1)
result["resolved_references"].append(
{
"pattern": pattern.pattern,
"resolved_to": replacement,
}
)
result["context_used"] = True
# Resolve relative paths
for pattern in self._RELATIVE_PATTERNS:
if pattern.search(resolved_text) and context.current_directory:
resolved_text = pattern.sub(
context.current_directory, resolved_text, count=1
)
result["resolved_references"].append(
{
"pattern": pattern.pattern,
"resolved_to": context.current_directory,
}
)
result["context_used"] = True
result["resolved_text"] = resolved_text
# Add to history
context.add_to_history(text)
return result
@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-020"])
def _resolve_pronoun(
self,
pattern: re.Pattern,
text: str,
context: SessionContext,
) -> str | None:
"""Resolve a pronoun reference using session context."""
# Find what was actually matched in text to determine resolution target
m = pattern.search(text)
matched_text = m.group().lower() if m else ""
# "that file" / "the same file" / "last file" -> most recent file
if "file" in matched_text:
if context.recent_files:
return context.recent_files[-1]
return None
# "last command" / "the same command" -> most recent command
if "command" in matched_text:
if context.recent_commands:
return context.recent_commands[-1]
return None
# Generic "it" / "this" / "that" / "there" -> try recent file, then command
if context.recent_files:
return context.recent_files[-1]
if context.recent_commands:
return context.recent_commands[-1]
return None
@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-020"])
def has_context_references(self, text: str) -> bool:
"""Check if text contains references that need context resolution."""
for pattern in self._PRONOUN_PATTERNS + self._RELATIVE_PATTERNS:
if pattern.search(text):
return True
return False