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
|
@@ -1,1050 +0,0 @@
|
|
|
1
|
-
"""Read a final-report's clarification rows, whichever schema wrote them.
|
|
2
|
-
|
|
3
|
-
A clarification is what a run owes the user — a decision, a file attachment,
|
|
4
|
-
a single data point. Each row carries a ``Blocks`` value out of
|
|
5
|
-
``{approval, next-phase, none}``. Rows with ``Blocks=approval`` are the
|
|
6
|
-
approval gate: they MUST resolve before the user flips the frontmatter
|
|
7
|
-
``approved`` field to ``true`` and starts the next ``implementation`` run.
|
|
8
|
-
A ``request-revision`` / ``reject`` answer still blocks the report that has
|
|
9
|
-
not yet incorporated it. The report that recorded the same id in
|
|
10
|
-
``supersessionLedger`` has already absorbed that return and does not block.
|
|
11
|
-
|
|
12
|
-
The two schemas store those rows in different places, and that is why the
|
|
13
|
-
read functions take a report **path**, not its text:
|
|
14
|
-
|
|
15
|
-
* schema-v1 — the ``## 1. Clarification Items`` markdown table (introduced
|
|
16
|
-
when §4.5.9 / §5.1 / §5.2 collapsed into a single section).
|
|
17
|
-
* schema-v2 — ``clarificationItems[]`` in the ``.data.json`` sibling. Its AI
|
|
18
|
-
handoff markdown renders them as one section per row under
|
|
19
|
-
``## Clarification and User Decisions``, not as a §1 table, so the §1 table
|
|
20
|
-
walk finds nothing there by construction.
|
|
21
|
-
|
|
22
|
-
Every gate goes through ``scan_approval_gate`` / ``scan_open_user_input`` so
|
|
23
|
-
run-prep (``_validate_approved_plan``), the wizard, and the user-response CLI
|
|
24
|
-
cannot disagree about what is still open.
|
|
25
|
-
|
|
26
|
-
Gate semantics are fail-closed: when the rows cannot be read with confidence
|
|
27
|
-
(§1 heading missing/drifted, table header unrecognized, a body row whose
|
|
28
|
-
metadata cell fails to parse, or a v2 row missing id/blocks/status), the scan
|
|
29
|
-
reports an ``unreadable_reason`` and callers must refuse approval instead of
|
|
30
|
-
soft-passing. ``parse_clarification_items`` keeps the lenient
|
|
31
|
-
None-on-absence contract for the schema-v1 HTML-view renderers, which only
|
|
32
|
-
need best-effort row extraction.
|
|
33
|
-
"""
|
|
34
|
-
from __future__ import annotations
|
|
35
|
-
|
|
36
|
-
import json
|
|
37
|
-
import re
|
|
38
|
-
from collections.abc import Mapping
|
|
39
|
-
from dataclasses import dataclass
|
|
40
|
-
from pathlib import Path
|
|
41
|
-
from typing import Optional
|
|
42
|
-
|
|
43
|
-
from okstra_ctl.final_report_paths import final_report_data_path
|
|
44
|
-
from okstra_ctl.json_boundary import load_owned_object
|
|
45
|
-
from okstra_ctl.md_table import is_separator_row, split_pipe_row, to_cell_text
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
# The final-report renderer (render_final_report.py:_inject_anchors) appends a
|
|
49
|
-
# ` <a id="slug"></a>` scroll anchor to every heading. The §1 slice MUST tolerate
|
|
50
|
-
# that trailing anchor — otherwise `_section_1_slice` fails on every *rendered*
|
|
51
|
-
# report, parse returns None, and the Blocks=approval approval gate is silently
|
|
52
|
-
# bypassed (run.py:_validate_approved_plan soft-passes None).
|
|
53
|
-
SECTION_HEADING_PATTERN = re.compile(
|
|
54
|
-
r'^##\s+1\.\s+Clarification Items\s*(?:<a id="[^"]+"></a>\s*)?$',
|
|
55
|
-
re.MULTILINE,
|
|
56
|
-
)
|
|
57
|
-
NEXT_TOP_LEVEL_HEADING_PATTERN = re.compile(r"^##\s+(?!1\.)", re.MULTILINE)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
@dataclass(frozen=True)
|
|
61
|
-
class ClarificationItem:
|
|
62
|
-
"""One row of the §1 table.
|
|
63
|
-
|
|
64
|
-
``raw_*`` fields preserve the exact cell text (after backtick stripping)
|
|
65
|
-
for diagnostics; canonical lowercased versions live in ``blocks`` /
|
|
66
|
-
``status`` for predicate use.
|
|
67
|
-
"""
|
|
68
|
-
row_id: str
|
|
69
|
-
kind: str # "material" | "decision" | "data-point" | other
|
|
70
|
-
blocks: str # canonical lowercase: "approval" | "next-phase" | "none" | other
|
|
71
|
-
status: str # canonical lowercase: "open" | "answered" | "resolved" | "obsolete" | other
|
|
72
|
-
raw_blocks: str
|
|
73
|
-
raw_status: str
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
# The final-report renderer injects a scroll anchor into ID-defining first
|
|
77
|
-
# cells — either leading (`<a id="c-001"></a>C-001`) or inside the bold marker
|
|
78
|
-
# (`**<a id="e-001"></a>E-001**`). Strip every such empty anchor during cell
|
|
79
|
-
# normalization so the ID parses as a bare token for clarification parsing AND
|
|
80
|
-
# the HTML view's `C-\d+` form detection, and so the anchor never leaks into
|
|
81
|
-
# the HTML view as html-escaped literal text.
|
|
82
|
-
_CELL_ANCHOR_RE = re.compile(r'<a id="([^"]*)"></a>')
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def _strip_backticks(cell: str) -> str:
|
|
86
|
-
s = _CELL_ANCHOR_RE.sub("", cell.strip()).strip()
|
|
87
|
-
if s.startswith("`") and s.endswith("`") and len(s) >= 2:
|
|
88
|
-
s = s[1:-1].strip()
|
|
89
|
-
return s
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def _split_pipe_row(line: str) -> list[str]:
|
|
93
|
-
"""``md_table.split_pipe_row`` + §1-specific cell normalization
|
|
94
|
-
(scroll-anchor removal, outer-backtick unwrap)."""
|
|
95
|
-
return [_strip_backticks(c) for c in split_pipe_row(line)]
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def _section_1_slice(report_text: str) -> Optional[str]:
|
|
99
|
-
"""Return the substring spanning the §1 section (heading exclusive of the
|
|
100
|
-
next ``##`` heading), or None if §1 is absent."""
|
|
101
|
-
start_match = SECTION_HEADING_PATTERN.search(report_text)
|
|
102
|
-
if not start_match:
|
|
103
|
-
return None
|
|
104
|
-
rest = report_text[start_match.end():]
|
|
105
|
-
end_match = NEXT_TOP_LEVEL_HEADING_PATTERN.search(rest)
|
|
106
|
-
return rest[: end_match.start()] if end_match else rest
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
_META_ID_RE = re.compile(r"([A-Za-z][A-Za-z0-9]*-\d+)")
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def _meta_field(cell: str, key: str) -> str:
|
|
113
|
-
"""Extract a ``<key>: value`` field from a stacked §1 meta cell.
|
|
114
|
-
|
|
115
|
-
The §1 table collapses the short columns into one ``<br>``-delimited
|
|
116
|
-
metadata cell (``**C-101**<br>Ticket: `DEV-1`<br>Kind: `decision`<br>
|
|
117
|
-
Blocks: `approval`<br>Status: open``). Values may be backtick-wrapped.
|
|
118
|
-
"""
|
|
119
|
-
m = re.search(
|
|
120
|
-
rf"{re.escape(key)}:\s*`?\s*([^`<|]+?)\s*`?\s*(?:<br\s*/?>|$)",
|
|
121
|
-
cell,
|
|
122
|
-
re.IGNORECASE,
|
|
123
|
-
)
|
|
124
|
-
return m.group(1).strip() if m else ""
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def _meta_id(cell: str) -> str:
|
|
128
|
-
"""The bold headline ID of a §1 meta cell (the part before the first <br>)."""
|
|
129
|
-
headline = re.split(r"<br\s*/?>", cell, maxsplit=1)[0]
|
|
130
|
-
m = _META_ID_RE.search(headline)
|
|
131
|
-
return m.group(1) if m else ""
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def parse_meta_cell(cell: str) -> Optional[ClarificationItem]:
|
|
135
|
-
"""Parse one §1 stacked meta cell into a ``ClarificationItem``, or ``None``
|
|
136
|
-
when the cell is not a §1 meta cell (no ``Blocks:``/``Status:`` markers —
|
|
137
|
-
e.g. a header or an unrelated table). Shared by the approval-gate parser
|
|
138
|
-
and the HTML view's form-attach pass so both read the cell identically.
|
|
139
|
-
"""
|
|
140
|
-
raw_blocks = _meta_field(cell, "Blocks")
|
|
141
|
-
raw_status = _meta_field(cell, "Status")
|
|
142
|
-
if not (raw_blocks and raw_status):
|
|
143
|
-
return None
|
|
144
|
-
return ClarificationItem(
|
|
145
|
-
row_id=_meta_id(cell),
|
|
146
|
-
kind=_meta_field(cell, "Kind").lower(),
|
|
147
|
-
blocks=raw_blocks.lower(),
|
|
148
|
-
status=raw_status.lower(),
|
|
149
|
-
raw_blocks=raw_blocks,
|
|
150
|
-
raw_status=raw_status,
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
@dataclass(frozen=True)
|
|
155
|
-
class _Section1Table:
|
|
156
|
-
"""Outcome of walking the §1 slice for its data table.
|
|
157
|
-
|
|
158
|
-
``items`` is ``None`` when no recognizable table header exists among the
|
|
159
|
-
pipe lines. ``unparsed_row_count`` counts body rows whose metadata cell
|
|
160
|
-
failed ``parse_meta_cell`` (all-empty filler rows excluded).
|
|
161
|
-
``has_pipe_lines`` distinguishes the renderer's legitimate table-less
|
|
162
|
-
placeholder (emptyState bullet) from a table whose header drifted.
|
|
163
|
-
"""
|
|
164
|
-
items: Optional[list[ClarificationItem]]
|
|
165
|
-
unparsed_row_count: int
|
|
166
|
-
has_pipe_lines: bool
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
def _walk_section_1_table(section: str) -> _Section1Table:
|
|
170
|
-
lines = section.splitlines()
|
|
171
|
-
has_pipe_lines = any(line.lstrip().startswith("|") for line in lines)
|
|
172
|
-
# Locate the §1 data table by its header. The merged-meta layout collapses
|
|
173
|
-
# ID/Ticket/Kind/Blocks/Status into one metadata cell and keeps the
|
|
174
|
-
# English `Statement` + `User input` columns; detect on those two (any
|
|
175
|
-
# other table — intro, legacy 5.1/5.2 — is rejected).
|
|
176
|
-
header_idx = -1
|
|
177
|
-
for idx, line in enumerate(lines):
|
|
178
|
-
if not line.lstrip().startswith("|"):
|
|
179
|
-
continue
|
|
180
|
-
cells = [c.lower() for c in _split_pipe_row(line)]
|
|
181
|
-
if "user input" in cells and any(c.startswith("statement") for c in cells):
|
|
182
|
-
header_idx = idx
|
|
183
|
-
break
|
|
184
|
-
if header_idx < 0:
|
|
185
|
-
return _Section1Table(None, 0, has_pipe_lines)
|
|
186
|
-
|
|
187
|
-
items: list[ClarificationItem] = []
|
|
188
|
-
unparsed = 0
|
|
189
|
-
body_started = False
|
|
190
|
-
for line in lines[header_idx + 1:]:
|
|
191
|
-
if not line.lstrip().startswith("|"):
|
|
192
|
-
if body_started:
|
|
193
|
-
break
|
|
194
|
-
continue
|
|
195
|
-
if is_separator_row(line):
|
|
196
|
-
body_started = True
|
|
197
|
-
continue
|
|
198
|
-
if not body_started:
|
|
199
|
-
continue
|
|
200
|
-
cells = _split_pipe_row(line)
|
|
201
|
-
if not any(cells):
|
|
202
|
-
continue
|
|
203
|
-
item = parse_meta_cell(cells[0])
|
|
204
|
-
if item is None:
|
|
205
|
-
unparsed += 1
|
|
206
|
-
continue
|
|
207
|
-
items.append(item)
|
|
208
|
-
return _Section1Table(items, unparsed, True)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
def parse_clarification_items(report_text: str) -> Optional[list[ClarificationItem]]:
|
|
212
|
-
"""Return the list of §1 rows. ``None`` means "no §1 meta table detected"
|
|
213
|
-
(missing section or unrecognized table header) — caller must NOT treat
|
|
214
|
-
that as "table is empty".
|
|
215
|
-
|
|
216
|
-
Lenient view-renderer contract: rows whose metadata cell fails to parse
|
|
217
|
-
are skipped, not surfaced. The approval gate must use
|
|
218
|
-
``scan_approval_gate`` instead, which fail-closes on those rows.
|
|
219
|
-
"""
|
|
220
|
-
section = _section_1_slice(report_text)
|
|
221
|
-
if section is None:
|
|
222
|
-
return None
|
|
223
|
-
return _walk_section_1_table(section).items
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
# schema-v2 는 clarification 을 §1 마크다운 테이블이 아니라 data.json 의
|
|
227
|
-
# `clarificationItems[]` 로 들고, AI 핸드오프 Markdown 은 그것을
|
|
228
|
-
# `## Clarification and User Decisions` 아래 JSON 블록으로 직렬화한다. §1 테이블
|
|
229
|
-
# 워크는 거기서 아무것도 못 찾으므로, 게이트가 v1 파서만 쓰면 열린 항목이 있는
|
|
230
|
-
# v2 리포트를 "읽을 수 없음"으로 떨어뜨린다. data.json 은 두 렌더러가 이미 읽는
|
|
231
|
-
# SSOT 다 — clarification 상태도 여기서 읽는다.
|
|
232
|
-
# 리포트 정본의 구조화 판본. 2.0 과 3.0 은 `clarificationItems[]` 를 같은 자리에
|
|
233
|
-
# 같은 모양으로 들고 있으므로 이 좁히기에는 차이가 없다. ADR-0019 가 2.0 을 계속
|
|
234
|
-
# 판독하라고 정했으므로 둘 다 받는다 — `stage_map.structured_report` 와 같은 규칙.
|
|
235
|
-
STRUCTURED_REPORT_VERSIONS = frozenset({"2.0", "3.0"})
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
def _read_report_text(report_path: Path) -> str:
|
|
239
|
-
return report_path.read_text(encoding="utf-8", errors="replace")
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
def _structured_report_data(report_path: Path) -> Optional[dict]:
|
|
243
|
-
"""``report_path`` 의 구조화 리포트 레코드, 아니면 ``None``.
|
|
244
|
-
|
|
245
|
-
파일이 없거나 JSON 이 깨졌거나 구조화 판본이 아니면 ``None`` — 호출자는 v1
|
|
246
|
-
마크다운 경로로 폴백하고, 진짜 구조화 리포트인데 레코드가 깨진 경우는 그
|
|
247
|
-
폴백이 "§1 없음" fail-closed 로 잡는다.
|
|
248
|
-
|
|
249
|
-
판본을 `2.0` 하나로 못박아 두던 동안 3.0 리포트가 여기서 ``None`` 이 됐고,
|
|
250
|
-
그러면 호출자가 JSON 본문에서 `## 1.` 헤딩을 찾다 실패해 **리포트 전문을
|
|
251
|
-
그대로 복사**했다 — 이 좁히기가 막으려던 바로 그 중복이다. 실측: 66K 짜리
|
|
252
|
-
추출본이 나와야 할 자리에 693K data.json 이 스테이징됐고, 패킷의
|
|
253
|
-
`## Clarification Carry-In Extract` 가 0행이 되어 워커가 사용자 답변을
|
|
254
|
-
전혀 받지 못했다.
|
|
255
|
-
"""
|
|
256
|
-
data_path = final_report_data_path(report_path)
|
|
257
|
-
if not data_path.is_file():
|
|
258
|
-
return None
|
|
259
|
-
try:
|
|
260
|
-
data = load_owned_object(data_path, artifact="final report record")
|
|
261
|
-
except (OSError, ValueError):
|
|
262
|
-
return None
|
|
263
|
-
if (
|
|
264
|
-
not isinstance(data, dict)
|
|
265
|
-
or data.get("schemaVersion") not in STRUCTURED_REPORT_VERSIONS
|
|
266
|
-
):
|
|
267
|
-
return None
|
|
268
|
-
return data
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
def _v2_row(entry: dict) -> Optional[dict]:
|
|
272
|
-
"""``clarificationItems[]`` 한 행을 §1 행과 같은 shape 으로. 필수 필드가
|
|
273
|
-
빠졌으면 ``None`` — 호출자가 fail-closed 로 셀 수 있게 한다."""
|
|
274
|
-
row_id = entry.get("id")
|
|
275
|
-
raw_blocks = entry.get("blocks")
|
|
276
|
-
raw_status = entry.get("status")
|
|
277
|
-
if not (isinstance(row_id, str) and row_id):
|
|
278
|
-
return None
|
|
279
|
-
if not (isinstance(raw_blocks, str) and raw_blocks):
|
|
280
|
-
return None
|
|
281
|
-
if not (isinstance(raw_status, str) and raw_status):
|
|
282
|
-
return None
|
|
283
|
-
kind = entry.get("kind")
|
|
284
|
-
item = ClarificationItem(
|
|
285
|
-
row_id=row_id,
|
|
286
|
-
kind=kind.lower() if isinstance(kind, str) else "",
|
|
287
|
-
blocks=raw_blocks.lower(),
|
|
288
|
-
status=raw_status.lower(),
|
|
289
|
-
raw_blocks=raw_blocks,
|
|
290
|
-
raw_status=raw_status,
|
|
291
|
-
)
|
|
292
|
-
options = entry.get("options")
|
|
293
|
-
return {
|
|
294
|
-
"item": item,
|
|
295
|
-
"statement": str(entry.get("statement") or ""),
|
|
296
|
-
"expected_form": str(entry.get("expectedForm") or ""),
|
|
297
|
-
"options": (
|
|
298
|
-
[option for option in options if isinstance(option, dict)]
|
|
299
|
-
if isinstance(options, list)
|
|
300
|
-
else []
|
|
301
|
-
),
|
|
302
|
-
"disposition": clarification_disposition(entry),
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
def _v2_clarification_rows(report_path: Path) -> Optional[list[dict]]:
|
|
307
|
-
"""schema-v2 clarification 행들, 이 리포트가 v2 가 아니면 ``None``.
|
|
308
|
-
필수 필드가 빠진 행은 건너뛰는 lenient 계약(§1 파서와 동일)."""
|
|
309
|
-
data = _structured_report_data(report_path)
|
|
310
|
-
if data is None:
|
|
311
|
-
return None
|
|
312
|
-
entries = data.get("clarificationItems")
|
|
313
|
-
if not isinstance(entries, list):
|
|
314
|
-
return []
|
|
315
|
-
rows = [_v2_row(e) for e in entries if isinstance(e, dict)]
|
|
316
|
-
return [row for row in rows if row is not None]
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
def read_clarification_rows(report_path: Path) -> list[dict]:
|
|
320
|
-
"""리포트 한 건의 clarification 행 — schema-v2 는 data.json 사이드카에서,
|
|
321
|
-
schema-v1 은 §1 테이블에서. 스키마 버전을 아는 유일한 읽기 지점이다.
|
|
322
|
-
|
|
323
|
-
``parse_section_1_rows`` 와 같은 lenient 계약: 어느 쪽에서도 행을 못 찾으면
|
|
324
|
-
``[]``.
|
|
325
|
-
"""
|
|
326
|
-
v2_rows = _v2_clarification_rows(report_path)
|
|
327
|
-
if v2_rows is not None:
|
|
328
|
-
return v2_rows
|
|
329
|
-
return parse_section_1_rows(_read_report_text(report_path))
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
def parse_section_1_rows(report_text: str) -> list[dict]:
|
|
333
|
-
"""§1 테이블 한 행마다 메타(ClarificationItem) + 원문 Statement / Expected
|
|
334
|
-
form 셀.
|
|
335
|
-
|
|
336
|
-
입력은 §1 을 담은 마크다운 본문이다 — 리포트 전문일 수도 있고, 다음 run 에
|
|
337
|
-
첨부되는 carry-in 본문(``clarification_response_with_sidecars``)일 수도
|
|
338
|
-
있다. §1 셀 추출의 단일 참조점 — 다른 모듈이 §1 레이아웃을 다시 훑지 않도록
|
|
339
|
-
이 함수로 통일한다.
|
|
340
|
-
"""
|
|
341
|
-
section = _section_1_slice(report_text)
|
|
342
|
-
if section is None:
|
|
343
|
-
return []
|
|
344
|
-
lines = section.splitlines()
|
|
345
|
-
header_idx = -1
|
|
346
|
-
for idx, line in enumerate(lines):
|
|
347
|
-
if not line.lstrip().startswith("|"):
|
|
348
|
-
continue
|
|
349
|
-
cells = [c.lower() for c in _split_pipe_row(line)]
|
|
350
|
-
if "user input" in cells and any(c.startswith("statement") for c in cells):
|
|
351
|
-
header_idx = idx
|
|
352
|
-
break
|
|
353
|
-
if header_idx < 0:
|
|
354
|
-
return []
|
|
355
|
-
header = [c.lower() for c in _split_pipe_row(lines[header_idx])]
|
|
356
|
-
s_col = next((j for j, h in enumerate(header) if h.startswith("statement")), -1)
|
|
357
|
-
e_col = next((j for j, h in enumerate(header) if h.startswith("expected form")), -1)
|
|
358
|
-
rows: list[dict] = []
|
|
359
|
-
body = False
|
|
360
|
-
for line in lines[header_idx + 1:]:
|
|
361
|
-
if not line.lstrip().startswith("|"):
|
|
362
|
-
if body:
|
|
363
|
-
break
|
|
364
|
-
continue
|
|
365
|
-
if is_separator_row(line):
|
|
366
|
-
body = True
|
|
367
|
-
continue
|
|
368
|
-
if not body:
|
|
369
|
-
continue
|
|
370
|
-
cells = _split_pipe_row(line)
|
|
371
|
-
item = parse_meta_cell(cells[0]) if cells else None
|
|
372
|
-
if item is None:
|
|
373
|
-
continue
|
|
374
|
-
rows.append({
|
|
375
|
-
"item": item,
|
|
376
|
-
"statement": cells[s_col] if 0 <= s_col < len(cells) else "",
|
|
377
|
-
"expected_form": cells[e_col] if 0 <= e_col < len(cells) else "",
|
|
378
|
-
# v1 cells are strings; the structured options live only in v2's
|
|
379
|
-
# data sibling. The key stays so callers never branch on schema.
|
|
380
|
-
"options": [],
|
|
381
|
-
})
|
|
382
|
-
return rows
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
# 아직 사용자 판단이 없는 상태. `answered` 는 판단 기록이며 다시 묻지 않는다.
|
|
386
|
-
UNRESOLVED_STATUSES = {"open"}
|
|
387
|
-
|
|
388
|
-
# `Blocks` values that gate the user's `approved: true` flip.
|
|
389
|
-
APPROVAL_BLOCKS = frozenset({"approval"})
|
|
390
|
-
# `Blocks` values that owe the user an answer at all. `next-phase` rows never
|
|
391
|
-
# gate approval, but the next phase cannot start with them open either, so the
|
|
392
|
-
# user-response skill must list them — counting only `approval` made a report
|
|
393
|
-
# whose open items were all `next-phase` look like it had none.
|
|
394
|
-
USER_INPUT_BLOCKS = frozenset({"approval", "next-phase"})
|
|
395
|
-
ANSWER_DISPOSITIONS = frozenset({
|
|
396
|
-
"answer",
|
|
397
|
-
"select",
|
|
398
|
-
"accept-risk",
|
|
399
|
-
"request-revision",
|
|
400
|
-
"reject",
|
|
401
|
-
})
|
|
402
|
-
# 사용자가 진행을 고른 처분. DISAGREE 표와 행은 증거로 남고 게이트는 내리다.
|
|
403
|
-
PROCEEDING_DISPOSITIONS = frozenset({"answer", "select", "accept-risk"})
|
|
404
|
-
# 사용자가 이 계획으로 진행하지 않겠다고 고른 처분.
|
|
405
|
-
RETURN_DISPOSITIONS = frozenset({"request-revision", "reject"})
|
|
406
|
-
# 이 런이 그 답을 본문에 반영했다고 원장이 적은 처분.
|
|
407
|
-
INCORPORATED_LEDGER_DISPOSITIONS = frozenset(
|
|
408
|
-
{"superseded", "no-dependent-statement"}
|
|
409
|
-
)
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
def clarification_disposition(row: Mapping[str, object]) -> str:
|
|
413
|
-
"""행에 기록된 사용자 처분. 없으면 빈 문자열."""
|
|
414
|
-
resolution = row.get("resolution")
|
|
415
|
-
if not isinstance(resolution, Mapping):
|
|
416
|
-
context = row.get("approvalContext")
|
|
417
|
-
if isinstance(context, Mapping):
|
|
418
|
-
resolution = context.get("resolution")
|
|
419
|
-
if isinstance(resolution, Mapping):
|
|
420
|
-
disposition = str(resolution.get("disposition") or "").strip()
|
|
421
|
-
if disposition:
|
|
422
|
-
return disposition
|
|
423
|
-
user_input = str(row.get("userInput") or "").strip()
|
|
424
|
-
options = row.get("options")
|
|
425
|
-
if user_input and isinstance(options, list):
|
|
426
|
-
for option in options:
|
|
427
|
-
if (
|
|
428
|
-
isinstance(option, Mapping)
|
|
429
|
-
and str(option.get("answer") or "").strip() == user_input
|
|
430
|
-
):
|
|
431
|
-
return str(option.get("disposition") or "").strip()
|
|
432
|
-
return ""
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
def incorporated_clarification_ids(
|
|
436
|
-
report_data: Mapping[str, object] | None,
|
|
437
|
-
) -> frozenset[str]:
|
|
438
|
-
"""이 런이 답을 본문에 반영했다고 원장에 적은 C-id.
|
|
439
|
-
|
|
440
|
-
`superseded` 와 `no-dependent-statement` 만 센다. 원장에 없는
|
|
441
|
-
`request-revision` / `reject` 는 아직 다음 계획을 막는 되돌림이다.
|
|
442
|
-
"""
|
|
443
|
-
if not isinstance(report_data, Mapping):
|
|
444
|
-
return frozenset()
|
|
445
|
-
planning = report_data.get("implementationPlanning")
|
|
446
|
-
if not isinstance(planning, Mapping):
|
|
447
|
-
return frozenset()
|
|
448
|
-
ledger = planning.get("supersessionLedger")
|
|
449
|
-
if not isinstance(ledger, list):
|
|
450
|
-
return frozenset()
|
|
451
|
-
ids: set[str] = set()
|
|
452
|
-
for entry in ledger:
|
|
453
|
-
if not isinstance(entry, Mapping):
|
|
454
|
-
continue
|
|
455
|
-
row_id = str(entry.get("clarificationId") or "").strip()
|
|
456
|
-
disposition = str(entry.get("disposition") or "").strip().lower()
|
|
457
|
-
if row_id and disposition in INCORPORATED_LEDGER_DISPOSITIONS:
|
|
458
|
-
ids.add(row_id)
|
|
459
|
-
return frozenset(ids)
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
def row_blocks_progress(
|
|
463
|
-
status: str,
|
|
464
|
-
disposition: str = "",
|
|
465
|
-
*,
|
|
466
|
-
incorporated: bool = False,
|
|
467
|
-
) -> bool:
|
|
468
|
-
"""이 행이 승인·다음 단계 진입을 막는가.
|
|
469
|
-
|
|
470
|
-
진행 처분(`accept-risk` / `select` / `answer`)은 고치지 않은 DISAGREE 를
|
|
471
|
-
행과 투표에 남긴 채로 게이트만 내린다. `request-revision` / `reject` 는
|
|
472
|
-
사용자가 진행을 거절한 것이므로 막는다. 다만 이 보고서 원장이 그 답을
|
|
473
|
-
이미 반영했으면(`incorporated`) 같은 되돌림이 다음 계획 런을 강제하지
|
|
474
|
-
않는다. 처분이 없는 `answered` 도 판단 기록이므로 막지 않는다.
|
|
475
|
-
"""
|
|
476
|
-
normalized_status = status.strip().lower()
|
|
477
|
-
normalized_disposition = disposition.strip().lower()
|
|
478
|
-
if normalized_status == "obsolete":
|
|
479
|
-
return False
|
|
480
|
-
if normalized_disposition in PROCEEDING_DISPOSITIONS:
|
|
481
|
-
return False
|
|
482
|
-
if normalized_disposition in RETURN_DISPOSITIONS:
|
|
483
|
-
return not incorporated
|
|
484
|
-
return normalized_status not in {"answered", "resolved"}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
def progress_blocking_ids(
|
|
488
|
-
rows: object,
|
|
489
|
-
blocking_values: frozenset[str] = APPROVAL_BLOCKS,
|
|
490
|
-
*,
|
|
491
|
-
report_data: Mapping[str, object] | None = None,
|
|
492
|
-
) -> list[str]:
|
|
493
|
-
"""게이트를 아직 막는 행 id. 사용자 진행 처분이 있는 행은 빠진다.
|
|
494
|
-
|
|
495
|
-
``report_data`` 가 있으면 원장에 반영된 되돌림 행도 빠진다.
|
|
496
|
-
"""
|
|
497
|
-
if not isinstance(rows, list):
|
|
498
|
-
return []
|
|
499
|
-
incorporated = incorporated_clarification_ids(report_data)
|
|
500
|
-
ids: list[str] = []
|
|
501
|
-
for row in rows:
|
|
502
|
-
if not isinstance(row, Mapping):
|
|
503
|
-
continue
|
|
504
|
-
blocks = str(row.get("blocks") or "").strip().lower()
|
|
505
|
-
status = str(row.get("status") or "").strip()
|
|
506
|
-
row_id = row.get("id")
|
|
507
|
-
if (
|
|
508
|
-
blocks in blocking_values
|
|
509
|
-
and row_blocks_progress(
|
|
510
|
-
status,
|
|
511
|
-
clarification_disposition(row),
|
|
512
|
-
incorporated=isinstance(row_id, str) and row_id in incorporated,
|
|
513
|
-
)
|
|
514
|
-
and isinstance(row_id, str)
|
|
515
|
-
and row_id
|
|
516
|
-
):
|
|
517
|
-
ids.append(row_id)
|
|
518
|
-
return ids
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
@dataclass(frozen=True)
|
|
522
|
-
class ClarificationScan:
|
|
523
|
-
"""Fail-closed read of the clarification rows blocking on one `Blocks`
|
|
524
|
-
value set.
|
|
525
|
-
|
|
526
|
-
``unreadable_reason`` is ``None`` only when the scan is confident: the
|
|
527
|
-
rows parsed cleanly (or the report is the legitimate table-less
|
|
528
|
-
placeholder) and ``blockers`` is therefore authoritative. A non-None
|
|
529
|
-
reason means the caller must refuse to act — never soft-pass.
|
|
530
|
-
"""
|
|
531
|
-
blockers: list[ClarificationItem]
|
|
532
|
-
unreadable_reason: Optional[str]
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
def scan_approval_gate(report_path: Path) -> ClarificationScan:
|
|
536
|
-
"""Scan for ``Blocks=approval`` rows that still block progress.
|
|
537
|
-
|
|
538
|
-
A recorded user proceeding disposition (`accept-risk` / `select` /
|
|
539
|
-
`answer`), including one that lives only in the sidecar, does not block.
|
|
540
|
-
``request-revision`` / ``reject`` still block unless this report's
|
|
541
|
-
``supersessionLedger`` already incorporated that id. The scan refuses to
|
|
542
|
-
guess whenever the schema drifted.
|
|
543
|
-
"""
|
|
544
|
-
return scan_clarification_blockers(
|
|
545
|
-
report_path,
|
|
546
|
-
APPROVAL_BLOCKS,
|
|
547
|
-
honor_sidecar_answers=True,
|
|
548
|
-
sidecar_unblocks_proceeding_only=True,
|
|
549
|
-
)
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
def scan_open_user_input(report_path: Path) -> ClarificationScan:
|
|
553
|
-
"""Scan for every unresolved row that still owes the user an answer
|
|
554
|
-
(``Blocks`` in ``{approval, next-phase}``)."""
|
|
555
|
-
return scan_clarification_blockers(
|
|
556
|
-
report_path, USER_INPUT_BLOCKS, honor_sidecar_answers=True
|
|
557
|
-
)
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
def scan_clarification_blockers(
|
|
561
|
-
report_path: Path,
|
|
562
|
-
blocking_values: frozenset[str],
|
|
563
|
-
*,
|
|
564
|
-
honor_sidecar_answers: bool,
|
|
565
|
-
sidecar_unblocks_proceeding_only: bool = False,
|
|
566
|
-
) -> ClarificationScan:
|
|
567
|
-
"""Shared fail-closed clarification walk for both gates above — schema-v2
|
|
568
|
-
reads its rows from the data sibling and schema-v1 from the §1 table.
|
|
569
|
-
``honor_sidecar_answers`` hides rows the sidecar already answered.
|
|
570
|
-
``sidecar_unblocks_proceeding_only`` keeps an unincorporated
|
|
571
|
-
``request-revision`` / ``reject`` as a blocker so a return choice cannot
|
|
572
|
-
start the next phase. A ledger entry for that id means this report already
|
|
573
|
-
absorbed the return.
|
|
574
|
-
"""
|
|
575
|
-
v2_scan = _scan_v2_blockers(report_path, blocking_values)
|
|
576
|
-
scan = (
|
|
577
|
-
v2_scan if v2_scan is not None
|
|
578
|
-
else scan_section_1_blockers(_read_report_text(report_path), blocking_values)
|
|
579
|
-
)
|
|
580
|
-
return (
|
|
581
|
-
_resolve_blockers_answered_by_user(
|
|
582
|
-
report_path,
|
|
583
|
-
scan,
|
|
584
|
-
proceeding_only=sidecar_unblocks_proceeding_only,
|
|
585
|
-
)
|
|
586
|
-
if honor_sidecar_answers
|
|
587
|
-
else scan
|
|
588
|
-
)
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
def _resolve_blockers_answered_by_user(
|
|
592
|
-
report_path: Path,
|
|
593
|
-
scan: ClarificationScan,
|
|
594
|
-
*,
|
|
595
|
-
proceeding_only: bool = False,
|
|
596
|
-
) -> ClarificationScan:
|
|
597
|
-
"""사용자가 사이드카로 답한 행을 blocker 에서 뺀 스캔.
|
|
598
|
-
|
|
599
|
-
답의 정본은 사용자의 `user-responses/` 사이드카다. 리포트의 `Status` 는 그
|
|
600
|
-
run 이 스스로 적어둔 값이고, 답이 사이드카로만 들어오는 경로(HTML 뷰의
|
|
601
|
-
`Export user response`, `okstra user-response write`)에서는 갱신되지 않는다.
|
|
602
|
-
사용자 입력 목록은 사이드카 답변이 있는 항목을 다시 묻지 않는다. 승인
|
|
603
|
-
게이트는 진행 처분만 차단에서 뺀다.
|
|
604
|
-
|
|
605
|
-
fail-closed 는 그대로다: 행 자체를 못 읽은 스캔(`unreadable_reason`)은
|
|
606
|
-
어떤 id 가 blocker 인지 모르는 상태이므로 사이드카로 덮지 않는다.
|
|
607
|
-
"""
|
|
608
|
-
if scan.unreadable_reason is not None or not scan.blockers:
|
|
609
|
-
return scan
|
|
610
|
-
dispositions = sidecar_dispositions(report_path)
|
|
611
|
-
if not dispositions:
|
|
612
|
-
return scan
|
|
613
|
-
answered_ids = {
|
|
614
|
-
row_id
|
|
615
|
-
for row_id, disposition in dispositions.items()
|
|
616
|
-
if (not proceeding_only) or disposition in PROCEEDING_DISPOSITIONS
|
|
617
|
-
}
|
|
618
|
-
if not answered_ids:
|
|
619
|
-
return scan
|
|
620
|
-
return ClarificationScan(
|
|
621
|
-
[b for b in scan.blockers if b.row_id not in answered_ids], None
|
|
622
|
-
)
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
def scan_section_1_blockers(
|
|
626
|
-
report_text: str, blocking_values: frozenset[str]
|
|
627
|
-
) -> ClarificationScan:
|
|
628
|
-
"""Fail-closed §1 walk over any markdown carrying the table — a schema-v1
|
|
629
|
-
report, or the carry-in body a reconciliation just produced."""
|
|
630
|
-
section = _section_1_slice(report_text)
|
|
631
|
-
if section is None:
|
|
632
|
-
if _LOOSE_SECTION_1_RE.search(report_text):
|
|
633
|
-
reason = (
|
|
634
|
-
"`## 1. Clarification Items` heading exists but does not match "
|
|
635
|
-
"the schema heading format (anchor/format drift)"
|
|
636
|
-
)
|
|
637
|
-
else:
|
|
638
|
-
expected = "/".join(sorted(blocking_values))
|
|
639
|
-
reason = (
|
|
640
|
-
"report has no `## 1. Clarification Items` section — the gate "
|
|
641
|
-
f"cannot confirm there are no unresolved `Blocks={expected}` rows"
|
|
642
|
-
)
|
|
643
|
-
return ClarificationScan([], reason)
|
|
644
|
-
table = _walk_section_1_table(section)
|
|
645
|
-
if table.items is None:
|
|
646
|
-
if table.has_pipe_lines:
|
|
647
|
-
return ClarificationScan([], (
|
|
648
|
-
"§1 contains a table but its header row is not the schema "
|
|
649
|
-
"header (`| ... | Statement | Expected form | User input |`)"
|
|
650
|
-
))
|
|
651
|
-
# Renderer's emptyState placeholder: heading is intact and no table
|
|
652
|
-
# was emitted — confidently "no blocking items".
|
|
653
|
-
return ClarificationScan([], None)
|
|
654
|
-
if table.unparsed_row_count:
|
|
655
|
-
return ClarificationScan([], (
|
|
656
|
-
f"§1 table has {table.unparsed_row_count} row(s) whose metadata "
|
|
657
|
-
"cell could not be parsed (Blocks/Status markers missing or "
|
|
658
|
-
"malformed)"
|
|
659
|
-
))
|
|
660
|
-
blockers = [
|
|
661
|
-
it for it in table.items
|
|
662
|
-
if it.blocks in blocking_values and row_blocks_progress(it.status)
|
|
663
|
-
]
|
|
664
|
-
return ClarificationScan(blockers, None)
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
def _scan_v2_blockers(
|
|
668
|
-
report_path: Path, blocking_values: frozenset[str]
|
|
669
|
-
) -> Optional[ClarificationScan]:
|
|
670
|
-
"""schema-v2 data.json 기준 스캔, 이 리포트가 v2 가 아니면 ``None``.
|
|
671
|
-
필수 필드가 빠진 행은 §1 의 unparsed row 와 같이 fail-closed 로 다룬다."""
|
|
672
|
-
data = _structured_report_data(report_path)
|
|
673
|
-
if data is None:
|
|
674
|
-
return None
|
|
675
|
-
entries = data.get("clarificationItems")
|
|
676
|
-
if entries is None:
|
|
677
|
-
return ClarificationScan([], None)
|
|
678
|
-
if not isinstance(entries, list):
|
|
679
|
-
return ClarificationScan([], (
|
|
680
|
-
"schema-v2 data.json `clarificationItems` is not an array — the "
|
|
681
|
-
"gate cannot read the clarification rows"
|
|
682
|
-
))
|
|
683
|
-
rows = [_v2_row(e) if isinstance(e, dict) else None for e in entries]
|
|
684
|
-
unparsed = sum(1 for row in rows if row is None)
|
|
685
|
-
if unparsed:
|
|
686
|
-
return ClarificationScan([], (
|
|
687
|
-
f"schema-v2 data.json has {unparsed} `clarificationItems` row(s) "
|
|
688
|
-
"missing id/blocks/status"
|
|
689
|
-
))
|
|
690
|
-
incorporated = incorporated_clarification_ids(data)
|
|
691
|
-
blockers = [
|
|
692
|
-
row["item"] for row in rows
|
|
693
|
-
if row["item"].blocks in blocking_values
|
|
694
|
-
and row_blocks_progress(
|
|
695
|
-
row["item"].status,
|
|
696
|
-
str(row.get("disposition") or ""),
|
|
697
|
-
incorporated=row["item"].row_id in incorporated,
|
|
698
|
-
)
|
|
699
|
-
]
|
|
700
|
-
return ClarificationScan(blockers, None)
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
# 느슨한 §1 헤딩 탐지: 엄격한 SECTION_HEADING_PATTERN 이 실패해도 이게 매칭되면
|
|
704
|
-
# "§1 헤딩은 있는데 형태가 어긋나 파싱에 실패" 한 상태다. trailing 부분을 보지
|
|
705
|
-
# 않으므로 앵커 변형·수동 편집·미래 렌더 변경 어디서든 헤딩의 존재만 잡는다.
|
|
706
|
-
_LOOSE_SECTION_1_RE = re.compile(r"^##\s+1\.\s+Clarification Items\b", re.MULTILINE)
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
def section_1_present_but_unparsed(report_text: str) -> bool:
|
|
710
|
-
"""§1 헤딩이 느슨 탐지엔 잡히지만 엄격 SECTION_HEADING_PATTERN 에는 매칭하지
|
|
711
|
-
못하는 경우 True — 헤딩 형태가 어긋나(앵커·포맷 drift) §1 슬라이스 자체가
|
|
712
|
-
실패하는 상태다.
|
|
713
|
-
|
|
714
|
-
이때 ``_section_1_slice`` 가 None 을 반환해 parse 가 통째로 None 이 되고 승인
|
|
715
|
-
게이트가 "schema 없음 → soft-pass" 로 조용히 열린다. §1 앵커 버그가 정확히 이
|
|
716
|
-
메커니즘으로 터졌다. 헤딩 자체가 없는 legacy 리포트(둘 다 불매칭)와, 엄격
|
|
717
|
-
매칭에 성공하는 정상 헤딩(테이블이 없는 emptyState placeholder 포함)은 False —
|
|
718
|
-
placeholder 는 헤딩이 멀쩡하므로 fail-closed 로 오인하지 않는다. 정규식만 넓혀 온
|
|
719
|
-
과거 수정과 달리, 이 판별은 "헤딩 형태 drift" 자체를 차단해 재발 클래스를 닫는다."""
|
|
720
|
-
if SECTION_HEADING_PATTERN.search(report_text):
|
|
721
|
-
return False
|
|
722
|
-
return bool(_LOOSE_SECTION_1_RE.search(report_text))
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
def user_response_sidecars(source: Path) -> list[Path]:
|
|
726
|
-
"""``source`` 형제 ``user-responses/`` 의 ``user-response-*.md`` 목록(이름 순).
|
|
727
|
-
|
|
728
|
-
``source`` 가 ``runs/<task-type>/reports/final-report-*.md`` 레이아웃일 때만
|
|
729
|
-
형제 ``user-responses/`` 디렉토리를 찾는다(HTML 뷰의 `Export user response`
|
|
730
|
-
가 내려준 파일을 사용자가 거기 저장). 그 외 경로·디렉토리 부재 시 빈 목록.
|
|
731
|
-
carry-in 첨부 본문(``clarification_response_with_sidecars``)과 위저드 안내
|
|
732
|
-
문구의 사이드카 카운트가 같은 한 곳을 보도록 하는 단일 참조점이다.
|
|
733
|
-
"""
|
|
734
|
-
responses_dir = source.parent.parent / "user-responses"
|
|
735
|
-
if source.parent.name != "reports" or not responses_dir.is_dir():
|
|
736
|
-
return []
|
|
737
|
-
return sorted(
|
|
738
|
-
p for p in responses_dir.glob("user-response-*.md") if p.is_file()
|
|
739
|
-
)
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
_ANALYSIS_REPORT_NAME_RE = re.compile(
|
|
743
|
-
r"^final-report-(?P<task_type>project-analysis|feature-analysis|"
|
|
744
|
-
r"change-impact-analysis)-(?P<seq>\d{3})\.md$"
|
|
745
|
-
)
|
|
746
|
-
_SIDECAR_FRONTMATTER_RE = re.compile(
|
|
747
|
-
r"\A---[ \t]*\r?\n(?P<body>.*?)(?:\r?\n)---[ \t]*(?:\r?\n|\Z)",
|
|
748
|
-
re.DOTALL,
|
|
749
|
-
)
|
|
750
|
-
_ANALYSIS_REVIEW_HEADING_RE = re.compile(
|
|
751
|
-
r"^## ANALYSIS REVIEW\s*$", re.MULTILINE
|
|
752
|
-
)
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
def _sidecar_frontmatter_value(text: str, key: str) -> str:
|
|
756
|
-
match = _SIDECAR_FRONTMATTER_RE.match(text)
|
|
757
|
-
if match is None:
|
|
758
|
-
return ""
|
|
759
|
-
value = re.search(
|
|
760
|
-
rf"^{re.escape(key)}:\s*(\S.*?)\s*$",
|
|
761
|
-
match.group("body"),
|
|
762
|
-
re.MULTILINE,
|
|
763
|
-
)
|
|
764
|
-
return value.group(1) if value else ""
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
def _sidecars_for_attachment(source: Path) -> list[Path]:
|
|
768
|
-
sidecars = user_response_sidecars(source)
|
|
769
|
-
report_match = _ANALYSIS_REPORT_NAME_RE.fullmatch(source.name)
|
|
770
|
-
if report_match is None:
|
|
771
|
-
return sidecars
|
|
772
|
-
expected_source = (
|
|
773
|
-
f"runs/{report_match.group('task_type')}/reports/{source.name}"
|
|
774
|
-
)
|
|
775
|
-
ordinary: list[Path] = []
|
|
776
|
-
candidates: list[tuple[Path, str]] = []
|
|
777
|
-
for sidecar in sidecars:
|
|
778
|
-
try:
|
|
779
|
-
text = sidecar.read_text(encoding="utf-8")
|
|
780
|
-
except OSError:
|
|
781
|
-
ordinary.append(sidecar)
|
|
782
|
-
continue
|
|
783
|
-
if _ANALYSIS_REVIEW_HEADING_RE.search(text) is None:
|
|
784
|
-
ordinary.append(sidecar)
|
|
785
|
-
continue
|
|
786
|
-
source_report = _sidecar_frontmatter_value(text, "source-report")
|
|
787
|
-
seq = _sidecar_frontmatter_value(text, "seq")
|
|
788
|
-
if source_report and source_report != expected_source:
|
|
789
|
-
continue
|
|
790
|
-
if seq and seq != report_match.group("seq"):
|
|
791
|
-
continue
|
|
792
|
-
candidates.append((sidecar, text))
|
|
793
|
-
|
|
794
|
-
from okstra_ctl.user_response import UserResponseError, parse_analysis_review
|
|
795
|
-
|
|
796
|
-
valid: list[Path] = []
|
|
797
|
-
malformed: list[Path] = []
|
|
798
|
-
for sidecar, text in candidates:
|
|
799
|
-
try:
|
|
800
|
-
review = parse_analysis_review(text)
|
|
801
|
-
except UserResponseError:
|
|
802
|
-
malformed.append(sidecar)
|
|
803
|
-
continue
|
|
804
|
-
if review is not None and (
|
|
805
|
-
review.source_report == expected_source
|
|
806
|
-
and review.seq == report_match.group("seq")
|
|
807
|
-
):
|
|
808
|
-
valid.append(sidecar)
|
|
809
|
-
else:
|
|
810
|
-
malformed.append(sidecar)
|
|
811
|
-
selected = valid if valid else malformed
|
|
812
|
-
return sorted([*ordinary, *selected])
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
def _sidecar_answer_records(source: Path) -> dict[str, tuple[str, str]]:
|
|
816
|
-
"""사이드카 답을 `{id: (value, disposition)}` 로 모은다.
|
|
817
|
-
|
|
818
|
-
`disposition` 이 `ANSWER_DISPOSITIONS` 에 속하는 항목만 답으로 센다.
|
|
819
|
-
`reframe` 은 답이 아니므로 집합에서 빠진다. 같은 id 는 이름순 마지막이
|
|
820
|
-
이긴다 — 최신이 reframe 이거나 값이 비면 앞선 답을 지운다.
|
|
821
|
-
"""
|
|
822
|
-
from okstra_ctl.user_response import parse_user_response_entries
|
|
823
|
-
|
|
824
|
-
answers: dict[str, tuple[str, str]] = {}
|
|
825
|
-
for sidecar in user_response_sidecars(source):
|
|
826
|
-
for entry in parse_user_response_entries(
|
|
827
|
-
sidecar.read_text(encoding="utf-8")
|
|
828
|
-
):
|
|
829
|
-
if entry.value and entry.disposition in ANSWER_DISPOSITIONS:
|
|
830
|
-
answers[entry.response_id] = (entry.value, entry.disposition)
|
|
831
|
-
else:
|
|
832
|
-
answers.pop(entry.response_id, None)
|
|
833
|
-
return answers
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
def sidecar_answers(source: Path) -> dict[str, str]:
|
|
837
|
-
"""`user-responses/` 사이드카들의 답변을 `{clarification-id: value}` 로 모은다.
|
|
838
|
-
|
|
839
|
-
사용자가 답한 항목이 무엇인지 아는 단일 참조점 — carry-in 병합도, 승인
|
|
840
|
-
게이트도, 스킬의 열린 항목 목록도 전부 이 한 곳을 본다.
|
|
841
|
-
"""
|
|
842
|
-
return {
|
|
843
|
-
row_id: value
|
|
844
|
-
for row_id, (value, _disposition) in _sidecar_answer_records(source).items()
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
def sidecar_dispositions(source: Path) -> dict[str, str]:
|
|
849
|
-
"""사이드카 답의 처분을 `{clarification-id: disposition}` 로 모은다."""
|
|
850
|
-
return {
|
|
851
|
-
row_id: disposition
|
|
852
|
-
for row_id, (_value, disposition) in _sidecar_answer_records(source).items()
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
def attached_user_responses_section(source: Path) -> str:
|
|
857
|
-
"""`source` 형제 `user-responses/` 사이드카만 모은 `# Attached User Responses`
|
|
858
|
-
섹션 본문. 사이드카 부재 시 빈 문자열.
|
|
859
|
-
|
|
860
|
-
plan 본문이 이미 별도 경로(`--approved-plan`)로 참조되는 implementation
|
|
861
|
-
carry-in 에서, 원문을 중복 복사하지 않고 사용자 답변만 instruction-set 에
|
|
862
|
-
첨부할 때 쓴다. `clarification_response_with_sidecars` 와 같은 직렬화 포맷을
|
|
863
|
-
한 곳에서 만들어 두 carry-in 경로가 갈라지지 않게 한다.
|
|
864
|
-
"""
|
|
865
|
-
sidecars = _sidecars_for_attachment(source)
|
|
866
|
-
if not sidecars:
|
|
867
|
-
return ""
|
|
868
|
-
parts = ["# Attached User Responses\n"]
|
|
869
|
-
for sidecar in sidecars:
|
|
870
|
-
parts.append(
|
|
871
|
-
f"\n## {sidecar.name}\n\n"
|
|
872
|
-
f"{sidecar.read_text(encoding='utf-8').strip()}\n"
|
|
873
|
-
)
|
|
874
|
-
return "".join(parts)
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
def clarification_response_with_sidecars(source: Path) -> str:
|
|
878
|
-
"""clarification-response 본문에 `user-responses/` 사이드카를 덧붙인 본문.
|
|
879
|
-
|
|
880
|
-
`resume-clarification` 의 설계된 입력은 사용자가 §1 의 `User input` 열을
|
|
881
|
-
채운 **직전 final-report 자체**다. 그 전문을 그대로 복사하면 리포트가
|
|
882
|
-
instruction-set 안에 두 번째로 존재하게 되고, report-writer 는 같은 내용을
|
|
883
|
-
`clarification-response.md` 로 한 번, 직전 리포트 경로로 다시 한 번 읽는다.
|
|
884
|
-
리포트는 run 이 누적될수록 커지므로 이 중복은 스스로 악화된다(실측: 283K
|
|
885
|
-
리포트가 892K 의 중복 읽기를 만들어 report-writer 를 timeout 시켰다).
|
|
886
|
-
|
|
887
|
-
그래서 소스가 final-report 일 때는 답변이 실린 clarification 행만 잘라내고
|
|
888
|
-
원문은 경로로 가리킨다 — `attached_user_responses_section` 이 implementation
|
|
889
|
-
carry-in 에서 이미 쓰는 "원문은 경로로, 답변만 첨부" 규칙과 같다. 행을 어디서
|
|
890
|
-
읽는지는 스키마가 정한다(v1 은 §1 표, v2 는 data.json). clarification 을
|
|
891
|
-
아예 담지 않은 소스(사용자가 직접 쓴 답변 파일)만 원문 그대로 복사한다.
|
|
892
|
-
"""
|
|
893
|
-
text = source.read_text(encoding="utf-8")
|
|
894
|
-
section = attached_user_responses_section(source)
|
|
895
|
-
answers = sidecar_answers(source)
|
|
896
|
-
body = _clarification_carry_body(source, text, answers)
|
|
897
|
-
if not section:
|
|
898
|
-
return body
|
|
899
|
-
return body.rstrip("\n") + "\n\n---\n\n" + section
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
SECTION_1_HEADING = "## 1. Clarification Items"
|
|
903
|
-
_SECTION_1_TABLE_HEADER = (
|
|
904
|
-
"| Record | Statement | Expected form | User input |\n"
|
|
905
|
-
"|---|---|---|---|"
|
|
906
|
-
)
|
|
907
|
-
_SECTION_1_EMPTY_STATE = "- The source report recorded no clarification items."
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
def _clarification_carry_body(
|
|
911
|
-
source: Path, text: str, answers: dict[str, str]
|
|
912
|
-
) -> str:
|
|
913
|
-
"""final-report 소스는 §1 + 원문 포인터로 좁히고, 그 외는 원문 그대로.
|
|
914
|
-
|
|
915
|
-
§1 이 있으면 사이드카 답변을 그 표의 `User input` 열에 병합해, 답이 표 안에
|
|
916
|
-
자리하도록 한다(파일 헤더가 선언하는 "답은 User input 열에" 계약을 실제로
|
|
917
|
-
참으로 만든다)."""
|
|
918
|
-
carried = _carry_section_1(source, text)
|
|
919
|
-
if carried is None:
|
|
920
|
-
return text
|
|
921
|
-
heading, section_body = carried
|
|
922
|
-
if answers:
|
|
923
|
-
section_body = _reconcile_user_input(section_body, answers)
|
|
924
|
-
return (
|
|
925
|
-
"# Clarification Response (carry-in)\n\n"
|
|
926
|
-
f"- Source report: `{source}`\n"
|
|
927
|
-
"- This file carries **only** the source report's Clarification Items "
|
|
928
|
-
"section; the report itself is read from the path above when a phase "
|
|
929
|
-
"needs it. Do not re-read the source report to find the answers — they "
|
|
930
|
-
"are in the `User input` column below.\n\n"
|
|
931
|
-
f"{heading}\n{section_body}\n"
|
|
932
|
-
)
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
def _carry_section_1(source: Path, text: str) -> Optional[tuple[str, str]]:
|
|
936
|
-
"""carry-in 본문에 실을 (헤딩, §1 본문). clarification 을 담지 않은 소스면
|
|
937
|
-
``None``.
|
|
938
|
-
|
|
939
|
-
schema-v2 는 행을 data.json 에 들고 AI 마크다운에는 §1 표가 없다. §1 슬라이스
|
|
940
|
-
만 보던 동안 v2 소스는 "좁힐 것이 없다" 로 판정돼 **리포트 전문이 그대로
|
|
941
|
-
복사**됐다 — 이 좁히기가 막으려던 바로 그 중복이다. carry-in 은 파생 문서이고
|
|
942
|
-
다운스트림(승인 게이트·프롬프트 빌더·검증 워커)이 §1 표 하나만 읽으므로, v2
|
|
943
|
-
행도 같은 표로 렌더한다."""
|
|
944
|
-
data = _structured_report_data(source)
|
|
945
|
-
if data is not None:
|
|
946
|
-
entries = data.get("clarificationItems")
|
|
947
|
-
return SECTION_1_HEADING, _structured_section_1_body(
|
|
948
|
-
entries if isinstance(entries, list) else []
|
|
949
|
-
)
|
|
950
|
-
slice_ = _section_1_slice(text)
|
|
951
|
-
if slice_ is None:
|
|
952
|
-
return None
|
|
953
|
-
heading = SECTION_HEADING_PATTERN.search(text)
|
|
954
|
-
assert heading is not None # _section_1_slice returned a slice
|
|
955
|
-
return heading.group(0), slice_.rstrip()
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
def _structured_section_1_body(entries: list) -> str:
|
|
959
|
-
"""schema-v2 `clarificationItems[]` 를 §1 표 본문으로.
|
|
960
|
-
|
|
961
|
-
메타 셀은 렌더러가 쓰는 모양 그대로다 — `Status:` 는 따옴표 없이 써야
|
|
962
|
-
`_reconcile_user_input` 이 답을 병합하면서 상태를 answered 로 넘길 수 있다."""
|
|
963
|
-
rows = [e for e in entries if isinstance(e, dict) and e.get("id")]
|
|
964
|
-
if not rows:
|
|
965
|
-
return f"\n{_SECTION_1_EMPTY_STATE}"
|
|
966
|
-
lines = ["", _SECTION_1_TABLE_HEADER]
|
|
967
|
-
for entry in rows:
|
|
968
|
-
meta = (
|
|
969
|
-
f"**{entry['id']}**"
|
|
970
|
-
f"<br>Ticket: `{to_cell_text(entry.get('ticketId'))}`"
|
|
971
|
-
f"<br>Kind: `{to_cell_text(entry.get('kind'))}`"
|
|
972
|
-
f"<br>Blocks: `{to_cell_text(entry.get('blocks'))}`"
|
|
973
|
-
f"<br>Status: {to_cell_text(entry.get('status'))}"
|
|
974
|
-
)
|
|
975
|
-
lines.append(
|
|
976
|
-
f"| {meta} | {to_cell_text(entry.get('statement'))} "
|
|
977
|
-
f"| {to_cell_text(entry.get('expectedForm'))} "
|
|
978
|
-
f"| {to_cell_text(entry.get('userInput'))} |"
|
|
979
|
-
)
|
|
980
|
-
return "\n".join(lines)
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
# The final-report renderer writes `Status: open` / `Status: answered` unquoted
|
|
984
|
-
# in the stacked meta cell; only those two are unresolved. Resolve in place so
|
|
985
|
-
# the meta cell's other fields (ID, Ticket, Kind, Blocks) are left untouched.
|
|
986
|
-
_STATUS_ANSWER_RE = re.compile(r"(Status:\s*)(?:open|answered)\b", re.IGNORECASE)
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
def _locate_user_input_column(lines: list[str]) -> tuple[int, int]:
|
|
990
|
-
"""§1 데이터 표의 헤더 줄 인덱스와 `User input` 열 인덱스. 표가 없으면 (-1, -1)."""
|
|
991
|
-
for idx, line in enumerate(lines):
|
|
992
|
-
if not line.lstrip().startswith("|"):
|
|
993
|
-
continue
|
|
994
|
-
cells = [c.lower() for c in _split_pipe_row(line)]
|
|
995
|
-
if "user input" in cells and any(c.startswith("statement") for c in cells):
|
|
996
|
-
return idx, cells.index("user input")
|
|
997
|
-
return -1, -1
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
def _reconcile_row(line: str, ui_col: int, answers: dict[str, str]) -> str:
|
|
1001
|
-
"""답이 있고 open/answered 인 행이면 `User input` 칸을 그 답으로 채우고 Status 를
|
|
1002
|
-
answered 로 바꾼 줄을, 그 외에는 원본 줄을 그대로 돌려준다.
|
|
1003
|
-
|
|
1004
|
-
칸에 이미 값이 있어도 사용자의 사이드카 답이 이긴다. 그 칸을 채우는 것은
|
|
1005
|
-
run 자신(직전 렌더가 옮겨 적은 값)이고, 사용자가 나중에 답을 바꾸면 둘이
|
|
1006
|
-
갈라진다 — 사용자가 쓴 쪽을 정본으로 삼지 않으면 run 이 자기가 적어둔 값으로
|
|
1007
|
-
계속 되돌아간다.
|
|
1008
|
-
|
|
1009
|
-
판정은 앵커/백틱을 벗긴 셀(`_split_pipe_row`)로 — 그래야 `_meta_id` 가 스크롤
|
|
1010
|
-
앵커의 소문자 slug 대신 진짜 대문자 ID 를 읽는다. 재조립은 원본 셀
|
|
1011
|
-
(`split_pipe_row`)로 해서 앵커를 보존한다."""
|
|
1012
|
-
norm = _split_pipe_row(line)
|
|
1013
|
-
item = parse_meta_cell(norm[0]) if norm else None
|
|
1014
|
-
if item is None or item.row_id not in answers:
|
|
1015
|
-
return line
|
|
1016
|
-
if item.status not in UNRESOLVED_STATUSES:
|
|
1017
|
-
return line
|
|
1018
|
-
raw = split_pipe_row(line)
|
|
1019
|
-
if not 0 <= ui_col < len(raw):
|
|
1020
|
-
return line
|
|
1021
|
-
raw[ui_col] = answers[item.row_id]
|
|
1022
|
-
raw[0] = _STATUS_ANSWER_RE.sub(r"\1answered", raw[0])
|
|
1023
|
-
return "| " + " | ".join(to_cell_text(c) for c in raw) + " |"
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
def _reconcile_user_input(section: str, answers: dict[str, str]) -> str:
|
|
1027
|
-
"""§1 표에서 사이드카 답이 있는 미해결 행의 `User input` 칸을 답으로 채우고
|
|
1028
|
-
Status 를 answered 로 바꾼 §1 본문을 돌려준다.
|
|
1029
|
-
|
|
1030
|
-
답의 정본 위치를 §1 표 안으로 옮긴다 — 표만 읽는 승인 게이트·프롬프트
|
|
1031
|
-
빌더·검증 워커가 모두 답을 보게 하려는 것. 사이드카는 §1 표 밖 별도 섹션에만
|
|
1032
|
-
있어서 표만 신뢰하는 소비자는 그 답을 놓쳤다."""
|
|
1033
|
-
lines = section.splitlines()
|
|
1034
|
-
header_idx, ui_col = _locate_user_input_column(lines)
|
|
1035
|
-
if header_idx < 0:
|
|
1036
|
-
return section
|
|
1037
|
-
out = list(lines)
|
|
1038
|
-
body = False
|
|
1039
|
-
for i in range(header_idx + 1, len(lines)):
|
|
1040
|
-
line = lines[i]
|
|
1041
|
-
if not line.lstrip().startswith("|"):
|
|
1042
|
-
if body:
|
|
1043
|
-
break
|
|
1044
|
-
continue
|
|
1045
|
-
if is_separator_row(line):
|
|
1046
|
-
body = True
|
|
1047
|
-
continue
|
|
1048
|
-
if body:
|
|
1049
|
-
out[i] = _reconcile_row(line, ui_col, answers)
|
|
1050
|
-
return "\n".join(out)
|