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,1334 @@
1
+ """Same-source canonical and executive views over Explore evidence.
2
+
3
+ The Explore result projection remains the only evidence source. This module
4
+ adds presentation advice and derived display views; it never mutates or
5
+ truncates the canonical node, edge, or finding collections.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import html
12
+ import json
13
+ import re
14
+ import unicodedata
15
+ from collections import defaultdict
16
+ from collections.abc import Mapping, Sequence
17
+ from typing import Any
18
+
19
+ EXPLORE_PRESENTATION_BUNDLE_VERSION = "loopx_explore_presentation_bundle_v0"
20
+ EXPLORE_PRESENTATION_ASSESSMENT_VERSION = "loopx_explore_presentation_assessment_v0"
21
+ EXPLORE_CANONICAL_VIEW_VERSION = "loopx_explore_canonical_view_v0"
22
+ EXPLORE_EXECUTIVE_VIEW_VERSION = "loopx_explore_executive_view_v0"
23
+
24
+ PRESENTATION_MODE_CANONICAL_ONLY = "canonical_only"
25
+ PRESENTATION_MODE_DUAL_VIEW = "dual_view"
26
+
27
+ _ACTIVE_STATUSES = {"open", "exploring", "blocked"}
28
+ _TERMINAL_STATUSES = {"resolved", "dead_end"}
29
+ _DECISION_TAGS = {
30
+ "active",
31
+ "baseline",
32
+ "capacity",
33
+ "contract",
34
+ "current-best",
35
+ "decision",
36
+ "executive",
37
+ "guardrail",
38
+ "incumbent",
39
+ "resource",
40
+ "risk",
41
+ }
42
+ _LEGACY_LEADER_TAGS = {"leader", "provisional-leader", "winner"}
43
+ _COUNTEREVIDENCE_TAGS = {
44
+ "counterevidence",
45
+ "negative",
46
+ "no-promote",
47
+ "no_promote",
48
+ "refuted",
49
+ "retired",
50
+ }
51
+ _EXECUTIVE_EXPANSION_EDGE_TYPES = {"answers", "depends_on", "leads_to", "refutes"}
52
+ _VOLATILE_VIEW_KEYS = {
53
+ "first_recorded_at",
54
+ "last_updated_at",
55
+ "update_count",
56
+ "generated_at",
57
+ "log_path",
58
+ }
59
+
60
+ DEFAULT_EXPLORE_PRESENTATION_POLICY: dict[str, float | int] = {
61
+ "decision_density_node_floor": 24,
62
+ "decision_density_ceiling": 0.35,
63
+ "terminal_ratio_node_floor": 30,
64
+ "terminal_ratio_floor": 0.60,
65
+ "terminal_neighborhood_floor": 2,
66
+ "decision_depth_floor": 6,
67
+ "readability_node_floor": 60,
68
+ "readability_edge_density_floor": 2.0,
69
+ "readability_label_chars": 96,
70
+ "readability_root_count_floor": 16,
71
+ "readability_root_ratio_floor": 0.30,
72
+ "stage_node_capacity": 14,
73
+ "executive_counterevidence_limit": 8,
74
+ "executive_hub_edge_degree_floor": 4,
75
+ }
76
+
77
+ _MERMAID_STATUS_CLASS = {
78
+ "open": "open",
79
+ "exploring": "exploring",
80
+ "blocked": "blocked",
81
+ "resolved": "resolved",
82
+ "dead_end": "deadend",
83
+ }
84
+
85
+ _SVG_STATUS_STYLE = {
86
+ "open": ("#f5f5f5", "#9e9e9e"),
87
+ "exploring": ("#e3f2fd", "#1e88e5"),
88
+ "blocked": ("#ffebee", "#e53935"),
89
+ "resolved": ("#e8f5e9", "#43a047"),
90
+ "dead_end": ("#eeeeee", "#9e9e9e"),
91
+ }
92
+ _SVG_NODE_HEADER_DISPLAY_WIDTH = 50
93
+ _SVG_NODE_DETAIL_DISPLAY_WIDTH = 62
94
+
95
+
96
+ def build_stage_lane_svg(
97
+ *,
98
+ title: str,
99
+ node_order: Sequence[str],
100
+ node_by_id: Mapping[str, Mapping[str, Any]],
101
+ edges: Sequence[Mapping[str, Any]],
102
+ ) -> str:
103
+ """Render one Evidence Stage as fixed left/right semantic lanes.
104
+
105
+ Mermaid ignores a nested subgraph's direction when one of its nodes has an
106
+ edge to another subgraph. That turns intended vertical lanes into a wide
107
+ card row on the real whiteboard renderer. SVG keeps the semantic lane and
108
+ edge model while making placement deterministic.
109
+ """
110
+
111
+ lane_nodes: dict[str, list[str]] = defaultdict(list)
112
+ for node_id in node_order:
113
+ lane_nodes[_explore_lane(node_by_id[node_id], node_by_id=node_by_id)].append(
114
+ node_id
115
+ )
116
+ preferred = [lane for lane in ("fix_pr", "capability") if lane in lane_nodes]
117
+ lanes = preferred + sorted(lane for lane in lane_nodes if lane not in preferred)
118
+ margin, lane_width, lane_gap = 32, 500, 44
119
+ lane_top, lane_header_height, lane_padding = 76, 48, 24
120
+ node_width, node_height, node_gap = 436, 96, 34
121
+ max_lane_nodes = max((len(lane_nodes[lane]) for lane in lanes), default=1)
122
+ lane_height = (
123
+ lane_header_height
124
+ + lane_padding * 2
125
+ + max_lane_nodes * node_height
126
+ + max(0, max_lane_nodes - 1) * node_gap
127
+ )
128
+ width = margin * 2 + len(lanes) * lane_width + max(0, len(lanes) - 1) * lane_gap
129
+ height = lane_top + lane_height + margin
130
+ positions: dict[str, tuple[float, float]] = {}
131
+ parts = [
132
+ f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">',
133
+ "<defs>",
134
+ '<marker id="loopx-arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth">',
135
+ '<path d="M0,0 L0,6 L9,3 z" fill="#607d8b"/>',
136
+ "</marker>",
137
+ "</defs>",
138
+ '<rect width="100%" height="100%" fill="#ffffff"/>',
139
+ f'<text x="{width / 2:.1f}" y="34" text-anchor="middle" font-family="Arial, sans-serif" font-size="20" font-weight="700" fill="#263238">{html.escape(title)}</text>',
140
+ ]
141
+ lane_titles = {"fix_pr": "PR issue-fix", "capability": "LoopX capability"}
142
+ for lane_index, lane in enumerate(lanes):
143
+ lane_x = margin + lane_index * (lane_width + lane_gap)
144
+ parts.extend(
145
+ [
146
+ f'<rect x="{lane_x}" y="{lane_top}" width="{lane_width}" height="{lane_height}" rx="16" fill="#fffde7" stroke="#c0b94f" stroke-width="2"/>',
147
+ f'<text x="{lane_x + lane_width / 2:.1f}" y="{lane_top + 31}" text-anchor="middle" font-family="Arial, sans-serif" font-size="18" font-weight="700" fill="#37474f">{html.escape(lane_titles.get(lane, lane.replace("_", " ").title()))}</text>',
148
+ ]
149
+ )
150
+ node_x = lane_x + (lane_width - node_width) / 2
151
+ node_y = lane_top + lane_header_height + lane_padding
152
+ for offset, node_id in enumerate(lane_nodes[lane]):
153
+ positions[node_id] = (node_x, node_y + offset * (node_height + node_gap))
154
+
155
+ edge_parts: list[str] = []
156
+ grouped_edges: dict[tuple[str, str], set[str]] = {}
157
+ for edge in edges:
158
+ source_id = str(edge.get("from_node") or "")
159
+ target_id = str(edge.get("to_node") or "")
160
+ if source_id not in positions or target_id not in positions:
161
+ continue
162
+ grouped_edges.setdefault((source_id, target_id), set()).add(
163
+ str(edge.get("edge_type") or "").strip()
164
+ )
165
+ for (source_id, target_id), edge_types in grouped_edges.items():
166
+ source_x, source_y = positions[source_id]
167
+ target_x, target_y = positions[target_id]
168
+ same_lane = _explore_lane(
169
+ node_by_id[source_id], node_by_id=node_by_id
170
+ ) == _explore_lane(node_by_id[target_id], node_by_id=node_by_id)
171
+ if same_lane:
172
+ if target_y >= source_y:
173
+ start_x = source_x + node_width / 2
174
+ end_x = target_x + node_width / 2
175
+ start_y = source_y + node_height
176
+ end_y = target_y
177
+ middle_y = (start_y + end_y) / 2
178
+ path = f"M {start_x:.1f} {start_y:.1f} C {start_x:.1f} {middle_y:.1f}, {end_x:.1f} {middle_y:.1f}, {end_x:.1f} {end_y:.1f}"
179
+ else:
180
+ start_x = source_x + node_width
181
+ end_x = target_x + node_width
182
+ start_y = source_y + node_height / 2
183
+ end_y = target_y + node_height / 2
184
+ side_x = max(source_x, target_x) + node_width + 18
185
+ path = f"M {start_x:.1f} {start_y:.1f} C {side_x:.1f} {start_y:.1f}, {side_x:.1f} {end_y:.1f}, {end_x:.1f} {end_y:.1f}"
186
+ else:
187
+ left_to_right = source_x < target_x
188
+ start_x = source_x + (node_width if left_to_right else 0)
189
+ end_x = target_x + (0 if left_to_right else node_width)
190
+ start_y = source_y + node_height / 2
191
+ end_y = target_y + node_height / 2
192
+ middle_x = (start_x + end_x) / 2
193
+ path = f"M {start_x:.1f} {start_y:.1f} C {middle_x:.1f} {start_y:.1f}, {middle_x:.1f} {end_y:.1f}, {end_x:.1f} {end_y:.1f}"
194
+ label = " / ".join(sorted(edge_type for edge_type in edge_types if edge_type))
195
+ edge_parts.append(
196
+ f'<path d="{path}" data-edge-types="{html.escape(label)}" fill="none" stroke="#607d8b" stroke-width="2" marker-end="url(#loopx-arrow)"/>'
197
+ )
198
+ parts.extend(edge_parts)
199
+ for node_id in node_order:
200
+ node = node_by_id[node_id]
201
+ node_x, node_y = positions[node_id]
202
+ fill, stroke = _SVG_STATUS_STYLE.get(
203
+ str(node.get("status") or "open"), _SVG_STATUS_STYLE["open"]
204
+ )
205
+ parts.append(
206
+ f'<rect x="{node_x:.1f}" y="{node_y:.1f}" width="{node_width}" height="{node_height}" rx="10" fill="{fill}" stroke="{stroke}" stroke-width="2"/>'
207
+ )
208
+ display_lines = _svg_node_display_lines(node)
209
+ for line_index, line in enumerate(display_lines):
210
+ weight = "700" if line_index == 0 else "400"
211
+ size = 14 if line_index == 0 else 12
212
+ parts.append(
213
+ f'<text x="{node_x + node_width / 2:.1f}" y="{node_y + 27 + line_index * 24:.1f}" text-anchor="middle" font-family="Arial, sans-serif" font-size="{size}" font-weight="{weight}" fill="#263238">{html.escape(line)}</text>'
214
+ )
215
+ parts.append("</svg>")
216
+ return "\n".join(parts)
217
+
218
+
219
+ def _material_rows(values: Sequence[Any] | None, *, id_key: str) -> list[dict[str, Any]]:
220
+ rows = []
221
+ for value in values or []:
222
+ if not isinstance(value, Mapping):
223
+ continue
224
+ row = {
225
+ key: item
226
+ for key, item in value.items()
227
+ if key not in _VOLATILE_VIEW_KEYS
228
+ }
229
+ if row.get(id_key):
230
+ rows.append(row)
231
+ return sorted(rows, key=lambda item: str(item.get(id_key) or ""))
232
+
233
+
234
+ def explore_source_digest(projection: Mapping[str, Any]) -> str:
235
+ """Return a stable digest for the complete canonical evidence projection."""
236
+
237
+ material = {
238
+ "goal_id": projection.get("goal_id"),
239
+ "nodes": _material_rows(projection.get("nodes"), id_key="node_id"),
240
+ "edges": _material_rows(projection.get("edges"), id_key="edge_id"),
241
+ "findings": _material_rows(projection.get("findings"), id_key="finding_id"),
242
+ }
243
+ encoded = json.dumps(
244
+ material,
245
+ ensure_ascii=False,
246
+ sort_keys=True,
247
+ separators=(",", ":"),
248
+ ).encode("utf-8")
249
+ return hashlib.sha256(encoded).hexdigest()
250
+
251
+
252
+ def explore_source_revision(projection: Mapping[str, Any], *, digest: str | None = None) -> str:
253
+ source_digest = digest or explore_source_digest(projection)
254
+ event_count = max(0, int(projection.get("source_event_count") or 0))
255
+ return f"events-{event_count}-{source_digest[:12]}"
256
+
257
+
258
+ def _parent_map(
259
+ nodes: Sequence[Mapping[str, Any]],
260
+ edges: Sequence[Mapping[str, Any]],
261
+ ) -> dict[str, str]:
262
+ parents = {
263
+ str(node.get("node_id") or ""): str(node.get("parent_id") or "")
264
+ for node in nodes
265
+ if str(node.get("node_id") or "") and str(node.get("parent_id") or "")
266
+ }
267
+ for edge in edges:
268
+ if str(edge.get("edge_type") or "") != "subtopic_of":
269
+ continue
270
+ child = str(edge.get("from_node") or "")
271
+ parent = str(edge.get("to_node") or "")
272
+ if child and parent:
273
+ parents.setdefault(child, parent)
274
+ return parents
275
+
276
+
277
+ def _lineage(node_id: str, parents: Mapping[str, str], node_ids: set[str]) -> list[str]:
278
+ path = [node_id]
279
+ seen = {node_id}
280
+ parent = parents.get(node_id)
281
+ while parent and parent in node_ids and parent not in seen:
282
+ path.append(parent)
283
+ seen.add(parent)
284
+ parent = parents.get(parent)
285
+ path.reverse()
286
+ return path
287
+
288
+
289
+ def _node_depths(node_ids: set[str], parents: Mapping[str, str]) -> dict[str, int]:
290
+ return {
291
+ node_id: max(0, len(_lineage(node_id, parents, node_ids)) - 1)
292
+ for node_id in node_ids
293
+ }
294
+
295
+
296
+ def _mermaid_id(value: str) -> str:
297
+ return re.sub(r"[^A-Za-z0-9_]", "_", str(value))
298
+
299
+
300
+ def _mermaid_label(value: str, *, limit: int = 60) -> str:
301
+ cleaned = re.sub(r'["\[\]{}<>`|]', "'", str(value or ""))
302
+ return _truncate_display(cleaned, limit=limit) or "untitled"
303
+
304
+
305
+ _METRIC_SIGNAL = re.compile(
306
+ r"(?:"
307
+ r"[+-]\s*\d+(?:\.\d+)?"
308
+ r"|\d+(?:\.\d+)?\s*/\s*[+-]?\s*\d+(?:\.\d+)?"
309
+ r"|\d+(?:\.\d+)?\s*(?:bp|bps|%|ms|sec|secs|seconds?|x|×)\b"
310
+ r")",
311
+ flags=re.IGNORECASE,
312
+ )
313
+
314
+ _NODE_STATUS_LABEL = {
315
+ "open": "OPEN",
316
+ "exploring": "ACTIVE",
317
+ "blocked": "BLOCKED",
318
+ "resolved": "DONE",
319
+ "dead_end": "NO-PROMOTE",
320
+ }
321
+
322
+
323
+ def _summary_clauses(value: str) -> list[str]:
324
+ normalized = re.sub(r"\s+", " ", str(value or "")).strip()
325
+ if not normalized:
326
+ return []
327
+ return [
328
+ clause.strip(" .;。;")
329
+ for clause in re.split(r"(?<=[!?。!?;;])\s*|(?<=\.)\s+", normalized)
330
+ if clause.strip(" .;。;")
331
+ ]
332
+
333
+
334
+ def _compact_detail_clause(value: str, *, limit: int) -> str:
335
+ """Keep a metric-bearing portion visible when a long clause is compacted."""
336
+
337
+ normalized = re.sub(r"\s+", " ", str(value or "")).strip()
338
+ metric = _METRIC_SIGNAL.search(normalized)
339
+ if metric and _display_width(normalized) > limit:
340
+ context_width = max(18, limit // 3)
341
+ prefix = normalized[: metric.start()].rstrip()
342
+ metric_and_tail = normalized[metric.start() :]
343
+ if _display_width(prefix) > context_width:
344
+ prefix = "…" + _tail_display(prefix, limit=context_width - 1)
345
+ normalized = f"{prefix} {metric_and_tail}".strip()
346
+ return _truncate_display(normalized, limit=limit)
347
+
348
+
349
+ def _node_decision_lines(
350
+ node: Mapping[str, Any],
351
+ *,
352
+ detail_limit: int,
353
+ ) -> list[str]:
354
+ """Project one metric-bearing evidence line and one conclusion line."""
355
+
356
+ clauses = _summary_clauses(str(node.get("summary") or ""))
357
+ if not clauses:
358
+ return []
359
+ metric_clause = next((clause for clause in clauses if _METRIC_SIGNAL.search(clause)), None)
360
+ selected = []
361
+ if metric_clause:
362
+ selected.append(metric_clause)
363
+ else:
364
+ selected.append(clauses[0])
365
+ if clauses[-1] != selected[0]:
366
+ selected.append(clauses[-1])
367
+ return [
368
+ _compact_detail_clause(clause, limit=detail_limit)
369
+ for clause in selected[:2]
370
+ ]
371
+
372
+
373
+ def _node_display_lines(
374
+ node: Mapping[str, Any],
375
+ *,
376
+ title_limit: int,
377
+ detail_limit: int,
378
+ ) -> list[str]:
379
+ status = str(node.get("status") or "open")
380
+ title = _truncate_display(
381
+ str(node.get("title") or node.get("node_id") or "untitled"),
382
+ limit=title_limit,
383
+ )
384
+ header = f"{title} · {_NODE_STATUS_LABEL.get(status, status.upper())}"
385
+ return [header, *_node_decision_lines(node, detail_limit=detail_limit)]
386
+
387
+
388
+ def _svg_node_display_lines(node: Mapping[str, Any]) -> list[str]:
389
+ status = str(node.get("status") or "open")
390
+ status_suffix = f" · {_NODE_STATUS_LABEL.get(status, status.upper())}"
391
+ title_limit = max(
392
+ 1,
393
+ _SVG_NODE_HEADER_DISPLAY_WIDTH - _display_width(status_suffix),
394
+ )
395
+ title = _truncate_display(
396
+ str(node.get("title") or node.get("node_id") or "untitled"),
397
+ limit=title_limit,
398
+ )
399
+ return [
400
+ f"{title}{status_suffix}",
401
+ *_node_decision_lines(
402
+ node,
403
+ detail_limit=_SVG_NODE_DETAIL_DISPLAY_WIDTH,
404
+ ),
405
+ ][:3]
406
+
407
+
408
+ def _node_detail_coverage(nodes: Sequence[Mapping[str, Any]]) -> dict[str, Any]:
409
+ summary_nodes = [node for node in nodes if str(node.get("summary") or "").strip()]
410
+ rendered = [node for node in summary_nodes if _node_decision_lines(node, detail_limit=112)]
411
+ metric_nodes = [
412
+ node for node in summary_nodes if _METRIC_SIGNAL.search(str(node.get("summary") or ""))
413
+ ]
414
+ rendered_metric_nodes = [
415
+ node
416
+ for node in metric_nodes
417
+ if _METRIC_SIGNAL.search(" ".join(_node_decision_lines(node, detail_limit=112)))
418
+ ]
419
+ complete = len(rendered) == len(summary_nodes) and len(rendered_metric_nodes) == len(metric_nodes)
420
+ return {
421
+ "summary_eligible_node_count": len(summary_nodes),
422
+ "summary_rendered_node_count": len(rendered),
423
+ "metric_eligible_node_count": len(metric_nodes),
424
+ "metric_rendered_node_count": len(rendered_metric_nodes),
425
+ "complete": complete,
426
+ }
427
+
428
+
429
+ def _canonical_group_specs(
430
+ nodes: Sequence[Mapping[str, Any]],
431
+ *,
432
+ group_node_limit: int,
433
+ ) -> list[dict[str, Any]]:
434
+ """Split stable source order into balanced bounded evidence stages."""
435
+
436
+ node_by_id = {str(node.get("node_id") or ""): node for node in nodes}
437
+ ordered_ids = list(node_by_id)
438
+ if not ordered_ids:
439
+ return []
440
+ limit = max(1, int(group_node_limit))
441
+ group_count = (len(ordered_ids) + limit - 1) // limit
442
+ base, extra = divmod(len(ordered_ids), group_count)
443
+ specs = []
444
+ offset = 0
445
+ for index in range(group_count):
446
+ size = base + (1 if index < extra else 0)
447
+ chunk = ordered_ids[offset : offset + size]
448
+ offset += size
449
+ first_title = str(node_by_id[chunk[0]].get("title") or chunk[0])
450
+ specs.append(
451
+ {
452
+ "title": (
453
+ f"Evidence stage {index + 1:02d} · "
454
+ f"{first_title}"
455
+ ),
456
+ "node_ids": chunk,
457
+ "order": offset - size,
458
+ }
459
+ )
460
+ return specs
461
+
462
+
463
+ def _explore_lane(
464
+ node: Mapping[str, Any],
465
+ *,
466
+ node_by_id: Mapping[str, Mapping[str, Any]] | None = None,
467
+ ) -> str:
468
+ """Return a generic lane id, inheriting explicit lane tags from lineage.
469
+
470
+ Projects can define any number of lanes with ``lane-<name>`` tags on a
471
+ node or one of its ancestors. The id fallbacks preserve useful behavior
472
+ for existing issue-fix projections that predate explicit lane tags.
473
+ """
474
+
475
+ direct_tags = {
476
+ str(tag).strip().lower()
477
+ for tag in node.get("tags") or []
478
+ if str(tag).strip()
479
+ }
480
+ direct_lanes = sorted(
481
+ tag for tag in direct_tags if tag.startswith("lane-") and len(tag) > 5
482
+ )
483
+ if direct_lanes:
484
+ return direct_lanes[0].removeprefix("lane-").replace("-", "_")
485
+ node_id = str(node.get("node_id") or "").lower()
486
+ if node_id.startswith("fix_") or direct_tags.intersection({"fix-pr", "fix_pr"}):
487
+ return "fix_pr"
488
+ if (
489
+ node_id.startswith("cap_")
490
+ or node_id.startswith("capability")
491
+ or "capability" in direct_tags
492
+ ):
493
+ return "capability"
494
+
495
+ ancestors = []
496
+ if node_by_id:
497
+ ancestors = [
498
+ node_by_id[str(ancestor_id)]
499
+ for ancestor_id in node.get("lineage") or []
500
+ if str(ancestor_id) in node_by_id
501
+ and str(ancestor_id) != str(node.get("node_id") or "")
502
+ ]
503
+ for candidate in reversed(ancestors):
504
+ tags = {
505
+ str(tag).strip().lower()
506
+ for tag in candidate.get("tags") or []
507
+ if str(tag).strip()
508
+ }
509
+ explicit_lanes = sorted(
510
+ tag for tag in tags if tag.startswith("lane-") and len(tag) > 5
511
+ )
512
+ if explicit_lanes:
513
+ return explicit_lanes[0].removeprefix("lane-").replace("-", "_")
514
+ for candidate in reversed(ancestors):
515
+ node_id = str(candidate.get("node_id") or "").lower()
516
+ tags = {
517
+ str(tag).strip().lower()
518
+ for tag in candidate.get("tags") or []
519
+ if str(tag).strip()
520
+ }
521
+ if node_id.startswith("fix_") or tags.intersection({"fix-pr", "fix_pr"}):
522
+ return "fix_pr"
523
+ if (
524
+ node_id.startswith("cap_")
525
+ or node_id.startswith("capability")
526
+ or "capability" in tags
527
+ ):
528
+ return "capability"
529
+ return "default"
530
+
531
+
532
+ def build_vertical_explore_mermaid(
533
+ nodes: Sequence[Mapping[str, Any]],
534
+ edges: Sequence[Mapping[str, Any]],
535
+ *,
536
+ view_role: str,
537
+ group_node_limit: int = 14,
538
+ ) -> dict[str, Any]:
539
+ """Render bounded Evidence Stage views over the canonical graph."""
540
+
541
+ all_nodes = [node for node in nodes if str(node.get("node_id") or "")]
542
+ node_list = [
543
+ node for node in all_nodes if str(node.get("node_kind") or "") != "area"
544
+ ] or all_nodes
545
+ group_limit = max(10, min(20, int(group_node_limit)))
546
+ primary_limit = group_limit - 2
547
+ groups = _canonical_group_specs(node_list, group_node_limit=primary_limit)
548
+ role = "executive" if view_role == "executive" else "canonical"
549
+ timeline_title = f"{role.title()} evidence timeline"
550
+
551
+ node_by_id = {str(node.get("node_id") or ""): node for node in all_nodes}
552
+ shown_ids = {str(node.get("node_id") or "") for node in node_list}
553
+ global_lanes = {
554
+ _explore_lane(node, node_by_id=node_by_id) for node in node_list
555
+ }
556
+ detail_coverage = _node_detail_coverage(node_list)
557
+ if not detail_coverage["complete"]:
558
+ raise ValueError("Explore node detail projection lost summary or metric evidence")
559
+ flow_direction = "TB"
560
+ lines = [
561
+ f"flowchart {flow_direction}",
562
+ f' subgraph {role}_timeline["{timeline_title}"]',
563
+ ]
564
+ lines.append(f" direction {flow_direction}")
565
+ group_chains = []
566
+ stage_views = []
567
+ for group_index, group in enumerate(groups, start=1):
568
+ group_id = f"{role}_group_{group_index}"
569
+ lines.append(f' subgraph {group_id}["{_mermaid_label(group["title"])}"]')
570
+ lines.append(" direction TB")
571
+ chain = []
572
+ for node_id in group["node_ids"]:
573
+ node = node_by_id[node_id]
574
+ status = str(node.get("status") or "open")
575
+ label = "<br/>".join(
576
+ _mermaid_label(line, limit=112)
577
+ for line in _node_display_lines(
578
+ node,
579
+ title_limit=64,
580
+ detail_limit=112,
581
+ )
582
+ )
583
+ mermaid_id = _mermaid_id(node_id)
584
+ chain.append(mermaid_id)
585
+ lines.append(
586
+ f' {mermaid_id}["{label}"]:::{_MERMAID_STATUS_CLASS.get(status, "open")}'
587
+ )
588
+ if len(chain) > 1:
589
+ lines.append(f" {' ~~~ '.join(chain)}")
590
+ lines.append(" end")
591
+ group_chains.append(chain)
592
+ primary_node_ids = list(group["node_ids"])
593
+ stage_node_order = list(primary_node_ids)
594
+ stage_node_ids = set(primary_node_ids)
595
+ relation_context = []
596
+ for edge in edges:
597
+ source = str(edge.get("from_node") or "")
598
+ target = str(edge.get("to_node") or "")
599
+ if source in stage_node_ids and target not in stage_node_ids:
600
+ local_id, candidate_id = source, target
601
+ elif target in stage_node_ids and source not in stage_node_ids:
602
+ local_id, candidate_id = target, source
603
+ else:
604
+ continue
605
+ candidate = node_by_id.get(candidate_id)
606
+ local = node_by_id.get(local_id)
607
+ if not candidate or not local:
608
+ continue
609
+ if str(candidate.get("node_kind") or "") == "area":
610
+ continue
611
+ if _explore_lane(candidate, node_by_id=node_by_id) == _explore_lane(
612
+ local,
613
+ node_by_id=node_by_id,
614
+ ):
615
+ continue
616
+ relation_context.append(candidate_id)
617
+ for candidate_id in relation_context:
618
+ if len(stage_node_order) >= group_limit:
619
+ break
620
+ if candidate_id in stage_node_ids:
621
+ continue
622
+ stage_node_order.append(candidate_id)
623
+ stage_node_ids.add(candidate_id)
624
+ present_lanes = {
625
+ _explore_lane(node_by_id[node_id], node_by_id=node_by_id)
626
+ for node_id in stage_node_order
627
+ }
628
+ for missing_lane in sorted(global_lanes - present_lanes):
629
+ if len(stage_node_order) >= group_limit:
630
+ break
631
+ candidate_id = next(
632
+ (
633
+ str(candidate.get("node_id") or "")
634
+ for candidate in node_list
635
+ if str(candidate.get("node_id") or "") not in stage_node_ids
636
+ and _explore_lane(candidate, node_by_id=node_by_id)
637
+ == missing_lane
638
+ ),
639
+ None,
640
+ )
641
+ if candidate_id:
642
+ stage_node_order.append(candidate_id)
643
+ stage_node_ids.add(candidate_id)
644
+ stage_edges = [
645
+ edge
646
+ for edge in edges
647
+ if str(edge.get("from_node") or "") in stage_node_ids
648
+ and str(edge.get("to_node") or "") in stage_node_ids
649
+ ]
650
+ stage_lanes: dict[str, list[str]] = defaultdict(list)
651
+ for node_id in stage_node_order:
652
+ stage_lanes[
653
+ _explore_lane(node_by_id[node_id], node_by_id=node_by_id)
654
+ ].append(node_id)
655
+ stage_lines = [
656
+ "flowchart TB",
657
+ f' subgraph {role}_stage_{group_index}["{_mermaid_label(group["title"])}"]',
658
+ " direction LR" if len(stage_lanes) > 1 else " direction TB",
659
+ ]
660
+ for lane_index, (lane, lane_node_ids) in enumerate(stage_lanes.items(), start=1):
661
+ lane_id = f"{role}_stage_{group_index}_lane_{lane_index}"
662
+ lane_title = {
663
+ "fix_pr": "PR issue-fix",
664
+ "capability": "LoopX capability",
665
+ }.get(lane, lane.replace("_", " ").title())
666
+ stage_lines.append(
667
+ f' subgraph {lane_id}["{_mermaid_label(lane_title)}"]'
668
+ )
669
+ stage_lines.append(" direction TB")
670
+ lane_chain = []
671
+ for node_id in lane_node_ids:
672
+ node = node_by_id[node_id]
673
+ status = str(node.get("status") or "open")
674
+ label = "<br/>".join(
675
+ _mermaid_label(line, limit=96)
676
+ for line in _node_display_lines(
677
+ node,
678
+ title_limit=56,
679
+ detail_limit=88,
680
+ )
681
+ )
682
+ stage_lines.append(
683
+ f' {_mermaid_id(node_id)}["{label}"]:::{_MERMAID_STATUS_CLASS.get(status, "open")}'
684
+ )
685
+ lane_chain.append(_mermaid_id(node_id))
686
+ if len(lane_chain) > 1:
687
+ stage_lines.append(f" {' ~~~ '.join(lane_chain)}")
688
+ stage_lines.append(" end")
689
+ stage_lines.append(" end")
690
+ for edge in stage_edges:
691
+ source = str(edge.get("from_node") or "")
692
+ target = str(edge.get("to_node") or "")
693
+ label = _mermaid_label(str(edge.get("edge_type") or ""))
694
+ stage_lines.append(
695
+ f" {_mermaid_id(source)} -->|{label}| {_mermaid_id(target)}"
696
+ )
697
+ stage_lines.append(
698
+ f" style {role}_stage_{group_index} fill:#ffffff,stroke:#90a4ae,stroke-width:2px"
699
+ )
700
+ stage_lines.extend(
701
+ [
702
+ " classDef open fill:#f5f5f5,stroke:#9e9e9e",
703
+ " classDef exploring fill:#e3f2fd,stroke:#1e88e5",
704
+ " classDef blocked fill:#ffebee,stroke:#e53935,stroke-width:2px",
705
+ " classDef resolved fill:#e8f5e9,stroke:#43a047",
706
+ " classDef deadend fill:#eeeeee,stroke:#9e9e9e,stroke-dasharray: 4 4",
707
+ ]
708
+ )
709
+ incoming_edges = [
710
+ edge
711
+ for edge in edges
712
+ if str(edge.get("to_node") or "") in stage_node_ids
713
+ and str(edge.get("from_node") or "") not in stage_node_ids
714
+ ]
715
+ outgoing_edges = [
716
+ edge
717
+ for edge in edges
718
+ if str(edge.get("from_node") or "") in stage_node_ids
719
+ and str(edge.get("to_node") or "") not in stage_node_ids
720
+ ]
721
+ stage_svg = build_stage_lane_svg(
722
+ title=group["title"],
723
+ node_order=stage_node_order,
724
+ node_by_id=node_by_id,
725
+ edges=stage_edges,
726
+ )
727
+ stage_views.append(
728
+ {
729
+ "stage_index": group_index,
730
+ "title": group["title"],
731
+ "node_ids": stage_node_order,
732
+ "primary_node_ids": primary_node_ids,
733
+ "context_node_ids": stage_node_order[len(primary_node_ids) :],
734
+ "node_count": len(stage_node_order),
735
+ "primary_node_count": len(primary_node_ids),
736
+ "context_node_count": len(stage_node_order) - len(primary_node_ids),
737
+ "fix_pr_node_count": sum(
738
+ 1
739
+ for node_id in stage_node_order
740
+ if _explore_lane(node_by_id[node_id], node_by_id=node_by_id)
741
+ == "fix_pr"
742
+ ),
743
+ "capability_node_count": sum(
744
+ 1
745
+ for node_id in stage_node_order
746
+ if _explore_lane(node_by_id[node_id], node_by_id=node_by_id)
747
+ == "capability"
748
+ ),
749
+ "lanes": sorted(
750
+ {
751
+ _explore_lane(node_by_id[node_id], node_by_id=node_by_id)
752
+ for node_id in stage_node_order
753
+ }
754
+ ),
755
+ "lane_count": len(
756
+ {
757
+ _explore_lane(node_by_id[node_id], node_by_id=node_by_id)
758
+ for node_id in stage_node_order
759
+ }
760
+ ),
761
+ "cross_lane_edge_count": sum(
762
+ 1
763
+ for edge in stage_edges
764
+ if _explore_lane(
765
+ node_by_id[str(edge.get("from_node") or "")],
766
+ node_by_id=node_by_id,
767
+ )
768
+ != _explore_lane(
769
+ node_by_id[str(edge.get("to_node") or "")],
770
+ node_by_id=node_by_id,
771
+ )
772
+ ),
773
+ "internal_edge_count": len(stage_edges),
774
+ "incoming_edge_count": len(incoming_edges),
775
+ "outgoing_edge_count": len(outgoing_edges),
776
+ "mermaid": "\n".join(stage_lines),
777
+ "svg": stage_svg,
778
+ "svg_layout": {
779
+ "strategy": "semantic_lane_columns",
780
+ "lane_order": [
781
+ lane
782
+ for lane in ("fix_pr", "capability")
783
+ if lane in stage_lanes
784
+ ]
785
+ + sorted(
786
+ lane
787
+ for lane in stage_lanes
788
+ if lane not in {"fix_pr", "capability"}
789
+ ),
790
+ "orientation": "left_to_right_lanes_top_to_bottom_nodes",
791
+ },
792
+ }
793
+ )
794
+ for previous, current in zip(group_chains, group_chains[1:]):
795
+ lines.append(f" {previous[-1]} ~~~ {current[0]}")
796
+ lines.append(" end")
797
+ for edge in edges:
798
+ source = str(edge.get("from_node") or "")
799
+ target = str(edge.get("to_node") or "")
800
+ if source not in shown_ids or target not in shown_ids:
801
+ continue
802
+ label = _mermaid_label(str(edge.get("edge_type") or ""))
803
+ lines.append(f" {_mermaid_id(source)} -->|{label}| {_mermaid_id(target)}")
804
+ lines.append(
805
+ f" style {role}_timeline fill:#ffffff,stroke:#90a4ae,stroke-width:2px"
806
+ )
807
+ for group_index in range(1, len(groups) + 1):
808
+ lines.append(
809
+ f" style {role}_group_{group_index} fill:#fafafa,stroke:#cfd8dc"
810
+ )
811
+ lines.extend(
812
+ [
813
+ " classDef open fill:#f5f5f5,stroke:#9e9e9e",
814
+ " classDef exploring fill:#e3f2fd,stroke:#1e88e5",
815
+ " classDef blocked fill:#ffebee,stroke:#e53935,stroke-width:2px",
816
+ " classDef resolved fill:#e8f5e9,stroke:#43a047",
817
+ " classDef deadend fill:#eeeeee,stroke:#9e9e9e,stroke-dasharray: 4 4",
818
+ ]
819
+ )
820
+ return {
821
+ "mermaid": "\n".join(lines),
822
+ "strategy": "vertical_evidence_timeline",
823
+ "view_role": role,
824
+ "group_count": len(groups),
825
+ "column_count": 1,
826
+ "orientation": "top_to_bottom",
827
+ "max_group_node_count": group_limit,
828
+ "primary_group_node_count": primary_limit,
829
+ "stage_views": stage_views,
830
+ "node_detail_coverage": detail_coverage,
831
+ }
832
+
833
+
834
+ def _display_width(value: str) -> int:
835
+ return sum(2 if unicodedata.east_asian_width(character) in {"W", "F"} else 1 for character in value)
836
+
837
+
838
+ def _truncate_display(value: str, *, limit: int) -> str:
839
+ cleaned = re.sub(r"\s+", " ", str(value or "")).strip()
840
+ if _display_width(cleaned) <= limit:
841
+ return cleaned
842
+ result = []
843
+ width = 0
844
+ for character in cleaned:
845
+ character_width = 2 if unicodedata.east_asian_width(character) in {"W", "F"} else 1
846
+ if width + character_width > max(1, limit - 1):
847
+ break
848
+ result.append(character)
849
+ width += character_width
850
+ truncated = "".join(result).rstrip()
851
+ if (
852
+ truncated
853
+ and len(truncated) < len(cleaned)
854
+ and truncated[-1].isascii()
855
+ and truncated[-1].isalnum()
856
+ and cleaned[len(truncated)].isascii()
857
+ and cleaned[len(truncated)].isalnum()
858
+ ):
859
+ boundary = truncated.rfind(" ")
860
+ if boundary >= max(1, len(truncated) // 2):
861
+ truncated = truncated[:boundary].rstrip()
862
+ return truncated + "…"
863
+
864
+
865
+ def _tail_display(value: str, *, limit: int) -> str:
866
+ cleaned = re.sub(r"\s+", " ", str(value or "")).strip()
867
+ if _display_width(cleaned) <= limit:
868
+ return cleaned
869
+ result = []
870
+ width = 0
871
+ for character in reversed(cleaned):
872
+ character_width = 2 if unicodedata.east_asian_width(character) in {"W", "F"} else 1
873
+ if width + character_width > limit:
874
+ break
875
+ result.append(character)
876
+ width += character_width
877
+ tail = "".join(reversed(result)).lstrip()
878
+ if tail and tail[0].isascii() and tail[0].isalnum():
879
+ boundary = tail.find(" ")
880
+ if 0 < boundary <= len(tail) // 2:
881
+ tail = tail[boundary + 1 :].lstrip()
882
+ return tail
883
+
884
+
885
+ def _tags(node: Mapping[str, Any]) -> set[str]:
886
+ return {str(tag or "").strip().lower() for tag in node.get("tags") or [] if str(tag or "").strip()}
887
+
888
+
889
+ def _decision_seed_ids(nodes: Sequence[Mapping[str, Any]]) -> set[str]:
890
+ seeds = set()
891
+ has_current_incumbent = any(
892
+ _tags(node).intersection({"current-best", "incumbent"})
893
+ for node in nodes
894
+ )
895
+ for node in nodes:
896
+ node_id = str(node.get("node_id") or "")
897
+ if not node_id:
898
+ continue
899
+ tags = _tags(node)
900
+ if (
901
+ str(node.get("status") or "") in _ACTIVE_STATUSES
902
+ or tags.intersection(_DECISION_TAGS)
903
+ or (not has_current_incumbent and tags.intersection(_LEGACY_LEADER_TAGS))
904
+ ):
905
+ seeds.add(node_id)
906
+ return seeds
907
+
908
+
909
+ def _terminal_neighborhood_count(
910
+ nodes: Sequence[Mapping[str, Any]],
911
+ parents: Mapping[str, str],
912
+ ) -> int:
913
+ statuses = {
914
+ str(node.get("node_id") or ""): str(node.get("status") or "")
915
+ for node in nodes
916
+ }
917
+ children: dict[str, list[str]] = defaultdict(list)
918
+ for child, parent in parents.items():
919
+ children[parent].append(child)
920
+ return sum(
921
+ 1
922
+ for child_ids in children.values()
923
+ if len(child_ids) >= 3
924
+ and sum(statuses.get(child) in _TERMINAL_STATUSES for child in child_ids) >= 3
925
+ )
926
+
927
+
928
+ def assess_explore_presentation(
929
+ projection: Mapping[str, Any],
930
+ *,
931
+ readability_check: Mapping[str, Any] | None = None,
932
+ policy: Mapping[str, float | int] | None = None,
933
+ ) -> dict[str, Any]:
934
+ """Recommend one or two views from several advisory readability signals."""
935
+
936
+ thresholds = dict(DEFAULT_EXPLORE_PRESENTATION_POLICY)
937
+ supplied_policy = dict(policy or {})
938
+ if (
939
+ "atlas_group_node_limit" in supplied_policy
940
+ and "stage_node_capacity" not in supplied_policy
941
+ ):
942
+ supplied_policy["stage_node_capacity"] = supplied_policy.pop(
943
+ "atlas_group_node_limit"
944
+ )
945
+ thresholds.update(supplied_policy)
946
+ nodes = [item for item in projection.get("nodes") or [] if isinstance(item, Mapping)]
947
+ edges = [item for item in projection.get("edges") or [] if isinstance(item, Mapping)]
948
+ node_ids = {str(node.get("node_id") or "") for node in nodes if str(node.get("node_id") or "")}
949
+ parents = _parent_map(nodes, edges)
950
+ depths = _node_depths(node_ids, parents)
951
+ decision_ids = _decision_seed_ids(nodes)
952
+ terminal_count = sum(str(node.get("status") or "") in _TERMINAL_STATUSES for node in nodes)
953
+ node_count = len(nodes)
954
+ edge_count = len(edges)
955
+ decision_density = len(decision_ids) / node_count if node_count else 1.0
956
+ terminal_ratio = terminal_count / node_count if node_count else 0.0
957
+ edge_density = edge_count / node_count if node_count else 0.0
958
+ max_depth = max(depths.values(), default=0)
959
+ root_count = sum(not parents.get(node_id) for node_id in node_ids)
960
+ root_ratio = root_count / node_count if node_count else 0.0
961
+ terminal_neighborhoods = _terminal_neighborhood_count(nodes, parents)
962
+ long_label_count = sum(
963
+ len(str(node.get("title") or "")) > int(thresholds["readability_label_chars"])
964
+ for node in nodes
965
+ )
966
+
967
+ reasons = []
968
+ if (
969
+ node_count >= int(thresholds["decision_density_node_floor"])
970
+ and decision_density < float(thresholds["decision_density_ceiling"])
971
+ ):
972
+ reasons.append("low_decision_density")
973
+ if (
974
+ terminal_neighborhoods >= int(thresholds["terminal_neighborhood_floor"])
975
+ or (
976
+ node_count >= int(thresholds["terminal_ratio_node_floor"])
977
+ and terminal_ratio >= float(thresholds["terminal_ratio_floor"])
978
+ )
979
+ ):
980
+ reasons.append("excessive_terminal_branches")
981
+ if max_depth >= int(thresholds["decision_depth_floor"]):
982
+ reasons.append("deep_decision_path")
983
+
984
+ observed_readability_failure = any(
985
+ readability_check and readability_check.get(key) is True
986
+ for key in (
987
+ "overlap_detected",
988
+ "text_overflow_detected",
989
+ "canvas_expansion_detected",
990
+ )
991
+ )
992
+ estimated_readability_failure = (
993
+ node_count >= int(thresholds["readability_node_floor"])
994
+ and (
995
+ edge_density >= float(thresholds["readability_edge_density_floor"])
996
+ or long_label_count > 0
997
+ or max_depth >= int(thresholds["decision_depth_floor"])
998
+ or (
999
+ root_count >= int(thresholds["readability_root_count_floor"])
1000
+ and root_ratio >= float(thresholds["readability_root_ratio_floor"])
1001
+ )
1002
+ )
1003
+ )
1004
+ if observed_readability_failure or estimated_readability_failure:
1005
+ reasons.append("readability_check_failed")
1006
+
1007
+ mode = (
1008
+ PRESENTATION_MODE_DUAL_VIEW
1009
+ if "readability_check_failed" in reasons or len(reasons) >= 2
1010
+ else PRESENTATION_MODE_CANONICAL_ONLY
1011
+ )
1012
+ return {
1013
+ "schema_version": EXPLORE_PRESENTATION_ASSESSMENT_VERSION,
1014
+ "presentation_mode": mode,
1015
+ "reason_codes": reasons,
1016
+ "metrics": {
1017
+ "node_count": node_count,
1018
+ "edge_count": edge_count,
1019
+ "decision_node_count": len(decision_ids),
1020
+ "decision_density": round(decision_density, 4),
1021
+ "terminal_node_count": terminal_count,
1022
+ "terminal_ratio": round(terminal_ratio, 4),
1023
+ "terminal_neighborhood_count": terminal_neighborhoods,
1024
+ "max_depth": max_depth,
1025
+ "root_node_count": root_count,
1026
+ "root_ratio": round(root_ratio, 4),
1027
+ "edge_density": round(edge_density, 4),
1028
+ "long_label_count": long_label_count,
1029
+ },
1030
+ "readability_check": {
1031
+ "source": "observed" if readability_check else "static_estimate",
1032
+ "failed": observed_readability_failure or estimated_readability_failure,
1033
+ },
1034
+ "policy": thresholds,
1035
+ "advisory_only": True,
1036
+ "canonical_truncation_allowed": False,
1037
+ }
1038
+
1039
+
1040
+ def _executive_roles(node: Mapping[str, Any]) -> list[str]:
1041
+ roles = []
1042
+ status = str(node.get("status") or "")
1043
+ tags = _tags(node)
1044
+ if status in _ACTIVE_STATUSES:
1045
+ roles.append("active_work")
1046
+ for role, role_tags in (
1047
+ ("decision_contract", {"contract", "decision"}),
1048
+ ("baseline", {"baseline"}),
1049
+ (
1050
+ "incumbent",
1051
+ {"current-best", "incumbent", "leader", "provisional-leader", "winner"},
1052
+ ),
1053
+ ("guardrail", {"guardrail", "risk"}),
1054
+ ("resource_state", {"capacity", "resource"}),
1055
+ ("counterevidence", {"counterevidence", "negative", "no-promote", "retired"}),
1056
+ ):
1057
+ if tags.intersection(role_tags):
1058
+ roles.append(role)
1059
+ if status == "dead_end" and "counterevidence" not in roles:
1060
+ roles.append("counterevidence")
1061
+ return roles or ["lineage_context"]
1062
+
1063
+
1064
+ def _executive_node_ids(
1065
+ nodes: Sequence[Mapping[str, Any]],
1066
+ edges: Sequence[Mapping[str, Any]],
1067
+ parents: Mapping[str, str],
1068
+ *,
1069
+ counterevidence_limit: int,
1070
+ ) -> set[str]:
1071
+ node_ids = {str(node.get("node_id") or "") for node in nodes if str(node.get("node_id") or "")}
1072
+ selected = _decision_seed_ids(nodes)
1073
+ if not selected:
1074
+ selected = {node_id for node_id in node_ids if not parents.get(node_id)}
1075
+ counterevidence_by_parent: dict[str, str] = {}
1076
+ for node in nodes:
1077
+ node_id = str(node.get("node_id") or "")
1078
+ status = str(node.get("status") or "")
1079
+ if not node_id or not (
1080
+ status == "dead_end" or _tags(node).intersection(_COUNTEREVIDENCE_TAGS)
1081
+ ):
1082
+ continue
1083
+ neighborhood = parents.get(node_id) or node_id
1084
+ counterevidence_by_parent.setdefault(neighborhood, node_id)
1085
+ selected.update(
1086
+ list(counterevidence_by_parent.values())[: max(0, counterevidence_limit)]
1087
+ )
1088
+ relation_seeds = set(selected)
1089
+ for edge in edges:
1090
+ if str(edge.get("edge_type") or "") not in _EXECUTIVE_EXPANSION_EDGE_TYPES:
1091
+ continue
1092
+ source = str(edge.get("from_node") or "")
1093
+ target = str(edge.get("to_node") or "")
1094
+ if source in relation_seeds or target in relation_seeds:
1095
+ selected.update({source, target}.intersection(node_ids))
1096
+ for node_id in tuple(selected):
1097
+ selected.update(_lineage(node_id, parents, node_ids))
1098
+ return selected
1099
+
1100
+
1101
+ def _view_node(
1102
+ node: Mapping[str, Any],
1103
+ *,
1104
+ parents: Mapping[str, str],
1105
+ node_ids: set[str],
1106
+ executive: bool,
1107
+ ) -> dict[str, Any]:
1108
+ view = dict(node)
1109
+ node_id = str(node.get("node_id") or "")
1110
+ view["source_node_id"] = node_id
1111
+ view["lineage"] = _lineage(node_id, parents, node_ids)
1112
+ if executive:
1113
+ view["executive_roles"] = _executive_roles(node)
1114
+ return view
1115
+
1116
+
1117
+ def _executive_edge_projection(
1118
+ edges: Sequence[Mapping[str, Any]],
1119
+ executive_ids: set[str],
1120
+ *,
1121
+ hub_degree_floor: int,
1122
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
1123
+ """Hide lineage and dense hub scaffolding that the executive layout already conveys."""
1124
+
1125
+ candidates = [
1126
+ edge
1127
+ for edge in edges
1128
+ if str(edge.get("from_node") or "") in executive_ids
1129
+ and str(edge.get("to_node") or "") in executive_ids
1130
+ ]
1131
+ outgoing_counts: dict[str, int] = defaultdict(int)
1132
+ for edge in candidates:
1133
+ outgoing_counts[str(edge.get("from_node") or "")] += 1
1134
+
1135
+ visible = []
1136
+ suppressed = []
1137
+ degree_floor = max(2, int(hub_degree_floor))
1138
+ for edge in candidates:
1139
+ edge_type = str(edge.get("edge_type") or "")
1140
+ source = str(edge.get("from_node") or "")
1141
+ suppression_reason = None
1142
+ if edge_type == "subtopic_of":
1143
+ suppression_reason = "lineage_encoded_on_node"
1144
+ elif edge_type == "supports" and outgoing_counts[source] >= degree_floor:
1145
+ suppression_reason = "dense_hub_scaffolding"
1146
+ projected = dict(edge, source_edge_id=str(edge.get("edge_id") or ""))
1147
+ if suppression_reason:
1148
+ suppressed.append(dict(projected, suppression_reason=suppression_reason))
1149
+ else:
1150
+ visible.append(projected)
1151
+ return visible, suppressed
1152
+
1153
+
1154
+ def build_explore_presentation_bundle(
1155
+ projection: Mapping[str, Any],
1156
+ *,
1157
+ readability_check: Mapping[str, Any] | None = None,
1158
+ policy: Mapping[str, float | int] | None = None,
1159
+ ) -> dict[str, Any]:
1160
+ """Build canonical and executive views atomically from one projection."""
1161
+
1162
+ nodes = [dict(item) for item in projection.get("nodes") or [] if isinstance(item, Mapping)]
1163
+ edges = [dict(item) for item in projection.get("edges") or [] if isinstance(item, Mapping)]
1164
+ findings = [dict(item) for item in projection.get("findings") or [] if isinstance(item, Mapping)]
1165
+ counts = projection.get("counts")
1166
+ declared_finding_count = (
1167
+ int(counts.get("finding_count") or 0)
1168
+ if isinstance(counts, Mapping)
1169
+ else len(findings)
1170
+ )
1171
+ if declared_finding_count > len(findings):
1172
+ raise ValueError(
1173
+ "Explore presentation requires the complete canonical finding set"
1174
+ )
1175
+ node_ids = {str(node.get("node_id") or "") for node in nodes if str(node.get("node_id") or "")}
1176
+ parents = _parent_map(nodes, edges)
1177
+ digest = explore_source_digest(projection)
1178
+ revision = explore_source_revision(projection, digest=digest)
1179
+ assessment = assess_explore_presentation(
1180
+ projection,
1181
+ readability_check=readability_check,
1182
+ policy=policy,
1183
+ )
1184
+
1185
+ canonical_nodes = [
1186
+ _view_node(node, parents=parents, node_ids=node_ids, executive=False)
1187
+ for node in nodes
1188
+ ]
1189
+ canonical_edges = [dict(edge, source_edge_id=str(edge.get("edge_id") or "")) for edge in edges]
1190
+ thresholds = dict(DEFAULT_EXPLORE_PRESENTATION_POLICY)
1191
+ thresholds.update(policy or {})
1192
+ canonical_layout = build_vertical_explore_mermaid(
1193
+ canonical_nodes,
1194
+ canonical_edges,
1195
+ view_role="canonical",
1196
+ group_node_limit=int(thresholds["stage_node_capacity"]),
1197
+ )
1198
+ canonical = {
1199
+ "schema_version": EXPLORE_CANONICAL_VIEW_VERSION,
1200
+ "goal_id": projection.get("goal_id"),
1201
+ "view_role": "canonical",
1202
+ "source_revision": revision,
1203
+ "source_digest": digest,
1204
+ "nodes": canonical_nodes,
1205
+ "edges": canonical_edges,
1206
+ "findings": findings,
1207
+ "mermaid": canonical_layout["mermaid"],
1208
+ "stage_views": canonical_layout["stage_views"],
1209
+ "graph_counts": {
1210
+ "node_count": len(canonical_nodes),
1211
+ "edge_count": len(canonical_edges),
1212
+ "finding_count": len(findings),
1213
+ },
1214
+ "filter": {
1215
+ "projection_mode": "canonical_full",
1216
+ "truncated": False,
1217
+ "layout": {
1218
+ key: value
1219
+ for key, value in canonical_layout.items()
1220
+ if key not in {"mermaid", "stage_views"}
1221
+ },
1222
+ },
1223
+ }
1224
+
1225
+ executive_ids = _executive_node_ids(
1226
+ nodes,
1227
+ edges,
1228
+ parents,
1229
+ counterevidence_limit=int(thresholds["executive_counterevidence_limit"]),
1230
+ )
1231
+ executive_nodes = [
1232
+ _view_node(node, parents=parents, node_ids=node_ids, executive=True)
1233
+ for node in nodes
1234
+ if str(node.get("node_id") or "") in executive_ids
1235
+ ]
1236
+ executive_edges, suppressed_executive_edges = _executive_edge_projection(
1237
+ edges,
1238
+ executive_ids,
1239
+ hub_degree_floor=int(thresholds["executive_hub_edge_degree_floor"]),
1240
+ )
1241
+ executive_layout = build_vertical_explore_mermaid(
1242
+ executive_nodes,
1243
+ executive_edges,
1244
+ view_role="executive",
1245
+ group_node_limit=int(thresholds["stage_node_capacity"]),
1246
+ )
1247
+ executive_findings = [
1248
+ finding
1249
+ for finding in findings
1250
+ if str(finding.get("node_id") or "") in executive_ids
1251
+ ]
1252
+ executive = {
1253
+ "schema_version": EXPLORE_EXECUTIVE_VIEW_VERSION,
1254
+ "goal_id": projection.get("goal_id"),
1255
+ "view_role": "executive",
1256
+ "source_revision": revision,
1257
+ "source_digest": digest,
1258
+ "source_node_ids": sorted(executive_ids),
1259
+ "nodes": executive_nodes,
1260
+ "edges": executive_edges,
1261
+ "findings": executive_findings,
1262
+ "mermaid": executive_layout["mermaid"],
1263
+ "stage_views": executive_layout["stage_views"],
1264
+ "graph_counts": {
1265
+ "node_count": len(executive_nodes),
1266
+ "edge_count": len(executive_edges),
1267
+ "canonical_edge_count": len(canonical_edges),
1268
+ "suppressed_edge_count": len(suppressed_executive_edges),
1269
+ "finding_count": len(executive_findings),
1270
+ "canonical_node_count": len(canonical_nodes),
1271
+ },
1272
+ "filter": {
1273
+ "projection_mode": "executive_auto",
1274
+ "selection": "active_or_tagged_plus_material_neighbors_and_lineage",
1275
+ "truncated": False,
1276
+ "edge_projection": {
1277
+ "selection": "material_edges_without_lineage_or_dense_hub_scaffolding",
1278
+ "suppressed_source_edge_ids": [
1279
+ str(edge.get("source_edge_id") or "")
1280
+ for edge in suppressed_executive_edges
1281
+ ],
1282
+ "suppression_counts": {
1283
+ reason: sum(
1284
+ 1
1285
+ for edge in suppressed_executive_edges
1286
+ if edge.get("suppression_reason") == reason
1287
+ )
1288
+ for reason in (
1289
+ "lineage_encoded_on_node",
1290
+ "dense_hub_scaffolding",
1291
+ )
1292
+ },
1293
+ },
1294
+ "layout": {
1295
+ key: value
1296
+ for key, value in executive_layout.items()
1297
+ if key not in {"mermaid", "stage_views"}
1298
+ },
1299
+ },
1300
+ }
1301
+ return {
1302
+ "ok": True,
1303
+ "schema_version": EXPLORE_PRESENTATION_BUNDLE_VERSION,
1304
+ "goal_id": projection.get("goal_id"),
1305
+ "presentation_mode": assessment["presentation_mode"],
1306
+ "reason_codes": assessment["reason_codes"],
1307
+ "source_revision": revision,
1308
+ "source_digest": digest,
1309
+ "assessment": assessment,
1310
+ "canonical": canonical,
1311
+ "executive": executive,
1312
+ }
1313
+
1314
+
1315
+ def validate_explore_view_freshness(
1316
+ projection: Mapping[str, Any],
1317
+ view: Mapping[str, Any],
1318
+ ) -> dict[str, Any]:
1319
+ """Fail closed when a derived display view is not from this projection."""
1320
+
1321
+ digest = explore_source_digest(projection)
1322
+ revision = explore_source_revision(projection, digest=digest)
1323
+ observed_digest = str(view.get("source_digest") or "")
1324
+ observed_revision = str(view.get("source_revision") or "")
1325
+ fresh = observed_digest == digest and observed_revision == revision
1326
+ return {
1327
+ "ok": fresh,
1328
+ "fresh": fresh,
1329
+ "expected_source_digest": digest,
1330
+ "observed_source_digest": observed_digest or None,
1331
+ "expected_source_revision": revision,
1332
+ "observed_source_revision": observed_revision or None,
1333
+ "reason": None if fresh else "derived Explore view is stale; rebuild it from the current canonical projection",
1334
+ }