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