Files
VianoBugAnalysis/src/viano_bug_analysis/analysis/change_request_contracts.py
T

221 lines
13 KiB
Python

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