okstra 0.200.0 → 0.201.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 -2
- package/dist/cli-registry.mjs +6 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/docs/cli.md +14 -3
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +7 -3
- package/runtime/bin/okstra-spawn-followups.py +2 -2
- package/runtime/prompts/duties/technical-verification-worker.md +44 -0
- package/runtime/prompts/launch.template.md +7 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +7 -2
- package/runtime/prompts/lead/plan-body-verification.md +3 -1
- package/runtime/prompts/lead/report-writer.md +11 -5
- package/runtime/prompts/lead/team-contract.md +6 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +7 -1
- package/runtime/prompts/profiles/final-verification.md +5 -0
- package/runtime/prompts/profiles/forbidden-actions.json +6 -0
- package/runtime/prompts/profiles/implementation-option-selection.md +7 -1
- package/runtime/prompts/profiles/implementation-planning.md +1 -0
- package/runtime/prompts/profiles/technical-verification.md +53 -0
- package/runtime/prompts/wizard/prompts.ko.json +2 -1
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -4
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +2 -0
- package/runtime/python/okstra_ctl/adapters/providers/zai/adapter.py +36 -5
- package/runtime/python/okstra_ctl/agent/invocation.py +14 -6
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +4 -3
- package/runtime/python/okstra_ctl/agent/prompt_cli/corrections.py +83 -22
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +44 -2
- package/runtime/python/okstra_ctl/conformance.py +2 -20
- package/runtime/python/okstra_ctl/dispatch_core.py +25 -5
- package/runtime/python/okstra_ctl/dispatch_state.py +2 -0
- package/runtime/python/okstra_ctl/domain/provider.py +0 -1
- package/runtime/python/okstra_ctl/domain/role.py +1 -0
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -1
- package/runtime/python/okstra_ctl/implementation_direction.py +64 -7
- package/runtime/python/okstra_ctl/implementation_options.py +58 -45
- package/runtime/python/okstra_ctl/model_pool.py +2 -5
- package/runtime/python/okstra_ctl/next_phase.py +3 -0
- package/runtime/python/okstra_ctl/plan_items.py +15 -0
- package/runtime/python/okstra_ctl/plan_items_cli.py +9 -3
- package/runtime/python/okstra_ctl/qa_commands.py +30 -0
- package/runtime/python/okstra_ctl/registry/provider_registry.py +11 -8
- package/runtime/python/okstra_ctl/render.py +3 -0
- package/runtime/python/okstra_ctl/render_final_report.py +1 -0
- package/runtime/python/okstra_ctl/report_assembly.py +8 -2
- package/runtime/python/okstra_ctl/report_contract.py +3 -0
- package/runtime/python/okstra_ctl/report_corrections.py +209 -93
- package/runtime/python/okstra_ctl/report_finalize.py +25 -8
- package/runtime/python/okstra_ctl/report_html/router.py +2 -0
- package/runtime/python/okstra_ctl/report_html/view_models/technical_verification.py +21 -0
- package/runtime/python/okstra_ctl/report_projections.py +4 -3
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +181 -47
- package/runtime/python/okstra_ctl/run.py +82 -0
- package/runtime/python/okstra_ctl/team.py +4 -1
- package/runtime/python/okstra_ctl/technical_verification.py +195 -0
- package/runtime/python/okstra_ctl/usage_identity.py +54 -0
- package/runtime/python/okstra_ctl/usage_report.py +22 -8
- package/runtime/python/okstra_ctl/verification_target.py +74 -0
- package/runtime/python/okstra_ctl/wizard/__init__.py +1 -1
- package/runtime/python/okstra_ctl/wizard/cli.py +2 -1
- package/runtime/python/okstra_ctl/wizard/confirmation.py +38 -2
- package/runtime/python/okstra_ctl/wizard/engine.py +3 -0
- package/runtime/python/okstra_ctl/wizard/ids.py +1 -0
- package/runtime/python/okstra_ctl/wizard/outcome.py +63 -0
- package/runtime/python/okstra_ctl/wizard/picker_navigation.py +2 -2
- package/runtime/python/okstra_ctl/wizard/registry.py +1 -1
- package/runtime/python/okstra_ctl/wizard/render.py +8 -55
- package/runtime/python/okstra_ctl/wizard/roles.py +11 -7
- package/runtime/python/okstra_ctl/wizard/sources.py +28 -2
- package/runtime/python/okstra_ctl/wizard/state.py +13 -6
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +8 -0
- package/runtime/python/okstra_ctl/worker_liveness.py +52 -39
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +2 -0
- package/runtime/python/okstra_ctl/workflow.py +8 -0
- package/runtime/python/okstra_ctl/write_policy.py +23 -0
- package/runtime/python/okstra_token_usage/blocks.py +50 -1
- package/runtime/python/okstra_token_usage/claude.py +42 -21
- package/runtime/python/okstra_token_usage/codex.py +17 -0
- package/runtime/python/okstra_token_usage/collect.py +299 -162
- package/runtime/python/okstra_token_usage/cursor.py +2 -3
- package/runtime/python/okstra_token_usage/report.py +35 -30
- package/runtime/python/okstra_token_usage/task_totals.py +3 -12
- package/runtime/schemas/final-report-v2.0.schema.json +298 -7
- package/runtime/schemas/final-report-v3.0.schema.json +298 -7
- package/runtime/schemas/report-narrative-v3.0.schema.json +1 -0
- package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +1 -1
- package/runtime/schemas/report-writer-corrections-v1.0.schema.json +30 -3
- package/runtime/skills/okstra-run/SKILL.md +10 -2
- package/runtime/skills/okstra-setup/SKILL.md +42 -7
- package/runtime/templates/report-writer-prompt-preamble.md +7 -3
- package/runtime/templates/reports/html/i18n/en.json +11 -0
- package/runtime/templates/reports/html/i18n/ko.json +11 -0
- package/runtime/templates/reports/html/tasks/implementation-option-selection.template.html +7 -3
- package/runtime/templates/reports/html/tasks/technical-verification.template.html +35 -0
- package/runtime/templates/reports/md/tasks/technical-verification.template.md +5 -0
- package/runtime/validators/validate-run.py +9 -4
|
@@ -26,6 +26,7 @@ from .report_synthesis_packet import (
|
|
|
26
26
|
from .conformance import strip_stage_declaration_label
|
|
27
27
|
from .design_prep import DesignPrepError, materialize_design_prep_requests
|
|
28
28
|
from .implementation_options import validate_blocked_answer_channel
|
|
29
|
+
from .technical_verification import validate_technical_verification_report
|
|
29
30
|
from .implementation_direction import (
|
|
30
31
|
load_selected_direction_snapshot,
|
|
31
32
|
validate_selected_direction_plan,
|
|
@@ -462,8 +463,7 @@ def _compose(
|
|
|
462
463
|
data["tokenUsage"] = usage
|
|
463
464
|
data["crossVerification"] = convergence_data["crossVerification"]
|
|
464
465
|
data["clarificationItems"] = _clarifications(ledger, activities, inputs["approval-decisions"].path)
|
|
465
|
-
#
|
|
466
|
-
# 이 원장을 다시 읽는 경로가 없어 사용자에게 질문이 0건으로 보인다.
|
|
466
|
+
# 작성자의 사실 분류와 리드 원장의 질문 연결은 양쪽 입력을 합친 뒤 검사한다.
|
|
467
467
|
for reason in validate_blocked_answer_channel(data):
|
|
468
468
|
_fail(
|
|
469
469
|
"lead",
|
|
@@ -888,6 +888,12 @@ def assemble_report(
|
|
|
888
888
|
_fail("orchestrator", manifest_path, "expectedReportRecordPath", "required")
|
|
889
889
|
target = Path(value)
|
|
890
890
|
target = target if target.is_absolute() else project_root / target
|
|
891
|
+
verification_errors = validate_technical_verification_report(data, target, project_root)
|
|
892
|
+
if verification_errors:
|
|
893
|
+
raise ReportAssemblyError(tuple(
|
|
894
|
+
AssemblyIssue("report-writer", str(target), "technicalVerification", error)
|
|
895
|
+
for error in verification_errors
|
|
896
|
+
))
|
|
891
897
|
_publish(target, data, replace)
|
|
892
898
|
if errors or direction_errors:
|
|
893
899
|
raise ReportAssemblyError(
|
|
@@ -9,6 +9,7 @@ PUBLIC_REPORT_TASK_TYPES = (
|
|
|
9
9
|
"requirements-discovery",
|
|
10
10
|
"improvement-discovery",
|
|
11
11
|
"error-analysis",
|
|
12
|
+
"technical-verification",
|
|
12
13
|
"implementation-option-selection",
|
|
13
14
|
"project-analysis",
|
|
14
15
|
"feature-analysis",
|
|
@@ -23,6 +24,7 @@ TASK_TYPE_DATA_PROPERTY = {
|
|
|
23
24
|
"requirements-discovery": "requirementsDiscovery",
|
|
24
25
|
"improvement-discovery": "improvementDiscovery",
|
|
25
26
|
"error-analysis": "errorAnalysis",
|
|
27
|
+
"technical-verification": "technicalVerification",
|
|
26
28
|
"implementation-option-selection": "implementationOptionSelection",
|
|
27
29
|
"project-analysis": "projectAnalysis",
|
|
28
30
|
"feature-analysis": "featureAnalysis",
|
|
@@ -47,6 +49,7 @@ TASK_TYPE_MARKDOWN_TEMPLATE = {
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
TASK_TYPE_REQUIRED_HUMAN_FIELDS = {
|
|
52
|
+
"technical-verification": ("technicalVerification.sourceReport", "technicalVerification.checks", "technicalVerification.routing"),
|
|
50
53
|
"requirements-discovery": (
|
|
51
54
|
"requirementsDiscovery.requestVerbatim",
|
|
52
55
|
"requirementsDiscovery.systemInterpretation",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"""
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
import hashlib
|
|
16
17
|
import json
|
|
17
18
|
import re
|
|
18
19
|
from copy import deepcopy
|
|
@@ -33,8 +34,8 @@ from .report_narrative import (
|
|
|
33
34
|
)
|
|
34
35
|
|
|
35
36
|
|
|
36
|
-
CORRECTION_KINDS = ("replace", "remove", "rewrite")
|
|
37
|
-
MECHANICAL_KINDS = frozenset({"replace", "remove"})
|
|
37
|
+
CORRECTION_KINDS = ("replace", "remove", "add", "move", "rewrite")
|
|
38
|
+
MECHANICAL_KINDS = frozenset({"replace", "remove", "add", "move"})
|
|
38
39
|
# okstra 가 렌더하는 절. 지시문 본문에 있으면 두 절이 갈라지므로 거절한다.
|
|
39
40
|
OKSTRA_OWNED_SECTIONS = ("## Corrections", "## Previous Attempt", "## Output")
|
|
40
41
|
|
|
@@ -57,7 +58,7 @@ class CorrectionsCheck:
|
|
|
57
58
|
|
|
58
59
|
ledger: dict[str, Any]
|
|
59
60
|
corrections: tuple[dict[str, Any], ...]
|
|
60
|
-
#
|
|
61
|
+
# 확정 값의 교정을 적용한 서사 자료. 기준 서사를 못 읽었으면 None.
|
|
61
62
|
scratch: dict[str, Any] | None
|
|
62
63
|
# 작성자 라운드 없이 끝나는가 — rewrite 가 없고 결함도 없다.
|
|
63
64
|
mechanical: bool
|
|
@@ -156,8 +157,8 @@ def _set(data: Any, tokens: Sequence[str | int], value: Any) -> None:
|
|
|
156
157
|
if not found:
|
|
157
158
|
raise KeyError(tokens)
|
|
158
159
|
last = tokens[-1]
|
|
159
|
-
if isinstance(last, int):
|
|
160
|
-
parent
|
|
160
|
+
if isinstance(last, int) and last == len(parent):
|
|
161
|
+
parent.append(value)
|
|
161
162
|
else:
|
|
162
163
|
parent[last] = value
|
|
163
164
|
|
|
@@ -194,6 +195,7 @@ def check_corrections(
|
|
|
194
195
|
schema: Mapping[str, Any],
|
|
195
196
|
block_rules: Sequence[str] = (),
|
|
196
197
|
semantic_validator: Callable[[dict[str, Any]], Sequence[str]] | None = None,
|
|
198
|
+
rewrite_results: Mapping[str, Any] | None = None,
|
|
197
199
|
) -> CorrectionsCheck:
|
|
198
200
|
"""원장을 기준 서사에 대조한다. 설계 §4.2 의 1~6단계.
|
|
199
201
|
|
|
@@ -204,11 +206,49 @@ def check_corrections(
|
|
|
204
206
|
적용 뒤에도 남아 있는 것만 보고한다.
|
|
205
207
|
3. 경로 해소와 `current` 일치. `rewrite` 는 배열의 `[len]` 추가 위치를 허용한다.
|
|
206
208
|
4. 작성자 소유 경로인가.
|
|
207
|
-
5.
|
|
209
|
+
5. 확정 값의 교정을 적용한 사본을 작성자 소유 값 스키마와 task 의미 검증기로
|
|
208
210
|
대조한다. rewrite 경로 아래로 특정된 결함은 뺀다(작성자가 다시 쓸 자리다).
|
|
209
211
|
6. rewrite 마다 그 경로의 스키마 제약 문장을 붙인다.
|
|
210
212
|
"""
|
|
211
|
-
ledger_dict = dict(ledger)
|
|
213
|
+
ledger_dict = deepcopy(dict(ledger))
|
|
214
|
+
digest = hashlib.sha256(base_narrative.encode("utf-8")).hexdigest()
|
|
215
|
+
expected_digest = ledger_dict.get("baseNarrativeSha256")
|
|
216
|
+
if expected_digest is not None and expected_digest != digest:
|
|
217
|
+
return CorrectionsCheck(ledger_dict, (), None, False, {}, (
|
|
218
|
+
_defect("ledger", "baseNarrativeSha256", "base narrative hash does not match"),
|
|
219
|
+
))
|
|
220
|
+
ledger_dict["baseNarrativeSha256"] = digest
|
|
221
|
+
corrections, parsed, defects = _parse_corrections(ledger_dict)
|
|
222
|
+
try:
|
|
223
|
+
data, _base_defects = parse_narrative_structure(base_narrative, schema)
|
|
224
|
+
except NarrativeContractError as exc:
|
|
225
|
+
defects.append(_defect("ledger", str(ledger_dict.get("baseNarrativePath") or "-"),
|
|
226
|
+
f"base narrative does not parse: {exc}"))
|
|
227
|
+
return CorrectionsCheck(ledger_dict, corrections, None, False, {}, tuple(defects))
|
|
228
|
+
if rewrite_results is not None:
|
|
229
|
+
ledger_dict, result_defects = _complete_rewrites(ledger_dict, rewrite_results, data)
|
|
230
|
+
defects.extend(result_defects)
|
|
231
|
+
corrections = tuple(ledger_dict["corrections"])
|
|
232
|
+
valid, path_defects = _checked_correction_paths(corrections, parsed, data)
|
|
233
|
+
defects.extend(path_defects)
|
|
234
|
+
scratch = _apply_mechanical_corrections(data, valid, parsed)
|
|
235
|
+
rewrites = [item for item in valid if item.get("kind") == "rewrite"]
|
|
236
|
+
derived = _recount_planning_steps(scratch, corrections, rewrites)
|
|
237
|
+
corrections = (*corrections, *derived)
|
|
238
|
+
ledger_dict["corrections"] = list(corrections)
|
|
239
|
+
constraints = {str(item["id"]): _constraints_for(str(item["path"]), block_rules, schema)
|
|
240
|
+
for item in rewrites}
|
|
241
|
+
errors = validate_writer_owned(scratch, schema)
|
|
242
|
+
if semantic_validator is not None:
|
|
243
|
+
errors.extend(semantic_validator(scratch))
|
|
244
|
+
defects.extend(_remaining_correction_defects(errors, rewrites))
|
|
245
|
+
return CorrectionsCheck(ledger_dict, corrections, scratch, not defects and not rewrites,
|
|
246
|
+
constraints, tuple(defects))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _parse_corrections(
|
|
250
|
+
ledger_dict: dict[str, Any],
|
|
251
|
+
) -> tuple[tuple[dict[str, Any], ...], dict[str, tuple[str | int, ...]], list[str]]:
|
|
212
252
|
defects: list[str] = [
|
|
213
253
|
_defect("ledger", error.split(": ", 1)[0], error)
|
|
214
254
|
for error in validate(ledger_dict, corrections_schema())
|
|
@@ -231,21 +271,16 @@ def check_corrections(
|
|
|
231
271
|
parsed[correction_id] = parse_field_path(path)
|
|
232
272
|
except ValueError as exc:
|
|
233
273
|
defects.append(_defect(correction_id, path, str(exc)))
|
|
234
|
-
|
|
235
|
-
return CorrectionsCheck(ledger_dict, corrections, None, False, {}, tuple(defects))
|
|
274
|
+
return corrections, parsed, defects
|
|
236
275
|
|
|
237
|
-
try:
|
|
238
|
-
data, _base_defects = parse_narrative_structure(base_narrative, schema)
|
|
239
|
-
except NarrativeContractError as exc:
|
|
240
|
-
defects.append(_defect(
|
|
241
|
-
"ledger", str(ledger_dict.get("baseNarrativePath") or "-"),
|
|
242
|
-
f"base narrative does not parse: {exc}",
|
|
243
|
-
))
|
|
244
|
-
return CorrectionsCheck(ledger_dict, corrections, None, False, {}, tuple(defects))
|
|
245
276
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
277
|
+
def _checked_correction_paths(
|
|
278
|
+
corrections: Sequence[dict[str, Any]],
|
|
279
|
+
parsed: Mapping[str, tuple[str | int, ...]],
|
|
280
|
+
data: dict[str, Any],
|
|
281
|
+
) -> tuple[list[dict[str, Any]], list[str]]:
|
|
282
|
+
valid: list[dict[str, Any]] = []
|
|
283
|
+
defects: list[str] = []
|
|
249
284
|
for item in corrections:
|
|
250
285
|
correction_id, path, kind = str(item.get("id")), str(item.get("path")), str(item.get("kind"))
|
|
251
286
|
tokens = parsed.get(correction_id)
|
|
@@ -255,72 +290,147 @@ def check_corrections(
|
|
|
255
290
|
if owned_defect:
|
|
256
291
|
defects.append(_defect(correction_id, path, owned_defect))
|
|
257
292
|
continue
|
|
258
|
-
|
|
259
|
-
if
|
|
260
|
-
|
|
261
|
-
appendable = (
|
|
262
|
-
kind == "rewrite"
|
|
263
|
-
and isinstance(tokens[-1], int)
|
|
264
|
-
and parent_found
|
|
265
|
-
and isinstance(parent, list)
|
|
266
|
-
and tokens[-1] == len(parent)
|
|
267
|
-
)
|
|
268
|
-
if not appendable:
|
|
269
|
-
defects.append(_defect(
|
|
270
|
-
correction_id, path,
|
|
271
|
-
"path does not resolve in the base narrative"
|
|
272
|
-
+ (" (a rewrite may append at index len(array))" if kind == "rewrite" else ""),
|
|
273
|
-
))
|
|
274
|
-
continue
|
|
275
|
-
elif "current" in item and not _current_matches(item["current"], value):
|
|
276
|
-
defects.append(_defect(
|
|
277
|
-
correction_id, path,
|
|
278
|
-
f"current value is {_short(value)}, not {_short(item['current'])}",
|
|
279
|
-
))
|
|
293
|
+
reason = _correction_path_defect(item, tokens, data)
|
|
294
|
+
if reason:
|
|
295
|
+
defects.append(_defect(correction_id, path, reason))
|
|
280
296
|
continue
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
297
|
+
valid.append(item)
|
|
298
|
+
return valid, defects
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _correction_path_defect(
|
|
302
|
+
item: Mapping[str, Any], tokens: Sequence[str | int], data: dict[str, Any],
|
|
303
|
+
) -> str | None:
|
|
304
|
+
found, value = _resolve(data, tokens)
|
|
305
|
+
kind = item.get("kind")
|
|
306
|
+
parent_found, parent = _parent(data, tokens)
|
|
307
|
+
appendable = parent_found and (
|
|
308
|
+
(isinstance(parent, dict) and isinstance(tokens[-1], str))
|
|
309
|
+
or (isinstance(parent, list) and tokens[-1] == len(parent))
|
|
310
|
+
)
|
|
311
|
+
if found and kind in {"add", "move"}:
|
|
312
|
+
return "destination already exists; use replace to change an existing value"
|
|
313
|
+
if not found and not (kind in {"add", "move", "rewrite"} and appendable):
|
|
314
|
+
return "path does not resolve in the base narrative"
|
|
315
|
+
if kind == "move":
|
|
316
|
+
try:
|
|
317
|
+
source = parse_field_path(str(item.get("fromPath") or ""))
|
|
318
|
+
except ValueError as exc:
|
|
319
|
+
return f"invalid fromPath: {exc}"
|
|
320
|
+
owned_defect = writer_owned_path_defect(str(item["fromPath"]))
|
|
321
|
+
if owned_defect:
|
|
322
|
+
return f"fromPath {owned_defect}"
|
|
323
|
+
if source == tuple(tokens[:len(source)]):
|
|
324
|
+
return "move destination must not be inside its source"
|
|
325
|
+
if any(isinstance(token, int) for token in (*source, *tokens)):
|
|
326
|
+
return "move supports object fields only; use add/remove for array items"
|
|
327
|
+
found, value = _resolve(data, source)
|
|
328
|
+
if not found:
|
|
329
|
+
return "fromPath does not resolve in the base narrative"
|
|
330
|
+
if found and "current" in item and not _current_matches(item["current"], value):
|
|
331
|
+
return f"current value is {_short(value)}, not {_short(item['current'])}"
|
|
332
|
+
return None
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _apply_mechanical_corrections(
|
|
336
|
+
data: dict[str, Any], corrections: Sequence[dict[str, Any]],
|
|
337
|
+
parsed: Mapping[str, tuple[str | int, ...]],
|
|
338
|
+
) -> dict[str, Any]:
|
|
339
|
+
scratch = deepcopy(data)
|
|
340
|
+
removals: list[tuple[str | int, ...]] = []
|
|
341
|
+
for item in corrections:
|
|
342
|
+
tokens = parsed[str(item["id"])]
|
|
343
|
+
if item.get("kind") in {"replace", "add"}:
|
|
344
|
+
_set(scratch, tokens, deepcopy(item.get("replacement")))
|
|
345
|
+
elif item.get("kind") == "move":
|
|
346
|
+
source = parse_field_path(str(item["fromPath"]))
|
|
347
|
+
_set(scratch, tokens, deepcopy(_resolve(data, source)[1]))
|
|
348
|
+
removals.append(source)
|
|
349
|
+
elif item.get("kind") == "remove":
|
|
350
|
+
removals.append(tokens)
|
|
351
|
+
for tokens in sorted(set(removals), key=lambda ts: tuple(map(_removal_order, ts)), reverse=True):
|
|
352
|
+
if _resolve(scratch, tokens)[0]:
|
|
353
|
+
_remove(scratch, tokens)
|
|
354
|
+
return scratch
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _remaining_correction_defects(
|
|
358
|
+
errors: Sequence[str], rewrites: Sequence[dict[str, Any]],
|
|
359
|
+
) -> list[str]:
|
|
360
|
+
defects: list[str] = []
|
|
361
|
+
for error in errors:
|
|
362
|
+
location, separator, _ = error.partition(": ")
|
|
363
|
+
location = location if separator else "-"
|
|
364
|
+
if any(location == item["path"] or location.startswith(f"{item['path']}.")
|
|
365
|
+
or location.startswith(f"{item['path']}[") for item in rewrites):
|
|
293
366
|
continue
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
367
|
+
defects.append(_defect("applied", location, error))
|
|
368
|
+
return defects
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _recount_planning_steps(
|
|
372
|
+
scratch: dict[str, Any], corrections: Sequence[dict[str, Any]],
|
|
373
|
+
rewrites: Sequence[dict[str, Any]],
|
|
374
|
+
) -> tuple[dict[str, Any], ...]:
|
|
375
|
+
planning = scratch.get("implementationPlanning")
|
|
376
|
+
if not isinstance(planning, dict) or any(
|
|
377
|
+
str(item["path"]).startswith("implementationPlanning") for item in rewrites
|
|
299
378
|
):
|
|
300
|
-
|
|
301
|
-
|
|
379
|
+
return ()
|
|
380
|
+
stages, stage_map = planning.get("stages"), planning.get("stageMap")
|
|
381
|
+
if not isinstance(stages, list) or not isinstance(stage_map, list):
|
|
382
|
+
return ()
|
|
383
|
+
derived: list[dict[str, Any]] = []
|
|
384
|
+
used = {item.get("id") for item in corrections}
|
|
385
|
+
for index, row in enumerate(stage_map):
|
|
386
|
+
if not isinstance(row, dict):
|
|
302
387
|
continue
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
return
|
|
322
|
-
|
|
323
|
-
|
|
388
|
+
matches = [stage for stage in stages if isinstance(stage, dict)
|
|
389
|
+
and stage.get("stage") == row.get("stage")]
|
|
390
|
+
if len(matches) != 1 or not isinstance(matches[0].get("stepwiseExecution"), list):
|
|
391
|
+
continue
|
|
392
|
+
steps = matches[0]["stepwiseExecution"]
|
|
393
|
+
if not all(isinstance(step, dict) for step in steps) or row.get("stepCount") == len(steps):
|
|
394
|
+
continue
|
|
395
|
+
correction_id = next((f"RC-{n:03d}" for n in range(1, 1000) if f"RC-{n:03d}" not in used), None)
|
|
396
|
+
if correction_id is None:
|
|
397
|
+
break
|
|
398
|
+
used.add(correction_id)
|
|
399
|
+
derived.append({
|
|
400
|
+
"id": correction_id, "kind": "replace" if "stepCount" in row else "add",
|
|
401
|
+
"path": f"implementationPlanning.stageMap[{index}].stepCount",
|
|
402
|
+
"replacement": len(steps), "reason": "Derived from the matching stage's stepwiseExecution rows",
|
|
403
|
+
**({"current": row["stepCount"]} if "stepCount" in row else {}),
|
|
404
|
+
})
|
|
405
|
+
row["stepCount"] = len(steps)
|
|
406
|
+
return tuple(derived)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _complete_rewrites(
|
|
410
|
+
ledger: dict[str, Any], results: Mapping[str, Any], data: dict[str, Any],
|
|
411
|
+
) -> tuple[dict[str, Any], list[str]]:
|
|
412
|
+
errors = validate(dict(results), corrections_schema()["$defs"]["RewriteResults"])
|
|
413
|
+
if errors:
|
|
414
|
+
return ledger, [_defect("rewrite-results", "-", error) for error in errors]
|
|
415
|
+
if results.get("baseNarrativeSha256") != ledger["baseNarrativeSha256"]:
|
|
416
|
+
errors.append("base narrative hash does not match")
|
|
417
|
+
rows = results.get("replacements")
|
|
418
|
+
rows = rows if isinstance(rows, list) else []
|
|
419
|
+
ids = [row.get("id") for row in rows if isinstance(row, dict)]
|
|
420
|
+
expected = {item["id"] for item in ledger["corrections"] if item.get("kind") == "rewrite"}
|
|
421
|
+
if len(ids) != len(set(ids)) or set(ids) != expected:
|
|
422
|
+
errors.append("replacement ids must name every rewrite exactly once and no other correction")
|
|
423
|
+
if errors:
|
|
424
|
+
return ledger, [_defect("rewrite-results", "-", error) for error in errors]
|
|
425
|
+
completed = deepcopy(ledger)
|
|
426
|
+
replacements = {row["id"]: row["replacement"] for row in rows}
|
|
427
|
+
for item in completed["corrections"]:
|
|
428
|
+
if item.get("kind") != "rewrite":
|
|
429
|
+
continue
|
|
430
|
+
found, _value = _resolve(data, parse_field_path(item["path"]))
|
|
431
|
+
item["kind"] = "replace" if found else "add"
|
|
432
|
+
item["replacement"] = replacements[item["id"]]
|
|
433
|
+
return completed, []
|
|
324
434
|
|
|
325
435
|
|
|
326
436
|
def _constraints_for(
|
|
@@ -348,11 +458,10 @@ def _constraints_for(
|
|
|
348
458
|
def render_applied_narrative(
|
|
349
459
|
check: CorrectionsCheck, schema: Mapping[str, Any],
|
|
350
460
|
) -> str:
|
|
351
|
-
"""
|
|
461
|
+
"""확정 값으로 구성된 원장을 적용한 서사 본문.
|
|
352
462
|
|
|
353
|
-
|
|
354
|
-
있거나 `rewrite` 가 섞인
|
|
355
|
-
써야 할 자리가 있다.
|
|
463
|
+
기계적 교정이나 작성자가 제출한 교체 값의 대조를 마친 문서다. 결함이
|
|
464
|
+
있거나 아직 교체 값이 제출되지 않은 `rewrite` 가 섞인 결과는 받지 않는다.
|
|
356
465
|
"""
|
|
357
466
|
if not check.mechanical or check.scratch is None:
|
|
358
467
|
pending = [
|
|
@@ -363,8 +472,8 @@ def render_applied_narrative(
|
|
|
363
472
|
list(check.defects)
|
|
364
473
|
or [_defect(
|
|
365
474
|
", ".join(pending) or "-", "-",
|
|
366
|
-
"rewrite entries need a writer round;
|
|
367
|
-
"
|
|
475
|
+
"rewrite entries need a writer round; supply rewrite results "
|
|
476
|
+
"or use only replace, remove, add and move entries",
|
|
368
477
|
)]
|
|
369
478
|
)
|
|
370
479
|
return render_narrative(check.scratch, schema)
|
|
@@ -398,10 +507,11 @@ def render_corrections_section(
|
|
|
398
507
|
lines = [
|
|
399
508
|
"## Corrections",
|
|
400
509
|
"",
|
|
401
|
-
f"Your previous attempt is preserved at `{base_narrative_rel}`.
|
|
402
|
-
"
|
|
403
|
-
"
|
|
404
|
-
f"
|
|
510
|
+
f"Your previous attempt is preserved at `{base_narrative_rel}`. "
|
|
511
|
+
"The runtime preserves all fields outside the corrections. Read only the target "
|
|
512
|
+
"values and cited evidence below; expand to related source sections when needed. "
|
|
513
|
+
f"Corrections ledger: `{corrections_rel}`.",
|
|
514
|
+
f"Base narrative SHA-256: `{check.ledger['baseNarrativeSha256']}`.",
|
|
405
515
|
"",
|
|
406
516
|
]
|
|
407
517
|
for item in check.corrections:
|
|
@@ -413,15 +523,21 @@ def render_corrections_section(
|
|
|
413
523
|
except ValueError:
|
|
414
524
|
label = f"`{path}`"
|
|
415
525
|
head = f"- {correction_id} `{kind}` — {label} (`{path}`): "
|
|
416
|
-
if kind
|
|
526
|
+
if kind in {"replace", "add"}:
|
|
417
527
|
current = (
|
|
418
528
|
f"current {_scalar_text(item['current'])}, " if "current" in item else ""
|
|
419
529
|
)
|
|
420
530
|
action = f"{current}write exactly {_scalar_text(item.get('replacement'))}."
|
|
421
531
|
elif kind == "remove":
|
|
422
532
|
action = "remove this item or field entirely; renumber the remaining `- Item N` rows 1..N."
|
|
533
|
+
elif kind == "move":
|
|
534
|
+
action = f"move the unchanged value from `{item.get('fromPath')}`."
|
|
423
535
|
else:
|
|
424
536
|
action = f"rewrite this part so that: {item.get('rule')}"
|
|
537
|
+
found, value = _resolve(check.scratch, parse_field_path(path))
|
|
538
|
+
action += " Current value: " + (json.dumps(value, ensure_ascii=False) if found else "<absent>") + "."
|
|
539
|
+
if item.get("evidenceRefs"):
|
|
540
|
+
action += " Evidence: " + ", ".join(f"`{ref}`" for ref in item["evidenceRefs"]) + "."
|
|
425
541
|
constraint_lines = check.constraints.get(correction_id, ())
|
|
426
542
|
if constraint_lines:
|
|
427
543
|
action += " Schema constraint: " + " ".join(constraint_lines)
|
|
@@ -597,8 +597,12 @@ def run_finalize(
|
|
|
597
597
|
|
|
598
598
|
if only:
|
|
599
599
|
selected = set(only)
|
|
600
|
-
if ctx.report_contract_version == "3.0"
|
|
601
|
-
selected
|
|
600
|
+
if ctx.report_contract_version == "3.0":
|
|
601
|
+
if STEP_TRANSLATE in selected:
|
|
602
|
+
selected.add(STEP_PREFLIGHT)
|
|
603
|
+
if selected - {STEP_TOKEN_USAGE}:
|
|
604
|
+
# 부분 재개도 이전 조립본이 아닌 현재 소유자 입력을 소비한다.
|
|
605
|
+
selected.add(STEP_PROJECT_ACTIVITY)
|
|
602
606
|
contract_order = (
|
|
603
607
|
V3_STEP_ORDER if ctx.report_contract_version == "3.0" else STEP_ORDER
|
|
604
608
|
)
|
|
@@ -644,13 +648,15 @@ def _execute_finalize_steps(
|
|
|
644
648
|
steps: list[dict[str, Any]] = []
|
|
645
649
|
first_failure = ""
|
|
646
650
|
validated = False
|
|
647
|
-
|
|
651
|
+
blocked: dict[str, str] = {}
|
|
648
652
|
for name, command in commands:
|
|
649
653
|
# 실패한 시퀀스가 worktree 를 거두면 재작업 대상이 사라지고, 검증 안 된
|
|
650
654
|
# 레코드를 기억하면 형제 task 가 그것을 결론으로 읽는다.
|
|
651
|
-
if name in _SKIPPED_AFTER_FAILURE and first_failure:
|
|
655
|
+
if name in _SKIPPED_AFTER_FAILURE and first_failure and name not in blocked:
|
|
652
656
|
continue
|
|
653
|
-
if name
|
|
657
|
+
if name in blocked:
|
|
658
|
+
steps.append({"name": name, "status": "skipped", "exitCode": None,
|
|
659
|
+
"blockedBy": blocked[name]})
|
|
654
660
|
continue
|
|
655
661
|
if before_step is not None:
|
|
656
662
|
before_step(name)
|
|
@@ -663,8 +669,16 @@ def _execute_finalize_steps(
|
|
|
663
669
|
detail="\n".join(filter(None, [result.stderr, result.stdout])) or f"{name} exited {result.returncode}",
|
|
664
670
|
)
|
|
665
671
|
steps.append(step)
|
|
666
|
-
if
|
|
667
|
-
|
|
672
|
+
if result.returncode != 0:
|
|
673
|
+
if name == STEP_PROJECT_ACTIVITY:
|
|
674
|
+
for consumer, _ in commands:
|
|
675
|
+
if consumer != STEP_TOKEN_USAGE or ctx.report_contract_version != "3.0":
|
|
676
|
+
blocked[consumer] = name
|
|
677
|
+
elif name == STEP_PREFLIGHT:
|
|
678
|
+
blocked[STEP_TRANSLATE] = name
|
|
679
|
+
blocked[STEP_SPAWN_FOLLOWUPS] = name
|
|
680
|
+
elif name == STEP_RENDER_VIEWS:
|
|
681
|
+
blocked[STEP_VALIDATE_RUN] = name
|
|
668
682
|
if result.returncode != 0 and not first_failure:
|
|
669
683
|
first_failure = f"{name} failed with exit code {result.returncode}"
|
|
670
684
|
if name == STEP_VALIDATE_RUN and result.returncode == 0:
|
|
@@ -1106,6 +1120,9 @@ def _step_summary_lines(
|
|
|
1106
1120
|
lines = []
|
|
1107
1121
|
for step in result.get("steps") or []:
|
|
1108
1122
|
code = step.get("exitCode")
|
|
1123
|
+
if step.get("status") == "skipped":
|
|
1124
|
+
lines.append(f" [skip] {step.get('name')} (blocked by {step.get('blockedBy')})")
|
|
1125
|
+
continue
|
|
1109
1126
|
mark = "ok " if code == 0 else "FAIL"
|
|
1110
1127
|
lines.append(f" [{mark}] {step.get('name')} (exit {code})")
|
|
1111
1128
|
for name in order:
|
|
@@ -1129,7 +1146,7 @@ def _recovery_step_names(
|
|
|
1129
1146
|
failed = {
|
|
1130
1147
|
string_value(step.get("name"))
|
|
1131
1148
|
for step in (result.get("steps") or [])
|
|
1132
|
-
if step.get("exitCode")
|
|
1149
|
+
if step.get("exitCode") not in (None, 0)
|
|
1133
1150
|
}
|
|
1134
1151
|
for index, name in enumerate(order):
|
|
1135
1152
|
if name in failed:
|
|
@@ -5,6 +5,7 @@ from ..report_contract import html_template_for
|
|
|
5
5
|
from .models import HtmlRoute
|
|
6
6
|
from .view_models.change_impact_analysis import build_change_impact_analysis_view
|
|
7
7
|
from .view_models.error_analysis import build_error_analysis_view
|
|
8
|
+
from .view_models.technical_verification import build_technical_verification_view
|
|
8
9
|
from .view_models.feature_analysis import build_feature_analysis_view
|
|
9
10
|
from .view_models.final_verification import build_final_verification_view
|
|
10
11
|
from .view_models.improvement_discovery import build_improvement_discovery_view
|
|
@@ -23,6 +24,7 @@ class HtmlRenderError(ValueError):
|
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
VIEW_BUILDERS = {
|
|
27
|
+
"technical-verification": build_technical_verification_view,
|
|
26
28
|
"requirements-discovery": build_requirements_discovery_view,
|
|
27
29
|
"improvement-discovery": build_improvement_discovery_view,
|
|
28
30
|
"error-analysis": build_error_analysis_view,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""기술 검증 입력과 관측 결과의 보고서 표시."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..common import evidence_index
|
|
6
|
+
from ..models import HumanReportView
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def build_technical_verification_view(data: dict) -> HumanReportView:
|
|
10
|
+
anchored = ("technicalVerification.checks",)
|
|
11
|
+
return HumanReportView(
|
|
12
|
+
"technical-verification",
|
|
13
|
+
"html/tasks/technical-verification.template.html",
|
|
14
|
+
{
|
|
15
|
+
"humanSummary": data["humanSummary"],
|
|
16
|
+
"verification": data["technicalVerification"],
|
|
17
|
+
"evidenceIndex": evidence_index(data, anchored),
|
|
18
|
+
},
|
|
19
|
+
(),
|
|
20
|
+
anchored_fields=anchored,
|
|
21
|
+
)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""역할별 실행 입력을 최종 리포트 조각으로 바꾸는 순수 투영."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
from okstra_token_usage.blocks import accounting_workers
|
|
5
|
+
|
|
4
6
|
import json
|
|
5
7
|
from copy import deepcopy
|
|
6
8
|
from typing import Any, Mapping, Sequence
|
|
@@ -148,8 +150,7 @@ def project_execution(
|
|
|
148
150
|
"role": "Okstra lead",
|
|
149
151
|
}
|
|
150
152
|
lead_usage = team_state.get("leadUsage")
|
|
151
|
-
|
|
152
|
-
worker_rows = workers if isinstance(workers, list) else []
|
|
153
|
+
worker_rows = accounting_workers(team_state)
|
|
153
154
|
result: dict[str, Any] = {
|
|
154
155
|
"executionStatus": [
|
|
155
156
|
_execution_row(
|
|
@@ -181,7 +182,7 @@ def _usage_row(summary: Mapping[str, Any], prefix: str, cost: object) -> dict[st
|
|
|
181
182
|
|
|
182
183
|
def _worker_details(team_state: Mapping[str, Any]) -> list[dict[str, Any]]:
|
|
183
184
|
result = []
|
|
184
|
-
for worker in team_state
|
|
185
|
+
for worker in accounting_workers(team_state):
|
|
185
186
|
if not isinstance(worker, Mapping):
|
|
186
187
|
continue
|
|
187
188
|
usage = worker.get("usage") if isinstance(worker.get("usage"), Mapping) else {}
|