221 lines
13 KiB
Python
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
|