| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223 |
- #!/usr/bin/env python3
- """
- Task JSONL context management.
- Provides:
- cmd_add_context - Add entry to JSONL context file
- cmd_validate - Validate JSONL context files
- cmd_list_context - List JSONL context entries
- Note:
- ``cmd_init_context`` was removed in v0.5.0-beta.12. JSONL context files
- are now seeded at ``task.py create`` time with a self-describing
- ``_example`` line; the AI agent curates real entries during Phase 1.3 of
- the workflow. See ``.trellis/workflow.md`` Phase 1.3 for the current
- instructions.
- """
- from __future__ import annotations
- import argparse
- import json
- from pathlib import Path
- from .log import Colors, colored
- from .paths import get_repo_root
- from .task_utils import resolve_task_dir
- # =============================================================================
- # Command: add-context
- # =============================================================================
- def cmd_add_context(args: argparse.Namespace) -> int:
- """Add entry to JSONL context file."""
- repo_root = get_repo_root()
- target_dir = resolve_task_dir(args.dir, repo_root)
- jsonl_name = args.file
- path = args.path
- reason = args.reason or "Added manually"
- if not target_dir.is_dir():
- print(colored(f"Error: Directory not found: {target_dir}", Colors.RED))
- return 1
- # Support shorthand
- if not jsonl_name.endswith(".jsonl"):
- jsonl_name = f"{jsonl_name}.jsonl"
- jsonl_file = target_dir / jsonl_name
- full_path = repo_root / path
- entry_type = "file"
- if full_path.is_dir():
- entry_type = "directory"
- if not path.endswith("/"):
- path = f"{path}/"
- elif not full_path.is_file():
- print(colored(f"Error: Path not found: {path}", Colors.RED))
- return 1
- # Check if already exists
- if jsonl_file.is_file():
- content = jsonl_file.read_text(encoding="utf-8")
- if f'"{path}"' in content:
- print(colored(f"Warning: Entry already exists for {path}", Colors.YELLOW))
- return 0
- # Add entry
- entry: dict
- if entry_type == "directory":
- entry = {"file": path, "type": "directory", "reason": reason}
- else:
- entry = {"file": path, "reason": reason}
- with jsonl_file.open("a", encoding="utf-8") as f:
- f.write(json.dumps(entry, ensure_ascii=False) + "\n")
- print(colored(f"Added {entry_type}: {path}", Colors.GREEN))
- return 0
- # =============================================================================
- # Command: validate
- # =============================================================================
- def cmd_validate(args: argparse.Namespace) -> int:
- """Validate JSONL context files."""
- repo_root = get_repo_root()
- target_dir = resolve_task_dir(args.dir, repo_root)
- if not target_dir.is_dir():
- print(colored("Error: task directory required", Colors.RED))
- return 1
- print(colored("=== Validating Context Files ===", Colors.BLUE))
- print(f"Target dir: {target_dir}")
- print()
- total_errors = 0
- for jsonl_name in ["implement.jsonl", "check.jsonl"]:
- jsonl_file = target_dir / jsonl_name
- errors = _validate_jsonl(jsonl_file, repo_root)
- total_errors += errors
- print()
- if total_errors == 0:
- print(colored("✓ All validations passed", Colors.GREEN))
- return 0
- else:
- print(colored(f"✗ Validation failed ({total_errors} errors)", Colors.RED))
- return 1
- def _validate_jsonl(jsonl_file: Path, repo_root: Path) -> int:
- """Validate a single JSONL file.
- Seed rows (no ``file`` field — typically ``{"_example": "..."}``) are
- skipped silently; they are self-describing comments, not real entries.
- """
- file_name = jsonl_file.name
- errors = 0
- if not jsonl_file.is_file():
- print(f" {colored(f'{file_name}: not found (skipped)', Colors.YELLOW)}")
- return 0
- line_num = 0
- real_entries = 0
- for line in jsonl_file.read_text(encoding="utf-8").splitlines():
- line_num += 1
- if not line.strip():
- continue
- try:
- data = json.loads(line)
- except json.JSONDecodeError:
- print(f" {colored(f'{file_name}:{line_num}: Invalid JSON', Colors.RED)}")
- errors += 1
- continue
- file_path = data.get("file")
- entry_type = data.get("type", "file")
- if not file_path:
- # Seed / comment row — skip silently
- continue
- real_entries += 1
- full_path = repo_root / file_path
- if entry_type == "directory":
- if not full_path.is_dir():
- print(f" {colored(f'{file_name}:{line_num}: Directory not found: {file_path}', Colors.RED)}")
- errors += 1
- else:
- if not full_path.is_file():
- print(f" {colored(f'{file_name}:{line_num}: File not found: {file_path}', Colors.RED)}")
- errors += 1
- if errors == 0:
- print(f" {colored(f'{file_name}: ✓ ({real_entries} entries)', Colors.GREEN)}")
- else:
- print(f" {colored(f'{file_name}: ✗ ({errors} errors)', Colors.RED)}")
- return errors
- # =============================================================================
- # Command: list-context
- # =============================================================================
- def cmd_list_context(args: argparse.Namespace) -> int:
- """List JSONL context entries."""
- repo_root = get_repo_root()
- target_dir = resolve_task_dir(args.dir, repo_root)
- if not target_dir.is_dir():
- print(colored("Error: task directory required", Colors.RED))
- return 1
- print(colored("=== Context Files ===", Colors.BLUE))
- print()
- for jsonl_name in ["implement.jsonl", "check.jsonl"]:
- jsonl_file = target_dir / jsonl_name
- if not jsonl_file.is_file():
- continue
- print(colored(f"[{jsonl_name}]", Colors.CYAN))
- count = 0
- seed_only = True
- for line in jsonl_file.read_text(encoding="utf-8").splitlines():
- if not line.strip():
- continue
- try:
- data = json.loads(line)
- except json.JSONDecodeError:
- continue
- file_path = data.get("file")
- if not file_path:
- # Seed / comment row — don't count as a real entry
- continue
- seed_only = False
- count += 1
- entry_type = data.get("type", "file")
- reason = data.get("reason", "-")
- if entry_type == "directory":
- print(f" {colored(f'{count}.', Colors.GREEN)} [DIR] {file_path}")
- else:
- print(f" {colored(f'{count}.', Colors.GREEN)} {file_path}")
- print(f" {colored('→', Colors.YELLOW)} {reason}")
- if seed_only:
- print(f" {colored('(no curated entries yet — only seed row)', Colors.YELLOW)}")
- print()
- return 0
|