okstra 0.146.1 → 0.148.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.
Files changed (137) hide show
  1. package/README.md +23 -9
  2. package/docs/architecture/storage-model.md +39 -65
  3. package/docs/architecture.md +68 -60
  4. package/docs/cli.md +40 -23
  5. package/docs/for-ai/skills/okstra-run.md +13 -34
  6. package/docs/performance-improvement-plan-v2.md +2 -2
  7. package/docs/pr-template-usage.md +1 -1
  8. package/docs/project-structure-overview.md +26 -22
  9. package/docs/task-process/README.md +4 -4
  10. package/docs/task-process/common-flow.md +12 -12
  11. package/docs/task-process/final-verification.md +2 -2
  12. package/docs/task-process/implementation.md +1 -1
  13. package/docs/task-process/release-handoff.md +1 -1
  14. package/package.json +2 -2
  15. package/runtime/BUILD.json +2 -2
  16. package/runtime/agents/workers/antigravity-worker.md +2 -2
  17. package/runtime/agents/workers/claude-worker.md +1 -1
  18. package/runtime/agents/workers/codex-worker.md +2 -2
  19. package/runtime/agents/workers/grok-worker.md +256 -0
  20. package/runtime/agents/workers/kimi-worker.md +256 -0
  21. package/runtime/agents/workers/report-writer-worker.md +12 -12
  22. package/runtime/bin/lib/okstra/cli.sh +13 -1
  23. package/runtime/bin/lib/okstra/globals.sh +3 -0
  24. package/runtime/bin/lib/okstra/usage.sh +17 -12
  25. package/runtime/bin/okstra-grok-exec.sh +5 -0
  26. package/runtime/bin/okstra-kimi-exec.sh +5 -0
  27. package/runtime/bin/okstra-provider-exec.py +235 -0
  28. package/runtime/bin/okstra-render-final-report.py +4 -4
  29. package/runtime/bin/okstra-render-report-views.py +100 -12
  30. package/runtime/bin/okstra.sh +3 -0
  31. package/runtime/prompts/lead/adapters/antigravity.md +48 -0
  32. package/runtime/prompts/lead/adapters/claude-code.md +13 -11
  33. package/runtime/prompts/lead/adapters/codex.md +7 -7
  34. package/runtime/prompts/lead/okstra-lead-contract.md +5 -5
  35. package/runtime/prompts/lead/report-writer.md +16 -12
  36. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  37. package/runtime/prompts/profiles/_common-contract.md +16 -10
  38. package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
  39. package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
  40. package/runtime/prompts/profiles/_implementation-executor.md +12 -12
  41. package/runtime/prompts/profiles/_implementation-self-check.md +4 -4
  42. package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
  43. package/runtime/prompts/profiles/change-impact-analysis.md +2 -0
  44. package/runtime/prompts/profiles/error-analysis.md +2 -0
  45. package/runtime/prompts/profiles/feature-analysis.md +2 -0
  46. package/runtime/prompts/profiles/final-verification.md +3 -1
  47. package/runtime/prompts/profiles/forbidden-actions.json +4 -4
  48. package/runtime/prompts/profiles/implementation-planning.md +3 -1
  49. package/runtime/prompts/profiles/implementation.md +2 -2
  50. package/runtime/prompts/profiles/improvement-discovery.md +6 -2
  51. package/runtime/prompts/profiles/project-analysis.md +2 -0
  52. package/runtime/prompts/profiles/release-handoff.md +7 -7
  53. package/runtime/prompts/profiles/requirements-discovery.md +2 -0
  54. package/runtime/prompts/wizard/prompts.ko.json +9 -1
  55. package/runtime/python/okstra_ctl/codex_dispatch.py +68 -87
  56. package/runtime/python/okstra_ctl/dispatch_core.py +4 -22
  57. package/runtime/python/okstra_ctl/final_report_schema.py +37 -12
  58. package/runtime/python/okstra_ctl/lead_events.py +1 -1
  59. package/runtime/python/okstra_ctl/lead_runtime.py +13 -2
  60. package/runtime/python/okstra_ctl/models.py +156 -8
  61. package/runtime/python/okstra_ctl/path_hints.py +9 -25
  62. package/runtime/python/okstra_ctl/paths.py +1 -1
  63. package/runtime/python/okstra_ctl/render.py +172 -74
  64. package/runtime/python/okstra_ctl/render_final_report.py +136 -28
  65. package/runtime/python/okstra_ctl/report_contract.py +124 -0
  66. package/runtime/python/okstra_ctl/report_finalize.py +1 -1
  67. package/runtime/python/okstra_ctl/report_html/__init__.py +10 -0
  68. package/runtime/python/okstra_ctl/report_html/common.py +86 -0
  69. package/runtime/python/okstra_ctl/report_html/filters.py +104 -0
  70. package/runtime/python/okstra_ctl/report_html/models.py +59 -0
  71. package/runtime/python/okstra_ctl/report_html/render.py +76 -0
  72. package/runtime/python/okstra_ctl/report_html/router.py +40 -0
  73. package/runtime/python/okstra_ctl/report_html/view_models/__init__.py +1 -0
  74. package/runtime/python/okstra_ctl/report_html/view_models/change_impact_analysis.py +39 -0
  75. package/runtime/python/okstra_ctl/report_html/view_models/error_analysis.py +49 -0
  76. package/runtime/python/okstra_ctl/report_html/view_models/feature_analysis.py +39 -0
  77. package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +47 -0
  78. package/runtime/python/okstra_ctl/report_html/view_models/implementation.py +47 -0
  79. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +103 -0
  80. package/runtime/python/okstra_ctl/report_html/view_models/improvement_discovery.py +43 -0
  81. package/runtime/python/okstra_ctl/report_html/view_models/project_analysis.py +54 -0
  82. package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +54 -0
  83. package/runtime/python/okstra_ctl/report_html/view_models/requirements_discovery.py +55 -0
  84. package/runtime/python/okstra_ctl/report_html/visualizations.py +139 -0
  85. package/runtime/python/okstra_ctl/report_view_artifacts.py +4 -1
  86. package/runtime/python/okstra_ctl/report_views.py +15 -43
  87. package/runtime/python/okstra_ctl/run.py +276 -51
  88. package/runtime/python/okstra_ctl/runner_resolution.py +103 -0
  89. package/runtime/python/okstra_ctl/schema_excerpt.py +7 -17
  90. package/runtime/python/okstra_ctl/team.py +2 -7
  91. package/runtime/python/okstra_ctl/wizard.py +194 -21
  92. package/runtime/python/okstra_ctl/worker_artifacts.py +46 -0
  93. package/runtime/python/okstra_ctl/workers.py +3 -1
  94. package/runtime/python/okstra_ctl/workflow.py +4 -2
  95. package/runtime/python/okstra_token_usage/__init__.py +1 -0
  96. package/runtime/python/okstra_token_usage/collect.py +32 -23
  97. package/runtime/python/okstra_token_usage/pricing.py +35 -3
  98. package/runtime/schemas/final-report-v2.0.schema.json +3923 -0
  99. package/runtime/skills/okstra-run/SKILL.md +31 -42
  100. package/runtime/templates/prd/pr-body.template.md +1 -1
  101. package/runtime/templates/reports/final-report-v2.template.md +66 -0
  102. package/runtime/templates/reports/html/assets/base.css +41 -0
  103. package/runtime/templates/reports/html/assets/base.js +5 -0
  104. package/runtime/templates/reports/html/base.template.html +79 -0
  105. package/runtime/templates/reports/html/macros/forms.html +47 -0
  106. package/runtime/templates/reports/html/macros/layout.html +19 -0
  107. package/runtime/templates/reports/html/macros/visualizations.html +27 -0
  108. package/runtime/templates/reports/html/tasks/change-impact-analysis.template.html +40 -0
  109. package/runtime/templates/reports/html/tasks/error-analysis.template.html +40 -0
  110. package/runtime/templates/reports/html/tasks/feature-analysis.template.html +40 -0
  111. package/runtime/templates/reports/html/tasks/final-verification.template.html +39 -0
  112. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +47 -0
  113. package/runtime/templates/reports/html/tasks/implementation.template.html +40 -0
  114. package/runtime/templates/reports/html/tasks/improvement-discovery.template.html +29 -0
  115. package/runtime/templates/reports/html/tasks/project-analysis.template.html +57 -0
  116. package/runtime/templates/reports/html/tasks/release-handoff.template.html +36 -0
  117. package/runtime/templates/reports/html/tasks/requirements-discovery.template.html +37 -0
  118. package/runtime/templates/reports/report.js +21 -4
  119. package/runtime/templates/reports/settings.template.json +4 -0
  120. package/runtime/templates/reports/task-brief.template.md +7 -7
  121. package/runtime/validators/validate-report-views.py +86 -4
  122. package/runtime/validators/validate-run.py +73 -15
  123. package/runtime/validators/validate_improvement_report.py +55 -0
  124. package/runtime/validators/validate_session_conformance.py +2 -1
  125. package/src/cli-registry.mjs +4 -4
  126. package/src/commands/execute/codex-dispatch.mjs +7 -10
  127. package/src/commands/execute/render-bundle.mjs +3 -3
  128. package/src/commands/execute/run.mjs +17 -52
  129. package/src/commands/execute/wizard.mjs +4 -1
  130. package/src/commands/lifecycle/doctor.mjs +6 -3
  131. package/src/commands/lifecycle/install.mjs +49 -21
  132. package/src/commands/report/finalize.mjs +2 -3
  133. package/src/commands/report/render-final-report.mjs +4 -2
  134. package/src/commands/report/render-views.mjs +8 -8
  135. package/src/lib/runtime-manifest.mjs +1 -1
  136. package/src/lib/runtime-resolver.mjs +2 -2
  137. package/src/lib/worker-agent-render.mjs +50 -0
@@ -0,0 +1,76 @@
1
+ """Render schema-v2 data directly into a task-specific HTML document."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import okstra_vendor # noqa: F401 # registers vendored dependency aliases
9
+ from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoescape
10
+
11
+ from ..final_report_schema import load_schema_for_data, validate
12
+ from .filters import audit_json, code_evidence, evidence_refs, inline_code, paragraphs
13
+ from .models import HtmlRunMeta
14
+ from .router import HtmlRenderError, resolve_html_route
15
+
16
+
17
+ def _sha256(path: Path) -> str:
18
+ return hashlib.sha256(path.read_bytes()).hexdigest()
19
+
20
+
21
+ def _templates_root(start: Path | None = None) -> Path:
22
+ if start is not None:
23
+ return start
24
+ here = Path(__file__).resolve()
25
+ for parent in [here, *here.parents]:
26
+ candidate = parent / "templates" / "reports"
27
+ if candidate.is_dir():
28
+ return candidate
29
+ raise HtmlRenderError("could not locate templates/reports")
30
+
31
+
32
+ def _html_path(data_path: Path) -> Path:
33
+ suffix = ".data.json"
34
+ if not data_path.name.endswith(suffix):
35
+ raise HtmlRenderError(f"v2 report path must end with {suffix}: {data_path}")
36
+ return data_path.with_name(data_path.name.removesuffix(suffix) + ".html")
37
+
38
+
39
+ def render_v2_html_view(
40
+ data_path: Path,
41
+ markdown_path: Path,
42
+ *,
43
+ run_meta: HtmlRunMeta,
44
+ templates_root: Path | None = None,
45
+ ) -> Path:
46
+ data = json.loads(data_path.read_text(encoding="utf-8"))
47
+ errors = validate(data, load_schema_for_data(data))
48
+ if errors:
49
+ raise HtmlRenderError("invalid v2 final-report data: " + "; ".join(errors[:5]))
50
+ if not markdown_path.is_file():
51
+ raise HtmlRenderError(f"v2 markdown sibling not found: {markdown_path}")
52
+ route = resolve_html_route(run_meta.task_type)
53
+ view = route.view_builder(data)
54
+ root = _templates_root(templates_root)
55
+ env = Environment(loader=FileSystemLoader(str(root)), autoescape=select_autoescape(("html",)), undefined=StrictUndefined)
56
+ env.policies["json.dumps_kwargs"] = {"sort_keys": True, "ensure_ascii": False}
57
+ env.filters["audit_json"] = audit_json
58
+ env.filters["code_evidence"] = code_evidence
59
+ env.filters["evidence_refs"] = evidence_refs
60
+ env.filters["inline_code"] = inline_code
61
+ env.filters["paragraphs"] = paragraphs
62
+ response_js = (root / "report.js").read_text(encoding="utf-8")
63
+ base_js = (root / "html/assets/base.js").read_text(encoding="utf-8")
64
+ context = {
65
+ **view.context,
66
+ "runMeta": run_meta,
67
+ "taskType": view.task_type,
68
+ "dataSha256": _sha256(data_path),
69
+ "markdownSha256": _sha256(markdown_path),
70
+ "clarificationItems": data.get("clarificationItems", []),
71
+ "css": (root / "html/assets/base.css").read_text(encoding="utf-8"),
72
+ "js": response_js + "\n" + base_js,
73
+ }
74
+ output_path = _html_path(data_path)
75
+ output_path.write_text(env.get_template(route.template_name).render(**context), encoding="utf-8")
76
+ return output_path
@@ -0,0 +1,40 @@
1
+ """Fail-closed task-type routing for schema-v2 HTML views."""
2
+ from __future__ import annotations
3
+
4
+ from ..report_contract import html_template_for
5
+ from .models import HtmlRoute
6
+ from .view_models.change_impact_analysis import build_change_impact_analysis_view
7
+ from .view_models.error_analysis import build_error_analysis_view
8
+ from .view_models.feature_analysis import build_feature_analysis_view
9
+ from .view_models.final_verification import build_final_verification_view
10
+ from .view_models.improvement_discovery import build_improvement_discovery_view
11
+ from .view_models.implementation import build_implementation_view
12
+ from .view_models.implementation_planning import build_implementation_planning_view
13
+ from .view_models.project_analysis import build_project_analysis_view
14
+ from .view_models.release_handoff import build_release_handoff_view
15
+ from .view_models.requirements_discovery import build_requirements_discovery_view
16
+
17
+
18
+ class HtmlRenderError(ValueError):
19
+ """Raised when a v2 task has no dedicated human view."""
20
+
21
+
22
+ VIEW_BUILDERS = {
23
+ "requirements-discovery": build_requirements_discovery_view,
24
+ "improvement-discovery": build_improvement_discovery_view,
25
+ "error-analysis": build_error_analysis_view,
26
+ "project-analysis": build_project_analysis_view,
27
+ "feature-analysis": build_feature_analysis_view,
28
+ "change-impact-analysis": build_change_impact_analysis_view,
29
+ "implementation-planning": build_implementation_planning_view,
30
+ "implementation": build_implementation_view,
31
+ "final-verification": build_final_verification_view,
32
+ "release-handoff": build_release_handoff_view,
33
+ }
34
+
35
+
36
+ def resolve_html_route(task_type: str) -> HtmlRoute:
37
+ builder = VIEW_BUILDERS.get(task_type)
38
+ if builder is None:
39
+ raise HtmlRenderError(f"no v2 html view builder for {task_type}")
40
+ return HtmlRoute(template_name=html_template_for(task_type), view_builder=builder)
@@ -0,0 +1 @@
1
+ """Task-specific human report view builders."""
@@ -0,0 +1,39 @@
1
+ """Human-first change-impact-analysis view model."""
2
+ from __future__ import annotations
3
+
4
+ from ..common import analysis_review_ids, audit_context, evidence_index
5
+ from ..models import HumanReportView, VisualEdge, VisualNode
6
+ from ..visualizations import flow_figure
7
+
8
+
9
+ def _impact_figure(change: dict):
10
+ nodes = [
11
+ VisualNode(row["id"], row["target"], "affected", row["level"], row["impactKind"])
12
+ for row in change.get("impactItems", [])
13
+ ]
14
+ edges: list[VisualEdge] = []
15
+ for index, row in enumerate(change.get("dependencyBlastRadius", []), start=1):
16
+ target_id = f"BR-{index:03d}"
17
+ nodes.append(VisualNode(target_id, row["affectedTarget"], "downstream", "risk", row["direction"]))
18
+ edges.append(VisualEdge(row["sourceImpactId"], target_id, row["direction"], "impact"))
19
+ return flow_figure(nodes=tuple(nodes), edges=tuple(edges), title="Change blast radius")
20
+
21
+
22
+ def build_change_impact_analysis_view(data: dict) -> HumanReportView:
23
+ change = data["changeImpactAnalysis"]
24
+ figure = _impact_figure(change)
25
+ context = {
26
+ "humanSummary": data["humanSummary"],
27
+ "change": change,
28
+ "narrative": change["userNarrative"],
29
+ "blastRadiusFigure": figure,
30
+ "analysisReviewIds": analysis_review_ids(data),
31
+ "evidenceIndex": evidence_index(data),
32
+ "audit": audit_context(data),
33
+ }
34
+ return HumanReportView(
35
+ task_type="change-impact-analysis",
36
+ template_name="html/tasks/change-impact-analysis.template.html",
37
+ context=context,
38
+ figures=(figure,),
39
+ )
@@ -0,0 +1,49 @@
1
+ """Human-first error-analysis view model."""
2
+ from __future__ import annotations
3
+
4
+ from ..common import audit_context, evidence_index
5
+ from ..models import HumanReportView, VisualEdge, VisualNode
6
+ from ..visualizations import cause_graph_figure
7
+
8
+
9
+ def _cause_figure(error: dict):
10
+ symptom = VisualNode(
11
+ "symptom", "Observed failure", "effect", "risk", error["observableFailure"]
12
+ )
13
+ causes = tuple(
14
+ VisualNode(
15
+ row["id"],
16
+ row["statement"],
17
+ "candidate",
18
+ row["confidence"],
19
+ row["disproveWith"],
20
+ )
21
+ for row in error.get("causeCandidates", [])
22
+ )
23
+ edges = tuple(
24
+ VisualEdge(row.id, symptom.id, "may cause", "hypothesis") for row in causes
25
+ )
26
+ return cause_graph_figure(
27
+ nodes=(symptom, *causes),
28
+ edges=edges,
29
+ title="Cause hypotheses and observed symptom",
30
+ )
31
+
32
+
33
+ def build_error_analysis_view(data: dict) -> HumanReportView:
34
+ error = data["errorAnalysis"]
35
+ figure = _cause_figure(error)
36
+ context = {
37
+ "humanSummary": data["humanSummary"],
38
+ "error": error,
39
+ "narrative": error["userNarrative"],
40
+ "causeFigure": figure,
41
+ "evidenceIndex": evidence_index(data),
42
+ "audit": audit_context(data),
43
+ }
44
+ return HumanReportView(
45
+ "error-analysis",
46
+ "html/tasks/error-analysis.template.html",
47
+ context,
48
+ (figure,),
49
+ )
@@ -0,0 +1,39 @@
1
+ """Human-first feature-analysis view model."""
2
+ from __future__ import annotations
3
+
4
+ from ..common import analysis_review_ids, audit_context, evidence_index
5
+ from ..models import HumanReportView, VisualNode
6
+ from ..visualizations import flow_figure
7
+
8
+
9
+ def _flow_nodes(feature: dict) -> tuple[VisualNode, ...]:
10
+ return tuple(
11
+ VisualNode(
12
+ id=row["id"],
13
+ label=row["kind"].title(),
14
+ group=row["kind"],
15
+ status="risk" if row["kind"] == "failure" else "stable",
16
+ detail=" → ".join(step["action"] for step in row.get("steps", [])),
17
+ )
18
+ for row in feature.get("flows", [])
19
+ )
20
+
21
+
22
+ def build_feature_analysis_view(data: dict) -> HumanReportView:
23
+ feature = data["featureAnalysis"]
24
+ figure = flow_figure(nodes=_flow_nodes(feature), edges=(), title="Feature behavior paths")
25
+ context = {
26
+ "humanSummary": data["humanSummary"],
27
+ "feature": feature,
28
+ "narrative": feature["userNarrative"],
29
+ "flowFigure": figure,
30
+ "analysisReviewIds": analysis_review_ids(data),
31
+ "evidenceIndex": evidence_index(data),
32
+ "audit": audit_context(data),
33
+ }
34
+ return HumanReportView(
35
+ task_type="feature-analysis",
36
+ template_name="html/tasks/feature-analysis.template.html",
37
+ context=context,
38
+ figures=(figure,),
39
+ )
@@ -0,0 +1,47 @@
1
+ """Human-first final-verification view model."""
2
+ from __future__ import annotations
3
+
4
+ from ..common import audit_context, evidence_index
5
+ from ..models import HumanReportView, VisualNode
6
+ from ..visualizations import coverage_figure
7
+
8
+
9
+ def _coverage_nodes(final: dict) -> tuple[VisualNode, ...]:
10
+ return tuple(
11
+ VisualNode(
12
+ row["id"],
13
+ row["requirement"],
14
+ "requirement",
15
+ row["status"],
16
+ row["artifact"],
17
+ )
18
+ for row in final["validationEvidence"]
19
+ )
20
+
21
+
22
+ def build_final_verification_view(data: dict) -> HumanReportView:
23
+ final = data["finalVerification"]
24
+ figure = coverage_figure(
25
+ rows=_coverage_nodes(final), title="Requirement verification coverage"
26
+ )
27
+ audit = audit_context(data)
28
+ audit["deliveryEvidence"] = {
29
+ "sourceImplementation": final["sourceImplementationReport"],
30
+ "readonlyCommandLog": final["readonlyCommandLog"],
31
+ }
32
+ context = {
33
+ "humanSummary": data["humanSummary"],
34
+ "verdict": data["verdictCard"],
35
+ "final": final,
36
+ "narrative": final["userNarrative"],
37
+ "coverageFigure": figure,
38
+ "releaseAllowed": data["verdictCard"]["verdictToken"] == "accepted",
39
+ "evidenceIndex": evidence_index(data),
40
+ "audit": audit,
41
+ }
42
+ return HumanReportView(
43
+ "final-verification",
44
+ "html/tasks/final-verification.template.html",
45
+ context,
46
+ (figure,),
47
+ )
@@ -0,0 +1,47 @@
1
+ """Human-first implementation delivery view model."""
2
+ from __future__ import annotations
3
+
4
+ from ..common import audit_context, evidence_index
5
+ from ..models import HumanReportView, VisualNode
6
+ from ..visualizations import change_map_figure
7
+
8
+
9
+ def _change_nodes(implementation: dict) -> tuple[VisualNode, ...]:
10
+ return tuple(
11
+ VisualNode(
12
+ row["file"],
13
+ row["file"],
14
+ row["planStep"],
15
+ row["action"],
16
+ row["lines"],
17
+ )
18
+ for row in implementation["diffSummary"]["files"]
19
+ )
20
+
21
+
22
+ def build_implementation_view(data: dict) -> HumanReportView:
23
+ implementation = data["implementation"]
24
+ figure = change_map_figure(
25
+ nodes=_change_nodes(implementation), title="Delivered change areas"
26
+ )
27
+ audit = audit_context(data)
28
+ audit["deliveryEvidence"] = {
29
+ "commits": implementation["commitList"],
30
+ "rawDiffStat": implementation["diffSummary"]["rawStat"],
31
+ "verifiers": implementation["verifierResults"],
32
+ "validationCommands": implementation["validationEvidence"],
33
+ }
34
+ context = {
35
+ "humanSummary": data["humanSummary"],
36
+ "implementation": implementation,
37
+ "narrative": implementation["userNarrative"],
38
+ "changeFigure": figure,
39
+ "evidenceIndex": evidence_index(data),
40
+ "audit": audit,
41
+ }
42
+ return HumanReportView(
43
+ "implementation",
44
+ "html/tasks/implementation.template.html",
45
+ context,
46
+ (figure,),
47
+ )
@@ -0,0 +1,103 @@
1
+ """Human-first implementation-planning view and approval state."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from dataclasses import dataclass
6
+
7
+ from ..common import audit_context, evidence_index
8
+ from ..models import HumanReportView, VisualEdge, VisualNode
9
+ from ..visualizations import stage_map_figure
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class PlanApprovalState:
14
+ option_names: tuple[str, ...]
15
+ recommended_option: str
16
+ disabled_reason: str
17
+ blocker_ids: tuple[str, ...]
18
+
19
+
20
+ def resolve_recommended_option(rec_name: str, names: tuple[str, ...]) -> str:
21
+ if rec_name in names:
22
+ return rec_name
23
+ matches = [
24
+ name
25
+ for name in names
26
+ if rec_name and (name.startswith(rec_name) or rec_name.startswith(name))
27
+ ]
28
+ return matches[0] if len(matches) == 1 else names[0]
29
+
30
+
31
+ def plan_approval_state(data: dict) -> PlanApprovalState | None:
32
+ planning = data.get("implementationPlanning")
33
+ if not isinstance(planning, dict):
34
+ return None
35
+ names = tuple(
36
+ row["name"]
37
+ for row in planning.get("optionCandidates", [])
38
+ if isinstance(row, dict) and row.get("name")
39
+ )
40
+ if not names:
41
+ return None
42
+ recommended = planning.get("recommendedOption", {}).get("name", "")
43
+ blockers = tuple(
44
+ row["id"]
45
+ for row in data.get("clarificationItems", [])
46
+ if row.get("blocks") == "approval"
47
+ and row.get("status") in {"open", "answered"}
48
+ )
49
+ reason = f"승인 차단 항목 {len(blockers)}건 미해소" if blockers else ""
50
+ return PlanApprovalState(
51
+ names,
52
+ resolve_recommended_option(recommended, names),
53
+ reason,
54
+ blockers,
55
+ )
56
+
57
+
58
+ def _stage_figure(planning: dict):
59
+ nodes = tuple(
60
+ VisualNode(
61
+ f"stage-{row['stage']}",
62
+ f"Stage {row['stage']} · {row['title']}",
63
+ "stage",
64
+ "planned",
65
+ row["exitContractSummary"],
66
+ )
67
+ for row in planning["stageMap"]
68
+ )
69
+ edges = tuple(
70
+ VisualEdge(f"stage-{dependency}", f"stage-{row['stage']}", "precedes", "dependency")
71
+ for row in planning["stageMap"]
72
+ for dependency in re.findall(r"\d+", row["dependsOn"])
73
+ )
74
+ return stage_map_figure(nodes=nodes, edges=edges, title="Implementation stage dependencies")
75
+
76
+
77
+ def build_implementation_planning_view(data: dict) -> HumanReportView:
78
+ planning = data["implementationPlanning"]
79
+ figure = _stage_figure(planning)
80
+ approval = plan_approval_state(data)
81
+ audit = audit_context(data)
82
+ audit["planBodyVerification"] = planning["planBodyVerification"]
83
+ context = {
84
+ "humanSummary": data["humanSummary"],
85
+ "planning": planning,
86
+ "narrative": planning["userNarrative"],
87
+ "stageFigure": figure,
88
+ "approval": approval,
89
+ "openDecisions": [
90
+ row
91
+ for row in data.get("clarificationItems", [])
92
+ if row.get("blocks") == "approval"
93
+ and row.get("status") in {"open", "answered"}
94
+ ],
95
+ "evidenceIndex": evidence_index(data),
96
+ "audit": audit,
97
+ }
98
+ return HumanReportView(
99
+ "implementation-planning",
100
+ "html/tasks/implementation-planning.template.html",
101
+ context,
102
+ (figure,),
103
+ )
@@ -0,0 +1,43 @@
1
+ """Human-first improvement-discovery view model."""
2
+ from __future__ import annotations
3
+
4
+ from ..common import audit_context, evidence_index
5
+ from ..models import HumanReportView, VisualNode
6
+ from ..visualizations import matrix_figure
7
+
8
+
9
+ def _candidate_nodes(improvement: dict) -> tuple[VisualNode, ...]:
10
+ return tuple(
11
+ VisualNode(
12
+ row["id"],
13
+ row["title"],
14
+ f"effort-{row['effort']}",
15
+ row["severity"],
16
+ row["expectedBehaviorAfter"],
17
+ )
18
+ for row in improvement.get("candidates", [])
19
+ )
20
+
21
+
22
+ def build_improvement_discovery_view(data: dict) -> HumanReportView:
23
+ improvement = data["improvementDiscovery"]
24
+ figure = matrix_figure(
25
+ items=_candidate_nodes(improvement),
26
+ title="Candidate impact and effort",
27
+ x_label="effort",
28
+ y_label="severity",
29
+ )
30
+ context = {
31
+ "humanSummary": data["humanSummary"],
32
+ "improvement": improvement,
33
+ "narrative": improvement["userNarrative"],
34
+ "matrixFigure": figure,
35
+ "evidenceIndex": evidence_index(data),
36
+ "audit": audit_context(data),
37
+ }
38
+ return HumanReportView(
39
+ "improvement-discovery",
40
+ "html/tasks/improvement-discovery.template.html",
41
+ context,
42
+ (figure,),
43
+ )
@@ -0,0 +1,54 @@
1
+ """Human-first project-analysis view model."""
2
+ from __future__ import annotations
3
+
4
+ from ..common import audit_context, evidence_index
5
+ from ..models import HumanReportView, VisualEdge, VisualNode
6
+ from ..visualizations import dependency_figure
7
+
8
+
9
+ def _component_nodes(project: dict) -> tuple[VisualNode, ...]:
10
+ return tuple(
11
+ VisualNode(
12
+ id=row["id"],
13
+ label=row["name"],
14
+ group=(row.get("paths") or ["project"])[0].split("/", 1)[0],
15
+ status="stable",
16
+ detail=row["responsibility"],
17
+ )
18
+ for row in project.get("components", [])
19
+ )
20
+
21
+
22
+ def _dependency_edges(project: dict) -> tuple[VisualEdge, ...]:
23
+ return tuple(
24
+ VisualEdge(
25
+ source=row["fromComponentId"],
26
+ target=row["toComponentId"],
27
+ label=row.get("direction", "dependency"),
28
+ kind="dependency",
29
+ )
30
+ for row in project.get("dependencies", [])
31
+ )
32
+
33
+
34
+ def build_project_analysis_view(data: dict) -> HumanReportView:
35
+ project = data["projectAnalysis"]
36
+ architecture = dependency_figure(
37
+ nodes=_component_nodes(project),
38
+ edges=_dependency_edges(project),
39
+ title="Project architecture and dependencies",
40
+ )
41
+ context = {
42
+ "humanSummary": data["humanSummary"],
43
+ "project": project,
44
+ "narrative": project["userNarrative"],
45
+ "architectureFigure": architecture,
46
+ "evidenceIndex": evidence_index(data),
47
+ "audit": audit_context(data),
48
+ }
49
+ return HumanReportView(
50
+ task_type="project-analysis",
51
+ template_name="html/tasks/project-analysis.template.html",
52
+ context=context,
53
+ figures=(architecture,),
54
+ )
@@ -0,0 +1,54 @@
1
+ """Human-first release-handoff view model."""
2
+ from __future__ import annotations
3
+
4
+ from urllib.parse import urlparse
5
+
6
+ from ..common import audit_context, evidence_index
7
+ from ..models import HumanReportView, VisualNode
8
+ from ..visualizations import timeline_figure
9
+
10
+
11
+ def _safe_http_url(value: str) -> str:
12
+ parsed = urlparse(value)
13
+ return value if parsed.scheme in {"http", "https"} and parsed.netloc else ""
14
+
15
+
16
+ def _timeline_nodes(handoff: dict) -> tuple[VisualNode, ...]:
17
+ selection = handoff["userSelections"]["h1"]
18
+ branch = handoff["featureBranchState"]["branchName"]
19
+ conflict = handoff["mergeConflictProbe"]["kind"]
20
+ pr = handoff["pullRequestOutcome"]["kind"]
21
+ return (
22
+ VisualNode("selection", selection, "handoff", "completed", "User-selected handoff"),
23
+ VisualNode("branch", branch, "handoff", "completed", "Feature branch state"),
24
+ VisualNode("conflict", conflict, "handoff", conflict, "Merge conflict probe"),
25
+ VisualNode("pull-request", pr, "handoff", pr, "Pull request outcome"),
26
+ )
27
+
28
+
29
+ def build_release_handoff_view(data: dict) -> HumanReportView:
30
+ handoff = data["releaseHandoff"]
31
+ figure = timeline_figure(
32
+ events=_timeline_nodes(handoff), title="Release handoff timeline"
33
+ )
34
+ audit = audit_context(data)
35
+ audit["deliveryEvidence"] = {
36
+ "executedCommands": handoff["executedCommands"],
37
+ "commits": handoff["commitList"],
38
+ "sourceVerification": handoff["sourceVerificationReport"],
39
+ }
40
+ context = {
41
+ "humanSummary": data["humanSummary"],
42
+ "handoff": handoff,
43
+ "narrative": handoff["userNarrative"],
44
+ "handoffFigure": figure,
45
+ "prUrl": _safe_http_url(handoff["pullRequestOutcome"].get("url", "")),
46
+ "evidenceIndex": evidence_index(data),
47
+ "audit": audit,
48
+ }
49
+ return HumanReportView(
50
+ "release-handoff",
51
+ "html/tasks/release-handoff.template.html",
52
+ context,
53
+ (figure,),
54
+ )
@@ -0,0 +1,55 @@
1
+ """Human-first requirements-discovery view model."""
2
+ from __future__ import annotations
3
+
4
+ from ..common import audit_context, evidence_index
5
+ from ..models import HumanReportView, VisualEdge, VisualNode
6
+ from ..visualizations import decision_flow_figure
7
+
8
+
9
+ def _routing_figure(data: dict, requirements: dict):
10
+ current = data["header"]["taskKey"]
11
+ nodes = {
12
+ current: VisualNode(
13
+ current,
14
+ "Current requirements task",
15
+ "current",
16
+ "active",
17
+ requirements["systemInterpretation"],
18
+ )
19
+ }
20
+ edges = []
21
+ for row in requirements.get("taskDependencies", []):
22
+ source = row["fromTaskKey"]
23
+ target = row["toTaskKey"]
24
+ nodes.setdefault(
25
+ source, VisualNode(source, source, "current", "active", row["relation"])
26
+ )
27
+ nodes.setdefault(
28
+ target, VisualNode(target, target, "next", "pending", row["relation"])
29
+ )
30
+ edges.append(VisualEdge(source, target, row["relation"], "dependency"))
31
+ return decision_flow_figure(
32
+ nodes=tuple(nodes.values()),
33
+ edges=tuple(edges),
34
+ title="Requirement routing and task dependencies",
35
+ )
36
+
37
+
38
+ def build_requirements_discovery_view(data: dict) -> HumanReportView:
39
+ requirements = data["requirementsDiscovery"]
40
+ figure = _routing_figure(data, requirements)
41
+ context = {
42
+ "humanSummary": data["humanSummary"],
43
+ "requirements": requirements,
44
+ "narrative": requirements["userNarrative"],
45
+ "routingFigure": figure,
46
+ "clarificationItems": data.get("clarificationItems", []),
47
+ "evidenceIndex": evidence_index(data),
48
+ "audit": audit_context(data),
49
+ }
50
+ return HumanReportView(
51
+ "requirements-discovery",
52
+ "html/tasks/requirements-discovery.template.html",
53
+ context,
54
+ (figure,),
55
+ )