projector.py 80 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056
  1. from __future__ import annotations
  2. import re
  3. from copy import deepcopy
  4. from typing import Any
  5. from ..card_details import CardBusinessDataProjector, CardDataNotFound
  6. from ..card_details.common import find_detail
  7. from ..inspector_projection import project_activity_detail, project_event_detail
  8. from ..module_audit.log_context import locate_log_module
  9. from ..retrieval_detail_projection import (
  10. QUERY_TOOL_NAMES,
  11. RETRIEVAL_RESULT_COLLECTION_KEYS,
  12. )
  13. from .comparison import align_business_projection, business_only_projection
  14. from .resolver import SourceResolver, pointer_value
  15. from .schema import SourceBinding, unresolved_binding
  16. class InspectorViewNotFound(LookupError):
  17. pass
  18. class InspectorWorkbenchProjector:
  19. """Application service that makes all three Inspector columns together."""
  20. def __init__(self) -> None:
  21. self._card_projector = CardBusinessDataProjector()
  22. def related_event_ids(
  23. self, detail_ref: str, *, bundle: dict[str, Any], view: dict[str, Any]
  24. ) -> list[int]:
  25. ref = str(detail_ref or "")
  26. ids: set[int] = set()
  27. if ref.startswith("event:"):
  28. value = _suffix_int(ref)
  29. if value:
  30. ids.add(value)
  31. node = find_detail(view, ref)
  32. _collect_event_ids(node, ids)
  33. if ref == "run:objective":
  34. _collect_event_ids(view.get("objective"), ids)
  35. round_match = re.fullmatch(r"round:(\d+):(goal|plan|result|evaluation)", ref)
  36. if round_match:
  37. round_index = int(round_match.group(1))
  38. round_ = next(
  39. (
  40. item
  41. for item in view.get("rounds") or []
  42. if _integer(item.get("roundIndex")) == round_index
  43. ),
  44. {},
  45. )
  46. role = round_match.group(2)
  47. target = {
  48. "goal": round_.get("goal"),
  49. "plan": (round_.get("plan") or {}).get("node"),
  50. "result": round_.get("overallEvaluation"),
  51. "evaluation": round_.get("overallEvaluation"),
  52. }.get(role)
  53. _collect_event_ids(target, ids)
  54. event_names = {
  55. "goal": {"begin_round"},
  56. "plan": {"record_multipath_plan"},
  57. }.get(role, set())
  58. for event in bundle.get("events") or []:
  59. if (
  60. event.get("event_name") in event_names
  61. and (
  62. role == "goal"
  63. or _integer(event.get("round_index")) == round_index
  64. )
  65. ):
  66. ids.add(_integer(event.get("id")) or 0)
  67. branch_match = re.fullmatch(
  68. r"round:(\d+):branch:(\d+):(task|retrieval|output)", ref
  69. )
  70. if branch_match:
  71. round_index, branch_id = int(branch_match.group(1)), int(branch_match.group(2))
  72. branch = _view_branch(view, round_index, branch_id)
  73. _collect_event_ids(branch, ids)
  74. if ref.startswith("creative:"):
  75. seed_id = _suffix_int(ref)
  76. seed = _bundle_event(bundle, seed_id)
  77. round_index = _integer((seed or {}).get("round_index"))
  78. branch_id = _integer((seed or {}).get("branch_id"))
  79. implementers = {
  80. _integer(event.get("id"))
  81. for event in bundle.get("events") or []
  82. if event.get("event_name") == "script_implementer"
  83. and _integer(event.get("round_index")) == round_index
  84. and _integer(event.get("branch_id")) == branch_id
  85. }
  86. for event in bundle.get("events") or []:
  87. if _integer(event.get("round_index")) != round_index or _integer(event.get("branch_id")) != branch_id:
  88. continue
  89. event_id = _integer(event.get("id"))
  90. if event_id in implementers or _integer(event.get("scope_event_id")) in implementers:
  91. ids.add(event_id or 0)
  92. if ref.startswith("multipath-decision:"):
  93. decision_id = _suffix_int(ref)
  94. decision = next(
  95. (
  96. item
  97. for item in bundle.get("multipathDecisions") or []
  98. if _integer(item.get("id")) == decision_id
  99. ),
  100. {},
  101. )
  102. round_index = _integer(decision.get("round_index"))
  103. for event in bundle.get("events") or []:
  104. if (
  105. event.get("event_name") == "script_multipath_evaluator"
  106. and _integer(event.get("round_index")) == round_index
  107. ):
  108. ids.add(_integer(event.get("id")) or 0)
  109. if ref.startswith("retrieval-agent:"):
  110. agent_id = _suffix_int(ref)
  111. for event in bundle.get("events") or []:
  112. if (
  113. _integer(event.get("id")) == agent_id
  114. or _integer(event.get("parent_event_id")) == agent_id
  115. or _integer(event.get("scope_event_id")) == agent_id
  116. ):
  117. ids.add(_integer(event.get("id")) or 0)
  118. return sorted(value for value in ids if value > 0)
  119. def project(
  120. self,
  121. detail_ref: str,
  122. *,
  123. script_build_id: int,
  124. bundle: dict[str, Any],
  125. view: dict[str, Any],
  126. event_details: dict[int, dict[str, Any]],
  127. log_contents: dict[int, str] | None = None,
  128. ) -> dict[str, Any]:
  129. ref = str(detail_ref or "").strip()
  130. def event_loader(event_id: int) -> dict[str, Any]:
  131. event = event_details.get(int(event_id))
  132. if event is None:
  133. raise KeyError(event_id)
  134. return event
  135. business = self._business_detail(
  136. ref,
  137. script_build_id=script_build_id,
  138. bundle=bundle,
  139. view=view,
  140. event_details=event_details,
  141. )
  142. try:
  143. card_data = self._card_projector.project(
  144. ref,
  145. bundle=bundle,
  146. view=view,
  147. load_event=event_loader,
  148. )
  149. except (CardDataNotFound, KeyError):
  150. card_data = _fallback_card_data(ref, business)
  151. resolver = SourceResolver(bundle=bundle, event_details=event_details)
  152. source_modules = self._modules(ref, business, card_data, resolver, event_details, bundle)
  153. if not source_modules:
  154. raise InspectorViewNotFound(f"未找到活动 {ref}")
  155. alignment_kind = (
  156. "retrieval-event"
  157. if business.get("detailKind") == "retrieval-event"
  158. else str(card_data.get("cardKind") or _fallback_card_kind(ref))
  159. )
  160. modules = align_business_projection(
  161. detail_ref=ref,
  162. card_kind=alignment_kind,
  163. business=business,
  164. source_modules=source_modules,
  165. resolver=resolver,
  166. bundle=bundle,
  167. )
  168. if not modules:
  169. raise InspectorViewNotFound(f"活动 {ref} 没有可展示的业务详情")
  170. if log_contents:
  171. log_refs: dict[int, str] = {}
  172. for event_id, event in event_details.items():
  173. log_module = locate_log_module(log_contents.get(event_id) or "", event)
  174. if log_module:
  175. log_refs[event_id] = resolver.add_log_anchor(
  176. event_id,
  177. log_module,
  178. event_msg_id=str(event.get("msg_id") or ""),
  179. )
  180. for module in modules:
  181. runtime_refs = list(module.get("runtimeRefs") or [])
  182. for event_id, log_ref in log_refs.items():
  183. if f"event:{event_id}" in runtime_refs and log_ref not in runtime_refs:
  184. runtime_refs.append(log_ref)
  185. module["runtimeRefs"] = runtime_refs
  186. completeness = str(card_data.get("completeness") or "partial")
  187. if any(
  188. binding.get("resolution") != "resolved"
  189. for binding in _all_bindings(modules)
  190. ):
  191. completeness = "partial" if completeness != "missing" else "missing"
  192. notices = [
  193. *[item for item in card_data.get("notices") or [] if isinstance(item, dict)],
  194. *_business_notices(business),
  195. ]
  196. used_source_ids = _used_source_ids(modules)
  197. return {
  198. "schemaVersion": "inspector-source-v2",
  199. "detailRef": ref,
  200. "cardKind": card_data.get("cardKind") or _fallback_card_kind(ref),
  201. "completeness": completeness,
  202. "businessProjection": business_only_projection(business),
  203. "modules": modules,
  204. "sources": {
  205. source_id: source
  206. for source_id, source in resolver.sources.items()
  207. if source_id in used_source_ids
  208. },
  209. "notices": _unique_notices(notices),
  210. }
  211. def _business_detail(
  212. self,
  213. detail_ref: str,
  214. *,
  215. script_build_id: int,
  216. bundle: dict[str, Any],
  217. view: dict[str, Any],
  218. event_details: dict[int, dict[str, Any]],
  219. ) -> dict[str, Any]:
  220. if detail_ref.startswith("event:"):
  221. event_id = _suffix_int(detail_ref)
  222. event = event_details.get(event_id or -1)
  223. if event is None:
  224. raise InspectorViewNotFound(f"未找到运行事件 {event_id}")
  225. try:
  226. return project_activity_detail(
  227. script_build_id,
  228. detail_ref,
  229. view,
  230. bundle,
  231. event_detail=event,
  232. )
  233. except KeyError:
  234. return project_event_detail(
  235. script_build_id,
  236. event,
  237. run_status=((view.get("header") or {}).get("status")),
  238. )
  239. event_detail = None
  240. if detail_ref.startswith("retrieval-agent:"):
  241. run = find_detail(view, detail_ref) or {}
  242. event_detail = event_details.get(_integer(run.get("eventId")) or -1)
  243. try:
  244. return project_activity_detail(
  245. script_build_id,
  246. detail_ref,
  247. view,
  248. bundle,
  249. event_detail=event_detail,
  250. )
  251. except KeyError as exc:
  252. raise InspectorViewNotFound(f"未找到活动 {detail_ref}") from exc
  253. def _modules(
  254. self,
  255. detail_ref: str,
  256. business: dict[str, Any],
  257. card_data: dict[str, Any],
  258. resolver: SourceResolver,
  259. event_details: dict[int, dict[str, Any]],
  260. bundle: dict[str, Any],
  261. ) -> list[dict[str, Any]]:
  262. if detail_ref.startswith("retrieval-direct:"):
  263. return _direct_group_modules(card_data, resolver, event_details)
  264. if detail_ref.startswith("retrieval-agent:"):
  265. return _retrieval_agent_modules(card_data, resolver, event_details)
  266. special = self._event_modules(detail_ref, business, resolver, event_details)
  267. if special is not None:
  268. return special
  269. business_values = _business_modules(business)
  270. planned = [
  271. item
  272. for item in (card_data.get("displayUse") or {}).get("businessModules") or []
  273. if isinstance(item, dict)
  274. ]
  275. fields = {
  276. str(item.get("id")): item
  277. for item in [
  278. *(card_data.get("businessInputs") or []),
  279. *(card_data.get("businessOutputs") or []),
  280. ]
  281. if isinstance(item, dict) and item.get("id")
  282. }
  283. if card_data.get("cardKind") == "implementation-task" and not any(
  284. str((field.get("source") or {}).get("kind") or "") == "runtime-event"
  285. for field in fields.values()
  286. ):
  287. return _historical_implementation_task_modules(fields, resolver)
  288. modules: list[dict[str, Any]] = []
  289. consumed: set[str] = set()
  290. for index, plan in enumerate(planned, 1):
  291. module_id = str(plan.get("id") or f"module-{index}")
  292. title = str(plan.get("title") or f"业务模块 {index}")
  293. business_module = _match_business_module(business_values, module_id, title)
  294. if business_module:
  295. consumed.add(str(business_module.get("id")))
  296. source_ids = [str(value) for value in plan.get("sourceIds") or []]
  297. bindings = [
  298. resolver.binding_for_field(
  299. fields[source_id],
  300. binding_id=f"{module_id}:source:{position}",
  301. )
  302. for position, source_id in enumerate(source_ids, 1)
  303. if source_id in fields
  304. ]
  305. if card_data.get("cardKind") == "implementation-task" and business_module:
  306. task_field = next(
  307. (
  308. fields[source_id]
  309. for source_id in source_ids
  310. if source_id in fields
  311. and str((fields[source_id].get("source") or {}).get("kind")) == "runtime-event"
  312. and str((fields[source_id].get("source") or {}).get("fieldPath")) == "input.content.task"
  313. ),
  314. None,
  315. )
  316. task_event_id = _suffix_int(str((task_field or {}).get("source", {}).get("ref") or ""))
  317. if task_event_id and _module_value(business_module):
  318. bindings = [
  319. resolver.text_span_binding(
  320. task_event_id,
  321. binding_id=f"{module_id}:task-span",
  322. path="input.content.task",
  323. exact_text=_module_value(business_module),
  324. role="input",
  325. ),
  326. *[
  327. binding
  328. for binding in bindings
  329. if binding.get("sourceId") != f"event:{task_event_id}"
  330. ],
  331. ]
  332. aggregate = _aggregate_binding(
  333. str(card_data.get("cardKind") or ""),
  334. module_id,
  335. source_ids,
  336. fields,
  337. resolver,
  338. bundle,
  339. )
  340. if aggregate is not None:
  341. bindings = [aggregate]
  342. if module_id == "process" and not bindings:
  343. bindings = [
  344. resolver.binding_for_event(
  345. event_id,
  346. binding_id=f"{module_id}:event:{event_id}",
  347. role="process",
  348. )
  349. for event_id in sorted(event_details)
  350. ]
  351. if card_data.get("cardKind") == "multipath-decision" and module_id == "outcomes":
  352. for binding in bindings:
  353. source_record = resolver.sources.get(str(binding.get("sourceId") or "")) or {}
  354. raw_record = source_record.get("rawRecord")
  355. branch_status = str((raw_record or {}).get("status") or "").lower() if isinstance(raw_record, dict) else ""
  356. if branch_status in {"merged", "adopted", "accepted", "selected"}:
  357. binding["evidence"]["adoption"] = "explicitly-adopted"
  358. binding["evidence"]["confidence"] = "exact"
  359. elif branch_status in {"discarded", "rejected", "not_adopted"}:
  360. binding["evidence"]["adoption"] = "explicitly-rejected"
  361. binding["evidence"]["confidence"] = "exact"
  362. if not bindings:
  363. binding = unresolved_binding(
  364. f"{module_id}:unresolved",
  365. reason=f"模块「{title}」没有保存可安全定位的原始记录",
  366. code="binding-map-missing",
  367. )
  368. resolver.add_unresolved_source(binding)
  369. bindings = [binding]
  370. value = _module_value(business_module)
  371. if value in (None, "", [], {}) and source_ids:
  372. values = [fields[source_id].get("value") for source_id in source_ids if source_id in fields]
  373. value = values[0] if len(values) == 1 else values
  374. _align_transforms_with_business(value, bindings, resolver, module_id)
  375. module = {
  376. "id": module_id,
  377. "title": title,
  378. "presentation": _presentation(business_module, title),
  379. "business": {"value": value},
  380. "bindings": bindings,
  381. "runtimeRefs": _runtime_refs(bindings),
  382. "defaultOpen": index <= 3,
  383. }
  384. if business_module and business_module.get("items"):
  385. business_items = _normalize_items(business_module.get("items") or [])
  386. item_rows = []
  387. child_bindings: list[SourceBinding] = []
  388. for item_index, item in enumerate(business_items, 1):
  389. item_bindings = (
  390. [_multipath_comparison_binding(
  391. item,
  392. item_index,
  393. bindings,
  394. resolver,
  395. bundle,
  396. )]
  397. if card_data.get("cardKind") == "multipath-decision" and module_id == "comparison"
  398. else _item_bindings(
  399. module_id,
  400. item_index,
  401. item,
  402. len(business_items),
  403. bindings,
  404. resolver,
  405. )
  406. )
  407. child_bindings.extend(item_bindings)
  408. item_rows.append(
  409. {
  410. "id": f"{module_id}:item:{item_index}",
  411. "label": item.get("label"),
  412. "value": item.get("value"),
  413. "bindings": item_bindings,
  414. }
  415. )
  416. module["business"] = {"items": item_rows}
  417. module["bindings"] = _unique_bindings(child_bindings)
  418. module["runtimeRefs"] = _runtime_refs(module["bindings"])
  419. elif business_module and business_module.get("steps"):
  420. step_rows = []
  421. step_bindings: list[SourceBinding] = []
  422. for step_index, step in enumerate(business_module.get("steps") or [], 1):
  423. if not isinstance(step, dict):
  424. continue
  425. step_event_id = _suffix_int(str(step.get("eventRef") or ""))
  426. if step_event_id and step_event_id in event_details:
  427. step_binding = resolver.binding_for_event(
  428. step_event_id,
  429. binding_id=f"{module_id}:step:{step_index}",
  430. path="",
  431. role="process",
  432. )
  433. step_binding["transform"] = {
  434. "kind": "parsed",
  435. "operation": "creative-step-v1",
  436. "outputKey": str(step_index),
  437. }
  438. step_binding["evidence"]["confidence"] = "deterministic"
  439. else:
  440. step_binding = unresolved_binding(
  441. f"{module_id}:step:{step_index}:unresolved",
  442. reason=f"创作步骤 {step_index} 没有保存可安全关联的 Event",
  443. source_id=(f"event:{step_event_id}" if step_event_id else None),
  444. resolution="unsafe",
  445. code="unsafe-association",
  446. )
  447. if step_binding["sourceId"] not in resolver.sources:
  448. resolver.add_unresolved_source(step_binding)
  449. step_bindings.append(step_binding)
  450. step_rows.append({
  451. "id": f"{module_id}:step:{step_index}",
  452. "label": step.get("action") or f"步骤 {step_index}",
  453. "value": step,
  454. "bindings": [step_binding],
  455. })
  456. module["business"] = {"items": step_rows}
  457. module["bindings"] = step_bindings
  458. module["runtimeRefs"] = _runtime_refs(step_bindings)
  459. modules.append(module)
  460. # The formal Inspector exposes only modules declared by the unified
  461. # card contract. Legacy business blocks are an input adapter, not a
  462. # second set of UI modules; appending unmatched blocks here created
  463. # duplicates such as Goal + “本轮目标构成” with no trustworthy binding.
  464. return modules
  465. def _event_modules(
  466. self,
  467. detail_ref: str,
  468. business: dict[str, Any],
  469. resolver: SourceResolver,
  470. event_details: dict[int, dict[str, Any]],
  471. ) -> list[dict[str, Any]] | None:
  472. if not detail_ref.startswith("event:"):
  473. return None
  474. event_id = _suffix_int(detail_ref)
  475. event = event_details.get(event_id or -1)
  476. if event is None:
  477. return None
  478. if business.get("detailKind") == "retrieval-event":
  479. return _retrieval_event_modules(business, event, resolver)
  480. if event.get("event_name") == "script_implementer":
  481. input_side = event.get("input") if isinstance(event.get("input"), dict) else {}
  482. content = input_side.get("content") if isinstance(input_side, dict) else None
  483. task = content.get("task") if isinstance(content, dict) else content
  484. sections = {
  485. str(item.get("id")): item
  486. for item in business.get("businessSections") or []
  487. if isinstance(item, dict)
  488. }
  489. action_binding = resolver.binding_for_event(
  490. event_id or 0,
  491. binding_id="action:event",
  492. path="event_name",
  493. role="process",
  494. )
  495. action_binding["transform"] = {
  496. "kind": "parsed",
  497. "operation": "implementation-activity-label-v1",
  498. "outputKey": "action",
  499. }
  500. action_binding["evidence"]["confidence"] = "deterministic"
  501. request_binding = resolver.binding_for_event(
  502. event_id or 0,
  503. binding_id="request:event",
  504. path="input.content.task",
  505. role="input",
  506. availability="direct-read",
  507. )
  508. request_binding["transform"] = {
  509. "kind": "parsed",
  510. "operation": "implementation-task-readable-sections-v1",
  511. "outputKey": "request",
  512. }
  513. request_binding["evidence"]["confidence"] = "deterministic"
  514. result_value = (sections.get("result") or {}).get("content")
  515. result_binding = resolver.text_span_binding(
  516. event_id or 0,
  517. binding_id="result:event",
  518. path="output.content.summary",
  519. exact_text=result_value,
  520. role="output",
  521. )
  522. return [
  523. {
  524. "id": "action",
  525. "title": "做了什么",
  526. "presentation": "text",
  527. "business": {"value": (sections.get("action") or {}).get("content")},
  528. "bindings": [action_binding],
  529. "runtimeRefs": [detail_ref],
  530. "defaultOpen": True,
  531. },
  532. {
  533. "id": "request",
  534. "title": "处理内容",
  535. "presentation": "text",
  536. "business": {"value": (sections.get("request") or {}).get("content")},
  537. "bindings": [request_binding],
  538. "runtimeRefs": [detail_ref],
  539. "defaultOpen": True,
  540. },
  541. {
  542. "id": "result",
  543. "title": "得到什么",
  544. "presentation": "text",
  545. "business": {"value": result_value},
  546. "bindings": [result_binding],
  547. "runtimeRefs": [detail_ref],
  548. "defaultOpen": True,
  549. },
  550. ]
  551. values = _business_modules(business)
  552. planning_paths = {
  553. "summary": "input.content.thought_summary",
  554. "thought": "input.content.thought",
  555. "plan": "input.content.plan",
  556. "action": "input.content.action",
  557. }
  558. modules: list[dict[str, Any]] = []
  559. is_planning = (
  560. event.get("event_name") == "think_and_plan"
  561. and event.get("agent_role") == "main"
  562. and _integer(event.get("agent_depth")) == 0
  563. )
  564. is_review = event.get("event_name") in {"script_multipath_evaluator", "script_evaluator"}
  565. if not is_planning and not is_review:
  566. return _generic_event_modules(
  567. detail_ref, business, event, resolver, event_id or 0
  568. )
  569. for index, item in enumerate(values, 1):
  570. module_id = str(item.get("id") or f"event-module-{index}")
  571. title = str(item.get("title") or f"运行模块 {index}")
  572. value = _module_value(item)
  573. input_title = title in {"评审范围", "评审标准", "评审对象", "标准"}
  574. if is_planning and module_id in planning_paths:
  575. binding = resolver.binding_for_event(
  576. event_id or 0,
  577. binding_id=f"{module_id}:event:{event_id}",
  578. path=planning_paths[module_id],
  579. role="input",
  580. availability="direct-read",
  581. )
  582. elif title == "记录说明" and not event.get("output"):
  583. binding = resolver.binding_for_event(
  584. event_id or 0,
  585. binding_id=f"{module_id}:event:{event_id}",
  586. role="status",
  587. availability="produced-by-run",
  588. )
  589. binding["transform"] = {
  590. "kind": "calculated",
  591. "operation": "runtime-missing-decision-notice-v1",
  592. }
  593. binding["evidence"]["confidence"] = "deterministic"
  594. else:
  595. side = "input" if input_title else "output"
  596. binding = resolver.text_span_binding(
  597. event_id or 0,
  598. binding_id=f"{module_id}:event:{event_id}",
  599. path=_event_text_path(event, side),
  600. exact_text=value,
  601. role="input" if input_title else "output",
  602. )
  603. business_payload: dict[str, Any] = {"value": value}
  604. bindings = [binding]
  605. if item.get("items"):
  606. rows = []
  607. bindings = []
  608. for item_index, child in enumerate(_normalize_items(item.get("items") or []), 1):
  609. child_bindings = []
  610. for leaf_index, leaf in enumerate(_leaf_texts(child.get("value")), 1):
  611. child_bindings.append(resolver.text_span_binding(
  612. event_id or 0,
  613. binding_id=f"{module_id}:item:{item_index}:leaf:{leaf_index}",
  614. path=_event_text_path(event, "input" if input_title else "output"),
  615. exact_text=leaf,
  616. role="input" if input_title else "output",
  617. ))
  618. if not child_bindings:
  619. fallback = resolver.binding_for_event(
  620. event_id or 0,
  621. binding_id=f"{module_id}:item:{item_index}:record",
  622. path=_event_text_path(event, "input" if input_title else "output"),
  623. role="input" if input_title else "output",
  624. availability="direct-read" if input_title else "returned-to-agent",
  625. )
  626. fallback["transform"] = {
  627. "kind": "parsed",
  628. "operation": "review-item-v1",
  629. "outputKey": str(item_index),
  630. }
  631. fallback["evidence"]["confidence"] = "deterministic"
  632. child_bindings = [fallback]
  633. bindings.extend(child_bindings)
  634. rows.append(
  635. {
  636. "id": f"{module_id}:item:{item_index}",
  637. "label": child.get("label"),
  638. "value": child.get("value"),
  639. "bindings": child_bindings,
  640. }
  641. )
  642. business_payload = {"items": rows}
  643. modules.append(
  644. {
  645. "id": module_id,
  646. "title": title,
  647. "presentation": _presentation(item, title),
  648. "business": business_payload,
  649. "bindings": bindings,
  650. "runtimeRefs": [detail_ref],
  651. "defaultOpen": index <= 3,
  652. }
  653. )
  654. return modules
  655. def _generic_event_modules(
  656. detail_ref: str,
  657. business: dict[str, Any],
  658. event: dict[str, Any],
  659. resolver: SourceResolver,
  660. event_id: int,
  661. ) -> list[dict[str, Any]] | None:
  662. """Bind the readable action/request/result projection to the real Event Body."""
  663. sections = [
  664. item
  665. for item in business.get("businessSections") or []
  666. if isinstance(item, dict)
  667. ]
  668. if not sections:
  669. return None
  670. modules: list[dict[str, Any]] = []
  671. for index, section in enumerate(sections, 1):
  672. module_id = str(section.get("id") or f"section-{index}")
  673. value = section.get("content")
  674. if module_id == "action":
  675. path, role, availability = "event_name", "process", "produced-by-run"
  676. operation = "runtime-activity-label-v1"
  677. elif module_id == "request":
  678. path, role, availability = _event_text_path(event, "input"), "input", "direct-read"
  679. operation = "runtime-request-readable-projection-v1"
  680. elif module_id == "result":
  681. path, role, availability = _event_text_path(event, "output"), "output", "returned-to-agent"
  682. operation = "runtime-result-readable-projection-v1"
  683. else:
  684. # The section is visibly derived from this Event, but there is no
  685. # card-specific field contract. Bind the complete Event instead of
  686. # falsely claiming that its original record is missing.
  687. path, role, availability = "", "process", "produced-by-run"
  688. operation = "runtime-business-section-v1"
  689. binding = resolver.binding_for_event(
  690. event_id,
  691. binding_id=f"{module_id}:event:{event_id}",
  692. path=path,
  693. role=role,
  694. availability=availability,
  695. )
  696. binding["transform"] = {
  697. "kind": "parsed",
  698. "operation": operation,
  699. "outputKey": module_id,
  700. }
  701. binding["evidence"]["confidence"] = "deterministic"
  702. modules.append({
  703. "id": module_id,
  704. "title": str(section.get("title") or f"业务模块 {index}"),
  705. "presentation": "text",
  706. "business": {"value": value},
  707. "bindings": [binding],
  708. "runtimeRefs": [detail_ref],
  709. "defaultOpen": True,
  710. })
  711. return modules
  712. def _retrieval_event_modules(
  713. business: dict[str, Any], event: dict[str, Any], resolver: SourceResolver
  714. ) -> list[dict[str, Any]]:
  715. event_id = _integer(event.get("id")) or 0
  716. conditions = business.get("queryConditions") or []
  717. condition_items = []
  718. condition_bindings: list[SourceBinding] = []
  719. for index, item in enumerate(conditions, 1):
  720. key = str(item.get("key") or index)
  721. binding = resolver.binding_for_event(
  722. event_id,
  723. binding_id=f"purpose:{key}",
  724. path=f"input.content.{key}",
  725. role="input",
  726. availability="direct-read",
  727. )
  728. condition_bindings.append(binding)
  729. condition_items.append(
  730. {
  731. "id": f"purpose:item:{key}",
  732. "label": item.get("label"),
  733. "value": item.get("value"),
  734. "bindings": [binding],
  735. }
  736. )
  737. if not condition_bindings:
  738. binding = resolver.binding_for_event(
  739. event_id,
  740. binding_id="purpose:event-input",
  741. path="input.content",
  742. role="input",
  743. availability="direct-read",
  744. )
  745. condition_bindings = [binding]
  746. name = str(event.get("event_name") or "")
  747. result_key = RETRIEVAL_RESULT_COLLECTION_KEYS.get(name)
  748. root = "output.content" + (f".{result_key}" if result_key else "")
  749. root_value = pointer_value(event, f"/{root.replace('.', '/')}")
  750. result_items = []
  751. result_bindings: list[SourceBinding] = []
  752. for index, item in enumerate(business.get("items") or [], 1):
  753. item_path = (
  754. f"{root}.{index - 1}"
  755. if isinstance(root_value, list)
  756. else root
  757. )
  758. binding = resolver.binding_for_event(
  759. event_id,
  760. binding_id=f"result:item:{index}",
  761. path=item_path,
  762. role="output",
  763. availability="returned-to-agent",
  764. )
  765. binding["transform"] = {
  766. "kind": "parsed",
  767. "operation": "retrieval-result-item-v1",
  768. "outputKey": str(index - 1),
  769. }
  770. binding["evidence"]["confidence"] = "deterministic"
  771. result_bindings.append(binding)
  772. result_items.append(
  773. {
  774. "id": f"result:item:{index}",
  775. "label": item.get("title"),
  776. "value": item,
  777. "bindings": [binding],
  778. }
  779. )
  780. if not result_bindings:
  781. empty_result = resolver.binding_for_event(
  782. event_id,
  783. binding_id="result:event-output",
  784. path="output.content",
  785. role="output",
  786. availability="returned-to-agent",
  787. )
  788. empty_result["transform"] = {
  789. "kind": "parsed",
  790. "operation": "retrieval-empty-or-failure-message-v1",
  791. "outputKey": "message",
  792. }
  793. empty_result["evidence"]["confidence"] = "deterministic"
  794. result_bindings = [empty_result]
  795. status_bindings = _event_status_bindings(
  796. resolver, event_id, prefix="status", include_output=True
  797. )
  798. event_source = resolver.sources.get(f"event:{event_id}")
  799. if event_source is not None and str((business.get("outcome") or {}).get("state")) == "empty":
  800. event_source["resultState"] = "empty"
  801. return [
  802. {
  803. "id": "purpose",
  804. "title": "目的 / 查询条件",
  805. "presentation": "items",
  806. "business": {"items": condition_items} if condition_items else {"value": "未记录调用输入"},
  807. "bindings": condition_bindings,
  808. "runtimeRefs": [f"event:{event_id}"],
  809. "defaultOpen": True,
  810. },
  811. {
  812. "id": "result",
  813. "title": "返回内容 / 结果列表",
  814. "presentation": "items",
  815. "business": {"items": result_items} if result_items else {"value": (business.get("outcome") or {}).get("message")},
  816. "bindings": result_bindings,
  817. "runtimeRefs": [f"event:{event_id}"],
  818. "defaultOpen": True,
  819. },
  820. {
  821. "id": "status",
  822. "title": "状态、耗时与完整性",
  823. "presentation": "notice",
  824. "business": {"value": {"outcome": business.get("outcome"), "completeness": business.get("completeness")}},
  825. "bindings": status_bindings,
  826. "runtimeRefs": [f"event:{event_id}"],
  827. "defaultOpen": True,
  828. },
  829. ]
  830. def _direct_group_modules(
  831. card_data: dict[str, Any],
  832. resolver: SourceResolver,
  833. event_details: dict[int, dict[str, Any]],
  834. ) -> list[dict[str, Any]]:
  835. events = [event_details[event_id] for event_id in sorted(event_details)]
  836. def rows(kind: str) -> tuple[list[dict[str, Any]], list[SourceBinding]]:
  837. values: list[dict[str, Any]] = []
  838. bindings: list[SourceBinding] = []
  839. for index, event in enumerate(events, 1):
  840. event_id = _integer(event.get("id")) or 0
  841. if kind == "input":
  842. path, role, availability = "input.content", "input", "direct-read"
  843. label = str(event.get("event_name") or f"调用 {index}")
  844. value = ((event.get("input") or {}).get("content") if isinstance(event.get("input"), dict) else None)
  845. elif kind == "output":
  846. path, role, availability = "output.content", "output", "returned-to-agent"
  847. label = str(event.get("event_name") or f"调用 {index}")
  848. value = ((event.get("output") or {}).get("content") if isinstance(event.get("output"), dict) else None)
  849. else:
  850. path, role, availability = "status", "status", "produced-by-run"
  851. label = str(event.get("event_name") or f"调用 {index}")
  852. value = {
  853. "status": event.get("status"),
  854. "durationMs": event.get("duration_ms"),
  855. "error": event.get("error_message"),
  856. }
  857. item_bindings = (
  858. _event_status_bindings(resolver, event_id, prefix=kind)
  859. if kind == "status"
  860. else [resolver.binding_for_event(
  861. event_id,
  862. binding_id=f"{kind}:event:{event_id}",
  863. path=path,
  864. role=role,
  865. availability=availability,
  866. )]
  867. )
  868. bindings.extend(item_bindings)
  869. values.append(
  870. {
  871. "id": f"{kind}:item:{event_id}",
  872. "label": label,
  873. "value": value,
  874. "bindings": item_bindings,
  875. }
  876. )
  877. return values, bindings
  878. input_rows, input_bindings = rows("input")
  879. output_rows, output_bindings = rows("output")
  880. status_rows, status_bindings = rows("status")
  881. failed_rows = [
  882. row
  883. for row in status_rows
  884. if str((row.get("value") or {}).get("status") or "").lower()
  885. in {"failed", "failure", "error", "interrupted"}
  886. ]
  887. failed_ids = {row["id"].rsplit(":", 1)[-1] for row in failed_rows}
  888. failed_bindings = [
  889. binding
  890. for binding in status_bindings
  891. if binding["id"].rsplit(":", 1)[-1] in failed_ids
  892. ]
  893. if not failed_rows:
  894. failed_bindings = [resolver.members_binding(
  895. {
  896. "id": "failures:none",
  897. "label": "失败调用数量",
  898. "value": 0,
  899. "relation": "persisted-output",
  900. },
  901. binding_id="failures:none-calculated",
  902. members=[{
  903. "id": f"failures:status:{event.get('id')}",
  904. "label": str(event.get("event_name") or event.get("id")),
  905. "value": event.get("status"),
  906. "source": {
  907. "kind": "runtime-event",
  908. "label": str(event.get("event_name") or f"Event {event.get('id')}"),
  909. "ref": f"event:{event.get('id')}",
  910. "fieldPath": "status",
  911. },
  912. "relation": "persisted-output",
  913. "completeness": "complete",
  914. } for event in events],
  915. reducer="no-failed-tool-status-v1",
  916. role="status",
  917. )]
  918. return [
  919. {
  920. "id": "purpose",
  921. "title": "读取目的",
  922. "presentation": "items",
  923. "business": {"items": input_rows},
  924. "bindings": input_bindings,
  925. "runtimeRefs": _runtime_refs(input_bindings),
  926. "defaultOpen": True,
  927. },
  928. {
  929. "id": "calls",
  930. "title": "逐次工具调用",
  931. "presentation": "items",
  932. "business": {"items": status_rows},
  933. "bindings": status_bindings,
  934. "runtimeRefs": _runtime_refs(status_bindings),
  935. "defaultOpen": True,
  936. },
  937. {
  938. "id": "results",
  939. "title": "实际返回的信息",
  940. "presentation": "items",
  941. "business": {"items": output_rows},
  942. "bindings": output_bindings,
  943. "runtimeRefs": _runtime_refs(output_bindings),
  944. "defaultOpen": True,
  945. },
  946. {
  947. "id": "failures",
  948. "title": "失败与缺失",
  949. "presentation": "notice",
  950. "business": {"items": failed_rows} if failed_rows else {"value": "未发现失败或中断的工具调用。"},
  951. "bindings": failed_bindings,
  952. "runtimeRefs": _runtime_refs(failed_bindings),
  953. "defaultOpen": bool(failed_rows),
  954. },
  955. ]
  956. def _retrieval_agent_modules(
  957. card_data: dict[str, Any],
  958. resolver: SourceResolver,
  959. event_details: dict[int, dict[str, Any]],
  960. ) -> list[dict[str, Any]]:
  961. events = [event_details[event_id] for event_id in sorted(event_details)]
  962. agent = next(
  963. (event for event in events if str(event.get("event_type") or "") == "agent_invoke"),
  964. events[0] if events else {},
  965. )
  966. agent_id = _integer(agent.get("id")) or 0
  967. children = [event for event in events if _integer(event.get("id")) != agent_id]
  968. queries = [
  969. event
  970. for event in children
  971. if str(event.get("event_name") or "") in QUERY_TOOL_NAMES
  972. ]
  973. task_binding = resolver.binding_for_event(
  974. agent_id,
  975. binding_id="task:agent-input",
  976. path="input.content.task",
  977. role="input",
  978. availability="direct-read",
  979. )
  980. screening_binding = resolver.binding_for_event(
  981. agent_id,
  982. binding_id="screening:agent-output",
  983. path="output.content",
  984. role="output",
  985. availability="produced-by-run",
  986. )
  987. task = ((agent.get("input") or {}).get("content") if isinstance(agent.get("input"), dict) else None)
  988. task = task.get("task") if isinstance(task, dict) else task
  989. screening = ((agent.get("output") or {}).get("content") if isinstance(agent.get("output"), dict) else None)
  990. query_rows: list[dict[str, Any]] = []
  991. result_rows: list[dict[str, Any]] = []
  992. status_rows: list[dict[str, Any]] = []
  993. query_bindings: list[SourceBinding] = []
  994. result_bindings: list[SourceBinding] = []
  995. status_bindings: list[SourceBinding] = []
  996. for index, event in enumerate(queries, 1):
  997. event_id = _integer(event.get("id")) or 0
  998. label = str(event.get("event_name") or f"查询 {index}")
  999. input_binding = resolver.binding_for_event(
  1000. event_id,
  1001. binding_id=f"queries:event:{event_id}",
  1002. path="input.content",
  1003. role="input",
  1004. availability="direct-read",
  1005. )
  1006. output_binding = resolver.binding_for_event(
  1007. event_id,
  1008. binding_id=f"raw:event:{event_id}",
  1009. path="output.content",
  1010. role="output",
  1011. availability="returned-to-agent",
  1012. )
  1013. status_item_bindings = _event_status_bindings(
  1014. resolver, event_id, prefix="failures"
  1015. )
  1016. query_bindings.append(input_binding)
  1017. result_bindings.append(output_binding)
  1018. status_bindings.extend(status_item_bindings)
  1019. query_rows.append({
  1020. "id": f"queries:item:{event_id}", "label": label,
  1021. "value": ((event.get("input") or {}).get("content") if isinstance(event.get("input"), dict) else None),
  1022. "bindings": [input_binding],
  1023. })
  1024. result_rows.append({
  1025. "id": f"raw:item:{event_id}", "label": label,
  1026. "value": ((event.get("output") or {}).get("content") if isinstance(event.get("output"), dict) else None),
  1027. "bindings": [output_binding],
  1028. })
  1029. status_rows.append({
  1030. "id": f"failures:item:{event_id}", "label": label,
  1031. "value": {"status": event.get("status"), "error": event.get("error_message")},
  1032. "bindings": status_item_bindings,
  1033. })
  1034. if not queries:
  1035. query_bindings = [
  1036. _calculation_binding(
  1037. resolver,
  1038. binding_id="queries:empty",
  1039. label="取数 Agent 查询事件计数",
  1040. value=[],
  1041. role="process",
  1042. )
  1043. ]
  1044. result_bindings = [
  1045. _calculation_binding(
  1046. resolver,
  1047. binding_id="raw:empty",
  1048. label="取数 Agent 原始命中计数",
  1049. value=[],
  1050. role="output",
  1051. )
  1052. ]
  1053. status_bindings = [
  1054. _calculation_binding(
  1055. resolver,
  1056. binding_id="failures:empty",
  1057. label="取数 Agent 失败查询计数",
  1058. value=[],
  1059. role="status",
  1060. )
  1061. ]
  1062. activity_rows: list[dict[str, Any]] = []
  1063. activity_bindings: list[SourceBinding] = []
  1064. for event in events:
  1065. event_id = _integer(event.get("id")) or 0
  1066. binding = resolver.binding_for_event(
  1067. event_id,
  1068. binding_id=f"activities:event:{event_id}",
  1069. role="process",
  1070. )
  1071. activity_bindings.append(binding)
  1072. activity_rows.append(
  1073. {
  1074. "id": f"activities:item:{event_id}",
  1075. "label": str(event.get("event_name") or f"Event {event_id}"),
  1076. "value": {
  1077. "eventName": event.get("event_name"),
  1078. "eventType": event.get("event_type"),
  1079. "status": event.get("status"),
  1080. },
  1081. "bindings": [binding],
  1082. }
  1083. )
  1084. return [
  1085. {
  1086. "id": "task", "title": "完整取数任务", "presentation": "text",
  1087. "business": {"value": task}, "bindings": [task_binding],
  1088. "runtimeRefs": [f"event:{agent_id}"], "defaultOpen": True,
  1089. },
  1090. {
  1091. "id": "queries", "title": "查询过程", "presentation": "items",
  1092. "business": {"items": query_rows}, "bindings": query_bindings,
  1093. "runtimeRefs": _runtime_refs(query_bindings), "defaultOpen": True,
  1094. },
  1095. {
  1096. "id": "raw", "title": "原始命中", "presentation": "items",
  1097. "business": {"items": result_rows}, "bindings": result_bindings,
  1098. "runtimeRefs": _runtime_refs(result_bindings), "defaultOpen": True,
  1099. },
  1100. {
  1101. "id": "screening", "title": "初筛整理", "presentation": "text",
  1102. "business": {"value": screening}, "bindings": [screening_binding],
  1103. "runtimeRefs": [f"event:{agent_id}"], "defaultOpen": True,
  1104. },
  1105. {
  1106. "id": "failures", "title": "失败与空结果", "presentation": "items",
  1107. "business": {"items": status_rows}, "bindings": status_bindings,
  1108. "runtimeRefs": _runtime_refs(status_bindings), "defaultOpen": False,
  1109. },
  1110. {
  1111. "id": "activities", "title": "运行记录", "presentation": "process",
  1112. "business": {"items": activity_rows}, "bindings": activity_bindings,
  1113. "runtimeRefs": _runtime_refs(activity_bindings), "defaultOpen": False,
  1114. },
  1115. ]
  1116. def _calculation_binding(
  1117. resolver: SourceResolver,
  1118. *,
  1119. binding_id: str,
  1120. label: str,
  1121. value: Any,
  1122. role: str,
  1123. ) -> SourceBinding:
  1124. binding = resolver.binding_for_field(
  1125. {
  1126. "id": binding_id,
  1127. "label": label,
  1128. "value": value,
  1129. "source": {
  1130. "kind": "calculation",
  1131. "label": label,
  1132. "ref": f"calculation:{binding_id}",
  1133. "fieldPath": "value",
  1134. },
  1135. "relation": "persisted-output",
  1136. "completeness": "complete",
  1137. },
  1138. binding_id=binding_id,
  1139. role=role,
  1140. )
  1141. binding["evidence"]["confidence"] = "deterministic"
  1142. return binding
  1143. def _event_status_bindings(
  1144. resolver: SourceResolver,
  1145. event_id: int,
  1146. *,
  1147. prefix: str,
  1148. include_output: bool = False,
  1149. ) -> list[SourceBinding]:
  1150. """Bind every Event value rendered by a status/diagnostic module."""
  1151. bindings = [
  1152. resolver.binding_for_event(
  1153. event_id,
  1154. binding_id=f"{prefix}:status:event:{event_id}",
  1155. path="status",
  1156. role="status",
  1157. ),
  1158. ]
  1159. event = resolver.event_details.get(int(event_id)) or {}
  1160. if "duration_ms" in event:
  1161. bindings.append(resolver.binding_for_event(
  1162. event_id,
  1163. binding_id=f"{prefix}:duration:event:{event_id}",
  1164. path="duration_ms",
  1165. role="status",
  1166. ))
  1167. if pointer_value(event, "/error_message") is not None or "error_message" in event:
  1168. bindings.append(resolver.binding_for_event(
  1169. event_id,
  1170. binding_id=f"{prefix}:error:event:{event_id}",
  1171. path="error_message",
  1172. role="status",
  1173. ))
  1174. if include_output:
  1175. outcome = resolver.binding_for_event(
  1176. event_id,
  1177. binding_id=f"{prefix}:outcome:event:{event_id}",
  1178. path="output.content",
  1179. role="output",
  1180. availability="returned-to-agent",
  1181. )
  1182. outcome["transform"] = {
  1183. "kind": "parsed",
  1184. "operation": "retrieval-outcome-v1",
  1185. "outputKey": "outcome",
  1186. }
  1187. outcome["evidence"]["confidence"] = "deterministic"
  1188. bindings.append(outcome)
  1189. completeness = resolver.binding_for_event(
  1190. event_id,
  1191. binding_id=f"{prefix}:completeness:event:{event_id}",
  1192. path="",
  1193. role="status",
  1194. )
  1195. completeness["transform"] = {
  1196. "kind": "parsed",
  1197. "operation": "event-body-completeness-v1",
  1198. "outputKey": "completeness",
  1199. }
  1200. completeness["evidence"]["confidence"] = "deterministic"
  1201. bindings.append(completeness)
  1202. return bindings
  1203. def _business_modules(detail: dict[str, Any]) -> list[dict[str, Any]]:
  1204. if isinstance(detail.get("blocks"), list):
  1205. modules: list[dict[str, Any]] = []
  1206. for index, item in enumerate(detail.get("blocks") or [], 1):
  1207. if not isinstance(item, dict):
  1208. continue
  1209. # Preserve the complete semantic block. Specialized business
  1210. # blocks carry their real payload in routes/steps/branches/rows,
  1211. # not in the generic value/items pair.
  1212. module = dict(item)
  1213. module["id"] = str(item.get("id") or f"block-{index}")
  1214. module["title"] = item.get("title") or f"业务模块 {index}"
  1215. if not module.get("items"):
  1216. module["items"] = item.get("branches") or item.get("rows")
  1217. modules.append(module)
  1218. return modules
  1219. return [
  1220. dict(item)
  1221. for item in detail.get("businessSections") or []
  1222. if isinstance(item, dict)
  1223. ]
  1224. def _match_business_module(
  1225. modules: list[dict[str, Any]], module_id: str, title: str
  1226. ) -> dict[str, Any] | None:
  1227. exact = next((item for item in modules if str(item.get("id")) == module_id), None)
  1228. if exact:
  1229. return exact
  1230. normalized = _normalized_title(title)
  1231. exact_title = next(
  1232. (item for item in modules if _normalized_title(str(item.get("title") or "")) == normalized),
  1233. None,
  1234. )
  1235. if exact_title:
  1236. return exact_title
  1237. # Do not use substring similarity here. A nearby title is not proof that
  1238. # two independently shaped modules contain the same business value.
  1239. return None
  1240. def _module_value(module: dict[str, Any] | None) -> Any:
  1241. if not module:
  1242. return None
  1243. if module.get("content") is not None:
  1244. return module.get("content")
  1245. if module.get("value") is not None:
  1246. return module.get("value")
  1247. block_type = str(module.get("type") or "")
  1248. if block_type == "implementation-plan":
  1249. return {
  1250. "mode": module.get("mode"),
  1251. "routes": module.get("routes") or [],
  1252. "reasoning": module.get("reasoning"),
  1253. }
  1254. if block_type == "creative-process":
  1255. return module.get("steps") or []
  1256. if block_type == "evaluation-branches":
  1257. return module.get("branches") or []
  1258. if module.get("rows") is not None:
  1259. return module.get("rows")
  1260. if module.get("routes") is not None:
  1261. return module.get("routes")
  1262. if module.get("steps") is not None:
  1263. return module.get("steps")
  1264. return None
  1265. def _presentation(module: dict[str, Any] | None, title: str) -> str:
  1266. raw = str((module or {}).get("presentation") or (module or {}).get("type") or "")
  1267. if raw in {"items", "list"}:
  1268. return "items"
  1269. if any(token in title for token in ("规划", "路线", "计划")):
  1270. return "plan"
  1271. if any(token in title for token in ("过程", "处理", "调用")):
  1272. return "process"
  1273. if "评审" in title:
  1274. return "review"
  1275. if any(token in title for token in ("候选表", "主脚本", "产物")):
  1276. return "artifact"
  1277. if any(token in title for token in ("说明", "缺失", "状态", "完整性")):
  1278. return "notice"
  1279. return "text"
  1280. def _runtime_refs(bindings: list[SourceBinding]) -> list[str]:
  1281. refs: list[str] = []
  1282. for binding in bindings:
  1283. source_id = str(binding.get("sourceId") or "")
  1284. if source_id.startswith("event:"):
  1285. refs.append(source_id)
  1286. selector = binding.get("selector") or {}
  1287. if selector.get("kind") == "members":
  1288. refs.extend(
  1289. str(member.get("sourceId") or "")
  1290. for member in selector.get("members") or []
  1291. if str(member.get("sourceId") or "").startswith("event:")
  1292. )
  1293. return list(dict.fromkeys(refs))
  1294. def _historical_implementation_task_modules(
  1295. fields: dict[str, dict[str, Any]],
  1296. resolver: SourceResolver,
  1297. ) -> list[dict[str, Any]]:
  1298. """Keep old Branch tasks readable without copying one blob into six modules."""
  1299. task_field = next(
  1300. (field for field in fields.values() if str(field.get("label") or "") == "完整实现任务"),
  1301. None,
  1302. )
  1303. target_field = next(
  1304. (field for field in fields.values() if str(field.get("label") or "") == "实现目标"),
  1305. None,
  1306. )
  1307. modules: list[dict[str, Any]] = []
  1308. if target_field and target_field.get("value") not in (None, ""):
  1309. binding = resolver.binding_for_field(target_field, binding_id="scope:historical-target")
  1310. modules.append({
  1311. "id": "scope",
  1312. "title": "实现范围",
  1313. "presentation": "text",
  1314. "business": {"value": target_field.get("value")},
  1315. "bindings": [binding],
  1316. "runtimeRefs": [],
  1317. "defaultOpen": True,
  1318. })
  1319. if task_field and task_field.get("value") not in (None, ""):
  1320. binding = resolver.binding_for_field(task_field, binding_id="task:historical-full")
  1321. modules.append({
  1322. "id": "historical-task",
  1323. "title": "完整实现任务(历史回退)",
  1324. "presentation": "text",
  1325. "business": {"value": task_field.get("value")},
  1326. "bindings": [binding],
  1327. "runtimeRefs": [],
  1328. "defaultOpen": True,
  1329. })
  1330. if modules:
  1331. return modules
  1332. binding = unresolved_binding(
  1333. "historical-task:missing",
  1334. reason="该历史方案没有保存实现 Agent 输入,也没有可用的 Branch 实现任务字段",
  1335. code="source-record-not-saved",
  1336. )
  1337. resolver.add_unresolved_source(binding)
  1338. return [{
  1339. "id": "historical-task",
  1340. "title": "实现任务记录缺失",
  1341. "presentation": "notice",
  1342. "business": {"value": "该历史方案未保存可恢复的完整实现任务。"},
  1343. "bindings": [binding],
  1344. "runtimeRefs": [],
  1345. "defaultOpen": True,
  1346. }]
  1347. def _multipath_comparison_binding(
  1348. item: dict[str, Any],
  1349. item_index: int,
  1350. module_bindings: list[SourceBinding],
  1351. resolver: SourceResolver,
  1352. bundle: dict[str, Any],
  1353. ) -> SourceBinding:
  1354. """Bind one comparison row to its review, Branch status and final decision."""
  1355. text = str(item.get("value") or item.get("label") or "")
  1356. match = re.search(r"方案\s*(\d+)", text)
  1357. branch_id = int(match.group(1)) if match else None
  1358. decision_id = next(
  1359. (
  1360. _suffix_int(str(binding.get("sourceId") or ""))
  1361. for binding in module_bindings
  1362. if "script_build_multipath_decision" in str(binding.get("sourceId") or "")
  1363. ),
  1364. None,
  1365. )
  1366. members: list[dict[str, Any]] = []
  1367. if branch_id is not None:
  1368. branch = next(
  1369. (row for row in bundle.get("branches") or [] if _integer(row.get("branch_id")) == branch_id),
  1370. None,
  1371. )
  1372. if branch is not None:
  1373. members.append({
  1374. "id": f"comparison:{item_index}:branch",
  1375. "label": f"方案 {branch_id} 处置状态",
  1376. "value": branch.get("status"),
  1377. "source": {
  1378. "kind": "database",
  1379. "label": "script_build_branch",
  1380. "ref": f"round:{branch.get('round_index')}:branch:{branch_id}:output",
  1381. "fieldPath": "status",
  1382. },
  1383. "relation": "persisted-output",
  1384. })
  1385. if decision_id is not None:
  1386. decision = next(
  1387. (row for row in bundle.get("multipathDecisions") or [] if _integer(row.get("id")) == decision_id),
  1388. None,
  1389. )
  1390. if decision is not None:
  1391. members.append({
  1392. "id": f"comparison:{item_index}:decision",
  1393. "label": "主 Agent 最终决定",
  1394. "value": decision.get("decision"),
  1395. "source": {
  1396. "kind": "database",
  1397. "label": "script_build_multipath_decision",
  1398. "ref": f"multipath-decision:{decision_id}",
  1399. "fieldPath": "decision",
  1400. },
  1401. "relation": "persisted-output",
  1402. })
  1403. for binding in module_bindings:
  1404. source_id = str(binding.get("sourceId") or "")
  1405. event_id = _suffix_int(source_id) if source_id.startswith("event:") else None
  1406. if event_id is None:
  1407. continue
  1408. event = resolver.event_details.get(event_id)
  1409. if event is None:
  1410. continue
  1411. members.append({
  1412. "id": f"comparison:{item_index}:review:{event_id}",
  1413. "label": "多方案评审",
  1414. "value": (event.get("output") or {}).get("content") if isinstance(event.get("output"), dict) else event.get("output"),
  1415. "source": {
  1416. "kind": "runtime-event",
  1417. "label": str(event.get("event_name") or f"Event {event_id}"),
  1418. "ref": f"event:{event_id}",
  1419. "fieldPath": "output.content",
  1420. },
  1421. "relation": "available-upstream",
  1422. })
  1423. return resolver.members_binding(
  1424. {
  1425. "id": f"comparison:item:{item_index}",
  1426. "label": str(item.get("label") or f"方案对照 {item_index}"),
  1427. "value": item.get("value"),
  1428. },
  1429. binding_id=f"comparison:item:{item_index}",
  1430. members=members,
  1431. reducer="multipath-review-and-decision-row-v1",
  1432. role="basis",
  1433. )
  1434. def _align_transforms_with_business(
  1435. value: Any,
  1436. bindings: list[SourceBinding],
  1437. resolver: SourceResolver,
  1438. module_id: str,
  1439. ) -> None:
  1440. """Make the transform describe what the first column actually renders."""
  1441. if value is None or not bindings:
  1442. return
  1443. if len(bindings) > 1:
  1444. for binding in bindings:
  1445. if binding.get("transform", {}).get("kind") == "direct":
  1446. binding["transform"] = {
  1447. "kind": "combined",
  1448. "operation": f"business-module:{module_id}",
  1449. }
  1450. binding["evidence"]["confidence"] = "deterministic"
  1451. return
  1452. binding = bindings[0]
  1453. if binding.get("transform", {}).get("kind") != "direct":
  1454. return
  1455. source = resolver.sources.get(str(binding.get("sourceId") or "")) or {}
  1456. selector = binding.get("selector") or {}
  1457. if selector.get("kind") == "json-pointer":
  1458. selected = pointer_value(source.get("rawRecord"), str(selector.get("path") or ""))
  1459. elif selector.get("kind") == "whole-record":
  1460. selected = source.get("rawRecord")
  1461. else:
  1462. return
  1463. if not _same_value(value, selected):
  1464. binding["transform"] = {
  1465. "kind": "parsed",
  1466. "operation": "business-module-projection-v1",
  1467. "outputKey": module_id,
  1468. }
  1469. binding["evidence"]["confidence"] = "deterministic"
  1470. def _normalize_items(items: list[Any]) -> list[dict[str, Any]]:
  1471. values: list[dict[str, Any]] = []
  1472. for index, item in enumerate(items, 1):
  1473. if isinstance(item, dict):
  1474. label = (
  1475. item.get("label")
  1476. or item.get("title")
  1477. or item.get("subject")
  1478. or item.get("summary")
  1479. or f"第 {index} 项"
  1480. )
  1481. value = item.get("value") if "value" in item else item
  1482. values.append({"label": label, "value": value})
  1483. else:
  1484. values.append({"label": f"第 {index} 项", "value": item})
  1485. return values
  1486. def _item_bindings(
  1487. module_id: str,
  1488. item_index: int,
  1489. item: dict[str, Any],
  1490. item_count: int,
  1491. module_bindings: list[SourceBinding],
  1492. resolver: SourceResolver,
  1493. ) -> list[SourceBinding]:
  1494. if not module_bindings:
  1495. binding = unresolved_binding(
  1496. f"{module_id}:item:{item_index}:unresolved",
  1497. reason=f"子项「{item.get('label')}」没有可定位的来源绑定",
  1498. code="binding-map-missing",
  1499. )
  1500. resolver.add_unresolved_source(binding)
  1501. return [binding]
  1502. if len(module_bindings) == item_count and item_index <= len(module_bindings):
  1503. binding = deepcopy(module_bindings[item_index - 1])
  1504. binding["id"] = f"{module_id}:item:{item_index}"
  1505. source = resolver.sources.get(str(binding.get("sourceId") or "")) or {}
  1506. selector = binding.get("selector") or {}
  1507. selected = pointer_value(source.get("rawRecord"), str(selector.get("path") or ""))
  1508. if not _same_value(item.get("value"), selected):
  1509. binding["transform"] = {
  1510. "kind": "parsed",
  1511. "operation": "business-item-v1",
  1512. "outputKey": str(item_index),
  1513. }
  1514. binding["evidence"]["confidence"] = "deterministic"
  1515. return [binding]
  1516. parent = module_bindings[0]
  1517. selector = parent.get("selector") or {}
  1518. source_id = str(parent.get("sourceId") or "")
  1519. source_record = resolver.sources.get(source_id) or {}
  1520. selected_parent = pointer_value(
  1521. source_record.get("rawRecord"), str(selector.get("path") or "")
  1522. )
  1523. if selector.get("kind") == "json-pointer" and isinstance(selected_parent, list):
  1524. binding = deepcopy(parent)
  1525. binding["id"] = f"{module_id}:item:{item_index}"
  1526. binding["selector"] = {
  1527. "kind": "json-pointer",
  1528. "path": f"{str(selector.get('path') or '').rstrip('/')}/{item_index - 1}",
  1529. }
  1530. return [binding]
  1531. event_id = _suffix_int(source_id) if source_id.startswith("event:") else None
  1532. if event_id:
  1533. role = str(parent.get("role") or "basis")
  1534. path = str(selector.get("path") or "")
  1535. if not path:
  1536. path = "input.content.task" if role in {"input", "constraint", "basis"} else "output.content.summary"
  1537. return [
  1538. resolver.text_span_binding(
  1539. event_id,
  1540. binding_id=f"{module_id}:item:{item_index}",
  1541. path=path.lstrip("/").replace("/", "."),
  1542. exact_text=_item_text(item.get("value")),
  1543. role=role,
  1544. )
  1545. ]
  1546. binding = unresolved_binding(
  1547. f"{module_id}:item:{item_index}:unresolved",
  1548. reason=f"子项「{item.get('label')}」是结构化重组结果,无法安全定位到单一原始区间",
  1549. source_id=source_id or None,
  1550. resolution="unsafe",
  1551. code="unsafe-association",
  1552. )
  1553. if source_id and source_id in resolver.sources:
  1554. return [binding]
  1555. resolver.add_unresolved_source(binding)
  1556. return [binding]
  1557. def _item_text(value: Any) -> str:
  1558. if isinstance(value, str):
  1559. return value
  1560. if not isinstance(value, dict):
  1561. return str(value or "")
  1562. for key in ("summary", "conclusion", "subject", "title", "evidence"):
  1563. if value.get(key):
  1564. return str(value[key])
  1565. return ""
  1566. def _leaf_texts(value: Any) -> list[str]:
  1567. """Return every visible textual leaf so a structured review item is fully bound."""
  1568. result: list[str] = []
  1569. if isinstance(value, str):
  1570. cleaned = value.strip()
  1571. if cleaned:
  1572. result.append(cleaned)
  1573. elif isinstance(value, dict):
  1574. for child in value.values():
  1575. result.extend(_leaf_texts(child))
  1576. elif isinstance(value, list):
  1577. for child in value:
  1578. result.extend(_leaf_texts(child))
  1579. elif value is not None:
  1580. result.append(str(value))
  1581. return list(dict.fromkeys(result))
  1582. def _same_value(left: Any, right: Any) -> bool:
  1583. try:
  1584. import json
  1585. return json.dumps(left, ensure_ascii=False, sort_keys=True, default=str) == json.dumps(
  1586. right, ensure_ascii=False, sort_keys=True, default=str
  1587. )
  1588. except (TypeError, ValueError):
  1589. return str(left) == str(right)
  1590. def _unique_bindings(bindings: list[SourceBinding]) -> list[SourceBinding]:
  1591. result: list[SourceBinding] = []
  1592. seen: set[str] = set()
  1593. for binding in bindings:
  1594. key = str(binding.get("id") or "")
  1595. if key in seen:
  1596. continue
  1597. seen.add(key)
  1598. result.append(binding)
  1599. return result
  1600. def _all_bindings(modules: list[dict[str, Any]]) -> list[SourceBinding]:
  1601. result: list[SourceBinding] = []
  1602. for module in modules:
  1603. result.extend(module.get("bindings") or [])
  1604. business = module.get("business") or {}
  1605. for item in business.get("items") or []:
  1606. if isinstance(item, dict):
  1607. result.extend(item.get("bindings") or [])
  1608. return result
  1609. def _used_source_ids(modules: list[dict[str, Any]]) -> set[str]:
  1610. used: set[str] = set()
  1611. def add_selector_members(selector: dict[str, Any]) -> None:
  1612. if selector.get("kind") != "members":
  1613. return
  1614. for member in selector.get("members") or []:
  1615. source_id = str(member.get("sourceId") or "")
  1616. if source_id:
  1617. used.add(source_id)
  1618. child = member.get("selector")
  1619. if isinstance(child, dict):
  1620. add_selector_members(child)
  1621. for module in modules:
  1622. for binding in module.get("bindings") or []:
  1623. source_id = str(binding.get("sourceId") or "")
  1624. if source_id:
  1625. used.add(source_id)
  1626. selector = binding.get("selector")
  1627. if isinstance(selector, dict):
  1628. add_selector_members(selector)
  1629. for source_id in module.get("runtimeRefs") or []:
  1630. if isinstance(source_id, str) and source_id:
  1631. used.add(source_id)
  1632. return used
  1633. def _aggregate_binding(
  1634. card_kind: str,
  1635. module_id: str,
  1636. source_ids: list[str],
  1637. fields: dict[str, dict[str, Any]],
  1638. resolver: SourceResolver,
  1639. bundle: dict[str, Any],
  1640. ) -> SourceBinding | None:
  1641. if not source_ids:
  1642. return None
  1643. target = next((fields[source_id] for source_id in source_ids if source_id in fields), None)
  1644. if target is None:
  1645. return None
  1646. eligible = (
  1647. (card_kind == "round-result" and module_id in {"completion", "dispositions", "domain", "changes"})
  1648. or (card_kind == "round-summary" and module_id == "plan")
  1649. or (card_kind == "final-result" and module_id in {"stats", "scale"})
  1650. or (card_kind == "domain-facts" and module_id in {"facts", "verification", "sources", "usage"})
  1651. or (card_kind == "retrieval-stage" and module_id in {"mode", "waves", "direct", "agents", "result"})
  1652. )
  1653. if not eligible:
  1654. return None
  1655. members: list[dict[str, Any]] = []
  1656. reducer = str((target.get("source") or {}).get("label") or "确定性聚合")
  1657. if card_kind == "round-summary" and module_id == "plan":
  1658. round_index = _round_index_from_field_id(str(target.get("id") or ""))
  1659. row = next(
  1660. (
  1661. item
  1662. for item in bundle.get("rounds") or []
  1663. if _integer(item.get("round_index")) == round_index
  1664. ),
  1665. None,
  1666. )
  1667. if row:
  1668. for path in ("race_or_divide", "multipath_plan", "plan_note"):
  1669. members.append(
  1670. _member_field(
  1671. "script_build_round",
  1672. f"round:{round_index}",
  1673. row,
  1674. path,
  1675. )
  1676. )
  1677. elif card_kind == "retrieval-stage":
  1678. value = target.get("value")
  1679. event_ids: list[int] = []
  1680. def collect_event_ids(node: Any) -> None:
  1681. if isinstance(node, dict):
  1682. event_id = _integer(node.get("eventId"))
  1683. if event_id:
  1684. event_ids.append(event_id)
  1685. for child in node.values():
  1686. collect_event_ids(child)
  1687. elif isinstance(node, list):
  1688. for child in node:
  1689. collect_event_ids(child)
  1690. collect_event_ids(value)
  1691. # Mode/wave/result are deterministic summaries of the whole scoped
  1692. # retrieval stage, so their members are every Event loaded for this
  1693. # card. Direct/Agent modules only retain the Events named by their
  1694. # own structured projection.
  1695. if module_id in {"mode", "waves", "result"}:
  1696. event_ids.extend(resolver.event_details)
  1697. for event_id in dict.fromkeys(event_ids):
  1698. event = resolver.event_details.get(event_id)
  1699. if not event:
  1700. continue
  1701. members.append(
  1702. {
  1703. "id": f"aggregate:retrieval:{module_id}:event:{event_id}",
  1704. "label": str(event.get("event_name") or f"Event {event_id}"),
  1705. "value": event,
  1706. "source": {
  1707. "kind": "runtime-event",
  1708. "label": str(event.get("event_name") or f"Event {event_id}"),
  1709. "ref": f"event:{event_id}",
  1710. "fieldPath": "",
  1711. },
  1712. "relation": "persisted-output",
  1713. "completeness": "complete",
  1714. }
  1715. )
  1716. elif card_kind == "round-result":
  1717. round_index = _round_index_from_field_id(str(target.get("id") or ""))
  1718. if module_id in {"dispositions", "changes", "completion"}:
  1719. for row in bundle.get("branches") or []:
  1720. if _integer(row.get("round_index")) != round_index:
  1721. continue
  1722. branch_id = _integer(row.get("branch_id"))
  1723. path = "status" if module_id == "dispositions" else "candidate_snapshot"
  1724. members.append(
  1725. {
  1726. "id": f"aggregate:{module_id}:branch:{branch_id}",
  1727. "label": f"方案 {branch_id}",
  1728. "value": row.get(path),
  1729. "source": {
  1730. "kind": "database",
  1731. "label": "script_build_branch",
  1732. "ref": f"round:{round_index}:branch:{branch_id}:output",
  1733. "fieldPath": path,
  1734. },
  1735. "relation": "persisted-output",
  1736. "completeness": "complete",
  1737. }
  1738. )
  1739. elif module_id == "domain":
  1740. for row in bundle.get("domainInfo") or []:
  1741. if _integer(row.get("round_index")) != round_index:
  1742. continue
  1743. members.append(
  1744. {
  1745. "id": f"aggregate:domain:{row.get('id')}",
  1746. "label": "领域事实",
  1747. "value": row,
  1748. "source": {
  1749. "kind": "database",
  1750. "label": "script_build_domain_info",
  1751. "ref": f"domain-info:{row.get('id')}",
  1752. "fieldPath": "",
  1753. },
  1754. "relation": "persisted-output",
  1755. "completeness": "complete",
  1756. }
  1757. )
  1758. elif card_kind == "final-result" and module_id == "stats":
  1759. for row in bundle.get("rounds") or []:
  1760. members.append(_member_field("script_build_round", f"round:{row.get('round_index')}:result", row, "status"))
  1761. for row in bundle.get("branches") or []:
  1762. members.append(_member_field("script_build_branch", f"round:{row.get('round_index')}:branch:{row.get('branch_id')}:output", row, "status"))
  1763. elif card_kind == "final-result" and module_id == "scale":
  1764. artifact = bundle.get("currentArtifact") or {}
  1765. for collection in ("paragraphs", "elements", "paragraphElements"):
  1766. members.append(
  1767. {
  1768. "id": f"aggregate:artifact:{collection}",
  1769. "label": collection,
  1770. "value": artifact.get(collection) or [],
  1771. "source": {
  1772. "kind": "artifact",
  1773. "label": "当前主脚本",
  1774. "ref": "artifact:base:current",
  1775. "fieldPath": collection,
  1776. },
  1777. "relation": "persisted-output",
  1778. "completeness": "complete",
  1779. }
  1780. )
  1781. elif card_kind == "domain-facts":
  1782. match = re.search(r"branch:(\d+)", str(target.get("id") or ""))
  1783. branch_id = _integer(match.group(1)) if match else None
  1784. path = {
  1785. "facts": "content",
  1786. "verification": "note",
  1787. "sources": "source",
  1788. "usage": "content",
  1789. }.get(module_id, "content")
  1790. for row in bundle.get("domainInfo") or []:
  1791. if _integer(row.get("branch_id")) != branch_id:
  1792. continue
  1793. members.append(
  1794. {
  1795. "id": f"aggregate:domain:{row.get('id')}:{path}",
  1796. "label": f"领域事实 {row.get('id')}",
  1797. "value": row.get(path),
  1798. "source": {
  1799. "kind": "database",
  1800. "label": "script_build_domain_info",
  1801. "ref": f"domain-info:{row.get('id')}",
  1802. "fieldPath": path,
  1803. },
  1804. "relation": "persisted-output",
  1805. "completeness": "complete" if row.get(path) not in (None, "", [], {}) else "partial",
  1806. }
  1807. )
  1808. if not members and card_kind == "domain-facts":
  1809. match = re.search(r"branch:(\d+)", str(target.get("id") or ""))
  1810. branch_id = _integer(match.group(1)) if match else None
  1811. return resolver.empty_database_result_binding(
  1812. binding_id=f"{module_id}:empty-result",
  1813. table="script_build_domain_info",
  1814. filters={"branch_id": branch_id},
  1815. label="领域事实查询结果",
  1816. )
  1817. if not members:
  1818. return None
  1819. return resolver.members_binding(
  1820. target,
  1821. binding_id=f"{module_id}:aggregate",
  1822. members=members,
  1823. reducer=reducer,
  1824. )
  1825. def _member_field(table: str, ref: str, row: dict[str, Any], path: str) -> dict[str, Any]:
  1826. return {
  1827. "id": f"aggregate:{table}:{row.get('id')}:{path}",
  1828. "label": f"{table}.{path}",
  1829. "value": row.get(path),
  1830. "source": {
  1831. "kind": "database",
  1832. "label": table,
  1833. "ref": ref,
  1834. "fieldPath": path,
  1835. },
  1836. "relation": "persisted-output",
  1837. "completeness": "complete",
  1838. }
  1839. def _round_index_from_field_id(value: str) -> int | None:
  1840. match = re.search(r"round:(\d+)", value)
  1841. return _integer(match.group(1)) if match else None
  1842. def _business_notices(detail: dict[str, Any]) -> list[dict[str, Any]]:
  1843. notices: list[dict[str, Any]] = []
  1844. for item in detail.get("notices") or []:
  1845. if isinstance(item, dict) and item.get("message"):
  1846. notices.append(item)
  1847. if detail.get("missingActivityNotice"):
  1848. notices.append({"level": "warning", "message": detail["missingActivityNotice"]})
  1849. return notices
  1850. def _unique_notices(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
  1851. result: list[dict[str, Any]] = []
  1852. seen: set[str] = set()
  1853. for item in items:
  1854. message = str(item.get("message") or "")
  1855. if not message or message in seen:
  1856. continue
  1857. seen.add(message)
  1858. result.append({"level": item.get("level") or "info", "message": message})
  1859. return result
  1860. def _fallback_card_data(detail_ref: str, business: dict[str, Any]) -> dict[str, Any]:
  1861. modules = _business_modules(business)
  1862. return {
  1863. "cardKind": _fallback_card_kind(detail_ref),
  1864. "completeness": "partial",
  1865. "businessInputs": [],
  1866. "businessOutputs": [],
  1867. "runtime": {"summary": "", "units": []},
  1868. "displayUse": {
  1869. "businessModules": [
  1870. {"id": item.get("id") or f"module-{index}", "title": item.get("title") or f"业务模块 {index}", "sourceIds": []}
  1871. for index, item in enumerate(modules, 1)
  1872. ],
  1873. "cardFields": [],
  1874. "unusedSourceIds": [],
  1875. },
  1876. "notices": [
  1877. {
  1878. "level": "warning",
  1879. "message": "该历史卡片没有完整结构化来源投影,已保留业务详情并显式标记断链。",
  1880. }
  1881. ],
  1882. }
  1883. def _fallback_card_kind(detail_ref: str) -> str:
  1884. if detail_ref.startswith("event:"):
  1885. return "runtime-event"
  1886. if detail_ref.startswith("round:"):
  1887. return detail_ref.rsplit(":", 1)[-1]
  1888. return detail_ref.split(":", 1)[0] or "unknown"
  1889. def _event_text_path(event: dict[str, Any], side: str) -> str:
  1890. wrapped = event.get(side)
  1891. content = wrapped.get("content") if isinstance(wrapped, dict) else None
  1892. if isinstance(content, dict):
  1893. preferred = "task" if side == "input" else "summary"
  1894. if isinstance(content.get(preferred), str):
  1895. return f"{side}.content.{preferred}"
  1896. return f"{side}.content"
  1897. def _normalized_title(value: str) -> str:
  1898. return re.sub(r"[\s/\u3001,,()()]+", "", value).replace("完整", "")
  1899. def _collect_event_ids(value: Any, ids: set[int], key: str = "") -> None:
  1900. if isinstance(value, dict):
  1901. for child_key, child in value.items():
  1902. lowered = str(child_key).lower()
  1903. if lowered in {
  1904. "eventid",
  1905. "event_id",
  1906. "implementereventid",
  1907. "scopeeventid",
  1908. "parenteventid",
  1909. }:
  1910. event_id = _integer(child)
  1911. if event_id:
  1912. ids.add(event_id)
  1913. elif lowered in {"eventref", "currentrevisionref", "detailref"}:
  1914. event_id = _suffix_int(str(child))
  1915. if event_id:
  1916. ids.add(event_id)
  1917. elif lowered in {"eventrefs", "technicalrefs", "revisionrefs"}:
  1918. for ref in child or []:
  1919. event_id = _suffix_int(str(ref))
  1920. if event_id:
  1921. ids.add(event_id)
  1922. _collect_event_ids(child, ids, lowered)
  1923. elif isinstance(value, list):
  1924. for child in value:
  1925. _collect_event_ids(child, ids, key)
  1926. def _view_branch(view: dict[str, Any], round_index: int, branch_id: int) -> dict[str, Any]:
  1927. round_ = next(
  1928. (item for item in view.get("rounds") or [] if _integer(item.get("roundIndex")) == round_index),
  1929. {},
  1930. )
  1931. return next(
  1932. (item for item in round_.get("branches") or [] if _integer(item.get("branchId")) == branch_id),
  1933. {},
  1934. )
  1935. def _bundle_event(bundle: dict[str, Any], event_id: int | None) -> dict[str, Any] | None:
  1936. return next(
  1937. (item for item in bundle.get("events") or [] if _integer(item.get("id")) == event_id),
  1938. None,
  1939. )
  1940. def _integer(value: Any) -> int | None:
  1941. try:
  1942. return int(value)
  1943. except (TypeError, ValueError):
  1944. return None
  1945. def _suffix_int(value: str) -> int | None:
  1946. return _integer(str(value or "").rsplit(":", 1)[-1])