- Convert legacy [DEF🆔Type] anchors to #region/#endregion across 329 files
- Reinstate _normalize_timestamp_value in sql_generator.py
- Fix MarkerLogger→logger migration in events.py (molecular CoT markers)
- Fix dataset_review orchestrator dependencies (_build_execution_snapshot)
- Fix config_manager stale-record deletion (moved to save path only)
- Add 77 missing [/DEF:] closers in 5 unbalanced test files
- Update assistant_chat.integration.test.js for #region format
- Apply molecular-cot-logging markers (REASON/REFLECT/EXPLORE) via logger.* methods
233 lines
7.4 KiB
Python
233 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Convert legacy # [DEF:...] / # [/DEF:...] annotations to #region/#endregion format.
|
|
|
|
ONLY modifies comment lines (starting with '#').
|
|
NEVER touches code, strings, imports, or non-comment lines.
|
|
Preserves original line endings (CRLF vs LF).
|
|
"""
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
SRC_DIR = Path(__file__).resolve().parent / "src"
|
|
|
|
|
|
def has_crlf(content: bytes) -> bool:
|
|
"""Check if content uses CRLF line endings."""
|
|
return b'\r\n' in content
|
|
|
|
|
|
def process_file(filepath: Path) -> tuple[int, int]:
|
|
"""Process a single file. Returns (blocks_converted, metadata_merged)."""
|
|
# Read as bytes to detect original line endings
|
|
with open(filepath, "rb") as f:
|
|
raw_bytes = f.read()
|
|
|
|
original_crlf = has_crlf(raw_bytes)
|
|
|
|
# Decode text (Python converts CRLF to LF automatically on decode in text mode,
|
|
# but we decoded from bytes, so handle it manually)
|
|
text = raw_bytes.decode('utf-8')
|
|
if original_crlf:
|
|
text = text.replace('\r\n', '\n')
|
|
|
|
lines = text.split('\n')
|
|
|
|
result = []
|
|
blocks_converted = 0
|
|
metadata_merged = 0
|
|
i = 0
|
|
n = len(lines)
|
|
|
|
while i < n:
|
|
line = lines[i]
|
|
stripped = line.rstrip('\r')
|
|
|
|
# Check for [SECTION: ...] or [/SECTION] lines
|
|
if re.match(r'^#\s*\[SECTION:\s*.*\]\s*$', stripped):
|
|
i += 1
|
|
continue
|
|
if re.match(r'^#\s*\[/SECTION\]\s*$', stripped):
|
|
i += 1
|
|
continue
|
|
|
|
# Check for [/DEF:Name] or [/DEF:Name:Type]
|
|
m = re.match(r'^#\s*\[/DEF:(\w+(?:\.\w+)*)(?::\w+)?\]\s*$', stripped)
|
|
if m:
|
|
name = m.group(1)
|
|
result.append(f"# #endregion {name}")
|
|
blocks_converted += 1
|
|
i += 1
|
|
continue
|
|
|
|
# Check for [DEF:Name:Type]
|
|
m = re.match(r'^#\s*\[DEF:(\w+(?:\.\w+)*):(\w+)\]\s*$', stripped)
|
|
if m:
|
|
name = m.group(1)
|
|
typ = m.group(2)
|
|
|
|
# Collect subsequent metadata lines
|
|
complexity = None
|
|
semantics = None
|
|
metadata_lines = [] # other metadata lines to preserve
|
|
j = i + 1
|
|
|
|
while j < n:
|
|
nxt = lines[j].rstrip('\r')
|
|
|
|
# Stop if not a comment
|
|
if not nxt.startswith('#'):
|
|
break
|
|
# Stop if it's another DEF or SECTION
|
|
if re.match(r'^#\s*\[', nxt):
|
|
break
|
|
# Skip blank comment lines (just '#') but continue collecting
|
|
if re.match(r'^#\s*$', nxt):
|
|
metadata_lines.append(lines[j])
|
|
j += 1
|
|
continue
|
|
|
|
# Check for @COMPLEXITY
|
|
cm = re.match(r'^#\s*@COMPLEXITY:\s*(\d+)\s*$', nxt)
|
|
if cm:
|
|
complexity = int(cm.group(1))
|
|
j += 1
|
|
continue
|
|
|
|
# Check for @SEMANTICS
|
|
sm = re.match(r'^#\s*@SEMANTICS:\s*(.+?)\s*$', nxt)
|
|
if sm:
|
|
semantics = sm.group(1).strip()
|
|
j += 1
|
|
continue
|
|
|
|
# Skip @PARAM, @RETURN, @THROW
|
|
if re.match(r'^#\s*@(PARAM|RETURN|THROW)\b', nxt):
|
|
j += 1
|
|
continue
|
|
|
|
# Convert @PURPOSE -> @BRIEF
|
|
pm = re.match(r'^#\s*@PURPOSE:\s*(.*?)\s*$', nxt)
|
|
if pm:
|
|
metadata_lines.append(f"# @BRIEF {pm.group(1).strip()}")
|
|
j += 1
|
|
continue
|
|
|
|
# Normalize @RELATION
|
|
rm = re.match(r'^#\s*@RELATION:\s*(.+?)\s*$', nxt)
|
|
if rm:
|
|
rel_text = rm.group(1).strip()
|
|
# Remove brackets ONLY from predicate (word before ->)
|
|
rel_text = re.sub(r'\[(\w+)\]\s*->', r'\1 ->', rel_text)
|
|
# Normalize whitespace around ->
|
|
rel_text = re.sub(r'\s*->\s*', ' -> ', rel_text)
|
|
metadata_lines.append(f"# @RELATION {rel_text}")
|
|
j += 1
|
|
continue
|
|
|
|
# Pass through any other tags as-is
|
|
if re.match(r'^#\s*@\w+', nxt):
|
|
metadata_lines.append(lines[j])
|
|
j += 1
|
|
continue
|
|
|
|
# If it doesn't match any known pattern, keep it but stop collecting
|
|
break
|
|
|
|
# Build the header
|
|
header_parts = [f"# #region {name}"]
|
|
if complexity is not None:
|
|
header_parts.append(f"[C:{complexity}]")
|
|
metadata_merged += 1
|
|
header_parts.append(f"[TYPE {typ}]")
|
|
if semantics is not None:
|
|
header_parts.append(f"[SEMANTICS {semantics}]")
|
|
metadata_merged += 1
|
|
|
|
result.append(" ".join(header_parts))
|
|
# Add preserved metadata lines
|
|
result.extend(metadata_lines)
|
|
|
|
blocks_converted += 1
|
|
i = j
|
|
continue
|
|
|
|
# Check for standalone @PURPOSE (not in a DEF block)
|
|
pm = re.match(r'^#\s*@PURPOSE:\s*(.*?)\s*$', stripped)
|
|
if pm:
|
|
result.append(f"# @BRIEF {pm.group(1).strip()}")
|
|
i += 1
|
|
continue
|
|
|
|
# Check for standalone @RELATION normalization
|
|
rm = re.match(r'^#\s*@RELATION:\s*(.+?)\s*$', stripped)
|
|
if rm:
|
|
rel_text = rm.group(1).strip()
|
|
# Remove brackets ONLY from predicate (word before ->)
|
|
rel_text = re.sub(r'\[(\w+)\]\s*->', r'\1 ->', rel_text)
|
|
# Normalize whitespace around ->
|
|
rel_text = re.sub(r'\s*->\s*', ' -> ', rel_text)
|
|
result.append(f"# @RELATION {rel_text}")
|
|
i += 1
|
|
continue
|
|
|
|
# Skip @PARAM, @RETURN, @THROW lines (standalone)
|
|
if re.match(r'^#\s*@(PARAM|RETURN|THROW)\b', stripped):
|
|
i += 1
|
|
continue
|
|
|
|
# Pass through everything else unchanged
|
|
result.append(line)
|
|
i += 1
|
|
|
|
# Build new content with preserved line endings
|
|
new_text = '\n'.join(result)
|
|
|
|
if new_text == text:
|
|
return 0, 0
|
|
|
|
# Convert back to CRLF if original had it
|
|
if original_crlf:
|
|
new_raw = new_text.replace('\n', '\r\n').encode('utf-8')
|
|
else:
|
|
new_raw = new_text.encode('utf-8')
|
|
|
|
with open(filepath, "wb") as f:
|
|
f.write(new_raw)
|
|
return blocks_converted, metadata_merged
|
|
|
|
|
|
def main():
|
|
total_blocks = 0
|
|
total_merged = 0
|
|
total_files = 0
|
|
|
|
# Collect all Python files
|
|
py_files = sorted(SRC_DIR.rglob("*.py"))
|
|
|
|
for filepath in py_files:
|
|
# Skip __pycache__, .venv, tests
|
|
rel = filepath.relative_to(SRC_DIR)
|
|
parts = list(rel.parts)
|
|
if "__pycache__" in parts or ".venv" in parts:
|
|
continue
|
|
if any(p.startswith("__tests__") or p == "tests" for p in parts):
|
|
continue
|
|
|
|
try:
|
|
blocks, merged = process_file(filepath)
|
|
if blocks > 0 or merged > 0:
|
|
total_files += 1
|
|
total_blocks += blocks
|
|
total_merged += merged
|
|
print(f" {rel}: {blocks} blocks, {merged} metadata merged")
|
|
except Exception as e:
|
|
print(f" ERROR processing {rel}: {e}")
|
|
|
|
print(f"\nTotal: {total_files} files processed, {total_blocks} DEF blocks converted, {total_merged} metadata merged")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|