Enrich cases with descriptive context and traced relationships
This commit is contained in:
1 parent
0dcd2ab6df
commit
87581e5cd2
5 files changed
+460
-18
No files matched your search
+152
-2
@@ -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"]
|
||||
@@ -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.
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
@@ -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})
|
||||
Reference in new issue
Block a user