bug_record.py 8.27 KB
"""c_bug_record_create (03-C §3.10): finalizes a Validator-judged bug.

Distinct from c_bug_report_create (03-C §3.1.1, human-submitted feedback with
no root cause yet -- owned by U-023, not this Unit): this controller's
input_contract requires root_cause_category/root_cause_detail/suggested_route
all as required fields, i.e. it's the "record a bug the Validator has already
judged" endpoint, called at the end of p_validator_root_cause_trace's
judging_root_cause -> bug_recorded transition (04-P §4.3).

**skill_defect + skill_iteration_suggestion (resolved 2026-07-31, U-018)**:
03-C §3.10 says root_cause_category=skill_defect "必须同时创建
skill_iteration_suggestion", but skill_iteration.before/after_git_commit_sha
are NOT NULL and can't be populated until an actual skill rewrite happens --
that's c_skill_update (src/skill_update.py), a later, separate step per
04-P §4.3's own transition table (bug_recorded is reached by this function
alone; skill_iteration_triggered -> c_skill_update is a *subsequent*
transition). So `skill_defect` is accepted here with
skill_iteration_suggestion_id left null; src/skill_update.py's
update_skill() fills it in once the iteration actually exists. See that
module's docstring for the full reasoning -- this was originally deferred
as an unenforced gap when U-015 was built (U-018 didn't exist yet to close
the loop) and has since been resolved, not left open.
"""

import json
import uuid

# core_engine added in U-035 -- U-034 deliberately left this app-layer
# whitelist unchanged even though it already widened the schema-level CHECK
# constraint, since classifying a change as core_engine is U-035's
# path-based rule (src/core_engine_classify.py), not something the
# Validator's existing 3-perspective judge_root_cause can produce on its own.
_VALID_ROOT_CAUSE_CATEGORIES = ("planning", "coder", "skill_defect", "core_engine")


class InvalidRootCauseCategoryError(Exception):
    """INVALID_ROOT_CAUSE_CATEGORY -- 03-C §3.10."""


class InvalidParamError(Exception):
    """INVALID_PARAM -- 03-C §3.1.1: description is empty, or project_id/
    related_task_id don't reference existing rows."""


def create_bug_report(conn, project_id, description, repro_steps=None, related_task_id=None):
    """c_bug_report_create (03-C §3.1.1): console-submitted human bug report.

    03-C's own input_contract for this controller lists only
    {description, repro_steps, related_task_id} -- no project_id -- but
    execution_event.project_id is NOT NULL (01-M 1.9) and bug_record has no
    project_id column of its own to derive one from when related_task_id is
    left empty ("留空则视为新发现的独立问题", 02-V §2.4). Requiring
    project_id here (like c_task_create already does per its own contract)
    is the only way to satisfy that constraint for a task-less report; this
    is a real 03-C gap, not a design choice, flagged rather than silently
    routed around -- see development-plan.md's 2026-07-31 note on this Unit.
    """
    if not description:
        raise InvalidParamError("description is required")
    if conn.execute("SELECT 1 FROM project WHERE project_id = ?", (project_id,)).fetchone() is None:
        raise InvalidParamError("project_id does not exist: " + str(project_id))
    if related_task_id is not None and conn.execute(
        "SELECT 1 FROM task WHERE task_id = ?", (related_task_id,)
    ).fetchone() is None:
        raise InvalidParamError("related_task_id does not exist: " + str(related_task_id))

    bug_id = str(uuid.uuid4())
    conn.execute(
        "INSERT INTO bug_record (bug_id, task_id, source, description, repro_steps, status) "
        "VALUES (?, ?, 'human', ?, ?, 'open')",
        (bug_id, related_task_id, description, repro_steps),
    )
    conn.execute(
        "INSERT INTO execution_event (event_id, project_id, task_id, event_type, payload) "
        "VALUES (?, ?, ?, 'bug_recorded', ?)",
        (str(uuid.uuid4()), project_id, related_task_id, json.dumps({"bug_id": bug_id, "source": "human"})),
    )
    conn.commit()
    return {"bug_id": bug_id, "status": "open"}


def create_bug_record(
    conn, task_id, description, root_cause_category, root_cause_detail, suggested_route,
    repro_steps=None, related_artifact_ids=None, skill_iteration_suggestion_id=None,
):
    """Creates a bug_record(source='validator', status='routed') -- 'routed'
    because by the time this is called, triage (root cause + suggested_route)
    is already complete; there's no separate triaging step for this origin
    (that's the human_reported branch's job, see claim_human_reported_bug /
    complete_human_reported_triage below)."""
    if root_cause_category not in _VALID_ROOT_CAUSE_CATEGORIES:
        raise InvalidRootCauseCategoryError(
            "root_cause_category must be one of {}, got {!r}".format(
                _VALID_ROOT_CAUSE_CATEGORIES, root_cause_category
            )
        )

    bug_id = str(uuid.uuid4())
    conn.execute(
        "INSERT INTO bug_record "
        "(bug_id, task_id, source, description, repro_steps, root_cause_category, root_cause_detail, "
        "related_artifact_ids, suggested_route, skill_iteration_suggestion_id, status) "
        "VALUES (?, ?, 'validator', ?, ?, ?, ?, ?, ?, ?, 'routed')",
        (
            bug_id, task_id, description, repro_steps, root_cause_category, root_cause_detail,
            json.dumps(related_artifact_ids) if related_artifact_ids else None,
            suggested_route, skill_iteration_suggestion_id,
        ),
    )
    conn.commit()
    return {"bug_id": bug_id}


def record_core_engine_change(conn, task_id, description):
    """U-035 (05 §5.8): audit-trail record for a self-target task that
    touched core-engine territory (`.claude/agents/*.md`/`src/`/`supervisor/`)
    and passed Validator on its first attempt -- NOT the Validator-FAIL
    root-cause-judgment path `create_bug_record()` serves. There is no
    "route" decision here (`suggested_route` stays null, unlike
    `create_bug_record`'s required field) and no open triage step -- the
    change already passed validation, so `status='resolved'` from creation.
    Exists so 03-C §3.11's `c_org_loop_route` core_engine branch and 04-P
    §4.6's `p_deploy_supervisor_blue_green` trigger text -- both phrased in
    terms of "a bug_record with root_cause_category=core_engine" -- have a
    real row to reference, satisfying that text even though this is a
    PASS-path audit record rather than a FAIL-path bug attribution.
    Returns {"bug_id"}."""
    bug_id = str(uuid.uuid4())
    conn.execute(
        "INSERT INTO bug_record "
        "(bug_id, task_id, source, description, root_cause_category, status) "
        "VALUES (?, ?, 'validator', ?, 'core_engine', 'resolved')",
        (bug_id, task_id, description),
    )
    conn.commit()
    return {"bug_id": bug_id}


def claim_human_reported_bug(conn, bug_id):
    """04-P §4.3: human_reported -> tracing_lineage. Validator claims an
    open, source=human bug_record. status: open -> triaging."""
    row = conn.execute("SELECT source, status FROM bug_record WHERE bug_id = ?", (bug_id,)).fetchone()
    if row is None:
        raise ValueError("no such bug_record: " + bug_id)
    source, status = row
    if source != "human" or status != "open":
        raise ValueError(
            "bug_record {} is not claimable (source={}, status={}); expected source=human, status=open".format(
                bug_id, source, status
            )
        )
    conn.execute("UPDATE bug_record SET status = 'triaging' WHERE bug_id = ?", (bug_id,))
    conn.commit()


def complete_human_reported_triage(conn, bug_id, root_cause_category, root_cause_detail, suggested_route):
    """04-P §4.3 补充说明: human_reported 起点的 judging_root_cause->bug_recorded
    是对已存在 bug_record 的字段补全式更新,不产生重复记录 -- status: triaging->routed."""
    if root_cause_category not in _VALID_ROOT_CAUSE_CATEGORIES:
        raise InvalidRootCauseCategoryError(
            "root_cause_category must be one of {}, got {!r}".format(
                _VALID_ROOT_CAUSE_CATEGORIES, root_cause_category
            )
        )
    conn.execute(
        "UPDATE bug_record SET root_cause_category = ?, root_cause_detail = ?, suggested_route = ?, "
        "status = 'routed' WHERE bug_id = ?",
        (root_cause_category, root_cause_detail, suggested_route, bug_id),
    )
    conn.commit()