CtrlK
BlogDocsLog inGet started
Tessl Logo

giuseppe-trisciuoglio/developer-kit

Comprehensive developer toolkit providing reusable skills for Java/Spring Boot, TypeScript/NestJS/React/Next.js, Python, PHP, AWS CloudFormation, AI/RAG, DevOps, and more.

89

Quality

89%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Risky

Do not use without reviewing

Overview
Quality
Evals
Security
Files

specs-task-tdd-updater.pyplugins/developer-kit-specs/hooks/

#!/usr/bin/env python3
"""Update task files with TDD generation and RED-phase verification metadata."""

from __future__ import annotations

import argparse
import importlib.util
import json
import sys
from dataclasses import asdict, dataclass
from datetime import date
from pathlib import Path
from typing import Any

SUMMARY_START = "<!-- specs:task-tdd summary:start -->"
SUMMARY_END = "<!-- specs:task-tdd summary:end -->"
SUMMARY_HEADING = "## TDD Test Generation Summary"


class TaskUpdateError(ValueError):
    """Raised when task file updates cannot be completed safely."""

    def __init__(self, message: str, *, code: str) -> None:
        super().__init__(message)
        self.code = code


@dataclass
class TaskUpdateResult:
    task_path: str
    status_before: str
    status_after: str
    test_references: list[dict[str, Any]]
    red_phase: dict[str, Any]
    summary_updated: bool

    def to_json(self) -> str:
        return json.dumps(asdict(self), indent=2)


def load_sibling_module(filename: str, module_name: str):
    module_path = Path(__file__).with_name(filename)
    spec = importlib.util.spec_from_file_location(module_name, module_path)
    if spec is None or spec.loader is None:
        raise TaskUpdateError(
            f"Unable to load module from {module_path}.",
            code="E3",
        )

    module = importlib.util.module_from_spec(spec)
    sys.modules[module_name] = module
    spec.loader.exec_module(module)
    return module


def load_parser_module():
    return load_sibling_module("specs-task-tdd-parser.py", "specs_task_tdd_parser")


def load_generator_module():
    return load_sibling_module("specs-task-tdd-generator.py", "specs_task_tdd_generator")


def load_red_phase_module():
    return load_sibling_module("specs-task-tdd-red-phase.py", "specs_task_tdd_red_phase")


def read_task_parts(task_path: str) -> tuple[str, str]:
    parser_module = load_parser_module()
    try:
        content = parser_module.read_task_file(task_path)
        return parser_module.split_frontmatter(content)
    except parser_module.TaskFileError as exc:
        raise TaskUpdateError(str(exc), code=exc.code) from exc


def parse_frontmatter_lines(frontmatter: str) -> tuple[list[dict[str, str]], dict[str, str]]:
    entries: list[dict[str, str]] = []
    values: dict[str, str] = {}

    for raw_line in frontmatter.splitlines():
        line = raw_line.rstrip("\n")
        stripped = line.strip()
        if not stripped:
            entries.append({"type": "blank", "raw": ""})
            continue
        if stripped.startswith("#"):
            entries.append({"type": "comment", "raw": line})
            continue
        if ":" not in line:
            raise TaskUpdateError(
                f"Invalid frontmatter line: '{line}'. Expected 'key: value' format.",
                code="E3",
            )

        key, raw_value = line.split(":", 1)
        normalized_key = key.strip()
        value = raw_value.strip()
        entries.append({"type": "field", "key": normalized_key, "value": value})
        values[normalized_key] = value

    return entries, values


def format_scalar(value: Any) -> str:
    if isinstance(value, bool):
        return "true" if value else "false"
    if value is None:
        return "null"
    if isinstance(value, (int, float)):
        return str(value)
    if isinstance(value, str):
        if value and all(ch.isalnum() or ch in {"-", "_", ".", "/"} for ch in value):
            return value
        return json.dumps(value)
    return json.dumps(value, ensure_ascii=True, separators=(",", ": "))


def merge_test_references(
    existing_value: str | None,
    new_reference: dict[str, Any],
) -> list[dict[str, Any]]:
    references: list[dict[str, Any]] = []
    if existing_value:
        try:
            parsed = json.loads(existing_value)
        except json.JSONDecodeError:
            parsed = []
        if isinstance(parsed, list):
            references = [item for item in parsed if isinstance(item, dict)]

    merged: list[dict[str, Any]] = []
    replaced = False
    for reference in references:
        if reference.get("path") == new_reference.get("path"):
            merged.append(new_reference)
            replaced = True
        else:
            merged.append(reference)

    if not replaced:
        merged.append(new_reference)
    return merged


def update_frontmatter(frontmatter: str, updates: dict[str, str]) -> str:
    entries, _ = parse_frontmatter_lines(frontmatter)
    seen: set[str] = set()
    output_lines: list[str] = []

    for entry in entries:
        entry_type = entry["type"]
        if entry_type == "field":
            key = entry["key"]
            value = updates.get(key, entry["value"])
            output_lines.append(f"{key}: {value}")
            seen.add(key)
            continue
        output_lines.append(entry["raw"])

    for key, value in updates.items():
        if key not in seen:
            output_lines.append(f"{key}: {value}")

    return "\n".join(output_lines).strip() + "\n"


def render_summary(
    generated,
    red_phase_result,
    *,
    generated_on: str,
) -> str:
    lines = [
        SUMMARY_START,
        SUMMARY_HEADING,
        "",
        f"**Generated On**: {generated_on}",
        f"**Test File**: `{generated.output_path}`",
        f"**Language**: `{generated.language}`",
        f"**Test Type**: `{generated.test_type}`",
        f"**RED Phase Status**: `{red_phase_result.status}`",
        f"**Verification Summary**: {red_phase_result.summary}",
        "",
        "### Generated Scenarios",
    ]

    for scenario in generated.scenarios:
        lines.append(f"- `{scenario.test_name}`: {scenario.description}")

    lines.extend(
        [
            "",
            "### Verification Details",
            f"- Framework: `{red_phase_result.framework}`",
            f"- Command: `{ ' '.join(red_phase_result.command) }`",
            f"- Return code: `{red_phase_result.returncode}`",
        ]
    )

    if red_phase_result.failure_artifacts:
        lines.append("- Failure artifacts:")
        for artifact in red_phase_result.failure_artifacts[:5]:
            lines.append(f"  - `{artifact}`")

    if red_phase_result.warnings:
        lines.append("- Warnings:")
        for warning in red_phase_result.warnings:
            lines.append(f"  - {warning}")

    lines.extend(["", SUMMARY_END])
    return "\n".join(lines).rstrip()


def upsert_summary(body: str, summary_block: str) -> tuple[str, bool]:
    if SUMMARY_START in body and SUMMARY_END in body:
        start = body.index(SUMMARY_START)
        end = body.index(SUMMARY_END) + len(SUMMARY_END)
        updated = body[:start].rstrip() + "\n\n" + summary_block + body[end:]
        return updated.rstrip() + "\n", True

    insertion = "\n\n" + summary_block + "\n"
    if "## Implementation Summary" in body:
        marker = body.index("## Implementation Summary")
        updated = body[:marker].rstrip() + insertion + "\n" + body[marker:].lstrip("\n")
        return updated.rstrip() + "\n", True

    return body.rstrip() + insertion, True


def build_test_reference(generated, *, generated_on: str) -> dict[str, Any]:
    return {
        "path": generated.output_path,
        "test_type": generated.test_type,
        "language": generated.language,
        "generated_at": generated_on,
        "scenario_count": len(generated.scenarios),
    }


def build_red_phase_payload(red_phase_result, *, verified_on: str) -> dict[str, Any]:
    return {
        "status": red_phase_result.status,
        "verified": bool(red_phase_result.red_confirmed),
        "verified_at": verified_on,
        "framework": red_phase_result.framework,
        "test_file": red_phase_result.test_file,
        "summary": red_phase_result.summary,
        "command": red_phase_result.command,
        "warnings": red_phase_result.warnings,
    }


def update_task_file(
    task_path: str,
    *,
    language: str | None = None,
    project_root: str = ".",
    test_type: str = "both",
) -> TaskUpdateResult:
    frontmatter, body = read_task_parts(task_path)
    _, existing_values = parse_frontmatter_lines(frontmatter)

    generator_module = load_generator_module()
    red_phase_module = load_red_phase_module()

    try:
        generated = generator_module.generate_from_task_file(
            task_path,
            language=language,
            test_type=test_type,
        )
    except generator_module.GenerationError as exc:
        raise TaskUpdateError(str(exc), code=exc.code) from exc

    try:
        red_phase_result = red_phase_module.verify_red_phase(
            task_path,
            language=language,
            project_root=project_root,
            test_type=test_type,
        )
    except red_phase_module.RedPhaseError as exc:
        raise TaskUpdateError(str(exc), code=exc.code) from exc

    today = date.today().isoformat()
    status_before = existing_values.get("status", "pending").strip('"')
    status_after = "red-phase" if red_phase_result.red_confirmed else status_before

    test_reference = build_test_reference(generated, generated_on=today)
    test_references = merge_test_references(existing_values.get("testReferences"), test_reference)
    red_phase_payload = build_red_phase_payload(red_phase_result, verified_on=today)

    updates = {
        "status": format_scalar(status_after),
        "testReferences": format_scalar(test_references),
        "redPhase": format_scalar(red_phase_payload),
    }
    if red_phase_result.red_confirmed:
        updates["red_phase_completed_date"] = format_scalar(today)

    updated_frontmatter = update_frontmatter(frontmatter, updates)
    summary_block = render_summary(generated, red_phase_result, generated_on=today)
    updated_body, summary_updated = upsert_summary(body, summary_block)

    task_file = Path(task_path)
    try:
        task_file.write_text(
            f"---\n{updated_frontmatter}---\n\n{updated_body.lstrip()}",
            encoding="utf-8",
        )
    except PermissionError as exc:
        raise TaskUpdateError(
            f"Permission denied: cannot update task file {task_path}",
            code="E4",
        ) from exc

    return TaskUpdateResult(
        task_path=str(task_file),
        status_before=status_before,
        status_after=status_after,
        test_references=test_references,
        red_phase=red_phase_payload,
        summary_updated=summary_updated,
    )


def build_arg_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Update a task file with TDD metadata.")
    parser.add_argument("--task", required=True, help="Path to the task markdown file.")
    parser.add_argument("--lang", help="Override language from task frontmatter.")
    parser.add_argument(
        "--project-root",
        default=".",
        help="Project root used to resolve generated test files and execute verification.",
    )
    parser.add_argument(
        "--test-type",
        default="both",
        help="unit, integration, or both (default: both).",
    )
    return parser


def main() -> int:
    parser = build_arg_parser()
    args = parser.parse_args()

    try:
        result = update_task_file(
            args.task,
            language=args.lang,
            project_root=args.project_root,
            test_type=args.test_type,
        )
    except TaskUpdateError as exc:
        print(json.dumps({"error": {"code": exc.code, "message": str(exc)}}, indent=2))
        return 1

    print(result.to_json())
    return 0


if __name__ == "__main__":
    sys.exit(main())

plugins

CHANGELOG.md

context7.json

CONTRIBUTING.md

README_CN.md

README_ES.md

README_IT.md

README.md

tessl.json

tile.json