okstra 0.186.7 → 0.187.0
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.
- package/README.md +1 -1
- package/dist/cli-registry.d.mts +88 -1
- package/dist/cli-registry.mjs +68 -111
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.mjs +0 -1
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/dist/commands/execute/run.mjs +8 -3
- package/dist/commands/execute/run.mjs.map +1 -1
- package/dist/commands/lifecycle/check-project.mjs +1 -14
- package/dist/commands/lifecycle/check-project.mjs.map +1 -1
- package/dist/commands/lifecycle/config.mjs +38 -40
- package/dist/commands/lifecycle/config.mjs.map +1 -1
- package/dist/commands/lifecycle/doctor.d.mts +22 -7
- package/dist/commands/lifecycle/doctor.mjs +77 -49
- package/dist/commands/lifecycle/doctor.mjs.map +1 -1
- package/dist/commands/lifecycle/install.d.mts +12 -10
- package/dist/commands/lifecycle/install.mjs +104 -39
- package/dist/commands/lifecycle/install.mjs.map +1 -1
- package/dist/commands/lifecycle/paths.mjs +8 -3
- package/dist/commands/lifecycle/paths.mjs.map +1 -1
- package/dist/commands/lifecycle/preflight.mjs +2 -1
- package/dist/commands/lifecycle/preflight.mjs.map +1 -1
- package/dist/commands/lifecycle/setup.mjs +22 -36
- package/dist/commands/lifecycle/setup.mjs.map +1 -1
- package/dist/commands/lifecycle/uninstall.d.mts +4 -2
- package/dist/commands/lifecycle/uninstall.mjs +59 -15
- package/dist/commands/lifecycle/uninstall.mjs.map +1 -1
- package/dist/commands/memory/memory.mjs +7 -1
- package/dist/commands/memory/memory.mjs.map +1 -1
- package/dist/lib/helper-scripts.d.mts +1 -1
- package/dist/lib/helper-scripts.mjs +10 -18
- package/dist/lib/helper-scripts.mjs.map +1 -1
- package/dist/lib/host-config.d.mts +72 -0
- package/dist/lib/host-config.mjs +404 -0
- package/dist/lib/host-config.mjs.map +1 -0
- package/dist/lib/host-registry-client.d.mts +2 -2
- package/dist/lib/host-registry-client.mjs +0 -3
- package/dist/lib/host-registry-client.mjs.map +1 -1
- package/dist/lib/install-assets.d.mts +1 -0
- package/dist/lib/install-assets.mjs +4 -0
- package/dist/lib/install-assets.mjs.map +1 -1
- package/dist/lib/proc.d.mts +2 -0
- package/dist/lib/proc.mjs +12 -0
- package/dist/lib/proc.mjs.map +1 -1
- package/dist/lib/python-command.d.mts +2 -0
- package/dist/lib/python-command.mjs +52 -0
- package/dist/lib/python-command.mjs.map +1 -0
- package/dist/lib/python-helper.d.mts +2 -5
- package/dist/lib/python-helper.mjs +3 -52
- package/dist/lib/python-helper.mjs.map +1 -1
- package/dist/lib/runtime-payload.d.mts +23 -0
- package/dist/lib/runtime-payload.mjs +57 -0
- package/dist/lib/runtime-payload.mjs.map +1 -0
- package/dist/lib/types.d.mts +24 -13
- package/docs/architecture/storage-model.md +21 -5
- package/docs/architecture.md +37 -36
- package/docs/cli.md +55 -76
- package/docs/coding-rules.md +295 -0
- package/docs/container.md +10 -36
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/for-ai/skills/okstra-code-review.md +0 -1
- package/docs/for-ai/skills/okstra-container-build.md +8 -41
- package/docs/for-ai/skills/okstra-inspect.md +4 -4
- package/docs/for-ai/skills/okstra-manager.md +2 -2
- package/docs/for-ai/skills/okstra-rollup.md +0 -1
- package/docs/for-ai/skills/okstra-run.md +3 -3
- package/docs/for-ai/skills/okstra-user-response.md +0 -1
- package/docs/performance-improvement-plan-v2.md +1 -1
- package/docs/project-structure-overview.md +62 -62
- package/docs/task-process/README.md +3 -3
- package/docs/task-process/common-flow.md +1 -1
- package/docs/task-process/error-analysis.md +3 -3
- package/docs/task-process/implementation-planning.md +3 -1
- package/docs/task-process/release-handoff.md +1 -1
- package/package.json +3 -2
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +6 -11
- package/runtime/agents/workers/report-writer-worker.md +1 -1
- package/runtime/bin/lib/okstra/cli.sh +4 -0
- package/runtime/bin/lib/okstra/globals.sh +2 -0
- package/runtime/bin/lib/okstra/interactive.sh +26 -173
- package/runtime/bin/lib/okstra/project-resolver.sh +23 -61
- package/runtime/bin/lib/okstra/usage.sh +3 -3
- package/runtime/bin/okstra-compact-reminder.sh +1 -5
- package/runtime/bin/okstra-error-log.py +19 -2
- package/runtime/bin/okstra-import-check.py +26 -0
- package/runtime/bin/okstra-inject-report-index.py +5 -4
- package/runtime/bin/okstra-provider-exec.py +22 -17
- package/runtime/bin/okstra-render-final-report.py +16 -6
- package/runtime/bin/okstra-render-report-views.py +25 -18
- package/runtime/bin/okstra-report-translate.py +30 -18
- package/runtime/bin/okstra-spawn-followups.py +4 -0
- package/runtime/bin/okstra-token-usage.py +3 -0
- package/runtime/bin/okstra.sh +4 -2
- package/runtime/bin/okstra_bootstrap.py +58 -0
- package/runtime/prompts/coding-preflight/overview.md +1 -1
- package/runtime/prompts/duties/planning-worker.md +1 -1
- package/runtime/prompts/launch.template.md +35 -9
- package/runtime/prompts/lead/convergence.md +96 -87
- package/runtime/prompts/lead/okstra-lead-contract.md +63 -12
- package/runtime/prompts/lead/plan-body-verification.md +65 -46
- package/runtime/prompts/lead/report-writer.md +34 -4
- package/runtime/prompts/lead/team-contract.md +4 -1
- package/runtime/prompts/profiles/_clarification-recommendation.md +2 -1
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +3 -3
- package/runtime/prompts/profiles/_coverage-critic.md +1 -1
- package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
- package/runtime/prompts/profiles/_implementation-executor.md +3 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +16 -2
- package/runtime/prompts/profiles/error-analysis.md +4 -3
- package/runtime/prompts/profiles/final-verification.md +2 -2
- package/runtime/prompts/profiles/implementation-planning.md +22 -17
- package/runtime/prompts/profiles/implementation.md +3 -2
- package/runtime/prompts/profiles/requirements-discovery.md +2 -1
- package/runtime/prompts/wizard/prompts.ko.json +29 -8
- package/runtime/python/okstra_ctl/__init__.py +6 -27
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -5
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +21 -2
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +3 -12
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +40 -22
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +14 -21
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +37 -20
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +86 -14
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +12 -12
- package/runtime/python/okstra_ctl/adapters/runtime/cmux.py +4 -7
- package/runtime/python/okstra_ctl/agent/__init__.py +1 -0
- package/runtime/python/okstra_ctl/{agent_activity.py → agent/activity.py} +12 -1
- package/runtime/python/okstra_ctl/{agent_invocation.py → agent/invocation.py} +34 -5
- package/runtime/python/okstra_ctl/agent/prompt_cli/__init__.py +19 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/__main__.py +11 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +215 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/dynamic_verifier.py +155 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/emit.py +66 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/inputs.py +138 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +408 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/results.py +197 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +107 -0
- package/runtime/python/okstra_ctl/analysis_packet.py +47 -4
- package/runtime/python/okstra_ctl/approval_decisions.py +260 -8
- package/runtime/python/okstra_ctl/assignment_resolver.py +0 -12
- package/runtime/python/okstra_ctl/attempt_evidence.py +12 -24
- package/runtime/python/okstra_ctl/blocking_checks.py +251 -0
- package/runtime/python/okstra_ctl/brief_frontmatter.py +0 -7
- package/runtime/python/okstra_ctl/clarification_items/__init__.py +102 -0
- package/runtime/python/okstra_ctl/clarification_items/carry.py +224 -0
- package/runtime/python/okstra_ctl/clarification_items/dispositions.py +135 -0
- package/runtime/python/okstra_ctl/clarification_items/parsing.py +253 -0
- package/runtime/python/okstra_ctl/clarification_items/rows.py +115 -0
- package/runtime/python/okstra_ctl/clarification_items/scan.py +215 -0
- package/runtime/python/okstra_ctl/clarification_items/sidecars.py +211 -0
- package/runtime/python/okstra_ctl/cmux.py +38 -31
- package/runtime/python/okstra_ctl/code_review_target.py +163 -1
- package/runtime/python/okstra_ctl/conformance.py +23 -0
- package/runtime/python/okstra_ctl/container.py +38 -421
- package/runtime/python/okstra_ctl/context_cost.py +16 -2
- package/runtime/python/okstra_ctl/contract_graph_cli.py +7 -0
- package/runtime/python/okstra_ctl/convergence.py +101 -15
- package/runtime/python/okstra_ctl/convergence_critic_prompt.py +345 -0
- package/runtime/python/okstra_ctl/convergence_engine.py +382 -70
- package/runtime/python/okstra_ctl/convergence_store.py +5 -29
- package/runtime/python/okstra_ctl/design_prep.py +108 -12
- package/runtime/python/okstra_ctl/design_snapshot.py +11 -1
- package/runtime/python/okstra_ctl/design_surfaces.py +29 -2
- package/runtime/python/okstra_ctl/dispatch_core.py +102 -25
- package/runtime/python/okstra_ctl/dispatch_state.py +268 -39
- package/runtime/python/okstra_ctl/doctor_cli.py +48 -0
- package/runtime/python/okstra_ctl/domain/worker_exec.py +10 -5
- package/runtime/python/okstra_ctl/domain/worker_presentation.py +21 -1
- package/runtime/python/okstra_ctl/domain/worker_stream.py +52 -21
- package/runtime/python/okstra_ctl/domain/write_policy.py +219 -0
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +9 -2
- package/runtime/python/okstra_ctl/error_log_core.py +1 -1
- package/runtime/python/okstra_ctl/error_report.py +14 -0
- package/runtime/python/okstra_ctl/error_zip.py +18 -2
- package/runtime/python/okstra_ctl/execution_identity.py +100 -7
- package/runtime/python/okstra_ctl/execution_manifest.py +67 -20
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +90 -8
- package/runtime/python/okstra_ctl/final_report_paths.py +17 -0
- package/runtime/python/okstra_ctl/final_report_schema.py +90 -16
- package/runtime/python/okstra_ctl/git_reconcile.py +22 -2
- package/runtime/python/okstra_ctl/handoff.py +24 -1
- package/runtime/python/okstra_ctl/ids.py +6 -16
- package/runtime/python/okstra_ctl/implementation_direction.py +142 -33
- package/runtime/python/okstra_ctl/implementation_options.py +25 -11
- package/runtime/python/okstra_ctl/implementation_outcome.py +5 -2
- package/runtime/python/okstra_ctl/improvement_lenses.py +0 -14
- package/runtime/python/okstra_ctl/incremental_carry.py +69 -8
- package/runtime/python/okstra_ctl/incremental_scope.py +173 -12
- package/runtime/python/okstra_ctl/index.py +2 -2
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +48 -8
- package/runtime/python/okstra_ctl/interactive_cli.py +223 -0
- package/runtime/python/okstra_ctl/json_boundary.py +10 -0
- package/runtime/python/okstra_ctl/listing.py +0 -91
- package/runtime/python/okstra_ctl/locks.py +5 -19
- package/runtime/python/okstra_ctl/log_report.py +14 -0
- package/runtime/python/okstra_ctl/manager_cli.py +22 -3
- package/runtime/python/okstra_ctl/manager_launch.py +4 -8
- package/runtime/python/okstra_ctl/material.py +2 -2
- package/runtime/python/okstra_ctl/migrate.py +23 -1
- package/runtime/python/okstra_ctl/model_cli.py +19 -5
- package/runtime/python/okstra_ctl/model_discovery.py +46 -47
- package/runtime/python/okstra_ctl/model_io/__init__.py +1 -0
- package/runtime/python/okstra_ctl/model_io/lines.py +163 -0
- package/runtime/python/okstra_ctl/model_io/references.py +370 -0
- package/runtime/python/okstra_ctl/model_io/renderers.py +498 -0
- package/runtime/python/okstra_ctl/model_io_cli.py +30 -945
- package/runtime/python/okstra_ctl/model_pool.py +10 -6
- package/runtime/python/okstra_ctl/models.py +8 -8
- package/runtime/python/okstra_ctl/mutation_recovery.py +5 -63
- package/runtime/python/okstra_ctl/next_phase.py +121 -69
- package/runtime/python/okstra_ctl/pane_reclaim.py +23 -0
- package/runtime/python/okstra_ctl/pane_title.py +11 -8
- package/runtime/python/okstra_ctl/path_hints.py +36 -11
- package/runtime/python/okstra_ctl/paths.py +76 -8
- package/runtime/python/okstra_ctl/plan_items.py +132 -16
- package/runtime/python/okstra_ctl/plan_items_cli.py +486 -36
- package/runtime/python/okstra_ctl/plan_run_root.py +1 -1
- package/runtime/python/okstra_ctl/plan_validate_cli.py +51 -0
- package/runtime/python/okstra_ctl/plan_verify_cli.py +80 -0
- package/runtime/python/okstra_ctl/prepare_error.py +20 -0
- package/runtime/python/okstra_ctl/prior_planning.py +157 -0
- package/runtime/python/okstra_ctl/profile_show.py +18 -1
- package/runtime/python/okstra_ctl/project_setup_cli.py +140 -0
- package/runtime/python/okstra_ctl/recap.py +22 -1
- package/runtime/python/okstra_ctl/reconcile.py +84 -0
- package/runtime/python/okstra_ctl/registry/host_registry.py +18 -1
- package/runtime/python/okstra_ctl/render.py +98 -32
- package/runtime/python/okstra_ctl/render_final_report.py +3 -7
- package/runtime/python/okstra_ctl/report_assembly.py +297 -7
- package/runtime/python/okstra_ctl/report_contract.py +30 -7
- package/runtime/python/okstra_ctl/report_finalize.py +206 -15
- package/runtime/python/okstra_ctl/report_html/common.py +2 -2
- package/runtime/python/okstra_ctl/report_html/render.py +8 -12
- package/runtime/python/okstra_ctl/report_language.py +8 -4
- package/runtime/python/okstra_ctl/report_narrative.py +90 -3
- package/runtime/python/okstra_ctl/report_projections.py +8 -2
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +258 -1
- package/runtime/python/okstra_ctl/report_translation.py +2 -35
- package/runtime/python/okstra_ctl/report_views.py +4 -22
- package/runtime/python/okstra_ctl/resolve_task_key.py +13 -0
- package/runtime/python/okstra_ctl/rollup.py +13 -0
- package/runtime/python/okstra_ctl/run.py +639 -138
- package/runtime/python/okstra_ctl/run_audit.py +13 -0
- package/runtime/python/okstra_ctl/run_index_row.py +0 -12
- package/runtime/python/okstra_ctl/seeding.py +21 -11
- package/runtime/python/okstra_ctl/sequence.py +1 -1
- package/runtime/python/okstra_ctl/session.py +64 -14
- package/runtime/python/okstra_ctl/set_work_status.py +18 -0
- package/runtime/python/okstra_ctl/stage_integrate.py +15 -1
- package/runtime/python/okstra_ctl/stage_ledger.py +1 -1
- package/runtime/python/okstra_ctl/stage_map.py +57 -11
- package/runtime/python/okstra_ctl/stage_map_cli.py +83 -0
- package/runtime/python/okstra_ctl/stage_map_view.py +65 -0
- package/runtime/python/okstra_ctl/stage_targets.py +3 -12
- package/runtime/python/okstra_ctl/task_list_cli.py +138 -0
- package/runtime/python/okstra_ctl/task_show_cli.py +66 -0
- package/runtime/python/okstra_ctl/task_target.py +4 -16
- package/runtime/python/okstra_ctl/team.py +49 -2
- package/runtime/python/okstra_ctl/time_report.py +17 -2
- package/runtime/python/okstra_ctl/usage_report.py +11 -0
- package/runtime/python/okstra_ctl/user_response.py +55 -218
- package/runtime/python/okstra_ctl/user_response_values.py +242 -0
- package/runtime/python/okstra_ctl/validation_contract.py +3 -0
- package/runtime/python/okstra_ctl/verification_target.py +68 -0
- package/runtime/python/okstra_ctl/wizard.py +457 -92
- package/runtime/python/okstra_ctl/worker_artifacts.py +75 -16
- package/runtime/python/okstra_ctl/worker_audit_check.py +22 -0
- package/runtime/python/okstra_ctl/worker_dispatch.py +21 -0
- package/runtime/python/okstra_ctl/worker_liveness.py +33 -0
- package/runtime/python/okstra_ctl/worker_prompt_body.py +15 -2
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +98 -7
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +24 -2
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +18 -3
- package/runtime/python/okstra_ctl/worker_request.py +2 -1
- package/runtime/python/okstra_ctl/worker_runner.py +16 -9
- package/runtime/python/okstra_ctl/worker_state.py +16 -1
- package/runtime/python/okstra_ctl/workflow.py +18 -1
- package/runtime/python/okstra_ctl/worktree/__init__.py +92 -0
- package/runtime/python/okstra_ctl/worktree/cleanliness.py +89 -0
- package/runtime/python/okstra_ctl/worktree/decisions.py +127 -0
- package/runtime/python/okstra_ctl/worktree/git_ops.py +165 -0
- package/runtime/python/okstra_ctl/worktree/linking.py +249 -0
- package/runtime/python/okstra_ctl/worktree/naming.py +91 -0
- package/runtime/python/okstra_ctl/worktree/provision.py +385 -0
- package/runtime/python/okstra_ctl/worktree/sync_config.py +181 -0
- package/runtime/python/okstra_ctl/worktree_cli.py +75 -0
- package/runtime/python/okstra_ctl/worktree_lookup_cli.py +39 -0
- package/runtime/python/okstra_ctl/worktree_registry.py +7 -0
- package/runtime/python/okstra_ctl/worktree_status_cli.py +53 -0
- package/runtime/python/okstra_ctl/write_policy.py +90 -213
- package/runtime/python/okstra_project/__init__.py +0 -4
- package/runtime/python/okstra_project/dirs.py +2 -2
- package/runtime/python/okstra_project/phase_pointer.py +84 -0
- package/runtime/python/okstra_project/slug.py +24 -0
- package/runtime/python/okstra_project/state.py +22 -230
- package/runtime/python/okstra_token_usage/claude.py +9 -1
- package/runtime/python/okstra_token_usage/cli.py +3 -1
- package/runtime/python/okstra_token_usage/collect.py +71 -10
- package/runtime/python/okstra_token_usage/cursor.py +2 -0
- package/runtime/python/okstra_token_usage/grok.py +1 -5
- package/runtime/python/okstra_token_usage/paths.py +26 -0
- package/runtime/python/okstra_token_usage/pricing.py +19 -7
- package/runtime/schemas/convergence-critic-results-v1.0.schema.json +5 -0
- package/runtime/schemas/execution-manifest-v2.schema.json +10 -10
- package/runtime/schemas/final-report-v2.0.schema.json +9 -26
- package/runtime/schemas/final-report-v3.0.schema.json +173 -41
- package/runtime/schemas/report-narrative-v3.0.schema.json +3 -2
- package/runtime/skills/okstra-brief-gen/SKILL.md +35 -2
- package/runtime/skills/okstra-container-build/SKILL.md +16 -47
- package/runtime/skills/okstra-inspect/facets/history.md +1 -1
- package/runtime/skills/okstra-inspect/facets/status.md +7 -6
- package/runtime/skills/okstra-pr-gen/SKILL.md +1 -1
- package/runtime/skills/okstra-run/SKILL.md +7 -5
- package/runtime/templates/report-writer-prompt-preamble.md +1 -1
- package/runtime/templates/reports/brief.template.md +6 -2
- package/runtime/templates/reports/error-analysis-input.template.md +2 -0
- package/runtime/templates/reports/final-verification-input.template.md +2 -0
- package/runtime/templates/reports/implementation-input.template.md +7 -1
- package/runtime/templates/reports/implementation-planning-input.template.md +4 -0
- package/runtime/templates/reports/quick-input.template.md +2 -0
- package/runtime/templates/reports/release-handoff-input.template.md +6 -1
- package/runtime/templates/reports/report.js +20 -3
- package/runtime/templates/reports/schedule.template.md +2 -0
- package/runtime/templates/reports/settings.template.json +0 -10
- package/runtime/templates/reports/task-brief.template.md +3 -0
- package/runtime/templates/reports/user-response.template.md +7 -3
- package/runtime/validators/checks/fixtures-01.py +57 -0
- package/runtime/validators/checks/fixtures-02.py +565 -0
- package/runtime/validators/checks/runners-01.py +108 -0
- package/runtime/validators/checks/validate-assets-01.py +60 -0
- package/runtime/validators/checks/validate-prompt-metadata-01.py +261 -0
- package/runtime/validators/checks/validate-tasks-01.py +46 -0
- package/runtime/validators/checks/validate-tasks-02.py +85 -0
- package/runtime/validators/checks/validate-tasks-03.py +61 -0
- package/runtime/validators/checks/validate-tasks-04.py +118 -0
- package/runtime/validators/forbidden_actions.py +73 -3
- package/runtime/validators/lib/common.sh +5 -0
- package/runtime/validators/lib/fixtures.sh +7 -591
- package/runtime/validators/lib/paths.sh +13 -0
- package/runtime/validators/lib/runners.sh +6 -104
- package/runtime/validators/lib/summary.sh +1 -1
- package/runtime/validators/lib/validate-assets.sh +6 -56
- package/runtime/validators/lib/validate-prompt-metadata.sh +6 -257
- package/runtime/validators/lib/validate-tasks.sh +9 -294
- package/runtime/validators/validate-implementation-plan-stages.py +4 -4
- package/runtime/validators/validate-run.py +1369 -2654
- package/runtime/validators/validate-workflow.sh +56 -16
- package/runtime/validators/validate_improvement_report.py +2 -1
- package/runtime/validators/validate_session_conformance.py +295 -49
- package/dist/commands/execute/agent-prompt.d.mts +0 -1
- package/dist/commands/execute/agent-prompt.mjs +0 -24
- package/dist/commands/execute/agent-prompt.mjs.map +0 -1
- package/dist/commands/execute/codex-dispatch.d.mts +0 -3
- package/dist/commands/execute/codex-dispatch.mjs +0 -6
- package/dist/commands/execute/codex-dispatch.mjs.map +0 -1
- package/dist/commands/execute/codex-run.d.mts +0 -3
- package/dist/commands/execute/codex-run.mjs +0 -62
- package/dist/commands/execute/codex-run.mjs.map +0 -1
- package/dist/commands/execute/convergence.d.mts +0 -1
- package/dist/commands/execute/convergence.mjs +0 -38
- package/dist/commands/execute/convergence.mjs.map +0 -1
- package/dist/commands/execute/error-log.d.mts +0 -1
- package/dist/commands/execute/error-log.mjs +0 -18
- package/dist/commands/execute/error-log.mjs.map +0 -1
- package/dist/commands/execute/git-reconcile.d.mts +0 -1
- package/dist/commands/execute/git-reconcile.mjs +0 -30
- package/dist/commands/execute/git-reconcile.mjs.map +0 -1
- package/dist/commands/execute/handoff.d.mts +0 -1
- package/dist/commands/execute/handoff.mjs +0 -31
- package/dist/commands/execute/handoff.mjs.map +0 -1
- package/dist/commands/execute/incremental-carry.d.mts +0 -1
- package/dist/commands/execute/incremental-carry.mjs +0 -20
- package/dist/commands/execute/incremental-carry.mjs.map +0 -1
- package/dist/commands/execute/incremental-scope.d.mts +0 -1
- package/dist/commands/execute/incremental-scope.mjs +0 -29
- package/dist/commands/execute/incremental-scope.mjs.map +0 -1
- package/dist/commands/execute/integrate-stages.d.mts +0 -1
- package/dist/commands/execute/integrate-stages.mjs +0 -24
- package/dist/commands/execute/integrate-stages.mjs.map +0 -1
- package/dist/commands/execute/pane-title.d.mts +0 -1
- package/dist/commands/execute/pane-title.mjs +0 -20
- package/dist/commands/execute/pane-title.mjs.map +0 -1
- package/dist/commands/execute/plan-items.d.mts +0 -1
- package/dist/commands/execute/plan-items.mjs +0 -9
- package/dist/commands/execute/plan-items.mjs.map +0 -1
- package/dist/commands/execute/plan-validate.d.mts +0 -1
- package/dist/commands/execute/plan-validate.mjs +0 -68
- package/dist/commands/execute/plan-validate.mjs.map +0 -1
- package/dist/commands/execute/plan-verify.d.mts +0 -1
- package/dist/commands/execute/plan-verify.mjs +0 -43
- package/dist/commands/execute/plan-verify.mjs.map +0 -1
- package/dist/commands/execute/spawn-followups.d.mts +0 -1
- package/dist/commands/execute/spawn-followups.mjs +0 -22
- package/dist/commands/execute/spawn-followups.mjs.map +0 -1
- package/dist/commands/execute/team.d.mts +0 -3
- package/dist/commands/execute/team.mjs +0 -66
- package/dist/commands/execute/team.mjs.map +0 -1
- package/dist/commands/execute/token-usage.d.mts +0 -1
- package/dist/commands/execute/token-usage.mjs +0 -19
- package/dist/commands/execute/token-usage.mjs.map +0 -1
- package/dist/commands/execute/worker-audit-check.d.mts +0 -1
- package/dist/commands/execute/worker-audit-check.mjs +0 -34
- package/dist/commands/execute/worker-audit-check.mjs.map +0 -1
- package/dist/commands/execute/worker-dispatch.d.mts +0 -7
- package/dist/commands/execute/worker-dispatch.mjs +0 -64
- package/dist/commands/execute/worker-dispatch.mjs.map +0 -1
- package/dist/commands/execute/worker-state.d.mts +0 -1
- package/dist/commands/execute/worker-state.mjs +0 -28
- package/dist/commands/execute/worker-state.mjs.map +0 -1
- package/dist/commands/execute/worktree-lookup.d.mts +0 -1
- package/dist/commands/execute/worktree-lookup.mjs +0 -92
- package/dist/commands/execute/worktree-lookup.mjs.map +0 -1
- package/dist/commands/execute/worktree-status.d.mts +0 -1
- package/dist/commands/execute/worktree-status.mjs +0 -121
- package/dist/commands/execute/worktree-status.mjs.map +0 -1
- package/dist/commands/inspect/code-review.d.mts +0 -1
- package/dist/commands/inspect/code-review.mjs +0 -32
- package/dist/commands/inspect/code-review.mjs.map +0 -1
- package/dist/commands/inspect/container.d.mts +0 -1
- package/dist/commands/inspect/container.mjs +0 -25
- package/dist/commands/inspect/container.mjs.map +0 -1
- package/dist/commands/inspect/context-cost.d.mts +0 -1
- package/dist/commands/inspect/context-cost.mjs +0 -25
- package/dist/commands/inspect/context-cost.mjs.map +0 -1
- package/dist/commands/inspect/design-prep.d.mts +0 -1
- package/dist/commands/inspect/design-prep.mjs +0 -22
- package/dist/commands/inspect/design-prep.mjs.map +0 -1
- package/dist/commands/inspect/error-report.d.mts +0 -1
- package/dist/commands/inspect/error-report.mjs +0 -25
- package/dist/commands/inspect/error-report.mjs.map +0 -1
- package/dist/commands/inspect/error-zip.d.mts +0 -1
- package/dist/commands/inspect/error-zip.mjs +0 -24
- package/dist/commands/inspect/error-zip.mjs.map +0 -1
- package/dist/commands/inspect/log-report.d.mts +0 -1
- package/dist/commands/inspect/log-report.mjs +0 -26
- package/dist/commands/inspect/log-report.mjs.map +0 -1
- package/dist/commands/inspect/model-io.d.mts +0 -1
- package/dist/commands/inspect/model-io.mjs +0 -25
- package/dist/commands/inspect/model-io.mjs.map +0 -1
- package/dist/commands/inspect/profile-show.d.mts +0 -1
- package/dist/commands/inspect/profile-show.mjs +0 -28
- package/dist/commands/inspect/profile-show.mjs.map +0 -1
- package/dist/commands/inspect/recap.d.mts +0 -1
- package/dist/commands/inspect/recap.mjs +0 -30
- package/dist/commands/inspect/recap.mjs.map +0 -1
- package/dist/commands/inspect/resolve-task-key.d.mts +0 -1
- package/dist/commands/inspect/resolve-task-key.mjs +0 -24
- package/dist/commands/inspect/resolve-task-key.mjs.map +0 -1
- package/dist/commands/inspect/rollup.d.mts +0 -1
- package/dist/commands/inspect/rollup.mjs +0 -25
- package/dist/commands/inspect/rollup.mjs.map +0 -1
- package/dist/commands/inspect/run-audit.d.mts +0 -1
- package/dist/commands/inspect/run-audit.mjs +0 -25
- package/dist/commands/inspect/run-audit.mjs.map +0 -1
- package/dist/commands/inspect/set-work-status.d.mts +0 -1
- package/dist/commands/inspect/set-work-status.mjs +0 -29
- package/dist/commands/inspect/set-work-status.mjs.map +0 -1
- package/dist/commands/inspect/stage-map.d.mts +0 -1
- package/dist/commands/inspect/stage-map.mjs +0 -131
- package/dist/commands/inspect/stage-map.mjs.map +0 -1
- package/dist/commands/inspect/task-list.d.mts +0 -1
- package/dist/commands/inspect/task-list.mjs +0 -149
- package/dist/commands/inspect/task-list.mjs.map +0 -1
- package/dist/commands/inspect/task-show.d.mts +0 -1
- package/dist/commands/inspect/task-show.mjs +0 -108
- package/dist/commands/inspect/task-show.mjs.map +0 -1
- package/dist/commands/inspect/time-report.d.mts +0 -1
- package/dist/commands/inspect/time-report.mjs +0 -24
- package/dist/commands/inspect/time-report.mjs.map +0 -1
- package/dist/commands/inspect/usage-report.d.mts +0 -1
- package/dist/commands/inspect/usage-report.mjs +0 -23
- package/dist/commands/inspect/usage-report.mjs.map +0 -1
- package/dist/commands/inspect/user-response.d.mts +0 -1
- package/dist/commands/inspect/user-response.mjs +0 -35
- package/dist/commands/inspect/user-response.mjs.map +0 -1
- package/dist/commands/inspect/worker-liveness.d.mts +0 -1
- package/dist/commands/inspect/worker-liveness.mjs +0 -45
- package/dist/commands/inspect/worker-liveness.mjs.map +0 -1
- package/dist/commands/lifecycle/contract-check.d.mts +0 -1
- package/dist/commands/lifecycle/contract-check.mjs +0 -18
- package/dist/commands/lifecycle/contract-check.mjs.map +0 -1
- package/dist/commands/lifecycle/migrate.d.mts +0 -1
- package/dist/commands/lifecycle/migrate.mjs +0 -30
- package/dist/commands/lifecycle/migrate.mjs.map +0 -1
- package/dist/commands/lifecycle/model.d.mts +0 -1
- package/dist/commands/lifecycle/model.mjs +0 -22
- package/dist/commands/lifecycle/model.mjs.map +0 -1
- package/dist/commands/manager.d.mts +0 -3
- package/dist/commands/manager.mjs +0 -50
- package/dist/commands/manager.mjs.map +0 -1
- package/dist/commands/report/agent-activity.d.mts +0 -1
- package/dist/commands/report/agent-activity.mjs +0 -20
- package/dist/commands/report/agent-activity.mjs.map +0 -1
- package/dist/commands/report/approval-decision.d.mts +0 -1
- package/dist/commands/report/approval-decision.mjs +0 -21
- package/dist/commands/report/approval-decision.mjs.map +0 -1
- package/dist/commands/report/design-snapshot.d.mts +0 -1
- package/dist/commands/report/design-snapshot.mjs +0 -19
- package/dist/commands/report/design-snapshot.mjs.map +0 -1
- package/dist/commands/report/finalize.d.mts +0 -4
- package/dist/commands/report/finalize.mjs +0 -64
- package/dist/commands/report/finalize.mjs.map +0 -1
- package/dist/commands/report/inject-report-index.d.mts +0 -1
- package/dist/commands/report/inject-report-index.mjs +0 -21
- package/dist/commands/report/inject-report-index.mjs.map +0 -1
- package/dist/commands/report/render-final-report.d.mts +0 -1
- package/dist/commands/report/render-final-report.mjs +0 -23
- package/dist/commands/report/render-final-report.mjs.map +0 -1
- package/dist/commands/report/render-views.d.mts +0 -1
- package/dist/commands/report/render-views.mjs +0 -27
- package/dist/commands/report/render-views.mjs.map +0 -1
- package/dist/commands/report/translate.d.mts +0 -1
- package/dist/commands/report/translate.mjs +0 -33
- package/dist/commands/report/translate.mjs.map +0 -1
- package/runtime/bin/lib/okstra/tmux-pane.sh +0 -40
- package/runtime/bin/lib/okstra-ctl/cmd-batch.sh +0 -59
- package/runtime/bin/lib/okstra-ctl/cmd-list.sh +0 -35
- package/runtime/bin/lib/okstra-ctl/cmd-open.sh +0 -36
- package/runtime/bin/lib/okstra-ctl/cmd-projects.sh +0 -26
- package/runtime/bin/lib/okstra-ctl/cmd-reconcile.sh +0 -29
- package/runtime/bin/lib/okstra-ctl/cmd-reindex.sh +0 -38
- package/runtime/bin/lib/okstra-ctl/cmd-rerun.sh +0 -345
- package/runtime/bin/lib/okstra-ctl/cmd-show.sh +0 -27
- package/runtime/bin/lib/okstra-ctl/cmd-tail.sh +0 -92
- package/runtime/bin/lib/okstra-ctl/main.sh +0 -41
- package/runtime/bin/lib/okstra-ctl/prepare.sh +0 -31
- package/runtime/bin/lib/okstra-ctl/usage.sh +0 -23
- package/runtime/bin/okstra-central.sh +0 -152
- package/runtime/bin/okstra-incremental-carry.py +0 -10
- package/runtime/bin/okstra-incremental-scope.py +0 -10
- package/runtime/bin/okstra-team-reconcile.sh +0 -36
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +0 -1156
- package/runtime/python/okstra_ctl/batch.py +0 -60
- package/runtime/python/okstra_ctl/clarification_items.py +0 -1050
- package/runtime/python/okstra_ctl/container_registry.py +0 -72
- package/runtime/python/okstra_ctl/improvement_assignment.py +0 -61
- package/runtime/python/okstra_ctl/resolver.py +0 -54
- package/runtime/python/okstra_ctl/team_reconcile.py +0 -275
- package/runtime/python/okstra_ctl/tmux.py +0 -134
- package/runtime/python/okstra_ctl/worktree.py +0 -1099
|
@@ -12,7 +12,7 @@ from .execution_identity import ExecutionManifest
|
|
|
12
12
|
WORKING_SCHEMA_VERSION = "1.0"
|
|
13
13
|
V2_WORKING_SCHEMA_VERSION = "2.0"
|
|
14
14
|
EXECUTION_IDENTITY_VERSION = 2
|
|
15
|
-
FINAL_SCHEMA_VERSION = "1.
|
|
15
|
+
FINAL_SCHEMA_VERSION = "1.4"
|
|
16
16
|
_AUDIENCES = {"analysis", "lead", "report-writer"}
|
|
17
17
|
# 발견을 낼 수 있는 audience. 리드는 계약상 Phase 6 자체 리뷰를 수행하므로
|
|
18
18
|
# 출처가 될 수 있지만, 교차검증 표는 아니다 — 합의 수는 아래에서 분석 워커만
|
|
@@ -25,17 +25,29 @@ _AUDIENCE_SOURCE_ROLES = {
|
|
|
25
25
|
"report-writer": frozenset({"report-writer"}),
|
|
26
26
|
}
|
|
27
27
|
_VERIFICATION_MODES = {"lightweight", "full-reanalysis"}
|
|
28
|
+
# `unverified` 가 없던 시절의 산출물. `reuse-final` 이 그대로 읽으므로 그
|
|
29
|
+
# 어휘로 검증한다 — 없던 키를 요구하면 이미 디스크에 있는 리포트가 무효가 된다.
|
|
30
|
+
_HISTORICAL_FINAL_VERSIONS = frozenset({"1.0", "1.1", "1.2", "1.3"})
|
|
28
31
|
_CLASSIFICATION_COUNT_KEYS = {
|
|
29
32
|
"full-consensus": "fullConsensus",
|
|
30
33
|
"partial-consensus": "partialConsensus",
|
|
31
34
|
"contested": "contested",
|
|
32
35
|
"worker-unique": "workerUnique",
|
|
36
|
+
# 아무도 판정하지 않은 finding. `contested` 와 정반대 상태다 — 하나는
|
|
37
|
+
# 동료가 보고 반박한 것이고 이것은 동료가 못 본 것이다. 같은 칸에
|
|
38
|
+
# 세면 워커가 다투지 않은 run 이 다툰 것으로 읽힌다.
|
|
39
|
+
"unverified": "unverified",
|
|
33
40
|
}
|
|
34
41
|
_VERDICTS = {"agree", "disagree", "supplement", "verification-error"}
|
|
35
42
|
_INPUT_VERDICTS = _VERDICTS | {"unverifiable"}
|
|
36
43
|
_DISAGREE_BASES = {"counter-evidence", "burden-not-met"}
|
|
37
44
|
_DISPATCH_STATUSES = {"completed", "timeout", "error", "not-run"}
|
|
38
|
-
_CRITIC_CLASSIFICATIONS = set(_CLASSIFICATION_COUNT_KEYS)
|
|
45
|
+
_CRITIC_CLASSIFICATIONS = set(_CLASSIFICATION_COUNT_KEYS)
|
|
46
|
+
# gap 전용 분류 하나. critic 이 스스로 "이건 F-NNN 과 같은 것" 이라고 선언한 gap
|
|
47
|
+
# 이며, 검증 라운드에 들어가지 않으므로 표가 없다. finding 분류에 넣으면
|
|
48
|
+
# `finalClassificationCounts` 에 대응하는 칸이 없는 값이 생긴다.
|
|
49
|
+
_CRITIC_DUPLICATE = "duplicate"
|
|
50
|
+
_CRITIC_GAP_CLASSIFICATIONS = _CRITIC_CLASSIFICATIONS | {_CRITIC_DUPLICATE}
|
|
39
51
|
_FINAL_V13_KEYS = {
|
|
40
52
|
"schemaVersion",
|
|
41
53
|
"taskKey",
|
|
@@ -49,6 +61,16 @@ _FINAL_V13_KEYS = {
|
|
|
49
61
|
"criticVerification",
|
|
50
62
|
"unverifiedGaps",
|
|
51
63
|
}
|
|
64
|
+
# 게이트가 닫힌 계획에 실리는 문구. 계획의 `round` 는 산출물 이름
|
|
65
|
+
# (`convergence-round-<N>-plan-...`)의 순번이라 finalize 계획에도 값이 있고,
|
|
66
|
+
# 리드가 그것을 "다음 디스패치 라운드"로 읽어 워커를 띄우면 `apply_round_results`
|
|
67
|
+
# 에서 막힌다. 계획 자체가 다음 명령을 말하게 해 그 오독을 없앤다.
|
|
68
|
+
FINALIZE_PLAN_NOTE = (
|
|
69
|
+
"the round gate closed: this plan is a finalize instruction, not a "
|
|
70
|
+
"dispatch round. Dispatch no reverify worker; run "
|
|
71
|
+
"`okstra convergence apply-critic-gaps` when the coverage critic is "
|
|
72
|
+
"enabled, then `okstra convergence finalize`."
|
|
73
|
+
)
|
|
52
74
|
|
|
53
75
|
|
|
54
76
|
class ConvergenceContractError(ValueError):
|
|
@@ -118,7 +140,6 @@ def seed_working_state(
|
|
|
118
140
|
source.get("groups"),
|
|
119
141
|
analysis_workers,
|
|
120
142
|
finding_sources,
|
|
121
|
-
adversarial=config["adversarial"],
|
|
122
143
|
)
|
|
123
144
|
state["findings"] = findings
|
|
124
145
|
state["queueFindingIds"] = queue
|
|
@@ -159,14 +180,20 @@ def plan_next_round(state: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
159
180
|
reason = "queue-empty"
|
|
160
181
|
elif len(history) >= effective_max:
|
|
161
182
|
reason = "max-rounds-reached"
|
|
183
|
+
# `round` 는 계획 산출물 이름의 `<N>` 이다. finalize 계획에도 이름이 필요해
|
|
184
|
+
# 값을 그대로 남기되, `dispatchable` 로 이 계획이 디스패치용인지 아닌지를
|
|
185
|
+
# 한 필드에서 읽히게 한다 — 두 액션 모두에 실어야 `dispatchable` 부재를
|
|
186
|
+
# false 로 오독하는 소비자가 생기지 않는다.
|
|
162
187
|
round_number = len(history) + 1
|
|
163
188
|
if reason is not None:
|
|
164
189
|
return {
|
|
165
190
|
"schemaVersion": WORKING_SCHEMA_VERSION,
|
|
166
191
|
"action": "finalize",
|
|
167
192
|
"round": round_number,
|
|
193
|
+
"dispatchable": False,
|
|
168
194
|
"inputQueueSize": len(queue),
|
|
169
195
|
"reason": reason,
|
|
196
|
+
"note": FINALIZE_PLAN_NOTE,
|
|
170
197
|
"dispatches": [],
|
|
171
198
|
"skippedWorkers": [],
|
|
172
199
|
}
|
|
@@ -180,15 +207,11 @@ def plan_next_round(state: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
180
207
|
if not isinstance(worker, Mapping) or worker.get("audience") != "analysis":
|
|
181
208
|
continue
|
|
182
209
|
worker_id = _required_string(worker, "workerId", "working state worker")
|
|
183
|
-
worker_participant_ref = _worker_participant_ref(worker)
|
|
184
210
|
eligible = [
|
|
185
211
|
finding_id
|
|
186
212
|
for finding_id in queue
|
|
187
213
|
if _worker_is_independent_from_finding(
|
|
188
|
-
|
|
189
|
-
worker_participant_ref,
|
|
190
|
-
findings[finding_id],
|
|
191
|
-
workers,
|
|
214
|
+
worker, findings[finding_id], workers
|
|
192
215
|
)
|
|
193
216
|
]
|
|
194
217
|
if eligible:
|
|
@@ -209,6 +232,7 @@ def plan_next_round(state: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
209
232
|
"schemaVersion": WORKING_SCHEMA_VERSION,
|
|
210
233
|
"action": "dispatch",
|
|
211
234
|
"round": round_number,
|
|
235
|
+
"dispatchable": True,
|
|
212
236
|
"inputQueueSize": len(queue),
|
|
213
237
|
"dispatches": dispatches,
|
|
214
238
|
"skippedWorkers": skipped,
|
|
@@ -221,14 +245,28 @@ def _worker_participant_ref(worker: Mapping[str, Any]) -> str | None:
|
|
|
221
245
|
|
|
222
246
|
|
|
223
247
|
def _worker_is_independent_from_finding(
|
|
224
|
-
|
|
225
|
-
participant_ref: str | None,
|
|
248
|
+
worker: Mapping[str, Any],
|
|
226
249
|
finding: Mapping[str, Any],
|
|
227
250
|
workers: list[Any],
|
|
228
251
|
) -> bool:
|
|
252
|
+
"""이 워커가 그 finding 의 원저자가 아닌가.
|
|
253
|
+
|
|
254
|
+
비교 기준은 role execution 이다 — participant(공급자+모델 세션)가 아니다.
|
|
255
|
+
ADR-0017 이 정하듯 공급자와 모델은 역할의 정체성이 아니고, 같은 모델이 두
|
|
256
|
+
역할에 배정되면 두 개의 독립된 워커가 된다. 저자와 검증자는 서로 다른 역할
|
|
257
|
+
계약·다른 duty·다른 세션이므로, 모델 이름이 같다는 이유로 검증에서 빼면
|
|
258
|
+
패널이 근거 없이 줄어든다 — 같은 doctrine 이 critic gap 에 대해서는 이미
|
|
259
|
+
`prompts/lead/convergence.md` §"Critic gaps" 에 적혀 있다.
|
|
260
|
+
|
|
261
|
+
`sourceRoleExecutionRef` 는 `participantRef` + `role` 로 유일하고
|
|
262
|
+
(`execution_manifest._validate_roles`), 워커마다 하나씩 검증된다
|
|
263
|
+
(`_validate_v2_workers` 의 duplicate 검사). v1 상태에는 그 필드가 없으므로
|
|
264
|
+
그때만 workerId 로 떨어진다.
|
|
265
|
+
"""
|
|
229
266
|
origin_worker = finding.get("originWorker")
|
|
230
|
-
|
|
231
|
-
|
|
267
|
+
source_ref = worker.get("sourceRoleExecutionRef")
|
|
268
|
+
if not isinstance(source_ref, str) or not source_ref:
|
|
269
|
+
return origin_worker != worker.get("workerId")
|
|
232
270
|
origin = next(
|
|
233
271
|
(
|
|
234
272
|
row
|
|
@@ -237,10 +275,10 @@ def _worker_is_independent_from_finding(
|
|
|
237
275
|
),
|
|
238
276
|
None,
|
|
239
277
|
)
|
|
240
|
-
|
|
241
|
-
|
|
278
|
+
origin_source_ref = (
|
|
279
|
+
origin.get("sourceRoleExecutionRef") if isinstance(origin, Mapping) else None
|
|
242
280
|
)
|
|
243
|
-
return
|
|
281
|
+
return origin_source_ref != source_ref
|
|
244
282
|
|
|
245
283
|
|
|
246
284
|
def _worker_execution_refs(worker: Mapping[str, Any]) -> dict[str, str]:
|
|
@@ -292,11 +330,15 @@ def classify_adversarial_round(
|
|
|
292
330
|
if len(disagrees) == len(usable):
|
|
293
331
|
return "worker-unique"
|
|
294
332
|
if any(vote["disagreeBasis"] == "counter-evidence" for vote in disagrees):
|
|
295
|
-
|
|
333
|
+
# 증거 기반 반증은 영구적이다 — 뒤 라운드의 동의가 그것을 지우지
|
|
334
|
+
# 못한다. 그러면 이 finding 을 다음 라운드로 넘겨도 검증자는 새 정보
|
|
335
|
+
# 없이 같은 표를 다시 던지고, 결과는 어차피 `contested` 다. 그 자리에서
|
|
336
|
+
# 종결해 재디스패치 비용을 0 으로 만든다.
|
|
337
|
+
return "contested"
|
|
296
338
|
burden_count = sum(
|
|
297
339
|
vote["disagreeBasis"] == "burden-not-met" for vote in disagrees
|
|
298
340
|
)
|
|
299
|
-
if burden_count
|
|
341
|
+
if burden_count * 2 >= len(usable):
|
|
300
342
|
return None
|
|
301
343
|
return "partial-consensus"
|
|
302
344
|
|
|
@@ -315,7 +357,41 @@ def _round_has_counter_evidence(
|
|
|
315
357
|
def _classify_adversarial_history(
|
|
316
358
|
rounds: list[Mapping[str, Any]],
|
|
317
359
|
) -> str | None:
|
|
318
|
-
"""
|
|
360
|
+
"""적대적 라운드 기록을 읽어 확정된 분류를 돌려준다.
|
|
361
|
+
|
|
362
|
+
분류를 낸 첫 라운드에서 멈춘다. `counter-evidence` 반증이 만드는
|
|
363
|
+
`contested` 도 그 라운드에서 종결이다 — 반증이 영구적이므로 다음 라운드는
|
|
364
|
+
같은 표를 다시 받을 뿐이다.
|
|
365
|
+
|
|
366
|
+
예외는 구 엔진이 남긴 상태 하나다. 구 엔진은 counter-evidence 를 마지막
|
|
367
|
+
라운드까지 끌고 갔고, 그 뒤로는 만장일치 반대(`worker-unique`)만 인정했다.
|
|
368
|
+
종결 라운드 뒤에도 이 finding 의 라운드 기록이 이어진다는 사실이 그 상태의
|
|
369
|
+
지문이다 — 현 엔진에서는 거기서 큐를 떠나므로 뒤 라운드가 생기지 않는다.
|
|
370
|
+
그때만 구 규칙으로 이어 읽어, 이미 기록된 세션을 소급해 실패시키지 않는다.
|
|
371
|
+
"""
|
|
372
|
+
rows = [
|
|
373
|
+
row
|
|
374
|
+
for row in rounds
|
|
375
|
+
if isinstance(row, Mapping) and isinstance(row.get("votes"), Mapping)
|
|
376
|
+
]
|
|
377
|
+
for index, row in enumerate(rows):
|
|
378
|
+
classification = classify_adversarial_round(row["votes"])
|
|
379
|
+
if classification is None:
|
|
380
|
+
continue
|
|
381
|
+
if classification == "contested" and index < len(rows) - 1:
|
|
382
|
+
return _legacy_adversarial_history(rows)
|
|
383
|
+
return classification
|
|
384
|
+
return None
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _legacy_adversarial_history(
|
|
388
|
+
rounds: list[Mapping[str, Any]],
|
|
389
|
+
) -> str | None:
|
|
390
|
+
"""구 엔진의 적대적 분류 — 역호환 재생 전용.
|
|
391
|
+
|
|
392
|
+
counter-evidence 를 한 번 본 뒤로는 만장일치 반대만 분류를 확정했고, 그
|
|
393
|
+
외에는 계속 다음 라운드로 넘겨 finalize 가 `contested` 를 찍게 했다.
|
|
394
|
+
"""
|
|
319
395
|
counter_evidence_seen = False
|
|
320
396
|
for row in rounds:
|
|
321
397
|
votes = row.get("votes") if isinstance(row, Mapping) else None
|
|
@@ -342,7 +418,18 @@ def apply_round_results(
|
|
|
342
418
|
expected_plan = plan_next_round(current)
|
|
343
419
|
supplied_plan = _object(plan, "round plan")
|
|
344
420
|
if expected_plan.get("action") != "dispatch":
|
|
345
|
-
|
|
421
|
+
# 예전 문구("no dispatch round to apply")는 상태를 말하지 않아서, 리드가
|
|
422
|
+
# finalize 계획의 `round` 를 디스패치 라운드로 읽고 워커를 띄운 뒤
|
|
423
|
+
# 여기서 막히면 무엇을 해야 하는지 알 수 없었다. 닫힌 사유와 다음 명령을
|
|
424
|
+
# 함께 낸다.
|
|
425
|
+
raise ConvergenceContractError(
|
|
426
|
+
"the round gate closed after round "
|
|
427
|
+
f"{len(current.get('roundHistory') or [])} "
|
|
428
|
+
f"(reason: {expected_plan.get('reason')}); the current plan is a "
|
|
429
|
+
"finalize instruction, not a dispatch round, and its `round` "
|
|
430
|
+
f"{expected_plan.get('round')} is only the artifact-name ordinal. "
|
|
431
|
+
"Dispatch nothing and run `okstra convergence finalize`."
|
|
432
|
+
)
|
|
346
433
|
if supplied_plan != expected_plan:
|
|
347
434
|
raise ConvergenceContractError("round plan does not match current working state")
|
|
348
435
|
payload = _object(results, "round results")
|
|
@@ -469,6 +556,9 @@ def apply_critic_gap_results(
|
|
|
469
556
|
gap["classification"] in {"contested", "worker-unique"}
|
|
470
557
|
for gap in ledger_gaps
|
|
471
558
|
)
|
|
559
|
+
duplicate = sum(
|
|
560
|
+
gap["classification"] == _CRITIC_DUPLICATE for gap in ledger_gaps
|
|
561
|
+
)
|
|
472
562
|
updated["config"]["critic"] = {
|
|
473
563
|
"provider": provider,
|
|
474
564
|
"modelExecutionValue": model,
|
|
@@ -476,6 +566,9 @@ def apply_critic_gap_results(
|
|
|
476
566
|
"gapsMerged": merged,
|
|
477
567
|
"gapsRejected": rejected,
|
|
478
568
|
"gapsUnverified": len(unverified),
|
|
569
|
+
# 선언된 중복은 제안됐으나 검증하지 않은 gap 이다. 넷 중 어느 칸에 넣어도
|
|
570
|
+
# 거짓이 된다 — 병합도 기각도 아니고, 판정을 못 받은 것도 아니다.
|
|
571
|
+
"gapsDuplicate": duplicate,
|
|
479
572
|
}
|
|
480
573
|
updated["unverifiedGaps"] = unverified
|
|
481
574
|
updated_errors = validate_working_state(updated)
|
|
@@ -499,6 +592,9 @@ def finalize_working_state(state: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
499
592
|
for finding in findings:
|
|
500
593
|
if finding["findingId"] not in queue:
|
|
501
594
|
continue
|
|
595
|
+
if _received_no_verdict(finding):
|
|
596
|
+
finding["classification"] = "unverified"
|
|
597
|
+
continue
|
|
502
598
|
finding["classification"] = (
|
|
503
599
|
"contested"
|
|
504
600
|
if adversarial
|
|
@@ -688,7 +784,7 @@ def validate_final_state(state: Mapping[str, Any]) -> list[str]:
|
|
|
688
784
|
if not isinstance(state, Mapping):
|
|
689
785
|
return ["final state must be an object"]
|
|
690
786
|
version = state.get("schemaVersion")
|
|
691
|
-
if version not in
|
|
787
|
+
if version not in _HISTORICAL_FINAL_VERSIONS | {FINAL_SCHEMA_VERSION}:
|
|
692
788
|
errors.append("unsupported final schemaVersion")
|
|
693
789
|
config = state.get("config")
|
|
694
790
|
if not isinstance(config, Mapping):
|
|
@@ -707,6 +803,10 @@ def validate_final_state(state: Mapping[str, Any]) -> list[str]:
|
|
|
707
803
|
history = []
|
|
708
804
|
errors.extend(_validate_round_history(history))
|
|
709
805
|
adversarial = bool(config.get("adversarial"))
|
|
806
|
+
historical = version in _HISTORICAL_FINAL_VERSIONS
|
|
807
|
+
allowed_classifications = set(_CLASSIFICATION_COUNT_KEYS)
|
|
808
|
+
if historical:
|
|
809
|
+
allowed_classifications.discard("unverified")
|
|
710
810
|
seen_ids: set[str] = set()
|
|
711
811
|
for finding in findings:
|
|
712
812
|
if not isinstance(finding, Mapping):
|
|
@@ -720,16 +820,26 @@ def validate_final_state(state: Mapping[str, Any]) -> list[str]:
|
|
|
720
820
|
else:
|
|
721
821
|
seen_ids.add(finding_id)
|
|
722
822
|
classification = finding.get("classification")
|
|
723
|
-
if classification not in
|
|
823
|
+
if classification not in allowed_classifications:
|
|
724
824
|
errors.append(f"finding {finding_id} has invalid classification")
|
|
725
825
|
errors.extend(_validate_finding_ledger(finding, len(history), adversarial))
|
|
726
826
|
rounds = finding.get("rounds") if isinstance(finding.get("rounds"), list) else []
|
|
727
|
-
|
|
827
|
+
# `unverified`, 그리고 마지막 라운드까지 판정을 못 받아 finalize 가 찍은
|
|
828
|
+
# `contested` 는 둘 다 "마지막 라운드까지 큐에 남아 있었다"는 뜻이다.
|
|
829
|
+
# 라운드 안에서 확정된 `contested`(증거 기반 반증)는 다르다 — 그때 큐를
|
|
830
|
+
# 떠나므로 뒤 라운드 기록이 없는 것이 정상이다.
|
|
831
|
+
settled_in_round = (
|
|
832
|
+
adversarial and _round_settled_classification(rounds) is not None
|
|
833
|
+
)
|
|
834
|
+
unresolved = classification in {"contested", "unverified"}
|
|
835
|
+
if unresolved and rounds and not settled_in_round:
|
|
728
836
|
if rounds[-1].get("round") != len(history):
|
|
729
837
|
errors.append(f"finding {finding_id} contested before the final round")
|
|
730
|
-
if
|
|
838
|
+
if not unresolved:
|
|
731
839
|
errors.extend(_validate_no_reappearance_after_resolution(finding, adversarial))
|
|
732
|
-
expected = _expected_final_classification(
|
|
840
|
+
expected = _expected_final_classification(
|
|
841
|
+
finding, adversarial, historical=historical
|
|
842
|
+
)
|
|
733
843
|
if expected is not None and classification != expected:
|
|
734
844
|
errors.append(
|
|
735
845
|
f"finding {finding_id} classification {classification} "
|
|
@@ -744,6 +854,8 @@ def validate_final_state(state: Mapping[str, Any]) -> list[str]:
|
|
|
744
854
|
errors.append("finalClassificationCounts must be an object")
|
|
745
855
|
else:
|
|
746
856
|
actual = classification_counts_from_findings(findings)
|
|
857
|
+
if historical and "unverified" not in declared:
|
|
858
|
+
actual.pop("unverified")
|
|
747
859
|
if dict(declared) != actual:
|
|
748
860
|
errors.append(
|
|
749
861
|
f"finalClassificationCounts {dict(declared)} does not match findings {actual}"
|
|
@@ -784,22 +896,34 @@ def _parse_critic_gap_results(
|
|
|
784
896
|
)
|
|
785
897
|
provider = _required_string(payload, "provider", "critic gap results")
|
|
786
898
|
model = _required_string(payload, "modelExecutionValue", "critic gap results")
|
|
787
|
-
|
|
899
|
+
roster_order = [
|
|
788
900
|
worker["workerId"]
|
|
789
901
|
for worker in state.get("workers", [])
|
|
790
902
|
if isinstance(worker, Mapping) and worker.get("audience") == "analysis"
|
|
791
|
-
|
|
903
|
+
]
|
|
904
|
+
roster = set(roster_order)
|
|
792
905
|
dispatches, completed = _parse_critic_dispatches(
|
|
793
|
-
payload.get("dispatches"), roster
|
|
906
|
+
payload.get("dispatches"), roster
|
|
907
|
+
)
|
|
908
|
+
known_findings = {
|
|
909
|
+
finding["findingId"]
|
|
910
|
+
for finding in state.get("findings", [])
|
|
911
|
+
if isinstance(finding, Mapping) and _nonempty_string(finding.get("findingId"))
|
|
912
|
+
}
|
|
913
|
+
gaps = _parse_critic_gaps(
|
|
914
|
+
payload.get("gaps"), roster, completed, known_findings
|
|
915
|
+
)
|
|
916
|
+
coverage_errors = _critic_gap_coverage_errors(
|
|
917
|
+
roster_order, gaps, {row["worker"] for row in dispatches}
|
|
794
918
|
)
|
|
795
|
-
|
|
919
|
+
if coverage_errors:
|
|
920
|
+
raise ConvergenceContractError("; ".join(coverage_errors))
|
|
796
921
|
return provider, model, dispatches, gaps
|
|
797
922
|
|
|
798
923
|
|
|
799
924
|
def _parse_critic_dispatches(
|
|
800
925
|
value: Any,
|
|
801
926
|
roster: set[str],
|
|
802
|
-
provider: str,
|
|
803
927
|
) -> tuple[list[dict[str, Any]], set[str]]:
|
|
804
928
|
if not isinstance(value, list):
|
|
805
929
|
raise ConvergenceContractError("critic gap results dispatches must be an array")
|
|
@@ -824,22 +948,64 @@ def _parse_critic_dispatches(
|
|
|
824
948
|
)
|
|
825
949
|
seen.add(worker)
|
|
826
950
|
dispatches.append({"worker": worker, "status": status, "durationMs": duration})
|
|
827
|
-
expected = set(roster)
|
|
828
|
-
if seen != expected:
|
|
829
|
-
missing = sorted(expected - seen)
|
|
830
|
-
raise ConvergenceContractError(
|
|
831
|
-
"critic dispatches must account for every non-critic analyser exactly "
|
|
832
|
-
f"once; missing: {', '.join(missing) or 'none'}"
|
|
833
|
-
)
|
|
834
951
|
completed = {row["worker"] for row in dispatches if row["status"] == "completed"}
|
|
835
952
|
return dispatches, completed
|
|
836
953
|
|
|
837
954
|
|
|
955
|
+
def _critic_gap_assignees(
|
|
956
|
+
roster_order: list[str],
|
|
957
|
+
gaps: list[Mapping[str, Any]],
|
|
958
|
+
) -> list[str]:
|
|
959
|
+
"""gap 하나당 검증자 한 명. 로스터 순서 위의 round-robin 이다.
|
|
960
|
+
|
|
961
|
+
종전 계약은 gap 배치를 분석자 전원에게 부치라고 요구했다. 거부도 승인과 같은
|
|
962
|
+
값을 치르는데, 그 라운드는 `rounds: []` 로 기록조차 남지 않는 직렬 구간이다.
|
|
963
|
+
gap 마다 검증자 하나면 gap 이 로스터보다 적을 때 디스패치 수가 그만큼 줄고,
|
|
964
|
+
많을 때는 각 분석자가 전체가 아니라 자기 몫만 읽는다.
|
|
965
|
+
|
|
966
|
+
선언한 중복(`duplicateOf`)은 배정에서 빠진다 — 검증하지 않기로 한 gap 이다.
|
|
967
|
+
"""
|
|
968
|
+
verifiable = [gap for gap in gaps if not gap.get("duplicateOf")]
|
|
969
|
+
if not roster_order:
|
|
970
|
+
return []
|
|
971
|
+
return [
|
|
972
|
+
roster_order[index % len(roster_order)]
|
|
973
|
+
for index in range(len(verifiable))
|
|
974
|
+
]
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
def _critic_gap_coverage_errors(
|
|
978
|
+
roster_order: list[str],
|
|
979
|
+
gaps: list[Mapping[str, Any]],
|
|
980
|
+
dispatched: set[str],
|
|
981
|
+
) -> list[str]:
|
|
982
|
+
"""디스패치 집합이 두 모양 중 하나인가.
|
|
983
|
+
|
|
984
|
+
구 산출물은 분석자 전원이 한 줄씩 있고(전면 fan-out), 새 산출물은 배정받은
|
|
985
|
+
분석자만 있다. 최종 산출물에는 어느 쪽으로 돌았는지 적히지 않으므로 둘 다
|
|
986
|
+
받는다 — `_validate_round_ledger_counts` 가 구/신 산술을 함께 받는 것과 같은
|
|
987
|
+
이유다. gap 수가 로스터 크기 이상이면 두 집합은 같아진다.
|
|
988
|
+
|
|
989
|
+
배정받은 분석자가 자기 몫 밖의 gap 에도 표를 던지는 것은 막지 않는다. 그건
|
|
990
|
+
계약 위반이 아니라 더 본 것이고, 그 사이 방치된 gap 은 표가 없어
|
|
991
|
+
`unverifiedGaps` 로 스스로 드러난다.
|
|
992
|
+
"""
|
|
993
|
+
assignees = set(_critic_gap_assignees(roster_order, gaps))
|
|
994
|
+
if dispatched == set(roster_order) or dispatched == assignees:
|
|
995
|
+
return []
|
|
996
|
+
missing = sorted(assignees - dispatched)
|
|
997
|
+
return [
|
|
998
|
+
"critic dispatches must cover every gap exactly once (round-robin over "
|
|
999
|
+
"the analyser roster) or account for every non-critic analyser exactly "
|
|
1000
|
+
f"once; missing: {', '.join(missing) or 'none'}"
|
|
1001
|
+
]
|
|
1002
|
+
|
|
1003
|
+
|
|
838
1004
|
def _parse_critic_gaps(
|
|
839
1005
|
value: Any,
|
|
840
1006
|
roster: set[str],
|
|
841
|
-
provider: str,
|
|
842
1007
|
completed: set[str],
|
|
1008
|
+
known_findings: set[str],
|
|
843
1009
|
) -> list[dict[str, Any]]:
|
|
844
1010
|
if not isinstance(value, list):
|
|
845
1011
|
raise ConvergenceContractError("critic gap results gaps must be an array")
|
|
@@ -851,7 +1017,7 @@ def _parse_critic_gaps(
|
|
|
851
1017
|
if gap_id in seen:
|
|
852
1018
|
raise ConvergenceContractError(f"duplicate critic gapId: {gap_id}")
|
|
853
1019
|
seen.add(gap_id)
|
|
854
|
-
parsed = _parse_critic_gap(gap, index, roster,
|
|
1020
|
+
parsed = _parse_critic_gap(gap, index, roster, completed, known_findings)
|
|
855
1021
|
parsed["gapId"] = gap_id
|
|
856
1022
|
gaps.append(parsed)
|
|
857
1023
|
return gaps
|
|
@@ -861,11 +1027,28 @@ def _parse_critic_gap(
|
|
|
861
1027
|
gap: Mapping[str, Any],
|
|
862
1028
|
index: int,
|
|
863
1029
|
roster: set[str],
|
|
864
|
-
provider: str,
|
|
865
1030
|
completed: set[str],
|
|
1031
|
+
known_findings: set[str],
|
|
866
1032
|
) -> dict[str, Any]:
|
|
867
1033
|
label = f"critic gaps[{index}]"
|
|
868
|
-
|
|
1034
|
+
duplicate_of = gap.get("duplicateOf")
|
|
1035
|
+
if duplicate_of is not None:
|
|
1036
|
+
if not _nonempty_string(duplicate_of):
|
|
1037
|
+
raise ConvergenceContractError(
|
|
1038
|
+
f"{label}.duplicateOf must be a non-empty finding id"
|
|
1039
|
+
)
|
|
1040
|
+
if duplicate_of not in known_findings:
|
|
1041
|
+
raise ConvergenceContractError(
|
|
1042
|
+
f"{label}.duplicateOf names no existing finding: {duplicate_of}"
|
|
1043
|
+
)
|
|
1044
|
+
if gap.get("votes"):
|
|
1045
|
+
raise ConvergenceContractError(
|
|
1046
|
+
f"{label} declares duplicateOf and must carry no votes: a "
|
|
1047
|
+
"declared duplicate never enters gap verification"
|
|
1048
|
+
)
|
|
1049
|
+
votes_value = _object(
|
|
1050
|
+
{} if duplicate_of is not None else gap.get("votes"), f"{label}.votes"
|
|
1051
|
+
)
|
|
869
1052
|
votes: dict[str, dict[str, Any]] = {}
|
|
870
1053
|
for worker, raw_vote in votes_value.items():
|
|
871
1054
|
if worker not in roster:
|
|
@@ -890,6 +1073,8 @@ def _parse_critic_gap(
|
|
|
890
1073
|
"originEvidence": _required_string(gap, "originEvidence", label),
|
|
891
1074
|
"votes": votes,
|
|
892
1075
|
}
|
|
1076
|
+
if duplicate_of is not None:
|
|
1077
|
+
parsed["duplicateOf"] = duplicate_of
|
|
893
1078
|
if "evidenceArtifacts" in gap:
|
|
894
1079
|
parsed["evidenceArtifacts"] = _parse_evidence_artifacts(
|
|
895
1080
|
gap.get("evidenceArtifacts"), label
|
|
@@ -906,7 +1091,12 @@ def _reduce_critic_gaps(
|
|
|
906
1091
|
unverified: list[dict[str, str]] = []
|
|
907
1092
|
next_number = _next_finding_number(state["findings"])
|
|
908
1093
|
for gap in gaps:
|
|
909
|
-
|
|
1094
|
+
duplicate_of = gap.get("duplicateOf")
|
|
1095
|
+
classification = (
|
|
1096
|
+
_CRITIC_DUPLICATE
|
|
1097
|
+
if duplicate_of
|
|
1098
|
+
else _critic_gap_classification(gap["votes"])
|
|
1099
|
+
)
|
|
910
1100
|
merged_id = None
|
|
911
1101
|
if classification in {"full-consensus", "partial-consensus"}:
|
|
912
1102
|
merged_id = f"F-{next_number:03d}"
|
|
@@ -932,6 +1122,7 @@ def _reduce_critic_gaps(
|
|
|
932
1122
|
"classification": classification,
|
|
933
1123
|
"mergedFindingId": merged_id,
|
|
934
1124
|
"votes": deepcopy(gap["votes"]),
|
|
1125
|
+
**({"duplicateOf": duplicate_of} if duplicate_of else {}),
|
|
935
1126
|
**(
|
|
936
1127
|
{"evidenceArtifacts": deepcopy(gap["evidenceArtifacts"])}
|
|
937
1128
|
if "evidenceArtifacts" in gap
|
|
@@ -1047,6 +1238,12 @@ def _validate_critic_contract(
|
|
|
1047
1238
|
unverified,
|
|
1048
1239
|
critic_config,
|
|
1049
1240
|
ledger,
|
|
1241
|
+
{
|
|
1242
|
+
finding["findingId"]
|
|
1243
|
+
for finding in findings
|
|
1244
|
+
if isinstance(finding, Mapping)
|
|
1245
|
+
and _nonempty_string(finding.get("findingId"))
|
|
1246
|
+
},
|
|
1050
1247
|
)
|
|
1051
1248
|
)
|
|
1052
1249
|
if critic_config.get("provider") != ledger.get("provider"):
|
|
@@ -1156,6 +1353,7 @@ def _validate_critic_gap_links(
|
|
|
1156
1353
|
unverified: list[Any],
|
|
1157
1354
|
config: Mapping[str, Any],
|
|
1158
1355
|
ledger: Mapping[str, Any],
|
|
1356
|
+
finding_ids: set[str],
|
|
1159
1357
|
) -> list[str]:
|
|
1160
1358
|
errors: list[str] = []
|
|
1161
1359
|
seen_gaps: set[str] = set()
|
|
@@ -1197,6 +1395,12 @@ def _validate_critic_gap_links(
|
|
|
1197
1395
|
)
|
|
1198
1396
|
if classification == "unverified" and isinstance(gap_id, str):
|
|
1199
1397
|
unverified_ids.add(gap_id)
|
|
1398
|
+
duplicate_of = gap.get("duplicateOf") if isinstance(gap, Mapping) else None
|
|
1399
|
+
if _nonempty_string(duplicate_of) and duplicate_of not in finding_ids:
|
|
1400
|
+
errors.append(
|
|
1401
|
+
f"criticVerification gap {gap_id} duplicateOf names no existing "
|
|
1402
|
+
f"finding: {duplicate_of}"
|
|
1403
|
+
)
|
|
1200
1404
|
if set(critic_findings) != merged_ids:
|
|
1201
1405
|
errors.append(
|
|
1202
1406
|
"critic-origin findings do not match criticVerification "
|
|
@@ -1224,7 +1428,7 @@ def _validate_critic_gap_row(
|
|
|
1224
1428
|
"mergedFindingId",
|
|
1225
1429
|
"votes",
|
|
1226
1430
|
}
|
|
1227
|
-
allowed = required | {"evidenceArtifacts"}
|
|
1431
|
+
allowed = required | {"evidenceArtifacts", "duplicateOf"}
|
|
1228
1432
|
if set(gap) - allowed:
|
|
1229
1433
|
errors.append(f"criticVerification.gaps[{index}] has unsupported fields")
|
|
1230
1434
|
if required - set(gap):
|
|
@@ -1236,7 +1440,7 @@ def _validate_critic_gap_row(
|
|
|
1236
1440
|
errors.append(f"criticVerification.gaps[{index}].gapId is invalid")
|
|
1237
1441
|
classification_valid = (
|
|
1238
1442
|
_nonempty_string(classification)
|
|
1239
|
-
and classification in
|
|
1443
|
+
and classification in _CRITIC_GAP_CLASSIFICATIONS
|
|
1240
1444
|
)
|
|
1241
1445
|
if not classification_valid:
|
|
1242
1446
|
errors.append(f"criticVerification gap {gap_id} has invalid classification")
|
|
@@ -1245,6 +1449,14 @@ def _validate_critic_gap_row(
|
|
|
1245
1449
|
errors.append(f"criticVerification gap {gap_id} requires mergedFindingId")
|
|
1246
1450
|
elif classification_valid and merged_id is not None:
|
|
1247
1451
|
errors.append(f"criticVerification gap {gap_id} must not be merged")
|
|
1452
|
+
if classification_valid and (
|
|
1453
|
+
(classification == _CRITIC_DUPLICATE)
|
|
1454
|
+
!= _nonempty_string(gap.get("duplicateOf"))
|
|
1455
|
+
):
|
|
1456
|
+
errors.append(
|
|
1457
|
+
f"criticVerification gap {gap_id} classification "
|
|
1458
|
+
f"`{classification}` and duplicateOf do not agree"
|
|
1459
|
+
)
|
|
1248
1460
|
errors.extend(_validate_critic_gap_content(gap, gap_id))
|
|
1249
1461
|
errors.extend(_validate_critic_gap_votes(gap_id, gap.get("votes"), classification))
|
|
1250
1462
|
return errors, gap_id, merged_id, classification
|
|
@@ -1311,6 +1523,14 @@ def _validate_critic_gap_votes(
|
|
|
1311
1523
|
) -> list[str]:
|
|
1312
1524
|
if not isinstance(votes, Mapping):
|
|
1313
1525
|
return [f"criticVerification gap {gap_id} votes must be an object"]
|
|
1526
|
+
if classification == _CRITIC_DUPLICATE:
|
|
1527
|
+
# 선언된 중복은 아무도 판정하지 않는다. 표가 한 장이라도 있으면 그 gap 은
|
|
1528
|
+
# 검증 라운드에 들어간 것이고, 그렇다면 분류가 표에서 나와야 한다.
|
|
1529
|
+
return (
|
|
1530
|
+
[]
|
|
1531
|
+
if not votes
|
|
1532
|
+
else [f"criticVerification gap {gap_id} duplicate must carry no votes"]
|
|
1533
|
+
)
|
|
1314
1534
|
errors: list[str] = []
|
|
1315
1535
|
parsed: dict[str, dict[str, Any]] = {}
|
|
1316
1536
|
for worker, vote in votes.items():
|
|
@@ -1363,7 +1583,7 @@ def _validate_critic_counts(
|
|
|
1363
1583
|
config: Mapping[str, Any],
|
|
1364
1584
|
classifications: list[Any],
|
|
1365
1585
|
) -> list[str]:
|
|
1366
|
-
|
|
1586
|
+
required = {
|
|
1367
1587
|
"provider",
|
|
1368
1588
|
"modelExecutionValue",
|
|
1369
1589
|
"gapsProposed",
|
|
@@ -1372,8 +1592,13 @@ def _validate_critic_counts(
|
|
|
1372
1592
|
"gapsUnverified",
|
|
1373
1593
|
}
|
|
1374
1594
|
errors: list[str] = []
|
|
1375
|
-
|
|
1595
|
+
# `gapsDuplicate` 이전에 쓰인 산출물에는 이 키가 없다. 중복이 0 건인 것과
|
|
1596
|
+
# 중복이라는 개념이 없던 것을 구별할 수 없으므로 부재를 0 으로 읽는다.
|
|
1597
|
+
if set(config) - {"gapsDuplicate"} != required:
|
|
1376
1598
|
errors.append("config.critic has unsupported fields")
|
|
1599
|
+
duplicate_count = sum(
|
|
1600
|
+
value == _CRITIC_DUPLICATE for value in classifications
|
|
1601
|
+
)
|
|
1377
1602
|
expected = {
|
|
1378
1603
|
"gapsProposed": len(classifications),
|
|
1379
1604
|
"gapsMerged": sum(
|
|
@@ -1388,24 +1613,30 @@ def _validate_critic_counts(
|
|
|
1388
1613
|
),
|
|
1389
1614
|
"gapsUnverified": sum(value == "unverified" for value in classifications),
|
|
1390
1615
|
}
|
|
1616
|
+
if "gapsDuplicate" in config or duplicate_count:
|
|
1617
|
+
expected["gapsDuplicate"] = duplicate_count
|
|
1391
1618
|
for key, value in expected.items():
|
|
1392
1619
|
declared = config.get(key)
|
|
1393
1620
|
if not isinstance(declared, int) or isinstance(declared, bool) or declared < 0:
|
|
1394
1621
|
errors.append(f"config.critic.{key} must be a non-negative integer")
|
|
1395
1622
|
elif declared != value:
|
|
1396
1623
|
errors.append(f"config.critic.{key} {declared} does not match ledger {value}")
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1624
|
+
counts = (
|
|
1625
|
+
config.get("gapsProposed"),
|
|
1626
|
+
config.get("gapsMerged"),
|
|
1627
|
+
config.get("gapsRejected"),
|
|
1628
|
+
config.get("gapsUnverified"),
|
|
1629
|
+
config.get("gapsDuplicate", 0),
|
|
1630
|
+
)
|
|
1402
1631
|
if all(
|
|
1403
1632
|
isinstance(value, int) and not isinstance(value, bool)
|
|
1404
1633
|
for value in counts
|
|
1405
1634
|
):
|
|
1406
|
-
|
|
1635
|
+
proposed, *parts = counts
|
|
1636
|
+
if proposed != sum(parts):
|
|
1407
1637
|
errors.append(
|
|
1408
|
-
"config.critic.gapsProposed must equal gapsMerged + gapsRejected
|
|
1638
|
+
"config.critic.gapsProposed must equal gapsMerged + gapsRejected "
|
|
1639
|
+
"+ gapsUnverified + gapsDuplicate"
|
|
1409
1640
|
)
|
|
1410
1641
|
return errors
|
|
1411
1642
|
|
|
@@ -1436,17 +1667,19 @@ def _validate_critic_roster(
|
|
|
1436
1667
|
worker = row.get("worker") if isinstance(row, Mapping) else None
|
|
1437
1668
|
if not _nonempty_string(worker) or worker not in roster:
|
|
1438
1669
|
errors.append(f"critic voter must be a non-critic analyser: {worker}")
|
|
1439
|
-
expected_dispatches = set(roster)
|
|
1440
1670
|
actual_dispatches = {
|
|
1441
1671
|
row.get("worker")
|
|
1442
1672
|
for row in dispatches
|
|
1443
1673
|
if isinstance(row, Mapping) and _nonempty_string(row.get("worker"))
|
|
1444
1674
|
} if isinstance(dispatches, list) else set()
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1675
|
+
errors.extend(
|
|
1676
|
+
_critic_gap_coverage_errors(
|
|
1677
|
+
[worker for worker in declared if _nonempty_string(worker)]
|
|
1678
|
+
if isinstance(declared, list) else [],
|
|
1679
|
+
[gap for gap in ledger.get("gaps") or [] if isinstance(gap, Mapping)],
|
|
1680
|
+
actual_dispatches,
|
|
1449
1681
|
)
|
|
1682
|
+
)
|
|
1450
1683
|
completed = {
|
|
1451
1684
|
row.get("worker")
|
|
1452
1685
|
for row in dispatches
|
|
@@ -1467,6 +1700,38 @@ def _validate_critic_roster(
|
|
|
1467
1700
|
return errors
|
|
1468
1701
|
|
|
1469
1702
|
|
|
1703
|
+
def _received_no_verdict(finding: Mapping[str, Any]) -> bool:
|
|
1704
|
+
"""이 finding 에 쓸 수 있는 표가 한 장도 없는가.
|
|
1705
|
+
|
|
1706
|
+
비결과 디스패치(`timeout` / `error` / 결과 파일 없음)는 계획된 워커마다
|
|
1707
|
+
`verification-error` 표로 기록된다(`_votes_for_finding`). 실행된 모든
|
|
1708
|
+
라운드가 그것뿐이면 아무도 이 finding 을 들여다보지 않은 것이고, 그 상태를
|
|
1709
|
+
`contested` 로 적는 것은 없는 판정을 지어내는 일이다. gap 쪽에는 이미 같은
|
|
1710
|
+
규칙이 있다(`prompts/lead/convergence.md` 의 "A gap that received no verdict
|
|
1711
|
+
is NOT a rejected gap").
|
|
1712
|
+
|
|
1713
|
+
표가 한 장이라도 기록돼 있어야 한다. 라운드가 아예 없는 finding 은 Round 0
|
|
1714
|
+
에서 이미 분류돼 큐를 떠난 쪽이 대부분이고, 최종 산출물에는 큐가 남지
|
|
1715
|
+
않으므로 검증기가 그 둘을 구별할 수 없다. 구별할 수 없는 것을 판정하면
|
|
1716
|
+
검증기와 엔진이 갈라진다.
|
|
1717
|
+
"""
|
|
1718
|
+
rounds = finding.get("rounds")
|
|
1719
|
+
if not isinstance(rounds, list):
|
|
1720
|
+
return False
|
|
1721
|
+
seen = False
|
|
1722
|
+
for row in rounds:
|
|
1723
|
+
votes = row.get("votes") if isinstance(row, Mapping) else None
|
|
1724
|
+
if not isinstance(votes, Mapping):
|
|
1725
|
+
continue
|
|
1726
|
+
for vote in votes.values():
|
|
1727
|
+
if not isinstance(vote, Mapping):
|
|
1728
|
+
continue
|
|
1729
|
+
seen = True
|
|
1730
|
+
if vote.get("verdict") != "verification-error":
|
|
1731
|
+
return False
|
|
1732
|
+
return seen
|
|
1733
|
+
|
|
1734
|
+
|
|
1470
1735
|
def _final_collaborative_classification(finding: Mapping[str, Any]) -> str:
|
|
1471
1736
|
usable: list[Mapping[str, Any]] = []
|
|
1472
1737
|
for round_row in finding.get("rounds", []):
|
|
@@ -1477,11 +1742,27 @@ def _final_collaborative_classification(finding: Mapping[str, Any]) -> str:
|
|
|
1477
1742
|
return "partial-consensus" if usable and agreeing > len(usable) / 2 else "contested"
|
|
1478
1743
|
|
|
1479
1744
|
|
|
1745
|
+
def _round_settled_classification(rounds: list[Any]) -> str | None:
|
|
1746
|
+
"""라운드 기록만으로 확정된 적대적 분류. finalize 가 찍은 값과 구별한다."""
|
|
1747
|
+
if not isinstance(rounds, list) or not rounds:
|
|
1748
|
+
return None
|
|
1749
|
+
try:
|
|
1750
|
+
return _classify_adversarial_history(rounds)
|
|
1751
|
+
except ConvergenceContractError:
|
|
1752
|
+
# 표 자체가 계약 위반이면 그 사실은 `_validate_finding_ledger` 가 이미
|
|
1753
|
+
# 별도 오류로 낸다. 여기서는 "라운드 안에서 확정되지 않았다"로만 읽는다.
|
|
1754
|
+
return None
|
|
1755
|
+
|
|
1756
|
+
|
|
1480
1757
|
def _expected_final_classification(
|
|
1481
1758
|
finding: Mapping[str, Any],
|
|
1482
1759
|
adversarial: bool,
|
|
1760
|
+
*,
|
|
1761
|
+
historical: bool = False,
|
|
1483
1762
|
) -> str | None:
|
|
1484
1763
|
rounds = finding.get("rounds")
|
|
1764
|
+
if not historical and _received_no_verdict(finding):
|
|
1765
|
+
return "unverified"
|
|
1485
1766
|
if not isinstance(rounds, list) or not rounds:
|
|
1486
1767
|
return None
|
|
1487
1768
|
if adversarial:
|
|
@@ -1511,16 +1792,28 @@ def _validate_round_ledger_counts(
|
|
|
1511
1792
|
if not isinstance(history_row, Mapping):
|
|
1512
1793
|
continue
|
|
1513
1794
|
ledgers = _round_ledgers(findings, round_number)
|
|
1514
|
-
|
|
1515
|
-
|
|
1795
|
+
# 두 엔진의 산술이 갈라지는 지점은 하나다 — `counter-evidence` 반증.
|
|
1796
|
+
# 지금은 그 라운드에서 큐를 떠나고(현), 구 엔진은 마지막 라운드까지
|
|
1797
|
+
# 끌고 갔다(구). 최종 산출물에는 큐가 남지 않아 어느 엔진이 썼는지
|
|
1798
|
+
# 구별할 수 없으므로 두 산술을 모두 받는다.
|
|
1799
|
+
accepted = {
|
|
1800
|
+
(len(ledgers), resolved, len(ledgers) - resolved)
|
|
1801
|
+
for resolved in {
|
|
1802
|
+
_resolved_ledger_count(findings, round_number, adversarial),
|
|
1803
|
+
_resolved_ledger_count(
|
|
1804
|
+
findings, round_number, adversarial, legacy=True
|
|
1805
|
+
),
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1516
1808
|
actual = (
|
|
1517
1809
|
history_row.get("inputQueueSize"),
|
|
1518
1810
|
history_row.get("resolvedCount"),
|
|
1519
1811
|
history_row.get("carriedForwardCount"),
|
|
1520
1812
|
)
|
|
1521
|
-
if actual
|
|
1813
|
+
if actual not in accepted:
|
|
1522
1814
|
errors.append(
|
|
1523
|
-
f"round {round_number} counters {actual} do not match finding
|
|
1815
|
+
f"round {round_number} counters {actual} do not match finding "
|
|
1816
|
+
f"ledgers {sorted(accepted)}"
|
|
1524
1817
|
)
|
|
1525
1818
|
return errors
|
|
1526
1819
|
|
|
@@ -1548,7 +1841,10 @@ def _resolved_ledger_count(
|
|
|
1548
1841
|
findings: list[Any],
|
|
1549
1842
|
round_number: int,
|
|
1550
1843
|
adversarial: bool,
|
|
1844
|
+
*,
|
|
1845
|
+
legacy: bool = False,
|
|
1551
1846
|
) -> int:
|
|
1847
|
+
"""이 라운드에서 큐를 떠난 finding 수. `legacy` 는 구 엔진 규칙으로 센다."""
|
|
1552
1848
|
resolved = 0
|
|
1553
1849
|
for finding in findings:
|
|
1554
1850
|
rounds = finding.get("rounds") if isinstance(finding, Mapping) else None
|
|
@@ -1569,11 +1865,12 @@ def _resolved_ledger_count(
|
|
|
1569
1865
|
if not isinstance(votes, Mapping):
|
|
1570
1866
|
continue
|
|
1571
1867
|
try:
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1868
|
+
if not adversarial:
|
|
1869
|
+
classification = classify_collaborative_round(votes)
|
|
1870
|
+
elif legacy:
|
|
1871
|
+
classification = _legacy_adversarial_history(current_rounds)
|
|
1872
|
+
else:
|
|
1873
|
+
classification = _classify_adversarial_history(current_rounds)
|
|
1577
1874
|
except ConvergenceContractError:
|
|
1578
1875
|
continue
|
|
1579
1876
|
resolved += classification is not None
|
|
@@ -1671,7 +1968,10 @@ def _validate_no_reappearance_after_resolution(
|
|
|
1671
1968
|
)
|
|
1672
1969
|
except ConvergenceContractError:
|
|
1673
1970
|
continue
|
|
1674
|
-
|
|
1971
|
+
# 중간 라운드가 `contested` 로 읽히는 상태는 구 엔진만 만들 수 있다 —
|
|
1972
|
+
# 현 엔진은 그 라운드에서 큐를 떠나므로 뒤 라운드가 생기지 않고, 그렇게
|
|
1973
|
+
# 종결된 finding 은 `contested` 라 애초에 이 검사를 받지 않는다.
|
|
1974
|
+
if classification is not None and classification != "contested":
|
|
1675
1975
|
return [
|
|
1676
1976
|
f"finding {finding.get('findingId')} reappears after resolution in round {index + 1}"
|
|
1677
1977
|
]
|
|
@@ -2133,9 +2433,21 @@ def _parse_groups(
|
|
|
2133
2433
|
value: Any,
|
|
2134
2434
|
analysis_workers: list[str],
|
|
2135
2435
|
finding_sources: list[str],
|
|
2136
|
-
*,
|
|
2137
|
-
adversarial: bool,
|
|
2138
2436
|
) -> tuple[list[dict[str, Any]], list[str]]:
|
|
2437
|
+
"""Round 0 을 분류하고 검증 큐를 만든다.
|
|
2438
|
+
|
|
2439
|
+
출처 role execution 이 둘 이상인 발견은 두 모드 모두 `full-consensus` 로
|
|
2440
|
+
확정한다. 독립적인 공동 도출이 이미 교차검증이기 때문이다 — 적대적 모드의
|
|
2441
|
+
입증 책임은 단일 출처 주장을 겨냥한다. 두 역할이 각자 도출한 발견을 큐에
|
|
2442
|
+
넣으면 검증자는 새 정보 없이 표를 다시 던지고 라운드만 소모한다.
|
|
2443
|
+
|
|
2444
|
+
`source_workers` 의 길이가 곧 서로 다른 role execution 의 수다.
|
|
2445
|
+
`_parse_group` 이 `analysis_workers` 를 훑어 만들고 그 목록은 workerId 가
|
|
2446
|
+
유일하며(`_parse_workers` 의 duplicate 검사), workerId 와
|
|
2447
|
+
`sourceRoleExecutionRef` 는 1:1 이다
|
|
2448
|
+
(`_validate_worker_execution_identity` 의 duplicate 검사). 한 워커가 두 번
|
|
2449
|
+
세어질 경로가 없다.
|
|
2450
|
+
"""
|
|
2139
2451
|
if not isinstance(value, list):
|
|
2140
2452
|
raise ConvergenceContractError("groups must be an array")
|
|
2141
2453
|
findings: list[dict[str, Any]] = []
|
|
@@ -2150,7 +2462,7 @@ def _parse_groups(
|
|
|
2150
2462
|
finding, source_workers = _parse_group(
|
|
2151
2463
|
group, index, analysis_workers, finding_sources
|
|
2152
2464
|
)
|
|
2153
|
-
if len(source_workers) >= 2
|
|
2465
|
+
if len(source_workers) >= 2:
|
|
2154
2466
|
finding["classification"] = "full-consensus"
|
|
2155
2467
|
else:
|
|
2156
2468
|
queue.append(finding_id)
|