"""Deterministic coding policy and reference gates; never model-editable.""" import html import json from importlib.resources import files from viano_bug_analysis.case.contracts import validate from viano_bug_analysis.lifecycle.storage import json_bytes class ChangeRequestError(Exception): def __init__(self, code): self.code = code super().__init__(code) def bugfix_policy(): return {"version": 1, "type": "bugfix", "precedence": "mandatory_over_case_guidance", "rules": ["Prefer the smallest practically sufficient change.", "Do not add features.", "Do not perform unnecessary refactoring.", "Preserve behavior outside the diagnosed defect.", "Do not change unrelated files or database rows.", "Do not invent APIs, schemas or symbols.", "Use only locally verified project context.", "Report missing implementation context instead of guessing.", "Do not deploy, restart services or access production."]} def scope_evidence(context): """Exact reference pairs accepted by the validator, never table-name guesses.""" result = {} for ref in sorted(context['local_context']): ids = [eid for eid, entry in sorted(context['evidence'].items()) if any(item_source(item) == ref or context.get('handoff_protocol') == 2 and entry['record']['content_ref'] == ref for item in entry['content']['items'])] if ids: result[ref] = ids return result def compact_handoff_context(context): """A deterministic task view; the full immutable context stays in the package.""" pairs = scope_evidence(context) view = {k: context[k] for k in ('case_id', 'package_id', 'analysis_revision', 'handoff_protocol', 'diagnosis_ref', 'diagnosis_sha256', 'diagnosis', 'interpretation', 'policy')} # Keep original observations and qualifications, not raw input inventories or # runtime/source catalogs already interpreted and investigated upstream. view['case'] = {k: context['case'][k] for k in ('bug', 'human_observations', 'expected_behavior', 'data_quality') if k in context['case']} view['evidence'] = {eid: {'content': entry['content'], 'content_ref': entry['record']['content_ref'], 'content_sha256': entry['record']['content_sha256']} for eid, entry in sorted(context['evidence'].items())} view['local_context'] = {ref: {**context['local_context'][ref], 'allowed_evidence_refs': ids} for ref, ids in pairs.items()} return view def build_prompt(context, *, compact=False, feedback=None, version=None): modern=context.get("handoff_protocol")==2 schema = json.loads(files("viano_bug_analysis.schemas").joinpath("change-request-output-v2.schema.json" if modern else "change-request-output-v1.schema.json").read_text()) requested_version = version version="change_request_v2" if modern else "change_request_v1" if compact and modern: version = requested_version or 'change_request_v3' visible = compact_handoff_context(context) if feedback: visible['validation_feedback'] = feedback if version in {'change_request_v6','change_request_v7','change_request_v8'}: from .phase_selection import select_context visible = select_context(visible,'handoff') if version == 'change_request_v8': from .verbose_context import project visible = project(visible) if version in {'change_request_v5','change_request_v6','change_request_v7','change_request_v8'}: from .json_consolidation import consolidate visible = consolidate(visible) # json_object/text-only adapters do not transmit the gateway schema # separately; keep the contract visible for those endpoints as well. user = json.dumps({'task': 'build_change_request', 'input': visible, 'output_schema': schema}, ensure_ascii=False, sort_keys=True, separators=(',', ':')) else: user = json_bytes({"task": "build_change_request", "input": context, "output_schema": schema}).decode() return {"prompt_version": version, "privacy": "configured", "output_schema": schema, "messages": [{"role": "system", "content": files("viano_bug_analysis.llm.prompts").joinpath(version+".txt").read_text(encoding="utf-8")}, {"role": "user", "content": user}]} def item_source(item): source = item.get("source") return item.get("source_ref") or (source.get("source_ref") if isinstance(source, dict) else None) def validate_request(payload, context): modern=context.get('handoff_protocol')==2 if modern: from jsonschema import Draft202012Validator schema=json.loads(files('viano_bug_analysis.schemas').joinpath('change-request-output-v2.schema.json').read_text()) if not Draft202012Validator(schema).is_valid(payload):raise ChangeRequestError('CHANGE_REQUEST_OUTPUT') human=payload['human_dev'] if human['status']=='assigned' and (not human['instructions'] or not human['expected_results']): raise ChangeRequestError('HUMAN_DEV_INCOMPLETE') if human['status']=='none' and (human['instructions'] or human['expected_results']): raise ChangeRequestError('HUMAN_DEV_CONTRADICTORY') tokens=[p['token'] for p in payload['placeholders']] if len(tokens)!=len(set(tokens)):raise ChangeRequestError('CHANGE_REQUEST_PLACEHOLDERS') if payload['status']=='none': if payload['affected_scope'] or payload['validation_requirements'] or tokens or payload['implementation_guidance']: raise ChangeRequestError('CHANGE_REQUEST_NO_TASK_CONTRADICTORY') return if payload['status']=='template' and (not tokens or not payload['missing_information']): raise ChangeRequestError('CHANGE_REQUEST_PLACEHOLDERS') if payload['status']=='ready' and tokens:raise ChangeRequestError('CHANGE_REQUEST_PLACEHOLDERS') else: validate(payload, "change-request-output") if context.get('parameter_grounding'): from .database_value_grounding import unsupported_assignments if unsupported_assignments(payload,context):raise ChangeRequestError('CHANGE_REQUEST_UNGROUNDED_DATABASE_VALUE') evidence = context["evidence"] local = context["local_context"] def refs(refs): if any(eid not in evidence for eid in refs): raise ChangeRequestError("CHANGE_REQUEST_EVIDENCE") paths = set() for scope in payload["affected_scope"]: ref = scope["source_ref"] if ref in paths or ref not in local or local[ref]["kind"] != scope["kind"]: raise ChangeRequestError("CHANGE_REQUEST_SCOPE") paths.add(ref) refs(scope["evidence_refs"]) if not any(item_source(item) == ref or modern and evidence[eid]["record"]["content_ref"]==ref for eid in scope["evidence_refs"] for item in evidence[eid]["content"]["items"]): raise ChangeRequestError("CHANGE_REQUEST_SCOPE_EVIDENCE") for check in payload["validation_requirements"]: refs(check["evidence_refs"]) if payload["status"] == "insufficient_context": if not payload["missing_information"]: raise ChangeRequestError("CHANGE_REQUEST_MISSING_INFORMATION") return if not paths or (payload["missing_information"] and payload["status"]!="template") or not payload["validation_requirements"]: raise ChangeRequestError("CHANGE_REQUEST_INCOMPLETE") root_refs = context["diagnosis"]["diagnosis"]["root_cause"]["evidence_refs"] if not any(item_source(item) in paths or modern and evidence[eid]["record"]["content_ref"] in paths for eid in root_refs for item in evidence[eid]["content"]["items"]): raise ChangeRequestError("CHANGE_REQUEST_ROOT_SCOPE") def make_document(context, payload, context_ref, call_ref): from viano_bug_analysis.evidence.store import digest document = {"format": "viano-change-request", "format_version": 1, **{f: context[f] for f in ("case_id", "package_id", "analysis_revision")}, "task": {"type": "bugfix", "id": f"CR-{context['analysis_revision']:04d}"}, "diagnosis_ref": context["diagnosis_ref"], "diagnosis_sha256": context["diagnosis_sha256"], "context_ref": context_ref, "context_sha256": digest(context), "llm_result_ref": call_ref, "policy": bugfix_policy(), "current_behavior": context["interpretation"]["observed_behavior"], "expected_behavior": context["interpretation"]["expected_behavior"], "root_cause": context["diagnosis"]["diagnosis"]["root_cause"], "project_context_refs": [local for ref, local in sorted(context["local_context"].items()) if ref in {s["source_ref"] for s in payload["affected_scope"]}], "request": payload} if context.get('handoff_protocol')==2: document.update(format_version=2,assignment='none' if payload['status']=='none' else 'assigned', human_dev_assigned=payload['human_dev']['status']=='assigned', request={k:v for k,v in payload.items() if k!='human_dev'}) return document def render_change_request(document): lines = ["# Bugfix change request", "", "Task: " + document["task"]["id"], "", "Diagnosis: " + document["diagnosis_ref"], "", "## Mandatory policy", ""] if document.get('format_version')==2: lines += ['Codingauftrag: '+('Kein Codingauftrag vorhanden.' if document['assignment']=='none' else document['request']['status']), '', 'Human-Dev-Auftrag vorhanden: '+('ja' if document['human_dev_assigned'] else 'nein'), ''] lines += ["- " + html.escape(rule) for rule in document["policy"]["rules"]] fields = {field: document[field] for field in ("current_behavior", "expected_behavior", "root_cause", "project_context_refs")} fields.update(document["request"]) for field, value in (sorted(fields.items()) if document.get("format_version")==2 else fields.items()): lines += ["", "## " + field.replace("_", " "), "", "```json", json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True).replace("`", "\\u0060"), "```"] return "\n".join(lines) + "\n" def render_human_request(context,payload): """Human-facing handoff, not a job tracked by the analysis worker.""" import re def literal(value): return re.sub(r'([\\`*_{}\[\]()#+.!|>~-])',r'\\\1',html.escape(str(value))) human=payload['human_dev'] lines=['# Human-Dev-Auftrag', '', 'Case: '+literal(context['case_id']), 'Diagnose: '+literal(context['diagnosis_ref']), '', 'Status: '+('Auftrag vorhanden' if human['status']=='assigned' else 'Kein Human-Dev-Auftrag vorhanden'), '', '## '+literal(human['title']), '', literal(human['reason'])] if human['status']=='assigned': lines+=['', '## Aufgabe', '', *['- '+literal(s) for s in human['instructions']], '', '## Erwartetes Ergebnis', '', *['- '+literal(s) for s in human['expected_results']]] return '\n'.join(lines)+'\n' def delivery_payload(context,payload): """Adapt pending legacy responses without rewriting their call or context.""" if context.get('handoff_protocol')==2:return payload result=dict(payload,placeholders=[]) if payload['status']=='ready': result['human_dev']={'status':'none','title':'Kein Human-Dev-Auftrag vorhanden', 'reason':'Aus dem bestehenden Codingauftrag wurde kein zusätzlicher menschlicher Auftrag abgeleitet.', 'instructions':[],'expected_results':[]} else: result.update(status='none',affected_scope=[],implementation_guidance=[],validation_requirements=[], required_change='Kein Codingauftrag vorhanden',implementation_intent='No coding assignment was formed.', human_dev={'status':'assigned','title':'Fehlende Informationen zur Fehlerbehebung ergänzen', 'reason':payload['reason'], 'instructions':list(payload['missing_information']), 'expected_results':['Die fehlenden Informationen extern in die Fehlerbehebung einbringen; keine Rückgabe an BugAnalysis erforderlich.']}) return result def make_delivery_document(context,payload,context_ref,call_ref): from viano_bug_analysis.evidence.store import digest effective=dict(context,handoff_protocol=2) projected=delivery_payload(context,payload) validate_request(projected,effective) doc=make_document(effective,projected,context_ref,call_ref) doc['context_sha256']=digest(context) return doc