Enrich cases with descriptive context and traced relationships

This commit is contained in:
Nico committed 2026-10-14 23:45:17 +02:00
1 parent 0dcd2ab6df
commit 87581e5cd2
5 files changed
+460 -18

No files matched your search

+152 -2
View File
@@ -16,7 +16,7 @@ user = "viano_reader"
password_file = "../service-data/research/world-db/reader.local.txt" password_file = "../service-data/research/world-db/reader.local.txt"
snapshot_id = "backup-sha256:4964f277ef5cf7f7f98bf4378971dcb55180bac813d49bcda6d2f03ff6969662" snapshot_id = "backup-sha256:4964f277ef5cf7f7f98bf4378971dcb55180bac813d49bcda6d2f03ff6969662"
# Explicit allowlist: no account or character tables. # 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] [limits]
max_rows = 50 max_rows = 50
@@ -28,6 +28,17 @@ max_lines = 80
timeout_seconds = 15 timeout_seconds = 15
max_cached_queries = 256 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]] [[enrichment.queries]]
kind = "source.schema" kind = "source.schema"
text = "CREATE TABLE `creature`" text = "CREATE TABLE `creature`"
@@ -56,5 +67,144 @@ limit = 1
entity_type = "quest" entity_type = "quest"
table = "quest_template" table = "quest_template"
column = "ID" 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 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"]
+38
View File
@@ -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 BUG-000020-spezifische Diagnose oder Wolpertinger-ID. Die LLM kann während der Untersuchung
weitere Tabellen und Source-Ausschnitte gezielt abfragen. 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 Mit aktivierter Projekt-Recherche wird `investigation_project_v1` verwendet. Der volle
Context bleibt unverändert archiviert. Der Prompt lässt Input-Inventar und doppelte 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. vorige Resultate weg; fremde Hypothesen behalten Evidenz-IDs/Provenienz, aber keine Payloads.
+12 -16
View File
@@ -1,6 +1,5 @@
"""Deterministic local facts collected before CaseBuilder seals a revision.""" """Deterministic local facts collected before CaseBuilder seals a revision."""
import copy import copy
import hashlib
from viano_bug_analysis.case import CaseAssembler from viano_bug_analysis.case import CaseAssembler
from viano_bug_analysis.case.contracts import read_json from viano_bug_analysis.case.contracts import read_json
@@ -38,28 +37,25 @@ class ProjectCaseEnricher:
queries.append({"kind":"database.rows", "table":rule["table"], queries.append({"kind":"database.rows", "table":rule["table"],
"columns":rule["columns"], "filters":[{"column":rule["column"], "operator":"eq", "value":value}], "columns":rule["columns"], "filters":[{"column":rule["column"], "operator":"eq", "value":value}],
"offset":0, "limit":rule.get("limit", 5)}) "offset":0, "limit":rule.get("limit", 5)})
if len(queries) > 40: from .context_expansion import ContextExpansion
raise ToolError("PROJECT_ENRICHMENT_QUERY_LIMIT") additions, facts, context, linked_entities = ContextExpansion(
additions, facts, context = {}, [], {"database":[], "source":[]} self.service, getattr(self.service,"enrichment",{})).collect(queries)
for query in queries: capture_refs = list(additions)
data = self.service.collect(query) for ref, data in list(additions.items()):
import json if ref in reader.files:
capture = json.loads(data) if reader.read_bytes(ref) != data:
ref = "research/project/" + hashlib.sha256(data).hexdigest() + "/capture.json" raise ToolError("PROJECT_CAPTURE_CHANGED")
additions[ref] = data del additions[ref]
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}})
index_ref = m["artifacts"].get("case_inputs") 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} 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}" base = f"research/project/revisions/{m['analysis_revision']:04d}"
facts_ref = base + "/facts.json" facts_ref = base + "/facts.json"
additions[facts_ref] = json_bytes({"format":"viano-reproduction", "format_version":1, 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) index.setdefault("reproductions", []).append(facts_ref)
for role, refs in context.items(): 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" new_index = base + "/case-inputs.json"
additions[new_index] = json_bytes(index) additions[new_index] = json_bytes(index)
generation = m["package_generation"] generation = m["package_generation"]
@@ -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
+135
View File
@@ -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})