# LOOM v1.0 Parser + Validator (plain text version)
:
loom_reader.txt
inside your .md folder.
# When you move it into your code repo, rename it to loom_reader.py.
import re
import sys
from dataclasses import dataclass, asdict
from typing import List
LOOM_TAG_PATTERN = re.compile(r'^\[LOOM:\s*(.+?)\s*\]\s*$', re.MULTILINE)
@dataclass
class LoomArtifact:
name: str
domain: str
object: str
intent: str
conditions: str
effects: str
verdict: str
errors: List[str]
FIELD_LABELS = ["Domain:", "Object:", "Intent:", "Conditions:", "Effects:"]
def split_artifacts(text: str) -> List[tuple]:
"""Return list of (name, body_text) for each [LOOM: ...] block."""
artifacts = []
matches = list(LOOM_TAG_PATTERN.finditer(text))
for i, m in enumerate(matches):
name = m.group(1).strip()
start = m.end()
end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
body = text[start:end].strip()
artifacts.append((name, body))
return artifacts
def extract_fields(body: str) -> dict:
"""
Extract the five fields from the artifact body.
Returns a dict mapping label (without colon) -> value (string or None).
"""
lines = body.splitlines()
fields = {label[:-1]: None for label in FIELD_LABELS}
current_label = None
buffer = []
def flush():
nonlocal buffer, current_label
if current_label is not None:
value = "\n".join(line.rstrip() for line in buffer).strip()
fields[current_label[:-1]] = value if value else None
buffer = []
for line in lines:
stripped = line.strip()
if any(stripped.startswith(label) for label in FIELD_LABELS):
flush()
for label in FIELD_LABELS:
if stripped.startswith(label):
current_label = label
content = stripped[len(label):].strip()
buffer = [content] if content else []
break
else:
if current_label is not None:
buffer.append(line)
flush()
return fields
def validate_artifact(name: str, body: str) -> LoomArtifact:
fields = extract_fields(body)
errors = []
for label in ["Domain", "Object", "Intent", "Conditions", "Effects"]:
if not fields.get(label):
errors.append(f"Missing {label} section")
verdict = "ACCEPTED" if not errors else "REJECTED"
return LoomArtifact(
name=name,
domain=fields.get("Domain") or "",
object=fields.get("Object") or "",
intent=fields.get("Intent") or "",
conditions=fields.get("Conditions") or "",
effects=fields.get("Effects") or "",
verdict=verdict,
errors=errors,
)
def parse_text(text: str) -> List[LoomArtifact]:
artifacts = []
for name, body in split_artifacts(text):
artifacts.append(validate_artifact(name, body))
return artifacts
def main():
if len(sys.argv) != 2:
print("Usage: python loom_reader.py <file.txt>")
sys.exit(1)
path = sys.argv[1]
with open(path, "r", encoding="utf-8") as f:
text = f.read()
artifacts = parse_text(text)
if not artifacts:
print("No [LOOM: ...] artifacts found.")
sys.exit(0)
for art in artifacts:
print("-----")
for k, v in asdict(art).items():
if k != "errors":
print(f"{k.capitalize()}: {v}")
if art.errors:
print("Errors:")
for e in art.errors:
print(f" - {e}")
print("-----")
if name == "__main__":
main()