okstra 0.206.0 → 0.207.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 +3 -3
- package/dist/cli-registry.mjs +7 -1
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/lifecycle/install.mjs +1 -1
- package/dist/commands/lifecycle/install.mjs.map +1 -1
- package/docs/architecture/storage-model.md +1 -0
- package/docs/architecture.md +40 -16
- package/docs/cli.md +17 -15
- package/docs/contributor-change-matrix.md +3 -2
- package/docs/performance-improvement-plan-v2.md +1 -1
- package/docs/project-structure-overview.md +43 -20
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/operations/code-review.json +1 -1
- package/runtime/bin/lib/okstra/usage.sh +3 -3
- package/runtime/bin/okstra-compact-reminder.sh +1 -1
- package/runtime/bin/okstra-spawn-followups.py +2 -2
- package/runtime/prompts/duties/direction-selection-worker.json +1 -1
- package/runtime/prompts/launch.template.md +2 -2
- package/runtime/prompts/lead/adapters/cmux.md +4 -3
- package/runtime/prompts/lead/context-loader.md +1 -1
- package/runtime/prompts/lead/convergence.md +44 -12
- package/runtime/prompts/lead/okstra-lead-contract.md +44 -73
- package/runtime/prompts/lead/phase-routing.md +64 -0
- package/runtime/prompts/lead/report-writer.md +10 -8
- package/runtime/prompts/lead/team-contract.md +1 -1
- package/runtime/prompts/profiles/_clarification-recommendation.md +4 -4
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +2 -2
- package/runtime/prompts/profiles/_coverage-critic.md +1 -1
- package/runtime/prompts/profiles/forbidden-actions.json +0 -94
- package/runtime/prompts/wizard/prompts.ko.json +2 -1
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +5 -5
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +3 -2
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +17 -26
- package/runtime/python/okstra_ctl/agent/prompt_cli/batch.py +183 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +60 -10
- package/runtime/python/okstra_ctl/agent/prompt_cli/corrections.py +1 -1
- package/runtime/python/okstra_ctl/agent/prompt_cli/jobs.py +21 -4
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +10 -1
- package/runtime/python/okstra_ctl/analysis_inputs.py +0 -39
- package/runtime/python/okstra_ctl/analysis_scope.py +31 -0
- package/runtime/python/okstra_ctl/approval_decisions.py +32 -2
- package/runtime/python/okstra_ctl/asset_roots.py +19 -0
- package/runtime/python/okstra_ctl/assignment_resolver.py +8 -0
- package/runtime/python/okstra_ctl/blocking_checks.py +7 -0
- package/runtime/python/okstra_ctl/code_review_target.py +92 -6
- package/runtime/python/okstra_ctl/consumers.py +12 -0
- package/runtime/python/okstra_ctl/dispatch_checkpoints.py +121 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +54 -32
- package/runtime/python/okstra_ctl/dispatch_state.py +34 -5
- package/runtime/python/okstra_ctl/doctor.py +2 -1
- package/runtime/python/okstra_ctl/domain/provider.py +5 -0
- package/runtime/python/okstra_ctl/domain/worker_presentation.py +21 -2
- package/runtime/python/okstra_ctl/domain/write_policy.py +2 -1
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +46 -9
- package/runtime/python/okstra_ctl/handoff.py +11 -466
- package/runtime/python/okstra_ctl/handoff_error.py +5 -0
- package/runtime/python/okstra_ctl/implementation_direction.py +0 -477
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +13 -1
- package/runtime/python/okstra_ctl/lead_progress.py +33 -1
- package/runtime/python/okstra_ctl/manager_view.py +26 -19
- package/runtime/python/okstra_ctl/model_io/lines.py +21 -4
- package/runtime/python/okstra_ctl/models.py +4 -1
- package/runtime/python/okstra_ctl/operation_invocation.py +11 -2
- package/runtime/python/okstra_ctl/option_comparison.py +3 -165
- package/runtime/python/okstra_ctl/option_votes.py +3 -191
- package/runtime/python/okstra_ctl/paths.py +8 -6
- package/runtime/python/okstra_ctl/phases/catalog.py +56 -12
- package/runtime/python/okstra_ctl/phases/change_impact_analysis/boundary.json +11 -0
- package/runtime/python/okstra_ctl/phases/change_impact_analysis/entry.py +39 -0
- package/runtime/python/okstra_ctl/{report_html/view_models/change_impact_analysis.py → phases/change_impact_analysis/report.py} +3 -3
- package/runtime/python/okstra_ctl/phases/change_impact_analysis/spec.md +26 -0
- package/runtime/python/okstra_ctl/phases/change_impact_analysis/validation.py +23 -0
- package/runtime/python/okstra_ctl/phases/error_analysis/__init__.py +1 -0
- package/runtime/python/okstra_ctl/phases/error_analysis/boundary.json +9 -0
- package/runtime/{prompts/profiles/error-analysis.md → python/okstra_ctl/phases/error_analysis/profile.md} +2 -2
- package/runtime/python/okstra_ctl/{report_html/view_models/error_analysis.py → phases/error_analysis/report.py} +9 -8
- package/runtime/{templates/reports → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis-input.template.md +1 -1
- package/runtime/python/okstra_ctl/phases/error_analysis/spec.md +118 -0
- package/runtime/python/okstra_ctl/phases/error_analysis/validation.py +241 -0
- package/runtime/python/okstra_ctl/phases/feature_analysis/__init__.py +1 -0
- package/runtime/python/okstra_ctl/phases/feature_analysis/boundary.json +8 -0
- package/runtime/python/okstra_ctl/phases/feature_analysis/entry.py +63 -0
- package/runtime/python/okstra_ctl/{report_html/view_models/feature_analysis.py → phases/feature_analysis/report.py} +12 -5
- package/runtime/python/okstra_ctl/phases/feature_analysis/spec.md +22 -0
- package/runtime/python/okstra_ctl/phases/feature_analysis/validation.py +27 -0
- package/runtime/python/okstra_ctl/phases/feature_analysis/wizard.py +95 -0
- package/runtime/python/okstra_ctl/phases/final_verification/boundary.json +8 -0
- package/runtime/python/okstra_ctl/phases/final_verification/profile.md +2 -2
- package/runtime/{templates/reports → python/okstra_ctl/phases/final_verification/report_assets}/final-verification-input.template.md +1 -1
- package/runtime/python/okstra_ctl/phases/final_verification/spec.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation/__init__.py +1 -0
- package/runtime/python/okstra_ctl/phases/implementation/boundary.json +17 -0
- package/runtime/python/okstra_ctl/{implementation_stage.py → phases/implementation/entry.py} +22 -10
- package/runtime/{prompts/host-orchestration/implementation.md → python/okstra_ctl/phases/implementation/host-rules.md} +1 -1
- package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-deliverable.md +1 -1
- package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-executor.md +4 -3
- package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-verifier.md +18 -7
- package/runtime/{prompts/profiles/implementation.md → python/okstra_ctl/phases/implementation/profile.md} +5 -5
- package/runtime/python/okstra_ctl/{report_html/view_models/implementation.py → phases/implementation/report.py} +3 -3
- package/runtime/{templates/reports → python/okstra_ctl/phases/implementation/report_assets}/implementation-input.template.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation/spec.md +238 -0
- package/runtime/python/okstra_ctl/phases/implementation/validation.py +205 -0
- package/runtime/python/okstra_ctl/phases/implementation/wizard.py +39 -0
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/__init__.py +1 -0
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/authoring.py +80 -0
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/boundary.json +10 -0
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/comparison.py +168 -0
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/entry.py +27 -0
- package/runtime/{prompts/profiles/implementation-option-selection.md → python/okstra_ctl/phases/implementation_option_selection/profile.md} +3 -3
- package/runtime/python/okstra_ctl/{report_html/view_models/implementation_option_selection.py → phases/implementation_option_selection/report.py} +2 -2
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/spec.md +83 -0
- package/runtime/python/okstra_ctl/{implementation_options.py → phases/implementation_option_selection/validation.py} +3 -3
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/votes.py +194 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/__init__.py +1 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +2345 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/boundary.json +12 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/entry.py +161 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/guidance.py +178 -0
- package/runtime/{prompts/lead → python/okstra_ctl/phases/implementation_planning/instructions}/plan-body-verification.md +61 -51
- package/runtime/python/okstra_ctl/phases/implementation_planning/plan_body.py +3295 -0
- package/runtime/{prompts/profiles/implementation-planning.md → python/okstra_ctl/phases/implementation_planning/profile.md} +74 -25
- package/runtime/python/okstra_ctl/phases/implementation_planning/report.py +237 -0
- package/runtime/{templates/reports → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning-input.template.md +2 -2
- package/runtime/python/okstra_ctl/phases/implementation_planning/spec.md +204 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/validation.py +597 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/wizard.py +166 -0
- package/runtime/python/okstra_ctl/phases/improvement_discovery/boundary.json +12 -0
- package/runtime/python/okstra_ctl/{improvement_lenses.py → phases/improvement_discovery/lenses.py} +1 -6
- package/runtime/{prompts/profiles/improvement-discovery.md → python/okstra_ctl/phases/improvement_discovery/profile.md} +5 -5
- package/runtime/python/okstra_ctl/{report_html/view_models/improvement_discovery.py → phases/improvement_discovery/report.py} +3 -3
- package/runtime/{templates/reports → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery-input.template.md +1 -2
- package/runtime/python/okstra_ctl/phases/improvement_discovery/spec.md +29 -0
- package/runtime/{validators/validate_improvement_report.py → python/okstra_ctl/phases/improvement_discovery/validation.py} +5 -14
- package/runtime/python/okstra_ctl/phases/project_analysis/__init__.py +1 -0
- package/runtime/python/okstra_ctl/phases/project_analysis/boundary.json +8 -0
- package/runtime/python/okstra_ctl/phases/project_analysis/entry.py +11 -0
- package/runtime/python/okstra_ctl/{report_html/view_models/project_analysis.py → phases/project_analysis/report.py} +3 -3
- package/runtime/python/okstra_ctl/phases/project_analysis/spec.md +33 -0
- package/runtime/python/okstra_ctl/phases/project_analysis/validation.py +55 -0
- package/runtime/python/okstra_ctl/phases/release_handoff/__init__.py +1 -0
- package/runtime/python/okstra_ctl/phases/release_handoff/boundary.json +17 -0
- package/runtime/python/okstra_ctl/phases/release_handoff/entry.py +147 -0
- package/runtime/python/okstra_ctl/phases/release_handoff/operations.py +446 -0
- package/runtime/{prompts/profiles/release-handoff.md → python/okstra_ctl/phases/release_handoff/profile.md} +3 -3
- package/runtime/python/okstra_ctl/{report_html/view_models/release_handoff.py → phases/release_handoff/report.py} +3 -3
- package/runtime/{templates/reports → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff-input.template.md +1 -1
- package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +233 -0
- package/runtime/python/okstra_ctl/phases/release_handoff/wizard.py +84 -0
- package/runtime/python/okstra_ctl/phases/requirements_discovery/__init__.py +1 -0
- package/runtime/python/okstra_ctl/phases/requirements_discovery/boundary.json +9 -0
- package/runtime/{prompts/profiles/requirements-discovery.md → python/okstra_ctl/phases/requirements_discovery/profile.md} +2 -3
- package/runtime/python/okstra_ctl/{report_html/view_models/requirements_discovery.py → phases/requirements_discovery/report.py} +3 -3
- package/runtime/python/okstra_ctl/phases/requirements_discovery/spec.md +132 -0
- package/runtime/{validators/validate_fanout.py → python/okstra_ctl/phases/requirements_discovery/validation.py} +11 -12
- package/runtime/python/okstra_ctl/phases/technical_verification/__init__.py +1 -0
- package/runtime/python/okstra_ctl/phases/technical_verification/boundary.json +9 -0
- package/runtime/python/okstra_ctl/phases/technical_verification/entry.py +100 -0
- package/runtime/{prompts/profiles/technical-verification.md → python/okstra_ctl/phases/technical_verification/profile.md} +2 -2
- package/runtime/python/okstra_ctl/{report_html/view_models/technical_verification.py → phases/technical_verification/report.py} +2 -2
- package/runtime/python/okstra_ctl/phases/technical_verification/spec.md +37 -0
- package/runtime/python/okstra_ctl/phases/technical_verification/validation.py +90 -0
- package/runtime/python/okstra_ctl/plan_approval.py +70 -0
- package/runtime/python/okstra_ctl/plan_items_cli.py +2 -2130
- package/runtime/python/okstra_ctl/process_group.py +118 -0
- package/runtime/python/okstra_ctl/profile_show.py +3 -3
- package/runtime/python/okstra_ctl/render.py +15 -4
- package/runtime/python/okstra_ctl/report_assembly.py +28 -92
- package/runtime/python/okstra_ctl/report_finalize.py +106 -2
- package/runtime/python/okstra_ctl/report_html/context_links.py +1 -1
- package/runtime/python/okstra_ctl/report_projections.py +1 -36
- package/runtime/python/okstra_ctl/report_routing.py +23 -0
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +4 -73
- package/runtime/python/okstra_ctl/report_validation_identity.py +38 -0
- package/runtime/python/okstra_ctl/report_views.py +1 -1
- package/runtime/python/okstra_ctl/run.py +68 -350
- package/runtime/python/okstra_ctl/run_artifact_prune.py +200 -0
- package/runtime/python/okstra_ctl/stage_map.py +13 -0
- package/runtime/python/okstra_ctl/team.py +108 -9
- package/runtime/python/okstra_ctl/technical_verification_facts.py +52 -0
- package/runtime/python/okstra_ctl/wizard/__init__.py +31 -31
- package/runtime/python/okstra_ctl/wizard/api.py +18 -0
- package/runtime/python/okstra_ctl/wizard/outcome.py +3 -12
- package/runtime/python/okstra_ctl/wizard/registry.py +20 -12
- package/runtime/python/okstra_ctl/wizard/steps_analysis.py +0 -97
- package/runtime/python/okstra_ctl/wizard/steps_options.py +8 -0
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +10 -263
- package/runtime/python/okstra_ctl/wizard/steps_roles.py +2 -1
- package/runtime/python/okstra_ctl/work_categories.py +1 -1
- package/runtime/python/okstra_ctl/worker_dispatch.py +44 -3
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +36 -0
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +19 -0
- package/runtime/python/okstra_ctl/worker_runner.py +21 -3
- package/runtime/python/okstra_ctl/workflow.py +26 -143
- package/runtime/python/okstra_ctl/write_policy.py +57 -7
- package/runtime/python/okstra_project/dirs.py +14 -0
- package/runtime/python/okstra_project/resolver.py +2 -1
- package/runtime/schemas/execution-manifest-v2.schema.json +2 -1
- package/runtime/skills/okstra-brief-gen/SKILL.md +3 -3
- package/runtime/skills/okstra-code-review/SKILL.md +70 -32
- package/runtime/skills/okstra-code-review/references/review-calibration.md +26 -6
- package/runtime/skills/okstra-run/SKILL.md +3 -3
- package/runtime/templates/manager/view.template.html +18 -1
- package/runtime/templates/reports/quick-input.template.md +1 -1
- package/runtime/templates/reports/task-brief.template.md +1 -1
- package/runtime/validators/validate-brief.py +2 -2
- package/runtime/validators/validate-run.py +299 -3940
- package/runtime/validators/validate_analysis_report.py +14 -126
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +0 -147
- package/runtime/python/okstra_ctl/technical_verification.py +0 -195
- /package/runtime/{prompts/profiles/change-impact-analysis.json → python/okstra_ctl/phases/change_impact_analysis/profile.json} +0 -0
- /package/runtime/{prompts/profiles/change-impact-analysis.md → python/okstra_ctl/phases/change_impact_analysis/profile.md} +0 -0
- /package/runtime/{templates/reports → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis-input.template.md +0 -0
- /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis.template.html +0 -0
- /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis.template.md +0 -0
- /package/runtime/{prompts/profiles/error-analysis.json → python/okstra_ctl/phases/error_analysis/profile.json} +0 -0
- /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis.template.html +0 -0
- /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis.template.md +0 -0
- /package/runtime/{prompts/profiles/feature-analysis.json → python/okstra_ctl/phases/feature_analysis/profile.json} +0 -0
- /package/runtime/{prompts/profiles/feature-analysis.md → python/okstra_ctl/phases/feature_analysis/profile.md} +0 -0
- /package/runtime/{templates/reports → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis-input.template.md +0 -0
- /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis.template.html +0 -0
- /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis.template.md +0 -0
- /package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-diff-review.md +0 -0
- /package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-self-check.md +0 -0
- /package/runtime/{prompts/profiles/implementation.json → python/okstra_ctl/phases/implementation/profile.json} +0 -0
- /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation/report_assets}/implementation.template.html +0 -0
- /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation/report_assets}/implementation.template.md +0 -0
- /package/runtime/{prompts/profiles/implementation-option-selection.json → python/okstra_ctl/phases/implementation_option_selection/profile.json} +0 -0
- /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation_option_selection/report_assets}/implementation-option-selection.template.html +0 -0
- /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation_option_selection/report_assets}/implementation-option-selection.template.md +0 -0
- /package/runtime/{prompts/host-orchestration/implementation-planning.md → python/okstra_ctl/phases/implementation_planning/host-rules.md} +0 -0
- /package/runtime/{prompts/profiles/implementation-planning.json → python/okstra_ctl/phases/implementation_planning/profile.json} +0 -0
- /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning.template.html +0 -0
- /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning.template.md +0 -0
- /package/runtime/{prompts/profiles/improvement-discovery.json → python/okstra_ctl/phases/improvement_discovery/profile.json} +0 -0
- /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery.template.html +0 -0
- /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery.template.md +0 -0
- /package/runtime/{prompts/profiles/project-analysis.json → python/okstra_ctl/phases/project_analysis/profile.json} +0 -0
- /package/runtime/{prompts/profiles/project-analysis.md → python/okstra_ctl/phases/project_analysis/profile.md} +0 -0
- /package/runtime/{templates/reports → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis-input.template.md +0 -0
- /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis.template.html +0 -0
- /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis.template.md +0 -0
- /package/runtime/{prompts/profiles/release-handoff.json → python/okstra_ctl/phases/release_handoff/profile.json} +0 -0
- /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff.template.html +0 -0
- /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff.template.md +0 -0
- /package/runtime/python/okstra_ctl/{fanout.py → phases/requirements_discovery/fanout.py} +0 -0
- /package/runtime/{prompts/profiles/requirements-discovery.json → python/okstra_ctl/phases/requirements_discovery/profile.json} +0 -0
- /package/runtime/{templates/reports → python/okstra_ctl/phases/requirements_discovery/report_assets}/fan-out-unit.template.md +0 -0
- /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/requirements_discovery/report_assets}/requirements-discovery.template.html +0 -0
- /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/requirements_discovery/report_assets}/requirements-discovery.template.md +0 -0
- /package/runtime/{prompts/profiles/technical-verification.json → python/okstra_ctl/phases/technical_verification/profile.json} +0 -0
- /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/technical_verification/report_assets}/technical-verification.template.html +0 -0
- /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/technical_verification/report_assets}/technical-verification.template.md +0 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""구현 계획 진입에서 방향 선택과 기존 계획 재실행을 구분한다."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from okstra_ctl.implementation_direction import (
|
|
11
|
+
DirectionSelectionError,
|
|
12
|
+
lexical_absolute_path,
|
|
13
|
+
resolve_selected_direction,
|
|
14
|
+
validate_task_artifact_path,
|
|
15
|
+
)
|
|
16
|
+
from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
|
|
17
|
+
from okstra_ctl.paths import task_dir, task_runs_dir
|
|
18
|
+
from okstra_ctl.wizard.ids import S_SELECTED_DIRECTION_PICK
|
|
19
|
+
from okstra_ctl.wizard.state import Prompt, WizardError, WizardState
|
|
20
|
+
from okstra_ctl.wizard.prompts import _opt, _p
|
|
21
|
+
from okstra_ctl.wizard.sources import _project_relative_path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# 후보는 리포트 **기록**으로 찾는다. 전체 열람본(`.md`)은 `2bcd575` 이후 완료
|
|
25
|
+
# 산출물이 아니라 요청 시 렌더라, `.md` 를 글롭하는 동안 이 목록은 항상 비었고
|
|
26
|
+
# 방향 선택 단계 자체가 뜨지 않았다 — 비교가 끝나도 계획으로 갈 길이 없었다.
|
|
27
|
+
_SELECTION_REPORT_RE = re.compile(
|
|
28
|
+
r"^final-report-implementation-option-selection-(?P<seq>\d{3,})\.data\.json$"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _selected_direction_candidates(state: WizardState) -> list[str]:
|
|
33
|
+
if not state.project_root or not state.task_group or not state.task_id:
|
|
34
|
+
return []
|
|
35
|
+
project_root = Path(state.project_root).resolve()
|
|
36
|
+
task_root = lexical_absolute_path(
|
|
37
|
+
task_dir(project_root, state.task_group, state.task_id)
|
|
38
|
+
)
|
|
39
|
+
reports = (
|
|
40
|
+
task_runs_dir(project_root, state.task_group, state.task_id)
|
|
41
|
+
/ "implementation-option-selection"
|
|
42
|
+
/ "reports"
|
|
43
|
+
)
|
|
44
|
+
candidates: list[tuple[int, Path]] = []
|
|
45
|
+
for report in reports.glob(
|
|
46
|
+
"final-report-implementation-option-selection-*.data.json"
|
|
47
|
+
):
|
|
48
|
+
match = _SELECTION_REPORT_RE.fullmatch(report.name)
|
|
49
|
+
if match is None:
|
|
50
|
+
continue
|
|
51
|
+
try:
|
|
52
|
+
validated = validate_task_artifact_path(
|
|
53
|
+
report, task_root, "selection report"
|
|
54
|
+
)
|
|
55
|
+
except DirectionSelectionError:
|
|
56
|
+
continue
|
|
57
|
+
candidates.append((int(match.group("seq")), validated))
|
|
58
|
+
return [
|
|
59
|
+
_project_relative_path(path, project_root)
|
|
60
|
+
for _, path in sorted(candidates, reverse=True)[:3]
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _planning_rerun_selected(state: WizardState) -> bool:
|
|
65
|
+
if (
|
|
66
|
+
not state.clarification_response_path
|
|
67
|
+
or not state.project_root
|
|
68
|
+
or not state.task_group
|
|
69
|
+
or not state.task_id
|
|
70
|
+
):
|
|
71
|
+
return False
|
|
72
|
+
project_root = Path(state.project_root).resolve()
|
|
73
|
+
raw_path = Path(state.clarification_response_path).expanduser()
|
|
74
|
+
path = lexical_absolute_path(
|
|
75
|
+
raw_path if raw_path.is_absolute() else project_root / raw_path
|
|
76
|
+
)
|
|
77
|
+
task_root = lexical_absolute_path(
|
|
78
|
+
task_dir(project_root, state.task_group, state.task_id)
|
|
79
|
+
)
|
|
80
|
+
reports = lexical_absolute_path(
|
|
81
|
+
task_runs_dir(project_root, state.task_group, state.task_id)
|
|
82
|
+
/ "implementation-planning"
|
|
83
|
+
/ "reports"
|
|
84
|
+
)
|
|
85
|
+
try:
|
|
86
|
+
validate_task_artifact_path(path, task_root, "planning report")
|
|
87
|
+
except DirectionSelectionError:
|
|
88
|
+
return False
|
|
89
|
+
return (
|
|
90
|
+
path.is_file()
|
|
91
|
+
and not path.is_symlink()
|
|
92
|
+
and re.fullmatch(
|
|
93
|
+
r"final-report-implementation-planning-\d{3,}\.(?:md|data\.json)", path.name
|
|
94
|
+
)
|
|
95
|
+
is not None
|
|
96
|
+
and path.parent == reports
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _build_selected_direction_pick(state: WizardState) -> Prompt:
|
|
101
|
+
candidates = _selected_direction_candidates(state)
|
|
102
|
+
t = _p(state.workspace_root, S_SELECTED_DIRECTION_PICK)
|
|
103
|
+
if not candidates:
|
|
104
|
+
raise WizardError(t["errors"]["none"])
|
|
105
|
+
return Prompt(
|
|
106
|
+
step=S_SELECTED_DIRECTION_PICK,
|
|
107
|
+
kind="pick",
|
|
108
|
+
label=t["label"],
|
|
109
|
+
options=[
|
|
110
|
+
_opt(path, _selection_option_label(state, path, t)) for path in candidates
|
|
111
|
+
],
|
|
112
|
+
echo_template=t["echo_template"],
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _selection_option_label(state: WizardState, rel_path: str, t: dict) -> str:
|
|
117
|
+
"""후보 경로에 그 리포트가 실제로 무엇을 담았는지 붙인다.
|
|
118
|
+
|
|
119
|
+
목록은 최신순이라 후보가 0건인 최근 재실행이 맨 위에 온다. 경로만 보이면
|
|
120
|
+
사용자가 그것을 고르고, 방향 없는 리포트로 계획을 열려다 막힌다.
|
|
121
|
+
"""
|
|
122
|
+
record = Path(state.project_root) / rel_path
|
|
123
|
+
try:
|
|
124
|
+
data = load_owned_object(record, artifact="implementation option data")
|
|
125
|
+
except (OSError, JsonBoundaryError):
|
|
126
|
+
return rel_path
|
|
127
|
+
selection = data.get("implementationOptionSelection")
|
|
128
|
+
if not isinstance(selection, Mapping):
|
|
129
|
+
return rel_path
|
|
130
|
+
rows = selection.get("rankedOptions")
|
|
131
|
+
ids = (
|
|
132
|
+
[
|
|
133
|
+
str(row.get("id"))
|
|
134
|
+
for row in rows
|
|
135
|
+
if isinstance(row, Mapping) and row.get("id")
|
|
136
|
+
]
|
|
137
|
+
if isinstance(rows, list)
|
|
138
|
+
else []
|
|
139
|
+
)
|
|
140
|
+
if not ids:
|
|
141
|
+
return t["labels"]["no_options"].format(path=rel_path)
|
|
142
|
+
return t["labels"]["options"].format(path=rel_path, ids=", ".join(ids))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _submit_selected_direction_pick(state: WizardState, value: str) -> Optional[str]:
|
|
146
|
+
t = _p(state.workspace_root, S_SELECTED_DIRECTION_PICK)
|
|
147
|
+
candidates = _selected_direction_candidates(state)
|
|
148
|
+
if value not in candidates:
|
|
149
|
+
raise WizardError(t["errors"]["unknown"].format(value=value))
|
|
150
|
+
try:
|
|
151
|
+
resolve_selected_direction(
|
|
152
|
+
Path(state.project_root) / value,
|
|
153
|
+
expected_task_key=f"{state.project_id}:{state.task_group}:{state.task_id}",
|
|
154
|
+
)
|
|
155
|
+
except DirectionSelectionError as exc:
|
|
156
|
+
raise WizardError(str(exc)) from exc
|
|
157
|
+
state.selected_direction_path = value
|
|
158
|
+
# 두 입력은 상호 배타다 — 방향이 정해진 순간 이 런은 새 계획이고,
|
|
159
|
+
# clarification 자리에 남은 값은 render-bundle 이 거절할 이유일 뿐이다.
|
|
160
|
+
# 거절을 여기서 앞당기는 대신 값을 비우고, 무엇이 비워졌는지 echo 로
|
|
161
|
+
# 알린다. 사용자 답변은 손실되지 않는다: 그 사이드카는
|
|
162
|
+
# `--selected-direction` 경로로 계획 런에 첨부된다(`run.py` 참조).
|
|
163
|
+
if state.clarification_response_path:
|
|
164
|
+
state.clarification_response_path = ""
|
|
165
|
+
return t["echo_variants"]["cleared_clarification"].format(value=value)
|
|
166
|
+
return f"selected-direction: {value}"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"allowed": " - improvement candidate discovery within the lens whitelist defined in `scripts/okstra_ctl/phases/improvement_discovery/lenses.py`\n - top-N ranking (default 8, brief `candidate-cap` overrides within 1..12)\n - per-candidate columns Lens / Scope / Severity / Effort / Consensus / Source workers / Recommended next-phase / Expected behavior after / Evidence (path:line)\n - Phase 1.5 reflect-back grilling log at `runs/improvement-discovery/<seq>/state/phase-1.5-grilling.md`\n - Phase 5.5 consensus classification (full / partial / contested / worker-unique / unverified)",
|
|
3
|
+
"forbidden": [
|
|
4
|
+
"source code edits of any kind",
|
|
5
|
+
"implementation planning, root-cause analysis, builds, migrations, or deployments",
|
|
6
|
+
"starting `implementation-planning`, `implementation`, `error-analysis`, or any other lifecycle phase inside this run",
|
|
7
|
+
"generating candidates outside the lens whitelist (Lens enum violation rejects the report)",
|
|
8
|
+
"exceeding the candidate cap (absolute cap 12)",
|
|
9
|
+
"free external data fetch beyond the brief's Source Material or Phase 1.5 resolved scope",
|
|
10
|
+
"interpreting user phrases like `다음 단계 진행해` as authorisation to enter another phase"
|
|
11
|
+
]
|
|
12
|
+
}
|
package/runtime/python/okstra_ctl/{improvement_lenses.py → phases/improvement_discovery/lenses.py}
RENAMED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
"""Improvement
|
|
2
|
-
|
|
3
|
-
profile / brief skill / validator / wizard MUST import from this module.
|
|
4
|
-
Re-defining the enum anywhere else violates the single-reference-point rule
|
|
5
|
-
and is rejected by tests/test_okstra_improvement_lenses.py.
|
|
6
|
-
"""
|
|
1
|
+
"""Improvement discovery lens vocabulary and candidate bounds."""
|
|
7
2
|
from __future__ import annotations
|
|
8
3
|
|
|
9
4
|
LENSES: tuple[str, ...] = (
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{{INCLUDE:_common-contract.md}}
|
|
14
14
|
- Brief consumption (phase-specific addendum — shared rules live in `_common-contract.md` under "Brief handoff contract"):
|
|
15
15
|
- this phase REQUIRES a codebase-scan brief whose frontmatter contains `scope: codebase`. A brief without that marker is rejected before worker dispatch.
|
|
16
|
-
- the brief's `priority-lenses` MUST be a non-empty subset (size 1..4) of the lens whitelist defined in `scripts/okstra_ctl/
|
|
16
|
+
- the brief's `priority-lenses` MUST be a non-empty subset (size 1..4) of the lens whitelist defined in `scripts/okstra_ctl/phases/improvement_discovery/lenses.py`. Lenses outside the whitelist are rejected.
|
|
17
17
|
- the brief's `scan-scope` defines the only paths workers may read for candidate evidence. `out-of-scope` paths MUST be ignored even when the codebase is otherwise reachable.
|
|
18
18
|
- the brief's `candidate-cap` (default 8 if absent, absolute cap 12) bounds the number of rows in `## 5.9 Improvement Candidates`.
|
|
19
19
|
- Apply the shared reporter-confirmation precondition as written. For this phase any unresolved `intent-check:` / `conversion-block:` row uses `Blocks=next-phase`.
|
|
@@ -52,11 +52,11 @@
|
|
|
52
52
|
- Report assembly instructions:
|
|
53
53
|
- current branch — `schemaVersion: 2.0`: author the structured data contract below and let the independent renderers produce the full reading copy and task-specific human HTML.
|
|
54
54
|
- v1 legacy branch: when validating or rerendering an existing schema-v1 report, preserve its `## 5.9 Improvement Candidates` table and legacy Markdown contract; do not rewrite that historical data into v2 implicitly.
|
|
55
|
-
- the `## 5.9 Improvement Candidates` table populated with rows that obey the 11-column schema from `
|
|
55
|
+
- the `## 5.9 Improvement Candidates` table populated with rows that obey the 11-column schema from `scripts/okstra_ctl/phases/improvement_discovery/validation.py` (Cand ID `I-NNN`, Lens from whitelist, Title, Scope ⊆ scan-scope, Severity, Effort, Consensus, Source workers `<worker>:<id>` from {claude, codex, antigravity}, Recommended next-phase ∈ {requirements-discovery, implementation-option-selection, error-analysis}, Expected behavior after, Evidence as path:line list). `Expected behavior after` states, in one observable sentence, what becomes different once the candidate is applied — it is the seed of the downstream brief's `EB-NNN` / `EO-NNN`. A candidate you cannot write this cell for is a preference, not a finding: drop it rather than filling the cell with a restatement of the title.
|
|
56
56
|
- `Consensus` cells in `## 5.9 Improvement Candidates` use the table enum exactly: `full`, `partial`, `contested`, `worker-unique`. Map convergence's `full-consensus` / `partial-consensus` labels to `full` / `partial` before writing the table.
|
|
57
|
-
- Verdict Token — **branch-specific, and the two branches do not share a vocabulary.** On the current v2 branch use the shared analysis enum: `analysis-complete` when every resolved lens was examined, `analysis-partial` when one could not be, `blocked` when the scan itself could not run. `schemas/final-report-v2.0.schema.json` admits only those three for `finalVerdict.verdictToken` — the report's single verdict-token home — so a v2 report carrying `candidates-ready` fails Phase 7. **Finding no candidates is not a verdict**: it is an empty `candidates[]` plus a `lensCoverage[]` row per lens with `status: no-candidate` and its evidence-backed rationale — the verdict stays `analysis-complete`. `candidates-ready` / `no-candidates` belong to the v1 legacy `## 7. Final Verdict` Markdown alone, where `
|
|
58
|
-
-
|
|
59
|
-
- author the shared narrative fields plus `improvementDiscovery.candidates[]`, `improvementDiscovery.lensCoverage[]`, `improvementDiscovery.selectionLimit`, and `improvementDiscovery.userNarrative` in the report narrative. `candidates[]` carries the same 11 logical fields described above; `lensCoverage[]` records either candidate IDs or an evidence-backed no-candidate rationale for every resolved lens. `schemas/final-report-v3.0.schema.json`, `schemas/report-narrative-v3.0.schema.json`, and `
|
|
57
|
+
- Verdict Token — **branch-specific, and the two branches do not share a vocabulary.** On the current v2 branch use the shared analysis enum: `analysis-complete` when every resolved lens was examined, `analysis-partial` when one could not be, `blocked` when the scan itself could not run. `schemas/final-report-v2.0.schema.json` admits only those three for `finalVerdict.verdictToken` — the report's single verdict-token home — so a v2 report carrying `candidates-ready` fails Phase 7. **Finding no candidates is not a verdict**: it is an empty `candidates[]` plus a `lensCoverage[]` row per lens with `status: no-candidate` and its evidence-backed rationale — the verdict stays `analysis-complete`. `candidates-ready` / `no-candidates` belong to the v1 legacy `## 7. Final Verdict` Markdown alone, where `scripts/okstra_ctl/phases/improvement_discovery/validation.py` enforces them. Continuation guidance is owned by `prompts/lead/phase-routing.md`.
|
|
58
|
+
- Record each candidate's recommended next phase and scope as facts for the common lead routing policy.
|
|
59
|
+
- author the shared narrative fields plus `improvementDiscovery.candidates[]`, `improvementDiscovery.lensCoverage[]`, `improvementDiscovery.selectionLimit`, and `improvementDiscovery.userNarrative` in the report narrative. `candidates[]` carries the same 11 logical fields described above; `lensCoverage[]` records either candidate IDs or an evidence-backed no-candidate rationale for every resolved lens. `schemas/final-report-v3.0.schema.json`, `schemas/report-narrative-v3.0.schema.json`, and `scripts/okstra_ctl/phases/improvement_discovery/validation.py` enforce this contract. Phase 7 assembles the final record and derives the reading copy and human HTML.
|
|
60
60
|
- Clarification request policy (phase-specific addenda — shared policy is in `_common-contract.md`):
|
|
61
61
|
- if scan-scope or priority-lenses cannot be made concrete during Phase 1.5, end the run with Verdict Token `blocked`, populate `## 1. Clarification Items` with `Blocks=next-phase` rows, and do not run worker dispatch
|
|
62
62
|
{{INCLUDE:_clarification-recommendation.md}}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""Human-first improvement-discovery view model."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
from
|
|
5
|
-
from
|
|
6
|
-
from
|
|
4
|
+
from okstra_ctl.report_html.common import evidence_index
|
|
5
|
+
from okstra_ctl.report_html.models import HumanReportView, VisualNode
|
|
6
|
+
from okstra_ctl.report_html.visualizations import matrix_figure
|
|
7
7
|
|
|
8
8
|
# Record fields this template anchors as `id-<row id>` (see
|
|
9
9
|
# `HumanReportView.anchored_fields`); ids elsewhere land in the ledger.
|
|
@@ -78,5 +78,4 @@ being reconstructed.
|
|
|
78
78
|
|
|
79
79
|
## Conversion Note
|
|
80
80
|
|
|
81
|
-
-
|
|
82
|
-
- The candidate row's Recommended next-phase determines which `--task-type` to launch with.
|
|
81
|
+
Use `prompts/lead/phase-routing.md` for user selection and candidate continuation.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Improvement discovery phase
|
|
2
|
+
|
|
3
|
+
## Purpose and ownership
|
|
4
|
+
|
|
5
|
+
This phase discovers evidence-backed improvement candidates within a codebase scan's scope and resolved lenses. `lenses.py` owns the lens vocabulary and candidate bounds. `validation.py` owns candidate scope, identity, source-worker, consensus and report checks. `report.py` and `report_assets/` own the human view and dedicated input and report templates.
|
|
6
|
+
|
|
7
|
+
Common analysis dispatch, convergence, clarification handling, report schemas and artifact paths remain shared. The brief validator imports the phase lens vocabulary for codebase briefs. The doctor uses the same vocabulary for its lens check. The lead chooses candidate continuation using `prompts/lead/phase-routing.md`; candidate validation checks the declared target vocabulary without choosing a target.
|
|
8
|
+
|
|
9
|
+
## Execution and output
|
|
10
|
+
|
|
11
|
+
`profile.md` directs the lead's bounded scope-and-lens discussion and records its resolved scope, lenses and primary-pass assignments in `state/phase-1.5-grilling.md`. The common worker-prompt policy supplies the explicit log anchor. Every analyser inspects every resolved lens. Candidate rows retain their scope, expected observable change, evidence and source-worker identities.
|
|
12
|
+
|
|
13
|
+
The run validator calls `validate_improvement_report` with the existing report path, run directory and parsed brief frontmatter. Schema-v2 data uses the shared schema validator followed by candidate checks. Existing schema-v1 Markdown uses the eleven-column candidate table and the matching Verdict Card and Final Verdict rows. The migration preserves these version branches and error messages; it does not add a new schema branch.
|
|
14
|
+
|
|
15
|
+
The validator requires the resolved-log headings, but does not prove that each declared inspection or user conversation occurred. Primary-pass rotation and preserved-behavior reconciliation remain profile instructions reviewed during convergence.
|
|
16
|
+
|
|
17
|
+
## Guarantees
|
|
18
|
+
|
|
19
|
+
| ID | Guarantee | Enforcement |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| ID-1 | Candidate IDs, lens count and vocabulary, scan scope, source workers, consensus, continuation vocabulary and observable-change cells are checked. | `scripts/okstra_ctl/phases/improvement_discovery/validation.py::_check_candidate_row` |
|
|
22
|
+
| ID-2 | Candidate caps remain in 1..12 and bound the candidate count. | `scripts/okstra_ctl/phases/improvement_discovery/validation.py::_check_candidates_cap` |
|
|
23
|
+
| ID-3 | The run log contains resolved scope and lens blocks. | `scripts/okstra_ctl/phases/improvement_discovery/validation.py::_check_grilling_log` |
|
|
24
|
+
| ID-4 | Legacy verdict rows use the legacy vocabulary and match the Verdict Card. | `scripts/okstra_ctl/phases/improvement_discovery/validation.py::_check_final_verdict`, `scripts/okstra_ctl/phases/improvement_discovery/validation.py::_check_verdict_card` |
|
|
25
|
+
| ID-5 | Codebase briefs use one to four priority lenses from the phase vocabulary. | `validators/validate-brief.py::check_codebase_scope` |
|
|
26
|
+
|
|
27
|
+
## Tests
|
|
28
|
+
|
|
29
|
+
`tests/test_improvement_discovery_validation.py` exercises both existing report branches with real report files and candidate mutations. The shared brief, rendering, doctor, prompt-anchor and catalog tests continue exercising the common callers.
|
|
@@ -1,26 +1,16 @@
|
|
|
1
1
|
"""Validator for final-report.md produced by the improvement-discovery phase.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md §6.5.
|
|
3
|
+
Validates candidate rows and the legacy/current report branches described in spec.md.
|
|
5
4
|
|
|
6
5
|
Called by validators/validate-run.py when task_type == "improvement-discovery".
|
|
7
6
|
"""
|
|
8
7
|
from __future__ import annotations
|
|
9
8
|
|
|
10
|
-
import json
|
|
11
9
|
import re
|
|
12
|
-
import sys
|
|
13
10
|
from dataclasses import dataclass, field
|
|
14
11
|
from pathlib import Path
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
# insert whichever exists so okstra_ctl is importable directly.
|
|
18
|
-
_VALIDATORS_DIR = Path(__file__).resolve().parent
|
|
19
|
-
for _ssot_dir in (_VALIDATORS_DIR.parent / "scripts", _VALIDATORS_DIR.parent / "python"):
|
|
20
|
-
if _ssot_dir.is_dir() and str(_ssot_dir) not in sys.path:
|
|
21
|
-
sys.path.insert(0, str(_ssot_dir))
|
|
22
|
-
|
|
23
|
-
from okstra_ctl.improvement_lenses import (
|
|
13
|
+
from .lenses import (
|
|
24
14
|
LENSES,
|
|
25
15
|
DEFAULT_CANDIDATE_CAP,
|
|
26
16
|
ABSOLUTE_CANDIDATE_CAP,
|
|
@@ -32,6 +22,7 @@ from okstra_ctl.final_report_schema import (
|
|
|
32
22
|
validate as validate_final_report_data,
|
|
33
23
|
)
|
|
34
24
|
from okstra_ctl.md_table import split_pipe_row
|
|
25
|
+
from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
|
|
35
26
|
|
|
36
27
|
|
|
37
28
|
_VERDICT_TOKENS = ("candidates-ready", "no-candidates", "blocked")
|
|
@@ -353,8 +344,8 @@ def validate_improvement_report(
|
|
|
353
344
|
data_path = final_report_data_path(report_path)
|
|
354
345
|
if data_path.exists():
|
|
355
346
|
try:
|
|
356
|
-
data =
|
|
357
|
-
except
|
|
347
|
+
data = load_owned_object(data_path, artifact="improvement report data")
|
|
348
|
+
except JsonBoundaryError as exc:
|
|
358
349
|
return ValidationResult(ok=False, errors=[f"invalid report data: {exc}"])
|
|
359
350
|
if data.get("schemaVersion") == "2.0":
|
|
360
351
|
return _validate_v2_report(data, run_dir, brief_frontmatter)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""프로젝트 구조 분석 단계의 입력과 보고서 정책."""
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"allowed": " - components, dependency directions, entry points, repositories, and external integrations\n - shallow feature index and scan scope\n",
|
|
3
|
+
"forbidden": [
|
|
4
|
+
"source or configuration edits",
|
|
5
|
+
"tests, builds, migrations, or deployments",
|
|
6
|
+
"starting any other lifecycle phase inside this run"
|
|
7
|
+
]
|
|
8
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""프로젝트 분석 단계가 받지 않는 상류 입력을 거부한다."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from okstra_ctl.prepare_error import PrepareError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def validate_project_analysis_inputs(analysis_target: str, evidence_inputs_raw: str) -> None:
|
|
8
|
+
if analysis_target:
|
|
9
|
+
raise PrepareError("project-analysis does not accept an analysis target")
|
|
10
|
+
if evidence_inputs_raw:
|
|
11
|
+
raise PrepareError("project-analysis does not accept evidence inputs")
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""Human-first project-analysis view model."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
from
|
|
5
|
-
from
|
|
6
|
-
from
|
|
4
|
+
from okstra_ctl.report_html.common import evidence_index
|
|
5
|
+
from okstra_ctl.report_html.models import HumanReportView, VisualEdge, VisualNode
|
|
6
|
+
from okstra_ctl.report_html.visualizations import dependency_figure, infrastructure_figure, workflow_figure
|
|
7
7
|
|
|
8
8
|
# Record fields this template anchors as `id-<row id>` (see
|
|
9
9
|
# `HumanReportView.anchored_fields`); ids elsewhere land in the ledger. The
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Project analysis phase
|
|
2
|
+
|
|
3
|
+
## Guarantees
|
|
4
|
+
|
|
5
|
+
| ID | Guarantee | Enforcement |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| PA-1 | Project analysis rejects analysis-target and evidence-input arguments before run artifacts are written | `scripts/okstra_ctl/phases/project_analysis/entry.py::validate_project_analysis_inputs`; `tests/run/test_analysis_prepare_inputs.py::test_project_analysis_rejects_target_and_evidence_before_context` |
|
|
8
|
+
| PA-2 | Feature-index entry points stay inside the declared analysis scope | `scripts/okstra_ctl/phases/project_analysis/validation.py::validate_project_semantics` |
|
|
9
|
+
| PA-3 | Interface and workflow component references name declared components when components are present | `scripts/okstra_ctl/phases/project_analysis/validation.py::_validate_component_references` |
|
|
10
|
+
| PA-4 | Source-commit and evidence-input snapshots remain common analysis contracts | `validators/validate_analysis_report.py::validate_analysis_snapshot` |
|
|
11
|
+
| PA-5 | Scope, architecture and terminology conclusions reflect inspected code | unenforced — phase instructions and independent worker review; no generic validator proves free-form conclusions |
|
|
12
|
+
|
|
13
|
+
## Scope and input
|
|
14
|
+
|
|
15
|
+
The phase maps a bounded project area: repositories, directories, components, dependency directions, entry points and external systems. The brief defines scope and preserved boundaries. The common wizard collects identity, scope and model choices; this phase adds no separate wizard flow.
|
|
16
|
+
|
|
17
|
+
`entry.py` rejects analysis-target and upstream evidence-input arguments. Common preparation retains the source commit and analysis scope. Shared `analysis_inputs.py` reads previously published project feature indexes for downstream feature and impact analysis; it remains a consumer contract rather than phase execution code.
|
|
18
|
+
|
|
19
|
+
## Procedure and outputs
|
|
20
|
+
|
|
21
|
+
`profile.md` owns the scope questions, evidence expectations, shallow feature index and optional technology, internal-interface and workflow descriptions. `profile.json` retains the worker roster and roles. The common read-only execution, clarification and analysis-review contracts still apply.
|
|
22
|
+
|
|
23
|
+
The output remains `projectAnalysis` plus `analysisCommon` in the common v2/v3 report schemas. `report.py` renders architecture, infrastructure and workflow figures with the existing HTML and Markdown body templates. Source changes, builds, migrations, deployments and implementation plans remain outside this phase's scope.
|
|
24
|
+
|
|
25
|
+
## Validation and continuation
|
|
26
|
+
|
|
27
|
+
`validation.py` checks representative entry-point inclusion and declared component references. `analysis_scope.py` provides the same lexical path rules to phase and common evidence validation. Common validation retains evidence-row checks, worker outcomes, scope confirmation and accepted analysis lineage.
|
|
28
|
+
|
|
29
|
+
The profile supplies no lifecycle destination selection rule. Common `next_phase.py` retains project analysis as a pending side-track result. This migration does not add an automatic continuation or invent a routing field.
|
|
30
|
+
|
|
31
|
+
## Tests
|
|
32
|
+
|
|
33
|
+
`tests/test_project_analysis_validation.py` checks declared and unknown interface/workflow component references, empty-component behavior and scoped feature entry points. Existing `tests/contract/test_validate_run_analysis.py` exercises the full common analysis validator, and `tests/run/test_analysis_prepare_inputs.py` checks rejection before preparation writes a run context. Common report rendering tests cover the phase body through the public router.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""프로젝트 지도 안의 진입점 범위와 구성요소 참조 검증."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from okstra_ctl.analysis_scope import _path_is_included
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def validate_project_semantics(data: dict, errors: list[str]) -> None:
|
|
8
|
+
common = data.get("analysisCommon") or {}
|
|
9
|
+
included = ((common.get("scope") or {}).get("includedPaths") or [])
|
|
10
|
+
analysis = data.get("projectAnalysis") or {}
|
|
11
|
+
for row in analysis.get("featureIndex") or []:
|
|
12
|
+
if not isinstance(row, dict):
|
|
13
|
+
continue
|
|
14
|
+
entry = row.get("representativeEntryPoint") or {}
|
|
15
|
+
if not _path_is_included(entry.get("path"), included):
|
|
16
|
+
errors.append(
|
|
17
|
+
f"projectAnalysis.featureIndex `{row.get('id') or '?'}` representative "
|
|
18
|
+
"entry point must be inside analysisCommon.scope.includedPaths"
|
|
19
|
+
)
|
|
20
|
+
_validate_component_references(analysis, errors)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _validate_component_references(analysis: dict, errors: list[str]) -> None:
|
|
24
|
+
"""Every componentId a row names has to be a component this report declares.
|
|
25
|
+
|
|
26
|
+
The reader clicks these — they render as links to the component's row — so
|
|
27
|
+
a reference to a component that was never listed is a dead anchor, and one
|
|
28
|
+
to a component that exists under a different id is worse: it reads as a
|
|
29
|
+
relationship the analysis never found.
|
|
30
|
+
"""
|
|
31
|
+
declared = {
|
|
32
|
+
row.get("id")
|
|
33
|
+
for row in analysis.get("components") or []
|
|
34
|
+
if isinstance(row, dict) and row.get("id")
|
|
35
|
+
}
|
|
36
|
+
if not declared:
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
def check(component_id: object, where: str) -> None:
|
|
40
|
+
if isinstance(component_id, str) and component_id and component_id not in declared:
|
|
41
|
+
errors.append(f"{where} names component `{component_id}`, which is not declared")
|
|
42
|
+
|
|
43
|
+
for row in analysis.get("internalInterfaces") or []:
|
|
44
|
+
if not isinstance(row, dict):
|
|
45
|
+
continue
|
|
46
|
+
where = f"projectAnalysis.internalInterfaces `{row.get('id') or '?'}`"
|
|
47
|
+
check(row.get("ownerComponentId"), where)
|
|
48
|
+
for consumer in row.get("consumers") or []:
|
|
49
|
+
check(consumer, where)
|
|
50
|
+
for row in analysis.get("workflows") or []:
|
|
51
|
+
if not isinstance(row, dict):
|
|
52
|
+
continue
|
|
53
|
+
for step in row.get("steps") or []:
|
|
54
|
+
if isinstance(step, dict):
|
|
55
|
+
check(step.get("componentId"), f"projectAnalysis.workflows `{row.get('id') or '?'}`")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""release-handoff 단계의 전용 정책과 실행 자원."""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"allowed": " - entering this phase only when the cited final-verification report's verdict is release-ready — `Verdict Token` exactly `accepted`, or exactly `conditional-accept` with every Conditional Acceptance Condition row declaring `blocksReleaseHandoff: false` (`okstra_ctl.release_gate.release_handoff_allowed`)\n - asking the user (via `AskUserQuestion` / interactive prompt) which delivery action to take: `local checkout`, `push + PR`, or `skip` (end the run)\n - asking the user to pick a PR base branch from `staging` | `preprod` | `prod` | `main` | `dev` | a user-supplied branch name\n - drafting PR title and PR body **inline as the Okstra lead** (no drafter worker and no delegated dispatch); the lead reviews its own draft with the user before any mutating git / gh command runs\n - read-only git inspection: `git status`, `git diff`, `git log`, `git rev-parse`\n - pushing the current feature branch to its origin remote via `git push -u origin <current-branch>` (the feature branch only — NEVER the base branch)\n - creating a pull request via `gh pr create --base <chosen-base> --head <current-branch>`; if a PR with the same head already exists, surface its URL and skip creation\n - the lead writes the final report directly (no `Report writer worker` dispatch); the report still conforms to the standard final-report template",
|
|
3
|
+
"forbidden": [
|
|
4
|
+
"entering this phase when the cited final-verification `Verdict Token` is `conditional-accept` or `blocked`, or when no final-verification report is cited",
|
|
5
|
+
"local commit commands of any kind (`git add`, `git commit`, `git restore --staged`, `git stash`), and any direct `git merge` / `git rebase` / `git rebase --onto` / `git cherry-pick` / `git commit --amend` run by the lead. The single exception is the merge commits `okstra handoff pr-plan` itself creates on a `merge-base` branch — the lead never merges by hand. Rewriting a stage branch breaks the PR stack that sits on it.",
|
|
6
|
+
"any git push variant that rewrites remote history, regardless of intent or whether the user said \"force it\": `git push --force`, `git push --force-with-lease`, `git push -f`, `git push +<refspec>`, or any other history-rewriting invocation",
|
|
7
|
+
"pushing directly to a release base branch — i.e. `git push origin <branch>` where `<branch>` is `main`, `master`, `prod`, `preprod`, `staging`, `dev`, or the branch the user chose as the release base in this run. The only permitted push targets are the branches `okstra handoff pr-plan` listed for this run (each stage `head_branch`, and any `merge-base` branch).",
|
|
8
|
+
"bypassing repo safeguards: `--no-verify` / `-n` on `git push`, bypassing GPG signing, disabling safeguards via equivalent flags, or any hook bypass.",
|
|
9
|
+
"release-publishing commands: `gh release create`, `gh release edit`, `npm publish`, `cargo publish`, `pip publish`, `twine upload`, `docker push`, `terraform apply`, `kubectl apply` against any non-local cluster.",
|
|
10
|
+
"source-code edits, refactors, or any modification to files outside the run's own artifact directories (`reports/`, `prompts/`, `state/`, `manifests/`, `worker-results/`, `status/`, `sessions/`). The diff being shipped MUST be exactly what the prior `implementation` run produced; release-handoff packages it, it does not re-author it.",
|
|
11
|
+
"executing any mutating command the user did NOT select. Examples: opening a PR when the user picked `local checkout`; pushing when the user picked `skip`; switching the release base branch silently after the user already chose one; opening a PR for a stage outside `HANDOFF_STAGES`.",
|
|
12
|
+
"squash-merging, or instructing anyone to squash-merge, a stage PR — and `gh pr merge` in any form. A squash replaces the commits the next stage's PR base points at, so the stack breaks; the PR body states merge-commit-or-rebase and the lead never merges.",
|
|
13
|
+
"retrying a failed git / gh command with weaker safety flags. If `git push` fails with non-fast-forward, the lead MUST stop, explain the failure to the user, and ask for instructions — it MUST NOT add `--force`.",
|
|
14
|
+
"worker dispatch of any kind, or any other parallel sub-agent fan-out. This phase runs entirely under the Okstra lead.",
|
|
15
|
+
"silently treating an unrecognised user reply as one of the menu options. If the user's answer does not match a presented choice, re-ask the question verbatim."
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""검증 보고서를 인계 입력으로 고정하고 PR 템플릿을 해석한다."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from okstra_ctl.json_boundary import load_owned_object
|
|
8
|
+
from okstra_ctl.path_resolve import relative_to_project_root
|
|
9
|
+
from okstra_ctl.plan_run_root import plan_run_root_from_approved_plan
|
|
10
|
+
from okstra_ctl.prepare_error import PrepareError
|
|
11
|
+
from okstra_ctl.pr_template import PrTemplateError, resolve_pr_template_path
|
|
12
|
+
from okstra_ctl.stage_map import parse_stage_map_for_prepare
|
|
13
|
+
from okstra_ctl.phases.catalog import migrated_report_template
|
|
14
|
+
from okstra_ctl.handoff_error import HandoffError
|
|
15
|
+
from .operations import _require_eligible, compute_eligibility, _parse_stages_csv
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _collect_handoff_source_report_rows(
|
|
19
|
+
rows: list, nums: list,
|
|
20
|
+
) -> list:
|
|
21
|
+
"""선택 stage 들의 마지막 verified 행에서 인용 표 행을 만든다 (last-wins)."""
|
|
22
|
+
last_verified: dict = {}
|
|
23
|
+
for r in rows:
|
|
24
|
+
if r.get("status") == "verified" and isinstance(r.get("stage"), int):
|
|
25
|
+
last_verified[r["stage"]] = r
|
|
26
|
+
return [(str(n), Path(last_verified[n]["report_path"])) for n in nums]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _render_handoff_sources(sources: list, project_root: Path) -> tuple[str, str]:
|
|
30
|
+
"""인용 표와 미해결 조건은 같은 원본 판정에서 렌더한다."""
|
|
31
|
+
from okstra_ctl.release_gate import release_handoff_allowed, verdict_token
|
|
32
|
+
|
|
33
|
+
rows, conditions = [], []
|
|
34
|
+
for stage, report in sources:
|
|
35
|
+
data = load_owned_object(report, artifact="handoff verification report")
|
|
36
|
+
if not release_handoff_allowed(data):
|
|
37
|
+
raise PrepareError(f"verification no longer release-ready: {report}")
|
|
38
|
+
relative = relative_to_project_root(report, project_root) or str(report)
|
|
39
|
+
rows.append(f"| {stage} | {relative} | `{verdict_token(data)}` |")
|
|
40
|
+
if verdict_token(data) == "conditional-accept":
|
|
41
|
+
for condition in data["finalVerdict"]["conditionalAcceptanceConditions"]:
|
|
42
|
+
conditions.append(
|
|
43
|
+
f"- Stage {stage}, `{condition.get('id', '')}`\n"
|
|
44
|
+
f" - Condition: {condition.get('condition', '')}\n"
|
|
45
|
+
f" - Evidence required: {condition.get('evidenceRequired', '')}\n"
|
|
46
|
+
f" - blocksReleaseHandoff: {json.dumps(condition['blocksReleaseHandoff'])}"
|
|
47
|
+
)
|
|
48
|
+
section = "## Unresolved Conditional Acceptance Conditions\n\n" + "\n".join(conditions) if conditions else ""
|
|
49
|
+
return "\n".join(rows), section
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _select_handoff_stages(stage_map: list, rows: list, selection: str) -> list[int]:
|
|
53
|
+
if selection:
|
|
54
|
+
try:
|
|
55
|
+
nums = _parse_stages_csv(selection)
|
|
56
|
+
except HandoffError as exc:
|
|
57
|
+
raise PrepareError(str(exc)) from exc
|
|
58
|
+
try:
|
|
59
|
+
_require_eligible(stage_map, rows, nums)
|
|
60
|
+
except HandoffError as exc:
|
|
61
|
+
raise PrepareError(str(exc)) from exc
|
|
62
|
+
else:
|
|
63
|
+
eligibility = compute_eligibility(stage_map, rows)
|
|
64
|
+
nums = [e["stage"] for e in eligibility if e["eligible"]]
|
|
65
|
+
if not nums:
|
|
66
|
+
blocked = "; ".join(
|
|
67
|
+
f"stage {e['stage']}: {', '.join(e['reasons'])}"
|
|
68
|
+
for e in eligibility) or "(no stage in the Stage Map)"
|
|
69
|
+
raise PrepareError(
|
|
70
|
+
"release-handoff has no eligible stage — every stage must "
|
|
71
|
+
"carry a release-ready single-stage final-verification of its "
|
|
72
|
+
f"recorded implementation commit. {blocked}"
|
|
73
|
+
)
|
|
74
|
+
return nums
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def materialize_handoff_input(
|
|
78
|
+
workspace_root: Path, project_root: Path, inp, *, install_hint: str,
|
|
79
|
+
) -> dict:
|
|
80
|
+
"""release-handoff 의 입력 문서를 검증 보고서 인용으로 자동 생성한다.
|
|
81
|
+
|
|
82
|
+
brief 는 entry phase 의 입력물이라 release-handoff 에는 없다 — 이 run 의
|
|
83
|
+
`--task-brief` 자리는 여기서 생성된 input 문서가 채운다. stage 자격은
|
|
84
|
+
okstra_ctl.handoff 의 SSOT 판정을 prepare 에서도 그대로 강제한다.
|
|
85
|
+
반환: ctx 에 올릴 {"HANDOFF_STAGES": ...}."""
|
|
86
|
+
from okstra_ctl.consumers import read_consumers
|
|
87
|
+
from okstra_ctl.paths import task_dir
|
|
88
|
+
from okstra_ctl.render import render_template_with_ctx
|
|
89
|
+
from okstra_ctl.run_context import _now_task_date
|
|
90
|
+
|
|
91
|
+
# Path("") 는 "." 로 정규화된다 — 빈 brief 센티널은 둘 다로 들어올 수 있다.
|
|
92
|
+
if str(inp.brief_path).strip() not in ("", "."):
|
|
93
|
+
raise PrepareError(
|
|
94
|
+
"--task-brief is not accepted for --task-type release-handoff — "
|
|
95
|
+
"the input document is generated from the cited final-verification "
|
|
96
|
+
"reports (pick stages via --stages, or leave it empty to take "
|
|
97
|
+
"every eligible stage)"
|
|
98
|
+
)
|
|
99
|
+
if not inp.approved_plan_path:
|
|
100
|
+
raise PrepareError(
|
|
101
|
+
"release-handoff requires --approved-plan "
|
|
102
|
+
"<implementation-planning final-report> — the Stage Map and the "
|
|
103
|
+
"consumers ledger drive stage eligibility"
|
|
104
|
+
)
|
|
105
|
+
plan = Path(inp.approved_plan_path)
|
|
106
|
+
if not plan.is_file():
|
|
107
|
+
raise PrepareError(f"approved plan not found: {plan}")
|
|
108
|
+
stage_map = parse_stage_map_for_prepare(str(plan))
|
|
109
|
+
rows = read_consumers(plan_run_root_from_approved_plan(plan))
|
|
110
|
+
|
|
111
|
+
nums = _select_handoff_stages(stage_map, rows, inp.stages)
|
|
112
|
+
stages_csv = ",".join(str(n) for n in nums)
|
|
113
|
+
report_rows = _collect_handoff_source_report_rows(rows, nums)
|
|
114
|
+
|
|
115
|
+
source_rows, source_conditions = _render_handoff_sources(report_rows, project_root)
|
|
116
|
+
|
|
117
|
+
template = (migrated_report_template(workspace_root, "release-handoff-input.template.md")
|
|
118
|
+
or workspace_root / "templates" / "reports" / "release-handoff-input.template.md")
|
|
119
|
+
if not template.is_file():
|
|
120
|
+
raise PrepareError(
|
|
121
|
+
f"release-handoff input template missing: {template}.{install_hint}"
|
|
122
|
+
)
|
|
123
|
+
out = (task_dir(project_root, inp.task_group, inp.task_id)
|
|
124
|
+
/ "release-handoff-input.md")
|
|
125
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
126
|
+
render_template_with_ctx(str(template), str(out), {
|
|
127
|
+
"TASK_KEY": f"{inp.project_id}:{inp.task_group}:{inp.task_id}",
|
|
128
|
+
"TASK_ID": inp.task_id,
|
|
129
|
+
"TASK_GROUP": inp.task_group,
|
|
130
|
+
"PROJECT_ID": inp.project_id,
|
|
131
|
+
"TASK_TYPE": inp.task_type,
|
|
132
|
+
"TASK_DATE": _now_task_date(),
|
|
133
|
+
"HANDOFF_STAGES": stages_csv,
|
|
134
|
+
"HANDOFF_SOURCE_REPORTS": source_rows,
|
|
135
|
+
"HANDOFF_CONDITIONS": source_conditions,
|
|
136
|
+
})
|
|
137
|
+
inp.brief_path = out
|
|
138
|
+
return {"HANDOFF_STAGES": stages_csv}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def resolve_pr_template(inp) -> tuple[str, str]:
|
|
142
|
+
"""release-handoff 전용 PR 본문 템플릿 경로와 출처를 해소한다."""
|
|
143
|
+
try:
|
|
144
|
+
resolved_tpl = resolve_pr_template_path(Path(inp.project_root), inp.pr_template_path)
|
|
145
|
+
except PrTemplateError as exc:
|
|
146
|
+
raise PrepareError(f"PR template resolution failed: {exc}") from exc
|
|
147
|
+
return str(resolved_tpl.path), resolved_tpl.source
|