"""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()