升级 Trellis 并完成项目审查(07-26-trellis-upgrade-project-audit)
这个提交包含在:
@@ -10,9 +10,9 @@ Provides:
|
||||
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.
|
||||
``_example`` line; the AI agent curates real entries during planning when
|
||||
the task needs sub-agent/spec context. See ``.trellis/workflow.md`` for the
|
||||
current planning artifact contract.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -21,10 +21,36 @@ import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from .config import get_context_injection_limits
|
||||
from .git import branch_exists_locally
|
||||
from .io import read_json
|
||||
from .log import Colors, colored
|
||||
from .paths import get_repo_root
|
||||
from .paths import FILE_TASK_JSON, get_repo_root
|
||||
from .task_utils import resolve_task_dir
|
||||
|
||||
# Extensions that look like code rather than spec/research docs. Entries with
|
||||
# one of these extensions outside .trellis/spec/, docs/docs-site, or the
|
||||
# task's own directory get a hygiene warning in `task.py validate` — the
|
||||
# reader is a sub-agent, not a human, so code paths belong in the diff the
|
||||
# agent reads itself, not in implement.jsonl / check.jsonl.
|
||||
_CODE_FILE_EXTENSIONS = {
|
||||
".ts",
|
||||
".tsx",
|
||||
".js",
|
||||
".jsx",
|
||||
".mjs",
|
||||
".cjs",
|
||||
".py",
|
||||
".go",
|
||||
".rs",
|
||||
".java",
|
||||
".rb",
|
||||
".c",
|
||||
".cc",
|
||||
".cpp",
|
||||
".h",
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Command: add-context
|
||||
@@ -97,10 +123,26 @@ def cmd_validate(args: argparse.Namespace) -> int:
|
||||
print(f"Target dir: {target_dir}")
|
||||
print()
|
||||
|
||||
# Warn (don't fail validation) when the recorded branch is stale — it
|
||||
# was likely already merged and deleted (#399 item 2).
|
||||
task_json_path = target_dir / FILE_TASK_JSON
|
||||
if task_json_path.is_file():
|
||||
task_data = read_json(task_json_path)
|
||||
stored_branch = task_data.get("branch") if task_data else None
|
||||
if stored_branch and not branch_exists_locally(stored_branch, repo_root):
|
||||
print(
|
||||
colored(
|
||||
f"Warning: recorded branch '{stored_branch}' no longer exists locally "
|
||||
"(likely merged and deleted).",
|
||||
Colors.YELLOW,
|
||||
)
|
||||
)
|
||||
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)
|
||||
errors = _validate_jsonl(jsonl_file, repo_root, target_dir)
|
||||
total_errors += errors
|
||||
|
||||
print()
|
||||
@@ -112,11 +154,33 @@ def cmd_validate(args: argparse.Namespace) -> int:
|
||||
return 1
|
||||
|
||||
|
||||
def _validate_jsonl(jsonl_file: Path, repo_root: Path) -> int:
|
||||
def _is_exempt_from_code_file_warning(file_path: str, task_rel: str) -> bool:
|
||||
"""Whether a jsonl entry path is exempt from the code-file hygiene warning.
|
||||
|
||||
Exempt: spec docs (``.trellis/spec/``), documentation (``docs``,
|
||||
``docs-site``), and the task's own directory (execution plans, generated
|
||||
artifacts, etc. legitimately live there).
|
||||
"""
|
||||
posix_path = file_path.replace("\\", "/").lstrip("/")
|
||||
exempt_prefixes = (".trellis/spec/", "docs/", "docs-site/")
|
||||
if posix_path.startswith(exempt_prefixes):
|
||||
return True
|
||||
if task_rel and (posix_path == task_rel or posix_path.startswith(f"{task_rel}/")):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _validate_jsonl(jsonl_file: Path, repo_root: Path, task_dir: Path | None = None) -> int:
|
||||
"""Validate a single JSONL file.
|
||||
|
||||
Seed rows (no ``file`` field — typically ``{"_example": "..."}``) are
|
||||
skipped silently; they are self-describing comments, not real entries.
|
||||
|
||||
Beyond hard errors (missing file/dir, invalid JSON), this also prints
|
||||
non-blocking hygiene warnings (never counted in ``errors``, never change
|
||||
the exit code): entries that look like code files rather than
|
||||
spec/research docs, and entries whose file size exceeds the configured
|
||||
sub-agent context injection cap (``context_injection.max_file_bytes``).
|
||||
"""
|
||||
file_name = jsonl_file.name
|
||||
errors = 0
|
||||
@@ -125,6 +189,15 @@ def _validate_jsonl(jsonl_file: Path, repo_root: Path) -> int:
|
||||
print(f" {colored(f'{file_name}: not found (skipped)', Colors.YELLOW)}")
|
||||
return 0
|
||||
|
||||
task_rel = ""
|
||||
if task_dir is not None:
|
||||
try:
|
||||
task_rel = task_dir.resolve().relative_to(repo_root.resolve()).as_posix()
|
||||
except ValueError:
|
||||
task_rel = ""
|
||||
|
||||
max_file_bytes = get_context_injection_limits(repo_root).get("max_file_bytes", 0)
|
||||
|
||||
line_num = 0
|
||||
real_entries = 0
|
||||
for line in jsonl_file.read_text(encoding="utf-8").splitlines():
|
||||
@@ -152,10 +225,33 @@ def _validate_jsonl(jsonl_file: Path, repo_root: Path) -> int:
|
||||
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
|
||||
continue
|
||||
|
||||
if not full_path.is_file():
|
||||
print(f" {colored(f'{file_name}:{line_num}: File not found: {file_path}', Colors.RED)}")
|
||||
errors += 1
|
||||
continue
|
||||
|
||||
extension = Path(file_path).suffix.lower()
|
||||
if extension in _CODE_FILE_EXTENSIONS and not _is_exempt_from_code_file_warning(
|
||||
file_path, task_rel
|
||||
):
|
||||
warning = (
|
||||
f"{file_name}:{line_num}: Warning: {file_path} looks like a code file — "
|
||||
"implement/check.jsonl should reference spec/research docs; "
|
||||
"agents read code themselves"
|
||||
)
|
||||
print(f" {colored(warning, Colors.YELLOW)}")
|
||||
|
||||
if max_file_bytes:
|
||||
size = full_path.stat().st_size
|
||||
if size > max_file_bytes:
|
||||
warning = (
|
||||
f"{file_name}:{line_num}: Warning: {file_path} is {size} bytes, "
|
||||
f"exceeds context_injection.max_file_bytes ({max_file_bytes}); "
|
||||
"injection will truncate it"
|
||||
)
|
||||
print(f" {colored(warning, Colors.YELLOW)}")
|
||||
|
||||
if errors == 0:
|
||||
print(f" {colored(f'{file_name}: ✓ ({real_entries} entries)', Colors.GREEN)}")
|
||||
|
||||
在新工单中引用
屏蔽一个用户