okstra 0.186.7 → 0.188.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 +107 -40
- 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 +39 -38
- 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 +47 -22
- 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/adapters/cmux.md +3 -3
- package/runtime/prompts/lead/convergence.md +99 -88
- 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 +35 -5
- 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 +63 -22
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +20 -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} +16 -2
- 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 +228 -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 +605 -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 +287 -10
- 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 +32 -4
- package/runtime/python/okstra_ctl/contract_graph_cli.py +7 -0
- package/runtime/python/okstra_ctl/convergence.py +107 -16
- 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 +236 -49
- package/runtime/python/okstra_ctl/dispatch_state.py +417 -41
- 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 +28 -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/exact_coverage.py +5 -0
- 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 +319 -17
- 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 +32 -13
- 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 +4 -8
- 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 +213 -16
- 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 +220 -34
- package/runtime/python/okstra_ctl/report_projections.py +8 -2
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +393 -1
- package/runtime/python/okstra_ctl/report_translation.py +2 -35
- package/runtime/python/okstra_ctl/report_view_artifacts.py +42 -2
- 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/verdict_blocks.py +28 -8
- 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 +58 -17
- 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/antigravity.py +50 -12
- 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 +171 -36
- 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 +23 -8
- 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 +3 -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 +1363 -2643
- 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
|
@@ -40,6 +40,7 @@ from okstra_ctl.final_report_paths import (
|
|
|
40
40
|
final_report_data_path,
|
|
41
41
|
final_report_markdown_path,
|
|
42
42
|
is_report_record_path,
|
|
43
|
+
sidecar_source_rel,
|
|
43
44
|
)
|
|
44
45
|
from okstra_ctl.json_boundary import (
|
|
45
46
|
JsonBoundaryError,
|
|
@@ -55,6 +56,25 @@ from okstra_ctl.ids import slugify_task_segment
|
|
|
55
56
|
from okstra_ctl.listing import list_runs, absolute_final_report_path
|
|
56
57
|
from okstra_ctl.paths import resolve_under_root
|
|
57
58
|
from okstra_ctl.run_context import dir_flock
|
|
59
|
+
from .user_response_values import ( # noqa: F401 — 재노출
|
|
60
|
+
AnalysisReviewRecord,
|
|
61
|
+
UserResponseError,
|
|
62
|
+
_ANALYSIS_REVIEW_HEADING_RE,
|
|
63
|
+
_ANALYSIS_REVIEW_STATUSES,
|
|
64
|
+
_ANALYSIS_SIDECAR_HEADING_RE,
|
|
65
|
+
_FRONTMATTER_DELIMITER_RE,
|
|
66
|
+
_NEXT_RESPONSE_HEADING_RE,
|
|
67
|
+
_YAML_FRONTMATTER_RE,
|
|
68
|
+
_authoritative_analysis_review_match,
|
|
69
|
+
_field,
|
|
70
|
+
_nearest_frontmatter_value,
|
|
71
|
+
_nearest_sidecar_filename,
|
|
72
|
+
_parsed_created_at,
|
|
73
|
+
_quoted_review_value,
|
|
74
|
+
_sidecar_metadata_value,
|
|
75
|
+
parse_analysis_review,
|
|
76
|
+
parse_user_response_entries,
|
|
77
|
+
)
|
|
58
78
|
from okstra_ctl.clarification_items import (
|
|
59
79
|
ClarificationItem,
|
|
60
80
|
UNRESOLVED_STATUSES,
|
|
@@ -64,28 +84,12 @@ from okstra_ctl.clarification_items import (
|
|
|
64
84
|
)
|
|
65
85
|
|
|
66
86
|
_PLAN_DECISION_HEADING_RE = re.compile(r"^## PLAN DECISION\s*$", re.MULTILINE)
|
|
67
|
-
_NEXT_RESPONSE_HEADING_RE = re.compile(r"^## ", re.MULTILINE)
|
|
68
|
-
_ANALYSIS_REVIEW_HEADING_RE = re.compile(r"^## ANALYSIS REVIEW\s*$", re.MULTILINE)
|
|
69
87
|
_DIRECTION_SELECTION_HEADING_RE = re.compile(
|
|
70
88
|
r"^## DIRECTION SELECTION\s*$", re.MULTILINE
|
|
71
89
|
)
|
|
72
90
|
_REPORT_AUTHORING_HEADING_RE = re.compile(
|
|
73
91
|
r"^## REPORT AUTHORING\s*$", re.MULTILINE
|
|
74
92
|
)
|
|
75
|
-
_ANALYSIS_SIDECAR_HEADING_RE = re.compile(
|
|
76
|
-
r"^## (?P<filename>user-response-[^\n]+\.md)\s*$", re.MULTILINE
|
|
77
|
-
)
|
|
78
|
-
_YAML_FRONTMATTER_RE = re.compile(
|
|
79
|
-
r"\A---[ \t]*\r?\n(?P<body>.*?)(?:\r?\n)---[ \t]*(?:\r?\n|\Z)",
|
|
80
|
-
re.DOTALL,
|
|
81
|
-
)
|
|
82
|
-
_FRONTMATTER_DELIMITER_RE = re.compile(r"^---[ \t]*$", re.MULTILINE)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
class UserResponseError(ValueError):
|
|
86
|
-
"""Raised when a user-response sidecar violates its supported format."""
|
|
87
|
-
|
|
88
|
-
|
|
89
93
|
@dataclass(frozen=True)
|
|
90
94
|
class PlanDecisionRecord:
|
|
91
95
|
"""sidecar 의 ``## PLAN DECISION`` 블록 + 매칭에 필요한 frontmatter 필드."""
|
|
@@ -100,21 +104,6 @@ class PlanDecisionRecord:
|
|
|
100
104
|
return self.status == PLAN_DECISION_APPROVED
|
|
101
105
|
|
|
102
106
|
|
|
103
|
-
@dataclass(frozen=True)
|
|
104
|
-
class AnalysisReviewRecord:
|
|
105
|
-
"""Validated user review of an analysis report."""
|
|
106
|
-
|
|
107
|
-
status: str
|
|
108
|
-
affected_ids: tuple[str, ...]
|
|
109
|
-
reason: str
|
|
110
|
-
additional_evidence: str
|
|
111
|
-
requested_scope_change: str
|
|
112
|
-
task_key: str
|
|
113
|
-
task_type: str
|
|
114
|
-
source_report: str
|
|
115
|
-
seq: str
|
|
116
|
-
|
|
117
|
-
|
|
118
107
|
@dataclass(frozen=True)
|
|
119
108
|
class DirectionSelectionRecord:
|
|
120
109
|
status: str
|
|
@@ -129,152 +118,12 @@ class DirectionSelectionRecord:
|
|
|
129
118
|
seq: str
|
|
130
119
|
|
|
131
120
|
|
|
132
|
-
_ANALYSIS_REVIEW_STATUSES = frozenset({
|
|
133
|
-
"accepted",
|
|
134
|
-
"revision-requested",
|
|
135
|
-
"rejected",
|
|
136
|
-
})
|
|
137
121
|
_ANALYSIS_REPORT_RE = re.compile(
|
|
138
122
|
r"^final-report-(?P<task_type>project-analysis|feature-analysis|"
|
|
139
123
|
r"change-impact-analysis)-(?P<seq>\d{3})\.(?:md|data\.json)$"
|
|
140
124
|
)
|
|
141
125
|
|
|
142
126
|
|
|
143
|
-
def _quoted_review_value(block: str, key: str) -> str:
|
|
144
|
-
match = re.search(
|
|
145
|
-
rf"^- {re.escape(key)}:\s*\n((?:\s*>.*\n?)+)", block, re.MULTILINE
|
|
146
|
-
)
|
|
147
|
-
if not match:
|
|
148
|
-
return ""
|
|
149
|
-
return "\n".join(
|
|
150
|
-
re.sub(r"^\s*>\s?", "", line)
|
|
151
|
-
for line in match.group(1).splitlines()
|
|
152
|
-
).strip()
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def _sidecar_metadata_value(sidecar_text: str, key: str) -> str:
|
|
156
|
-
frontmatter = _YAML_FRONTMATTER_RE.match(sidecar_text)
|
|
157
|
-
if frontmatter is None:
|
|
158
|
-
return ""
|
|
159
|
-
match = re.search(
|
|
160
|
-
rf"^{re.escape(key)}:[ \t]*(\S.*?)[ \t]*$",
|
|
161
|
-
frontmatter.group("body"),
|
|
162
|
-
re.MULTILINE,
|
|
163
|
-
)
|
|
164
|
-
return match.group(1) if match else ""
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def _nearest_frontmatter_value(
|
|
168
|
-
sidecar_text: str, key: str, before_offset: int
|
|
169
|
-
) -> str:
|
|
170
|
-
delimiters = [
|
|
171
|
-
match
|
|
172
|
-
for match in _FRONTMATTER_DELIMITER_RE.finditer(sidecar_text)
|
|
173
|
-
if match.start() < before_offset
|
|
174
|
-
]
|
|
175
|
-
for opening, closing in reversed(list(zip(delimiters, delimiters[1:]))):
|
|
176
|
-
body = sidecar_text[opening.end():closing.start()]
|
|
177
|
-
match = re.search(
|
|
178
|
-
rf"^{re.escape(key)}:[ \t]*(\S.*?)[ \t]*$",
|
|
179
|
-
body,
|
|
180
|
-
re.MULTILINE,
|
|
181
|
-
)
|
|
182
|
-
if match:
|
|
183
|
-
return match.group(1)
|
|
184
|
-
return _sidecar_metadata_value(sidecar_text, key)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def _parsed_created_at(value: str) -> dt.datetime | None:
|
|
188
|
-
if re.fullmatch(
|
|
189
|
-
r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z",
|
|
190
|
-
value,
|
|
191
|
-
) is None:
|
|
192
|
-
return None
|
|
193
|
-
try:
|
|
194
|
-
parsed = dt.datetime.fromisoformat(value[:-1] + "+00:00")
|
|
195
|
-
except ValueError:
|
|
196
|
-
return None
|
|
197
|
-
if parsed.tzinfo is None:
|
|
198
|
-
return None
|
|
199
|
-
return parsed.astimezone(dt.timezone.utc)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def _nearest_sidecar_filename(sidecar_text: str, before_offset: int) -> str:
|
|
203
|
-
headings = [
|
|
204
|
-
match
|
|
205
|
-
for match in _ANALYSIS_SIDECAR_HEADING_RE.finditer(sidecar_text)
|
|
206
|
-
if match.start() < before_offset
|
|
207
|
-
]
|
|
208
|
-
return headings[-1].group("filename") if headings else ""
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
def _authoritative_analysis_review_match(
|
|
212
|
-
sidecar_text: str, matches: list[re.Match[str]]
|
|
213
|
-
) -> re.Match[str]:
|
|
214
|
-
candidates: list[tuple[dt.datetime, str, int, re.Match[str]]] = []
|
|
215
|
-
for match in matches:
|
|
216
|
-
created_at = _parsed_created_at(
|
|
217
|
-
_nearest_frontmatter_value(sidecar_text, "created-at", match.start())
|
|
218
|
-
)
|
|
219
|
-
if created_at is None:
|
|
220
|
-
continue
|
|
221
|
-
candidates.append((
|
|
222
|
-
created_at,
|
|
223
|
-
_nearest_sidecar_filename(sidecar_text, match.start()),
|
|
224
|
-
match.start(),
|
|
225
|
-
match,
|
|
226
|
-
))
|
|
227
|
-
if not candidates:
|
|
228
|
-
raise UserResponseError(
|
|
229
|
-
"ANALYSIS REVIEW requires a valid canonical created-at"
|
|
230
|
-
)
|
|
231
|
-
return max(candidates, key=lambda candidate: candidate[:3])[3]
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
def parse_analysis_review(sidecar_text: str) -> AnalysisReviewRecord | None:
|
|
235
|
-
"""Parse the bounded ``## ANALYSIS REVIEW`` block from a sidecar.
|
|
236
|
-
|
|
237
|
-
A sidecar without this optional block is not a review. Once the block is
|
|
238
|
-
present, status and the fields that make a rejection actionable are
|
|
239
|
-
validated fail-closed.
|
|
240
|
-
"""
|
|
241
|
-
matches = list(_ANALYSIS_REVIEW_HEADING_RE.finditer(sidecar_text))
|
|
242
|
-
if not matches:
|
|
243
|
-
return None
|
|
244
|
-
match = _authoritative_analysis_review_match(sidecar_text, matches)
|
|
245
|
-
block = sidecar_text[match.end():]
|
|
246
|
-
next_heading = _NEXT_RESPONSE_HEADING_RE.search(block)
|
|
247
|
-
if next_heading:
|
|
248
|
-
block = block[:next_heading.start()]
|
|
249
|
-
status = _field(block, "Status")
|
|
250
|
-
if status not in _ANALYSIS_REVIEW_STATUSES:
|
|
251
|
-
raise UserResponseError("ANALYSIS REVIEW Status is invalid")
|
|
252
|
-
affected = _field(block, "Affected-IDs") or ""
|
|
253
|
-
affected_ids = tuple(item.strip() for item in affected.split(",") if item.strip())
|
|
254
|
-
reason = _quoted_review_value(block, "Reason")
|
|
255
|
-
if status in {"revision-requested", "rejected"} and (not affected_ids or not reason):
|
|
256
|
-
raise UserResponseError(
|
|
257
|
-
f"ANALYSIS REVIEW {status} requires Affected-IDs and Reason"
|
|
258
|
-
)
|
|
259
|
-
return AnalysisReviewRecord(
|
|
260
|
-
status=status,
|
|
261
|
-
affected_ids=affected_ids,
|
|
262
|
-
reason=reason,
|
|
263
|
-
additional_evidence=_quoted_review_value(block, "Additional-Evidence"),
|
|
264
|
-
requested_scope_change=_quoted_review_value(block, "Requested-Scope-Change"),
|
|
265
|
-
task_key=_nearest_frontmatter_value(
|
|
266
|
-
sidecar_text, "task-key", match.start()
|
|
267
|
-
),
|
|
268
|
-
task_type=_nearest_frontmatter_value(
|
|
269
|
-
sidecar_text, "task-type", match.start()
|
|
270
|
-
),
|
|
271
|
-
source_report=_nearest_frontmatter_value(
|
|
272
|
-
sidecar_text, "source-report", match.start()
|
|
273
|
-
),
|
|
274
|
-
seq=_nearest_frontmatter_value(sidecar_text, "seq", match.start()),
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
|
|
278
127
|
def _matching_analysis_review_sidecars(
|
|
279
128
|
report_path: Path,
|
|
280
129
|
sidecar_name: re.Pattern[str],
|
|
@@ -474,16 +323,6 @@ def parse_report_authoring(sidecar_text: str) -> UserReportAuthoring | None:
|
|
|
474
323
|
return UserReportAuthoring(status=status, reason=reason)
|
|
475
324
|
|
|
476
325
|
|
|
477
|
-
_RESPONSE_HEADING_RE = re.compile(r"^## (?P<id>[A-Za-z][A-Za-z0-9]*-\d+)\s*$", re.MULTILINE)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
def _field(block: str, key: str) -> Optional[str]:
|
|
481
|
-
m = re.search(
|
|
482
|
-
rf"^- {re.escape(key)}:[ \t]*(\S.*?)[ \t]*$", block, re.MULTILINE
|
|
483
|
-
)
|
|
484
|
-
return m.group(1) if m else None
|
|
485
|
-
|
|
486
|
-
|
|
487
326
|
def _direction_identity_fields(block: str) -> tuple[str, str]:
|
|
488
327
|
match = re.search(
|
|
489
328
|
r"^- Option-ID:[ \t]*(?P<option_id>[^\r\n]*)\r?\n"
|
|
@@ -497,15 +336,6 @@ def _direction_identity_fields(block: str) -> tuple[str, str]:
|
|
|
497
336
|
return match.group("option_id"), match.group("option_name")
|
|
498
337
|
|
|
499
338
|
|
|
500
|
-
def _value(block: str) -> str:
|
|
501
|
-
# Value 는 "- Value:" 다음 줄들의 " > " 인용 블록.
|
|
502
|
-
m = re.search(r"^- Value:\s*\n((?:\s*>.*\n?)+)", block, re.MULTILINE)
|
|
503
|
-
if not m:
|
|
504
|
-
return ""
|
|
505
|
-
lines = [re.sub(r"^\s*>\s?", "", ln) for ln in m.group(1).splitlines()]
|
|
506
|
-
return "\n".join(lines).strip()
|
|
507
|
-
|
|
508
|
-
|
|
509
339
|
def parse_direction_selection(
|
|
510
340
|
sidecar_text: str,
|
|
511
341
|
) -> DirectionSelectionRecord | None:
|
|
@@ -550,26 +380,6 @@ def parse_direction_selection(
|
|
|
550
380
|
)
|
|
551
381
|
|
|
552
382
|
|
|
553
|
-
def parse_user_response_entries(sidecar_text: str) -> list[UserResponseEntry]:
|
|
554
|
-
"""Reverse of ``serialize_user_response`` for the per-response ``## C-*``
|
|
555
|
-
blocks. The ``## PLAN DECISION`` block is skipped (read separately by
|
|
556
|
-
``parse_plan_decision``)."""
|
|
557
|
-
entries: list[UserResponseEntry] = []
|
|
558
|
-
matches = list(_RESPONSE_HEADING_RE.finditer(sidecar_text))
|
|
559
|
-
for i, m in enumerate(matches):
|
|
560
|
-
end = matches[i + 1].start() if i + 1 < len(matches) else len(sidecar_text)
|
|
561
|
-
block = sidecar_text[m.end():end]
|
|
562
|
-
rationale = _field(block, "Rationale")
|
|
563
|
-
entries.append(UserResponseEntry(
|
|
564
|
-
response_id=m.group("id"),
|
|
565
|
-
kind=_field(block, "Kind") or "",
|
|
566
|
-
value=_value(block),
|
|
567
|
-
rationale=rationale,
|
|
568
|
-
disposition=_field(block, "Disposition") or "answer",
|
|
569
|
-
))
|
|
570
|
-
return entries
|
|
571
|
-
|
|
572
|
-
|
|
573
383
|
_SEQ_FROM_REPORT_RE = re.compile(
|
|
574
384
|
r"final-report-.+-(\w+)\.(?:md|data\.json)$"
|
|
575
385
|
)
|
|
@@ -633,9 +443,6 @@ def _load_report_record(
|
|
|
633
443
|
return record
|
|
634
444
|
|
|
635
445
|
|
|
636
|
-
def _relative_from_runs(path: Path) -> str:
|
|
637
|
-
indices = [index for index, part in enumerate(path.parts) if part == "runs"]
|
|
638
|
-
return Path(*path.parts[indices[-1]:]).as_posix() if indices else path.name
|
|
639
446
|
|
|
640
447
|
|
|
641
448
|
def _record_identity(record: Mapping[str, Any]) -> tuple[str, str]:
|
|
@@ -678,7 +485,7 @@ def resolve_report_context(
|
|
|
678
485
|
raise UserResponseError("report filename has no run sequence")
|
|
679
486
|
run_root = resolved.parent.parent
|
|
680
487
|
data_path = final_report_data_path(resolved) if record else None
|
|
681
|
-
source_data =
|
|
488
|
+
source_data = sidecar_source_rel(data_path) if data_path else ""
|
|
682
489
|
task_type_root = run_root.parent if run_root.name.startswith("stage-") else run_root
|
|
683
490
|
task_root = task_type_root.parent.parent
|
|
684
491
|
project_root = task_root.parents[3] if len(task_root.parents) >= 4 else task_root
|
|
@@ -689,7 +496,7 @@ def resolve_report_context(
|
|
|
689
496
|
task_key=task_key,
|
|
690
497
|
task_type=task_type,
|
|
691
498
|
seq=seq,
|
|
692
|
-
source_report=
|
|
499
|
+
source_report=sidecar_source_rel(markdown),
|
|
693
500
|
source_data=source_data,
|
|
694
501
|
source_data_sha256=(
|
|
695
502
|
hashlib.sha256(data_path.read_bytes()).hexdigest() if data_path else ""
|
|
@@ -764,9 +571,14 @@ def _validate_owned_report_context(
|
|
|
764
571
|
raise UserResponseError("report resolves outside the selected project")
|
|
765
572
|
_reject_symlinks_below(project_root, data_path, "report")
|
|
766
573
|
markdown = _lexical_absolute(final_report_markdown_path(data_path))
|
|
574
|
+
# 읽기 사본(`.md`)은 on-demand 로만 렌더된다. 타입드 트랜잭션은 바로 아래에서
|
|
575
|
+
# 버전 있는 레코드를 요구하고, 그 경로는 `.md` 본문을 한 번도 읽지 않는다
|
|
576
|
+
# (schema-v1 만 `markdown_path` 를 읽으며, 그때 그 경로는 리포트 자신이다).
|
|
577
|
+
# 존재를 요구하면 `.md` 를 렌더한 적 없는 리포트의 응답 채널이 통째로 닫힌다.
|
|
578
|
+
# 경로는 sidecar 의 `source-report` 신원 문자열로만 쓰이므로 계산은 유지한다.
|
|
767
579
|
_reject_symlinks_below(project_root, markdown, "report reading copy")
|
|
768
|
-
if not data_path.is_file()
|
|
769
|
-
raise UserResponseError("report record
|
|
580
|
+
if not data_path.is_file():
|
|
581
|
+
raise UserResponseError("report record must be a regular file")
|
|
770
582
|
record = _load_report_record(data_path, validate_schema=True)
|
|
771
583
|
if record is None:
|
|
772
584
|
raise UserResponseError("typed transactions require a versioned report record")
|
|
@@ -2283,8 +2095,33 @@ def _add_list_arguments(parser: argparse.ArgumentParser) -> None:
|
|
|
2283
2095
|
parser.add_argument("--limit", type=int, default=3)
|
|
2284
2096
|
|
|
2285
2097
|
|
|
2098
|
+
_CLI_EPILOG = r"""Usage:
|
|
2099
|
+
okstra user-response list --home <dir> --project <id> [--limit <n>]
|
|
2100
|
+
okstra user-response list-view --home <dir> --project <id> [--limit <n>]
|
|
2101
|
+
okstra user-response show --report <md>
|
|
2102
|
+
okstra user-response show-view --report <md|data.json> --project-root <dir>
|
|
2103
|
+
okstra user-response begin --report <md|data.json> --task-key <key>
|
|
2104
|
+
okstra user-response answer --transaction <id> --clarification-id <C-NNN> \
|
|
2105
|
+
--kind <kind> --option-number <N>
|
|
2106
|
+
okstra user-response answer --transaction <id> --clarification-id <C-NNN> \
|
|
2107
|
+
--kind <kind> --disposition <value> --value-file <md> [--rationale-file <md>]
|
|
2108
|
+
okstra user-response plan-decision --transaction <id> --status <value> \
|
|
2109
|
+
[--implementation-option <name>] [--reason-file <md>]
|
|
2110
|
+
okstra user-response legacy-report-authoring --transaction <id> \
|
|
2111
|
+
--status <approved|denied> --reason-file <md>
|
|
2112
|
+
okstra user-response finalize --transaction <id>
|
|
2113
|
+
|
|
2114
|
+
Exit codes: 0 ok / 1 오류
|
|
2115
|
+
"""
|
|
2116
|
+
_CLI_DESCRIPTION = "\uc5f4\ub9b0 clarification\uc744 \uace0\uc815 \ud14d\uc2a4\ud2b8\ub85c \uc77d\uace0 sidecar \ud2b8\ub79c\uc7ad\uc158 \uae30\ub85d."
|
|
2117
|
+
|
|
2118
|
+
|
|
2286
2119
|
def _build_parser() -> argparse.ArgumentParser:
|
|
2287
|
-
parser = argparse.ArgumentParser(
|
|
2120
|
+
parser = argparse.ArgumentParser(
|
|
2121
|
+
description=_CLI_DESCRIPTION,
|
|
2122
|
+
epilog=_CLI_EPILOG,
|
|
2123
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
2124
|
+
prog="okstra user-response")
|
|
2288
2125
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
2289
2126
|
_add_list_arguments(sub.add_parser("list"))
|
|
2290
2127
|
_add_list_arguments(sub.add_parser("list-view"))
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""사용자 응답 사이드카의 값 타입과 파싱.
|
|
2
|
+
|
|
3
|
+
`user_response`(2,383줄) 와 `report_views`(1,000줄+) 에 흩어져 있던
|
|
4
|
+
자족적인 값·파싱 단위다. 어떤 사이드카를 분석
|
|
5
|
+
리포트에 붙일지 고르는 `clarification_items._sidecars_for_attachment` 가
|
|
6
|
+
`parse_analysis_review` 하나 때문에 `user_response` 전체를 함수 본문에서 지연
|
|
7
|
+
import 해야 했다 — `user_response` 는 반대로 `clarification_items` 를 최상위에서
|
|
8
|
+
끌어오기 때문이다.
|
|
9
|
+
|
|
10
|
+
이 모듈은 표준 라이브러리 외에 아무것도 import 하지 않는다. `user_response` 가
|
|
11
|
+
모든 이름을 재노출하므로 기존 호출부는 바뀌지 않는다.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import datetime as dt
|
|
16
|
+
import re
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_NEXT_RESPONSE_HEADING_RE = re.compile(r"^## ", re.MULTILINE)
|
|
22
|
+
|
|
23
|
+
_ANALYSIS_REVIEW_HEADING_RE = re.compile(r"^## ANALYSIS REVIEW\s*$", re.MULTILINE)
|
|
24
|
+
|
|
25
|
+
_ANALYSIS_SIDECAR_HEADING_RE = re.compile(
|
|
26
|
+
r"^## (?P<filename>user-response-[^\n]+\.md)\s*$", re.MULTILINE
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
_YAML_FRONTMATTER_RE = re.compile(
|
|
30
|
+
r"\A---[ \t]*\r?\n(?P<body>.*?)(?:\r?\n)---[ \t]*(?:\r?\n|\Z)",
|
|
31
|
+
re.DOTALL,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_FRONTMATTER_DELIMITER_RE = re.compile(r"^---[ \t]*$", re.MULTILINE)
|
|
35
|
+
|
|
36
|
+
class UserResponseError(ValueError):
|
|
37
|
+
"""Raised when a user-response sidecar violates its supported format."""
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class AnalysisReviewRecord:
|
|
41
|
+
"""Validated user review of an analysis report."""
|
|
42
|
+
|
|
43
|
+
status: str
|
|
44
|
+
affected_ids: tuple[str, ...]
|
|
45
|
+
reason: str
|
|
46
|
+
additional_evidence: str
|
|
47
|
+
requested_scope_change: str
|
|
48
|
+
task_key: str
|
|
49
|
+
task_type: str
|
|
50
|
+
source_report: str
|
|
51
|
+
seq: str
|
|
52
|
+
|
|
53
|
+
_ANALYSIS_REVIEW_STATUSES = frozenset({
|
|
54
|
+
"accepted",
|
|
55
|
+
"revision-requested",
|
|
56
|
+
"rejected",
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
def _quoted_review_value(block: str, key: str) -> str:
|
|
60
|
+
match = re.search(
|
|
61
|
+
rf"^- {re.escape(key)}:\s*\n((?:\s*>.*\n?)+)", block, re.MULTILINE
|
|
62
|
+
)
|
|
63
|
+
if not match:
|
|
64
|
+
return ""
|
|
65
|
+
return "\n".join(
|
|
66
|
+
re.sub(r"^\s*>\s?", "", line)
|
|
67
|
+
for line in match.group(1).splitlines()
|
|
68
|
+
).strip()
|
|
69
|
+
|
|
70
|
+
def _sidecar_metadata_value(sidecar_text: str, key: str) -> str:
|
|
71
|
+
frontmatter = _YAML_FRONTMATTER_RE.match(sidecar_text)
|
|
72
|
+
if frontmatter is None:
|
|
73
|
+
return ""
|
|
74
|
+
match = re.search(
|
|
75
|
+
rf"^{re.escape(key)}:[ \t]*(\S.*?)[ \t]*$",
|
|
76
|
+
frontmatter.group("body"),
|
|
77
|
+
re.MULTILINE,
|
|
78
|
+
)
|
|
79
|
+
return match.group(1) if match else ""
|
|
80
|
+
|
|
81
|
+
def _nearest_frontmatter_value(
|
|
82
|
+
sidecar_text: str, key: str, before_offset: int
|
|
83
|
+
) -> str:
|
|
84
|
+
delimiters = [
|
|
85
|
+
match
|
|
86
|
+
for match in _FRONTMATTER_DELIMITER_RE.finditer(sidecar_text)
|
|
87
|
+
if match.start() < before_offset
|
|
88
|
+
]
|
|
89
|
+
for opening, closing in reversed(list(zip(delimiters, delimiters[1:]))):
|
|
90
|
+
body = sidecar_text[opening.end():closing.start()]
|
|
91
|
+
match = re.search(
|
|
92
|
+
rf"^{re.escape(key)}:[ \t]*(\S.*?)[ \t]*$",
|
|
93
|
+
body,
|
|
94
|
+
re.MULTILINE,
|
|
95
|
+
)
|
|
96
|
+
if match:
|
|
97
|
+
return match.group(1)
|
|
98
|
+
return _sidecar_metadata_value(sidecar_text, key)
|
|
99
|
+
|
|
100
|
+
def _parsed_created_at(value: str) -> dt.datetime | None:
|
|
101
|
+
if re.fullmatch(
|
|
102
|
+
r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z",
|
|
103
|
+
value,
|
|
104
|
+
) is None:
|
|
105
|
+
return None
|
|
106
|
+
try:
|
|
107
|
+
parsed = dt.datetime.fromisoformat(value[:-1] + "+00:00")
|
|
108
|
+
except ValueError:
|
|
109
|
+
return None
|
|
110
|
+
if parsed.tzinfo is None:
|
|
111
|
+
return None
|
|
112
|
+
return parsed.astimezone(dt.timezone.utc)
|
|
113
|
+
|
|
114
|
+
def _nearest_sidecar_filename(sidecar_text: str, before_offset: int) -> str:
|
|
115
|
+
headings = [
|
|
116
|
+
match
|
|
117
|
+
for match in _ANALYSIS_SIDECAR_HEADING_RE.finditer(sidecar_text)
|
|
118
|
+
if match.start() < before_offset
|
|
119
|
+
]
|
|
120
|
+
return headings[-1].group("filename") if headings else ""
|
|
121
|
+
|
|
122
|
+
def _authoritative_analysis_review_match(
|
|
123
|
+
sidecar_text: str, matches: list[re.Match[str]]
|
|
124
|
+
) -> re.Match[str]:
|
|
125
|
+
candidates: list[tuple[dt.datetime, str, int, re.Match[str]]] = []
|
|
126
|
+
for match in matches:
|
|
127
|
+
created_at = _parsed_created_at(
|
|
128
|
+
_nearest_frontmatter_value(sidecar_text, "created-at", match.start())
|
|
129
|
+
)
|
|
130
|
+
if created_at is None:
|
|
131
|
+
continue
|
|
132
|
+
candidates.append((
|
|
133
|
+
created_at,
|
|
134
|
+
_nearest_sidecar_filename(sidecar_text, match.start()),
|
|
135
|
+
match.start(),
|
|
136
|
+
match,
|
|
137
|
+
))
|
|
138
|
+
if not candidates:
|
|
139
|
+
raise UserResponseError(
|
|
140
|
+
"ANALYSIS REVIEW requires a valid canonical created-at"
|
|
141
|
+
)
|
|
142
|
+
return max(candidates, key=lambda candidate: candidate[:3])[3]
|
|
143
|
+
|
|
144
|
+
def parse_analysis_review(sidecar_text: str) -> AnalysisReviewRecord | None:
|
|
145
|
+
"""Parse the bounded ``## ANALYSIS REVIEW`` block from a sidecar.
|
|
146
|
+
|
|
147
|
+
A sidecar without this optional block is not a review. Once the block is
|
|
148
|
+
present, status and the fields that make a rejection actionable are
|
|
149
|
+
validated fail-closed.
|
|
150
|
+
"""
|
|
151
|
+
matches = list(_ANALYSIS_REVIEW_HEADING_RE.finditer(sidecar_text))
|
|
152
|
+
if not matches:
|
|
153
|
+
return None
|
|
154
|
+
match = _authoritative_analysis_review_match(sidecar_text, matches)
|
|
155
|
+
block = sidecar_text[match.end():]
|
|
156
|
+
next_heading = _NEXT_RESPONSE_HEADING_RE.search(block)
|
|
157
|
+
if next_heading:
|
|
158
|
+
block = block[:next_heading.start()]
|
|
159
|
+
status = _field(block, "Status")
|
|
160
|
+
if status not in _ANALYSIS_REVIEW_STATUSES:
|
|
161
|
+
raise UserResponseError("ANALYSIS REVIEW Status is invalid")
|
|
162
|
+
affected = _field(block, "Affected-IDs") or ""
|
|
163
|
+
affected_ids = tuple(item.strip() for item in affected.split(",") if item.strip())
|
|
164
|
+
reason = _quoted_review_value(block, "Reason")
|
|
165
|
+
if status in {"revision-requested", "rejected"} and (not affected_ids or not reason):
|
|
166
|
+
raise UserResponseError(
|
|
167
|
+
f"ANALYSIS REVIEW {status} requires Affected-IDs and Reason"
|
|
168
|
+
)
|
|
169
|
+
return AnalysisReviewRecord(
|
|
170
|
+
status=status,
|
|
171
|
+
affected_ids=affected_ids,
|
|
172
|
+
reason=reason,
|
|
173
|
+
additional_evidence=_quoted_review_value(block, "Additional-Evidence"),
|
|
174
|
+
requested_scope_change=_quoted_review_value(block, "Requested-Scope-Change"),
|
|
175
|
+
task_key=_nearest_frontmatter_value(
|
|
176
|
+
sidecar_text, "task-key", match.start()
|
|
177
|
+
),
|
|
178
|
+
task_type=_nearest_frontmatter_value(
|
|
179
|
+
sidecar_text, "task-type", match.start()
|
|
180
|
+
),
|
|
181
|
+
source_report=_nearest_frontmatter_value(
|
|
182
|
+
sidecar_text, "source-report", match.start()
|
|
183
|
+
),
|
|
184
|
+
seq=_nearest_frontmatter_value(sidecar_text, "seq", match.start()),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def _field(block: str, key: str) -> Optional[str]:
|
|
188
|
+
m = re.search(
|
|
189
|
+
rf"^- {re.escape(key)}:[ \t]*(\S.*?)[ \t]*$", block, re.MULTILINE
|
|
190
|
+
)
|
|
191
|
+
return m.group(1) if m else None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@dataclass(frozen=True)
|
|
195
|
+
class UserResponseEntry:
|
|
196
|
+
response_id: str
|
|
197
|
+
kind: str
|
|
198
|
+
value: str
|
|
199
|
+
rationale: Optional[str] = None
|
|
200
|
+
disposition: str = "answer"
|
|
201
|
+
|
|
202
|
+
@dataclass(frozen=True)
|
|
203
|
+
class UserPlanDecision:
|
|
204
|
+
"""HTML 계획 결정 위젯의 Export 결과.
|
|
205
|
+
|
|
206
|
+
``status`` 가 승인이 아니면 ``reason`` 이 필수다 — 사유 없는 반려는 다음
|
|
207
|
+
run 이 무엇을 고쳐야 하는지 알 수 없어 되돌아올 수밖에 없다.
|
|
208
|
+
``implementation_option`` 이 빈 문자열이면 라인을 생략한다 (소비 측은
|
|
209
|
+
Recommended Option 폴백)."""
|
|
210
|
+
status: str
|
|
211
|
+
implementation_option: str = ""
|
|
212
|
+
reason: str = ""
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
_RESPONSE_HEADING_RE = re.compile(r"^## (?P<id>[A-Za-z][A-Za-z0-9]*-\d+)\s*$", re.MULTILINE)
|
|
216
|
+
|
|
217
|
+
def _value(block: str) -> str:
|
|
218
|
+
# Value 는 "- Value:" 다음 줄들의 " > " 인용 블록.
|
|
219
|
+
m = re.search(r"^- Value:\s*\n((?:\s*>.*\n?)+)", block, re.MULTILINE)
|
|
220
|
+
if not m:
|
|
221
|
+
return ""
|
|
222
|
+
lines = [re.sub(r"^\s*>\s?", "", ln) for ln in m.group(1).splitlines()]
|
|
223
|
+
return "\n".join(lines).strip()
|
|
224
|
+
|
|
225
|
+
def parse_user_response_entries(sidecar_text: str) -> list[UserResponseEntry]:
|
|
226
|
+
"""Reverse of ``serialize_user_response`` for the per-response ``## C-*``
|
|
227
|
+
blocks. The ``## PLAN DECISION`` block is skipped (read separately by
|
|
228
|
+
``parse_plan_decision``)."""
|
|
229
|
+
entries: list[UserResponseEntry] = []
|
|
230
|
+
matches = list(_RESPONSE_HEADING_RE.finditer(sidecar_text))
|
|
231
|
+
for i, m in enumerate(matches):
|
|
232
|
+
end = matches[i + 1].start() if i + 1 < len(matches) else len(sidecar_text)
|
|
233
|
+
block = sidecar_text[m.end():end]
|
|
234
|
+
rationale = _field(block, "Rationale")
|
|
235
|
+
entries.append(UserResponseEntry(
|
|
236
|
+
response_id=m.group("id"),
|
|
237
|
+
kind=_field(block, "Kind") or "",
|
|
238
|
+
value=_value(block),
|
|
239
|
+
rationale=rationale,
|
|
240
|
+
disposition=_field(block, "Disposition") or "answer",
|
|
241
|
+
))
|
|
242
|
+
return entries
|
|
@@ -106,10 +106,33 @@ def parse_finding_votes(text: str, *, adversarial: bool) -> dict[str, FindingVot
|
|
|
106
106
|
an unfamiliar token silently read as a different verdict.
|
|
107
107
|
"""
|
|
108
108
|
vocabulary = ADVERSARIAL_VERDICTS if adversarial else COLLABORATIVE_VERDICTS
|
|
109
|
-
return
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
return _collect_blocks(
|
|
110
|
+
_scan_blocks(text),
|
|
111
|
+
lambda item_id, fields: _finding_vote(item_id, fields, vocabulary),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _collect_blocks(blocks: dict[str, dict[str, str]], parse) -> dict:
|
|
116
|
+
"""모든 블록을 읽고 결함을 한 번에 보고한다.
|
|
117
|
+
|
|
118
|
+
첫 결함에서 멈추면 15건 중 13건이 같은 결함인 응답도 한 건씩만 드러나,
|
|
119
|
+
교정 디스패치를 그 수만큼 반복하게 된다(2026-09-02 실측). 한 응답의 결함은
|
|
120
|
+
서로 독립이므로 전부 모아 하나의 오류로 낸다.
|
|
121
|
+
"""
|
|
122
|
+
parsed: dict = {}
|
|
123
|
+
errors: list[str] = []
|
|
124
|
+
for item_id, fields in blocks.items():
|
|
125
|
+
try:
|
|
126
|
+
parsed[item_id] = parse(item_id, fields)
|
|
127
|
+
except VerdictBlockError as exc:
|
|
128
|
+
errors.append(str(exc))
|
|
129
|
+
if errors:
|
|
130
|
+
if len(errors) == 1:
|
|
131
|
+
raise VerdictBlockError(errors[0])
|
|
132
|
+
raise VerdictBlockError(
|
|
133
|
+
f"{len(errors)} of {len(blocks)} blocks are malformed: " + "; ".join(errors)
|
|
134
|
+
)
|
|
135
|
+
return parsed
|
|
113
136
|
|
|
114
137
|
|
|
115
138
|
def _finding_vote(
|
|
@@ -155,10 +178,7 @@ class VerdictBlock:
|
|
|
155
178
|
|
|
156
179
|
def parse_verdict_blocks(text: str) -> dict[str, VerdictBlock]:
|
|
157
180
|
"""Every `### <item-id>` plan-body verdict block in *text*, keyed by item id."""
|
|
158
|
-
return
|
|
159
|
-
item_id: _block(item_id, fields)
|
|
160
|
-
for item_id, fields in _scan_blocks(text).items()
|
|
161
|
-
}
|
|
181
|
+
return _collect_blocks(_scan_blocks(text), _block)
|
|
162
182
|
|
|
163
183
|
|
|
164
184
|
def _verdict_token(item_id: str, raw: str) -> tuple[str, str]:
|