artifact_write.py 3.38 KB
"""c_artifact_write (03-C §3.7): the only writer of `artifact` rows.

Flagged as an unowned gap when scoping U-011 (REQ-02's happy AND failure
cases both need it directly, and no Unit in development-plan.md's 43-unit
breakdown built it). Built now because U-014 (end-to-end acceptance) is the
first Unit that actually needs artifacts to exist for real, not seeded.

Simplification, noted rather than silently assumed: `validation_status` is
set to 'passed' unconditionally at write time. REQ-02's boundary case
("PRD 中出现互相矛盾的场景假设时,一致性校验应在 verifying 态被拦截") describes
semantic content review, which would need its own LLM-judgment call this
codebase doesn't build yet (distinct from work_node_execution's schema-shape
validation, already real since U-013) -- out of scope here, same as every
other REQ boundary/failure case this session has deferred rather than faked.
"""

import json
import os
import subprocess
import uuid

_GIT_IDENTITY = ["-c", "user.name=graphlemon", "-c", "user.email=graphlemon@localhost"]


class InvalidDependencyError(Exception):
    """REQ-02 failure case: depends_on_artifact_ids references a nonexistent artifact_id."""


class GitCommitFailedError(Exception):
    """GIT_COMMIT_FAILED -- 03-C §3.7."""


def write_artifact(
    conn, target_folder_path, artifact_type, task_id, produced_by_run_id,
    file_path, content, depends_on_artifact_ids=None,
):
    """Writes `content` to `<target_folder_path>/<file_path>`, commits it to
    the target project's own git history (not graphlemon's -- 01-M 1.5:
    artifact.git_commit_sha is the *target project's* commit), and creates
    the immutable `artifact` row. Returns {"artifact_id", "git_commit_sha"}.
    """
    depends_on_artifact_ids = depends_on_artifact_ids or []
    for dep_id in depends_on_artifact_ids:
        if conn.execute("SELECT 1 FROM artifact WHERE artifact_id = ?", (dep_id,)).fetchone() is None:
            raise InvalidDependencyError("depends_on_artifact_ids references nonexistent artifact: " + dep_id)

    abs_path = os.path.join(target_folder_path, file_path)
    os.makedirs(os.path.dirname(abs_path), exist_ok=True)
    with open(abs_path, "w", encoding="utf-8") as f:
        f.write(content)

    try:
        subprocess.run(
            ["git", "add", file_path], cwd=target_folder_path, check=True, capture_output=True, text=True
        )
        subprocess.run(
            ["git"] + _GIT_IDENTITY + [
                "commit", "-m", "graphlemon: {} artifact for task {}".format(artifact_type, task_id)
            ],
            cwd=target_folder_path, check=True, capture_output=True, text=True,
        )
        sha = subprocess.run(
            ["git", "rev-parse", "HEAD"], cwd=target_folder_path, check=True, capture_output=True, text=True
        ).stdout.strip()
    except subprocess.CalledProcessError as e:
        raise GitCommitFailedError(str(e)) from e

    artifact_id = str(uuid.uuid4())
    conn.execute(
        "INSERT INTO artifact "
        "(artifact_id, artifact_type, task_id, produced_by_run_id, file_path, git_commit_sha, "
        "depends_on_artifact_ids, validation_status) VALUES (?, ?, ?, ?, ?, ?, ?, 'passed')",
        (
            artifact_id, artifact_type, task_id, produced_by_run_id, file_path, sha,
            json.dumps(depends_on_artifact_ids),
        ),
    )
    conn.commit()
    return {"artifact_id": artifact_id, "git_commit_sha": sha}