okstra 0.146.0 → 0.147.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 +2 -2
- package/docs/architecture/storage-model.md +8 -7
- package/docs/architecture.md +18 -12
- package/docs/cli.md +3 -3
- package/docs/project-structure-overview.md +16 -14
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +10 -10
- package/runtime/bin/okstra-render-final-report.py +4 -4
- package/runtime/bin/okstra-render-report-views.py +100 -12
- package/runtime/bin/okstra-trace-cleanup.sh +13 -9
- package/runtime/prompts/lead/okstra-lead-contract.md +4 -4
- package/runtime/prompts/lead/report-writer.md +15 -11
- package/runtime/prompts/profiles/_common-contract.md +13 -7
- package/runtime/prompts/profiles/improvement-discovery.md +3 -1
- package/runtime/python/okstra_ctl/final_report_schema.py +37 -12
- package/runtime/python/okstra_ctl/render_final_report.py +136 -28
- package/runtime/python/okstra_ctl/report_contract.py +124 -0
- package/runtime/python/okstra_ctl/report_finalize.py +1 -1
- package/runtime/python/okstra_ctl/report_html/__init__.py +10 -0
- package/runtime/python/okstra_ctl/report_html/common.py +50 -0
- package/runtime/python/okstra_ctl/report_html/models.py +59 -0
- package/runtime/python/okstra_ctl/report_html/render.py +69 -0
- package/runtime/python/okstra_ctl/report_html/router.py +40 -0
- package/runtime/python/okstra_ctl/report_html/view_models/__init__.py +1 -0
- package/runtime/python/okstra_ctl/report_html/view_models/change_impact_analysis.py +38 -0
- package/runtime/python/okstra_ctl/report_html/view_models/error_analysis.py +48 -0
- package/runtime/python/okstra_ctl/report_html/view_models/feature_analysis.py +38 -0
- package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +46 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation.py +46 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +102 -0
- package/runtime/python/okstra_ctl/report_html/view_models/improvement_discovery.py +42 -0
- package/runtime/python/okstra_ctl/report_html/view_models/project_analysis.py +54 -0
- package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +53 -0
- package/runtime/python/okstra_ctl/report_html/view_models/requirements_discovery.py +54 -0
- package/runtime/python/okstra_ctl/report_html/visualizations.py +113 -0
- package/runtime/python/okstra_ctl/report_view_artifacts.py +4 -1
- package/runtime/python/okstra_ctl/report_views.py +15 -43
- package/runtime/python/okstra_ctl/run.py +12 -6
- package/runtime/python/okstra_ctl/schema_excerpt.py +7 -17
- package/runtime/schemas/final-report-v2.0.schema.json +3923 -0
- package/runtime/templates/reports/final-report-v2.template.md +66 -0
- package/runtime/templates/reports/html/assets/base.css +38 -0
- package/runtime/templates/reports/html/assets/base.js +5 -0
- package/runtime/templates/reports/html/base.template.html +65 -0
- package/runtime/templates/reports/html/macros/forms.html +47 -0
- package/runtime/templates/reports/html/macros/layout.html +19 -0
- package/runtime/templates/reports/html/macros/visualizations.html +17 -0
- package/runtime/templates/reports/html/tasks/change-impact-analysis.template.html +40 -0
- package/runtime/templates/reports/html/tasks/error-analysis.template.html +40 -0
- package/runtime/templates/reports/html/tasks/feature-analysis.template.html +40 -0
- package/runtime/templates/reports/html/tasks/final-verification.template.html +39 -0
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +47 -0
- package/runtime/templates/reports/html/tasks/implementation.template.html +40 -0
- package/runtime/templates/reports/html/tasks/improvement-discovery.template.html +29 -0
- package/runtime/templates/reports/html/tasks/project-analysis.template.html +57 -0
- package/runtime/templates/reports/html/tasks/release-handoff.template.html +36 -0
- package/runtime/templates/reports/html/tasks/requirements-discovery.template.html +37 -0
- package/runtime/validators/validate-report-views.py +86 -4
- package/runtime/validators/validate-run.py +62 -9
- package/runtime/validators/validate_improvement_report.py +55 -0
- package/src/commands/lifecycle/install.mjs +18 -13
- package/src/commands/report/finalize.mjs +2 -3
- package/src/commands/report/render-final-report.mjs +4 -2
- package/src/commands/report/render-views.mjs +8 -8
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
"""Render `final-report-<task-type>-<seq>.md` from its JSON SSOT.
|
|
2
2
|
|
|
3
3
|
The JSON SSOT lives next to the rendered markdown as
|
|
4
|
-
``final-report-<task-type>-<seq>.data.json``.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
the canonical user-facing markdown.
|
|
4
|
+
``final-report-<task-type>-<seq>.data.json``. Its ``schemaVersion`` selects the
|
|
5
|
+
matching final-report schema. Report-writer-worker writes the data.json in
|
|
6
|
+
Phase 6; this renderer + the matching Jinja2 template deterministically
|
|
7
|
+
produce the canonical AI-facing markdown.
|
|
9
8
|
|
|
10
9
|
Why this exists: prior to v0.32, report-writer-worker wrote the markdown
|
|
11
10
|
directly. Free-form authoring led to silent contract violations — missing
|
|
@@ -13,13 +12,10 @@ columns in the Execution Status table, omitted §4 phase-continuation
|
|
|
13
12
|
rows, ad-hoc ``## Index`` sections. Routing everything through one
|
|
14
13
|
template + schema cuts those failure modes to zero.
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
(section list + ID index) — so every ``FU-001`` / ``E-001`` / ``S-001``
|
|
21
|
-
token is clickable and the reader can jump to any section. This runs on
|
|
22
|
-
every render (including the Phase 7 re-render) and is idempotent.
|
|
15
|
+
For schema-v1 compatibility, the top-of-report ``## Index`` remains a
|
|
16
|
+
deterministic post-render section built by ``_inject_index_and_anchors``.
|
|
17
|
+
Schema v2 keeps its compact fixed AI-handoff order and does not inject the
|
|
18
|
+
legacy reader index; the task-specific HTML provides the human navigation.
|
|
23
19
|
|
|
24
20
|
Phase 7 mutation flow: ``okstra-token-usage.py --substitute-data`` fills
|
|
25
21
|
the ``tokenUsage`` and ``executionStatus[].totalTokens`` etc. cells in
|
|
@@ -27,9 +23,9 @@ data.json, then re-invokes this renderer so the markdown stays in sync.
|
|
|
27
23
|
The markdown is never hand-edited.
|
|
28
24
|
|
|
29
25
|
As of v0.33+, the renderer itself is the schema-enforcement seam: data.json
|
|
30
|
-
is validated against
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
is validated against its version-selected schema before any Jinja2 rendering
|
|
27
|
+
begins, so schema violations are caught at write-time rather than only by the
|
|
28
|
+
post-hoc ``validators/validate-run.py``.
|
|
33
29
|
"""
|
|
34
30
|
from __future__ import annotations
|
|
35
31
|
|
|
@@ -48,16 +44,37 @@ from typing import Any
|
|
|
48
44
|
import okstra_vendor # noqa: F401 — side effect: sys.modules aliases
|
|
49
45
|
from jinja2 import ChainableUndefined, Environment, FileSystemLoader
|
|
50
46
|
|
|
51
|
-
from okstra_ctl.final_report_schema import
|
|
47
|
+
from okstra_ctl.final_report_schema import (
|
|
48
|
+
SchemaError,
|
|
49
|
+
load_schema_for_data,
|
|
50
|
+
validate as schema_validate,
|
|
51
|
+
)
|
|
52
52
|
from okstra_ctl.i18n import I18nError, SUPPORTED_LANGS, load_dictionary, make_jinja_global
|
|
53
53
|
from okstra_ctl.md_table import UNESCAPED_PIPE_RE, to_cell_text
|
|
54
54
|
from okstra_ctl.models import UnknownModelError, resolve_model_metadata
|
|
55
|
+
from okstra_ctl.report_contract import TASK_TYPE_DATA_PROPERTY
|
|
55
56
|
from okstra_ctl.schema_excerpt import excerpt_cut_from_version
|
|
56
57
|
from okstra_ctl.seeding import installed_version
|
|
57
58
|
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
TEMPLATE_BY_SCHEMA_VERSION = {
|
|
61
|
+
"1.0": ("templates", "reports", "final-report.template.md"),
|
|
62
|
+
"2.0": ("templates", "reports", "final-report-v2.template.md"),
|
|
63
|
+
}
|
|
64
|
+
DEFAULT_TEMPLATE_REL = TEMPLATE_BY_SCHEMA_VERSION["1.0"]
|
|
65
|
+
|
|
66
|
+
TASK_DELIVERABLE_TITLES = {
|
|
67
|
+
"requirements-discovery": "Requirements Discovery",
|
|
68
|
+
"improvement-discovery": "Improvement Discovery",
|
|
69
|
+
"error-analysis": "Error Analysis",
|
|
70
|
+
"project-analysis": "Project Analysis",
|
|
71
|
+
"feature-analysis": "Feature Analysis",
|
|
72
|
+
"change-impact-analysis": "Change Impact Analysis",
|
|
73
|
+
"implementation-planning": "Implementation Planning",
|
|
74
|
+
"implementation": "Implementation",
|
|
75
|
+
"final-verification": "Final Verification",
|
|
76
|
+
"release-handoff": "Release Handoff",
|
|
77
|
+
}
|
|
61
78
|
|
|
62
79
|
class FinalReportRenderError(RuntimeError):
|
|
63
80
|
"""Raised when the data.json cannot be rendered. Wraps jinja2 errors
|
|
@@ -438,12 +455,11 @@ def _inject_index_and_anchors(markdown: str, dictionary: dict | None) -> str:
|
|
|
438
455
|
|
|
439
456
|
|
|
440
457
|
def inject_index_into_file(md_path: Path, *, report_language: str = "en") -> int:
|
|
441
|
-
"""Apply the top-of-report index
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
number of bytes written.
|
|
458
|
+
"""Apply the legacy top-of-report index to an existing Markdown report.
|
|
459
|
+
|
|
460
|
+
This remains for schema-v1 and quick compatibility artifacts. Schema-v2
|
|
461
|
+
reports use the fixed AI-handoff structure and never call this seam.
|
|
462
|
+
Idempotent; returns the number of bytes written.
|
|
447
463
|
"""
|
|
448
464
|
if not md_path.is_file():
|
|
449
465
|
raise FinalReportRenderError(f"report markdown not found: {md_path}")
|
|
@@ -470,7 +486,7 @@ def _enforce_schema(data: dict) -> None:
|
|
|
470
486
|
Phase 7 재렌더를 hard-fail 시키는 것은 과도하다.
|
|
471
487
|
"""
|
|
472
488
|
try:
|
|
473
|
-
schema =
|
|
489
|
+
schema = load_schema_for_data(data)
|
|
474
490
|
except SchemaError as exc:
|
|
475
491
|
print(
|
|
476
492
|
f"render-final-report: schema not locatable; skipping schema enforcement ({exc})",
|
|
@@ -544,6 +560,7 @@ def _build_environment(template_dir: Path) -> Environment:
|
|
|
544
560
|
env.filters["yaml_scalar"] = _yaml_scalar
|
|
545
561
|
env.filters["yaml_inline_list"] = _yaml_inline_list
|
|
546
562
|
env.filters["model_detail"] = _model_detail
|
|
563
|
+
env.filters["json_block"] = _json_markdown_block
|
|
547
564
|
# `mdcell` neutralises the two things in worker prose that can break a
|
|
548
565
|
# markdown table row: a literal `|` (splits the row) and a newline
|
|
549
566
|
# (truncates it, dropping every later column). Table-cell interpolations
|
|
@@ -558,6 +575,61 @@ def _build_environment(template_dir: Path) -> Environment:
|
|
|
558
575
|
return env
|
|
559
576
|
|
|
560
577
|
|
|
578
|
+
def _json_markdown_block(value: Any) -> str:
|
|
579
|
+
serialized = json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True)
|
|
580
|
+
return "\n".join(f" {line}" for line in serialized.splitlines())
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _without_human_narrative(value: Any) -> Any:
|
|
584
|
+
if isinstance(value, dict):
|
|
585
|
+
return {
|
|
586
|
+
key: _without_human_narrative(item)
|
|
587
|
+
for key, item in value.items()
|
|
588
|
+
if key != "userNarrative"
|
|
589
|
+
}
|
|
590
|
+
if isinstance(value, list):
|
|
591
|
+
return [_without_human_narrative(item) for item in value]
|
|
592
|
+
return value
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _ai_markdown_context(data: dict) -> dict:
|
|
596
|
+
context = _with_optional_defaults(data)
|
|
597
|
+
header = data.get("header") if isinstance(data.get("header"), dict) else {}
|
|
598
|
+
task_type = header.get("taskType", "")
|
|
599
|
+
property_name = TASK_TYPE_DATA_PROPERTY.get(task_type, "")
|
|
600
|
+
context["aiTaskDeliverableTitle"] = TASK_DELIVERABLE_TITLES.get(
|
|
601
|
+
task_type, task_type
|
|
602
|
+
)
|
|
603
|
+
context["aiTaskDeliverable"] = _without_human_narrative(
|
|
604
|
+
data.get(property_name, {})
|
|
605
|
+
)
|
|
606
|
+
context["aiEvidenceLedger"] = {
|
|
607
|
+
"evidence": data.get("evidence", {}),
|
|
608
|
+
"missingInformation": data.get("missingInformation", []),
|
|
609
|
+
"endStateCoverage": data.get("endStateCoverage", []),
|
|
610
|
+
"analysisCommon": data.get("analysisCommon"),
|
|
611
|
+
}
|
|
612
|
+
context["aiRoutingContract"] = {
|
|
613
|
+
"recommendedNextSteps": data.get("recommendedNextSteps", []),
|
|
614
|
+
"followUpTasks": data.get("followUpTasks", []),
|
|
615
|
+
}
|
|
616
|
+
context["aiDecisionContext"] = {
|
|
617
|
+
"rationale": data.get("rationale", {}),
|
|
618
|
+
"summary": data.get("summary", []),
|
|
619
|
+
"ticketCoverage": data.get("ticketCoverage"),
|
|
620
|
+
"finalVerdict": data.get("finalVerdict", {}),
|
|
621
|
+
}
|
|
622
|
+
context["aiBlockingIds"] = [
|
|
623
|
+
row.get("id")
|
|
624
|
+
for row in data.get("clarificationItems", [])
|
|
625
|
+
if isinstance(row, dict)
|
|
626
|
+
and row.get("status") in {"open", "answered"}
|
|
627
|
+
and row.get("blocks") in {"approval", "next-phase"}
|
|
628
|
+
and isinstance(row.get("id"), str)
|
|
629
|
+
]
|
|
630
|
+
return context
|
|
631
|
+
|
|
632
|
+
|
|
561
633
|
def resolve_report_language(data: dict, *, override: str | None) -> str:
|
|
562
634
|
"""우선순위: override > data.meta.reportLanguage > 'en'."""
|
|
563
635
|
if override is not None:
|
|
@@ -638,8 +710,14 @@ def render(
|
|
|
638
710
|
|
|
639
711
|
try:
|
|
640
712
|
template = env.get_template(template_path.name)
|
|
641
|
-
|
|
642
|
-
|
|
713
|
+
context = (
|
|
714
|
+
_ai_markdown_context(data)
|
|
715
|
+
if data.get("schemaVersion") == "2.0"
|
|
716
|
+
else _with_optional_defaults(data)
|
|
717
|
+
)
|
|
718
|
+
rendered = template.render(**context)
|
|
719
|
+
if data.get("schemaVersion") == "1.0":
|
|
720
|
+
rendered = _inject_index_and_anchors(rendered, dictionary)
|
|
643
721
|
return _ventilate_prose(rendered)
|
|
644
722
|
except I18nError as exc:
|
|
645
723
|
raise FinalReportRenderError(
|
|
@@ -680,6 +758,36 @@ def find_default_template(start: Path | None = None) -> Path:
|
|
|
680
758
|
)
|
|
681
759
|
|
|
682
760
|
|
|
761
|
+
def find_default_template_for_data(
|
|
762
|
+
data: dict, start: Path | None = None
|
|
763
|
+
) -> Path:
|
|
764
|
+
"""Locate the Markdown template selected by ``data.schemaVersion``."""
|
|
765
|
+
version = data.get("schemaVersion")
|
|
766
|
+
try:
|
|
767
|
+
relative_path = TEMPLATE_BY_SCHEMA_VERSION[version]
|
|
768
|
+
except KeyError as exc:
|
|
769
|
+
raise FinalReportRenderError(
|
|
770
|
+
f"unsupported final-report schemaVersion: {version}"
|
|
771
|
+
) from exc
|
|
772
|
+
|
|
773
|
+
okstra_home = os.environ.get("OKSTRA_HOME")
|
|
774
|
+
if okstra_home:
|
|
775
|
+
candidate = Path(okstra_home).joinpath(*relative_path)
|
|
776
|
+
if candidate.is_file():
|
|
777
|
+
return candidate
|
|
778
|
+
|
|
779
|
+
here = Path(start or __file__).resolve()
|
|
780
|
+
for parent in [here, *here.parents]:
|
|
781
|
+
candidate = parent.joinpath(*relative_path)
|
|
782
|
+
if candidate.is_file():
|
|
783
|
+
return candidate
|
|
784
|
+
|
|
785
|
+
raise FinalReportRenderError(
|
|
786
|
+
f"could not locate {relative_path[-1]}. Set OKSTRA_HOME or run from a "
|
|
787
|
+
"checkout that contains templates/reports/."
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
|
|
683
791
|
def _bundle_excerpt_path(data_path: Path) -> Path | None:
|
|
684
792
|
"""The task bundle's schema excerpt, found by walking up from *data_path*."""
|
|
685
793
|
for ancestor in data_path.resolve().parents:
|
|
@@ -746,7 +854,7 @@ def render_to_file(
|
|
|
746
854
|
# 프로젝트의 .okstra 트리만 위로 뒤지다 templates/reports 를 못 찾아
|
|
747
855
|
# 설치본에서 항상 'could not locate template' 으로 실패한다(OKSTRA_HOME 을
|
|
748
856
|
# 수동 설정해야 했던 원인). 프로젝트별 override 는 --template 으로 한다.
|
|
749
|
-
resolved_template = template_path or
|
|
857
|
+
resolved_template = template_path or find_default_template_for_data(data)
|
|
750
858
|
try:
|
|
751
859
|
rendered = render(
|
|
752
860
|
data,
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Single registry for public final-report task contracts."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
CURRENT_REPORT_SCHEMA_VERSION = "2.0"
|
|
6
|
+
LEGACY_REPORT_SCHEMA_VERSION = "1.0"
|
|
7
|
+
|
|
8
|
+
PUBLIC_REPORT_TASK_TYPES = (
|
|
9
|
+
"requirements-discovery",
|
|
10
|
+
"improvement-discovery",
|
|
11
|
+
"error-analysis",
|
|
12
|
+
"project-analysis",
|
|
13
|
+
"feature-analysis",
|
|
14
|
+
"change-impact-analysis",
|
|
15
|
+
"implementation-planning",
|
|
16
|
+
"implementation",
|
|
17
|
+
"final-verification",
|
|
18
|
+
"release-handoff",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
TASK_TYPE_DATA_PROPERTY = {
|
|
22
|
+
"requirements-discovery": "requirementsDiscovery",
|
|
23
|
+
"improvement-discovery": "improvementDiscovery",
|
|
24
|
+
"error-analysis": "errorAnalysis",
|
|
25
|
+
"project-analysis": "projectAnalysis",
|
|
26
|
+
"feature-analysis": "featureAnalysis",
|
|
27
|
+
"change-impact-analysis": "changeImpactAnalysis",
|
|
28
|
+
"implementation-planning": "implementationPlanning",
|
|
29
|
+
"implementation": "implementation",
|
|
30
|
+
"final-verification": "finalVerification",
|
|
31
|
+
"release-handoff": "releaseHandoff",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
TASK_TYPE_HTML_TEMPLATE = {
|
|
35
|
+
task_type: f"html/tasks/{task_type}.template.html"
|
|
36
|
+
for task_type in PUBLIC_REPORT_TASK_TYPES
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
TASK_TYPE_REQUIRED_HUMAN_FIELDS = {
|
|
40
|
+
"requirements-discovery": (
|
|
41
|
+
"requirementsDiscovery.requestVerbatim",
|
|
42
|
+
"requirementsDiscovery.systemInterpretation",
|
|
43
|
+
"requirementsDiscovery.classification",
|
|
44
|
+
"requirementsDiscovery.confirmedRequirements",
|
|
45
|
+
"requirementsDiscovery.unresolvedRequirements",
|
|
46
|
+
),
|
|
47
|
+
"improvement-discovery": (
|
|
48
|
+
"improvementDiscovery.candidates",
|
|
49
|
+
"improvementDiscovery.lensCoverage",
|
|
50
|
+
),
|
|
51
|
+
"error-analysis": (
|
|
52
|
+
"errorAnalysis.symptomVerbatim",
|
|
53
|
+
"errorAnalysis.observableFailure",
|
|
54
|
+
"errorAnalysis.causeCandidates.confidence",
|
|
55
|
+
"errorAnalysis.causeCandidates.falsifyingEvidenceChecked",
|
|
56
|
+
"errorAnalysis.causeCandidates.disproveWith",
|
|
57
|
+
),
|
|
58
|
+
"project-analysis": (
|
|
59
|
+
"projectAnalysis.components",
|
|
60
|
+
"projectAnalysis.entryPoints",
|
|
61
|
+
"projectAnalysis.dataStores",
|
|
62
|
+
"projectAnalysis.externalSystems",
|
|
63
|
+
"projectAnalysis.rankedHotspots",
|
|
64
|
+
"projectAnalysis.qualityCoverage",
|
|
65
|
+
"projectAnalysis.featureIndex",
|
|
66
|
+
),
|
|
67
|
+
"feature-analysis": (
|
|
68
|
+
"featureAnalysis.flows",
|
|
69
|
+
"featureAnalysis.domainRules",
|
|
70
|
+
"featureAnalysis.stateChanges",
|
|
71
|
+
"featureAnalysis.externalInteractions",
|
|
72
|
+
"featureAnalysis.testCoverage",
|
|
73
|
+
),
|
|
74
|
+
"change-impact-analysis": (
|
|
75
|
+
"changeImpactAnalysis.impactItems",
|
|
76
|
+
"changeImpactAnalysis.dependencyBlastRadius",
|
|
77
|
+
"changeImpactAnalysis.unaffectedBoundaries",
|
|
78
|
+
"changeImpactAnalysis.compatibilityImpact",
|
|
79
|
+
"changeImpactAnalysis.migrationImpact",
|
|
80
|
+
"changeImpactAnalysis.rollbackImpact",
|
|
81
|
+
"changeImpactAnalysis.securityAndPerformanceImpact",
|
|
82
|
+
"changeImpactAnalysis.planningInputs",
|
|
83
|
+
),
|
|
84
|
+
"implementation-planning": (
|
|
85
|
+
"implementationPlanning.optionCandidates",
|
|
86
|
+
"implementationPlanning.tradeoffMatrix",
|
|
87
|
+
"implementationPlanning.recommendedOption",
|
|
88
|
+
"implementationPlanning.stageMap",
|
|
89
|
+
"implementationPlanning.validationChecklist",
|
|
90
|
+
"implementationPlanning.rollbackStrategy",
|
|
91
|
+
),
|
|
92
|
+
"implementation": (
|
|
93
|
+
"implementation.diffSummary",
|
|
94
|
+
"implementation.requirementCoverage",
|
|
95
|
+
"implementation.validationEvidence",
|
|
96
|
+
"implementation.manualUserTest",
|
|
97
|
+
),
|
|
98
|
+
"final-verification": (
|
|
99
|
+
"finalVerification.validationEvidence",
|
|
100
|
+
"finalVerification.acceptanceBlockers",
|
|
101
|
+
"finalVerification.residualRisk",
|
|
102
|
+
"finalVerification.manualUserTest",
|
|
103
|
+
),
|
|
104
|
+
"release-handoff": (
|
|
105
|
+
"releaseHandoff.userSelections",
|
|
106
|
+
"releaseHandoff.featureBranchState",
|
|
107
|
+
"releaseHandoff.pullRequestOutcome",
|
|
108
|
+
"releaseHandoff.mergeConflictProbe",
|
|
109
|
+
),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def report_property_for(task_type: str) -> str:
|
|
114
|
+
try:
|
|
115
|
+
return TASK_TYPE_DATA_PROPERTY[task_type]
|
|
116
|
+
except KeyError as exc:
|
|
117
|
+
raise ValueError(f"unsupported public report task type: {task_type}") from exc
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def html_template_for(task_type: str) -> str:
|
|
121
|
+
try:
|
|
122
|
+
return TASK_TYPE_HTML_TEMPLATE[task_type]
|
|
123
|
+
except KeyError as exc:
|
|
124
|
+
raise ValueError(f"no task-specific html template for: {task_type}") from exc
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Task-specific human HTML rendering for final-report schema v2."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def render_v2_html_view(*args, **kwargs):
|
|
5
|
+
"""Load the vendored template runtime only when a v2 view is rendered."""
|
|
6
|
+
from .render import render_v2_html_view as render
|
|
7
|
+
|
|
8
|
+
return render(*args, **kwargs)
|
|
9
|
+
|
|
10
|
+
__all__ = ["render_v2_html_view"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Common data transformations that carry no task-specific section order."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def audit_context(data: dict) -> dict[str, object]:
|
|
6
|
+
return {
|
|
7
|
+
"crossVerification": data.get("crossVerification", {}),
|
|
8
|
+
"executionStatus": data.get("executionStatus", []),
|
|
9
|
+
"tokenUsage": data.get("tokenUsage", {}),
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def evidence_index(data: dict) -> dict[str, object]:
|
|
14
|
+
rows: dict[str, object] = {}
|
|
15
|
+
for section in ("primary", "secondary"):
|
|
16
|
+
for row in data.get("evidence", {}).get(section, []):
|
|
17
|
+
row_id = row.get("id")
|
|
18
|
+
if row_id:
|
|
19
|
+
rows[row_id] = row
|
|
20
|
+
analysis = data.get("analysisCommon", {})
|
|
21
|
+
for collection in ("confirmedFacts", "inferences", "unknowns"):
|
|
22
|
+
for row in analysis.get(collection, []):
|
|
23
|
+
row_id = row.get("id")
|
|
24
|
+
if row_id:
|
|
25
|
+
rows[row_id] = row
|
|
26
|
+
return rows
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _collect_ids(value: object, found: set[str]) -> None:
|
|
30
|
+
if isinstance(value, dict):
|
|
31
|
+
row_id = value.get("id")
|
|
32
|
+
if isinstance(row_id, str) and row_id:
|
|
33
|
+
found.add(row_id)
|
|
34
|
+
for nested in value.values():
|
|
35
|
+
_collect_ids(nested, found)
|
|
36
|
+
elif isinstance(value, list):
|
|
37
|
+
for nested in value:
|
|
38
|
+
_collect_ids(nested, found)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def analysis_review_ids(data: dict) -> tuple[str, ...]:
|
|
42
|
+
found: set[str] = set()
|
|
43
|
+
_collect_ids(data.get("analysisCommon", {}), found)
|
|
44
|
+
task_type = data.get("header", {}).get("taskType", "")
|
|
45
|
+
from ..report_contract import TASK_TYPE_DATA_PROPERTY
|
|
46
|
+
|
|
47
|
+
property_name = TASK_TYPE_DATA_PROPERTY.get(task_type)
|
|
48
|
+
if property_name:
|
|
49
|
+
_collect_ids(data.get(property_name, {}), found)
|
|
50
|
+
return tuple(sorted(found))
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Stable view-model types shared by task-specific HTML renderers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class HtmlRunMeta:
|
|
10
|
+
task_key: str
|
|
11
|
+
task_type: str
|
|
12
|
+
seq: str
|
|
13
|
+
source_report: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class VisualNode:
|
|
18
|
+
id: str
|
|
19
|
+
label: str
|
|
20
|
+
group: str
|
|
21
|
+
status: str
|
|
22
|
+
detail: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class VisualEdge:
|
|
27
|
+
source: str
|
|
28
|
+
target: str
|
|
29
|
+
label: str
|
|
30
|
+
kind: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class FigureModel:
|
|
35
|
+
figure_id: str
|
|
36
|
+
kind: str
|
|
37
|
+
title: str
|
|
38
|
+
summary: str
|
|
39
|
+
nodes: tuple[VisualNode, ...]
|
|
40
|
+
edges: tuple[VisualEdge, ...]
|
|
41
|
+
svg: str
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def table_row_ids(self) -> tuple[str, ...]:
|
|
45
|
+
return tuple(node.id for node in self.nodes)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class HumanReportView:
|
|
50
|
+
task_type: str
|
|
51
|
+
template_name: str
|
|
52
|
+
context: dict[str, object]
|
|
53
|
+
figures: tuple[FigureModel, ...]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class HtmlRoute:
|
|
58
|
+
template_name: str
|
|
59
|
+
view_builder: Callable[[dict], HumanReportView]
|
|
@@ -0,0 +1,69 @@
|
|
|
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 .models import HtmlRunMeta
|
|
13
|
+
from .router import HtmlRenderError, resolve_html_route
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _sha256(path: Path) -> str:
|
|
17
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _templates_root(start: Path | None = None) -> Path:
|
|
21
|
+
if start is not None:
|
|
22
|
+
return start
|
|
23
|
+
here = Path(__file__).resolve()
|
|
24
|
+
for parent in [here, *here.parents]:
|
|
25
|
+
candidate = parent / "templates" / "reports"
|
|
26
|
+
if candidate.is_dir():
|
|
27
|
+
return candidate
|
|
28
|
+
raise HtmlRenderError("could not locate templates/reports")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _html_path(data_path: Path) -> Path:
|
|
32
|
+
suffix = ".data.json"
|
|
33
|
+
if not data_path.name.endswith(suffix):
|
|
34
|
+
raise HtmlRenderError(f"v2 report path must end with {suffix}: {data_path}")
|
|
35
|
+
return data_path.with_name(data_path.name.removesuffix(suffix) + ".html")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def render_v2_html_view(
|
|
39
|
+
data_path: Path,
|
|
40
|
+
markdown_path: Path,
|
|
41
|
+
*,
|
|
42
|
+
run_meta: HtmlRunMeta,
|
|
43
|
+
templates_root: Path | None = None,
|
|
44
|
+
) -> Path:
|
|
45
|
+
data = json.loads(data_path.read_text(encoding="utf-8"))
|
|
46
|
+
errors = validate(data, load_schema_for_data(data))
|
|
47
|
+
if errors:
|
|
48
|
+
raise HtmlRenderError("invalid v2 final-report data: " + "; ".join(errors[:5]))
|
|
49
|
+
if not markdown_path.is_file():
|
|
50
|
+
raise HtmlRenderError(f"v2 markdown sibling not found: {markdown_path}")
|
|
51
|
+
route = resolve_html_route(run_meta.task_type)
|
|
52
|
+
view = route.view_builder(data)
|
|
53
|
+
root = _templates_root(templates_root)
|
|
54
|
+
env = Environment(loader=FileSystemLoader(str(root)), autoescape=select_autoescape(("html",)), undefined=StrictUndefined)
|
|
55
|
+
response_js = (root / "report.js").read_text(encoding="utf-8")
|
|
56
|
+
base_js = (root / "html/assets/base.js").read_text(encoding="utf-8")
|
|
57
|
+
context = {
|
|
58
|
+
**view.context,
|
|
59
|
+
"runMeta": run_meta,
|
|
60
|
+
"taskType": view.task_type,
|
|
61
|
+
"dataSha256": _sha256(data_path),
|
|
62
|
+
"markdownSha256": _sha256(markdown_path),
|
|
63
|
+
"clarificationItems": data.get("clarificationItems", []),
|
|
64
|
+
"css": (root / "html/assets/base.css").read_text(encoding="utf-8"),
|
|
65
|
+
"js": response_js + "\n" + base_js,
|
|
66
|
+
}
|
|
67
|
+
output_path = _html_path(data_path)
|
|
68
|
+
output_path.write_text(env.get_template(route.template_name).render(**context), encoding="utf-8")
|
|
69
|
+
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,38 @@
|
|
|
1
|
+
"""Human-first change-impact-analysis view model."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from ..common import analysis_review_ids, audit_context
|
|
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
|
+
"audit": audit_context(data),
|
|
32
|
+
}
|
|
33
|
+
return HumanReportView(
|
|
34
|
+
task_type="change-impact-analysis",
|
|
35
|
+
template_name="html/tasks/change-impact-analysis.template.html",
|
|
36
|
+
context=context,
|
|
37
|
+
figures=(figure,),
|
|
38
|
+
)
|