From 87581e5cd2de18512582ab1294b9f054c0a03f06 Mon Sep 17 00:00:00 2001 From: Nitsu45 Date: Wed, 14 Oct 2026 23:45:17 +0200 Subject: [PATCH] Enrich cases with descriptive context and traced relationships --- config/project.example.toml | 154 +++++++++++++++++- docs/project-research-v1.md | 38 +++++ .../tools/case_enrichment.py | 28 ++-- .../tools/context_expansion.py | 123 ++++++++++++++ tests/test_context_expansion.py | 135 +++++++++++++++ 5 files changed, 460 insertions(+), 18 deletions(-) create mode 100644 src/viano_bug_analysis/tools/context_expansion.py create mode 100644 tests/test_context_expansion.py diff --git a/config/project.example.toml b/config/project.example.toml index d2107a3..987fd28 100644 --- a/config/project.example.toml +++ b/config/project.example.toml @@ -16,7 +16,7 @@ user = "viano_reader" password_file = "../service-data/research/world-db/reader.local.txt" snapshot_id = "backup-sha256:4964f277ef5cf7f7f98bf4378971dcb55180bac813d49bcda6d2f03ff6969662" # Explicit allowlist: no account or character tables. -tables = ["creature_template", "creature_template_locale", "creature", "quest_template", "quest_template_addon", "smart_scripts", "conditions", "game_event", "game_event_creature", "spell_script_names", "gameobject_template", "gameobject", "creature_equip_template"] +tables = ["creature_template", "creature_template_locale", "creature", "quest_template", "quest_template_addon", "smart_scripts", "conditions", "game_event", "game_event_creature", "spell_script_names", "gameobject_template", "gameobject", "creature_equip_template", "item_script_names", "creature_queststarter", "spell_dbc"] [limits] max_rows = 50 @@ -28,6 +28,17 @@ max_lines = 80 timeout_seconds = 15 max_cached_queries = 256 +[enrichment] +max_queries = 24 +max_depth = 3 +max_relationships = 128 + +[[enrichment.texts]] +table = "quest_template" +id_column = "ID" +context_role = "quest_definition" +columns = ["LogTitle", "LogDescription", "QuestDescription"] + [[enrichment.queries]] kind = "source.schema" text = "CREATE TABLE `creature`" @@ -56,5 +67,144 @@ limit = 1 entity_type = "quest" table = "quest_template" column = "ID" -columns = ["ID", "LogTitle", "RequiredNpcOrGo1", "RequiredNpcOrGo2", "RequiredNpcOrGo3", "RequiredNpcOrGo4", "RequiredItemId1", "RequiredItemId2", "RequiredSpell"] +columns = ["ID", "LogTitle", "LogDescription", "QuestDescription", "StartItem", "RequiredNpcOrGo1", "RequiredNpcOrGo2", "RequiredNpcOrGo3", "RequiredNpcOrGo4", "RequiredItemId1", "RequiredItemId2", "RequiredItemId3", "RequiredItemId4", "RequiredItemId5", "RequiredItemId6", "RequiredSpell"] limit = 1 + +[[enrichment.relationships]] +from_table = "quest_template" +from_id_column = "ID" +from_column = "StartItem" +relation = "quest_start_item" +entity_type = "item" +table = "item_script_names" +column = "Id" +columns = ["Id", "ScriptName"] + +[[enrichment.relationships]] +from_table = "quest_template" +from_id_column = "ID" +from_column = "RequiredItemId1" +relation = "quest_required_item" +entity_type = "item" +table = "item_script_names" +column = "Id" +columns = ["Id", "ScriptName"] + +[[enrichment.relationships]] +from_table = "quest_template" +from_id_column = "ID" +from_column = "RequiredItemId2" +relation = "quest_required_item" +entity_type = "item" +table = "item_script_names" +column = "Id" +columns = ["Id", "ScriptName"] + +[[enrichment.relationships]] +from_table = "quest_template" +from_id_column = "ID" +from_column = "ID" +relation = "quest_starter_records" +table = "creature_queststarter" +column = "quest" +columns = ["id", "quest"] + +[[enrichment.relationships]] +from_table = "creature_queststarter" +from_id_column = "quest" +from_column = "id" +relation = "quest_giver" +entity_type = "creature" +table = "creature_template" +column = "entry" +columns = ["entry", "name", "AIName", "ScriptName"] + +[[enrichment.relationships]] +from_table = "creature_template" +from_id_column = "entry" +from_column = "entry" +when_column = "AIName" +when_value = "SmartAI" +relation = "creature_smartai_records" +table = "smart_scripts" +column = "entryorguid" +columns = ["entryorguid", "source_type", "id", "link", "event_type", "event_param1", "event_param2", "event_param3", "event_param4", "action_type", "action_param1", "action_param2", "action_param3", "action_param4", "action_param5", "action_param6", "target_type", "target_param1", "target_param2", "target_param3", "comment"] +filters = [{column = "source_type", operator = "eq", value = 0}] +limit = 10 + +[[enrichment.relationships]] +from_table = "creature_template" +from_id_column = "entry" +from_column = "ScriptName" +relation = "creature_script_binding" +kind = "source.symbol" +path_prefix = "src/server/scripts/" +limit = 5 + +[[enrichment.relationships]] +from_table = "item_script_names" +from_id_column = "Id" +from_column = "ScriptName" +relation = "item_script_binding" +kind = "source.symbol" +path_prefix = "src/server/scripts/" +limit = 5 + +[[enrichment.relationships]] +from_table = "quest_template" +from_id_column = "ID" +from_column = "RequiredSpell" +relation = "quest_required_spell" +entity_type = "spell" +table = "spell_script_names" +column = "spell_id" +columns = ["spell_id", "ScriptName"] + +[[enrichment.relationships]] +from_table = "spell_script_names" +from_id_column = "spell_id" +from_column = "ScriptName" +relation = "spell_script_binding" +kind = "source.symbol" +path_prefix = "src/server/scripts/" +limit = 5 + +[[enrichment.relationships]] +from_table = "quest_template" +from_id_column = "ID" +from_column = "RequiredItemId3" +relation = "quest_required_item" +entity_type = "item" +table = "item_script_names" +column = "Id" +columns = ["Id", "ScriptName"] + +[[enrichment.relationships]] +from_table = "quest_template" +from_id_column = "ID" +from_column = "RequiredItemId4" +relation = "quest_required_item" +entity_type = "item" +table = "item_script_names" +column = "Id" +columns = ["Id", "ScriptName"] + +[[enrichment.relationships]] +from_table = "quest_template" +from_id_column = "ID" +from_column = "RequiredItemId5" +relation = "quest_required_item" +entity_type = "item" +table = "item_script_names" +column = "Id" +columns = ["Id", "ScriptName"] + +[[enrichment.relationships]] +from_table = "quest_template" +from_id_column = "ID" +from_column = "RequiredItemId6" +relation = "quest_required_item" +entity_type = "item" +table = "item_script_names" +column = "Id" +columns = ["Id", "ScriptName"] diff --git a/docs/project-research-v1.md b/docs/project-research-v1.md index eb71a1e..12be96b 100644 --- a/docs/project-research-v1.md +++ b/docs/project-research-v1.md @@ -101,6 +101,44 @@ Die Beispielkonfiguration löst Creature- und Quest-IDs generisch auf. Sie enth BUG-000020-spezifische Diagnose oder Wolpertinger-ID. Die LLM kann während der Untersuchung weitere Tabellen und Source-Ausschnitte gezielt abfragen. +### Kontextorientierte Erweiterung + +Die Quest-Aufbereitung übernimmt nun auch `LogDescription`, `QuestDescription` und +`StartItem`. Damit bleiben Handlung, Zweck des mitgegebenen Gegenstands und die +beschriebenen Voraussetzungen sichtbar. Questtexte beschreiben die im Template +formulierte Mechanik; sie beweisen keine tatsächlich ausgeführte Serveraktion. +`enrichment.texts` hebt solche Texte als eigene `project_description`-Fakten mit +Quellspalte, Rollenlabel und Herkunft hervor. Im numerischen Query-Fakt bleibt ein +Textverweis statt einer zweiten Kopie. Die vollständige unveränderte Antwort liegt +weiterhin im Capture; die Interpretation sieht den Text einmal in lesbarem Kontext. + +`enrichment.relationships` verfolgt ausdrücklich konfigurierte Beziehungen aus +zurückgegebenen DB-Spalten. `from_table`, `from_column` und `from_id_column` bestimmen +den Ursprung. `table`, `column` und `columns` bestimmen die folgende DB-Abfrage. +`relation` benennt den Zusammenhang; `entity_type` ergänzt eine referenzierte Entität. +Die Herkunft jeder Beziehung zeigt mit Capture und JSON-Pointer auf den Originalwert. +Ein referenziertes Item wird nicht als beobachtetes Laufzeitobjekt oder Ursache markiert. + +Die Vcore-Konfiguration verfolgt mitgegebene/geforderte Items, Queststarter, +SmartAI-Datensätze und ausdrücklich gebundene Creature-/Item-/Spell-Scriptnamen. +SmartAI-Abfragen berücksichtigen `source_type = 0`, damit numerisch gleiche IDs anderer +Entitätstypen nicht vermischt werden. Scriptnamen werden als Symbol gesucht, nicht als +beliebige Zahl über die gesamte Historie. Weitere Beziehungen bleiben konfigurierbar; +das generische Python-Modul enthält keine WoW-IDs oder Quest-Sonderfälle. + +`when_column`/`when_value` begrenzen bedingte Beziehungen. Standardmäßig werden null, +leere Zeichenfolgen und numerische Null nicht verfolgt; `ignore_values` kann dies für +andere Profile anpassen. Identische Queries werden nur einmal gesammelt. Zyklen und +Verzweigungen sind durch konfigurierbare `max_queries`, `max_depth` und +`max_relationships` begrenzt. Ausgelassene Abfragen/Beziehungen werden als +`project_context_coverage` sichtbar statt als vollständige Ermittlung ausgegeben. + +Leere Item-Script- oder Queststarter-Resultate sind nur Befunde innerhalb dieser +Tabellen. Sie beweisen weder ein fehlendes Item noch, dass eine Quest nicht durch einen +anderen Mechanismus vergeben werden kann. Client-/Hotfix-Itemdaten werden durch diesen +Meilenstein nicht importiert oder erfunden. Eine komplette Item→Spell→Creature-Kette +ist deshalb nicht in jedem Case automatisch auflösbar; fehlende Teile bleiben Recherchebedarf. + Mit aktivierter Projekt-Recherche wird `investigation_project_v1` verwendet. Der volle Context bleibt unverändert archiviert. Der Prompt lässt Input-Inventar und doppelte vorige Resultate weg; fremde Hypothesen behalten Evidenz-IDs/Provenienz, aber keine Payloads. diff --git a/src/viano_bug_analysis/tools/case_enrichment.py b/src/viano_bug_analysis/tools/case_enrichment.py index dbfde27..7f06ebd 100644 --- a/src/viano_bug_analysis/tools/case_enrichment.py +++ b/src/viano_bug_analysis/tools/case_enrichment.py @@ -1,6 +1,5 @@ """Deterministic local facts collected before CaseBuilder seals a revision.""" import copy -import hashlib from viano_bug_analysis.case import CaseAssembler from viano_bug_analysis.case.contracts import read_json @@ -38,28 +37,25 @@ class ProjectCaseEnricher: queries.append({"kind":"database.rows", "table":rule["table"], "columns":rule["columns"], "filters":[{"column":rule["column"], "operator":"eq", "value":value}], "offset":0, "limit":rule.get("limit", 5)}) - if len(queries) > 40: - raise ToolError("PROJECT_ENRICHMENT_QUERY_LIMIT") - additions, facts, context = {}, [], {"database":[], "source":[]} - for query in queries: - data = self.service.collect(query) - import json - capture = json.loads(data) - ref = "research/project/" + hashlib.sha256(data).hexdigest() + "/capture.json" - additions[ref] = data - role = "database" if query["kind"].startswith("database.") else "source" - context[role].append(ref) - facts.append({"name":"project_query", "value":{ - "query":query, "origin":capture["origin"], "result":capture["result"], "capture_ref":ref}}) + from .context_expansion import ContextExpansion + additions, facts, context, linked_entities = ContextExpansion( + self.service, getattr(self.service,"enrichment",{})).collect(queries) + capture_refs = list(additions) + for ref, data in list(additions.items()): + if ref in reader.files: + if reader.read_bytes(ref) != data: + raise ToolError("PROJECT_CAPTURE_CHANGED") + del additions[ref] index_ref = m["artifacts"].get("case_inputs") index = read_json(reader, index_ref, "case-inputs", 1024**2) if index_ref else {"format":"viano-case-inputs", "format_version":1} base = f"research/project/revisions/{m['analysis_revision']:04d}" facts_ref = base + "/facts.json" additions[facts_ref] = json_bytes({"format":"viano-reproduction", "format_version":1, - "facts":facts, "attachments":list(additions)}) + "facts":facts, "entities":linked_entities, "attachments":capture_refs}) index.setdefault("reproductions", []).append(facts_ref) for role, refs in context.items(): - index.setdefault("context", {}).setdefault(role, []).extend(dict.fromkeys(refs)) + existing_refs=index.setdefault("context", {}).setdefault(role, []) + existing_refs.extend(ref for ref in dict.fromkeys(refs) if ref not in existing_refs) new_index = base + "/case-inputs.json" additions[new_index] = json_bytes(index) generation = m["package_generation"] diff --git a/src/viano_bug_analysis/tools/context_expansion.py b/src/viano_bug_analysis/tools/context_expansion.py new file mode 100644 index 0000000..9467b29 --- /dev/null +++ b/src/viano_bug_analysis/tools/context_expansion.py @@ -0,0 +1,123 @@ +"""Bounded traversal of operator-declared data relationships, never diagnosis.""" +import copy +import hashlib +import json +import re + +from viano_bug_analysis.lifecycle.storage import json_bytes +from .config import ToolError + + +class ContextExpansion: + def __init__(self, service, settings): + self.service = service + self.rules = settings.get("relationships", []) + self.text_rules = settings.get("texts", []) + self.max_queries = settings.get("max_queries", 40) + self.max_depth = settings.get("max_depth", 2) + self.max_relationships = settings.get("max_relationships", 128) + if any(type(v) is not int or v <= 0 for v in (self.max_queries, self.max_depth, self.max_relationships)): + raise ToolError("PROJECT_ENRICHMENT_CONFIG") + + def collect(self, queries): + queue = [(copy.deepcopy(q), 0) for q in queries] + seen, edges, entities = set(), [], [] + additions, facts, context = {}, [], {"database": [], "source": []} + omitted, depth_limited, omitted_relationships = 0, 0, 0 + while queue: + query, depth = queue.pop(0) + query_key = json_bytes(query) + if query_key in seen: + continue + seen.add(query_key) + if len(additions) >= self.max_queries: + omitted += 1 + continue + data = self.service.collect(query) + capture = json.loads(data) + ref = "research/project/" + hashlib.sha256(data).hexdigest() + "/capture.json" + additions[ref] = data + role = "database" if query["kind"].startswith("database.") else "source" + context[role].append(ref) + visible_result = copy.deepcopy(capture["result"]) + descriptions = [] + if query["kind"] == "database.rows": + for rule in self.text_rules: + if rule["table"] != query["table"]: + continue + for i, item in enumerate(capture["result"]["items"]): + row = item.get("row", {}) + texts = [] + for column in rule["columns"]: + text = row.get(column) + if not isinstance(text, str) or not text.strip(): + continue + origin = {"source_ref":ref, "json_pointer":f"/result/items/{i}/row/{column}"} + texts.append({"column":column, "text":text, "origin":origin}) + # The raw text is archived once and presented once to the model. + visible_result["items"][i]["row"][column] = {"text_ref":origin} + if texts: + descriptions.append({"name":"project_description", "value":{ + "context_role":rule["context_role"], "table":query["table"], + "entity_id":row.get(rule.get("id_column","")), "texts":texts, + "meaning":"source_description_not_verified_runtime_behavior"}}) + facts.append({"name": "project_query", "value": { + "query": query, "origin": capture["origin"], "result": visible_result, "capture_ref": ref}}) + facts.extend(descriptions) + if query["kind"] != "database.rows": + continue + for rule in self.rules: + if rule["from_table"] != query["table"]: + continue + for i, item in enumerate(capture["result"]["items"]): + row = item.get("row", {}) + if "when_column" in rule and row.get(rule["when_column"]) != rule["when_value"]: + continue + value = row.get(rule["from_column"]) + if value in rule.get("ignore_values", [None, "", 0]) or type(value) is bool: + continue + if not isinstance(value, (str, int)): + raise ToolError("PROJECT_ENRICHMENT_REFERENCE") + if len(edges) >= self.max_relationships: + omitted_relationships += 1 + continue + origin = {"source_ref": ref, "json_pointer": f"/result/items/{i}/row/{rule['from_column']}"} + target_type = rule.get("entity_type") + if target_type: + # A reference identifies a related entity, not a runtime observation + # or a proven cause. Never transfer the captured NPC's role to it. + entity = {"type": target_type, "id": str(value), + "roles": [rule["relation"]], "origin": origin} + if entity not in entities: + entities.append(entity) + source_id = row.get(rule.get("from_id_column", "")) + edge = {"relation": rule["relation"], "from_table": query["table"], + "from_id": source_id, "from_column": rule["from_column"], + "target_type": target_type, "target_value": value, "origin": origin, + "meaning": "explicit_data_reference_not_causation"} + if edge not in edges: + edges.append(edge) + if depth >= self.max_depth: + depth_limited += 1 + continue + if rule.get("kind", "database.rows") == "source.symbol": + if not isinstance(value, str) or not re.fullmatch("[A-Za-z_][A-Za-z0-9_]*", value): + continue + following = {"kind": "source.symbol", "text": value, + "path_prefix": rule.get("path_prefix", ""), "case_sensitive": True, + "offset": 0, "limit": rule.get("limit", 5)} + else: + following = {"kind": "database.rows", "table": rule["table"], + "columns": rule["columns"], "filters": [{"column": rule["column"], "operator": "eq", "value": value}], + "offset": 0, "limit": rule.get("limit", 5)} + following["filters"].extend(copy.deepcopy(rule.get("filters", []))) + queue.append((following, depth + 1)) + for edge in edges: + facts.append({"name": "project_relationship", "value": edge, "origin": edge["origin"]}) + if self.rules or omitted or depth_limited or omitted_relationships: + facts.append({"name": "project_context_coverage", "value": { + "queried": len(additions), "max_queries": self.max_queries, "max_depth": self.max_depth, + "omitted_queries": omitted, "depth_limited_relationships": depth_limited, + "omitted_relationships": omitted_relationships, "max_relationships": self.max_relationships, + "meaning": "bounded_context_collection_not_exhaustive_investigation"}}) + return additions, facts, context, entities diff --git a/tests/test_context_expansion.py b/tests/test_context_expansion.py new file mode 100644 index 0000000..97d511a --- /dev/null +++ b/tests/test_context_expansion.py @@ -0,0 +1,135 @@ +import json +import pytest + +from viano_bug_analysis.tools.context_expansion import ContextExpansion +from viano_bug_analysis.tools.project_query import ProjectResearch +from viano_bug_analysis.tools.config import ToolError +from viano_bug_analysis.case import CaseBuilder +from viano_bug_analysis.package import PackageReader +from test_case_builder import case_setup + + +class RelatedData: + def __init__(self):self.calls=[] + def binding(self):return {"snapshot_id":"generic-fixture"} + def validate(self,query):pass + def query(self,query): + self.calls.append(query) + value=query['filters'][0]['value'] + rows={('tasks',10):[{'id':10,'description':'Use the supplied capture device to find the missing objects.','device':22,'required':22,'unused':0}], + ('devices',22):[{'id':22,'name':'Capture device','script':'capture_handler'}]} + return {'items':[{'table':query['table'],'row':r} for r in rows.get((query['table'],value),[])], + 'coverage':'complete_filtered_rows','truncated':False} + + +def initial(): + return {'kind':'database.rows','table':'tasks','columns':['id','description','device','required','unused'], + 'filters':[{'column':'id','operator':'eq','value':10}],'offset':0,'limit':1} + + +def rule(column='device'): + return {'from_table':'tasks','from_id_column':'id','from_column':column, + 'relation':'provided_device','entity_type':'device','table':'devices','column':'id','columns':['id','name','script']} + + +def test_relationship_traversal_keeps_text_and_deduplicates_queries(): + db=RelatedData();service=ProjectResearch({},database=db) + additions,facts,context,entities=ContextExpansion(service,{'relationships':[rule(),rule('required'),rule('unused')]}).collect([initial()]) + assert len(db.calls)==2 and len(additions)==2 + assert 'Use the supplied capture device' in str(facts) + assert {e['id'] for e in entities}=={'22'} + assert all(e['roles']==['provided_device'] for e in entities) + edges=[f for f in facts if f['name']=='project_relationship'] + assert {e['value']['from_column'] for e in edges}=={'device','required'} + for edge in edges: + origin=edge['origin'];capture=json.loads(additions[origin['source_ref']]) + assert capture['result']['items'][0]['row'][edge['value']['from_column']]==22 + assert all(e['value']['meaning']=='explicit_data_reference_not_causation' for e in edges) + + +def test_descriptive_context_is_presented_once_and_keeps_raw_proof(): + service=ProjectResearch({},database=RelatedData()) + additions,facts,_,_=ContextExpansion(service,{'texts':[{'table':'tasks','id_column':'id','context_role':'task_definition','columns':['description']}]}).collect([initial()]) + description=next(f for f in facts if f['name']=='project_description')['value'] + assert description['entity_id']==10 and description['context_role']=='task_definition' + assert 'not_verified_runtime_behavior' in description['meaning'] + text=description['texts'][0] + capture=json.loads(additions[text['origin']['source_ref']]) + assert text['text']==capture['result']['items'][0]['row']['description'] + row=facts[0]['value']['result']['items'][0]['row'] + assert row['description']=={'text_ref':text['origin']} + + +def test_cycle_and_operational_limits_are_explicit(): + db=RelatedData();service=ProjectResearch({},database=db) + cycle={'from_table':'devices','from_id_column':'id','from_column':'id','relation':'self','table':'devices','column':'id','columns':['id','name','script']} + result=ContextExpansion(service,{'relationships':[rule(),cycle]}).collect([initial()]) + assert len(db.calls)==2 + db.calls.clear() + service=ProjectResearch({},database=db) + _,facts,_,_=ContextExpansion(service,{'relationships':[rule()],'max_queries':1}).collect([initial()]) + assert len(db.calls)==1 and facts[-1]['value']['omitted_queries']==1 + _,facts,_,_=ContextExpansion(service,{'relationships':[rule(),rule('required')],'max_relationships':1}).collect([initial()]) + assert facts[-1]['value']['omitted_relationships']==1 + + +def test_depth_limit_does_not_claim_complete_graph(): + service=ProjectResearch({},database=RelatedData()) + further={'from_table':'devices','from_id_column':'id','from_column':'id','relation':'device_detail','table':'other','column':'id','columns':['id']} + _,facts,_,_=ContextExpansion(service,{'relationships':[rule(),further],'max_depth':1}).collect([initial()]) + assert facts[-1]['value']['depth_limited_relationships']==1 + assert 'not_exhaustive' in facts[-1]['value']['meaning'] + + +def test_missing_relationship_rows_remain_uninterpreted(): + service=ProjectResearch({},database=RelatedData()) + absent=rule();absent['table']='unknown_devices' + _,facts,_,entities=ContextExpansion(service,{'relationships':[absent]}).collect([initial()]) + results=[f for f in facts if f['name']=='project_query'] + assert results[-1]['value']['result']['items']==[] + assert entities[0]['id']=='22' # Referenced identity survives missing metadata. + assert 'root_cause' not in str(facts) + + +def test_script_search_uses_bound_symbol_not_numeric_grep(): + db=RelatedData() + class Source: + def binding(self):return {'commit':'fixture'} + def validate(self,q):assert q['kind']=='source.symbol' + def query(self,q):return {'items':[{'path':'capture.cpp','line':3,'text':'class capture_handler {};'}],'coverage':'complete_selected_text_search','truncated':False} + service=ProjectResearch({},database=db,source=Source()) + script={'from_table':'devices','from_column':'script','from_id_column':'id','relation':'device_script','kind':'source.symbol','path_prefix':'src/'} + _,facts,context,_=ContextExpansion(service,{'relationships':[rule(),script]}).collect([initial()]) + assert len(context['source'])==1 + assert facts[2]['value']['query']['text']=='capture_handler' + + +def test_casebuilder_links_entities_and_preserves_prior_inputs(case_setup): + store=case_setup[0] + before=store.latest('case-A','stable-package') + with PackageReader(before) as reader:project=reader.manifest['project'] + service=ProjectResearch(project,database=RelatedData()) + service.enrichment={'queries':[initial()],'relationships':[rule()]} + built=CaseBuilder(store,project_research=service).build('case-A','stable-package') + with PackageReader(before) as old,PackageReader(built.package) as reader: + case=json.loads(reader.read_bytes(built.case_ref)) + assert any(e['type']=='device' and e['id']=='22' for e in case['entities']) + assert 'provided_device' in str(case['runtime_facts']) + for path in old.files: + if path!='manifest.json':assert old.read_bytes(path)==reader.read_bytes(path) + assert CaseBuilder(store,project_research=service).build('case-A','stable-package').reused + from viano_bug_analysis.lifecycle import State + for i,target in enumerate([State.INTERPRETING_CASE,State.GENERATING_HYPOTHESES,State.INVESTIGATING_HYPOTHESES, + State.BUILDING_DIAGNOSIS,State.OUTPUT_READY,State.HANDED_OFF,State.REOPENED]): + store.advance('case-A','stable-package',target,step_id=f'context-reopen-{i}') + reopened=CaseBuilder(store,project_research=service).build('case-A','stable-package') + assert reopened.analysis_revision==2 + with PackageReader(reopened.package) as reader: + case=json.loads(reader.read_bytes(reopened.case_ref)) + paths=[r['path'] for r in case['database_context']['refs']] + assert len(paths)==len(set(paths)) + + +@pytest.mark.parametrize('field',['max_queries','max_depth','max_relationships']) +def test_invalid_limits_rejected(field): + with pytest.raises(ToolError):ContextExpansion(ProjectResearch({}),{field:True})