okstra 0.179.2 → 0.183.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli-registry.mjs +30 -2
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/incremental-carry.mjs +9 -8
- package/dist/commands/execute/incremental-carry.mjs.map +1 -1
- package/dist/commands/execute/plan-verify.mjs +3 -1
- package/dist/commands/execute/plan-verify.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.d.mts +4 -2
- package/dist/commands/execute/render-bundle.mjs +46 -5
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/dist/commands/execute/run.mjs +11 -3
- package/dist/commands/execute/run.mjs.map +1 -1
- package/dist/commands/inspect/model-io.d.mts +1 -0
- package/dist/commands/inspect/model-io.mjs +25 -0
- package/dist/commands/inspect/model-io.mjs.map +1 -0
- package/dist/commands/inspect/stage-map.mjs +29 -8
- package/dist/commands/inspect/stage-map.mjs.map +1 -1
- package/dist/commands/inspect/task-list.mjs +52 -6
- package/dist/commands/inspect/task-list.mjs.map +1 -1
- package/dist/commands/inspect/user-response.mjs +14 -4
- package/dist/commands/inspect/user-response.mjs.map +1 -1
- package/dist/commands/lifecycle/check-project.d.mts +1 -0
- package/dist/commands/lifecycle/check-project.mjs +69 -50
- package/dist/commands/lifecycle/check-project.mjs.map +1 -1
- package/dist/commands/lifecycle/contract-check.d.mts +1 -0
- package/dist/commands/lifecycle/contract-check.mjs +18 -0
- package/dist/commands/lifecycle/contract-check.mjs.map +1 -0
- package/dist/commands/lifecycle/preflight.mjs +154 -51
- package/dist/commands/lifecycle/preflight.mjs.map +1 -1
- package/dist/commands/pr/pr.d.mts +1 -0
- package/dist/commands/pr/pr.mjs +19 -1
- package/dist/commands/pr/pr.mjs.map +1 -1
- package/dist/commands/report/agent-activity.mjs +2 -2
- package/dist/commands/report/approval-decision.d.mts +1 -0
- package/dist/commands/report/approval-decision.mjs +21 -0
- package/dist/commands/report/approval-decision.mjs.map +1 -0
- package/dist/commands/report/design-snapshot.d.mts +1 -0
- package/dist/commands/report/design-snapshot.mjs +19 -0
- package/dist/commands/report/design-snapshot.mjs.map +1 -0
- package/dist/commands/report/translate.mjs +3 -0
- package/dist/commands/report/translate.mjs.map +1 -1
- package/dist/lib/host-registry-client.mjs +13 -9
- package/dist/lib/host-registry-client.mjs.map +1 -1
- package/docs/architecture/storage-model.md +1 -1
- package/docs/architecture.md +21 -10
- package/docs/cli.md +36 -20
- package/docs/container.md +6 -4
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-brief-gen.md +5 -3
- package/docs/for-ai/skills/okstra-code-review.md +4 -4
- package/docs/for-ai/skills/okstra-container-build.md +20 -17
- package/docs/for-ai/skills/okstra-inspect.md +20 -23
- package/docs/for-ai/skills/okstra-manager.md +19 -18
- package/docs/for-ai/skills/okstra-memory.md +2 -2
- package/docs/for-ai/skills/okstra-pr-gen.md +3 -3
- package/docs/for-ai/skills/okstra-rollup.md +14 -13
- package/docs/for-ai/skills/okstra-run.md +7 -3
- package/docs/for-ai/skills/okstra-schedule-gen.md +15 -18
- package/docs/for-ai/skills/okstra-setup.md +7 -7
- package/docs/for-ai/skills/okstra-usage.md +5 -4
- package/docs/for-ai/skills/okstra-user-response.md +50 -32
- package/docs/project-structure-overview.md +43 -31
- package/docs/task-process/implementation-planning.md +2 -2
- package/docs/task-process/implementation.md +7 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +4 -4
- package/runtime/agents/workers/report-writer-worker.md +15 -164
- package/runtime/agents/workers/translator-worker.md +5 -13
- package/runtime/bin/okstra-error-log.py +51 -11
- package/runtime/bin/okstra-report-translate.py +210 -23
- package/runtime/prompts/host-orchestration/implementation.md +1 -1
- package/runtime/prompts/launch.template.md +9 -12
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/context-loader.md +41 -141
- package/runtime/prompts/lead/convergence.md +10 -8
- package/runtime/prompts/lead/okstra-lead-contract.md +40 -49
- package/runtime/prompts/lead/plan-body-verification.md +217 -23
- package/runtime/prompts/lead/report-writer.md +81 -421
- package/runtime/prompts/lead/team-contract.md +9 -54
- package/runtime/prompts/profiles/_clarification-recommendation.md +5 -4
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +3 -2
- package/runtime/prompts/profiles/_common-contract.md +4 -4
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
- package/runtime/prompts/profiles/change-impact-analysis.md +1 -1
- package/runtime/prompts/profiles/error-analysis.md +1 -1
- package/runtime/prompts/profiles/feature-analysis.md +1 -1
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +20 -12
- package/runtime/prompts/profiles/improvement-discovery.md +1 -1
- package/runtime/prompts/profiles/project-analysis.md +1 -1
- package/runtime/prompts/profiles/release-handoff.md +0 -1
- package/runtime/prompts/profiles/requirements-discovery.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +0 -1
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +69 -17
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +13 -4
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +8 -2
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +50 -5
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +66 -5
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +70 -2
- package/runtime/python/okstra_ctl/agent_activity.py +141 -38
- package/runtime/python/okstra_ctl/agent_invocation.py +19 -6
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +70 -23
- package/runtime/python/okstra_ctl/analysis_inputs.py +5 -4
- package/runtime/python/okstra_ctl/analysis_packet.py +124 -3
- package/runtime/python/okstra_ctl/approval_decisions.py +328 -0
- package/runtime/python/okstra_ctl/attempt_evidence.py +2 -2
- package/runtime/python/okstra_ctl/backfill.py +13 -10
- package/runtime/python/okstra_ctl/batch.py +2 -4
- package/runtime/python/okstra_ctl/build_tools.py +6 -3
- package/runtime/python/okstra_ctl/claim_reproduction.py +101 -0
- package/runtime/python/okstra_ctl/clarification_items.py +27 -13
- package/runtime/python/okstra_ctl/cmux.py +130 -52
- package/runtime/python/okstra_ctl/code_review_target.py +34 -8
- package/runtime/python/okstra_ctl/consumers.py +5 -4
- package/runtime/python/okstra_ctl/container.py +103 -8
- package/runtime/python/okstra_ctl/context_cost.py +2 -1
- package/runtime/python/okstra_ctl/contract_graph.py +497 -0
- package/runtime/python/okstra_ctl/contract_graph_cli.py +62 -0
- package/runtime/python/okstra_ctl/convergence.py +338 -17
- package/runtime/python/okstra_ctl/convergence_engine.py +10 -18
- package/runtime/python/okstra_ctl/convergence_provenance.py +58 -8
- package/runtime/python/okstra_ctl/convergence_store.py +55 -34
- package/runtime/python/okstra_ctl/design_prep.py +7 -4
- package/runtime/python/okstra_ctl/design_snapshot.py +134 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +97 -69
- package/runtime/python/okstra_ctl/dispatch_state.py +163 -63
- package/runtime/python/okstra_ctl/doctor.py +6 -3
- package/runtime/python/okstra_ctl/domain/worker_presentation.py +70 -9
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +16 -30
- package/runtime/python/okstra_ctl/error_log_write.py +35 -30
- package/runtime/python/okstra_ctl/error_report.py +26 -1
- package/runtime/python/okstra_ctl/error_zip.py +27 -5
- package/runtime/python/okstra_ctl/execution_identity.py +3 -2
- package/runtime/python/okstra_ctl/execution_manifest.py +7 -4
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -2
- package/runtime/python/okstra_ctl/final_report_schema.py +26 -17
- package/runtime/python/okstra_ctl/fix_cycles.py +2 -2
- package/runtime/python/okstra_ctl/fixed_text.py +39 -0
- package/runtime/python/okstra_ctl/git_reconcile.py +41 -9
- package/runtime/python/okstra_ctl/handoff.py +5 -4
- package/runtime/python/okstra_ctl/i18n.py +4 -2
- package/runtime/python/okstra_ctl/implementation_direction.py +22 -14
- package/runtime/python/okstra_ctl/implementation_outcome.py +4 -7
- package/runtime/python/okstra_ctl/incremental_carry.py +130 -17
- package/runtime/python/okstra_ctl/incremental_scope.py +12 -4
- package/runtime/python/okstra_ctl/index.py +8 -11
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +79 -7
- package/runtime/python/okstra_ctl/invocation.py +3 -6
- package/runtime/python/okstra_ctl/json_boundary.py +366 -0
- package/runtime/python/okstra_ctl/json_registry.py +10 -12
- package/runtime/python/okstra_ctl/jsonl.py +19 -2
- package/runtime/python/okstra_ctl/lead_events.py +33 -1
- package/runtime/python/okstra_ctl/listing.py +3 -3
- package/runtime/python/okstra_ctl/log_report.py +24 -2
- package/runtime/python/okstra_ctl/manager_cli.py +92 -7
- package/runtime/python/okstra_ctl/manager_store.py +12 -10
- package/runtime/python/okstra_ctl/material.py +5 -1
- package/runtime/python/okstra_ctl/migrate.py +29 -25
- package/runtime/python/okstra_ctl/model_cli.py +3 -15
- package/runtime/python/okstra_ctl/model_io_cli.py +1051 -0
- package/runtime/python/okstra_ctl/mutation_probe.py +13 -4
- package/runtime/python/okstra_ctl/pane_reclaim.py +3 -2
- package/runtime/python/okstra_ctl/path_hints.py +12 -0
- package/runtime/python/okstra_ctl/paths.py +21 -0
- package/runtime/python/okstra_ctl/plan_items.py +115 -4
- package/runtime/python/okstra_ctl/plan_items_cli.py +626 -34
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +2 -1
- package/runtime/python/okstra_ctl/pr_template.py +3 -2
- package/runtime/python/okstra_ctl/project_meta.py +5 -7
- package/runtime/python/okstra_ctl/recap.py +5 -4
- package/runtime/python/okstra_ctl/reconcile.py +21 -27
- package/runtime/python/okstra_ctl/registry/host_discovery.py +3 -2
- package/runtime/python/okstra_ctl/registry/provider_registry.py +3 -2
- package/runtime/python/okstra_ctl/render.py +74 -16
- package/runtime/python/okstra_ctl/render_final_report.py +10 -8
- package/runtime/python/okstra_ctl/report_assembly.py +509 -0
- package/runtime/python/okstra_ctl/report_contract.py +2 -1
- package/runtime/python/okstra_ctl/report_finalize.py +67 -32
- package/runtime/python/okstra_ctl/report_html/render.py +3 -2
- package/runtime/python/okstra_ctl/report_inputs.py +72 -0
- package/runtime/python/okstra_ctl/report_language.py +3 -2
- package/runtime/python/okstra_ctl/report_markdown.py +81 -8
- package/runtime/python/okstra_ctl/report_narrative.py +351 -0
- package/runtime/python/okstra_ctl/report_projections.py +265 -0
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +518 -0
- package/runtime/python/okstra_ctl/report_views.py +3 -2
- package/runtime/python/okstra_ctl/rollup.py +65 -4
- package/runtime/python/okstra_ctl/run.py +184 -65
- package/runtime/python/okstra_ctl/run_audit.py +3 -2
- package/runtime/python/okstra_ctl/run_context.py +6 -9
- package/runtime/python/okstra_ctl/run_index_row.py +2 -8
- package/runtime/python/okstra_ctl/schedule_semantics.py +5 -2
- package/runtime/python/okstra_ctl/schema_excerpt.py +15 -8
- package/runtime/python/okstra_ctl/session_transcript.py +27 -1
- package/runtime/python/okstra_ctl/set_work_status.py +64 -38
- package/runtime/python/okstra_ctl/stage_fix_carry.py +8 -6
- package/runtime/python/okstra_ctl/stage_ledger.py +132 -18
- package/runtime/python/okstra_ctl/stage_map.py +96 -28
- package/runtime/python/okstra_ctl/stage_targets.py +3 -4
- package/runtime/python/okstra_ctl/team.py +3 -2
- package/runtime/python/okstra_ctl/team_reconcile.py +11 -2
- package/runtime/python/okstra_ctl/time_report.py +51 -4
- package/runtime/python/okstra_ctl/usage_identity.py +2 -1
- package/runtime/python/okstra_ctl/usage_report.py +58 -4
- package/runtime/python/okstra_ctl/user_response.py +1431 -66
- package/runtime/python/okstra_ctl/wizard.py +11 -44
- package/runtime/python/okstra_ctl/work_categories.py +3 -2
- package/runtime/python/okstra_ctl/worker_dispatch.py +5 -2
- package/runtime/python/okstra_ctl/worker_prompt_body.py +49 -3
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -2
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +31 -3
- package/runtime/python/okstra_ctl/worker_runner.py +14 -12
- package/runtime/python/okstra_ctl/workflow.py +2 -1
- package/runtime/python/okstra_ctl/worktree.py +3 -2
- package/runtime/python/okstra_ctl/wrapper_status.py +4 -2
- package/runtime/python/okstra_ctl/write_policy.py +4 -2
- package/runtime/python/okstra_token_usage/antigravity.py +39 -12
- package/runtime/python/okstra_token_usage/collect.py +90 -38
- package/runtime/python/okstra_token_usage/grok.py +127 -0
- package/runtime/schemas/final-report-v2.0.schema.json +9 -0
- package/runtime/schemas/final-report-v3.0.schema.json +10219 -0
- package/runtime/schemas/report-narrative-v3.0.schema.json +30 -0
- package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +140 -0
- package/runtime/skills/okstra-brief-gen/SKILL.md +9 -7
- package/runtime/skills/okstra-code-review/SKILL.md +21 -11
- package/runtime/skills/okstra-container-build/SKILL.md +18 -18
- package/runtime/skills/okstra-inspect/SKILL.md +12 -11
- package/runtime/skills/okstra-inspect/facets/error-zip.md +8 -8
- package/runtime/skills/okstra-inspect/facets/errors.md +2 -2
- package/runtime/skills/okstra-inspect/facets/history.md +9 -14
- package/runtime/skills/okstra-inspect/facets/logs.md +2 -2
- package/runtime/skills/okstra-inspect/facets/recap.md +5 -5
- package/runtime/skills/okstra-inspect/facets/report.md +6 -10
- package/runtime/skills/okstra-inspect/facets/status.md +9 -8
- package/runtime/skills/okstra-inspect/facets/time.md +3 -3
- package/runtime/skills/okstra-manager/SKILL.md +16 -14
- package/runtime/skills/okstra-memory/SKILL.md +3 -3
- package/runtime/skills/okstra-pr-gen/SKILL.md +5 -4
- package/runtime/skills/okstra-rollup/SKILL.md +6 -16
- package/runtime/skills/okstra-run/SKILL.md +7 -7
- package/runtime/skills/okstra-schedule-gen/SKILL.md +21 -17
- package/runtime/skills/okstra-setup/SKILL.md +21 -13
- package/runtime/skills/okstra-setup/references/project-config.md +2 -2
- package/runtime/skills/okstra-usage/SKILL.md +10 -10
- package/runtime/skills/okstra-user-response/SKILL.md +78 -107
- package/runtime/templates/report-writer-prompt-preamble.md +29 -19
- package/runtime/templates/reports/html/macros/forms.html +6 -4
- package/runtime/templates/reports/schedule.template.md +4 -4
- package/runtime/templates/worker-error-contract.md +17 -29
- package/runtime/validators/validate-run.py +636 -72
- package/runtime/validators/validate_session_conformance.py +43 -7
|
@@ -15,7 +15,8 @@ class WorkerDispatchRequest:
|
|
|
15
15
|
workspace_root: Path | None = None
|
|
16
16
|
okstra_bin: Path | None = None
|
|
17
17
|
requested_workers: tuple[str, ...] = ()
|
|
18
|
-
|
|
18
|
+
# 기본은 "정하지 않음" 이다. 그래야 엔트리포인트가 역할의 예산을 쓴다.
|
|
19
|
+
idle_timeout_seconds: int | None = None
|
|
19
20
|
dispatch_kind: str = "initial"
|
|
20
21
|
jobs_file: Path | None = None
|
|
21
22
|
|
|
@@ -21,6 +21,7 @@ from dataclasses import dataclass
|
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
|
|
23
23
|
from okstra_project.dirs import okstra_home, project_json_path
|
|
24
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
24
25
|
|
|
25
26
|
_DEFAULT_FILENAME = "pr-body.template.md"
|
|
26
27
|
|
|
@@ -49,8 +50,8 @@ def _read_json_field(path: Path, field: str) -> str:
|
|
|
49
50
|
if not path.is_file():
|
|
50
51
|
return ""
|
|
51
52
|
try:
|
|
52
|
-
data =
|
|
53
|
-
except
|
|
53
|
+
data = load_owned_object(path, artifact="PR template configuration")
|
|
54
|
+
except JsonBoundaryError:
|
|
54
55
|
return ""
|
|
55
56
|
if not isinstance(data, dict):
|
|
56
57
|
return ""
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"""프로젝트 meta.json 업서트."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
4
|
from pathlib import Path
|
|
7
5
|
from typing import Optional
|
|
8
6
|
|
|
7
|
+
from .json_boundary import load_owned_object, write_owned_object_atomic
|
|
8
|
+
|
|
9
9
|
|
|
10
10
|
def _project_meta_path(home: Path, project_id: str) -> Path:
|
|
11
11
|
return home / "projects" / project_id / "meta.json"
|
|
@@ -16,7 +16,7 @@ def load_project_meta(home: Path, project_id: str) -> Optional[dict]:
|
|
|
16
16
|
p = _project_meta_path(home, project_id)
|
|
17
17
|
if not p.is_file():
|
|
18
18
|
return None
|
|
19
|
-
return
|
|
19
|
+
return load_owned_object(p, artifact="project metadata")
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def upsert_project_meta(home: Path, project_id: str, *,
|
|
@@ -29,7 +29,7 @@ def upsert_project_meta(home: Path, project_id: str, *,
|
|
|
29
29
|
target = _project_meta_path(home, project_id)
|
|
30
30
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
31
31
|
if target.is_file():
|
|
32
|
-
meta =
|
|
32
|
+
meta = load_owned_object(target, artifact="project metadata")
|
|
33
33
|
else:
|
|
34
34
|
meta = {
|
|
35
35
|
"projectId": project_id,
|
|
@@ -46,6 +46,4 @@ def upsert_project_meta(home: Path, project_id: str, *,
|
|
|
46
46
|
meta["activeCount"] = int(meta.get("activeCount", 0)) + 1
|
|
47
47
|
if finished:
|
|
48
48
|
meta["activeCount"] = max(0, int(meta.get("activeCount", 0)) - 1)
|
|
49
|
-
|
|
50
|
-
tmp.write_text(json.dumps(meta, indent=2) + "\n")
|
|
51
|
-
os.replace(tmp, target)
|
|
49
|
+
write_owned_object_atomic(target, meta, artifact="project metadata")
|
|
@@ -15,6 +15,8 @@ from okstra_ctl import next_phase
|
|
|
15
15
|
from okstra_ctl.clarification_items import sidecar_answers, user_response_sidecars
|
|
16
16
|
from okstra_ctl.ids import slugify_task_segment
|
|
17
17
|
from okstra_ctl.incremental_scope import preview_link_availability_for_report
|
|
18
|
+
from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
|
|
19
|
+
from okstra_ctl.jsonl import append_jsonl
|
|
18
20
|
from okstra_ctl.paths import task_timeline_file
|
|
19
21
|
from okstra_project import read_task_key
|
|
20
22
|
from okstra_ctl.run_context import dir_flock
|
|
@@ -29,8 +31,8 @@ def _load_timeline(task_root: Path) -> list[dict]:
|
|
|
29
31
|
if not path.is_file():
|
|
30
32
|
return []
|
|
31
33
|
try:
|
|
32
|
-
data =
|
|
33
|
-
except
|
|
34
|
+
data = load_owned_object(path, artifact="task timeline")
|
|
35
|
+
except (JsonBoundaryError, OSError):
|
|
34
36
|
return []
|
|
35
37
|
runs = data.get("runs", [])
|
|
36
38
|
return runs if isinstance(runs, list) else []
|
|
@@ -150,8 +152,7 @@ def append_recap_entry(task_root, project_root, *, kind, mode, question,
|
|
|
150
152
|
# 과 같은 append-only idiom — 데이터 fd 가 아닌 별도 .lock 파일을 잠가야
|
|
151
153
|
# flush 가 락 안에서 끝난다).
|
|
152
154
|
with dir_flock(recap_dir, ".recap-log.lock"):
|
|
153
|
-
|
|
154
|
-
fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
155
|
+
append_jsonl(log_path, entry, ensure_ascii=False, compact=False)
|
|
155
156
|
return {"logPath": project_rel(log_path, project_root), "entry": entry}
|
|
156
157
|
|
|
157
158
|
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
"""reconciliation/normalization."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
4
|
from datetime import datetime, timezone
|
|
7
5
|
from pathlib import Path
|
|
8
6
|
from typing import Optional
|
|
9
7
|
|
|
10
|
-
from .jsonl import append_jsonl, rotate_recent_if_needed
|
|
8
|
+
from .jsonl import append_jsonl, rewrite_jsonl, rotate_recent_if_needed
|
|
11
9
|
from .locks import central_lock
|
|
12
10
|
from .paths import resolve_under_root
|
|
13
11
|
from .project_meta import upsert_project_meta
|
|
14
12
|
from .run_index_row import read_run_index, slim_run_row
|
|
13
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
DEFAULT_ABORT_AFTER_SECONDS = 12 * 3600 # 12시간 무진척이면 aborted 로 간주
|
|
@@ -70,8 +69,8 @@ def _read_run_manifest_validation(project_root: Path, run_dir_rel: str,
|
|
|
70
69
|
if not manifest.is_file():
|
|
71
70
|
return "not-run"
|
|
72
71
|
try:
|
|
73
|
-
data =
|
|
74
|
-
except
|
|
72
|
+
data = load_owned_object(manifest, artifact="run manifest")
|
|
73
|
+
except JsonBoundaryError:
|
|
75
74
|
return "not-run"
|
|
76
75
|
validation = data.get("validation")
|
|
77
76
|
if isinstance(validation, dict):
|
|
@@ -94,11 +93,7 @@ def _sync_project_index(home: Path, project_id: str, updated_rows: list) -> None
|
|
|
94
93
|
src = by_id.get(er.get("runId"))
|
|
95
94
|
if src:
|
|
96
95
|
er.update({k: src[k] for k in ("status", "finishedAt", "validation")})
|
|
97
|
-
|
|
98
|
-
with tmp.open("w") as f:
|
|
99
|
-
for er in existing:
|
|
100
|
-
f.write(json.dumps(slim_run_row(er), separators=(",", ":")) + "\n")
|
|
101
|
-
os.replace(tmp, proj_index)
|
|
96
|
+
rewrite_jsonl(proj_index, [slim_run_row(row) for row in existing])
|
|
102
97
|
|
|
103
98
|
|
|
104
99
|
def reconcile_active(home: Path, *,
|
|
@@ -116,6 +111,20 @@ def reconcile_active(home: Path, *,
|
|
|
116
111
|
project=project)
|
|
117
112
|
|
|
118
113
|
|
|
114
|
+
def _publish_promoted(
|
|
115
|
+
home: Path, active: Path, survivors: list[dict], promoted: list[dict]
|
|
116
|
+
) -> None:
|
|
117
|
+
rewrite_jsonl(active, [slim_run_row(row) for row in survivors])
|
|
118
|
+
for row in promoted:
|
|
119
|
+
append_jsonl(home / "recent.jsonl", slim_run_row(row))
|
|
120
|
+
_sync_project_index(home, row["projectId"], [row])
|
|
121
|
+
upsert_project_meta(
|
|
122
|
+
home, row["projectId"], project_root=row["projectRoot"],
|
|
123
|
+
when=row["finishedAt"], finished=True,
|
|
124
|
+
)
|
|
125
|
+
rotate_recent_if_needed(home)
|
|
126
|
+
|
|
127
|
+
|
|
119
128
|
def _reconcile_active_locked(home: Path, *,
|
|
120
129
|
abort_after_seconds: int,
|
|
121
130
|
project: Optional[str] = None) -> dict:
|
|
@@ -171,18 +180,7 @@ def _reconcile_active_locked(home: Path, *,
|
|
|
171
180
|
summary["running"] += 1
|
|
172
181
|
if not promoted:
|
|
173
182
|
return summary
|
|
174
|
-
|
|
175
|
-
with tmp.open("w") as f:
|
|
176
|
-
for row in survivors:
|
|
177
|
-
f.write(json.dumps(slim_run_row(row), separators=(",", ":")) + "\n")
|
|
178
|
-
os.replace(tmp, active)
|
|
179
|
-
for row in promoted:
|
|
180
|
-
append_jsonl(home / "recent.jsonl", slim_run_row(row))
|
|
181
|
-
_sync_project_index(home, row["projectId"], [row])
|
|
182
|
-
upsert_project_meta(home, row["projectId"],
|
|
183
|
-
project_root=row["projectRoot"],
|
|
184
|
-
when=row["finishedAt"], finished=True)
|
|
185
|
-
rotate_recent_if_needed(home)
|
|
183
|
+
_publish_promoted(home, active, survivors, promoted)
|
|
186
184
|
return summary
|
|
187
185
|
|
|
188
186
|
|
|
@@ -225,11 +223,7 @@ def _reconcile_recent_locked(home: Path, *, project: Optional[str] = None) -> di
|
|
|
225
223
|
summary["completed" if new_status == "completed" else "failed"] += 1
|
|
226
224
|
if not updated_by_project:
|
|
227
225
|
return summary
|
|
228
|
-
|
|
229
|
-
with tmp.open("w") as f:
|
|
230
|
-
for row in rows:
|
|
231
|
-
f.write(json.dumps(slim_run_row(row), separators=(",", ":")) + "\n")
|
|
232
|
-
os.replace(tmp, recent)
|
|
226
|
+
rewrite_jsonl(recent, [slim_run_row(row) for row in rows])
|
|
233
227
|
for pid, urows in updated_by_project.items():
|
|
234
228
|
_sync_project_index(home, pid, urows)
|
|
235
229
|
return summary
|
|
@@ -7,6 +7,7 @@ from dataclasses import dataclass
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
9
|
from ..domain.host import HostAdapterContractError
|
|
10
|
+
from ..json_boundary import JsonBoundaryError, load_owned_object
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
@dataclass(frozen=True)
|
|
@@ -66,8 +67,8 @@ def _read_manifest(manifest_path: Path) -> HostManifest:
|
|
|
66
67
|
|
|
67
68
|
def _read_json_object(manifest_path: Path) -> dict[str, object]:
|
|
68
69
|
try:
|
|
69
|
-
raw =
|
|
70
|
-
except
|
|
70
|
+
raw = load_owned_object(manifest_path, artifact="host adapter manifest")
|
|
71
|
+
except JsonBoundaryError as exc:
|
|
71
72
|
raise HostAdapterContractError(
|
|
72
73
|
f"invalid host manifest: {manifest_path}"
|
|
73
74
|
) from exc
|
|
@@ -9,6 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
from okstra_project.dirs import okstra_home
|
|
10
10
|
|
|
11
11
|
from ..domain.provider import ProviderSpec, UnknownProviderError
|
|
12
|
+
from ..json_boundary import JsonBoundaryError, load_owned_object
|
|
12
13
|
from .factory_loader import FactoryLoadError, load_relative_factory
|
|
13
14
|
|
|
14
15
|
|
|
@@ -121,8 +122,8 @@ def _manifest_paths(roots: Iterable[Path]) -> tuple[Path, ...]:
|
|
|
121
122
|
|
|
122
123
|
def _read_manifest(manifest_path: Path) -> tuple[str, str]:
|
|
123
124
|
try:
|
|
124
|
-
raw =
|
|
125
|
-
except
|
|
125
|
+
raw = load_owned_object(manifest_path, artifact="provider adapter manifest")
|
|
126
|
+
except JsonBoundaryError as exc:
|
|
126
127
|
raise ProviderAdapterContractError(f"invalid provider manifest: {manifest_path}") from exc
|
|
127
128
|
if not isinstance(raw, dict) or raw.get("schemaVersion") != 1:
|
|
128
129
|
raise ProviderAdapterContractError(f"invalid provider manifest: {manifest_path}")
|
|
@@ -46,6 +46,8 @@ from .registry.host_registry import default_host_registry
|
|
|
46
46
|
from .registry.provider_registry import default_provider_registry
|
|
47
47
|
from .path_hints import compact_active_run_context, hydrate_run_context
|
|
48
48
|
from .paths import task_timeline_file
|
|
49
|
+
from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
|
|
50
|
+
from .report_contract import CURRENT_REPORT_SCHEMA_VERSION
|
|
49
51
|
from .worker_artifacts import artifacts_from_context
|
|
50
52
|
from .workflow import PHASE_SEQUENCE
|
|
51
53
|
|
|
@@ -73,7 +75,9 @@ def _load_ctx(ctx_arg: str) -> dict:
|
|
|
73
75
|
s = ctx_arg.lstrip()
|
|
74
76
|
if s.startswith("{"):
|
|
75
77
|
return hydrate_run_context(json.loads(ctx_arg))
|
|
76
|
-
return hydrate_run_context(
|
|
78
|
+
return hydrate_run_context(
|
|
79
|
+
load_owned_object(Path(ctx_arg), artifact="render context")
|
|
80
|
+
)
|
|
77
81
|
|
|
78
82
|
|
|
79
83
|
def _write_text(path: Path, text: str) -> None:
|
|
@@ -82,7 +86,7 @@ def _write_text(path: Path, text: str) -> None:
|
|
|
82
86
|
|
|
83
87
|
|
|
84
88
|
def _write_json(path: Path, payload: dict) -> None:
|
|
85
|
-
|
|
89
|
+
write_owned_object_atomic(path, payload, artifact="rendered runtime object")
|
|
86
90
|
|
|
87
91
|
|
|
88
92
|
def _execution_identity(ctx: Mapping[str, object]) -> dict:
|
|
@@ -873,7 +877,7 @@ def render_task_catalog_discovery(output_path: str, ctx: dict) -> None:
|
|
|
873
877
|
entries = []
|
|
874
878
|
for manifest_path in sorted(tasks_root.rglob(TASK_MANIFEST_FILENAME)):
|
|
875
879
|
try:
|
|
876
|
-
manifest =
|
|
880
|
+
manifest = load_owned_object(manifest_path, artifact="task manifest")
|
|
877
881
|
except Exception:
|
|
878
882
|
continue
|
|
879
883
|
if not isinstance(manifest, dict):
|
|
@@ -891,7 +895,7 @@ def render_task_catalog_discovery(output_path: str, ctx: dict) -> None:
|
|
|
891
895
|
latest_run = {}
|
|
892
896
|
if timeline_file.is_file():
|
|
893
897
|
try:
|
|
894
|
-
payload =
|
|
898
|
+
payload = load_owned_object(timeline_file, artifact="task timeline")
|
|
895
899
|
except Exception:
|
|
896
900
|
payload = {}
|
|
897
901
|
runs = payload.get("runs", []) if isinstance(payload, dict) else []
|
|
@@ -981,7 +985,9 @@ def render_latest_task_discovery(output_path: str, ctx: dict) -> None:
|
|
|
981
985
|
task_manifest = {}
|
|
982
986
|
if task_manifest_path.exists():
|
|
983
987
|
try:
|
|
984
|
-
task_manifest =
|
|
988
|
+
task_manifest = load_owned_object(
|
|
989
|
+
task_manifest_path, artifact="task manifest"
|
|
990
|
+
)
|
|
985
991
|
except Exception:
|
|
986
992
|
task_manifest = {}
|
|
987
993
|
workflow = (
|
|
@@ -1343,7 +1349,7 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
|
1343
1349
|
existing = {}
|
|
1344
1350
|
if path.exists():
|
|
1345
1351
|
try:
|
|
1346
|
-
existing =
|
|
1352
|
+
existing = load_owned_object(path, artifact="task manifest")
|
|
1347
1353
|
except Exception:
|
|
1348
1354
|
existing = {}
|
|
1349
1355
|
reviewers = _resolve_workers(ctx)
|
|
@@ -1645,21 +1651,23 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
|
|
|
1645
1651
|
existing_run_manifest = {}
|
|
1646
1652
|
if run_manifest_exists:
|
|
1647
1653
|
try:
|
|
1648
|
-
loaded_run_manifest =
|
|
1649
|
-
run_manifest_file
|
|
1654
|
+
loaded_run_manifest = load_owned_object(
|
|
1655
|
+
run_manifest_file, artifact="run manifest"
|
|
1650
1656
|
)
|
|
1651
1657
|
existing_run_manifest = (
|
|
1652
1658
|
loaded_run_manifest
|
|
1653
1659
|
if isinstance(loaded_run_manifest, dict)
|
|
1654
1660
|
else {}
|
|
1655
1661
|
)
|
|
1656
|
-
except
|
|
1662
|
+
except JsonBoundaryError:
|
|
1657
1663
|
existing_run_manifest = {}
|
|
1658
1664
|
task_manifest_path = Path(ctx.get("TASK_MANIFEST_PATH", ""))
|
|
1659
1665
|
task_manifest = {}
|
|
1660
1666
|
if task_manifest_path.exists():
|
|
1661
1667
|
try:
|
|
1662
|
-
task_manifest =
|
|
1668
|
+
task_manifest = load_owned_object(
|
|
1669
|
+
task_manifest_path, artifact="task manifest"
|
|
1670
|
+
)
|
|
1663
1671
|
except Exception:
|
|
1664
1672
|
task_manifest = {}
|
|
1665
1673
|
reviewers = _resolve_workers(ctx)
|
|
@@ -1686,12 +1694,16 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
|
|
|
1686
1694
|
execution_identity = _execution_identity(ctx)
|
|
1687
1695
|
payload = {
|
|
1688
1696
|
"schemaVersion": execution_identity.get("schemaVersion", "1.0"),
|
|
1697
|
+
"reportContractVersion": CURRENT_REPORT_SCHEMA_VERSION,
|
|
1689
1698
|
"okstraVersion": ctx.get("OKSTRA_VERSION", ""),
|
|
1690
1699
|
"projectId": ctx.get("PROJECT_ID", ""),
|
|
1691
1700
|
"taskGroup": ctx.get("TASK_GROUP", ""),
|
|
1692
1701
|
"taskId": ctx.get("TASK_ID", ""),
|
|
1693
1702
|
"taskKey": ctx.get("TASK_KEY", ""),
|
|
1694
1703
|
"taskType": ctx.get("TASK_TYPE", ""),
|
|
1704
|
+
"runTimestamp": ctx.get("RUN_TIMESTAMP_ISO", ""),
|
|
1705
|
+
"reportLanguage": ctx.get("REPORT_LANGUAGE", "en") or "en",
|
|
1706
|
+
"leadModel": ctx.get("LEAD_MODEL", ""),
|
|
1695
1707
|
# Every other path in this manifest is project-relative; this is the
|
|
1696
1708
|
# anchor they resolve against. Convergence reads it to place run
|
|
1697
1709
|
# artifacts, so a manifest without it cannot seed a round.
|
|
@@ -1763,6 +1775,10 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
|
|
|
1763
1775
|
"workerPromptsDirectoryPath": ctx.get("RUN_PROMPTS_RELATIVE_PATH", ""),
|
|
1764
1776
|
"workerPromptPathByWorkerId": worker_prompt_paths,
|
|
1765
1777
|
"expectedReportRecordPath": ctx.get("FINAL_REPORT_RECORD_RELATIVE_PATH", ""),
|
|
1778
|
+
"reportNarrativePath": ctx.get("REPORT_WRITER_NARRATIVE_RELATIVE_PATH", ""),
|
|
1779
|
+
"approvalDecisionsPath": ctx.get("APPROVAL_DECISIONS_RELATIVE_PATH", ""),
|
|
1780
|
+
"designPreparationPath": ctx.get("DESIGN_PREPARATION_RELATIVE_PATH", ""),
|
|
1781
|
+
"planBodyVerificationPath": ctx.get("PLAN_BODY_VERIFICATION_STATE_RELATIVE_PATH", ""),
|
|
1766
1782
|
"expectedStatusPath": ctx.get("FINAL_STATUS_RELATIVE_PATH", ""),
|
|
1767
1783
|
"teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
|
|
1768
1784
|
"activeRunContextPath": ctx.get("ACTIVE_RUN_CONTEXT_RELATIVE_PATH", ""),
|
|
@@ -1847,7 +1863,10 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
|
|
|
1847
1863
|
payload["analysisScopeConfirmation"] = scope_confirmation
|
|
1848
1864
|
if ctx.get("FIX_CYCLE_ID"):
|
|
1849
1865
|
payload["fixCycleId"] = ctx["FIX_CYCLE_ID"]
|
|
1850
|
-
if
|
|
1866
|
+
if (
|
|
1867
|
+
CURRENT_REPORT_SCHEMA_VERSION == "3.0"
|
|
1868
|
+
or ctx.get("TASK_TYPE") == "implementation-planning"
|
|
1869
|
+
):
|
|
1851
1870
|
if not run_manifest_exists:
|
|
1852
1871
|
payload["activityContractVersion"] = 1
|
|
1853
1872
|
elif "activityContractVersion" in existing_run_manifest:
|
|
@@ -1864,6 +1883,27 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
|
|
|
1864
1883
|
payload.update(execution_identity)
|
|
1865
1884
|
_guard_invocation_manifest_rewrite(existing_run_manifest, payload)
|
|
1866
1885
|
_write_json(Path(run_manifest_path), payload)
|
|
1886
|
+
if CURRENT_REPORT_SCHEMA_VERSION == "3.0":
|
|
1887
|
+
_initialize_report_ledgers(ctx, payload)
|
|
1888
|
+
|
|
1889
|
+
|
|
1890
|
+
def _initialize_report_ledgers(ctx: Mapping[str, Any], manifest: Mapping[str, Any]) -> None:
|
|
1891
|
+
approval_value = str(ctx.get("APPROVAL_DECISIONS_PATH") or "")
|
|
1892
|
+
approval_path = Path(approval_value)
|
|
1893
|
+
if approval_value and not approval_path.is_file():
|
|
1894
|
+
_write_json(approval_path, {
|
|
1895
|
+
"schemaVersion": "1.0",
|
|
1896
|
+
"owner": "lead",
|
|
1897
|
+
"taskKey": manifest.get("taskKey"),
|
|
1898
|
+
"taskType": manifest.get("taskType"),
|
|
1899
|
+
"runSeq": (manifest.get("runSequencesByCategory") or {}).get("manifests"),
|
|
1900
|
+
"activeClarifications": [],
|
|
1901
|
+
"carriedDecisions": [],
|
|
1902
|
+
})
|
|
1903
|
+
activity_value = str(ctx.get("LEAD_EVENTS_PATH") or "")
|
|
1904
|
+
activity_path = Path(activity_value)
|
|
1905
|
+
if activity_value and not activity_path.is_file():
|
|
1906
|
+
_write_text(activity_path, "")
|
|
1867
1907
|
|
|
1868
1908
|
|
|
1869
1909
|
_MODEL_ASSIGNMENT_KEYS = {
|
|
@@ -1904,8 +1944,18 @@ _IMMUTABLE_INVOCATION_KEYS = (
|
|
|
1904
1944
|
"workerPromptPathByWorkerId",
|
|
1905
1945
|
"workerResultsDirectoryPath",
|
|
1906
1946
|
"expectedReportRecordPath",
|
|
1947
|
+
"reportNarrativePath",
|
|
1948
|
+
"approvalDecisionsPath",
|
|
1949
|
+
"designPreparationPath",
|
|
1950
|
+
"planBodyVerificationPath",
|
|
1907
1951
|
"reportTemplatePath",
|
|
1908
1952
|
)
|
|
1953
|
+
_V3_REPORT_PATH_KEYS = frozenset({
|
|
1954
|
+
"reportNarrativePath",
|
|
1955
|
+
"approvalDecisionsPath",
|
|
1956
|
+
"designPreparationPath",
|
|
1957
|
+
"planBodyVerificationPath",
|
|
1958
|
+
})
|
|
1909
1959
|
|
|
1910
1960
|
|
|
1911
1961
|
def _guard_invocation_manifest_rewrite(
|
|
@@ -1951,6 +2001,10 @@ def _validate_invocation_manifest(payload: Mapping[str, object]) -> None:
|
|
|
1951
2001
|
item
|
|
1952
2002
|
for item in _IMMUTABLE_INVOCATION_KEYS[4:]
|
|
1953
2003
|
if item != "workerPromptPathByWorkerId"
|
|
2004
|
+
and (
|
|
2005
|
+
item not in _V3_REPORT_PATH_KEYS
|
|
2006
|
+
or payload.get("reportContractVersion") == "3.0"
|
|
2007
|
+
)
|
|
1954
2008
|
):
|
|
1955
2009
|
if not payload.get(key):
|
|
1956
2010
|
raise ValueError(f"incomplete invocation manifest path: {key}")
|
|
@@ -2039,7 +2093,9 @@ def render_timeline(timeline_path: str, ctx: dict) -> None:
|
|
|
2039
2093
|
task_manifest = {}
|
|
2040
2094
|
if task_manifest_path.exists():
|
|
2041
2095
|
try:
|
|
2042
|
-
task_manifest =
|
|
2096
|
+
task_manifest = load_owned_object(
|
|
2097
|
+
task_manifest_path, artifact="task manifest"
|
|
2098
|
+
)
|
|
2043
2099
|
except Exception:
|
|
2044
2100
|
task_manifest = {}
|
|
2045
2101
|
reviewers = _resolve_workers(ctx)
|
|
@@ -2048,7 +2104,7 @@ def render_timeline(timeline_path: str, ctx: dict) -> None:
|
|
|
2048
2104
|
existing = {}
|
|
2049
2105
|
if path.exists():
|
|
2050
2106
|
try:
|
|
2051
|
-
existing =
|
|
2107
|
+
existing = load_owned_object(path, artifact="task timeline")
|
|
2052
2108
|
except Exception:
|
|
2053
2109
|
existing = {}
|
|
2054
2110
|
runs = existing.get("runs", [])
|
|
@@ -2143,7 +2199,9 @@ def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
|
|
|
2143
2199
|
task_manifest = {}
|
|
2144
2200
|
if task_manifest_path.exists():
|
|
2145
2201
|
try:
|
|
2146
|
-
task_manifest =
|
|
2202
|
+
task_manifest = load_owned_object(
|
|
2203
|
+
task_manifest_path, artifact="task manifest"
|
|
2204
|
+
)
|
|
2147
2205
|
except Exception:
|
|
2148
2206
|
task_manifest = {}
|
|
2149
2207
|
workflow = (
|
|
@@ -2338,8 +2396,8 @@ def build_available_mcp_servers_block(project_root: Path) -> str:
|
|
|
2338
2396
|
"""
|
|
2339
2397
|
config_path = project_json_path(project_root)
|
|
2340
2398
|
try:
|
|
2341
|
-
raw =
|
|
2342
|
-
except
|
|
2399
|
+
raw = load_owned_object(config_path, artifact="project config")
|
|
2400
|
+
except JsonBoundaryError:
|
|
2343
2401
|
return _NO_MCP_SERVERS_LINE
|
|
2344
2402
|
servers = raw.get("mcpServers") if isinstance(raw, dict) else None
|
|
2345
2403
|
if not isinstance(servers, list) or not servers:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The JSON SSOT lives next to the rendered markdown as
|
|
4
4
|
``final-report-<task-type>-<seq>.data.json``. Its ``schemaVersion`` selects the
|
|
5
|
-
matching final-report schema. Report
|
|
5
|
+
matching final-report schema. Report assembly writes the data.json in
|
|
6
6
|
Phase 6; this renderer + the matching Jinja2 template deterministically
|
|
7
7
|
produce the canonical AI-facing markdown.
|
|
8
8
|
|
|
@@ -49,6 +49,7 @@ from okstra_ctl.final_report_schema import (
|
|
|
49
49
|
load_schema_for_data,
|
|
50
50
|
validate as schema_validate,
|
|
51
51
|
)
|
|
52
|
+
from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
|
|
52
53
|
from okstra_ctl.i18n import I18nError, SUPPORTED_LANGS, load_dictionary, make_jinja_global
|
|
53
54
|
from okstra_ctl.md_table import UNESCAPED_PIPE_RE, to_cell_text
|
|
54
55
|
from okstra_ctl.models import UnknownModelError, resolve_model_metadata
|
|
@@ -66,6 +67,7 @@ from okstra_ctl.usage_cells import format_duration_ms, format_int, format_usd
|
|
|
66
67
|
|
|
67
68
|
TEMPLATE_BY_SCHEMA_VERSION = {
|
|
68
69
|
"2.0": ("templates", "reports", "final-report-v2.template.md"),
|
|
70
|
+
"3.0": ("templates", "reports", "final-report-v2.template.md"),
|
|
69
71
|
}
|
|
70
72
|
DEFAULT_TEMPLATE_REL = TEMPLATE_BY_SCHEMA_VERSION[CURRENT_REPORT_SCHEMA_VERSION]
|
|
71
73
|
|
|
@@ -601,8 +603,8 @@ def validate_report_language(data: dict) -> str:
|
|
|
601
603
|
if candidate == "auto":
|
|
602
604
|
raise FinalReportRenderError(
|
|
603
605
|
"reportLanguage 'auto' must be resolved by the lead before "
|
|
604
|
-
"the renderer runs;
|
|
605
|
-
"
|
|
606
|
+
"the renderer runs; report assembly copies the resolved 'en' or "
|
|
607
|
+
"'ko' from the run manifest into data.json.meta.reportLanguage."
|
|
606
608
|
)
|
|
607
609
|
if candidate not in SUPPORTED_LANGS:
|
|
608
610
|
raise FinalReportRenderError(
|
|
@@ -757,9 +759,9 @@ def _with_excerpt_drift_hint(
|
|
|
757
759
|
) -> FinalReportRenderError:
|
|
758
760
|
"""Name the contract drift behind a schema failure, when that is the cause.
|
|
759
761
|
|
|
760
|
-
A long run straddles its own runtime upgrade: the report-writer authors
|
|
761
|
-
against the excerpt frozen into the bundle at prep time, while
|
|
762
|
-
|
|
762
|
+
A long run straddles its own runtime upgrade: the report-writer authors its
|
|
763
|
+
narrative against the excerpt frozen into the bundle at prep time, while
|
|
764
|
+
assembly and the renderer validate against the installed schema. Without this the author sees only
|
|
763
765
|
`additional property ... not allowed` for a field the excerpt told it to
|
|
764
766
|
write, and has no way to tell a real mistake from a stale bundle.
|
|
765
767
|
|
|
@@ -802,8 +804,8 @@ def render_to_file(
|
|
|
802
804
|
if not data_path.is_file():
|
|
803
805
|
raise FinalReportRenderError(f"data file not found: {data_path}")
|
|
804
806
|
try:
|
|
805
|
-
data =
|
|
806
|
-
except
|
|
807
|
+
data = load_owned_object(data_path, artifact="final report record")
|
|
808
|
+
except JsonBoundaryError as exc:
|
|
807
809
|
raise FinalReportRenderError(f"invalid JSON in {data_path}: {exc}") from exc
|
|
808
810
|
|
|
809
811
|
# 템플릿은 data.json 위치가 아니라 이 모듈(설치본은 ~/.okstra/lib/python,
|