okstra 0.144.0 → 0.145.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 +4 -1
- package/docs/architecture.md +18 -2
- package/docs/cli.md +39 -2
- package/docs/project-structure-overview.md +16 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/lead/convergence.md +11 -3
- package/runtime/prompts/lead/okstra-lead-contract.md +7 -1
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/profiles/change-impact-analysis.md +24 -0
- package/runtime/prompts/profiles/feature-analysis.md +24 -0
- package/runtime/prompts/profiles/forbidden-actions.json +18 -0
- package/runtime/prompts/profiles/project-analysis.md +24 -0
- package/runtime/prompts/wizard/prompts.ko.json +44 -1
- package/runtime/python/okstra_ctl/analysis_inputs.py +369 -0
- package/runtime/python/okstra_ctl/clarification_items.py +74 -1
- package/runtime/python/okstra_ctl/render.py +77 -4
- package/runtime/python/okstra_ctl/render_final_report.py +13 -4
- package/runtime/python/okstra_ctl/report_views.py +134 -3
- package/runtime/python/okstra_ctl/run.py +118 -0
- package/runtime/python/okstra_ctl/run_context.py +34 -2
- package/runtime/python/okstra_ctl/schema_excerpt.py +12 -4
- package/runtime/python/okstra_ctl/user_response.py +309 -3
- package/runtime/python/okstra_ctl/wizard.py +545 -32
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +3 -0
- package/runtime/python/okstra_ctl/workflow.py +22 -0
- package/runtime/schemas/final-report-v1.0.schema.json +849 -3
- package/runtime/skills/okstra-run/SKILL.md +13 -1
- package/runtime/templates/reports/change-impact-analysis-input.template.md +58 -0
- package/runtime/templates/reports/feature-analysis-input.template.md +59 -0
- package/runtime/templates/reports/final-report.template.md +220 -0
- package/runtime/templates/reports/i18n/en.json +8 -0
- package/runtime/templates/reports/i18n/ko.json +8 -0
- package/runtime/templates/reports/project-analysis-input.template.md +58 -0
- package/runtime/templates/reports/report.js +84 -5
- package/runtime/templates/reports/user-response.template.md +19 -1
- package/runtime/validators/validate-report-views.py +61 -7
- package/runtime/validators/validate-run.py +42 -0
- package/runtime/validators/validate_analysis_report.py +864 -0
- package/src/commands/execute/render-bundle.mjs +3 -0
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
(``scripts/okstra-render-report-views.py``).
|
|
4
4
|
|
|
5
5
|
Checks, for a given final-report MD path:
|
|
6
|
-
1. ``*.html`` sibling exists
|
|
6
|
+
1. ``*.html`` sibling exists when the report has clarification rows,
|
|
7
|
+
an analysis-review contract, or a plan-approval contract.
|
|
7
8
|
2. HTML's ``source-sha256`` in run-meta matches the current MD body —
|
|
8
9
|
stale html detection.
|
|
9
10
|
3. HTML's §5.6 / §5.7 / §5.8 deliverable regions contain no
|
|
@@ -13,6 +14,8 @@ Checks, for a given final-report MD path:
|
|
|
13
14
|
``<img src=>`` — self-contained guarantee.
|
|
14
15
|
5. Every Response ID in HTML matches the §1 Clarification Items table
|
|
15
16
|
of the source MD (1:1).
|
|
17
|
+
6. Analysis reports contain all three Analysis Review decisions and an
|
|
18
|
+
affected-ID selector exactly matching the structured IDs in data.json.
|
|
16
19
|
|
|
17
20
|
Exit codes: 0 on success, 1 on any failure. Failures are printed one
|
|
18
21
|
per line to stderr.
|
|
@@ -34,6 +37,7 @@ from okstra_ctl.clarification_items import ( # noqa: E402
|
|
|
34
37
|
section_1_present_but_unparsed,
|
|
35
38
|
)
|
|
36
39
|
from okstra_ctl.report_views import ( # noqa: E402
|
|
40
|
+
analysis_review_context,
|
|
37
41
|
extract_html_digest,
|
|
38
42
|
plan_approval_context,
|
|
39
43
|
source_digest,
|
|
@@ -47,6 +51,18 @@ _EXTERNAL_URL_RE = re.compile(
|
|
|
47
51
|
)
|
|
48
52
|
|
|
49
53
|
_RESPONSE_ID_ATTR_RE = re.compile(r'data-response-id="(C-\d+)"')
|
|
54
|
+
_ANALYSIS_REVIEW_SECTION_RE = re.compile(
|
|
55
|
+
r'<section id="analysis-review">(?P<body>.*?)</section>', re.DOTALL
|
|
56
|
+
)
|
|
57
|
+
_ANALYSIS_REVIEW_STATUS_RE = re.compile(
|
|
58
|
+
r'name="analysis-review-status"\s+value="([^"]+)"'
|
|
59
|
+
)
|
|
60
|
+
_ANALYSIS_REVIEW_SELECTOR_RE = re.compile(
|
|
61
|
+
r'<select id="analysis-review-affected-ids"[^>]*>(?P<body>.*?)</select>',
|
|
62
|
+
re.DOTALL,
|
|
63
|
+
)
|
|
64
|
+
_OPTION_VALUE_RE = re.compile(r'<option value="([^"]+)">')
|
|
65
|
+
_ANALYSIS_REVIEW_STATUSES = ["accepted", "rejected", "revision-requested"]
|
|
50
66
|
|
|
51
67
|
|
|
52
68
|
def _main_body(html_text: str) -> str:
|
|
@@ -78,6 +94,34 @@ def _no_form_sections(html_body: str) -> list[str]:
|
|
|
78
94
|
return chunks
|
|
79
95
|
|
|
80
96
|
|
|
97
|
+
def _validate_analysis_review_html(
|
|
98
|
+
html_text: str, expected_ids: tuple[str, ...]
|
|
99
|
+
) -> list[str]:
|
|
100
|
+
section = _ANALYSIS_REVIEW_SECTION_RE.search(html_text)
|
|
101
|
+
if section is None:
|
|
102
|
+
return ["analysis report html is missing the Analysis Review control"]
|
|
103
|
+
body = section.group("body")
|
|
104
|
+
failures: list[str] = []
|
|
105
|
+
statuses = sorted(_ANALYSIS_REVIEW_STATUS_RE.findall(body))
|
|
106
|
+
if statuses != _ANALYSIS_REVIEW_STATUSES:
|
|
107
|
+
failures.append(
|
|
108
|
+
"Analysis Review controls must be exactly accepted, "
|
|
109
|
+
"revision-requested, and rejected"
|
|
110
|
+
)
|
|
111
|
+
selector = _ANALYSIS_REVIEW_SELECTOR_RE.search(body)
|
|
112
|
+
actual_ids = (
|
|
113
|
+
sorted(_OPTION_VALUE_RE.findall(selector.group("body")))
|
|
114
|
+
if selector is not None
|
|
115
|
+
else []
|
|
116
|
+
)
|
|
117
|
+
if actual_ids != sorted(expected_ids):
|
|
118
|
+
failures.append(
|
|
119
|
+
f"Analysis Review selector mismatch: data.json has "
|
|
120
|
+
f"{sorted(expected_ids)}, HTML has {actual_ids}"
|
|
121
|
+
)
|
|
122
|
+
return failures
|
|
123
|
+
|
|
124
|
+
|
|
81
125
|
def validate(report_path: Path) -> list[str]:
|
|
82
126
|
failures: list[str] = []
|
|
83
127
|
if not report_path.is_file():
|
|
@@ -94,14 +138,16 @@ def validate(report_path: Path) -> list[str]:
|
|
|
94
138
|
"form parity. Re-render the report so §1 matches the schema."
|
|
95
139
|
]
|
|
96
140
|
md_ids = _md_response_ids(md)
|
|
141
|
+
review_ctx = analysis_review_context(report_path)
|
|
97
142
|
|
|
98
|
-
# (1) sibling artifact exists
|
|
99
|
-
#
|
|
100
|
-
# for §1 C-* rows, so a clarification-free report intentionally has no
|
|
101
|
-
# html sibling (see report_views.report_has_clarification_items). When
|
|
102
|
-
# there are no C-* rows and no html, that is the expected skip state.
|
|
143
|
+
# (1) sibling artifact exists when clarification rows or a structured
|
|
144
|
+
# analysis-review / plan-approval contract require interactive controls.
|
|
103
145
|
if not html_path.is_file():
|
|
104
|
-
if
|
|
146
|
+
if (
|
|
147
|
+
not md_ids
|
|
148
|
+
and plan_approval_context(report_path, md) is None
|
|
149
|
+
and review_ctx is None
|
|
150
|
+
):
|
|
105
151
|
return []
|
|
106
152
|
return [f"missing html artifact: {html_path}"]
|
|
107
153
|
|
|
@@ -146,6 +192,14 @@ def validate(report_path: Path) -> list[str]:
|
|
|
146
192
|
f"Response ID mismatch: MD §1 has {md_ids}, HTML has {html_ids}"
|
|
147
193
|
)
|
|
148
194
|
|
|
195
|
+
review_section_present = _ANALYSIS_REVIEW_SECTION_RE.search(html_text) is not None
|
|
196
|
+
if review_ctx is not None:
|
|
197
|
+
failures.extend(
|
|
198
|
+
_validate_analysis_review_html(html_text, review_ctx.selector_ids)
|
|
199
|
+
)
|
|
200
|
+
elif review_section_present:
|
|
201
|
+
failures.append("non-analysis report html must not contain Analysis Review controls")
|
|
202
|
+
|
|
149
203
|
return failures
|
|
150
204
|
|
|
151
205
|
|
|
@@ -21,6 +21,8 @@ from typing import Any
|
|
|
21
21
|
# copy under ~/.okstra/lib. The manifests advertise the bare script path, so
|
|
22
22
|
# it must self-bootstrap rather than rely on an inherited PYTHONPATH.
|
|
23
23
|
_VALIDATORS_DIR = Path(__file__).resolve().parent
|
|
24
|
+
if str(_VALIDATORS_DIR) not in sys.path:
|
|
25
|
+
sys.path.insert(0, str(_VALIDATORS_DIR))
|
|
24
26
|
for _ssot_dir in (_VALIDATORS_DIR.parent / "scripts", _VALIDATORS_DIR.parent / "python"):
|
|
25
27
|
if _ssot_dir.is_dir() and str(_ssot_dir) not in sys.path:
|
|
26
28
|
sys.path.insert(0, str(_ssot_dir))
|
|
@@ -96,6 +98,7 @@ from okstra_ctl.worker_prompt_contract import ( # noqa: E402
|
|
|
96
98
|
PromptRecord,
|
|
97
99
|
validate_initial_prompt_records,
|
|
98
100
|
)
|
|
101
|
+
from validate_analysis_report import validate_analysis_report # noqa: E402
|
|
99
102
|
from okstra_ctl.convergence_engine import ( # noqa: E402
|
|
100
103
|
grouped_input_digest,
|
|
101
104
|
validate_final_state,
|
|
@@ -2988,6 +2991,9 @@ def validate_final_report_data(
|
|
|
2988
2991
|
failures: list[str],
|
|
2989
2992
|
*,
|
|
2990
2993
|
report_contracts: set[str] | None = None,
|
|
2994
|
+
run_manifest: dict | None = None,
|
|
2995
|
+
project_root: Path | None = None,
|
|
2996
|
+
clarification_text: str = "",
|
|
2991
2997
|
) -> Mapping[str, Any] | None:
|
|
2992
2998
|
"""Validate the final-report data.json against the v1.0 schema.
|
|
2993
2999
|
|
|
@@ -3036,6 +3042,21 @@ def validate_final_report_data(
|
|
|
3036
3042
|
errors = schema_validate(data, schema)
|
|
3037
3043
|
for err in errors:
|
|
3038
3044
|
failures.append(f"final-report data.json: {err}")
|
|
3045
|
+
if errors:
|
|
3046
|
+
return data
|
|
3047
|
+
|
|
3048
|
+
analysis_result = validate_analysis_report(
|
|
3049
|
+
data=data,
|
|
3050
|
+
report_path=report_path,
|
|
3051
|
+
project_root=project_root or report_path.parent,
|
|
3052
|
+
run_manifest=run_manifest or {},
|
|
3053
|
+
clarification_text=clarification_text,
|
|
3054
|
+
)
|
|
3055
|
+
manifest_task_type = str((run_manifest or {}).get("taskType") or "")
|
|
3056
|
+
task_type = manifest_task_type or str(
|
|
3057
|
+
(data.get("header") or {}).get("taskType") or ""
|
|
3058
|
+
)
|
|
3059
|
+
failures.extend(f"{task_type}: {error}" for error in analysis_result.errors)
|
|
3039
3060
|
|
|
3040
3061
|
_validate_rationale_evidence(data, failures)
|
|
3041
3062
|
_validate_no_opaque_id_references(data, failures)
|
|
@@ -4117,6 +4138,22 @@ def _carry_in_source_for_run(run_manifest_path: Path) -> str:
|
|
|
4117
4138
|
return raw.strip() if isinstance(raw, str) else ""
|
|
4118
4139
|
|
|
4119
4140
|
|
|
4141
|
+
def _clarification_text_for_run(
|
|
4142
|
+
run_manifest_path: Path, report_path: Path
|
|
4143
|
+
) -> str:
|
|
4144
|
+
"""Read only the clarification input explicitly attached to this run."""
|
|
4145
|
+
if not _carry_in_source_for_run(run_manifest_path):
|
|
4146
|
+
return ""
|
|
4147
|
+
task_root = _task_root_from_run_dir(report_path.parent.parent)
|
|
4148
|
+
staged = task_root / "instruction-set" / "clarification-response.md"
|
|
4149
|
+
if not staged.is_file():
|
|
4150
|
+
return ""
|
|
4151
|
+
try:
|
|
4152
|
+
return staged.read_text(encoding="utf-8")
|
|
4153
|
+
except OSError:
|
|
4154
|
+
return ""
|
|
4155
|
+
|
|
4156
|
+
|
|
4120
4157
|
def _validate_clarification_carry_in_recorded(
|
|
4121
4158
|
data: dict,
|
|
4122
4159
|
report_path: Path,
|
|
@@ -7066,6 +7103,11 @@ def main() -> int:
|
|
|
7066
7103
|
report_path,
|
|
7067
7104
|
failures,
|
|
7068
7105
|
report_contracts=report_contracts,
|
|
7106
|
+
run_manifest=run_manifest,
|
|
7107
|
+
project_root=project_root,
|
|
7108
|
+
clarification_text=_clarification_text_for_run(
|
|
7109
|
+
run_manifest_path, report_path
|
|
7110
|
+
),
|
|
7069
7111
|
)
|
|
7070
7112
|
validation_data = report_data if isinstance(report_data, Mapping) else {}
|
|
7071
7113
|
_validate_fix_cycle(run_manifest, validation_data, failures)
|