loopx 0.4.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (811) hide show
  1. loopx/__init__.py +5 -0
  2. loopx/agent_onboarding.py +654 -0
  3. loopx/agent_registry.py +112 -0
  4. loopx/ark_managed_agent_host.py +59 -0
  5. loopx/authority.py +805 -0
  6. loopx/benchmark.py +2875 -0
  7. loopx/benchmark_adapters/__init__.py +1 -0
  8. loopx/benchmark_adapters/agentissue.py +2644 -0
  9. loopx/benchmark_adapters/agents_last_exam.py +3998 -0
  10. loopx/benchmark_adapters/edgebench.py +322 -0
  11. loopx/benchmark_adapters/skillsbench.py +5978 -0
  12. loopx/benchmark_adapters/skillsbench_acp_failure_policy.py +143 -0
  13. loopx/benchmark_adapters/skillsbench_acp_process.py +31 -0
  14. loopx/benchmark_adapters/skillsbench_acp_relay.py +4832 -0
  15. loopx/benchmark_adapters/skillsbench_batch.py +124 -0
  16. loopx/benchmark_adapters/skillsbench_bridge_guard.py +209 -0
  17. loopx/benchmark_adapters/skillsbench_bridge_summary.py +203 -0
  18. loopx/benchmark_adapters/skillsbench_codex_goal_recovery.py +271 -0
  19. loopx/benchmark_adapters/skillsbench_codex_goal_trace.py +81 -0
  20. loopx/benchmark_adapters/skillsbench_codex_runtime.py +339 -0
  21. loopx/benchmark_adapters/skillsbench_dockerfile_runtime.py +467 -0
  22. loopx/benchmark_adapters/skillsbench_failure_signals.py +652 -0
  23. loopx/benchmark_adapters/skillsbench_proxy_runtime.py +327 -0
  24. loopx/benchmark_adapters/skillsbench_remote_bridge.py +402 -0
  25. loopx/benchmark_adapters/skillsbench_result_discovery.py +143 -0
  26. loopx/benchmark_adapters/skillsbench_runner_profile.py +436 -0
  27. loopx/benchmark_adapters/skillsbench_runner_source.py +99 -0
  28. loopx/benchmark_adapters/skillsbench_setup_preflight.py +771 -0
  29. loopx/benchmark_adapters/skillsbench_signals.py +15 -0
  30. loopx/benchmark_adapters/skillsbench_task_source.py +141 -0
  31. loopx/benchmark_adapters/skillsbench_turn_route.py +723 -0
  32. loopx/benchmark_adapters/skillsbench_turn_runtime.py +1069 -0
  33. loopx/benchmark_adapters/skillsbench_typed_repair.py +689 -0
  34. loopx/benchmark_adapters/skillsbench_uv_cache.py +111 -0
  35. loopx/benchmark_adapters/skillsbench_verifier_bootstrap.py +227 -0
  36. loopx/benchmark_adapters/skillsbench_verifier_cache.py +138 -0
  37. loopx/benchmark_adapters/terminal_bench.py +10078 -0
  38. loopx/benchmark_case_analysis.py +1276 -0
  39. loopx/benchmark_case_state.py +1079 -0
  40. loopx/benchmark_core/__init__.py +239 -0
  41. loopx/benchmark_core/adapter.py +84 -0
  42. loopx/benchmark_core/artifacts.py +517 -0
  43. loopx/benchmark_core/attempts.py +199 -0
  44. loopx/benchmark_core/container_exec.py +216 -0
  45. loopx/benchmark_core/io.py +68 -0
  46. loopx/benchmark_core/lifecycle.py +211 -0
  47. loopx/benchmark_core/loop_protocol.py +689 -0
  48. loopx/benchmark_core/observable_handles.py +348 -0
  49. loopx/benchmark_core/parity.py +256 -0
  50. loopx/benchmark_core/remote_closeout.py +482 -0
  51. loopx/benchmark_core/rounds.py +215 -0
  52. loopx/benchmark_core/route_profile.py +509 -0
  53. loopx/benchmark_core/run_permissions.py +206 -0
  54. loopx/benchmark_core/split_control.py +925 -0
  55. loopx/benchmark_core/turn_fidelity.py +326 -0
  56. loopx/benchmark_ledger.py +3793 -0
  57. loopx/benchmark_ledger_countability.py +372 -0
  58. loopx/benchmark_ledger_current.py +724 -0
  59. loopx/benchmark_trajectory.py +405 -0
  60. loopx/benchmarks/__init__.py +1 -0
  61. loopx/benchmarks/qualification/__init__.py +1 -0
  62. loopx/benchmarks/qualification/release_outcome_baseline.py +360 -0
  63. loopx/benchmarks/read_models/__init__.py +1 -0
  64. loopx/benchmarks/read_models/benchmark_attempt_accounting.py +53 -0
  65. loopx/benchmarks/read_models/benchmark_comparison.py +414 -0
  66. loopx/benchmarks/read_models/benchmark_event_timeline.py +113 -0
  67. loopx/benchmarks/read_models/benchmark_experiment_report.py +475 -0
  68. loopx/benchmarks/read_models/benchmark_learning_ledger.py +137 -0
  69. loopx/benchmarks/read_models/benchmark_lifecycle_contracts.py +228 -0
  70. loopx/benchmarks/read_models/benchmark_projection.py +723 -0
  71. loopx/benchmarks/read_models/benchmark_result.py +146 -0
  72. loopx/benchmarks/read_models/benchmark_run_execution_contract.py +116 -0
  73. loopx/benchmarks/read_models/benchmark_run_failure.py +157 -0
  74. loopx/benchmarks/read_models/benchmark_run_metrics.py +213 -0
  75. loopx/benchmarks/read_models/benchmark_run_post_execution.py +635 -0
  76. loopx/benchmarks/read_models/benchmark_run_pre_execution.py +541 -0
  77. loopx/benchmarks/read_models/benchmark_status_compaction.py +1255 -0
  78. loopx/benchmarks/read_models/benchmark_status_runner.py +780 -0
  79. loopx/benchmarks/read_models/goal_start_control_score.py +857 -0
  80. loopx/benchmarks/read_models/skillsbench_post_run_debug.py +746 -0
  81. loopx/benchmarks/read_models/skillsbench_verifier_attribution.py +269 -0
  82. loopx/bootstrap.py +1116 -0
  83. loopx/bootstrap_command_pack.py +2167 -0
  84. loopx/boundary_authority.py +199 -0
  85. loopx/canary/__init__.py +1 -0
  86. loopx/canary/maintainability_ratchet.py +800 -0
  87. loopx/canary/planner.py +1984 -0
  88. loopx/canary/premerge.py +1130 -0
  89. loopx/canary/qualification_profiles.py +309 -0
  90. loopx/canary/quality_surface_catalog.py +838 -0
  91. loopx/canary/release_profiles.py +51 -0
  92. loopx/canary/runner.py +1107 -0
  93. loopx/canary/smoke_health.py +581 -0
  94. loopx/canary/smoke_profiles.py +212 -0
  95. loopx/capabilities/__init__.py +0 -0
  96. loopx/capabilities/agent_turn_recall/__init__.py +17 -0
  97. loopx/capabilities/agent_turn_recall/cli.py +369 -0
  98. loopx/capabilities/agent_turn_recall/core.py +296 -0
  99. loopx/capabilities/auto_research/__init__.py +16 -0
  100. loopx/capabilities/auto_research/bootstrap_contract.py +157 -0
  101. loopx/capabilities/auto_research/cli.py +1468 -0
  102. loopx/capabilities/auto_research/core.py +11 -0
  103. loopx/capabilities/auto_research/defaults.py +79 -0
  104. loopx/capabilities/auto_research/demo_e2e.py +1848 -0
  105. loopx/capabilities/auto_research/demo_supervisor.py +186 -0
  106. loopx/capabilities/auto_research/evidence_packet.py +767 -0
  107. loopx/capabilities/auto_research/human_view.py +794 -0
  108. loopx/capabilities/auto_research/kernel.py +191 -0
  109. loopx/capabilities/auto_research/knn_demo_workspace.py +322 -0
  110. loopx/capabilities/auto_research/live_evidence.py +248 -0
  111. loopx/capabilities/auto_research/preset.py +176 -0
  112. loopx/capabilities/auto_research/research_state.py +1085 -0
  113. loopx/capabilities/auto_research/role_profiles.py +394 -0
  114. loopx/capabilities/auto_research/rollout_append.py +97 -0
  115. loopx/capabilities/auto_research/terminal_result_contract.py +422 -0
  116. loopx/capabilities/auto_research/terminal_result_projection.py +171 -0
  117. loopx/capabilities/auto_research/terminal_result_query.py +233 -0
  118. loopx/capabilities/auto_research/terminal_results.py +349 -0
  119. loopx/capabilities/auto_research/user_contract.py +190 -0
  120. loopx/capabilities/auto_research/worker_loop.py +163 -0
  121. loopx/capabilities/auto_research/worker_runtime.py +777 -0
  122. loopx/capabilities/auto_research/worker_skill/SKILL.md +343 -0
  123. loopx/capabilities/benchmark_toolkit/__init__.py +19 -0
  124. loopx/capabilities/benchmark_toolkit/integrity.py +387 -0
  125. loopx/capabilities/catalog.py +1875 -0
  126. loopx/capabilities/change_quality/__init__.py +19 -0
  127. loopx/capabilities/change_quality/cli.py +171 -0
  128. loopx/capabilities/change_quality/context.py +156 -0
  129. loopx/capabilities/change_quality/oracles.py +269 -0
  130. loopx/capabilities/change_quality/policy.py +34 -0
  131. loopx/capabilities/change_quality/receipt.py +482 -0
  132. loopx/capabilities/change_quality/result.py +493 -0
  133. loopx/capabilities/change_quality/scope.py +171 -0
  134. loopx/capabilities/change_quality/shadow.py +680 -0
  135. loopx/capabilities/content_ops/__init__.py +0 -0
  136. loopx/capabilities/content_ops/cli.py +649 -0
  137. loopx/capabilities/content_ops/connector_packets.py +164 -0
  138. loopx/capabilities/content_ops/item_lifecycle.py +1000 -0
  139. loopx/capabilities/content_ops/layout.py +451 -0
  140. loopx/capabilities/content_ops/markdown.py +456 -0
  141. loopx/capabilities/content_ops/schemas.py +51 -0
  142. loopx/capabilities/content_ops/social_browser_x.py +107 -0
  143. loopx/capabilities/content_ops/surface.py +1956 -0
  144. loopx/capabilities/content_ops/templates/layout-catalog-v0.json +72 -0
  145. loopx/capabilities/context_providers/__init__.py +36 -0
  146. loopx/capabilities/context_providers/base.py +189 -0
  147. loopx/capabilities/context_providers/factory.py +32 -0
  148. loopx/capabilities/context_providers/openviking.py +702 -0
  149. loopx/capabilities/context_providers/service_ownership.py +185 -0
  150. loopx/capabilities/decision_context/__init__.py +129 -0
  151. loopx/capabilities/decision_context/architecture.py +83 -0
  152. loopx/capabilities/decision_context/assembler.py +849 -0
  153. loopx/capabilities/decision_context/catalog_entry.py +195 -0
  154. loopx/capabilities/decision_context/cli.py +310 -0
  155. loopx/capabilities/decision_context/cursor_commit.py +535 -0
  156. loopx/capabilities/decision_context/outcome_feedback.py +352 -0
  157. loopx/capabilities/decision_context/packets.py +654 -0
  158. loopx/capabilities/decision_context/private_state.py +189 -0
  159. loopx/capabilities/decision_context/profile.py +453 -0
  160. loopx/capabilities/decision_context/providers.py +228 -0
  161. loopx/capabilities/decision_context/review_settlement.py +136 -0
  162. loopx/capabilities/decision_context/runtime.py +273 -0
  163. loopx/capabilities/decision_context/sources.py +415 -0
  164. loopx/capabilities/explore/__init__.py +1 -0
  165. loopx/capabilities/explore/activation.py +198 -0
  166. loopx/capabilities/explore/adaptive_replay_planner.py +221 -0
  167. loopx/capabilities/explore/child_replay_runtime.py +463 -0
  168. loopx/capabilities/explore/composition_frontier.py +291 -0
  169. loopx/capabilities/explore/counterfactual_runtime.py +578 -0
  170. loopx/capabilities/explore/episode_runtime.py +647 -0
  171. loopx/capabilities/explore/harness_checkpoint.py +171 -0
  172. loopx/capabilities/explore/harness_gate.py +115 -0
  173. loopx/capabilities/explore/harness_runtime.py +1124 -0
  174. loopx/capabilities/explore/replay_metrics.py +206 -0
  175. loopx/capabilities/explore/replay_runtime.py +1271 -0
  176. loopx/capabilities/explore/resource_portfolio.py +173 -0
  177. loopx/capabilities/explore/result_log.py +974 -0
  178. loopx/capabilities/explore/router_state.py +432 -0
  179. loopx/capabilities/explore/source_history_reconcile.py +255 -0
  180. loopx/capabilities/explore/speculative_scheduler.py +498 -0
  181. loopx/capabilities/explore/todo_branch_plan.py +650 -0
  182. loopx/capabilities/explore/todo_evidence.py +141 -0
  183. loopx/capabilities/explore/trace_runtime.py +284 -0
  184. loopx/capabilities/explore/worker_branch_plan.py +1257 -0
  185. loopx/capabilities/integration_branch/__init__.py +13 -0
  186. loopx/capabilities/integration_branch/cli.py +148 -0
  187. loopx/capabilities/integration_branch/core.py +916 -0
  188. loopx/capabilities/issue_fix/__init__.py +19 -0
  189. loopx/capabilities/issue_fix/acceptance_loop.py +1050 -0
  190. loopx/capabilities/issue_fix/candidate_evidence.py +503 -0
  191. loopx/capabilities/issue_fix/candidate_preflight.py +676 -0
  192. loopx/capabilities/issue_fix/cli.py +1822 -0
  193. loopx/capabilities/issue_fix/cli_input.py +87 -0
  194. loopx/capabilities/issue_fix/content_ops_cli.py +148 -0
  195. loopx/capabilities/issue_fix/discovered_issue_promotion.py +947 -0
  196. loopx/capabilities/issue_fix/explore_projection.py +710 -0
  197. loopx/capabilities/issue_fix/feasibility.py +542 -0
  198. loopx/capabilities/issue_fix/github_public.py +661 -0
  199. loopx/capabilities/issue_fix/intake_surface.py +832 -0
  200. loopx/capabilities/issue_fix/metadata_preview.py +218 -0
  201. loopx/capabilities/issue_fix/metrics_projection.py +1340 -0
  202. loopx/capabilities/issue_fix/metrics_supplement.py +634 -0
  203. loopx/capabilities/issue_fix/metrics_supplement_cli.py +127 -0
  204. loopx/capabilities/issue_fix/outcome_projection.py +1235 -0
  205. loopx/capabilities/issue_fix/periodic_report.py +189 -0
  206. loopx/capabilities/issue_fix/pr_description.py +418 -0
  207. loopx/capabilities/issue_fix/pr_gate_reconcile.py +496 -0
  208. loopx/capabilities/issue_fix/pr_gate_reconcile_cli.py +464 -0
  209. loopx/capabilities/issue_fix/pr_lifecycle.py +1327 -0
  210. loopx/capabilities/issue_fix/pr_lifecycle_rollout.py +85 -0
  211. loopx/capabilities/issue_fix/pr_monitor_materialization.py +257 -0
  212. loopx/capabilities/issue_fix/pr_review_ack.py +439 -0
  213. loopx/capabilities/issue_fix/provider_hooks.py +24 -0
  214. loopx/capabilities/issue_fix/repository_commit_evidence.py +186 -0
  215. loopx/capabilities/issue_fix/repository_context.py +457 -0
  216. loopx/capabilities/issue_fix/repository_memory.py +459 -0
  217. loopx/capabilities/issue_fix/repository_memory_provider.py +1454 -0
  218. loopx/capabilities/issue_fix/repository_snapshot.py +454 -0
  219. loopx/capabilities/issue_fix/reviewer_cli.py +917 -0
  220. loopx/capabilities/issue_fix/reviewer_notification.py +882 -0
  221. loopx/capabilities/issue_fix/reviewer_notification_drain.py +942 -0
  222. loopx/capabilities/issue_fix/reviewer_recommendation.py +1057 -0
  223. loopx/capabilities/issue_fix/reviewer_request.py +1282 -0
  224. loopx/capabilities/issue_fix/reward_memory.py +879 -0
  225. loopx/capabilities/issue_fix/workflow_plan.py +1286 -0
  226. loopx/capabilities/material_lifecycle/__init__.py +161 -0
  227. loopx/capabilities/material_lifecycle/_validation.py +183 -0
  228. loopx/capabilities/material_lifecycle/apply.py +672 -0
  229. loopx/capabilities/material_lifecycle/architecture.py +122 -0
  230. loopx/capabilities/material_lifecycle/cli.py +161 -0
  231. loopx/capabilities/material_lifecycle/decision_planning.py +470 -0
  232. loopx/capabilities/material_lifecycle/explore_execution.py +306 -0
  233. loopx/capabilities/material_lifecycle/intake.py +869 -0
  234. loopx/capabilities/material_lifecycle/inventory.py +147 -0
  235. loopx/capabilities/material_lifecycle/lifecycle.py +98 -0
  236. loopx/capabilities/material_lifecycle/preparation.py +147 -0
  237. loopx/capabilities/material_lifecycle/project_skill.py +83 -0
  238. loopx/capabilities/material_lifecycle/ranking.py +267 -0
  239. loopx/capabilities/material_lifecycle/readable_projection.py +500 -0
  240. loopx/capabilities/material_lifecycle/rebuild.py +480 -0
  241. loopx/capabilities/material_lifecycle/settlement.py +238 -0
  242. loopx/capabilities/periodic_report/__init__.py +71 -0
  243. loopx/capabilities/periodic_report/adapters.py +939 -0
  244. loopx/capabilities/periodic_report/archive.py +422 -0
  245. loopx/capabilities/periodic_report/bindings.py +705 -0
  246. loopx/capabilities/periodic_report/cli.py +277 -0
  247. loopx/capabilities/periodic_report/core.py +691 -0
  248. loopx/capabilities/periodic_report/extension_envelope.py +66 -0
  249. loopx/capabilities/periodic_report/presets.py +103 -0
  250. loopx/capabilities/periodic_report/profile.py +235 -0
  251. loopx/capabilities/periodic_report/project_progress.py +179 -0
  252. loopx/capabilities/periodic_report/triggers.py +452 -0
  253. loopx/capabilities/pr_review_queue/__init__.py +17 -0
  254. loopx/capabilities/pr_review_queue/core.py +506 -0
  255. loopx/capabilities/pr_review_queue/review_contract.py +506 -0
  256. loopx/capabilities/registry.py +192 -0
  257. loopx/capabilities/reward_memory/__init__.py +75 -0
  258. loopx/capabilities/reward_memory/application.py +819 -0
  259. loopx/capabilities/reward_memory/architecture.py +572 -0
  260. loopx/capabilities/reward_memory/candidate_review.py +511 -0
  261. loopx/capabilities/reward_memory/cli.py +469 -0
  262. loopx/capabilities/reward_memory/dogfood.py +574 -0
  263. loopx/capabilities/reward_memory/evaluation.py +296 -0
  264. loopx/capabilities/reward_memory/evaluation_fixtures.py +362 -0
  265. loopx/capabilities/reward_memory/experiment.py +567 -0
  266. loopx/capabilities/reward_memory/health.py +222 -0
  267. loopx/capabilities/reward_memory/ingestion.py +519 -0
  268. loopx/capabilities/reward_memory/registry.py +600 -0
  269. loopx/capabilities/reward_memory/runtime_hooks.py +312 -0
  270. loopx/capabilities/reward_memory/scoped_feedback.py +173 -0
  271. loopx/capabilities/semantic_preference/__init__.py +12 -0
  272. loopx/capabilities/semantic_preference/cli.py +189 -0
  273. loopx/capabilities/semantic_preference/contract.py +592 -0
  274. loopx/capabilities/semantic_preference/reward_memory.py +62 -0
  275. loopx/capabilities/value_connectors/__init__.py +1 -0
  276. loopx/capabilities/value_connectors/cli.py +401 -0
  277. loopx/capabilities/value_connectors/finance_extension_migration.py +108 -0
  278. loopx/capabilities/value_connectors/install_check.py +147 -0
  279. loopx/capabilities/value_connectors/planner.py +733 -0
  280. loopx/capabilities/value_connectors/source_map.py +446 -0
  281. loopx/claude_goal_baseline.py +138 -0
  282. loopx/claude_goal_mode/__init__.py +23 -0
  283. loopx/claude_goal_mode/hooks/goal_policy.py +212 -0
  284. loopx/claude_goal_mode/hooks/goal_state.py +139 -0
  285. loopx/claude_goal_mode/mcp/loopx_mcp.py +167 -0
  286. loopx/claude_goal_mode/scripts/connect.py +103 -0
  287. loopx/claude_goal_mode/scripts/goalmode_cmd.py +241 -0
  288. loopx/claude_goal_mode/scripts/install.py +328 -0
  289. loopx/claude_goal_mode/statusline/goal_status.py +97 -0
  290. loopx/cli.py +836 -0
  291. loopx/cli_commands/__init__.py +334 -0
  292. loopx/cli_commands/_host_thread.py +13 -0
  293. loopx/cli_commands/agentissue_runner_flow.py +447 -0
  294. loopx/cli_commands/agents_last_exam.py +160 -0
  295. loopx/cli_commands/agents_last_exam_baked_input.py +302 -0
  296. loopx/cli_commands/agents_last_exam_host_codex.py +374 -0
  297. loopx/cli_commands/agents_last_exam_launch_dry_run.py +372 -0
  298. loopx/cli_commands/agents_last_exam_local_plan.py +322 -0
  299. loopx/cli_commands/agents_last_exam_runner_source.py +352 -0
  300. loopx/cli_commands/agents_last_exam_task_material.py +335 -0
  301. loopx/cli_commands/agents_last_exam_validation_gate.py +236 -0
  302. loopx/cli_commands/benchmark_boundary.py +499 -0
  303. loopx/cli_commands/benchmark_dispatch.py +161 -0
  304. loopx/cli_commands/benchmark_release_outcome.py +123 -0
  305. loopx/cli_commands/benchmark_review_lifecycle.py +1275 -0
  306. loopx/cli_commands/benchmark_run_ledger.py +763 -0
  307. loopx/cli_commands/benchmark_run_ledger_case_analysis.py +249 -0
  308. loopx/cli_commands/benchmark_run_ledger_classification.py +45 -0
  309. loopx/cli_commands/benchmark_run_ledger_maintenance.py +486 -0
  310. loopx/cli_commands/benchmark_run_ledger_maintenance_registration.py +342 -0
  311. loopx/cli_commands/benchmark_run_ledger_maintenance_rendering.py +233 -0
  312. loopx/cli_commands/benchmark_run_ledger_parity.py +92 -0
  313. loopx/cli_commands/bootstrap_connect.py +238 -0
  314. loopx/cli_commands/canary.py +707 -0
  315. loopx/cli_commands/canary_release_qualification.py +79 -0
  316. loopx/cli_commands/capability.py +96 -0
  317. loopx/cli_commands/doctor.py +43 -0
  318. loopx/cli_commands/dreaming.py +143 -0
  319. loopx/cli_commands/edgebench.py +205 -0
  320. loopx/cli_commands/evidence_log.py +275 -0
  321. loopx/cli_commands/explore.py +989 -0
  322. loopx/cli_commands/explore_planning_commands.py +157 -0
  323. loopx/cli_commands/extension.py +271 -0
  324. loopx/cli_commands/first_run_report.py +73 -0
  325. loopx/cli_commands/goal_channel.py +656 -0
  326. loopx/cli_commands/handoff_mode.py +158 -0
  327. loopx/cli_commands/history.py +622 -0
  328. loopx/cli_commands/host_mode_plan.py +113 -0
  329. loopx/cli_commands/lark_inbox.py +431 -0
  330. loopx/cli_commands/lark_kanban.py +629 -0
  331. loopx/cli_commands/ml_experiment.py +321 -0
  332. loopx/cli_commands/multi_agent.py +211 -0
  333. loopx/cli_commands/opencode2_goal_worker.py +217 -0
  334. loopx/cli_commands/pr_review.py +167 -0
  335. loopx/cli_commands/presentation.py +218 -0
  336. loopx/cli_commands/preset.py +96 -0
  337. loopx/cli_commands/project.py +150 -0
  338. loopx/cli_commands/project_lifecycle.py +915 -0
  339. loopx/cli_commands/quota.py +859 -0
  340. loopx/cli_commands/quota_registration.py +241 -0
  341. loopx/cli_commands/quota_request.py +113 -0
  342. loopx/cli_commands/ready_score.py +110 -0
  343. loopx/cli_commands/registry_admin.py +975 -0
  344. loopx/cli_commands/registry_admin_configure.py +344 -0
  345. loopx/cli_commands/registry_admin_peer.py +84 -0
  346. loopx/cli_commands/registry_authority.py +218 -0
  347. loopx/cli_commands/review_batch.py +146 -0
  348. loopx/cli_commands/slash_commands.py +145 -0
  349. loopx/cli_commands/start_goal.py +251 -0
  350. loopx/cli_commands/starter.py +175 -0
  351. loopx/cli_commands/starter_bootstrap.py +179 -0
  352. loopx/cli_commands/starter_bootstrap_registration.py +198 -0
  353. loopx/cli_commands/starter_runtime_idle.py +107 -0
  354. loopx/cli_commands/starter_scheduler.py +207 -0
  355. loopx/cli_commands/starter_session_runtime.py +152 -0
  356. loopx/cli_commands/starter_visible_common.py +54 -0
  357. loopx/cli_commands/starter_visible_driver.py +161 -0
  358. loopx/cli_commands/starter_visible_pilot.py +278 -0
  359. loopx/cli_commands/status.py +867 -0
  360. loopx/cli_commands/status_registration.py +239 -0
  361. loopx/cli_commands/summary_all.py +222 -0
  362. loopx/cli_commands/support_control.py +809 -0
  363. loopx/cli_commands/support_control_registry.py +68 -0
  364. loopx/cli_commands/support_control_supervisor.py +289 -0
  365. loopx/cli_commands/task_lease.py +306 -0
  366. loopx/cli_commands/terminal_bench_adapter.py +717 -0
  367. loopx/cli_commands/terminal_bench_environment_result.py +1246 -0
  368. loopx/cli_commands/todo.py +940 -0
  369. loopx/cli_commands/todo_argument_validation.py +572 -0
  370. loopx/cli_commands/todo_event.py +114 -0
  371. loopx/cli_commands/turn.py +804 -0
  372. loopx/cli_commands/version.py +46 -0
  373. loopx/cli_commands/worker_bridge.py +659 -0
  374. loopx/cli_rollout.py +314 -0
  375. loopx/codex_cli_goal_tui.py +672 -0
  376. loopx/codex_cli_probe.py +1530 -0
  377. loopx/codex_cli_probe_markdown.py +935 -0
  378. loopx/codex_cli_runtime_probe.py +733 -0
  379. loopx/codex_cli_scheduler.py +564 -0
  380. loopx/codex_goal_baseline.py +620 -0
  381. loopx/configuration_catalog.py +617 -0
  382. loopx/configure_goal.py +1375 -0
  383. loopx/contract.py +996 -0
  384. loopx/control_plane/__init__.py +71 -0
  385. loopx/control_plane/agents/__init__.py +1 -0
  386. loopx/control_plane/agents/agent_lane_recommendation.py +516 -0
  387. loopx/control_plane/agents/agent_scope.py +1578 -0
  388. loopx/control_plane/agents/agent_scope_frontier.py +60 -0
  389. loopx/control_plane/agents/capability_gate.py +531 -0
  390. loopx/control_plane/agents/identity.py +140 -0
  391. loopx/control_plane/agents/legacy_migration.py +169 -0
  392. loopx/control_plane/agents/management_projection.py +658 -0
  393. loopx/control_plane/agents/material_frontier.py +608 -0
  394. loopx/control_plane/agents/material_handoff.py +156 -0
  395. loopx/control_plane/agents/multi_agent/__init__.py +1 -0
  396. loopx/control_plane/agents/multi_agent/codex_executable.py +207 -0
  397. loopx/control_plane/agents/multi_agent/collective_round_ledger.py +387 -0
  398. loopx/control_plane/agents/multi_agent/contract.py +474 -0
  399. loopx/control_plane/agents/multi_agent/recipe.py +110 -0
  400. loopx/control_plane/agents/multi_agent/role_successor.py +297 -0
  401. loopx/control_plane/agents/multi_agent/runtime_scripts.py +426 -0
  402. loopx/control_plane/agents/multi_agent/visible_launch_policy.py +149 -0
  403. loopx/control_plane/agents/multi_agent/visible_wake_scheduler.py +392 -0
  404. loopx/control_plane/agents/profile.py +216 -0
  405. loopx/control_plane/agents/runtime_model.py +73 -0
  406. loopx/control_plane/agents/subagent_activity.py +164 -0
  407. loopx/control_plane/agents/supervisor.py +544 -0
  408. loopx/control_plane/agents/supervisor_events.py +462 -0
  409. loopx/control_plane/agents/supervisor_inject.py +204 -0
  410. loopx/control_plane/agents/work_mode.py +56 -0
  411. loopx/control_plane/agents/workspace_guard.py +364 -0
  412. loopx/control_plane/effect_program.py +644 -0
  413. loopx/control_plane/goals/__init__.py +1 -0
  414. loopx/control_plane/goals/active_state_event_projection.py +103 -0
  415. loopx/control_plane/goals/active_state_metadata.py +47 -0
  416. loopx/control_plane/goals/active_state_sections.py +58 -0
  417. loopx/control_plane/goals/configure_goal_service.py +354 -0
  418. loopx/control_plane/goals/contract_health.py +132 -0
  419. loopx/control_plane/goals/dreaming.py +152 -0
  420. loopx/control_plane/goals/global_registry_health.py +199 -0
  421. loopx/control_plane/goals/global_registry_shadow.py +33 -0
  422. loopx/control_plane/goals/goal_channel.py +34 -0
  423. loopx/control_plane/goals/goal_channel_projection.py +560 -0
  424. loopx/control_plane/goals/goal_frontier/__init__.py +1917 -0
  425. loopx/control_plane/goals/goal_frontier/ack_policy.py +149 -0
  426. loopx/control_plane/goals/goal_frontier/outcome_continuity.py +437 -0
  427. loopx/control_plane/goals/goal_frontier/replan_rules.py +210 -0
  428. loopx/control_plane/goals/goal_frontier/semantic_history.py +314 -0
  429. loopx/control_plane/goals/goal_frontier/terminal.py +180 -0
  430. loopx/control_plane/goals/goal_vision.py +443 -0
  431. loopx/control_plane/goals/goal_vision_policy.py +36 -0
  432. loopx/control_plane/goals/goal_vision_state.py +62 -0
  433. loopx/control_plane/goals/goal_vision_wait.py +290 -0
  434. loopx/control_plane/goals/path_resolution.py +20 -0
  435. loopx/control_plane/goals/start_contract.py +206 -0
  436. loopx/control_plane/goals/vision_checkpoint.py +92 -0
  437. loopx/control_plane/handoff/__init__.py +1 -0
  438. loopx/control_plane/handoff/cross_runtime_impl_review.py +311 -0
  439. loopx/control_plane/handoff/delivery_contract.py +161 -0
  440. loopx/control_plane/handoff/handoff_runs.py +71 -0
  441. loopx/control_plane/handoff/project_handoff.py +155 -0
  442. loopx/control_plane/handoff/review_batch.py +463 -0
  443. loopx/control_plane/handoff/review_packet_context.py +216 -0
  444. loopx/control_plane/heartbeat/agent.py +173 -0
  445. loopx/control_plane/heartbeat/budget.py +66 -0
  446. loopx/control_plane/heartbeat/builder.py +501 -0
  447. loopx/control_plane/heartbeat/host.py +64 -0
  448. loopx/control_plane/heartbeat/rules.py +68 -0
  449. loopx/control_plane/heartbeat/task_body.py +759 -0
  450. loopx/control_plane/heartbeat/visible_goal.py +86 -0
  451. loopx/control_plane/projects/__init__.py +1 -0
  452. loopx/control_plane/projects/contract.py +25 -0
  453. loopx/control_plane/projects/registry.py +663 -0
  454. loopx/control_plane/quota/__init__.py +1 -0
  455. loopx/control_plane/quota/cli_projection.py +704 -0
  456. loopx/control_plane/quota/decision_summary.py +431 -0
  457. loopx/control_plane/quota/effect_program.py +152 -0
  458. loopx/control_plane/quota/error_codes.py +19 -0
  459. loopx/control_plane/quota/goal_boundary.py +464 -0
  460. loopx/control_plane/quota/heartbeat_receipt.py +277 -0
  461. loopx/control_plane/quota/heartbeat_recommendation.py +718 -0
  462. loopx/control_plane/quota/host_poll_receipts.py +162 -0
  463. loopx/control_plane/quota/live_decision.py +142 -0
  464. loopx/control_plane/quota/monitor_poll.py +786 -0
  465. loopx/control_plane/quota/policy_constants.py +40 -0
  466. loopx/control_plane/quota/projection_repair.py +262 -0
  467. loopx/control_plane/quota/recent_runs.py +210 -0
  468. loopx/control_plane/quota/scheduler_ack.py +490 -0
  469. loopx/control_plane/quota/selected_todo_projection.py +139 -0
  470. loopx/control_plane/quota/settlement.py +437 -0
  471. loopx/control_plane/quota/settlement_cli.py +246 -0
  472. loopx/control_plane/quota/settlement_validation.py +64 -0
  473. loopx/control_plane/quota/settlement_workspace_causality.py +180 -0
  474. loopx/control_plane/quota/should_run.py +249 -0
  475. loopx/control_plane/quota/should_run_packet.py +1165 -0
  476. loopx/control_plane/quota/should_run_prepare.py +675 -0
  477. loopx/control_plane/quota/slot_accounting.py +1123 -0
  478. loopx/control_plane/quota/spend_sources.py +11 -0
  479. loopx/control_plane/quota/stall_repair.py +397 -0
  480. loopx/control_plane/quota/states.py +29 -0
  481. loopx/control_plane/quota/task_orchestration.py +448 -0
  482. loopx/control_plane/quota/task_orchestration_admission.py +497 -0
  483. loopx/control_plane/quota/turn_envelope.py +889 -0
  484. loopx/control_plane/quota/usage_summary.py +140 -0
  485. loopx/control_plane/reward_memory.py +43 -0
  486. loopx/control_plane/runtime/__init__.py +2 -0
  487. loopx/control_plane/runtime/active_user_assisted_pilot.py +275 -0
  488. loopx/control_plane/runtime/agent_scoped_evidence_log.py +435 -0
  489. loopx/control_plane/runtime/decision_freshness.py +203 -0
  490. loopx/control_plane/runtime/event_ledger.py +197 -0
  491. loopx/control_plane/runtime/event_store_migration_bridge.py +196 -0
  492. loopx/control_plane/runtime/goal_project_route.py +70 -0
  493. loopx/control_plane/runtime/local_state_write_correctness.py +242 -0
  494. loopx/control_plane/runtime/promotion_readiness.py +152 -0
  495. loopx/control_plane/runtime/public_safety.py +120 -0
  496. loopx/control_plane/runtime/run_artifacts.py +78 -0
  497. loopx/control_plane/runtime/run_compaction.py +397 -0
  498. loopx/control_plane/runtime/run_context_retention.py +241 -0
  499. loopx/control_plane/runtime/run_history.py +132 -0
  500. loopx/control_plane/runtime/run_index_duplicates.py +205 -0
  501. loopx/control_plane/runtime/run_index_rebuild.py +263 -0
  502. loopx/control_plane/runtime/run_ingest_health.py +336 -0
  503. loopx/control_plane/runtime/runtime_projection_route.py +624 -0
  504. loopx/control_plane/runtime/runtime_projection_writer.py +98 -0
  505. loopx/control_plane/runtime/session_runtime.py +339 -0
  506. loopx/control_plane/runtime/shared_runtime_material_projection.py +332 -0
  507. loopx/control_plane/runtime/shared_runtime_refresh_projection.py +183 -0
  508. loopx/control_plane/runtime/stale_latest_run.py +90 -0
  509. loopx/control_plane/runtime/status_classifications.py +49 -0
  510. loopx/control_plane/runtime/status_projection_cache.py +235 -0
  511. loopx/control_plane/runtime/stride_observation.py +144 -0
  512. loopx/control_plane/runtime/time.py +39 -0
  513. loopx/control_plane/runtime/trajectory_hygiene.py +149 -0
  514. loopx/control_plane/runtime/validation_command.py +69 -0
  515. loopx/control_plane/scheduler/__init__.py +1 -0
  516. loopx/control_plane/scheduler/ack.py +329 -0
  517. loopx/control_plane/scheduler/arbitration.py +188 -0
  518. loopx/control_plane/scheduler/automation_liveness.py +183 -0
  519. loopx/control_plane/scheduler/execution_context.py +555 -0
  520. loopx/control_plane/scheduler/external_evidence_observation.py +428 -0
  521. loopx/control_plane/scheduler/monitor_display.py +143 -0
  522. loopx/control_plane/scheduler/monitor_poll_policy.py +161 -0
  523. loopx/control_plane/scheduler/monitor_poll_writeback.py +351 -0
  524. loopx/control_plane/scheduler/monitor_target.py +64 -0
  525. loopx/control_plane/scheduler/monitor_todo.py +146 -0
  526. loopx/control_plane/scheduler/monitor_wait.py +237 -0
  527. loopx/control_plane/scheduler/scheduler_hint.py +1284 -0
  528. loopx/control_plane/scheduler/state.py +354 -0
  529. loopx/control_plane/scheduler/state_transition_rules.py +179 -0
  530. loopx/control_plane/scheduler/time.py +10 -0
  531. loopx/control_plane/settlement_driver.py +293 -0
  532. loopx/control_plane/status/__init__.py +6 -0
  533. loopx/control_plane/status/active_state_projection.py +105 -0
  534. loopx/control_plane/status/agent_lane_projection.py +375 -0
  535. loopx/control_plane/status/attention_projection.py +74 -0
  536. loopx/control_plane/status/autonomous_replan_projection.py +103 -0
  537. loopx/control_plane/status/collection.py +140 -0
  538. loopx/control_plane/status/contract_projection.py +31 -0
  539. loopx/control_plane/status/dreaming_projection.py +52 -0
  540. loopx/control_plane/status/goal_attention_projection.py +157 -0
  541. loopx/control_plane/status/lifecycle_projection.py +110 -0
  542. loopx/control_plane/status/monitor_display_projection.py +69 -0
  543. loopx/control_plane/status/registry_health_projection.py +75 -0
  544. loopx/control_plane/status/run_projection.py +70 -0
  545. loopx/control_plane/status/runtime_summaries.py +161 -0
  546. loopx/control_plane/testing/__init__.py +1 -0
  547. loopx/control_plane/testing/actual_default_model_behavior_portfolio.py +1371 -0
  548. loopx/control_plane/testing/canary_harness.py +182 -0
  549. loopx/control_plane/testing/capability_monitor_repair_tool_behavior.py +674 -0
  550. loopx/control_plane/testing/cli_output_budget.py +807 -0
  551. loopx/control_plane/testing/cli_output_differential.py +250 -0
  552. loopx/control_plane/testing/cli_output_semantics.py +87 -0
  553. loopx/control_plane/testing/control_plane_composition_scenarios.py +225 -0
  554. loopx/control_plane/testing/decision_replay.py +268 -0
  555. loopx/control_plane/testing/doubao_model_behavior_actor.py +559 -0
  556. loopx/control_plane/testing/model_behavior_corpus.py +344 -0
  557. loopx/control_plane/testing/model_behavior_qualification.py +769 -0
  558. loopx/control_plane/testing/model_behavior_retained_cases.py +235 -0
  559. loopx/control_plane/testing/model_tool_behavior.py +536 -0
  560. loopx/control_plane/testing/onboarding_model_behavior_qualification.py +642 -0
  561. loopx/control_plane/testing/quota_fixtures.py +208 -0
  562. loopx/control_plane/testing/quota_should_run_parity.py +57 -0
  563. loopx/control_plane/testing/release_commit_qualification.py +671 -0
  564. loopx/control_plane/testing/replan_semantic_action_behavior.py +1302 -0
  565. loopx/control_plane/testing/scoped_gate_successor_tool_behavior.py +527 -0
  566. loopx/control_plane/testing/selected_todo_tool_behavior.py +1002 -0
  567. loopx/control_plane/testing/terminal_settlement_tool_behavior.py +656 -0
  568. loopx/control_plane/todos/__init__.py +1 -0
  569. loopx/control_plane/todos/active_state_editing.py +296 -0
  570. loopx/control_plane/todos/active_state_todo_parser.py +138 -0
  571. loopx/control_plane/todos/active_state_todos.py +175 -0
  572. loopx/control_plane/todos/addition.py +103 -0
  573. loopx/control_plane/todos/claim_visibility.py +253 -0
  574. loopx/control_plane/todos/completed_archive.py +139 -0
  575. loopx/control_plane/todos/completion_fence.py +49 -0
  576. loopx/control_plane/todos/completion_policy.py +153 -0
  577. loopx/control_plane/todos/completion_validation.py +248 -0
  578. loopx/control_plane/todos/completion_validation_accountability.py +27 -0
  579. loopx/control_plane/todos/completion_validation_projection.py +57 -0
  580. loopx/control_plane/todos/contract.py +1476 -0
  581. loopx/control_plane/todos/decision_scope.py +554 -0
  582. loopx/control_plane/todos/deferred_resume.py +546 -0
  583. loopx/control_plane/todos/durable_completion.py +201 -0
  584. loopx/control_plane/todos/event_writeback.py +484 -0
  585. loopx/control_plane/todos/frontier_deadline.py +132 -0
  586. loopx/control_plane/todos/handoff_gate.py +283 -0
  587. loopx/control_plane/todos/handoff_mode.py +444 -0
  588. loopx/control_plane/todos/handoff_note.py +202 -0
  589. loopx/control_plane/todos/line_update.py +361 -0
  590. loopx/control_plane/todos/list_projection.py +205 -0
  591. loopx/control_plane/todos/markdown.py +199 -0
  592. loopx/control_plane/todos/monitor_metadata.py +88 -0
  593. loopx/control_plane/todos/mutation_authority.py +299 -0
  594. loopx/control_plane/todos/projection.py +655 -0
  595. loopx/control_plane/todos/quota_summary.py +1138 -0
  596. loopx/control_plane/todos/route_continuation.py +267 -0
  597. loopx/control_plane/todos/succession_warning.py +174 -0
  598. loopx/control_plane/todos/summary_item.py +223 -0
  599. loopx/control_plane/todos/text.py +30 -0
  600. loopx/control_plane/todos/todo_index.py +226 -0
  601. loopx/control_plane/todos/todo_summary.py +1458 -0
  602. loopx/control_plane/todos/unblock_resume.py +326 -0
  603. loopx/control_plane/todos/user_gate.py +263 -0
  604. loopx/control_plane/todos/write_hint.py +63 -0
  605. loopx/control_plane/todos/write_policy.py +135 -0
  606. loopx/control_plane/turn_driver/__init__.py +85 -0
  607. loopx/control_plane/turn_driver/codex_cli.py +502 -0
  608. loopx/control_plane/turn_driver/driver.py +355 -0
  609. loopx/control_plane/turn_driver/executor.py +1468 -0
  610. loopx/control_plane/turn_driver/loop_controller.py +669 -0
  611. loopx/control_plane/turn_driver/settlement.py +318 -0
  612. loopx/control_plane/turn_driver/transaction.py +375 -0
  613. loopx/control_plane/work_items/__init__.py +1 -0
  614. loopx/control_plane/work_items/attention_fields.py +56 -0
  615. loopx/control_plane/work_items/attention_item.py +77 -0
  616. loopx/control_plane/work_items/attention_queue.py +322 -0
  617. loopx/control_plane/work_items/attention_routing.py +213 -0
  618. loopx/control_plane/work_items/autonomous_candidates.py +135 -0
  619. loopx/control_plane/work_items/autonomous_replan_ack.py +276 -0
  620. loopx/control_plane/work_items/autonomous_replan_obligation.py +786 -0
  621. loopx/control_plane/work_items/backlog_hygiene.py +59 -0
  622. loopx/control_plane/work_items/capability_monitor_fallback.py +221 -0
  623. loopx/control_plane/work_items/delivery_batch_scale.py +66 -0
  624. loopx/control_plane/work_items/delivery_outcome.py +152 -0
  625. loopx/control_plane/work_items/delivery_signals.py +113 -0
  626. loopx/control_plane/work_items/execution_obligation.py +235 -0
  627. loopx/control_plane/work_items/goal_route_hint.py +320 -0
  628. loopx/control_plane/work_items/interaction_contract.py +1540 -0
  629. loopx/control_plane/work_items/issue_meta_surface.py +159 -0
  630. loopx/control_plane/work_items/lifecycle.py +139 -0
  631. loopx/control_plane/work_items/operator_inbox.py +266 -0
  632. loopx/control_plane/work_items/outcome_followthrough.py +69 -0
  633. loopx/control_plane/work_items/primary_action.py +326 -0
  634. loopx/control_plane/work_items/progress_observation.py +630 -0
  635. loopx/control_plane/work_items/project_asset.py +675 -0
  636. loopx/control_plane/work_items/repair_delta.py +693 -0
  637. loopx/control_plane/work_items/runtime_capability_reentry.py +168 -0
  638. loopx/control_plane/work_items/semantic_replan_writeback.py +177 -0
  639. loopx/control_plane/work_items/status_contract.py +49 -0
  640. loopx/control_plane/work_items/task_graph.py +1046 -0
  641. loopx/control_plane/work_items/task_lease.py +1254 -0
  642. loopx/control_plane/work_items/task_lease_settlement.py +422 -0
  643. loopx/control_plane/work_items/work_lane.py +510 -0
  644. loopx/control_plane/work_items/work_lane_context.py +161 -0
  645. loopx/demo.py +247 -0
  646. loopx/diagnose.py +633 -0
  647. loopx/doctor.py +1251 -0
  648. loopx/domain_packs/__init__.py +1 -0
  649. loopx/domain_packs/issue_fix.py +571 -0
  650. loopx/domain_packs/ml_experiment.py +854 -0
  651. loopx/domain_state.py +137 -0
  652. loopx/dreaming.py +706 -0
  653. loopx/entrypoint.py +16 -0
  654. loopx/event_sourced_state.py +981 -0
  655. loopx/execution_profile.py +286 -0
  656. loopx/experiments/__init__.py +1 -0
  657. loopx/experiments/planner_worker/__init__.py +1 -0
  658. loopx/experiments/planner_worker/contract.py +523 -0
  659. loopx/experiments/planner_worker/runtime.py +391 -0
  660. loopx/experiments/planner_worker/traex.py +461 -0
  661. loopx/explore_graph.py +11 -0
  662. loopx/extensions/__init__.py +1 -0
  663. loopx/extensions/bundled.py +28 -0
  664. loopx/extensions/execution_envelope.py +126 -0
  665. loopx/extensions/lark/__init__.py +11 -0
  666. loopx/extensions/lark/event_collector.py +478 -0
  667. loopx/extensions/lark/event_collector_runtime.py +506 -0
  668. loopx/extensions/lark/event_inbox.py +454 -0
  669. loopx/extensions/lark/extension.toml +88 -0
  670. loopx/extensions/lark/goal_channel.py +44 -0
  671. loopx/extensions/lark/goal_channel_contracts.py +388 -0
  672. loopx/extensions/lark/goal_channel_lifecycle.py +218 -0
  673. loopx/extensions/lark/goal_channel_runtime.py +792 -0
  674. loopx/extensions/lark/goal_channel_setup.py +805 -0
  675. loopx/extensions/lark/goal_channel_targets.py +215 -0
  676. loopx/extensions/lark/goal_channel_transport.py +281 -0
  677. loopx/extensions/lark/inbox_reactions.py +650 -0
  678. loopx/extensions/lark/inbox_reply.py +430 -0
  679. loopx/extensions/lark/presentation/__init__.py +11 -0
  680. loopx/extensions/lark/presentation/explore_results.py +2276 -0
  681. loopx/extensions/lark/presentation/explore_singleflight.py +127 -0
  682. loopx/extensions/lark/presentation/explore_source_guard.py +121 -0
  683. loopx/extensions/lark/presentation/explore_stage_document.py +703 -0
  684. loopx/extensions/lark/presentation/explore_visual_integrity.py +122 -0
  685. loopx/extensions/lark/presentation/explore_visual_readback.py +452 -0
  686. loopx/extensions/lark/presentation/explore_visual_styles.py +156 -0
  687. loopx/extensions/lark/presentation/issue_fix_surface.py +612 -0
  688. loopx/extensions/lark/presentation/kanban.py +2791 -0
  689. loopx/extensions/lark/presentation/message_card.py +112 -0
  690. loopx/extensions/lark/presentation/periodic_report.py +261 -0
  691. loopx/extensions/lark/presentation/projection_rows.py +600 -0
  692. loopx/extensions/lark/presentation/record_io.py +95 -0
  693. loopx/extensions/lark/presentation/sync_receipt.py +145 -0
  694. loopx/extensions/lark/private_json.py +40 -0
  695. loopx/extensions/lark/provider.py +86 -0
  696. loopx/extensions/lark/reviewer_notification.py +604 -0
  697. loopx/extensions/manifest.py +385 -0
  698. loopx/extensions/openviking_periodic_report/__init__.py +17 -0
  699. loopx/extensions/openviking_periodic_report/activation.py +173 -0
  700. loopx/extensions/openviking_periodic_report/extension.toml +17 -0
  701. loopx/extensions/openviking_periodic_report/provider.py +355 -0
  702. loopx/extensions/openviking_periodic_report/sink.py +117 -0
  703. loopx/extensions/openviking_semantic_preference/__init__.py +5 -0
  704. loopx/extensions/openviking_semantic_preference/extension.toml +16 -0
  705. loopx/extensions/openviking_semantic_preference/history_export.py +484 -0
  706. loopx/extensions/openviking_semantic_preference/project_peer.py +68 -0
  707. loopx/extensions/openviking_semantic_preference/provider.py +312 -0
  708. loopx/extensions/presentation.py +979 -0
  709. loopx/extensions/process_runtime.py +204 -0
  710. loopx/extensions/readiness.py +168 -0
  711. loopx/extensions/runtime.py +931 -0
  712. loopx/extensions/scaffold.py +335 -0
  713. loopx/feedback.py +581 -0
  714. loopx/file_lock.py +382 -0
  715. loopx/global_registry.py +842 -0
  716. loopx/global_risks.py +970 -0
  717. loopx/global_todos.py +568 -0
  718. loopx/handoff_budget.py +28 -0
  719. loopx/heartbeat_prequota.py +80 -0
  720. loopx/heartbeat_prompt.py +159 -0
  721. loopx/help_surface.py +516 -0
  722. loopx/history.py +1507 -0
  723. loopx/host_loop_activation.py +1311 -0
  724. loopx/host_mode_planner.py +991 -0
  725. loopx/install_contract.py +1 -0
  726. loopx/interface_budget.py +196 -0
  727. loopx/long_task_cadence.py +208 -0
  728. loopx/materials.py +185 -0
  729. loopx/ml_experiment.py +3 -0
  730. loopx/onboarding.py +214 -0
  731. loopx/opencode2_goal_mode/README.md +81 -0
  732. loopx/opencode2_goal_mode/__init__.py +9 -0
  733. loopx/opencode2_goal_mode/opencode2-goal-worker.mjs +1018 -0
  734. loopx/opencode_goal_mode/README.md +99 -0
  735. loopx/opencode_goal_mode/__init__.py +13 -0
  736. loopx/opencode_goal_mode/goal-bridge-runtime.mjs +858 -0
  737. loopx/opencode_goal_mode/loopx-goal.js +8 -0
  738. loopx/operator_gate.py +420 -0
  739. loopx/orchestration.py +127 -0
  740. loopx/paths.py +59 -0
  741. loopx/pi_goal_mode/README.md +67 -0
  742. loopx/pi_goal_mode/__init__.py +13 -0
  743. loopx/pi_goal_mode/loopx-goal.ts +254 -0
  744. loopx/pi_goal_mode/pi-goal-loop-runtime.mjs +574 -0
  745. loopx/pr_review.py +1206 -0
  746. loopx/presentation/__init__.py +1 -0
  747. loopx/presentation/explore_views.py +1334 -0
  748. loopx/presentation/markdown.py +61 -0
  749. loopx/presentation/projection_source_reconcile.py +140 -0
  750. loopx/presentation/public_safety.py +42 -0
  751. loopx/presentation/renderers/__init__.py +17 -0
  752. loopx/presentation/renderers/goal_channel_html.py +269 -0
  753. loopx/presentation/renderers/periodic_report_html.py +786 -0
  754. loopx/presentation/renderers/periodic_report_markdown.py +184 -0
  755. loopx/presentation/renderers/quota_event_markdown.py +116 -0
  756. loopx/presentation/renderers/quota_markdown.py +1112 -0
  757. loopx/presentation/renderers/status_markdown.py +1570 -0
  758. loopx/presentation/renderers/trajectory_hygiene_markdown.py +39 -0
  759. loopx/presentation/renderers/turn_envelope_markdown.py +33 -0
  760. loopx/presentation/sinks/__init__.py +5 -0
  761. loopx/presentation/sinks/openviking_periodic_report.py +7 -0
  762. loopx/presentation/static_site.py +691 -0
  763. loopx/presets.py +369 -0
  764. loopx/project_alias.py +217 -0
  765. loopx/project_map.py +589 -0
  766. loopx/project_prompt.py +1153 -0
  767. loopx/project_skill_cli.py +125 -0
  768. loopx/project_skill_delivery.py +470 -0
  769. loopx/project_uninstall.py +462 -0
  770. loopx/promotion_gate.py +197 -0
  771. loopx/quota.py +1197 -0
  772. loopx/ready_score.py +413 -0
  773. loopx/registry.py +621 -0
  774. loopx/registry_writability.py +64 -0
  775. loopx/release_candidate.py +148 -0
  776. loopx/release_manifest.py +316 -0
  777. loopx/repository_identity.py +100 -0
  778. loopx/review_packet.py +1024 -0
  779. loopx/rollout_event_log.py +505 -0
  780. loopx/runtime.py +112 -0
  781. loopx/self_update.py +750 -0
  782. loopx/session_runtime.py +418 -0
  783. loopx/skill_install_readback.py +500 -0
  784. loopx/slash_command_install.py +1393 -0
  785. loopx/slash_commands.py +264 -0
  786. loopx/state_backup.py +573 -0
  787. loopx/state_migration.py +350 -0
  788. loopx/state_projection.py +809 -0
  789. loopx/state_refresh.py +1416 -0
  790. loopx/status.py +1383 -0
  791. loopx/status_server.py +935 -0
  792. loopx/summary_all.py +725 -0
  793. loopx/terminal_bench_agent.py +2056 -0
  794. loopx/thread_agent_binding.py +408 -0
  795. loopx/todo_followups.py +168 -0
  796. loopx/todo_suggestion_prompt.py +204 -0
  797. loopx/todos.py +2229 -0
  798. loopx/turn_identity.py +17 -0
  799. loopx/upgrade.py +1083 -0
  800. loopx/visible_governance.py +667 -0
  801. loopx/visible_multi_agent_launcher.py +1253 -0
  802. loopx/visible_multi_agent_tmux.py +429 -0
  803. loopx/worker_bridge.py +1574 -0
  804. loopx-0.4.8.dist-info/METADATA +708 -0
  805. loopx-0.4.8.dist-info/RECORD +811 -0
  806. loopx-0.4.8.dist-info/WHEEL +5 -0
  807. loopx-0.4.8.dist-info/entry_points.txt +5 -0
  808. loopx-0.4.8.dist-info/licenses/LICENSE +202 -0
  809. loopx-0.4.8.dist-info/licenses/LICENSE-MIT +21 -0
  810. loopx-0.4.8.dist-info/licenses/NOTICE +6 -0
  811. loopx-0.4.8.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2276 @@
1
+ """Project the LoopX exploration topology into a Feishu/Lark Base result board.
2
+
3
+ This sink renders the bounded projection built by
4
+ ``loopx.capabilities.explore.result_log`` into three Bitable tables (Nodes,
5
+ Edges, Findings) plus one interactive result card that answers three operator
6
+ questions: what has been explored, where is the loop blocked and why, and what
7
+ was found. It follows the Lark Kanban adapter contract: all external effects
8
+ go through ``lark-cli`` commands behind an injectable runner, every write is
9
+ dry-run unless ``execute=True``, and shared-visibility rows pass the
10
+ public-safe redaction used by the Kanban sync. Card content is transport-free;
11
+ an approved gateway sends or updates the actual Lark message. The Mermaid
12
+ topology source in the projection is for Feishu docs or any diagram renderer.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import hashlib
18
+ import json
19
+ from dataclasses import dataclass, field
20
+ from pathlib import Path
21
+ from typing import Any, Mapping
22
+
23
+ from ....capabilities.explore.result_log import (
24
+ EXPLORE_RESULT_PROJECTION_VERSION,
25
+ FINDING_STATUS_CONFIRMED,
26
+ FINDING_STATUS_REFUTED,
27
+ FINDING_STATUS_TENTATIVE,
28
+ NODE_KINDS,
29
+ NODE_STATUS_BLOCKED,
30
+ NODE_STATUS_DEAD_END,
31
+ NODE_STATUS_EXPLORING,
32
+ NODE_STATUS_OPEN,
33
+ NODE_STATUS_RESOLVED,
34
+ EDGE_TYPES,
35
+ build_explore_graph_view,
36
+ )
37
+ from ....presentation.explore_views import (
38
+ PRESENTATION_MODE_DUAL_VIEW,
39
+ build_explore_presentation_bundle,
40
+ explore_source_digest,
41
+ explore_source_revision,
42
+ validate_explore_view_freshness,
43
+ )
44
+ from .kanban import (
45
+ DEFAULT_CLI_BIN,
46
+ CommandRunner,
47
+ _command_error,
48
+ _extract_base_token,
49
+ _extract_table_id,
50
+ _extract_created_record_id,
51
+ _public_safe_text,
52
+ _run_command,
53
+ _select_options,
54
+ default_subprocess_runner,
55
+ lark_record_rows,
56
+ now_lark_datetime,
57
+ parse_lark_base_url,
58
+ )
59
+ from .explore_singleflight import (
60
+ default_lark_explore_config_path,
61
+ singleflight_issue_fix_material_sync,
62
+ source_lark_explore_config_path,
63
+ )
64
+ from .explore_stage_document import ensure_stage_whiteboards
65
+ from .explore_visual_integrity import (
66
+ cross_role_stage_token_conflict_delivery,
67
+ finalize_visual_role_results,
68
+ )
69
+ from .explore_visual_readback import (
70
+ defer_visual_stage_after_host_timeout,
71
+ publish_visual_with_readback,
72
+ settle_visual_stage_readbacks,
73
+ )
74
+ from .explore_visual_styles import (
75
+ BOARD_STYLE_AUTO_FLOW,
76
+ board_source_with_delivery_marker,
77
+ explore_board_style,
78
+ resolve_explore_board_style,
79
+ resolve_explore_board_style_update,
80
+ summarize_explore_visual_sync,
81
+ )
82
+ from .message_card import build_lark_markdown_reply_card
83
+
84
+ LARK_EXPLORE_SCHEMA_VERSION = "loopx_lark_explore_result_board_v0"
85
+ LARK_EXPLORE_LOCAL_CONFIG_VERSION = "loopx_lark_explore_local_config_v0"
86
+ LARK_EXPLORE_SYNC_VERSION = "loopx_lark_explore_sync_v0"
87
+ LARK_EXPLORE_READBACK_VERSION = "loopx_lark_explore_readback_v0"
88
+ LARK_EXPLORE_CARD_VERSION = "loopx_lark_explore_card_v0"
89
+ LARK_EXPLORE_VISUAL_SYNC_VERSION = "loopx_lark_explore_visual_sync_v0"
90
+ LARK_EXPLORE_VISUALS_SYNC_VERSION = "loopx_lark_explore_visuals_sync_v0"
91
+
92
+ DEFAULT_EXPLORE_BASE_NAME = "LoopX Exploration Results"
93
+ SINK_VISIBILITY_OWNER_ONLY = "owner-only"
94
+ SINK_VISIBILITY_SHARED = "shared"
95
+ SINK_VISIBILITIES = {SINK_VISIBILITY_OWNER_ONLY, SINK_VISIBILITY_SHARED}
96
+
97
+ TABLE_NODES = "nodes"
98
+ TABLE_EDGES = "edges"
99
+ TABLE_FINDINGS = "findings"
100
+ EXPLORE_TABLE_KEYS = (TABLE_NODES, TABLE_EDGES, TABLE_FINDINGS)
101
+ EXPLORE_TABLE_NAMES = {
102
+ TABLE_NODES: "Nodes",
103
+ TABLE_EDGES: "Edges",
104
+ TABLE_FINDINGS: "Findings",
105
+ }
106
+
107
+ _GOAL_ID_FIELD = "LoopX Goal ID"
108
+ _RESULT_ID_FIELD = "LoopX Result ID"
109
+
110
+
111
+ def _number_field(name: str, *, precision: int) -> dict[str, Any]:
112
+ return {
113
+ "name": name,
114
+ "type": "number",
115
+ "style": {
116
+ "type": "plain",
117
+ "precision": precision,
118
+ "percentage": False,
119
+ "thousands_separator": False,
120
+ },
121
+ }
122
+
123
+
124
+ def _text_field(name: str) -> dict[str, Any]:
125
+ return {"name": name, "type": "text", "style": {"type": "plain"}}
126
+
127
+
128
+ def _link_field(name: str, *, link_table: str) -> dict[str, Any]:
129
+ return {"name": name, "type": "link", "link_table": link_table}
130
+
131
+
132
+ def _select_field(name: str, options: list[str]) -> dict[str, Any]:
133
+ return {
134
+ "name": name,
135
+ "type": "select",
136
+ "multiple": False,
137
+ "options": _select_options(options),
138
+ }
139
+
140
+
141
+ _LINEAGE_FIELDS = [
142
+ _text_field(_GOAL_ID_FIELD),
143
+ _text_field(_RESULT_ID_FIELD),
144
+ _text_field("Source ID"),
145
+ _text_field("Row Lifecycle"),
146
+ _text_field("Supersedes"),
147
+ _text_field("Superseded By"),
148
+ ]
149
+
150
+
151
+ def lark_explore_field_definitions(table_key: str) -> list[dict[str, Any]]:
152
+ if table_key == TABLE_NODES:
153
+ return [
154
+ _text_field("Title"),
155
+ _select_field("Kind", sorted(NODE_KINDS)),
156
+ _select_field(
157
+ "Status",
158
+ [
159
+ NODE_STATUS_OPEN,
160
+ NODE_STATUS_EXPLORING,
161
+ NODE_STATUS_BLOCKED,
162
+ NODE_STATUS_RESOLVED,
163
+ NODE_STATUS_DEAD_END,
164
+ ],
165
+ ),
166
+ _text_field("Summary"),
167
+ _text_field("Blocked Reason"),
168
+ _text_field("Parent Node"),
169
+ _number_field("Findings", precision=0),
170
+ _text_field("Evidence Refs"),
171
+ _text_field("Tags"),
172
+ _text_field("Agent ID"),
173
+ _text_field("First Recorded At"),
174
+ _text_field("Last Updated At"),
175
+ *_LINEAGE_FIELDS,
176
+ ]
177
+ if table_key == TABLE_EDGES:
178
+ return [
179
+ _text_field("From Node"),
180
+ _text_field("To Node"),
181
+ _link_field("From Node Link", link_table=EXPLORE_TABLE_NAMES[TABLE_NODES]),
182
+ _link_field("To Node Link", link_table=EXPLORE_TABLE_NAMES[TABLE_NODES]),
183
+ _select_field("Type", sorted(EDGE_TYPES)),
184
+ _number_field("Confidence", precision=2),
185
+ _text_field("Condition"),
186
+ _text_field("State Transition"),
187
+ _text_field("Summary"),
188
+ _text_field("Last Updated At"),
189
+ _text_field(_GOAL_ID_FIELD),
190
+ _text_field(_RESULT_ID_FIELD),
191
+ _text_field("Source ID"),
192
+ ]
193
+ if table_key == TABLE_FINDINGS:
194
+ return [
195
+ _text_field("Finding"),
196
+ _text_field("Summary"),
197
+ _select_field(
198
+ "Status",
199
+ [
200
+ FINDING_STATUS_TENTATIVE,
201
+ FINDING_STATUS_CONFIRMED,
202
+ FINDING_STATUS_REFUTED,
203
+ ],
204
+ ),
205
+ _number_field("Confidence", precision=2),
206
+ _text_field("Node"),
207
+ _text_field("Evidence Refs"),
208
+ _text_field("Tags"),
209
+ _text_field("Agent ID"),
210
+ _text_field("First Recorded At"),
211
+ _text_field("Last Updated At"),
212
+ *_LINEAGE_FIELDS,
213
+ ]
214
+ raise ValueError(f"unknown explore table key: {table_key}")
215
+
216
+
217
+ def lark_explore_schema_payload() -> dict[str, Any]:
218
+ return {
219
+ "ok": True,
220
+ "schema_version": LARK_EXPLORE_SCHEMA_VERSION,
221
+ "source_of_truth": "loopx_explore_result_log_projected_to_lark_base",
222
+ "adapter_role": "read_only_result_dashboard",
223
+ "projection_schema_version": EXPLORE_RESULT_PROJECTION_VERSION,
224
+ "loopx_mapping": {
225
+ "node": "Nodes row keyed by LoopX Result ID; Status=blocked rows answer where the loop is stuck",
226
+ "edge": "Edges row keyed by LoopX Result ID; typed relation between two nodes",
227
+ "finding": "Findings row keyed by LoopX Result ID; latest finding event wins",
228
+ "topology": "Mermaid flowchart source in the projection, for Feishu docs or any renderer",
229
+ "lineage": "Row Lifecycle, Supersedes, Superseded By, Source ID columns",
230
+ "card": "compact interactive card built from the same projection",
231
+ },
232
+ "tables": {
233
+ key: {
234
+ "name": EXPLORE_TABLE_NAMES[key],
235
+ "fields": lark_explore_field_definitions(key),
236
+ }
237
+ for key in EXPLORE_TABLE_KEYS
238
+ },
239
+ "write_boundary": (
240
+ "Rows are a projection of the local explore result log. The board "
241
+ "never receives worker commands, local paths, credentials, or raw "
242
+ "transcripts; card send/update happens through an approved gateway."
243
+ ),
244
+ }
245
+
246
+
247
+ @dataclass(frozen=True)
248
+ class LarkExploreConfig:
249
+ base_token: str
250
+ table_ids: dict[str, str] = field(default_factory=dict)
251
+ cli_bin: str = DEFAULT_CLI_BIN
252
+ identity: str = "user"
253
+
254
+ def table_id(self, table_key: str) -> str:
255
+ table_id = str(self.table_ids.get(table_key) or "").strip()
256
+ if not table_id:
257
+ raise ValueError(f"missing table id for {table_key}; run `loopx explore feishu-setup` first")
258
+ return table_id
259
+
260
+
261
+ def read_lark_explore_local_config(path: Path) -> dict[str, Any]:
262
+ config_path = path.expanduser()
263
+ if not config_path.exists():
264
+ return {
265
+ "ok": True,
266
+ "exists": False,
267
+ "schema_version": LARK_EXPLORE_LOCAL_CONFIG_VERSION,
268
+ "path": str(config_path),
269
+ "board": None,
270
+ }
271
+ try:
272
+ payload = json.loads(config_path.read_text(encoding="utf-8-sig"))
273
+ except json.JSONDecodeError as exc:
274
+ return {
275
+ "ok": False,
276
+ "exists": True,
277
+ "schema_version": LARK_EXPLORE_LOCAL_CONFIG_VERSION,
278
+ "path": str(config_path),
279
+ "error": f"invalid JSON: {exc}",
280
+ "board": None,
281
+ }
282
+ if not isinstance(payload, dict):
283
+ return {
284
+ "ok": False,
285
+ "exists": True,
286
+ "schema_version": LARK_EXPLORE_LOCAL_CONFIG_VERSION,
287
+ "path": str(config_path),
288
+ "error": "config root must be a JSON object",
289
+ "board": None,
290
+ }
291
+ payload.setdefault("schema_version", LARK_EXPLORE_LOCAL_CONFIG_VERSION)
292
+ payload["ok"] = True
293
+ payload["exists"] = True
294
+ payload["path"] = str(config_path)
295
+ return payload
296
+
297
+
298
+ def write_lark_explore_local_config(path: Path, payload: dict[str, Any]) -> None:
299
+ config_path = path.expanduser()
300
+ config_path.parent.mkdir(parents=True, exist_ok=True)
301
+ to_write = dict(payload)
302
+ to_write.pop("ok", None)
303
+ to_write.pop("exists", None)
304
+ to_write.pop("path", None)
305
+ to_write["schema_version"] = LARK_EXPLORE_LOCAL_CONFIG_VERSION
306
+ to_write["updated_at"] = now_lark_datetime()
307
+ config_path.write_text(json.dumps(to_write, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
308
+
309
+
310
+ def lark_explore_config_from_payload(
311
+ payload: Mapping[str, Any],
312
+ ) -> LarkExploreConfig | None:
313
+ board = payload.get("board")
314
+ if not isinstance(board, dict):
315
+ return None
316
+ base_token = str(board.get("base_token") or "").strip()
317
+ tables = board.get("tables") if isinstance(board.get("tables"), dict) else {}
318
+ table_ids = {str(key): str(value).strip() for key, value in tables.items() if str(value or "").strip()}
319
+ if not base_token or not table_ids:
320
+ return None
321
+ return LarkExploreConfig(
322
+ **{"base_" + "token": base_token},
323
+ table_ids=table_ids,
324
+ cli_bin=str(board.get("cli_bin") or DEFAULT_CLI_BIN),
325
+ identity=str(board.get("identity") or "user"),
326
+ )
327
+
328
+
329
+ def _record_json_args(values: Mapping[str, Any]) -> str:
330
+ return json.dumps(dict(values), ensure_ascii=False, separators=(",", ":"))
331
+
332
+
333
+ def _build_upsert_command(
334
+ config: LarkExploreConfig,
335
+ *,
336
+ table_id: str,
337
+ record_id: str | None,
338
+ values: Mapping[str, Any],
339
+ ) -> list[str]:
340
+ args = [
341
+ config.cli_bin,
342
+ "base",
343
+ "+record-upsert",
344
+ "--as",
345
+ config.identity,
346
+ "--base-token",
347
+ config.base_token,
348
+ "--table-id",
349
+ table_id,
350
+ ]
351
+ if record_id:
352
+ args.extend(["--record-id", record_id])
353
+ args.extend(["--json", _record_json_args(values)])
354
+ return args
355
+
356
+
357
+ def _build_record_list_command(
358
+ config: LarkExploreConfig,
359
+ *,
360
+ table_id: str,
361
+ goal_id: str,
362
+ offset: int = 0,
363
+ ) -> list[str]:
364
+ return [
365
+ config.cli_bin,
366
+ "base",
367
+ "+record-list",
368
+ "--as",
369
+ config.identity,
370
+ "--base-token",
371
+ config.base_token,
372
+ "--table-id",
373
+ table_id,
374
+ "--filter-json",
375
+ _record_json_args(
376
+ {
377
+ "logic": "and",
378
+ "conditions": [[_GOAL_ID_FIELD, "==", goal_id]],
379
+ }
380
+ ),
381
+ "--format",
382
+ "json",
383
+ "--offset",
384
+ str(offset),
385
+ "--limit",
386
+ "200",
387
+ ]
388
+
389
+
390
+ def _record_list_has_more(payload: Mapping[str, Any]) -> bool:
391
+ data = payload.get("data") if isinstance(payload.get("data"), Mapping) else payload
392
+ return bool(data.get("has_more")) if isinstance(data, Mapping) else False
393
+
394
+
395
+ def _record_scan_goal_ids(goal_id: str, *, public_safe: bool) -> tuple[str, ...]:
396
+ desired_goal_id = _public_safe_text(goal_id) if public_safe else goal_id
397
+ if public_safe:
398
+ return (desired_goal_id,)
399
+ legacy_shared_goal_id = _public_safe_text(goal_id)
400
+ if legacy_shared_goal_id == desired_goal_id:
401
+ return (desired_goal_id,)
402
+ return desired_goal_id, legacy_shared_goal_id
403
+
404
+
405
+ def _normalize_lark_value(value: Any) -> Any:
406
+ if value is None or value == "":
407
+ return ""
408
+ if isinstance(value, Mapping):
409
+ return {
410
+ str(key): _normalize_lark_value(item) for key, item in sorted(value.items(), key=lambda pair: str(pair[0]))
411
+ }
412
+ if isinstance(value, list):
413
+ normalized = [_normalize_lark_value(item) for item in value]
414
+ if len(normalized) == 1 and isinstance(normalized[0], str):
415
+ return normalized[0]
416
+ return normalized
417
+ return value
418
+
419
+
420
+ def _lark_values_match(values: Mapping[str, Any], record: Mapping[str, Any]) -> bool:
421
+ return all(
422
+ _normalize_lark_value(record.get(field_name)) == _normalize_lark_value(expected)
423
+ for field_name, expected in values.items()
424
+ )
425
+
426
+
427
+ def _skipped_sync_command() -> dict[str, Any]:
428
+ return {
429
+ "command": "",
430
+ "executed": False,
431
+ "ok": True,
432
+ "returncode": None,
433
+ "stdout": "",
434
+ "stderr": "",
435
+ "json": None,
436
+ "skipped": True,
437
+ "reason": "unchanged",
438
+ }
439
+
440
+
441
+ def _persist_lark_explore_record_map(
442
+ config: LarkExploreConfig,
443
+ *,
444
+ config_path: Path | None,
445
+ local: Mapping[str, Any],
446
+ record_map: Mapping[str, str],
447
+ ) -> None:
448
+ if not config_path:
449
+ return
450
+ existing_records = dict(local.get("result_records") or {}) if isinstance(local.get("result_records"), dict) else {}
451
+ if existing_records == dict(record_map) and bool(local.get("exists")):
452
+ return
453
+ board = local.get("board") if isinstance(local.get("board"), dict) else {}
454
+ if not board:
455
+ board = {
456
+ "base_token": config.base_token,
457
+ "tables": dict(config.table_ids),
458
+ "cli_bin": config.cli_bin,
459
+ "identity": config.identity,
460
+ }
461
+ updated = {key: value for key, value in local.items() if key not in {"ok", "exists", "path", "updated_at"}}
462
+ updated.update(
463
+ {
464
+ "schema_version": LARK_EXPLORE_LOCAL_CONFIG_VERSION,
465
+ "board": board,
466
+ "result_records": dict(record_map),
467
+ "card": local.get("card") if isinstance(local.get("card"), dict) else {},
468
+ }
469
+ )
470
+ write_lark_explore_local_config(config_path, updated)
471
+
472
+
473
+ AUTO_DOC_TITLE = "LoopX Explore 实时探索拓扑"
474
+ AUTO_DOC_CREATE_XML = (
475
+ "<title>" + AUTO_DOC_TITLE + "</title>"
476
+ "<h1>实时探索拓扑</h1>"
477
+ "<p>Executive 总览画板;由 feishu-sync 自动更新。</p>"
478
+ '<whiteboard type="blank"></whiteboard>'
479
+ "<h2>LoopX Explore · Canonical 全局画板</h2>"
480
+ "<p>Canonical 全局画板;由 feishu-sync 自动更新。</p>"
481
+ '<whiteboard type="blank"></whiteboard>'
482
+ )
483
+ AUTO_DOC_APPEND_XML = (
484
+ "<h1>实时探索拓扑</h1>"
485
+ "<p>Executive 总览画板;由 feishu-sync 自动更新。</p>"
486
+ '<whiteboard type="blank"></whiteboard>'
487
+ "<h2>LoopX Explore · Canonical 全局画板</h2>"
488
+ "<p>Canonical 全局画板;由 feishu-sync 自动更新。</p>"
489
+ '<whiteboard type="blank"></whiteboard>'
490
+ )
491
+
492
+
493
+ def _created_overview_boards(command: Mapping[str, Any]) -> list[dict[str, Any]]:
494
+ """Return newly created whiteboard blocks in document order."""
495
+
496
+ payload = command.get("json")
497
+ data = payload.get("data") if isinstance(payload, Mapping) else None
498
+ document = data.get("document") if isinstance(data, Mapping) else None
499
+ blocks = document.get("new_blocks") if isinstance(document, Mapping) else None
500
+ boards = []
501
+ for block in blocks or []:
502
+ if not isinstance(block, Mapping):
503
+ continue
504
+ block_type = str(block.get("block_type") or "")
505
+ block_token = str(block.get("block_token") or "").strip()
506
+ if block_type == "whiteboard" and block_token:
507
+ boards.append(
508
+ {
509
+ "block_id": str(block.get("block_id") or "").strip() or None,
510
+ "block_token": block_token,
511
+ }
512
+ )
513
+ return boards
514
+
515
+
516
+ def configure_lark_explore_visual_sink(
517
+ *,
518
+ config_path: Path,
519
+ whiteboard_token: str | None = None,
520
+ docx_token: str | None = None,
521
+ statuses: list[str] | None = None,
522
+ tags: list[str] | None = None,
523
+ projection_mode: str = "canonical_filtered",
524
+ include_ancestors: bool = True,
525
+ mermaid_node_limit: int = 100,
526
+ stage_capacity: int = 14,
527
+ stage_whiteboard_tokens: list[str] | None = None,
528
+ board_style: str | None = None,
529
+ view_role: str | None = None,
530
+ auto_create: bool = False,
531
+ execute: bool = False,
532
+ ) -> dict[str, Any]:
533
+ """Configure an optional owner-facing whiteboard over canonical Explore data.
534
+
535
+ ``auto_create=True`` bootstraps the hosting document and the two overview
536
+ boards (executive topology + canonical global) without manual token
537
+ plumbing. Canonical Evidence Stage boards are created lazily by
538
+ ``feishu-sync`` unless explicit stage tokens are supplied.
539
+ """
540
+
541
+ token = str(whiteboard_token or "").strip()
542
+ document_token = str(docx_token or "").strip()
543
+ if not token and not document_token and not auto_create:
544
+ raise ValueError("whiteboard_token or docx_token is required")
545
+ valid_modes = {
546
+ "canonical_filtered",
547
+ "issue_fix_two_lane",
548
+ "canonical_full",
549
+ "executive_auto",
550
+ }
551
+ if projection_mode not in valid_modes:
552
+ raise ValueError(f"projection_mode must be one of {sorted(valid_modes)}")
553
+ role = str(view_role or "").strip() or None
554
+ if role not in {None, "canonical", "executive"}:
555
+ raise ValueError("view_role must be canonical or executive")
556
+ expected_mode = {"canonical": "canonical_full", "executive": "executive_auto"}.get(role)
557
+ if expected_mode and projection_mode != expected_mode:
558
+ raise ValueError(f"view_role {role} requires projection_mode {expected_mode}")
559
+ if not 10 <= int(stage_capacity) <= 20:
560
+ raise ValueError("stage_capacity must be between 10 and 20")
561
+ local = read_lark_explore_local_config(config_path)
562
+ if not local.get("ok") or not local.get("exists"):
563
+ raise ValueError("run `loopx explore feishu-setup` before configuring a visual sink")
564
+ visual_sinks = local.get("visual_sinks")
565
+ existing_sink = (
566
+ visual_sinks.get(role)
567
+ if role and isinstance(visual_sinks, Mapping)
568
+ else local.get("visual_sink") if not role else None
569
+ )
570
+ style = resolve_explore_board_style_update(
571
+ board_style,
572
+ existing_sink if isinstance(existing_sink, Mapping) else {},
573
+ )
574
+ tokens = [
575
+ str(item).strip()
576
+ for item in stage_whiteboard_tokens or []
577
+ if str(item).strip()
578
+ ]
579
+ if not tokens and token:
580
+ tokens = [token]
581
+ elif token and token not in tokens:
582
+ tokens.insert(0, token)
583
+
584
+ def _auto_failure(error: str) -> dict[str, Any]:
585
+ return {
586
+ "ok": False,
587
+ "schema_version": "loopx_lark_explore_visual_sink_configure_v0",
588
+ "execute": execute,
589
+ "status": "auto_create_failed",
590
+ "config_path": str(config_path),
591
+ "view_role": role,
592
+ "error": error,
593
+ }
594
+
595
+ overview_boards: list[dict[str, Any]] = []
596
+ auto_created_document: dict[str, Any] | None = None
597
+ if auto_create:
598
+ if execute:
599
+ board_meta = local.get("board") if isinstance(local.get("board"), dict) else {}
600
+ cli_bin = str(board_meta.get("cli_bin") or DEFAULT_CLI_BIN)
601
+ identity = str(board_meta.get("identity") or "user")
602
+ stored_sinks = (
603
+ local.get("visual_sinks")
604
+ if isinstance(local.get("visual_sinks"), Mapping)
605
+ else {}
606
+ )
607
+ stored_canonical = (
608
+ stored_sinks.get("canonical")
609
+ if isinstance(stored_sinks.get("canonical"), Mapping)
610
+ else {}
611
+ )
612
+ stored_executive = (
613
+ stored_sinks.get("executive")
614
+ if isinstance(stored_sinks.get("executive"), Mapping)
615
+ else {}
616
+ )
617
+ if not document_token:
618
+ document_token = str(
619
+ stored_canonical.get("docx_token")
620
+ or stored_executive.get("docx_token")
621
+ or ""
622
+ ).strip()
623
+ reused_overview = (
624
+ document_token
625
+ and str(stored_canonical.get("overview_whiteboard_token") or "").strip()
626
+ and str(stored_executive.get("overview_whiteboard_token") or "").strip()
627
+ )
628
+ if reused_overview:
629
+ overview_boards = [
630
+ {"block_token": str(stored_executive["overview_whiteboard_token"])},
631
+ {"block_token": str(stored_canonical["overview_whiteboard_token"])},
632
+ ]
633
+ elif not document_token:
634
+ create_command = _run_command(
635
+ [
636
+ cli_bin,
637
+ "docs",
638
+ "+create",
639
+ "--as",
640
+ identity,
641
+ "--content",
642
+ AUTO_DOC_CREATE_XML,
643
+ "--format",
644
+ "json",
645
+ ],
646
+ execute=True,
647
+ runner=default_subprocess_runner,
648
+ )
649
+ if not create_command.get("ok"):
650
+ return _auto_failure(_command_error(create_command))
651
+ document_token = str(
652
+ (
653
+ (create_command.get("json") or {})
654
+ .get("data", {})
655
+ .get("document", {})
656
+ .get("document_id")
657
+ or ""
658
+ )
659
+ ).strip()
660
+ if not document_token:
661
+ return _auto_failure(
662
+ "created document did not return a document_id"
663
+ )
664
+ overview_boards = _created_overview_boards(create_command)
665
+ else:
666
+ append_command = _run_command(
667
+ [
668
+ cli_bin,
669
+ "docs",
670
+ "+update",
671
+ "--as",
672
+ identity,
673
+ "--doc",
674
+ document_token,
675
+ "--command",
676
+ "append",
677
+ "--content",
678
+ AUTO_DOC_APPEND_XML,
679
+ "--format",
680
+ "json",
681
+ ],
682
+ execute=True,
683
+ runner=default_subprocess_runner,
684
+ )
685
+ if not append_command.get("ok"):
686
+ return _auto_failure(_command_error(append_command))
687
+ overview_boards = _created_overview_boards(append_command)
688
+ if len(overview_boards) < 2:
689
+ return _auto_failure(
690
+ "auto-created document did not return both overview whiteboards"
691
+ )
692
+ auto_created_document = {
693
+ "docx_token": document_token,
694
+ "overview_boards": overview_boards,
695
+ }
696
+ else:
697
+ overview_boards = [
698
+ {"block_token": "planned-executive-overview"},
699
+ {"block_token": "planned-canonical-overview"},
700
+ ]
701
+
702
+ def _role_sink(
703
+ role_name: str,
704
+ overview_token: str | None,
705
+ *,
706
+ stage_enabled: bool,
707
+ ) -> dict[str, Any]:
708
+ mode = {
709
+ "canonical": "canonical_full",
710
+ "executive": "executive_auto",
711
+ }.get(role_name, projection_mode)
712
+ sink = {
713
+ "schema_version": "loopx_lark_explore_visual_sink_config_v0",
714
+ "whiteboard_token": token or None,
715
+ "docx_token": document_token or None,
716
+ "statuses": [str(item) for item in statuses or [] if str(item).strip()],
717
+ "tags": [str(item) for item in tags or [] if str(item).strip()],
718
+ "projection_mode": mode,
719
+ "include_ancestors": bool(include_ancestors),
720
+ "mermaid_node_limit": max(1, int(mermaid_node_limit)),
721
+ "stage_capacity": int(stage_capacity),
722
+ "board_style": style.name,
723
+ "renderer": style.renderer,
724
+ "presentation_mode": "stage_document",
725
+ "stage_boards_enabled": stage_enabled,
726
+ "stage_whiteboards": [
727
+ {"stage_index": index, "whiteboard_token": stage_token}
728
+ for index, stage_token in enumerate(tokens, start=1)
729
+ ],
730
+ "view_role": role_name,
731
+ }
732
+ if overview_token:
733
+ sink["overview_whiteboard_token"] = overview_token
734
+ return sink
735
+
736
+ if auto_create:
737
+ if board_style is None:
738
+ style = explore_board_style(BOARD_STYLE_AUTO_FLOW)
739
+ executive_board = overview_boards[0] if overview_boards else {}
740
+ canonical_board = overview_boards[1] if len(overview_boards) > 1 else {}
741
+ explicit_stage_tokens = bool(tokens)
742
+ planned_sinks: dict[str, dict[str, Any]] = {}
743
+ if role in {None, "canonical"}:
744
+ planned_sinks["canonical"] = _role_sink(
745
+ "canonical",
746
+ str(canonical_board.get("block_token") or "").strip() or None,
747
+ stage_enabled=True,
748
+ )
749
+ if not explicit_stage_tokens:
750
+ planned_sinks["canonical"]["stage_whiteboards"] = []
751
+ if role in {None, "executive"}:
752
+ planned_sinks["executive"] = _role_sink(
753
+ "executive",
754
+ str(executive_board.get("block_token") or "").strip() or None,
755
+ stage_enabled=explicit_stage_tokens and role == "executive",
756
+ )
757
+ if not explicit_stage_tokens or role != "executive":
758
+ planned_sinks["executive"]["stage_boards_enabled"] = False
759
+ planned_sinks["executive"]["stage_whiteboards"] = []
760
+ if execute:
761
+ updated = {
762
+ key: value
763
+ for key, value in local.items()
764
+ if key not in {"ok", "exists", "path", "updated_at"}
765
+ }
766
+ sinks = dict(updated.get("visual_sinks") or {})
767
+ sinks.update(planned_sinks)
768
+ updated["visual_sinks"] = sinks
769
+ write_lark_explore_local_config(config_path, updated)
770
+ return {
771
+ "ok": True,
772
+ "schema_version": "loopx_lark_explore_visual_sink_configure_v0",
773
+ "execute": execute,
774
+ "status": "auto_configured" if execute else "would_auto_create",
775
+ "config_path": str(config_path),
776
+ "view_role": role,
777
+ "auto_create": True,
778
+ "document": auto_created_document
779
+ or {
780
+ "docx_token": document_token or "planned-docx",
781
+ "overview_boards": overview_boards,
782
+ },
783
+ "visual_sinks": planned_sinks,
784
+ }
785
+
786
+ visual_sink = {
787
+ "schema_version": "loopx_lark_explore_visual_sink_config_v0",
788
+ "whiteboard_token": token or None,
789
+ "docx_token": document_token or None,
790
+ "statuses": [str(item) for item in statuses or [] if str(item).strip()],
791
+ "tags": [str(item) for item in tags or [] if str(item).strip()],
792
+ "projection_mode": projection_mode,
793
+ "include_ancestors": bool(include_ancestors),
794
+ "mermaid_node_limit": max(1, int(mermaid_node_limit)),
795
+ "stage_capacity": int(stage_capacity),
796
+ "board_style": style.name,
797
+ "renderer": style.renderer,
798
+ "presentation_mode": "stage_document",
799
+ "stage_boards_enabled": True,
800
+ "stage_whiteboards": [
801
+ {"stage_index": index, "whiteboard_token": stage_token}
802
+ for index, stage_token in enumerate(tokens, start=1)
803
+ ],
804
+ }
805
+ if role:
806
+ visual_sink["view_role"] = role
807
+ if execute:
808
+ updated = {key: value for key, value in local.items() if key not in {"ok", "exists", "path", "updated_at"}}
809
+ if role:
810
+ visual_sinks = dict(updated.get("visual_sinks") or {})
811
+ visual_sinks[role] = visual_sink
812
+ updated["visual_sinks"] = visual_sinks
813
+ else:
814
+ updated["visual_sink"] = visual_sink
815
+ write_lark_explore_local_config(config_path, updated)
816
+ return {
817
+ "ok": True,
818
+ "schema_version": "loopx_lark_explore_visual_sink_configure_v0",
819
+ "execute": execute,
820
+ "status": "configured" if execute else "would_configure",
821
+ "config_path": str(config_path),
822
+ "view_role": role,
823
+ "visual_sink": visual_sink,
824
+ }
825
+
826
+
827
+ def sync_explore_visual_to_lark(
828
+ config: LarkExploreConfig,
829
+ *,
830
+ projection: Mapping[str, Any],
831
+ visual_sink: Mapping[str, Any] | None,
832
+ config_path: Path,
833
+ semantic_digest: str,
834
+ display_projection: Mapping[str, Any] | None = None,
835
+ view_key: str | None = None,
836
+ execute: bool = False,
837
+ runner: CommandRunner = default_subprocess_runner,
838
+ defer_readback: bool = False,
839
+ ) -> dict[str, Any]:
840
+ """Publish a configured whiteboard without conflating it with Base rows."""
841
+
842
+ if not isinstance(visual_sink, Mapping):
843
+ return {
844
+ "ok": True,
845
+ "schema_version": LARK_EXPLORE_VISUAL_SYNC_VERSION,
846
+ "status": "not_configured",
847
+ "execute": execute,
848
+ "published": False,
849
+ }
850
+ whiteboard_token = str(visual_sink.get("whiteboard_token") or "").strip()
851
+ if not whiteboard_token:
852
+ return {
853
+ "ok": False,
854
+ "schema_version": LARK_EXPLORE_VISUAL_SYNC_VERSION,
855
+ "status": "invalid_config",
856
+ "execute": execute,
857
+ "published": False,
858
+ "error": "visual_sink.whiteboard_token is required",
859
+ }
860
+ source_digest = explore_source_digest(projection)
861
+ source_revision = explore_source_revision(projection, digest=source_digest)
862
+ if isinstance(display_projection, Mapping) and (
863
+ display_projection.get("source_digest") or display_projection.get("source_revision")
864
+ ):
865
+ freshness = validate_explore_view_freshness(projection, display_projection)
866
+ if not freshness["fresh"]:
867
+ return {
868
+ "ok": False,
869
+ "schema_version": LARK_EXPLORE_VISUAL_SYNC_VERSION,
870
+ "status": "stale_projection",
871
+ "execute": execute,
872
+ "published": False,
873
+ "source_digest": source_digest,
874
+ "source_revision": source_revision,
875
+ "freshness": freshness,
876
+ "error": freshness["reason"],
877
+ }
878
+ graph = (
879
+ dict(display_projection)
880
+ if isinstance(display_projection, Mapping)
881
+ else build_explore_graph_view(
882
+ projection.get("nodes") or [],
883
+ projection.get("edges") or [],
884
+ statuses=visual_sink.get("statuses") or [],
885
+ tags=visual_sink.get("tags") or [],
886
+ include_ancestors=bool(visual_sink.get("include_ancestors", True)),
887
+ node_limit=max(1, int(visual_sink.get("mermaid_node_limit") or 100)),
888
+ )
889
+ )
890
+ sink_key = str(view_key or visual_sink.get("view_role") or "visual").strip() or "visual"
891
+ try:
892
+ style = resolve_explore_board_style(visual_sink)
893
+ except ValueError as exc:
894
+ return {
895
+ "ok": False,
896
+ "schema_version": LARK_EXPLORE_VISUAL_SYNC_VERSION,
897
+ "status": "invalid_config",
898
+ "execute": execute,
899
+ "published": False,
900
+ "error": str(exc),
901
+ }
902
+ rendered_source = str(graph.get(style.source_key) or "")
903
+ style_fallback = False
904
+ if not rendered_source.strip() and style.source_key == "svg":
905
+ mermaid_source = str(graph.get("mermaid") or "")
906
+ if mermaid_source.strip():
907
+ style = explore_board_style(BOARD_STYLE_AUTO_FLOW)
908
+ rendered_source = mermaid_source
909
+ style_fallback = True
910
+ if not rendered_source.strip():
911
+ return {
912
+ "ok": False,
913
+ "schema_version": LARK_EXPLORE_VISUAL_SYNC_VERSION,
914
+ "status": "invalid_projection",
915
+ "execute": execute,
916
+ "published": False,
917
+ "view_role": str(graph.get("view_role") or sink_key),
918
+ "renderer": style.renderer,
919
+ "error": f"display projection does not contain {style.source_key} source",
920
+ }
921
+ delivery_material = json.dumps(
922
+ {
923
+ "source_digest": semantic_digest,
924
+ "source_revision": source_revision,
925
+ "view_role": sink_key,
926
+ "whiteboard_token": whiteboard_token,
927
+ "board_style": style.name,
928
+ "renderer": style.renderer,
929
+ "rendered_source": rendered_source,
930
+ },
931
+ ensure_ascii=True,
932
+ sort_keys=True,
933
+ separators=(",", ":"),
934
+ ).encode("utf-8")
935
+ delivery_digest = hashlib.sha256(delivery_material).hexdigest()
936
+ delivery_marker = f"LoopX delivery {delivery_digest[:20]}"
937
+ published_source = board_source_with_delivery_marker(
938
+ rendered_source,
939
+ delivery_marker,
940
+ style=style,
941
+ )
942
+ source_name = f".loopx-explore-{sink_key}-{delivery_digest[:12]}.{style.extension}"
943
+ command = [
944
+ config.cli_bin,
945
+ "whiteboard",
946
+ "+update",
947
+ "--as",
948
+ config.identity,
949
+ "--whiteboard-token",
950
+ whiteboard_token,
951
+ "--input_format",
952
+ style.input_format,
953
+ "--source",
954
+ f"@{source_name}",
955
+ "--overwrite",
956
+ "--idempotent-token",
957
+ f"loopx-explore-{sink_key}-{delivery_digest[:20]}",
958
+ "--format",
959
+ "json",
960
+ ]
961
+ delivery = publish_visual_with_readback(
962
+ cli_bin=config.cli_bin,
963
+ identity=config.identity,
964
+ whiteboard_token=whiteboard_token,
965
+ marker=delivery_marker,
966
+ command=command,
967
+ published_source=published_source,
968
+ source_path=config_path.parent / source_name,
969
+ execute=execute,
970
+ runner=runner,
971
+ defer_readback=defer_readback,
972
+ )
973
+ result = delivery["command"]
974
+ readback = delivery["readback"]
975
+ delivery_ok = bool(delivery["ok"])
976
+ retryable = bool(delivery["retryable"])
977
+ publish_attempts = delivery["publish_attempts"]
978
+ return {
979
+ "ok": delivery_ok,
980
+ "schema_version": LARK_EXPLORE_VISUAL_SYNC_VERSION,
981
+ "status": (
982
+ "published"
983
+ if execute and delivery_ok
984
+ else "would_publish"
985
+ if not execute
986
+ else "publish_unverified"
987
+ if result.get("ok") and delivery_marker
988
+ else "publish_failed"
989
+ ),
990
+ "execute": execute,
991
+ "published": bool(execute and delivery_ok),
992
+ "external_write_performed": delivery["external_write_performed"],
993
+ "reconciled_existing_delivery": delivery["reconciled_existing_delivery"],
994
+ "prepublish_readback": delivery["prepublish_readback"],
995
+ "semantic_digest": semantic_digest,
996
+ "delivery_digest": delivery_digest,
997
+ "source_digest": source_digest,
998
+ "source_revision": source_revision,
999
+ "view_role": str(graph.get("view_role") or sink_key),
1000
+ "board_style": style.name,
1001
+ "style_fallback": style_fallback,
1002
+ "renderer": style.renderer,
1003
+ "input_format": style.input_format,
1004
+ "docx_token": str(visual_sink.get("docx_token") or "") or None,
1005
+ "graph_counts": graph.get("graph_counts"),
1006
+ "filter": graph.get("filter"),
1007
+ "command": result,
1008
+ "publish_attempt_count": len(publish_attempts),
1009
+ "publish_attempts": publish_attempts,
1010
+ "readback": readback,
1011
+ "retryable": retryable,
1012
+ "required_action": (
1013
+ "retry Explore visual sync; post-publish marker readback did not settle"
1014
+ if retryable
1015
+ else None
1016
+ ),
1017
+ "error": (
1018
+ None
1019
+ if delivery_ok
1020
+ else str(readback.get("error") or "")
1021
+ if result.get("ok")
1022
+ else _command_error(result)
1023
+ ),
1024
+ }
1025
+
1026
+
1027
+ def explore_visual_semantic_digest(projection: Mapping[str, Any]) -> str:
1028
+ """Return a timestamp-free digest for an explicit visual projection sync."""
1029
+
1030
+ return explore_source_digest(projection)
1031
+
1032
+
1033
+ def sync_explore_visuals_to_lark(
1034
+ config: LarkExploreConfig,
1035
+ *,
1036
+ projection: Mapping[str, Any],
1037
+ visual_sinks: Mapping[str, Any],
1038
+ config_path: Path,
1039
+ execute: bool = False,
1040
+ runner: CommandRunner = default_subprocess_runner,
1041
+ ) -> dict[str, Any]:
1042
+ """Generate and publish configured same-source Explore views."""
1043
+
1044
+ bundle = build_explore_presentation_bundle(projection)
1045
+ requested_roles = [
1046
+ role
1047
+ for role in ("canonical", "executive")
1048
+ if isinstance(visual_sinks.get(role), Mapping)
1049
+ ]
1050
+ active_roles = ["canonical"]
1051
+ if bundle["presentation_mode"] == PRESENTATION_MODE_DUAL_VIEW:
1052
+ active_roles.append("executive")
1053
+ conflict = cross_role_stage_token_conflict_delivery(
1054
+ bundle=bundle, visual_sinks=visual_sinks, requested_roles=requested_roles,
1055
+ active_roles=active_roles, execute=execute,
1056
+ schema_version=LARK_EXPLORE_VISUALS_SYNC_VERSION,
1057
+ )
1058
+ if conflict:
1059
+ return conflict
1060
+ results: dict[str, Any] = {}
1061
+ stage_readback_targets: list[tuple[dict[str, Any], str]] = []
1062
+ readback_host_wait_exhausted = False
1063
+ for role in requested_roles:
1064
+ if role not in active_roles:
1065
+ results[role] = {
1066
+ "ok": True,
1067
+ "status": "not_recommended",
1068
+ "execute": execute,
1069
+ "published": False,
1070
+ "source_digest": bundle["source_digest"],
1071
+ "source_revision": bundle["source_revision"],
1072
+ "view_role": role,
1073
+ }
1074
+ continue
1075
+ role_sink = visual_sinks[role]
1076
+ stage_capacity = int(role_sink.get("stage_capacity") or 14)
1077
+ role_bundle = build_explore_presentation_bundle(
1078
+ projection,
1079
+ policy={
1080
+ "stage_node_capacity": stage_capacity,
1081
+ },
1082
+ )
1083
+ role_view = role_bundle[role]
1084
+ stage_views = [
1085
+ item
1086
+ for item in role_view.get("stage_views") or []
1087
+ if isinstance(item, Mapping)
1088
+ ]
1089
+ stage_boards_enabled = bool(role_sink.get("stage_boards_enabled", True))
1090
+ configured_stages: dict[int, dict[str, Any]] = {}
1091
+ section_commands: list[dict[str, Any]] = []
1092
+ reconciliation: dict[str, Any] = {}
1093
+ if stage_boards_enabled:
1094
+ configured_stages, section_commands, _, reconciliation, stage_config_error = ensure_stage_whiteboards(
1095
+ config,
1096
+ role=role,
1097
+ role_sink=role_sink,
1098
+ stage_views=stage_views,
1099
+ config_path=config_path,
1100
+ execute=execute,
1101
+ runner=runner,
1102
+ read_local_config=read_lark_explore_local_config,
1103
+ write_local_config=write_lark_explore_local_config,
1104
+ )
1105
+ missing_stage_indexes = [
1106
+ int(stage["stage_index"])
1107
+ for stage in stage_views
1108
+ if int(stage["stage_index"]) not in configured_stages
1109
+ ]
1110
+ if stage_config_error or missing_stage_indexes:
1111
+ results[role] = {
1112
+ "ok": False,
1113
+ "status": "stage_whiteboards_missing",
1114
+ "execute": execute,
1115
+ "published": False,
1116
+ "view_role": role,
1117
+ "source_digest": role_bundle["source_digest"],
1118
+ "source_revision": role_bundle["source_revision"],
1119
+ "required_stage_count": len(stage_views),
1120
+ "configured_stage_count": len(configured_stages),
1121
+ "missing_stage_indexes": missing_stage_indexes,
1122
+ "section_commands": section_commands,
1123
+ "reconciliation": reconciliation,
1124
+ "error": stage_config_error
1125
+ or "configure one document section and whiteboard token for each Evidence Stage",
1126
+ }
1127
+ continue
1128
+ stage_results = []
1129
+ role_nodes = {
1130
+ str(node.get("node_id") or ""): node
1131
+ for node in role_view.get("nodes") or []
1132
+ if isinstance(node, Mapping)
1133
+ }
1134
+ role_edges = [
1135
+ edge for edge in role_view.get("edges") or [] if isinstance(edge, Mapping)
1136
+ ]
1137
+ if stage_boards_enabled:
1138
+ for stage in stage_views:
1139
+ stage_index = int(stage["stage_index"])
1140
+ stage_node_ids = {str(item) for item in stage.get("node_ids") or []}
1141
+ stage_projection = dict(role_view)
1142
+ stage_projection["mermaid"] = str(stage.get("mermaid") or "")
1143
+ stage_projection["svg"] = str(stage.get("svg") or "")
1144
+ stage_projection["nodes"] = [
1145
+ role_nodes[node_id]
1146
+ for node_id in stage.get("node_ids") or []
1147
+ if node_id in role_nodes
1148
+ ]
1149
+ stage_projection["edges"] = [
1150
+ edge
1151
+ for edge in role_edges
1152
+ if str(edge.get("from_node") or "") in stage_node_ids
1153
+ and str(edge.get("to_node") or "") in stage_node_ids
1154
+ ]
1155
+ stage_projection["stage"] = dict(stage)
1156
+ stage_sink = dict(role_sink)
1157
+ stage_sink["whiteboard_token"] = str(
1158
+ configured_stages[stage_index].get("whiteboard_token") or ""
1159
+ ).strip()
1160
+ stage_result = sync_explore_visual_to_lark(
1161
+ config,
1162
+ projection=projection,
1163
+ visual_sink=stage_sink,
1164
+ config_path=config_path,
1165
+ semantic_digest=role_bundle["source_digest"],
1166
+ display_projection=stage_projection,
1167
+ view_key=f"{role}_stage_{stage_index:02d}",
1168
+ execute=bool(execute and not readback_host_wait_exhausted),
1169
+ runner=runner,
1170
+ defer_readback=bool(execute and not readback_host_wait_exhausted),
1171
+ )
1172
+ if execute and readback_host_wait_exhausted:
1173
+ defer_visual_stage_after_host_timeout(stage_result)
1174
+ stage_result = dict(stage_result, stage=dict(stage))
1175
+ stage_results.append(stage_result)
1176
+ prepublish_readback = stage_result.get("prepublish_readback")
1177
+ if isinstance(prepublish_readback, Mapping) and prepublish_readback.get(
1178
+ "host_wait_exhausted"
1179
+ ):
1180
+ readback_host_wait_exhausted = True
1181
+ if (
1182
+ execute
1183
+ and stage_result.get("retryable")
1184
+ and stage_result.get("external_write_performed")
1185
+ and str(stage_sink.get("whiteboard_token") or "").strip()
1186
+ ):
1187
+ stage_readback_targets.append(
1188
+ (stage_result, str(stage_sink["whiteboard_token"]))
1189
+ )
1190
+ role_ok = all(bool(item.get("ok")) for item in stage_results)
1191
+ role_retryable = any(bool(item.get("retryable")) for item in stage_results)
1192
+ overview_result = None
1193
+ overview_token = str(role_sink.get("overview_whiteboard_token") or "").strip()
1194
+ if overview_token:
1195
+ overview_projection = dict(role_view)
1196
+ overview_projection.pop("stage_views", None)
1197
+ overview_sink = dict(role_sink)
1198
+ overview_sink["whiteboard_token"] = overview_token
1199
+ overview_result = sync_explore_visual_to_lark(
1200
+ config,
1201
+ projection=projection,
1202
+ visual_sink=overview_sink,
1203
+ config_path=config_path,
1204
+ semantic_digest=role_bundle["source_digest"],
1205
+ display_projection=overview_projection,
1206
+ view_key=f"{role}_overview",
1207
+ execute=bool(execute and not readback_host_wait_exhausted),
1208
+ runner=runner,
1209
+ defer_readback=bool(execute and not readback_host_wait_exhausted),
1210
+ )
1211
+ if execute and readback_host_wait_exhausted:
1212
+ defer_visual_stage_after_host_timeout(overview_result)
1213
+ role_ok = role_ok and bool(overview_result.get("ok"))
1214
+ role_retryable = role_retryable or bool(overview_result.get("retryable"))
1215
+ overview_readback = overview_result.get("prepublish_readback")
1216
+ if isinstance(overview_readback, Mapping) and overview_readback.get(
1217
+ "host_wait_exhausted"
1218
+ ):
1219
+ readback_host_wait_exhausted = True
1220
+ if (
1221
+ execute
1222
+ and overview_result.get("retryable")
1223
+ and overview_result.get("external_write_performed")
1224
+ ):
1225
+ stage_readback_targets.append((overview_result, overview_token))
1226
+ role_delivery_material = "|".join(
1227
+ str(item.get("delivery_digest") or "")
1228
+ for item in stage_results + ([overview_result] if overview_result else [])
1229
+ ).encode("utf-8")
1230
+ results[role] = {
1231
+ "ok": role_ok,
1232
+ "status": (
1233
+ "published" if execute and role_ok else "would_publish" if role_ok else "publish_failed"
1234
+ ),
1235
+ "execute": execute,
1236
+ "published": bool(execute and role_ok),
1237
+ "retryable": role_retryable,
1238
+ "required_action": (
1239
+ f"retry Explore visual sync for the {role} marker readback"
1240
+ if role_retryable
1241
+ else None
1242
+ ),
1243
+ "view_role": role,
1244
+ "board_style": (
1245
+ str(stage_results[0].get("board_style") or "")
1246
+ if stage_results
1247
+ else ""
1248
+ ),
1249
+ "renderer": (
1250
+ str(stage_results[0].get("renderer") or "")
1251
+ if stage_results
1252
+ else ""
1253
+ ),
1254
+ "presentation_mode": "stage_document",
1255
+ "source_digest": role_bundle["source_digest"],
1256
+ "source_revision": role_bundle["source_revision"],
1257
+ "delivery_digest": hashlib.sha256(role_delivery_material).hexdigest(),
1258
+ "stage_count": len(stage_results),
1259
+ "stage_capacity": stage_capacity,
1260
+ "stages": stage_results,
1261
+ "overview": overview_result,
1262
+ "section_commands": section_commands,
1263
+ "reconciliation": reconciliation,
1264
+ "reconciled_stage_count": sum(
1265
+ bool(item.get("reconciled_existing_delivery"))
1266
+ for item in stage_results
1267
+ ),
1268
+ "updated_stage_count": sum(
1269
+ bool(item.get("external_write_performed")) for item in stage_results
1270
+ ),
1271
+ "readback_host_wait_exhausted": any(
1272
+ bool((item.get("readback") or {}).get("host_wait_exhausted"))
1273
+ for item in stage_results
1274
+ if isinstance(item.get("readback"), Mapping)
1275
+ ),
1276
+ }
1277
+ if stage_readback_targets:
1278
+ settle_visual_stage_readbacks(
1279
+ cli_bin=config.cli_bin, identity=config.identity,
1280
+ stage_targets=stage_readback_targets, runner=runner,
1281
+ )
1282
+ finalize_visual_role_results(results, execute=execute)
1283
+ delivery = summarize_explore_visual_sync(
1284
+ views=results,
1285
+ configured_roles=requested_roles,
1286
+ recommended_roles=active_roles,
1287
+ execute=execute,
1288
+ )
1289
+ return {
1290
+ **delivery,
1291
+ "schema_version": LARK_EXPLORE_VISUALS_SYNC_VERSION,
1292
+ "execute": execute,
1293
+ "presentation_mode": bundle["presentation_mode"],
1294
+ "reason_codes": bundle["reason_codes"],
1295
+ "source_digest": bundle["source_digest"],
1296
+ "source_revision": bundle["source_revision"],
1297
+ "recommended_roles": active_roles,
1298
+ "views": results,
1299
+ "readback_host_wait_exhausted": any(
1300
+ bool(view.get("readback_host_wait_exhausted"))
1301
+ for view in results.values()
1302
+ if isinstance(view, Mapping)
1303
+ ),
1304
+ }
1305
+
1306
+
1307
+ def _visual_delivery_digests(sync_payload: Mapping[str, Any] | None) -> dict[str, str]:
1308
+ """Return the configured role digests that make a visual write idempotent."""
1309
+
1310
+ if not isinstance(sync_payload, Mapping):
1311
+ return {}
1312
+ views = sync_payload.get("views")
1313
+ if not isinstance(views, Mapping):
1314
+ return {}
1315
+ digests = {}
1316
+ for role, view in views.items():
1317
+ if not isinstance(view, Mapping):
1318
+ continue
1319
+ role_digest = str(view.get("delivery_digest") or "").strip()
1320
+ if role_digest:
1321
+ digests[str(role)] = role_digest
1322
+ overview = view.get("overview")
1323
+ if isinstance(overview, Mapping):
1324
+ overview_digest = str(overview.get("delivery_digest") or "").strip()
1325
+ if overview_digest:
1326
+ digests[f"{role}_overview"] = overview_digest
1327
+ for stage in view.get("stages") or []:
1328
+ if not isinstance(stage, Mapping):
1329
+ continue
1330
+ stage_digest = str(stage.get("delivery_digest") or "").strip()
1331
+ stage_index = int(stage.get("stage", {}).get("stage_index") or 0)
1332
+ if stage_digest and stage_index:
1333
+ digests[f"{role}_stage_{stage_index:02d}"] = stage_digest
1334
+ return digests
1335
+
1336
+
1337
+ def setup_lark_explore_board(
1338
+ *,
1339
+ config_path: Path,
1340
+ base_name: str = DEFAULT_EXPLORE_BASE_NAME,
1341
+ base_url: str | None = None,
1342
+ base_token: str | None = None,
1343
+ cli_bin: str = DEFAULT_CLI_BIN,
1344
+ identity: str = "user",
1345
+ execute: bool = False,
1346
+ runner: CommandRunner = default_subprocess_runner,
1347
+ ) -> dict[str, Any]:
1348
+ """Create (or complete) the three-table exploration result board."""
1349
+
1350
+ commands: list[dict[str, Any]] = []
1351
+ warnings: list[str] = []
1352
+ existing = read_lark_explore_local_config(config_path)
1353
+ existing_board = existing.get("board") if isinstance(existing.get("board"), dict) else {}
1354
+ existing_tables = existing_board.get("tables") if isinstance(existing_board.get("tables"), dict) else {}
1355
+ parsed_url = parse_lark_base_url(base_url) if base_url else {}
1356
+ effective_base_token = str(
1357
+ base_token or parsed_url.get("base_token") or existing_board.get("base_token") or ""
1358
+ ).strip()
1359
+ effective_base_url = str(base_url or existing_board.get("base_url") or "").strip()
1360
+ table_ids = {
1361
+ key: str(existing_tables.get(key) or "").strip()
1362
+ for key in EXPLORE_TABLE_KEYS
1363
+ if str(existing_tables.get(key) or "").strip()
1364
+ }
1365
+
1366
+ def failure(error: str | None = None) -> dict[str, Any]:
1367
+ return {
1368
+ "ok": False,
1369
+ "schema_version": LARK_EXPLORE_SCHEMA_VERSION,
1370
+ "execute": execute,
1371
+ "config_path": str(config_path),
1372
+ "base_token": effective_base_token or None,
1373
+ "tables": table_ids,
1374
+ "commands": commands,
1375
+ "warnings": warnings,
1376
+ "error": error
1377
+ or next(
1378
+ (_command_error(item) for item in commands if not item.get("ok")),
1379
+ "unknown",
1380
+ ),
1381
+ }
1382
+
1383
+ if not effective_base_token:
1384
+ create = _run_command(
1385
+ [cli_bin, "base", "+base-create", "--as", identity, "--name", base_name],
1386
+ execute=execute,
1387
+ runner=runner,
1388
+ )
1389
+ commands.append(create)
1390
+ if execute:
1391
+ if not create.get("ok"):
1392
+ return failure()
1393
+ effective_base_token = _extract_base_token(create.get("json")) or ""
1394
+ if not effective_base_token:
1395
+ return failure("base-create did not return a usable Base token")
1396
+ create_data = create.get("json", {}).get("data") if isinstance(create.get("json"), dict) else {}
1397
+ create_base = (
1398
+ create_data.get("base")
1399
+ if isinstance(create_data, dict) and isinstance(create_data.get("base"), dict)
1400
+ else {}
1401
+ )
1402
+ effective_base_url = str(create_base.get("url") or effective_base_url).strip()
1403
+ else:
1404
+ effective_base_token = "<base-token-from-create>"
1405
+
1406
+ for table_key in EXPLORE_TABLE_KEYS:
1407
+ if table_ids.get(table_key):
1408
+ continue
1409
+ table_create = _run_command(
1410
+ [
1411
+ cli_bin,
1412
+ "base",
1413
+ "+table-create",
1414
+ "--as",
1415
+ identity,
1416
+ "--base-token",
1417
+ effective_base_token,
1418
+ "--name",
1419
+ EXPLORE_TABLE_NAMES[table_key],
1420
+ "--fields",
1421
+ json.dumps(lark_explore_field_definitions(table_key), ensure_ascii=False),
1422
+ ],
1423
+ execute=execute,
1424
+ runner=runner,
1425
+ )
1426
+ commands.append(table_create)
1427
+ if execute:
1428
+ if not table_create.get("ok"):
1429
+ return failure()
1430
+ table_id = _extract_table_id(table_create.get("json")) or ""
1431
+ if not table_id:
1432
+ return failure(f"table-create for {EXPLORE_TABLE_NAMES[table_key]} did not return a table id")
1433
+ table_ids[table_key] = table_id
1434
+ else:
1435
+ table_ids[table_key] = f"<table-id-from-table-create:{table_key}>"
1436
+
1437
+ board = {
1438
+ "base_token": effective_base_token,
1439
+ "base_url": effective_base_url,
1440
+ "base_name": base_name or existing_board.get("base_name") or "",
1441
+ "cli_bin": cli_bin,
1442
+ "identity": identity,
1443
+ "tables": table_ids,
1444
+ }
1445
+ if execute:
1446
+ updated = {key: value for key, value in existing.items() if key not in {"ok", "exists", "path", "updated_at"}}
1447
+ updated.update(
1448
+ {
1449
+ "schema_version": LARK_EXPLORE_LOCAL_CONFIG_VERSION,
1450
+ "board": board,
1451
+ "result_records": (
1452
+ existing.get("result_records") if isinstance(existing.get("result_records"), dict) else {}
1453
+ ),
1454
+ "card": existing.get("card") if isinstance(existing.get("card"), dict) else {},
1455
+ }
1456
+ )
1457
+ write_lark_explore_local_config(
1458
+ config_path,
1459
+ updated,
1460
+ )
1461
+
1462
+ return {
1463
+ "ok": True,
1464
+ "schema_version": LARK_EXPLORE_SCHEMA_VERSION,
1465
+ "execute": execute,
1466
+ "config_path": str(config_path),
1467
+ "base_token": effective_base_token,
1468
+ "tables": table_ids,
1469
+ "board": board,
1470
+ "commands": commands,
1471
+ "warnings": warnings,
1472
+ "next_commands": [
1473
+ "loopx explore node --goal-id <goal-id> --title <topic> --status exploring",
1474
+ "loopx explore finding --goal-id <goal-id> --title <finding> --node <node-id>",
1475
+ "loopx explore feishu-sync --goal-id <goal-id> --execute",
1476
+ ],
1477
+ "error": None,
1478
+ }
1479
+
1480
+
1481
+ def _joined(values: Any) -> str:
1482
+ if isinstance(values, list):
1483
+ return ", ".join(str(item) for item in values if str(item or "").strip())
1484
+ return str(values or "")
1485
+
1486
+
1487
+ def _lifecycle_values(item: Mapping[str, Any], *, source_id: str) -> dict[str, Any]:
1488
+ return {
1489
+ "Source ID": source_id,
1490
+ "Row Lifecycle": "superseded" if str(item.get("superseded_by") or "") else "current",
1491
+ "Supersedes": _joined(item.get("supersedes")),
1492
+ "Superseded By": str(item.get("superseded_by") or ""),
1493
+ }
1494
+
1495
+
1496
+ def _node_record_values(node: Mapping[str, Any], *, goal_id: str, source_id: str) -> dict[str, Any]:
1497
+ return {
1498
+ "Title": str(node.get("title") or ""),
1499
+ "Kind": str(node.get("node_kind") or ""),
1500
+ "Status": str(node.get("status") or ""),
1501
+ "Summary": str(node.get("summary") or ""),
1502
+ "Blocked Reason": str(node.get("blocked_reason") or ""),
1503
+ "Parent Node": str(node.get("parent_id") or ""),
1504
+ "Findings": node.get("finding_count"),
1505
+ "Evidence Refs": _joined(node.get("evidence_refs")),
1506
+ "Tags": _joined(node.get("tags")),
1507
+ "Agent ID": str(node.get("agent_id") or ""),
1508
+ "First Recorded At": str(node.get("first_recorded_at") or ""),
1509
+ "Last Updated At": str(node.get("last_updated_at") or ""),
1510
+ _GOAL_ID_FIELD: goal_id,
1511
+ _RESULT_ID_FIELD: str(node.get("node_id") or ""),
1512
+ **_lifecycle_values(node, source_id=source_id),
1513
+ }
1514
+
1515
+
1516
+ def _edge_record_values(edge: Mapping[str, Any], *, goal_id: str, source_id: str) -> dict[str, Any]:
1517
+ return {
1518
+ "From Node": str(edge.get("from_node") or ""),
1519
+ "To Node": str(edge.get("to_node") or ""),
1520
+ "Type": str(edge.get("edge_type") or ""),
1521
+ "Confidence": edge.get("confidence"),
1522
+ "Condition": str(edge.get("summary") or ""),
1523
+ "State Transition": str(edge.get("edge_type") or ""),
1524
+ "Summary": str(edge.get("summary") or ""),
1525
+ "Last Updated At": str(edge.get("last_updated_at") or ""),
1526
+ _GOAL_ID_FIELD: goal_id,
1527
+ _RESULT_ID_FIELD: str(edge.get("edge_id") or ""),
1528
+ "Source ID": source_id,
1529
+ }
1530
+
1531
+
1532
+ def _finding_record_values(finding: Mapping[str, Any], *, goal_id: str, source_id: str) -> dict[str, Any]:
1533
+ return {
1534
+ "Finding": str(finding.get("finding") or ""),
1535
+ "Summary": str(finding.get("summary") or ""),
1536
+ "Status": str(finding.get("status") or ""),
1537
+ "Confidence": finding.get("confidence"),
1538
+ "Node": str(finding.get("node_id") or ""),
1539
+ "Evidence Refs": _joined(finding.get("evidence_refs")),
1540
+ "Tags": _joined(finding.get("tags")),
1541
+ "Agent ID": str(finding.get("agent_id") or ""),
1542
+ "First Recorded At": str(finding.get("first_recorded_at") or ""),
1543
+ "Last Updated At": str(finding.get("last_updated_at") or ""),
1544
+ _GOAL_ID_FIELD: goal_id,
1545
+ _RESULT_ID_FIELD: str(finding.get("finding_id") or ""),
1546
+ **_lifecycle_values(finding, source_id=source_id),
1547
+ }
1548
+
1549
+
1550
+ def _public_safe_values(values: dict[str, Any]) -> dict[str, Any]:
1551
+ return {key: _public_safe_text(value) if isinstance(value, str) else value for key, value in values.items()}
1552
+
1553
+
1554
+ def _with_edge_link_values(
1555
+ values: dict[str, Any],
1556
+ *,
1557
+ record_map: Mapping[str, str],
1558
+ goal_id: str,
1559
+ ) -> dict[str, Any]:
1560
+ """Add linked-record cells so the Lark Base itself represents the graph.
1561
+
1562
+ The plain text node ids remain as readable stable keys. The link fields are
1563
+ best-effort because legacy boards may not have the schema yet; Lark ignores
1564
+ unknown fields only when the request is not sent, so callers should create
1565
+ the fields before enabling live sync against an existing board.
1566
+ """
1567
+
1568
+ linked = dict(values)
1569
+ from_record = str(record_map.get(f"{goal_id}:{TABLE_NODES}:{values.get('From Node')}") or "")
1570
+ to_record = str(record_map.get(f"{goal_id}:{TABLE_NODES}:{values.get('To Node')}") or "")
1571
+ if from_record:
1572
+ linked["From Node Link"] = [{"id": from_record}]
1573
+ if to_record:
1574
+ linked["To Node Link"] = [{"id": to_record}]
1575
+ return linked
1576
+
1577
+
1578
+ def sync_explore_results_to_lark(
1579
+ config: LarkExploreConfig,
1580
+ *,
1581
+ projection: Mapping[str, Any],
1582
+ config_path: Path | None = None,
1583
+ sink_visibility: str = SINK_VISIBILITY_OWNER_ONLY,
1584
+ execute: bool = False,
1585
+ runner: CommandRunner = default_subprocess_runner,
1586
+ ) -> dict[str, Any]:
1587
+ if not isinstance(projection, Mapping):
1588
+ raise ValueError("projection must be a JSON object")
1589
+ if projection.get("schema_version") != EXPLORE_RESULT_PROJECTION_VERSION:
1590
+ raise ValueError(f"projection must use schema {EXPLORE_RESULT_PROJECTION_VERSION}")
1591
+ if sink_visibility not in SINK_VISIBILITIES:
1592
+ raise ValueError(f"sink_visibility must be one of {sorted(SINK_VISIBILITIES)}")
1593
+ public_safe = sink_visibility == SINK_VISIBILITY_SHARED
1594
+ goal_id = str(projection.get("goal_id") or "").strip()
1595
+ if not goal_id:
1596
+ raise ValueError("projection is missing goal_id")
1597
+ source_id = f"loopx-explore:{goal_id}"
1598
+
1599
+ rows_by_table: dict[str, list[dict[str, Any]]] = {
1600
+ TABLE_NODES: [
1601
+ _node_record_values(item, goal_id=goal_id, source_id=source_id)
1602
+ for item in projection.get("nodes") or []
1603
+ if isinstance(item, Mapping)
1604
+ ],
1605
+ TABLE_EDGES: [
1606
+ _edge_record_values(item, goal_id=goal_id, source_id=source_id)
1607
+ for item in projection.get("edges") or []
1608
+ if isinstance(item, Mapping)
1609
+ ],
1610
+ TABLE_FINDINGS: [
1611
+ _finding_record_values(item, goal_id=goal_id, source_id=source_id)
1612
+ for item in projection.get("findings") or []
1613
+ if isinstance(item, Mapping)
1614
+ ],
1615
+ }
1616
+ if public_safe:
1617
+ rows_by_table = {
1618
+ table_key: [_public_safe_values(values) for values in rows] for table_key, rows in rows_by_table.items()
1619
+ }
1620
+ remote_goal_id = _public_safe_text(goal_id) if public_safe else goal_id
1621
+ scan_goal_ids = _record_scan_goal_ids(goal_id, public_safe=public_safe)
1622
+
1623
+ local = read_lark_explore_local_config(config_path) if config_path else {}
1624
+ record_map = dict(local.get("result_records") or {}) if isinstance(local.get("result_records"), dict) else {}
1625
+ commands: list[dict[str, Any]] = []
1626
+ warnings: list[str] = []
1627
+ remote_records: dict[str, dict[str, Any]] = {}
1628
+ duplicate_remote_rows = 0
1629
+ expected_keys = {
1630
+ f"{goal_id}:{table_key}:{str(values.get(_RESULT_ID_FIELD) or '').strip()}"
1631
+ for table_key in EXPLORE_TABLE_KEYS
1632
+ for values in rows_by_table[table_key]
1633
+ }
1634
+
1635
+ if execute:
1636
+ for table_key in EXPLORE_TABLE_KEYS:
1637
+ if not rows_by_table[table_key]:
1638
+ continue
1639
+ for scan_goal_id in scan_goal_ids:
1640
+ offset = 0
1641
+ while True:
1642
+ list_result = _run_command(
1643
+ _build_record_list_command(
1644
+ config,
1645
+ table_id=config.table_id(table_key),
1646
+ goal_id=scan_goal_id,
1647
+ offset=offset,
1648
+ ),
1649
+ execute=True,
1650
+ runner=runner,
1651
+ )
1652
+ commands.append(list_result)
1653
+ if not list_result.get("ok"):
1654
+ warnings.append(
1655
+ f"record-list for {table_key} failed; continuing with cached record ids"
1656
+ )
1657
+ break
1658
+ payload = list_result.get("json") if isinstance(list_result.get("json"), dict) else {}
1659
+ page_records = lark_record_rows(payload)
1660
+ for record in page_records:
1661
+ result_id = str(record.get(_RESULT_ID_FIELD) or "").strip()
1662
+ row_goal_id = str(record.get(_GOAL_ID_FIELD) or "").strip()
1663
+ record_id = str(record.get("_record_id") or "").strip()
1664
+ if not (result_id and row_goal_id and record_id):
1665
+ continue
1666
+ if row_goal_id != scan_goal_id:
1667
+ continue
1668
+ key = f"{goal_id}:{table_key}:{result_id}"
1669
+ if key not in expected_keys:
1670
+ continue
1671
+ if key in remote_records:
1672
+ duplicate_remote_rows += 1
1673
+ continue
1674
+ remote_records[key] = record
1675
+ record_map[key] = record_id
1676
+ if not _record_list_has_more(payload):
1677
+ break
1678
+ if not page_records:
1679
+ warnings.append(
1680
+ f"record-list for {table_key} reported more rows but returned an empty page"
1681
+ )
1682
+ break
1683
+ offset += len(page_records)
1684
+
1685
+ if duplicate_remote_rows:
1686
+ warnings.append(f"found {duplicate_remote_rows} duplicate remote result rows; reused the first row")
1687
+ _persist_lark_explore_record_map(
1688
+ config,
1689
+ config_path=config_path,
1690
+ local=local,
1691
+ record_map=record_map,
1692
+ )
1693
+
1694
+ results: list[dict[str, Any]] = []
1695
+ ok = True
1696
+ skipped_rows = 0
1697
+ written_rows = 0
1698
+ for table_key in EXPLORE_TABLE_KEYS:
1699
+ for values in rows_by_table[table_key]:
1700
+ result_id = str(values.get(_RESULT_ID_FIELD) or "").strip()
1701
+ key = f"{goal_id}:{table_key}:{result_id}"
1702
+ remote_record = remote_records.get(key)
1703
+ if execute and record_map.get(key) and remote_record and _lark_values_match(values, remote_record):
1704
+ result = _skipped_sync_command()
1705
+ skipped_rows += 1
1706
+ else:
1707
+ result = _run_command(
1708
+ _build_upsert_command(
1709
+ config,
1710
+ table_id=config.table_id(table_key),
1711
+ record_id=record_map.get(key) if remote_record else None,
1712
+ values=values,
1713
+ ),
1714
+ execute=execute,
1715
+ runner=runner,
1716
+ )
1717
+ commands.append(result)
1718
+ if execute and result.get("ok"):
1719
+ written_rows += 1
1720
+ record_id = _extract_created_record_id(result.get("json")) or record_map.get(key)
1721
+ if execute and result.get("ok") and record_id:
1722
+ record_map[key] = record_id
1723
+ if not result.get("skipped"):
1724
+ remote_records[key] = {**values, "_record_id": record_id}
1725
+ _persist_lark_explore_record_map(
1726
+ config,
1727
+ config_path=config_path,
1728
+ local=local,
1729
+ record_map=record_map,
1730
+ )
1731
+ results.append(
1732
+ {
1733
+ "table": table_key,
1734
+ "result_id": result_id,
1735
+ "record_id": record_id,
1736
+ "command": result,
1737
+ "values": values,
1738
+ }
1739
+ )
1740
+ ok = ok and bool(result.get("ok"))
1741
+ if execute and not result.get("ok"):
1742
+ break
1743
+ if execute and not ok:
1744
+ break
1745
+
1746
+ if table_key == TABLE_NODES:
1747
+ rows_by_table[TABLE_EDGES] = [
1748
+ _with_edge_link_values(values, record_map=record_map, goal_id=goal_id)
1749
+ for values in rows_by_table[TABLE_EDGES]
1750
+ ]
1751
+
1752
+ readback_records: dict[str, dict[str, Any]] = {}
1753
+ readback_duplicate_rows = 0
1754
+ readback_failed_tables: list[str] = []
1755
+ readback_source = "not_performed"
1756
+ if execute and ok and written_rows == 0:
1757
+ readback_records = dict(remote_records)
1758
+ readback_duplicate_rows = duplicate_remote_rows
1759
+ readback_source = "initial_scan"
1760
+ elif execute and ok:
1761
+ readback_source = "post_write_scan"
1762
+ for table_key in EXPLORE_TABLE_KEYS:
1763
+ if not rows_by_table[table_key]:
1764
+ continue
1765
+ offset = 0
1766
+ while True:
1767
+ list_result = _run_command(
1768
+ _build_record_list_command(
1769
+ config,
1770
+ table_id=config.table_id(table_key),
1771
+ goal_id=remote_goal_id,
1772
+ offset=offset,
1773
+ ),
1774
+ execute=True,
1775
+ runner=runner,
1776
+ )
1777
+ commands.append(list_result)
1778
+ if not list_result.get("ok"):
1779
+ readback_failed_tables.append(table_key)
1780
+ break
1781
+ payload = list_result.get("json") if isinstance(list_result.get("json"), dict) else {}
1782
+ page_records = lark_record_rows(payload)
1783
+ for record in page_records:
1784
+ result_id = str(record.get(_RESULT_ID_FIELD) or "").strip()
1785
+ row_goal_id = str(record.get(_GOAL_ID_FIELD) or "").strip()
1786
+ record_id = str(record.get("_record_id") or "").strip()
1787
+ if not (result_id and row_goal_id and record_id):
1788
+ continue
1789
+ if row_goal_id != remote_goal_id:
1790
+ continue
1791
+ key = f"{goal_id}:{table_key}:{result_id}"
1792
+ if key not in expected_keys:
1793
+ continue
1794
+ if key in readback_records:
1795
+ readback_duplicate_rows += 1
1796
+ continue
1797
+ readback_records[key] = record
1798
+ record_map[key] = record_id
1799
+ if not _record_list_has_more(payload):
1800
+ break
1801
+ if not page_records:
1802
+ readback_failed_tables.append(table_key)
1803
+ break
1804
+ offset += len(page_records)
1805
+
1806
+ expected_values = {
1807
+ f"{goal_id}:{table_key}:{str(values.get(_RESULT_ID_FIELD) or '').strip()}": values
1808
+ for table_key in EXPLORE_TABLE_KEYS
1809
+ for values in rows_by_table[table_key]
1810
+ }
1811
+ missing_readback_keys = sorted(expected_keys - set(readback_records))
1812
+ mismatched_readback_keys = sorted(
1813
+ key
1814
+ for key, values in expected_values.items()
1815
+ if key in readback_records and not _lark_values_match(values, readback_records[key])
1816
+ )
1817
+ readback_verified = bool(
1818
+ execute
1819
+ and ok
1820
+ and not readback_failed_tables
1821
+ and not missing_readback_keys
1822
+ and not mismatched_readback_keys
1823
+ and readback_duplicate_rows == 0
1824
+ )
1825
+ readback = {
1826
+ "ok": readback_verified,
1827
+ "schema_version": LARK_EXPLORE_READBACK_VERSION,
1828
+ "performed": bool(execute and ok),
1829
+ "verified": readback_verified,
1830
+ "source": readback_source,
1831
+ "expected_result_count": len(expected_keys),
1832
+ "observed_result_count": len(readback_records),
1833
+ "missing_result_ids": [key.rsplit(":", 1)[-1] for key in missing_readback_keys],
1834
+ "mismatched_result_ids": [key.rsplit(":", 1)[-1] for key in mismatched_readback_keys],
1835
+ "duplicate_remote_rows": readback_duplicate_rows,
1836
+ "failed_tables": readback_failed_tables,
1837
+ }
1838
+ if execute and ok and not readback_verified:
1839
+ warnings.append("post-write row/result-id readback did not verify the Explore projection")
1840
+ if execute and readback_verified:
1841
+ _persist_lark_explore_record_map(
1842
+ config,
1843
+ config_path=config_path,
1844
+ local=local,
1845
+ record_map=record_map,
1846
+ )
1847
+ ok = ok and (not execute or readback_verified)
1848
+
1849
+ return {
1850
+ "ok": ok,
1851
+ "schema_version": LARK_EXPLORE_SYNC_VERSION,
1852
+ "execute": execute,
1853
+ "goal_id": goal_id,
1854
+ "source_id": source_id,
1855
+ "sink_visibility": sink_visibility,
1856
+ "public_safe_redaction": public_safe,
1857
+ "projection_schema_version": projection.get("schema_version"),
1858
+ "row_counts": {table_key: len(rows_by_table[table_key]) for table_key in EXPLORE_TABLE_KEYS},
1859
+ "written_rows": written_rows,
1860
+ "skipped_rows": skipped_rows,
1861
+ "duplicate_remote_rows": duplicate_remote_rows,
1862
+ "readback": readback,
1863
+ "records": results,
1864
+ "commands": commands,
1865
+ "warnings": warnings,
1866
+ "config_path": str(config_path) if config_path else None,
1867
+ "error": None
1868
+ if ok
1869
+ else (
1870
+ "row/result-id readback failed"
1871
+ if readback.get("performed") and not readback_verified
1872
+ else next(
1873
+ (_command_error(item) for item in commands if not item.get("ok")),
1874
+ "unknown",
1875
+ )
1876
+ ),
1877
+ }
1878
+
1879
+
1880
+ @singleflight_issue_fix_material_sync(source_lark_explore_config_path)
1881
+ def sync_issue_fix_explore_on_material_change(
1882
+ *,
1883
+ registry_path: Path,
1884
+ goal_id: str,
1885
+ agent_id: str | None = None,
1886
+ project: Path | None = None,
1887
+ state_file: Path | None = None,
1888
+ execute: bool = False,
1889
+ external_sink_delivery_authorized: bool = True,
1890
+ runner: CommandRunner = default_subprocess_runner,
1891
+ ) -> dict[str, Any]:
1892
+ """Project issue-fix facts and sync Lark only when the graph digest changed.
1893
+
1894
+ The canonical Explore result log is updated before this adapter runs. A
1895
+ failed or interrupted Lark write therefore remains retryable: the stored
1896
+ sink digest advances only after a successful remote sync.
1897
+ """
1898
+
1899
+ from ....capabilities.issue_fix.explore_projection import (
1900
+ build_issue_fix_executive_visual_projection,
1901
+ project_issue_fix_explore_graph,
1902
+ )
1903
+ from .explore_source_guard import (
1904
+ resolve_explore_source_registry,
1905
+ source_projection_coverage_guard,
1906
+ )
1907
+
1908
+ source_registry_path, compact_source_runtime_route = resolve_explore_source_registry(
1909
+ registry_path=registry_path,
1910
+ goal_id=goal_id,
1911
+ )
1912
+ projection_result = project_issue_fix_explore_graph(
1913
+ registry_path=source_registry_path,
1914
+ goal_id=goal_id,
1915
+ agent_id=agent_id,
1916
+ project=project,
1917
+ state_file=state_file,
1918
+ execute=execute,
1919
+ )
1920
+ projection_result["source_runtime_route"] = compact_source_runtime_route
1921
+ config_path = default_lark_explore_config_path(source_registry_path)
1922
+ local = read_lark_explore_local_config(config_path)
1923
+ config = lark_explore_config_from_payload(local) if local.get("ok") else None
1924
+ sync_state = (
1925
+ local.get("automatic_projection_sync") if isinstance(local.get("automatic_projection_sync"), dict) else {}
1926
+ )
1927
+ prior = sync_state.get(goal_id) if isinstance(sync_state.get(goal_id), dict) else {}
1928
+ digest = str(projection_result.get("semantic_digest") or "")
1929
+ prior_digest = str(prior.get("canonical_rows_semantic_digest") or prior.get("semantic_digest") or "")
1930
+ prior_row_readback_digest = str(prior.get("canonical_rows_readback_semantic_digest") or "")
1931
+ source_projection_guard = source_projection_coverage_guard(
1932
+ local=local,
1933
+ projection=projection_result.get("projection")
1934
+ if isinstance(projection_result.get("projection"), Mapping)
1935
+ else {},
1936
+ goal_id=goal_id,
1937
+ )
1938
+ visual_sinks = (
1939
+ local.get("visual_sinks")
1940
+ if isinstance(local.get("visual_sinks"), dict) and local.get("visual_sinks")
1941
+ else None
1942
+ )
1943
+ visual_sink = (
1944
+ local.get("visual_sink")
1945
+ if visual_sinks is None and isinstance(local.get("visual_sink"), dict)
1946
+ else None
1947
+ )
1948
+ prior_visual_digest = str(prior.get("visual_semantic_digest") or "")
1949
+ prior_visual_delivery_digests = (
1950
+ prior.get("visual_delivery_digests")
1951
+ if isinstance(prior.get("visual_delivery_digests"), dict)
1952
+ else {}
1953
+ )
1954
+ needs_row_sync = bool(digest and (digest != prior_digest or digest != prior_row_readback_digest))
1955
+ needs_visual_sync = bool(
1956
+ (visual_sinks or visual_sink) and digest and digest != prior_visual_digest
1957
+ )
1958
+ visual_preview: dict[str, Any] | None = None
1959
+ expected_visual_delivery_digests: dict[str, str] = {}
1960
+ needs_sync = needs_row_sync or needs_visual_sync
1961
+ if not projection_result.get("applicable"):
1962
+ return {
1963
+ "ok": True,
1964
+ "schema_version": "issue_fix_explore_lark_material_sync_v0",
1965
+ "status": "not_applicable",
1966
+ "execute": execute,
1967
+ "needs_sync": False,
1968
+ "needs_row_sync": False,
1969
+ "needs_visual_sync": False,
1970
+ "row_readback_verified": None,
1971
+ "semantic_digest": digest,
1972
+ "prior_semantic_digest": prior_digest or None,
1973
+ "projection": projection_result,
1974
+ "lark_sync": None,
1975
+ "config_path": str(config_path),
1976
+ }
1977
+ if config is None:
1978
+ invalid_config = local.get("ok") is not True or isinstance(local.get("board"), dict)
1979
+ return {
1980
+ "ok": not invalid_config,
1981
+ "schema_version": "issue_fix_explore_lark_material_sync_v0",
1982
+ "status": "invalid_config" if invalid_config else "not_configured",
1983
+ "execute": execute,
1984
+ "needs_sync": needs_sync,
1985
+ "needs_row_sync": needs_row_sync,
1986
+ "needs_visual_sync": needs_visual_sync,
1987
+ "row_readback_verified": None,
1988
+ "semantic_digest": digest,
1989
+ "prior_semantic_digest": prior_digest or None,
1990
+ "projection": projection_result,
1991
+ "lark_sync": None,
1992
+ "config_path": str(config_path),
1993
+ }
1994
+ if not source_projection_guard["ok"]:
1995
+ return {
1996
+ "ok": False,
1997
+ "schema_version": "issue_fix_explore_lark_material_sync_v0",
1998
+ "status": "source_projection_regression_blocked",
1999
+ "execute": execute,
2000
+ "needs_sync": True,
2001
+ "needs_row_sync": needs_row_sync,
2002
+ "needs_visual_sync": needs_visual_sync,
2003
+ "row_readback_verified": False,
2004
+ "semantic_digest": digest,
2005
+ "prior_semantic_digest": prior_digest or None,
2006
+ "source_runtime_route": compact_source_runtime_route,
2007
+ "source_projection_guard": source_projection_guard,
2008
+ "projection": projection_result,
2009
+ "lark_sync": None,
2010
+ "canonical_rows_sync": None,
2011
+ "visual_preview": None,
2012
+ "visual_sync": None,
2013
+ "external_write_performed": False,
2014
+ "retryable": True,
2015
+ "required_action": (
2016
+ "reconcile the registry-declared Explore source runtime before retry"
2017
+ ),
2018
+ "error": (
2019
+ "registry-declared Explore source omits result ids already registered "
2020
+ "by the sink; refusing a regressive remote write"
2021
+ ),
2022
+ "config_path": str(config_path),
2023
+ }
2024
+ if visual_sinks:
2025
+ visual_preview = sync_explore_visuals_to_lark(
2026
+ config,
2027
+ projection=projection_result["projection"],
2028
+ visual_sinks=visual_sinks,
2029
+ config_path=config_path,
2030
+ execute=False,
2031
+ runner=runner,
2032
+ )
2033
+ expected_visual_delivery_digests = _visual_delivery_digests(visual_preview)
2034
+ needs_visual_sync = bool(
2035
+ digest
2036
+ and (
2037
+ not visual_preview.get("ok")
2038
+ or any(bool(view.get("reconciliation", {}).get("required")) for view in visual_preview.get("views", {}).values())
2039
+ or any(
2040
+ prior_visual_delivery_digests.get(role) != delivery_digest
2041
+ for role, delivery_digest in expected_visual_delivery_digests.items()
2042
+ )
2043
+ )
2044
+ )
2045
+ needs_sync = needs_row_sync or needs_visual_sync
2046
+ if not needs_sync:
2047
+ return {
2048
+ "ok": True,
2049
+ "schema_version": "issue_fix_explore_lark_material_sync_v0",
2050
+ "status": "unchanged",
2051
+ "execute": execute,
2052
+ "needs_sync": False,
2053
+ "needs_row_sync": False,
2054
+ "needs_visual_sync": False,
2055
+ "row_readback_verified": prior_row_readback_digest == digest,
2056
+ "semantic_digest": digest,
2057
+ "prior_semantic_digest": prior_digest or None,
2058
+ "projection": projection_result,
2059
+ "lark_sync": None,
2060
+ "visual_preview": visual_preview,
2061
+ "config_path": str(config_path),
2062
+ }
2063
+ if not execute:
2064
+ return {
2065
+ "ok": True,
2066
+ "schema_version": "issue_fix_explore_lark_material_sync_v0",
2067
+ "status": "would_sync",
2068
+ "execute": False,
2069
+ "needs_sync": True,
2070
+ "needs_row_sync": needs_row_sync,
2071
+ "needs_visual_sync": needs_visual_sync,
2072
+ "row_readback_verified": False,
2073
+ "semantic_digest": digest,
2074
+ "prior_semantic_digest": prior_digest or None,
2075
+ "projection": projection_result,
2076
+ "lark_sync": None,
2077
+ "visual_preview": visual_preview,
2078
+ "config_path": str(config_path),
2079
+ }
2080
+ if not external_sink_delivery_authorized:
2081
+ return {
2082
+ "ok": True,
2083
+ "schema_version": "issue_fix_explore_lark_material_sync_v0",
2084
+ "status": "external_sink_suppressed",
2085
+ "execute": True,
2086
+ "external_sink_delivery_authorized": False,
2087
+ "needs_sync": True,
2088
+ "needs_row_sync": needs_row_sync,
2089
+ "needs_visual_sync": needs_visual_sync,
2090
+ "row_readback_verified": False,
2091
+ "semantic_digest": digest,
2092
+ "prior_semantic_digest": prior_digest or None,
2093
+ "prior_visual_semantic_digest": prior_visual_digest or None,
2094
+ "prior_visual_delivery_digests": dict(prior_visual_delivery_digests),
2095
+ "projection": projection_result,
2096
+ "lark_sync": None,
2097
+ "canonical_rows_sync": None,
2098
+ "visual_preview": visual_preview,
2099
+ "visual_sync": None,
2100
+ "retryable": True,
2101
+ "config_path": str(config_path),
2102
+ }
2103
+ lark_sync = (
2104
+ sync_explore_results_to_lark(
2105
+ config,
2106
+ projection=projection_result["projection"],
2107
+ config_path=config_path,
2108
+ execute=True,
2109
+ runner=runner,
2110
+ )
2111
+ if needs_row_sync
2112
+ else None
2113
+ )
2114
+ if not needs_visual_sync:
2115
+ visual_sync = None
2116
+ elif visual_sinks:
2117
+ visual_sync = sync_explore_visuals_to_lark(
2118
+ config,
2119
+ projection=projection_result["projection"],
2120
+ visual_sinks=visual_sinks,
2121
+ config_path=config_path,
2122
+ execute=True,
2123
+ runner=runner,
2124
+ )
2125
+ else:
2126
+ visual_sync = sync_explore_visual_to_lark(
2127
+ config,
2128
+ projection=projection_result["projection"],
2129
+ visual_sink=visual_sink,
2130
+ config_path=config_path,
2131
+ semantic_digest=digest,
2132
+ display_projection=build_issue_fix_executive_visual_projection(projection_result["projection"])
2133
+ if str(visual_sink.get("projection_mode") or "") == "issue_fix_two_lane"
2134
+ else None,
2135
+ execute=True,
2136
+ runner=runner,
2137
+ )
2138
+ row_ok = lark_sync is None or bool(lark_sync.get("ok"))
2139
+ row_readback_verified = bool(
2140
+ lark_sync is None
2141
+ or (
2142
+ isinstance(lark_sync.get("readback"), Mapping)
2143
+ and lark_sync["readback"].get("verified") is True
2144
+ )
2145
+ )
2146
+ row_ok = row_ok and row_readback_verified
2147
+ visual_ok = visual_sync is None or bool(visual_sync.get("ok"))
2148
+ if row_ok or visual_ok:
2149
+ updated_sync_state = dict(sync_state)
2150
+ updated_goal_state = dict(prior)
2151
+ if needs_row_sync and row_ok:
2152
+ updated_goal_state.update(
2153
+ {
2154
+ "semantic_digest": digest,
2155
+ "canonical_rows_semantic_digest": digest,
2156
+ "canonical_rows_readback_semantic_digest": digest,
2157
+ "canonical_rows_synced_at": now_lark_datetime(),
2158
+ }
2159
+ )
2160
+ if needs_visual_sync and visual_ok:
2161
+ updated_goal_state.update(
2162
+ {
2163
+ "visual_semantic_digest": digest,
2164
+ "visual_published_at": now_lark_datetime(),
2165
+ }
2166
+ )
2167
+ if visual_sinks:
2168
+ updated_goal_state["visual_delivery_digests"] = _visual_delivery_digests(
2169
+ visual_sync
2170
+ )
2171
+ updated_goal_state["synced_at"] = now_lark_datetime()
2172
+ updated_sync_state[goal_id] = updated_goal_state
2173
+ # The row sync persists record ids incrementally. Re-read that write
2174
+ # before adding sink-specific digests so a partial success remains
2175
+ # retryable without restoring a stale record map.
2176
+ persisted_local = read_lark_explore_local_config(config_path)
2177
+ updated_local = dict(persisted_local if persisted_local.get("ok") else local)
2178
+ updated_local["automatic_projection_sync"] = updated_sync_state
2179
+ write_lark_explore_local_config(config_path, updated_local)
2180
+ all_ok = row_ok and visual_ok
2181
+ return {
2182
+ "ok": all_ok,
2183
+ "schema_version": "issue_fix_explore_lark_material_sync_v0",
2184
+ "status": "synced" if all_ok else "sync_failed",
2185
+ "execute": True,
2186
+ "needs_sync": True,
2187
+ "needs_row_sync": needs_row_sync,
2188
+ "needs_visual_sync": needs_visual_sync,
2189
+ "row_readback_verified": row_readback_verified,
2190
+ "semantic_digest": digest,
2191
+ "prior_semantic_digest": prior_digest or None,
2192
+ "prior_visual_semantic_digest": prior_visual_digest or None,
2193
+ "prior_visual_delivery_digests": dict(prior_visual_delivery_digests),
2194
+ "projection": projection_result,
2195
+ "lark_sync": lark_sync,
2196
+ "canonical_rows_sync": lark_sync,
2197
+ "visual_preview": visual_preview,
2198
+ "visual_sync": visual_sync,
2199
+ "config_path": str(config_path),
2200
+ }
2201
+ def build_explore_card_markdown(projection: Mapping[str, Any]) -> str:
2202
+ counts = projection.get("counts") if isinstance(projection.get("counts"), dict) else {}
2203
+ by_status = counts.get("nodes_by_status") if isinstance(counts.get("nodes_by_status"), dict) else {}
2204
+ status_parts = [
2205
+ f"{by_status.get(status, 0)} {label}"
2206
+ for status, label in (
2207
+ (NODE_STATUS_EXPLORING, "exploring"),
2208
+ (NODE_STATUS_BLOCKED, "blocked"),
2209
+ (NODE_STATUS_RESOLVED, "resolved"),
2210
+ (NODE_STATUS_OPEN, "open"),
2211
+ )
2212
+ if by_status.get(status, 0)
2213
+ ]
2214
+ lines = [
2215
+ (
2216
+ f"**Exploration map**: {counts.get('node_count', 0)} nodes"
2217
+ + (f" ({', '.join(status_parts)})" if status_parts else "")
2218
+ + f", {counts.get('edge_count', 0)} edges, "
2219
+ f"{counts.get('finding_count', 0)} findings"
2220
+ ),
2221
+ "",
2222
+ ]
2223
+ stuck = [item for item in projection.get("stuck") or [] if isinstance(item, Mapping)]
2224
+ if stuck:
2225
+ lines.append("**Blocked**")
2226
+ for node in stuck:
2227
+ reason = str(node.get("blocked_reason") or "").strip()
2228
+ lines.append(f"- {node.get('title')}" + (f" - {reason}" if reason else ""))
2229
+ lines.append("")
2230
+ findings = [item for item in projection.get("findings") or [] if isinstance(item, Mapping)]
2231
+ if findings:
2232
+ lines.append("**Latest findings**")
2233
+ for finding in findings[:5]:
2234
+ lines.append(f"- [{finding.get('status')}] {finding.get('finding')}")
2235
+ lines.append("")
2236
+ frontier = [item for item in projection.get("frontier") or [] if isinstance(item, Mapping)]
2237
+ if frontier:
2238
+ lines.append("**Exploring now**")
2239
+ for node in frontier[:5]:
2240
+ lines.append(f"- {node.get('title')}")
2241
+ return "\n".join(lines).strip()
2242
+
2243
+
2244
+ def build_explore_result_card(
2245
+ projection: Mapping[str, Any],
2246
+ *,
2247
+ title: str | None = None,
2248
+ template: str = "blue",
2249
+ message_id: str | None = None,
2250
+ ) -> dict[str, Any]:
2251
+ if not isinstance(projection, Mapping):
2252
+ raise ValueError("projection must be a JSON object")
2253
+ if projection.get("schema_version") != EXPLORE_RESULT_PROJECTION_VERSION:
2254
+ raise ValueError(f"projection must use schema {EXPLORE_RESULT_PROJECTION_VERSION}")
2255
+ goal_id = str(projection.get("goal_id") or "").strip()
2256
+ markdown = build_explore_card_markdown(projection)
2257
+ card = build_lark_markdown_reply_card(
2258
+ markdown,
2259
+ title=title or f"Exploration map: {goal_id}",
2260
+ template=template,
2261
+ footer=(
2262
+ f"LoopX explore | {projection.get('generated_at')} | {projection.get('source_event_count')} result events"
2263
+ ),
2264
+ )
2265
+ return {
2266
+ "ok": True,
2267
+ "schema_version": LARK_EXPLORE_CARD_VERSION,
2268
+ "goal_id": goal_id,
2269
+ "message_id": message_id or None,
2270
+ "card": card,
2271
+ "card_markdown": markdown,
2272
+ "send_boundary": (
2273
+ "Card content only. Send or update the Lark message through an "
2274
+ "approved gateway (bot or lark-cli) after the operator permits the write."
2275
+ ),
2276
+ }