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"
|
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"]
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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