okstra 0.189.4 → 0.191.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.mjs +9 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/chat/chat.mjs +54 -4
- package/dist/commands/chat/chat.mjs.map +1 -1
- package/dist/commands/lifecycle/install.mjs +12 -5
- package/dist/commands/lifecycle/install.mjs.map +1 -1
- package/dist/commands/lifecycle/uninstall.mjs +2 -1
- package/dist/commands/lifecycle/uninstall.mjs.map +1 -1
- package/dist/lib/host-config.d.mts +5 -3
- package/dist/lib/host-config.mjs +89 -31
- package/dist/lib/host-config.mjs.map +1 -1
- package/docs/architecture/storage-model.md +4 -1
- package/docs/architecture.md +8 -4
- package/docs/cli.md +10 -8
- package/docs/for-ai/skills/okstra-brief-gen.md +1 -1
- package/docs/for-ai/skills/okstra-chat.md +3 -3
- package/docs/for-ai/skills/okstra-inspect.md +11 -1
- package/docs/for-ai/skills/okstra-rollup.md +1 -0
- package/docs/for-ai/skills/okstra-run.md +5 -4
- package/docs/for-ai/skills/okstra-user-response.md +1 -1
- package/docs/project-structure-overview.md +17 -5
- package/docs/task-process/README.md +1 -1
- package/docs/task-process/error-analysis.md +2 -2
- package/docs/task-process/final-verification.md +1 -1
- package/docs/task-process/implementation.md +1 -1
- package/docs/task-process/release-handoff.md +8 -6
- package/docs/task-process/requirements-discovery.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +4 -0
- package/runtime/bin/okstra-report-translate.py +10 -4
- package/runtime/prompts/launch.template.md +11 -5
- package/runtime/prompts/lead/adapters/cmux.md +4 -7
- package/runtime/prompts/lead/context-loader.md +2 -0
- package/runtime/prompts/lead/convergence.md +20 -83
- package/runtime/prompts/lead/okstra-lead-contract.md +24 -6
- package/runtime/prompts/lead/plan-body-verification.md +10 -3
- package/runtime/prompts/lead/report-writer.md +7 -4
- package/runtime/prompts/lead/team-contract.md +15 -9
- package/runtime/prompts/profiles/_common-contract.md +1 -0
- package/runtime/prompts/profiles/_implementation-deliverable.md +2 -0
- package/runtime/prompts/profiles/_implementation-diff-review.md +4 -0
- package/runtime/prompts/profiles/_implementation-executor.md +9 -5
- package/runtime/prompts/profiles/_implementation-self-check.md +2 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -4
- package/runtime/prompts/profiles/_stage-discipline.md +12 -5
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation.md +2 -0
- package/runtime/prompts/profiles/release-handoff.md +4 -3
- package/runtime/prompts/wizard/prompts.ko.json +2 -1
- package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +9 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +3 -3
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +31 -3
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +3 -3
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -2
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +13 -7
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +5 -2
- package/runtime/python/okstra_ctl/agent/invocation.py +107 -5
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +16 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/dynamic_verifier.py +3 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/jobs.py +87 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +72 -3
- package/runtime/python/okstra_ctl/analysis_packet.py +47 -6
- package/runtime/python/okstra_ctl/cmux.py +34 -13
- package/runtime/python/okstra_ctl/consumers.py +30 -7
- package/runtime/python/okstra_ctl/convergence.py +110 -21
- package/runtime/python/okstra_ctl/convergence_provenance.py +83 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +127 -5
- package/runtime/python/okstra_ctl/dispatch_state.py +44 -13
- package/runtime/python/okstra_ctl/domain/host.py +5 -0
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +1 -0
- package/runtime/python/okstra_ctl/error_log_write.py +44 -4
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +84 -7
- package/runtime/python/okstra_ctl/fixed_text.py +25 -7
- package/runtime/python/okstra_ctl/group_context.py +475 -10
- package/runtime/python/okstra_ctl/handoff.py +109 -32
- package/runtime/python/okstra_ctl/handoff_verification.py +80 -0
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +52 -10
- package/runtime/python/okstra_ctl/lead_progress.py +291 -0
- package/runtime/python/okstra_ctl/model_io/renderers.py +60 -1
- package/runtime/python/okstra_ctl/model_io_cli.py +6 -1
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +4 -0
- package/runtime/python/okstra_ctl/recap.py +179 -17
- package/runtime/python/okstra_ctl/reconcile.py +55 -4
- package/runtime/python/okstra_ctl/render.py +10 -1
- package/runtime/python/okstra_ctl/report_assembly.py +7 -6
- package/runtime/python/okstra_ctl/report_finalize.py +204 -11
- package/runtime/python/okstra_ctl/report_html/render.py +15 -4
- package/runtime/python/okstra_ctl/run.py +48 -14
- package/runtime/python/okstra_ctl/stage_targets.py +66 -0
- package/runtime/python/okstra_ctl/verdict_blocks.py +39 -12
- package/runtime/python/okstra_ctl/wizard/__init__.py +148 -0
- package/runtime/python/okstra_ctl/wizard/__main__.py +13 -0
- package/runtime/python/okstra_ctl/wizard/cli.py +176 -0
- package/runtime/python/okstra_ctl/wizard/confirmation.py +331 -0
- package/runtime/python/okstra_ctl/wizard/engine.py +384 -0
- package/runtime/python/okstra_ctl/wizard/ids.py +411 -0
- package/runtime/python/okstra_ctl/wizard/picker_navigation.py +75 -0
- package/runtime/python/okstra_ctl/wizard/prompts.py +202 -0
- package/runtime/python/okstra_ctl/wizard/registry.py +836 -0
- package/runtime/python/okstra_ctl/wizard/render.py +189 -0
- package/runtime/python/okstra_ctl/wizard/roles.py +737 -0
- package/runtime/python/okstra_ctl/wizard/sources.py +703 -0
- package/runtime/python/okstra_ctl/wizard/state.py +691 -0
- package/runtime/python/okstra_ctl/wizard/statefile.py +145 -0
- package/runtime/python/okstra_ctl/wizard/steps_analysis.py +558 -0
- package/runtime/python/okstra_ctl/wizard/steps_identity.py +828 -0
- package/runtime/python/okstra_ctl/wizard/steps_options.py +340 -0
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +959 -0
- package/runtime/python/okstra_ctl/wizard/steps_roles.py +672 -0
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +43 -3
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +14 -0
- package/runtime/python/okstra_ctl/workflow.py +2 -2
- package/runtime/python/okstra_ctl/worktree/__init__.py +2 -0
- package/runtime/python/okstra_ctl/worktree/cleanliness.py +7 -4
- package/runtime/python/okstra_ctl/worktree/git_ops.py +7 -0
- package/runtime/python/okstra_ctl/worktree/naming.py +15 -6
- package/runtime/python/okstra_ctl/worktree/provision.py +2 -1
- package/runtime/python/okstra_ctl/worktree_registry.py +27 -0
- package/runtime/python/okstra_ctl/wrapper_status.py +4 -0
- package/runtime/python/okstra_token_usage/codex.py +87 -2
- package/runtime/python/okstra_token_usage/collect.py +224 -49
- package/runtime/python/okstra_token_usage/pricing.py +8 -4
- package/runtime/skills/okstra-brief-gen/SKILL.md +22 -3
- package/runtime/skills/okstra-chat/SKILL.md +8 -6
- package/runtime/skills/okstra-inspect/SKILL.md +2 -2
- package/runtime/skills/okstra-inspect/facets/recap.md +52 -4
- package/runtime/skills/okstra-rollup/SKILL.md +1 -1
- package/runtime/skills/okstra-run/SKILL.md +23 -9
- package/runtime/skills/okstra-schedule-gen/SKILL.md +2 -0
- package/runtime/skills/okstra-user-response/SKILL.md +1 -1
- package/runtime/templates/reports/brief.template.md +15 -7
- package/runtime/templates/reports/final-verification-input.template.md +2 -0
- package/runtime/templates/reports/group-context.template.md +7 -0
- package/runtime/templates/reports/html/tasks/release-handoff.template.html +2 -2
- package/runtime/templates/reports/implementation-planning-input.template.md +3 -0
- package/runtime/templates/reports/improvement-discovery-input.template.md +3 -0
- package/runtime/templates/reports/release-handoff-input.template.md +5 -3
- package/runtime/templates/reports/schedule.template.md +13 -4
- package/runtime/templates/reports/task-brief.template.md +1 -1
- package/runtime/templates/reverify-output-contract.md +5 -0
- package/runtime/templates/worker-error-contract.md +2 -0
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-run.py +81 -22
- package/runtime/validators/validate_session_conformance.py +102 -2
- package/runtime/python/okstra_ctl/wizard.py +0 -7168
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""위저드 상태 파일 — `load_state_file`·`save_state_file` 과 v2 companion 파일 탐색."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from okstra_ctl.convergence_store import write_json_atomic
|
|
9
|
+
from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
|
|
10
|
+
|
|
11
|
+
from .state import WizardError, WizardState
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ---- File I/O helpers (used by CLI) -------------------------------------
|
|
15
|
+
|
|
16
|
+
_LEGACY_SOURCE_FIELD = "legacySource"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _legacy_v2_companion_path(path: Path) -> Path:
|
|
20
|
+
return path.with_name(f"{path.name}.v2.json")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _content_addressed_v2_companion_path(
|
|
24
|
+
path: Path,
|
|
25
|
+
source_digest: str,
|
|
26
|
+
suffix: int | None = None,
|
|
27
|
+
) -> Path:
|
|
28
|
+
numbered = f".{suffix}" if suffix is not None else ""
|
|
29
|
+
return path.with_name(f"{path.name}.v2.{source_digest}{numbered}.json")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _content_addressed_v2_companion_paths(
|
|
33
|
+
path: Path,
|
|
34
|
+
source_digest: str,
|
|
35
|
+
) -> tuple[Path, ...]:
|
|
36
|
+
canonical = _content_addressed_v2_companion_path(path, source_digest)
|
|
37
|
+
numbered_prefix = f"{path.name}.v2.{source_digest}."
|
|
38
|
+
numbered = tuple(sorted(
|
|
39
|
+
candidate
|
|
40
|
+
for candidate in path.parent.iterdir()
|
|
41
|
+
if candidate.is_file()
|
|
42
|
+
and candidate.name.startswith(numbered_prefix)
|
|
43
|
+
and candidate.name.endswith(".json")
|
|
44
|
+
))
|
|
45
|
+
return (canonical, *numbered)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _available_v2_companion_path(path: Path, source_digest: str) -> Path:
|
|
49
|
+
candidate = _content_addressed_v2_companion_path(path, source_digest)
|
|
50
|
+
suffix = 1
|
|
51
|
+
while candidate.exists():
|
|
52
|
+
candidate = _content_addressed_v2_companion_path(
|
|
53
|
+
path,
|
|
54
|
+
source_digest,
|
|
55
|
+
suffix,
|
|
56
|
+
)
|
|
57
|
+
suffix += 1
|
|
58
|
+
return candidate
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _legacy_source_metadata(path: Path, source: bytes) -> dict[str, str]:
|
|
62
|
+
return {
|
|
63
|
+
"path": str(path.resolve()),
|
|
64
|
+
"sha256": hashlib.sha256(source).hexdigest(),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _matching_v2_companion(
|
|
69
|
+
path: Path,
|
|
70
|
+
source_metadata: dict[str, str],
|
|
71
|
+
) -> dict[str, Any] | None:
|
|
72
|
+
try:
|
|
73
|
+
payload = load_owned_object(path, artifact="wizard state")
|
|
74
|
+
except (OSError, UnicodeError, JsonBoundaryError):
|
|
75
|
+
return None
|
|
76
|
+
if not isinstance(payload, dict):
|
|
77
|
+
return None
|
|
78
|
+
if payload.get("executionIdentityVersion") != 2:
|
|
79
|
+
return None
|
|
80
|
+
if payload.get(_LEGACY_SOURCE_FIELD) != source_metadata:
|
|
81
|
+
return None
|
|
82
|
+
return payload
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _load_matching_v2_state(
|
|
86
|
+
paths: tuple[Path, ...],
|
|
87
|
+
source_metadata: dict[str, str],
|
|
88
|
+
) -> tuple[WizardState | None, Path | None]:
|
|
89
|
+
for companion_path in paths:
|
|
90
|
+
payload = _matching_v2_companion(companion_path, source_metadata)
|
|
91
|
+
if payload is None:
|
|
92
|
+
continue
|
|
93
|
+
try:
|
|
94
|
+
return WizardState.from_json(payload), companion_path
|
|
95
|
+
except WizardError:
|
|
96
|
+
continue
|
|
97
|
+
return None, None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def load_state_file(path: Path) -> WizardState:
|
|
101
|
+
source_path = Path(path)
|
|
102
|
+
source = source_path.read_bytes()
|
|
103
|
+
data = load_owned_object(source_path, artifact="wizard state")
|
|
104
|
+
source_version = data.get("executionIdentityVersion", 1)
|
|
105
|
+
source_metadata = _legacy_source_metadata(source_path, source)
|
|
106
|
+
source_digest = source_metadata["sha256"]
|
|
107
|
+
resumed_state = None
|
|
108
|
+
companion_path = None
|
|
109
|
+
if source_version in (None, 1):
|
|
110
|
+
content_paths = _content_addressed_v2_companion_paths(
|
|
111
|
+
source_path,
|
|
112
|
+
source_digest,
|
|
113
|
+
)
|
|
114
|
+
resumed_state, companion_path = _load_matching_v2_state(
|
|
115
|
+
content_paths,
|
|
116
|
+
source_metadata,
|
|
117
|
+
)
|
|
118
|
+
if resumed_state is None:
|
|
119
|
+
resumed_state, _ = _load_matching_v2_state(
|
|
120
|
+
(_legacy_v2_companion_path(source_path),),
|
|
121
|
+
source_metadata,
|
|
122
|
+
)
|
|
123
|
+
state = resumed_state or WizardState.from_json(data)
|
|
124
|
+
state._source_execution_identity_version = source_version
|
|
125
|
+
if source_version in (None, 1):
|
|
126
|
+
state._legacy_source_metadata = source_metadata
|
|
127
|
+
state._v2_companion_path = companion_path or _available_v2_companion_path(
|
|
128
|
+
source_path,
|
|
129
|
+
source_digest,
|
|
130
|
+
)
|
|
131
|
+
return state
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _save_resumed_state_file(path: Path, state: WizardState) -> None:
|
|
135
|
+
"""Persist native v2 state while keeping legacy resume sources read-only."""
|
|
136
|
+
if getattr(state, "_source_execution_identity_version", 2) in (None, 1):
|
|
137
|
+
payload = state.to_json()
|
|
138
|
+
payload[_LEGACY_SOURCE_FIELD] = state._legacy_source_metadata
|
|
139
|
+
write_json_atomic(state._v2_companion_path, payload)
|
|
140
|
+
return
|
|
141
|
+
save_state_file(path, state)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def save_state_file(path: Path, state: WizardState) -> None:
|
|
145
|
+
write_json_atomic(Path(path), state.to_json())
|
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
"""analysis 단계 — analysis target, feature/project evidence, design prep 의 build/submit."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
from okstra_ctl.analysis_inputs import (
|
|
10
|
+
ANALYSIS_TASK_TYPES,
|
|
11
|
+
AnalysisInputError,
|
|
12
|
+
AnalysisReportCandidate,
|
|
13
|
+
list_evidence_candidates,
|
|
14
|
+
load_analysis_report_candidate,
|
|
15
|
+
resolve_analysis_target,
|
|
16
|
+
resolve_evidence_inputs,
|
|
17
|
+
)
|
|
18
|
+
from okstra_ctl.design_prep import (
|
|
19
|
+
DesignPrepError,
|
|
20
|
+
load_design_prep_items,
|
|
21
|
+
resolve_design_prep,
|
|
22
|
+
write_design_prep_input,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from .ids import (
|
|
26
|
+
PICK_SKIP,
|
|
27
|
+
PICK_TYPE_CUSTOM,
|
|
28
|
+
S_ANALYSIS_TARGET,
|
|
29
|
+
S_ANALYSIS_TARGET_PICK,
|
|
30
|
+
S_DESIGN_PREP_CONFIRM,
|
|
31
|
+
S_DESIGN_PREP_DECISION,
|
|
32
|
+
S_DESIGN_PREP_OVERRIDES,
|
|
33
|
+
S_FEATURE_EVIDENCE,
|
|
34
|
+
S_FEATURE_EVIDENCE_PICK,
|
|
35
|
+
S_PROJECT_EVIDENCE,
|
|
36
|
+
S_PROJECT_EVIDENCE_PICK,
|
|
37
|
+
_RESUME_REUSE_PHASES,
|
|
38
|
+
)
|
|
39
|
+
from .state import Prompt, WizardError, WizardState
|
|
40
|
+
from .prompts import _opt, _p, _static_options
|
|
41
|
+
from .sources import _analysis_current_commit, _has_prior_run_inputs, _resolve_path
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _analysis_evidence_paths(state: WizardState) -> list[Path]:
|
|
45
|
+
return [
|
|
46
|
+
Path(path)
|
|
47
|
+
for path in (state.feature_evidence_path, state.project_evidence_path)
|
|
48
|
+
if path
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _resolve_analysis_evidence(state: WizardState):
|
|
53
|
+
try:
|
|
54
|
+
return resolve_evidence_inputs(
|
|
55
|
+
Path(state.project_root),
|
|
56
|
+
state.task_type,
|
|
57
|
+
_analysis_evidence_paths(state),
|
|
58
|
+
_analysis_current_commit(state),
|
|
59
|
+
)
|
|
60
|
+
except AnalysisInputError as exc:
|
|
61
|
+
raise WizardError(str(exc)) from exc
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _accept_analysis_evidence(
|
|
65
|
+
state: WizardState, raw_path: str, field_name: str
|
|
66
|
+
) -> AnalysisReportCandidate:
|
|
67
|
+
report_path = _resolve_path(raw_path, Path(state.project_root))
|
|
68
|
+
try:
|
|
69
|
+
candidate = load_analysis_report_candidate(
|
|
70
|
+
Path(state.project_root), report_path
|
|
71
|
+
)
|
|
72
|
+
except AnalysisInputError as exc:
|
|
73
|
+
raise WizardError(str(exc)) from exc
|
|
74
|
+
previous = getattr(state, field_name)
|
|
75
|
+
setattr(state, field_name, str(candidate.report_path))
|
|
76
|
+
try:
|
|
77
|
+
_resolve_analysis_evidence(state)
|
|
78
|
+
except WizardError:
|
|
79
|
+
setattr(state, field_name, previous)
|
|
80
|
+
raise
|
|
81
|
+
return candidate
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _evidence_option_label(candidate: AnalysisReportCandidate) -> str:
|
|
85
|
+
return f"{candidate.task_key} · {candidate.task_type} · run {candidate.run_seq}"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _build_analysis_evidence_pick(
|
|
89
|
+
state: WizardState, *, step: str, relation: str
|
|
90
|
+
) -> Prompt:
|
|
91
|
+
t = _p(state.workspace_root, step)
|
|
92
|
+
candidates = list_evidence_candidates(
|
|
93
|
+
Path(state.project_root), state.task_type, relation
|
|
94
|
+
)[:2]
|
|
95
|
+
options = [
|
|
96
|
+
_opt(str(candidate.report_path), _evidence_option_label(candidate))
|
|
97
|
+
for candidate in candidates
|
|
98
|
+
]
|
|
99
|
+
options.extend([
|
|
100
|
+
_opt(PICK_SKIP, t["options"][PICK_SKIP]),
|
|
101
|
+
_opt(PICK_TYPE_CUSTOM, t["options"][PICK_TYPE_CUSTOM]),
|
|
102
|
+
])
|
|
103
|
+
return Prompt(step=step, kind="pick", label=t["label"], options=options,
|
|
104
|
+
echo_template=t["echo_template"])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _submit_analysis_evidence_pick(
|
|
108
|
+
state: WizardState,
|
|
109
|
+
value: str,
|
|
110
|
+
*,
|
|
111
|
+
step: str,
|
|
112
|
+
field_name: str,
|
|
113
|
+
pending_name: str,
|
|
114
|
+
target_after_skip: bool = False,
|
|
115
|
+
) -> Optional[str]:
|
|
116
|
+
t = _p(state.workspace_root, step)
|
|
117
|
+
if value == PICK_SKIP:
|
|
118
|
+
setattr(state, field_name, "")
|
|
119
|
+
setattr(state, pending_name, False)
|
|
120
|
+
if target_after_skip:
|
|
121
|
+
state.analysis_target_pending_text = True
|
|
122
|
+
return t["echo_variants"]["skip"]
|
|
123
|
+
if value == PICK_TYPE_CUSTOM:
|
|
124
|
+
setattr(state, pending_name, True)
|
|
125
|
+
return None
|
|
126
|
+
candidate = _accept_analysis_evidence(state, value, field_name)
|
|
127
|
+
setattr(state, pending_name, False)
|
|
128
|
+
return t["echo_variants"]["selected"].format(path=candidate.report_path)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _build_feature_evidence_pick(state: WizardState) -> Prompt:
|
|
132
|
+
return _build_analysis_evidence_pick(
|
|
133
|
+
state, step=S_FEATURE_EVIDENCE_PICK, relation="feature-baseline"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _submit_feature_evidence_pick(state: WizardState, value: str) -> Optional[str]:
|
|
138
|
+
return _submit_analysis_evidence_pick(
|
|
139
|
+
state, value, step=S_FEATURE_EVIDENCE_PICK,
|
|
140
|
+
field_name="feature_evidence_path",
|
|
141
|
+
pending_name="feature_evidence_pending_text",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _build_feature_evidence(state: WizardState) -> Prompt:
|
|
146
|
+
t = _p(state.workspace_root, S_FEATURE_EVIDENCE)
|
|
147
|
+
return Prompt(step=S_FEATURE_EVIDENCE, kind="text", label=t["label"],
|
|
148
|
+
echo_template=t["echo_template"])
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _submit_feature_evidence(state: WizardState, value: str) -> Optional[str]:
|
|
152
|
+
candidate = _accept_analysis_evidence(state, value, "feature_evidence_path")
|
|
153
|
+
state.feature_evidence_pending_text = False
|
|
154
|
+
return f"feature-evidence: {candidate.report_path}"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _build_project_evidence_pick(state: WizardState) -> Prompt:
|
|
158
|
+
return _build_analysis_evidence_pick(
|
|
159
|
+
state, step=S_PROJECT_EVIDENCE_PICK, relation="project-context"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _submit_project_evidence_pick(state: WizardState, value: str) -> Optional[str]:
|
|
164
|
+
return _submit_analysis_evidence_pick(
|
|
165
|
+
state, value, step=S_PROJECT_EVIDENCE_PICK,
|
|
166
|
+
field_name="project_evidence_path",
|
|
167
|
+
pending_name="project_evidence_pending_text",
|
|
168
|
+
target_after_skip=state.task_type == "feature-analysis",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _build_project_evidence(state: WizardState) -> Prompt:
|
|
173
|
+
t = _p(state.workspace_root, S_PROJECT_EVIDENCE)
|
|
174
|
+
return Prompt(step=S_PROJECT_EVIDENCE, kind="text", label=t["label"],
|
|
175
|
+
echo_template=t["echo_template"])
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _submit_project_evidence(state: WizardState, value: str) -> Optional[str]:
|
|
179
|
+
candidate = _accept_analysis_evidence(state, value, "project_evidence_path")
|
|
180
|
+
state.project_evidence_pending_text = False
|
|
181
|
+
return f"project-evidence: {candidate.report_path}"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _feature_description(feature: dict[object, object]) -> str:
|
|
185
|
+
name = str(feature.get("name") or "")
|
|
186
|
+
summary = str(feature.get("summary") or "")
|
|
187
|
+
entry_point = str(
|
|
188
|
+
feature.get("entryPoint") or feature.get("entrypoint") or ""
|
|
189
|
+
)
|
|
190
|
+
return " · ".join(value for value in (name, summary, entry_point) if value)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _build_analysis_target_pick(state: WizardState) -> Prompt:
|
|
194
|
+
t = _p(state.workspace_root, S_ANALYSIS_TARGET_PICK)
|
|
195
|
+
try:
|
|
196
|
+
candidate = load_analysis_report_candidate(
|
|
197
|
+
Path(state.project_root), Path(state.project_evidence_path)
|
|
198
|
+
)
|
|
199
|
+
except AnalysisInputError as exc:
|
|
200
|
+
raise WizardError(str(exc)) from exc
|
|
201
|
+
options = [
|
|
202
|
+
_opt(str(feature["id"]), str(feature["id"]), _feature_description(feature))
|
|
203
|
+
for feature in candidate.feature_index
|
|
204
|
+
if isinstance(feature, dict) and isinstance(feature.get("id"), str)
|
|
205
|
+
][:3]
|
|
206
|
+
options.append(_opt(PICK_TYPE_CUSTOM, t["options"][PICK_TYPE_CUSTOM]))
|
|
207
|
+
return Prompt(step=S_ANALYSIS_TARGET_PICK, kind="pick", label=t["label"],
|
|
208
|
+
options=options, echo_template=t["echo_template"])
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _accept_analysis_target(state: WizardState, value: str) -> str:
|
|
212
|
+
candidates: dict[Path, AnalysisReportCandidate] = {}
|
|
213
|
+
for path in _analysis_evidence_paths(state):
|
|
214
|
+
try:
|
|
215
|
+
candidate = load_analysis_report_candidate(Path(state.project_root), path)
|
|
216
|
+
except AnalysisInputError as exc:
|
|
217
|
+
raise WizardError(str(exc)) from exc
|
|
218
|
+
candidates[candidate.report_path] = candidate
|
|
219
|
+
try:
|
|
220
|
+
target = resolve_analysis_target(
|
|
221
|
+
value, _resolve_analysis_evidence(state), candidates
|
|
222
|
+
)
|
|
223
|
+
except AnalysisInputError as exc:
|
|
224
|
+
raise WizardError(str(exc)) from exc
|
|
225
|
+
requested_value = target["requestedValue"]
|
|
226
|
+
if not isinstance(requested_value, str):
|
|
227
|
+
raise WizardError("analysis target resolver returned an invalid requestedValue")
|
|
228
|
+
state.analysis_target = requested_value
|
|
229
|
+
state.analysis_target_pending_text = False
|
|
230
|
+
return requested_value
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _submit_analysis_target_pick(state: WizardState, value: str) -> Optional[str]:
|
|
234
|
+
if value == PICK_TYPE_CUSTOM:
|
|
235
|
+
state.analysis_target_pending_text = True
|
|
236
|
+
return None
|
|
237
|
+
return f"analysis-target: {_accept_analysis_target(state, value)}"
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _build_analysis_target(state: WizardState) -> Prompt:
|
|
241
|
+
t = _p(state.workspace_root, S_ANALYSIS_TARGET)
|
|
242
|
+
return Prompt(step=S_ANALYSIS_TARGET, kind="text", label=t["label"],
|
|
243
|
+
echo_template=t["echo_template"])
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _submit_analysis_target(state: WizardState, value: str) -> Optional[str]:
|
|
247
|
+
return f"analysis-target: {_accept_analysis_target(state, value)}"
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _design_prep_items_by_id(state: WizardState) -> dict[str, dict[str, Any]]:
|
|
251
|
+
try:
|
|
252
|
+
items = load_design_prep_items(Path(state.approved_plan_path))
|
|
253
|
+
except DesignPrepError as exc:
|
|
254
|
+
raise WizardError(str(exc)) from exc
|
|
255
|
+
return {str(item["id"]): item for item in items}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _selected_impl_stage_numbers(state: WizardState) -> set[int]:
|
|
259
|
+
numbers: set[int] = set()
|
|
260
|
+
for token in (state.selected_stages or state.selected_stage or "").split(","):
|
|
261
|
+
token = token.strip()
|
|
262
|
+
if token.isdigit():
|
|
263
|
+
numbers.add(int(token))
|
|
264
|
+
return numbers
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _blocking_unanswered_prep_ids(state: WizardState) -> list[str] | None:
|
|
268
|
+
"""고른 스테이지에서 구현을 막는 unanswered blocked id. 로드 불가면 None."""
|
|
269
|
+
report_path = Path(state.approved_plan_path)
|
|
270
|
+
if not re.fullmatch(
|
|
271
|
+
r"final-report-implementation-planning-\d+\.data\.json",
|
|
272
|
+
report_path.name,
|
|
273
|
+
):
|
|
274
|
+
return None
|
|
275
|
+
if not report_path.is_file():
|
|
276
|
+
return None
|
|
277
|
+
try:
|
|
278
|
+
items = load_design_prep_items(report_path)
|
|
279
|
+
effective = resolve_design_prep(report_path)
|
|
280
|
+
except DesignPrepError as exc:
|
|
281
|
+
raise WizardError(str(exc)) from exc
|
|
282
|
+
decisions = {
|
|
283
|
+
str(item["id"]): item.get("decision")
|
|
284
|
+
for item in effective.effective_items
|
|
285
|
+
}
|
|
286
|
+
stages = _selected_impl_stage_numbers(state)
|
|
287
|
+
# provisional 미응답은 구현을 막지 않는다. 작업 가정을 그대로 쓴다.
|
|
288
|
+
return [
|
|
289
|
+
str(item["id"])
|
|
290
|
+
for item in items
|
|
291
|
+
if item.get("status") == "blocked"
|
|
292
|
+
and decisions.get(str(item["id"])) == "unanswered"
|
|
293
|
+
and (
|
|
294
|
+
not stages
|
|
295
|
+
or stages.intersection(
|
|
296
|
+
stage for stage in (item.get("stageRefs") or [])
|
|
297
|
+
if isinstance(stage, int)
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
]
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _prune_design_prep_session(state: WizardState, wanted: list[str]) -> None:
|
|
304
|
+
"""이미 만든 큐에서 더 이상 막지 않는 id 를 뺀다. 새 id 는 넣지 않는다."""
|
|
305
|
+
wanted_set = set(wanted)
|
|
306
|
+
if state.design_prep_current and state.design_prep_current not in wanted_set:
|
|
307
|
+
state.design_prep_current = ""
|
|
308
|
+
state.design_prep_decision = ""
|
|
309
|
+
state.design_prep_overrides_json = ""
|
|
310
|
+
state.design_prep_notes = ""
|
|
311
|
+
state.design_prep_queue = [
|
|
312
|
+
item_id for item_id in state.design_prep_queue if item_id in wanted_set
|
|
313
|
+
]
|
|
314
|
+
if not state.design_prep_current:
|
|
315
|
+
_advance_design_prep_item(state)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _ensure_design_prep_queue(state: WizardState) -> None:
|
|
319
|
+
wanted = _blocking_unanswered_prep_ids(state)
|
|
320
|
+
if wanted is None:
|
|
321
|
+
return
|
|
322
|
+
if state.design_prep_current or state.design_prep_queue:
|
|
323
|
+
_prune_design_prep_session(state, wanted)
|
|
324
|
+
return
|
|
325
|
+
if S_DESIGN_PREP_DECISION in state.answered:
|
|
326
|
+
return
|
|
327
|
+
state.design_prep_queue = list(wanted)
|
|
328
|
+
_advance_design_prep_item(state)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _advance_design_prep_item(state: WizardState) -> None:
|
|
332
|
+
state.design_prep_current = (
|
|
333
|
+
state.design_prep_queue.pop(0) if state.design_prep_queue else ""
|
|
334
|
+
)
|
|
335
|
+
state.design_prep_decision = ""
|
|
336
|
+
state.design_prep_overrides_json = ""
|
|
337
|
+
state.design_prep_notes = ""
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _current_design_prep_item(state: WizardState) -> dict[str, Any]:
|
|
341
|
+
item = _design_prep_items_by_id(state).get(state.design_prep_current)
|
|
342
|
+
if item is None:
|
|
343
|
+
raise WizardError(
|
|
344
|
+
f"design preparation item is missing: {state.design_prep_current}"
|
|
345
|
+
)
|
|
346
|
+
return item
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _design_prep_decision_applies(state: WizardState) -> bool:
|
|
350
|
+
if state.task_type != "implementation" or not state.approved_plan_path:
|
|
351
|
+
return False
|
|
352
|
+
_ensure_design_prep_queue(state)
|
|
353
|
+
return bool(state.design_prep_current and not state.design_prep_decision)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _build_design_prep_decision(state: WizardState) -> Prompt:
|
|
357
|
+
item = _current_design_prep_item(state)
|
|
358
|
+
proposal = item.get("aiProposal") or {}
|
|
359
|
+
t = _p(
|
|
360
|
+
state.workspace_root,
|
|
361
|
+
S_DESIGN_PREP_DECISION,
|
|
362
|
+
item_id=str(item["id"]),
|
|
363
|
+
title=str(item["title"]),
|
|
364
|
+
stage_refs=json.dumps(item.get("stageRefs") or [], ensure_ascii=False),
|
|
365
|
+
proposal_summary=str(proposal.get("summary") or ""),
|
|
366
|
+
details=json.dumps(proposal.get("details") or [], ensure_ascii=False),
|
|
367
|
+
assumptions=json.dumps(proposal.get("assumptions") or [], ensure_ascii=False),
|
|
368
|
+
guardrails=json.dumps(item.get("guardrails") or [], ensure_ascii=False),
|
|
369
|
+
request_path=str(item.get("requestPath") or ""),
|
|
370
|
+
)
|
|
371
|
+
return Prompt(
|
|
372
|
+
step=S_DESIGN_PREP_DECISION,
|
|
373
|
+
kind="pick",
|
|
374
|
+
label=t["label"],
|
|
375
|
+
options=[_opt(value, label) for value, label in _static_options(t)],
|
|
376
|
+
echo_template=t["echo_template"],
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _submit_design_prep_decision(
|
|
381
|
+
state: WizardState,
|
|
382
|
+
value: str,
|
|
383
|
+
) -> Optional[str]:
|
|
384
|
+
t = _p(state.workspace_root, S_DESIGN_PREP_DECISION,
|
|
385
|
+
item_id="", title="", stage_refs="", proposal_summary="",
|
|
386
|
+
details="", assumptions="", guardrails="", request_path="")
|
|
387
|
+
if value not in ("accept-draft", "modify-draft", "reject-draft", "later"):
|
|
388
|
+
raise WizardError(t["errors"]["invalid_decision"])
|
|
389
|
+
if value == "later":
|
|
390
|
+
item_id = state.design_prep_current
|
|
391
|
+
_advance_design_prep_item(state)
|
|
392
|
+
return t["echo_variants"]["later"].format(item_id=item_id)
|
|
393
|
+
state.design_prep_decision = value
|
|
394
|
+
state.design_prep_overrides_json = ""
|
|
395
|
+
state.design_prep_notes = ""
|
|
396
|
+
return t["echo_template"].format(value=value)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _design_prep_overrides_applies(state: WizardState) -> bool:
|
|
400
|
+
if state.design_prep_decision == "modify-draft":
|
|
401
|
+
return not state.design_prep_overrides_json
|
|
402
|
+
if state.design_prep_decision == "reject-draft":
|
|
403
|
+
return not state.design_prep_notes
|
|
404
|
+
return False
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _build_design_prep_overrides(state: WizardState) -> Prompt:
|
|
408
|
+
t = _p(
|
|
409
|
+
state.workspace_root,
|
|
410
|
+
S_DESIGN_PREP_OVERRIDES,
|
|
411
|
+
item_id=state.design_prep_current,
|
|
412
|
+
decision=state.design_prep_decision,
|
|
413
|
+
)
|
|
414
|
+
return Prompt(
|
|
415
|
+
step=S_DESIGN_PREP_OVERRIDES,
|
|
416
|
+
kind="text",
|
|
417
|
+
label=t["label"],
|
|
418
|
+
echo_template=t["echo_template"],
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _submit_design_prep_overrides(
|
|
423
|
+
state: WizardState,
|
|
424
|
+
value: str,
|
|
425
|
+
) -> Optional[str]:
|
|
426
|
+
t = _p(state.workspace_root, S_DESIGN_PREP_OVERRIDES,
|
|
427
|
+
item_id=state.design_prep_current,
|
|
428
|
+
decision=state.design_prep_decision)
|
|
429
|
+
if state.design_prep_decision == "reject-draft":
|
|
430
|
+
if not value.strip():
|
|
431
|
+
raise WizardError(t["errors"]["note_required"])
|
|
432
|
+
state.design_prep_notes = value.strip()
|
|
433
|
+
return t["echo_variants"]["note"]
|
|
434
|
+
try:
|
|
435
|
+
overrides = json.loads(value)
|
|
436
|
+
except json.JSONDecodeError as exc:
|
|
437
|
+
raise WizardError(t["errors"]["invalid_json"].format(error=exc)) from exc
|
|
438
|
+
if not isinstance(overrides, dict):
|
|
439
|
+
raise WizardError(t["errors"]["object_required"])
|
|
440
|
+
state.design_prep_overrides_json = json.dumps(
|
|
441
|
+
overrides, ensure_ascii=False, sort_keys=True
|
|
442
|
+
)
|
|
443
|
+
return t["echo_variants"]["overrides"]
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _design_prep_confirm_applies(state: WizardState) -> bool:
|
|
447
|
+
if state.task_type == "implementation" and state.approved_plan_path:
|
|
448
|
+
_ensure_design_prep_queue(state)
|
|
449
|
+
if state.design_prep_decision == "accept-draft":
|
|
450
|
+
return True
|
|
451
|
+
if state.design_prep_decision == "modify-draft":
|
|
452
|
+
return bool(state.design_prep_overrides_json)
|
|
453
|
+
if state.design_prep_decision == "reject-draft":
|
|
454
|
+
return bool(state.design_prep_notes)
|
|
455
|
+
return False
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _build_design_prep_confirm(state: WizardState) -> Prompt:
|
|
459
|
+
item = _current_design_prep_item(state)
|
|
460
|
+
t = _p(
|
|
461
|
+
state.workspace_root,
|
|
462
|
+
S_DESIGN_PREP_CONFIRM,
|
|
463
|
+
item_id=state.design_prep_current,
|
|
464
|
+
decision=state.design_prep_decision,
|
|
465
|
+
item_snapshot=json.dumps(item, ensure_ascii=False, indent=2, sort_keys=True),
|
|
466
|
+
overrides=state.design_prep_overrides_json or "{}",
|
|
467
|
+
notes=state.design_prep_notes or "(none)",
|
|
468
|
+
)
|
|
469
|
+
return Prompt(
|
|
470
|
+
step=S_DESIGN_PREP_CONFIRM,
|
|
471
|
+
kind="pick",
|
|
472
|
+
label=t["label"],
|
|
473
|
+
options=[_opt(value, label) for value, label in _static_options(t)],
|
|
474
|
+
echo_template=t["echo_template"],
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _submit_design_prep_confirm(
|
|
479
|
+
state: WizardState,
|
|
480
|
+
value: str,
|
|
481
|
+
) -> Optional[str]:
|
|
482
|
+
item_id = state.design_prep_current
|
|
483
|
+
t = _p(state.workspace_root, S_DESIGN_PREP_CONFIRM,
|
|
484
|
+
item_id=item_id, decision=state.design_prep_decision,
|
|
485
|
+
item_snapshot="", overrides="", notes="")
|
|
486
|
+
if value == "no":
|
|
487
|
+
state.design_prep_decision = ""
|
|
488
|
+
state.design_prep_overrides_json = ""
|
|
489
|
+
state.design_prep_notes = ""
|
|
490
|
+
return t["echo_variants"]["revise"].format(item_id=item_id)
|
|
491
|
+
if value != "yes":
|
|
492
|
+
raise WizardError(t["errors"]["confirmation_required"])
|
|
493
|
+
overrides = json.loads(state.design_prep_overrides_json or "{}")
|
|
494
|
+
try:
|
|
495
|
+
path = write_design_prep_input(
|
|
496
|
+
Path(state.approved_plan_path),
|
|
497
|
+
item_id,
|
|
498
|
+
state.design_prep_decision,
|
|
499
|
+
overrides,
|
|
500
|
+
state.design_prep_notes,
|
|
501
|
+
captured_by="okstra-wizard",
|
|
502
|
+
)
|
|
503
|
+
except DesignPrepError as exc:
|
|
504
|
+
raise WizardError(str(exc)) from exc
|
|
505
|
+
match = re.search(r"-r(\d+)-([0-9a-f-]{36})\.md$", path.name)
|
|
506
|
+
if match is None:
|
|
507
|
+
raise WizardError(f"design preparation writer returned invalid path: {path}")
|
|
508
|
+
echo = t["echo_variants"]["written"].format(
|
|
509
|
+
item_id=item_id,
|
|
510
|
+
revision=int(match.group(1)),
|
|
511
|
+
input_id=match.group(2),
|
|
512
|
+
path=path,
|
|
513
|
+
)
|
|
514
|
+
_advance_design_prep_item(state)
|
|
515
|
+
return echo
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _analysis_inputs_after_reuse(state: WizardState) -> bool:
|
|
519
|
+
return (
|
|
520
|
+
state.reuse_previous is not None
|
|
521
|
+
or state.task_type not in _RESUME_REUSE_PHASES
|
|
522
|
+
or not _has_prior_run_inputs(state)
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _restore_analysis_evidence_inputs(
|
|
527
|
+
state: WizardState, evidence_raw: object
|
|
528
|
+
) -> tuple[tuple[Any, ...], dict[Path, AnalysisReportCandidate]]:
|
|
529
|
+
if state.task_type not in ANALYSIS_TASK_TYPES or not isinstance(evidence_raw, str):
|
|
530
|
+
return (), {}
|
|
531
|
+
report_paths = [
|
|
532
|
+
_resolve_path(item.strip(), Path(state.project_root))
|
|
533
|
+
for item in evidence_raw.split(",")
|
|
534
|
+
if item.strip()
|
|
535
|
+
]
|
|
536
|
+
try:
|
|
537
|
+
candidates = [
|
|
538
|
+
load_analysis_report_candidate(Path(state.project_root), path)
|
|
539
|
+
for path in report_paths
|
|
540
|
+
]
|
|
541
|
+
evidence_inputs = resolve_evidence_inputs(
|
|
542
|
+
Path(state.project_root),
|
|
543
|
+
state.task_type,
|
|
544
|
+
[candidate.report_path for candidate in candidates],
|
|
545
|
+
_analysis_current_commit(state),
|
|
546
|
+
)
|
|
547
|
+
except AnalysisInputError as exc:
|
|
548
|
+
raise WizardError(str(exc)) from exc
|
|
549
|
+
state.feature_evidence_path = ""
|
|
550
|
+
state.project_evidence_path = ""
|
|
551
|
+
for evidence in evidence_inputs:
|
|
552
|
+
if evidence.relation == "feature-baseline":
|
|
553
|
+
state.feature_evidence_path = str(evidence.report_path)
|
|
554
|
+
elif evidence.relation == "project-context":
|
|
555
|
+
state.project_evidence_path = str(evidence.report_path)
|
|
556
|
+
return evidence_inputs, {
|
|
557
|
+
candidate.report_path: candidate for candidate in candidates
|
|
558
|
+
}
|