okstra 0.219.0 → 0.220.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 +5 -5
- package/dist/cli-registry.mjs +18 -10
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/lifecycle/config.mjs +2 -3
- package/dist/commands/lifecycle/config.mjs.map +1 -1
- package/dist/lib/runtime-payload.mjs +1 -2
- package/dist/lib/runtime-payload.mjs.map +1 -1
- package/docs/architecture/storage-model.md +1 -1
- package/docs/architecture.md +9 -7
- package/docs/cli.md +5 -3
- package/docs/project-structure-overview.md +3 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/launch.template.md +1 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +5 -4
- package/runtime/prompts/lead/report-writer.md +10 -17
- package/runtime/prompts/lead/team-contract.md +1 -2
- package/runtime/prompts/wizard/prompts.ko.json +0 -1
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +1 -1
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +0 -1
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +1 -1
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +0 -1
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +0 -1
- package/runtime/python/okstra_ctl/agent/invocation.py +1 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +2 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/jobs.py +0 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +2 -27
- package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +0 -4
- package/runtime/python/okstra_ctl/blocking_checks.py +8 -0
- package/runtime/python/okstra_ctl/convergence_provenance.py +8 -2
- package/runtime/python/okstra_ctl/correction_cli.py +59 -0
- package/runtime/python/okstra_ctl/correction_round.py +168 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +21 -349
- package/runtime/python/okstra_ctl/dispatch_state.py +0 -13
- package/runtime/python/okstra_ctl/domain/provider.py +1 -1
- package/runtime/python/okstra_ctl/domain/role.py +0 -6
- package/runtime/python/okstra_ctl/final_report_paths.py +0 -16
- package/runtime/python/okstra_ctl/i18n.py +2 -2
- package/runtime/python/okstra_ctl/path_hints.py +0 -4
- package/runtime/python/okstra_ctl/paths.py +0 -3
- package/runtime/python/okstra_ctl/phases/final_verification/profile.md +1 -0
- package/runtime/python/okstra_ctl/phases/final_verification/spec.md +2 -0
- package/runtime/python/okstra_ctl/phases/final_verification/target.py +17 -1
- package/runtime/python/okstra_ctl/phases/implementation/entry.py +12 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +30 -6
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-executor.md +1 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-verifier.md +2 -0
- package/runtime/python/okstra_ctl/phases/implementation/profile.md +2 -1
- package/runtime/python/okstra_ctl/phases/implementation/report_assets/implementation.template.html +6 -0
- package/runtime/python/okstra_ctl/phases/implementation/spec.md +6 -1
- package/runtime/python/okstra_ctl/phases/implementation/validation.py +62 -1
- package/runtime/python/okstra_ctl/phases/release_handoff/boundary.json +1 -1
- package/runtime/python/okstra_ctl/phases/release_handoff/operations.py +24 -3
- package/runtime/python/okstra_ctl/phases/release_handoff/profile.md +9 -9
- package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +4 -2
- package/runtime/python/okstra_ctl/render.py +0 -10
- package/runtime/python/okstra_ctl/render_final_report.py +4 -4
- package/runtime/python/okstra_ctl/report_finalize.py +37 -76
- package/runtime/python/okstra_ctl/report_html/render.py +4 -44
- package/runtime/python/okstra_ctl/report_language.py +3 -3
- package/runtime/python/okstra_ctl/report_prose_keys.py +153 -0
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +1 -1
- package/runtime/python/okstra_ctl/run.py +1 -21
- package/runtime/python/okstra_ctl/stage_done.py +183 -0
- package/runtime/python/okstra_ctl/stage_fail.py +114 -0
- package/runtime/python/okstra_ctl/stage_integrate.py +37 -2
- package/runtime/python/okstra_ctl/stage_rebase.py +189 -0
- package/runtime/python/okstra_ctl/stage_reopen.py +12 -8
- package/runtime/python/okstra_ctl/stage_targets.py +29 -62
- package/runtime/python/okstra_ctl/team.py +1 -6
- package/runtime/python/okstra_ctl/wizard/confirmation.py +0 -8
- package/runtime/python/okstra_ctl/wizard/registry.py +2 -3
- package/runtime/python/okstra_ctl/wizard/sources.py +10 -0
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +3 -12
- package/runtime/python/okstra_ctl/worktree/__init__.py +1 -1
- package/runtime/python/okstra_ctl/worktree/decisions.py +3 -4
- package/runtime/python/okstra_ctl/worktree/provision.py +27 -0
- package/runtime/python/okstra_ctl/worktree/rebase.py +108 -0
- package/runtime/python/okstra_ctl/worktree_registry.py +14 -0
- package/runtime/schemas/final-report-v3.0.schema.json +78 -0
- package/runtime/skills/_fragments/wizard-render-bundle.md +1 -1
- package/runtime/skills/okstra-discussion/SKILL.md +1 -1
- package/runtime/skills/okstra-run/SKILL.md +1 -1
- package/runtime/skills/okstra-setup/SKILL.md +2 -2
- package/runtime/skills/okstra-setup/references/project-config.md +2 -2
- package/runtime/templates/report-writer-prompt-preamble.md +1 -1
- package/runtime/templates/reports/html/i18n/en.json +6 -1
- package/runtime/templates/reports/html/i18n/ko.json +6 -1
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-run.py +3 -28
- package/runtime/agents/operations/report-translation.json +0 -6
- package/runtime/agents/roles/translator.json +0 -19
- package/runtime/bin/okstra-report-translate.py +0 -404
- package/runtime/prompts/duties/translator.json +0 -32
- package/runtime/python/okstra_ctl/report_translation.py +0 -509
- package/runtime/python/okstra_ctl/report_translation_dispatch.py +0 -356
- package/runtime/templates/translator-prompt-preamble.md +0 -36
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This file is the audience-specific contract for initial `analysis` workers. Read it end-to-end from the path selected by the dispatch prompt. Before doing work, also read the shared file named by `**Worker Error Contract Path:**`; that file is the only source for error-sidecar rules.
|
|
4
4
|
|
|
5
|
-
Your identity is the stored role execution (`executionLabel`). Canonical roles are `leader`, `analyser`, `critic`, `designer`, `planner`, `implementer`, `verifier`, `report-writer
|
|
5
|
+
Your identity is the stored role execution (`executionLabel`). Canonical roles are `leader`, `analyser`, `critic`, `designer`, `planner`, `implementer`, `verifier`, and `report-writer`. `lead` and `executor` are compatibility aliases only.
|
|
6
6
|
|
|
7
7
|
## Required reading
|
|
8
8
|
|
|
@@ -40,6 +40,7 @@ from okstra_ctl.phases.implementation.validation import (
|
|
|
40
40
|
_validate_verifier_reran_independently,
|
|
41
41
|
_validate_verifier_discrepancy_is_not_passed,
|
|
42
42
|
_validate_verifier_fail_blocks_verdict,
|
|
43
|
+
_validate_correction_rounds,
|
|
43
44
|
_validate_lead_fixes,
|
|
44
45
|
_validate_verifier_command_log_is_read_only as validate_verifier_command_log_is_read_only,
|
|
45
46
|
_validate_verifier_discrepancy_names_checklist_phase as validate_verifier_discrepancy_names_checklist_phase,
|
|
@@ -230,7 +231,6 @@ from okstra_ctl.workflow import ( # noqa: E402
|
|
|
230
231
|
)
|
|
231
232
|
from okstra_ctl.final_report_paths import ( # noqa: E402
|
|
232
233
|
final_report_data_path as _data_path_for,
|
|
233
|
-
translation_sidecar_path,
|
|
234
234
|
)
|
|
235
235
|
from okstra_token_usage.report import _match_worker_index # noqa: E402
|
|
236
236
|
from okstra_ctl.phases.requirements_discovery.validation import (
|
|
@@ -3188,6 +3188,7 @@ def validate_final_report_data(
|
|
|
3188
3188
|
_validate_stage_carry_sidecar_exists(data, report_path, failures)
|
|
3189
3189
|
_validate_lead_fixes(data, failures)
|
|
3190
3190
|
_validate_lead_fix_carry_head(data, report_path, failures)
|
|
3191
|
+
_validate_correction_rounds(data, manifest, failures)
|
|
3191
3192
|
_validate_lead_authored_report(data, report_path, failures)
|
|
3192
3193
|
if task_type == "error-analysis":
|
|
3193
3194
|
_validate_error_analysis_consistency(data, failures)
|
|
@@ -3328,7 +3329,7 @@ _CITATION_LIST_KEYS = ("evidenceRefs", "supportingEvidence", "falsifyingEvidence
|
|
|
3328
3329
|
|
|
3329
3330
|
def _prose_pointers(node: object, key: str = "", pointer: str = "") -> list[tuple[str, str]]:
|
|
3330
3331
|
"""(JSON pointer, value) for every string under a reader-facing prose key."""
|
|
3331
|
-
from okstra_ctl.
|
|
3332
|
+
from okstra_ctl.report_prose_keys import PROSE_KEYS
|
|
3332
3333
|
|
|
3333
3334
|
out: list[tuple[str, str]] = []
|
|
3334
3335
|
if isinstance(node, dict):
|
|
@@ -4685,29 +4686,6 @@ def _validate_improvement_discovery(
|
|
|
4685
4686
|
failures.append(f"improvement-discovery: {err}")
|
|
4686
4687
|
|
|
4687
4688
|
|
|
4688
|
-
def _validate_translation_sidecar(
|
|
4689
|
-
data: Mapping[str, Any], report_path: Path, failures: list[str]
|
|
4690
|
-
) -> None:
|
|
4691
|
-
"""영어가 아닌 사람 HTML 은 번역 사이드카가 있어야 한다.
|
|
4692
|
-
|
|
4693
|
-
사이드카가 없으면 렌더러가 크롬만 번역하고 본문은 영어로 남긴다.
|
|
4694
|
-
`render-views` 가 그 조합을 거부하고, 여기가 같은 규칙의 두 번째 게이트다.
|
|
4695
|
-
"""
|
|
4696
|
-
if not data:
|
|
4697
|
-
return
|
|
4698
|
-
lang = str((data.get("meta") or {}).get("reportLanguage") or "en")
|
|
4699
|
-
if lang == "en":
|
|
4700
|
-
return
|
|
4701
|
-
sidecar = translation_sidecar_path(report_path, lang)
|
|
4702
|
-
if not sidecar.is_file():
|
|
4703
|
-
failures.append(
|
|
4704
|
-
f"final-report has reportLanguage {lang!r} but no translation "
|
|
4705
|
-
f"sidecar at {sidecar.name}. The human HTML rendered from the "
|
|
4706
|
-
"English source instead; re-run `report-finalize --only translate "
|
|
4707
|
-
"--only render-views` to dispatch the translator and overlay it."
|
|
4708
|
-
)
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
4689
|
def _validate_session_conformance(
|
|
4712
4690
|
team_state: dict,
|
|
4713
4691
|
team_state_path: Path,
|
|
@@ -6003,9 +5981,6 @@ def main() -> int:
|
|
|
6003
5981
|
),
|
|
6004
5982
|
)
|
|
6005
5983
|
validation_data = report_data if isinstance(report_data, Mapping) else {}
|
|
6006
|
-
_validate_translation_sidecar(
|
|
6007
|
-
validation_data, _data_path_for(report_path), failures
|
|
6008
|
-
)
|
|
6009
5984
|
validate_report(
|
|
6010
5985
|
report_path,
|
|
6011
5986
|
failures,
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schemaVersion": "1.0",
|
|
3
|
-
"id": "translator",
|
|
4
|
-
"legacyAliases": [],
|
|
5
|
-
"identity": "Renders the report's reader-facing text in another language without editing the underlying decision.",
|
|
6
|
-
"responsibilities": [
|
|
7
|
-
"Carry the source's precision, uncertainty, and operational force into the target language.",
|
|
8
|
-
"Preserve identifiers, paths, commands, and status tokens exactly as the source writes them."
|
|
9
|
-
],
|
|
10
|
-
"requiredCapabilities": [
|
|
11
|
-
"extended-artifact-authoring",
|
|
12
|
-
"source-readonly",
|
|
13
|
-
"worker-artifact-io"
|
|
14
|
-
],
|
|
15
|
-
"prohibitions": [
|
|
16
|
-
"Do not edit the source, translate an unassigned file, or add analysis of your own.",
|
|
17
|
-
"Do not strengthen or weaken a normative statement in translation."
|
|
18
|
-
]
|
|
19
|
-
}
|
|
@@ -1,404 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""CLI entrypoint for the Phase 7 translation sidecar.
|
|
3
|
-
|
|
4
|
-
Usage:
|
|
5
|
-
okstra-report-translate.py extract <path-to-final-report.data.json>
|
|
6
|
-
okstra-report-translate.py check <path-to-sidecar.i18n.<lang>.json>
|
|
7
|
-
|
|
8
|
-
``extract`` writes the translator's work list — every pointer the report holds
|
|
9
|
-
a translatable string at, paired with the English text. The translator fills
|
|
10
|
-
in the values rather than authoring pointers, so a sidecar cannot cite a path
|
|
11
|
-
the document does not have.
|
|
12
|
-
|
|
13
|
-
``check`` is the translator's own gate before it returns: it resolves every
|
|
14
|
-
pointer in the sidecar against the report and reports what is still English.
|
|
15
|
-
Without it a bad sidecar surfaces at render time, after the worker is gone.
|
|
16
|
-
|
|
17
|
-
This script is the canonical single-reference-point. The Node CLI
|
|
18
|
-
(``bin/okstra report-translate``) is a thin wrapper that spawns it.
|
|
19
|
-
"""
|
|
20
|
-
from __future__ import annotations
|
|
21
|
-
|
|
22
|
-
import argparse
|
|
23
|
-
import hashlib
|
|
24
|
-
import json
|
|
25
|
-
import re
|
|
26
|
-
import os
|
|
27
|
-
import sys
|
|
28
|
-
from pathlib import Path
|
|
29
|
-
|
|
30
|
-
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
31
|
-
from okstra_bootstrap import prefer_colocated_modules # noqa: E402
|
|
32
|
-
|
|
33
|
-
prefer_colocated_modules(__file__, "okstra_ctl/report_translation.py")
|
|
34
|
-
|
|
35
|
-
from okstra_ctl.report_translation import ( # noqa: E402
|
|
36
|
-
extract,
|
|
37
|
-
group_translation_pointers,
|
|
38
|
-
overlay,
|
|
39
|
-
)
|
|
40
|
-
from okstra_ctl.final_report_paths import ( # noqa: E402
|
|
41
|
-
translation_sidecar_path,
|
|
42
|
-
translation_source_path,
|
|
43
|
-
)
|
|
44
|
-
from okstra_ctl.json_boundary import ( # noqa: E402
|
|
45
|
-
JsonBoundaryError,
|
|
46
|
-
load_owned_object,
|
|
47
|
-
load_owned_object_snapshot,
|
|
48
|
-
write_owned_object_atomic,
|
|
49
|
-
)
|
|
50
|
-
from okstra_ctl.fixed_text import block, line # noqa: E402
|
|
51
|
-
from okstra_ctl.convergence import ( # noqa: E402
|
|
52
|
-
ConvergenceContractError,
|
|
53
|
-
RunArtifactAuthority,
|
|
54
|
-
canonical_run_report_artifact,
|
|
55
|
-
validated_run_authority,
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _load(path: Path) -> dict:
|
|
60
|
-
try:
|
|
61
|
-
return load_owned_object(path, artifact="report translation artifact")
|
|
62
|
-
except JsonBoundaryError as exc:
|
|
63
|
-
raise SystemExit(f"error: {exc}") from exc
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def _translation_authority(
|
|
67
|
-
manifest_path: Path,
|
|
68
|
-
) -> tuple[RunArtifactAuthority, Path]:
|
|
69
|
-
try:
|
|
70
|
-
authority = validated_run_authority(manifest_path)
|
|
71
|
-
report = canonical_run_report_artifact(authority)
|
|
72
|
-
except (ConvergenceContractError, JsonBoundaryError) as exc:
|
|
73
|
-
raise SystemExit(f"error: {exc}") from exc
|
|
74
|
-
return authority, report
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def _report_snapshot(path: Path):
|
|
78
|
-
try:
|
|
79
|
-
return load_owned_object_snapshot(
|
|
80
|
-
path, artifact="report translation source"
|
|
81
|
-
)
|
|
82
|
-
except JsonBoundaryError as exc:
|
|
83
|
-
raise SystemExit(f"error: {exc}") from exc
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def _source_payload(
|
|
87
|
-
authority: RunArtifactAuthority, data_path: Path, raw_bytes: bytes, data: dict
|
|
88
|
-
) -> dict:
|
|
89
|
-
strings = extract(data)
|
|
90
|
-
return {
|
|
91
|
-
"taskKey": authority.task_key,
|
|
92
|
-
"runManifestPath": authority.manifest_ref,
|
|
93
|
-
"sourceData": data_path.name,
|
|
94
|
-
"sourceDataPath": data_path.relative_to(authority.project_root).as_posix(),
|
|
95
|
-
"sourceDataSha256": hashlib.sha256(raw_bytes).hexdigest(),
|
|
96
|
-
"lang": str((data.get("meta") or {}).get("reportLanguage") or ""),
|
|
97
|
-
"strings": strings,
|
|
98
|
-
"translationGroups": group_translation_pointers(data, strings),
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def _validate_source_payload(
|
|
103
|
-
source: dict, expected: dict, supplied_digest: str
|
|
104
|
-
) -> None:
|
|
105
|
-
for key in (
|
|
106
|
-
"taskKey", "runManifestPath", "sourceData", "sourceDataPath",
|
|
107
|
-
"sourceDataSha256", "lang", "strings",
|
|
108
|
-
):
|
|
109
|
-
if source.get(key) != expected.get(key):
|
|
110
|
-
raise SystemExit(
|
|
111
|
-
"error: report changed after translation source publication"
|
|
112
|
-
)
|
|
113
|
-
if supplied_digest != expected["sourceDataSha256"]:
|
|
114
|
-
raise SystemExit(
|
|
115
|
-
"error: report changed after translation source publication"
|
|
116
|
-
)
|
|
117
|
-
if "translationGroups" in source and (
|
|
118
|
-
source["translationGroups"] != expected["translationGroups"]
|
|
119
|
-
):
|
|
120
|
-
raise SystemExit("error: translation groups do not match report source")
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def cmd_extract(args: argparse.Namespace) -> int:
|
|
124
|
-
data_path = Path(args.data).resolve()
|
|
125
|
-
data = _load(data_path)
|
|
126
|
-
strings = extract(data)
|
|
127
|
-
out_path = translation_source_path(data_path)
|
|
128
|
-
lang = str((data.get("meta") or {}).get("reportLanguage") or "")
|
|
129
|
-
write_owned_object_atomic(
|
|
130
|
-
out_path,
|
|
131
|
-
{"lang": lang, "sourceData": data_path.name, "strings": strings},
|
|
132
|
-
artifact="translation source",
|
|
133
|
-
)
|
|
134
|
-
print(
|
|
135
|
-
json.dumps(
|
|
136
|
-
{
|
|
137
|
-
"ok": True,
|
|
138
|
-
"sourcePath": str(out_path),
|
|
139
|
-
"sidecarPath": str(translation_sidecar_path(data_path, lang)) if lang else "",
|
|
140
|
-
"lang": lang,
|
|
141
|
-
"stringCount": len(strings),
|
|
142
|
-
"charCount": sum(len(v) for v in strings.values()),
|
|
143
|
-
},
|
|
144
|
-
ensure_ascii=False,
|
|
145
|
-
)
|
|
146
|
-
)
|
|
147
|
-
return 0
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def cmd_source(args: argparse.Namespace) -> int:
|
|
151
|
-
authority, data_path = _translation_authority(Path(args.run_manifest))
|
|
152
|
-
snapshot = _report_snapshot(data_path)
|
|
153
|
-
data = snapshot.value
|
|
154
|
-
payload = _source_payload(authority, data_path, snapshot.raw_bytes, data)
|
|
155
|
-
out_path = translation_source_path(data_path)
|
|
156
|
-
if out_path.is_symlink():
|
|
157
|
-
raise SystemExit("error: translation source path is a symlink")
|
|
158
|
-
write_owned_object_atomic(
|
|
159
|
-
out_path, payload, artifact="translation source"
|
|
160
|
-
)
|
|
161
|
-
strings = payload["strings"]
|
|
162
|
-
print("# Translation work list")
|
|
163
|
-
print(line("Task key", authority.task_key), end="")
|
|
164
|
-
print(line("Report", payload["sourceDataPath"]), end="")
|
|
165
|
-
print(line("Run manifest", authority.manifest_ref), end="")
|
|
166
|
-
print(line("Source digest", payload["sourceDataSha256"]), end="")
|
|
167
|
-
groups = payload["translationGroups"]
|
|
168
|
-
print(line("Field count", len(strings)), end="")
|
|
169
|
-
print(line("String count", len(groups)), end="")
|
|
170
|
-
print(line("Source characters", sum(len(value) for value in strings.values())), end="")
|
|
171
|
-
print(line("Translation characters", sum(len(strings[row[0]]) for row in groups)), end="")
|
|
172
|
-
for index, pointers in enumerate(groups, 1):
|
|
173
|
-
print(f"\n## T-{index:03d}\n{block(strings[pointers[0]])}")
|
|
174
|
-
return 0
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def _translation_block(rows: list[str]) -> str:
|
|
178
|
-
# Markdown 의 `\`` 는 백틱 리터럴이고, 이전 `source` 출력이 본문 백틱을 그렇게
|
|
179
|
-
# 내보냈다. 렌더러는 Markdown 이스케이프를 모르므로 여기서 백틱으로 되돌린다.
|
|
180
|
-
return "\n".join(rows).strip().replace("\\`", "`")
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
_T_HEADING_RE = re.compile(r"^#{1,6}\s+T-\d{3}\b")
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def _translation_blocks(path: Path) -> list[str]:
|
|
187
|
-
text = path.read_text(encoding="utf-8")
|
|
188
|
-
blocks: list[str] = []
|
|
189
|
-
current: list[str] | None = None
|
|
190
|
-
for number, row in enumerate(text.splitlines(), start=1):
|
|
191
|
-
# 헤딩 수준이 다른 `### T-NNN` 은 블록 경계로 안 보여 본문에 묻히고, 그러면
|
|
192
|
-
# 블록 수 불일치라는 엉뚱한 메시지가 났다(실측 2026-09-08: id 164개 전부
|
|
193
|
-
# 일치, 틀린 것은 `#` 개수뿐). 경계처럼 생긴 줄은 여기서 이름을 대고 거절한다.
|
|
194
|
-
if _T_HEADING_RE.match(row) and not row.startswith("## T-"):
|
|
195
|
-
raise SystemExit(
|
|
196
|
-
f"error: translation heading must be level 2 — line {number} is "
|
|
197
|
-
f"{row.split()[0]!r}, expected '## T-NNN' ({path.name})"
|
|
198
|
-
)
|
|
199
|
-
if row.startswith("## T-"):
|
|
200
|
-
if current is not None:
|
|
201
|
-
blocks.append(_translation_block(current))
|
|
202
|
-
expected = f"## T-{len(blocks) + 1:03d}"
|
|
203
|
-
if row != expected:
|
|
204
|
-
raise SystemExit(
|
|
205
|
-
f"error: expected translation heading {expected} at line {number}, "
|
|
206
|
-
f"found {row!r} ({path.name})"
|
|
207
|
-
)
|
|
208
|
-
current = []
|
|
209
|
-
elif current is not None:
|
|
210
|
-
current.append(row)
|
|
211
|
-
if current is not None:
|
|
212
|
-
blocks.append(_translation_block(current))
|
|
213
|
-
return blocks
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
def cmd_write(args: argparse.Namespace) -> int:
|
|
217
|
-
authority, data_path = _translation_authority(Path(args.run_manifest))
|
|
218
|
-
snapshot = _report_snapshot(data_path)
|
|
219
|
-
data = snapshot.value
|
|
220
|
-
expected = _source_payload(
|
|
221
|
-
authority, data_path, snapshot.raw_bytes, data
|
|
222
|
-
)
|
|
223
|
-
source_path = translation_source_path(data_path)
|
|
224
|
-
if source_path.is_symlink():
|
|
225
|
-
raise SystemExit("error: translation source path is a symlink")
|
|
226
|
-
source = _load(source_path)
|
|
227
|
-
_validate_source_payload(source, expected, args.source_digest)
|
|
228
|
-
sources = expected["strings"]
|
|
229
|
-
# 발행된 구형 작업 목록은 필드당 한 블록이므로 진행 중 번역의 번호를 보존한다.
|
|
230
|
-
groups = source.get("translationGroups", [[pointer] for pointer in sources])
|
|
231
|
-
translated = _translation_blocks(Path(args.translations))
|
|
232
|
-
if len(translated) != len(groups):
|
|
233
|
-
raise SystemExit(
|
|
234
|
-
f"error: translation blocks must match every T-NNN item — "
|
|
235
|
-
f"{len(translated)} blocks in {Path(args.translations).name}, "
|
|
236
|
-
f"{len(groups)} items in the translation source"
|
|
237
|
-
)
|
|
238
|
-
empty = [f"T-{index + 1:03d}" for index, value in enumerate(translated) if not value]
|
|
239
|
-
if empty:
|
|
240
|
-
raise SystemExit(
|
|
241
|
-
"error: translation blocks must match every T-NNN item — empty: "
|
|
242
|
-
+ ", ".join(empty[:10])
|
|
243
|
-
)
|
|
244
|
-
lang = expected["lang"]
|
|
245
|
-
sidecar = translation_sidecar_path(data_path, lang)
|
|
246
|
-
if sidecar.is_symlink():
|
|
247
|
-
raise SystemExit("error: translation sidecar path is a symlink")
|
|
248
|
-
strings = {
|
|
249
|
-
pointer: value
|
|
250
|
-
for pointers, value in zip(groups, translated)
|
|
251
|
-
for pointer in pointers
|
|
252
|
-
}
|
|
253
|
-
_, report = overlay(data, strings)
|
|
254
|
-
if report.unresolved or report.applied != len(strings):
|
|
255
|
-
raise SystemExit("error: translations do not validate against report source")
|
|
256
|
-
write_owned_object_atomic(
|
|
257
|
-
sidecar,
|
|
258
|
-
{
|
|
259
|
-
"taskKey": authority.task_key,
|
|
260
|
-
"runManifestPath": authority.manifest_ref,
|
|
261
|
-
"lang": lang,
|
|
262
|
-
"sourceData": data_path.name,
|
|
263
|
-
"sourceDataPath": expected["sourceDataPath"],
|
|
264
|
-
"sourceDataSha256": expected["sourceDataSha256"],
|
|
265
|
-
"strings": strings,
|
|
266
|
-
},
|
|
267
|
-
artifact="report translation sidecar",
|
|
268
|
-
)
|
|
269
|
-
print("Translation sidecar\n" + line("Status", "ready"), end="")
|
|
270
|
-
return 0
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
def cmd_check_data(args: argparse.Namespace) -> int:
|
|
274
|
-
authority, data_path = _translation_authority(Path(args.run_manifest))
|
|
275
|
-
snapshot = _report_snapshot(data_path)
|
|
276
|
-
expected = _source_payload(
|
|
277
|
-
authority, data_path, snapshot.raw_bytes, snapshot.value
|
|
278
|
-
)
|
|
279
|
-
sidecar_path = translation_sidecar_path(data_path, expected["lang"])
|
|
280
|
-
if sidecar_path.is_symlink():
|
|
281
|
-
raise SystemExit("error: translation sidecar path is a symlink")
|
|
282
|
-
sidecar = _load(sidecar_path)
|
|
283
|
-
_validate_authority_sidecar(sidecar, expected)
|
|
284
|
-
payload, code = _check_sidecar(sidecar_path)
|
|
285
|
-
print("Translation check\n" + line("Status", "ready" if payload["ok"] else "error")
|
|
286
|
-
+ line("Applied strings", payload["applied"])
|
|
287
|
-
+ line("Unresolved strings", len(payload["unresolved"])), end="")
|
|
288
|
-
return code
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
def _validate_authority_sidecar(sidecar: dict, expected: dict) -> None:
|
|
292
|
-
for key in (
|
|
293
|
-
"taskKey", "runManifestPath", "lang", "sourceData", "sourceDataPath",
|
|
294
|
-
"sourceDataSha256",
|
|
295
|
-
):
|
|
296
|
-
if sidecar.get(key) != expected.get(key):
|
|
297
|
-
raise SystemExit(
|
|
298
|
-
"error: translation sidecar does not match run authority"
|
|
299
|
-
)
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
def cmd_check(args: argparse.Namespace) -> int:
|
|
303
|
-
sidecar_file = Path(args.sidecar).resolve()
|
|
304
|
-
payload, code = _check_sidecar(sidecar_file)
|
|
305
|
-
print(json.dumps(payload, ensure_ascii=False))
|
|
306
|
-
return code
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
def _check_sidecar(sidecar_file: Path) -> tuple[dict, int]:
|
|
310
|
-
sidecar = _load(sidecar_file)
|
|
311
|
-
strings = sidecar.get("strings")
|
|
312
|
-
if not isinstance(strings, dict):
|
|
313
|
-
raise SystemExit(f"error: {sidecar_file} has no 'strings' object")
|
|
314
|
-
source_name = str(sidecar.get("sourceData") or "")
|
|
315
|
-
if not source_name:
|
|
316
|
-
raise SystemExit(f"error: {sidecar_file} does not name its sourceData")
|
|
317
|
-
data_path = sidecar_file.parent / source_name
|
|
318
|
-
if not data_path.is_file():
|
|
319
|
-
raise SystemExit(f"error: sourceData not found beside the sidecar: {data_path}")
|
|
320
|
-
|
|
321
|
-
_, report = overlay(_load(data_path), strings)
|
|
322
|
-
offered = report.applied + len(report.untranslated)
|
|
323
|
-
payload = {
|
|
324
|
-
"ok": not report.unresolved,
|
|
325
|
-
"applied": report.applied,
|
|
326
|
-
"offered": offered,
|
|
327
|
-
"untranslated": list(report.untranslated),
|
|
328
|
-
"unresolved": list(report.unresolved),
|
|
329
|
-
}
|
|
330
|
-
if report.unresolved:
|
|
331
|
-
# A pointer that resolves nowhere means the sidecar was written against
|
|
332
|
-
# a different report. Rendering it would silently drop those strings.
|
|
333
|
-
sys.stderr.write(
|
|
334
|
-
f"error: {len(report.unresolved)} pointer(s) do not resolve in "
|
|
335
|
-
f"{data_path.name}\n"
|
|
336
|
-
)
|
|
337
|
-
return payload, 1
|
|
338
|
-
return payload, 0
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
_CLI_EPILOG = r"""A thin spawn shim over `scripts/okstra-report-translate.py` (installed at
|
|
342
|
-
`$HOME/.okstra/bin/okstra-report-translate.py`).
|
|
343
|
-
|
|
344
|
-
The final-report data.json is authored in English and stays the SSOT. When the
|
|
345
|
-
resolved report language is not English, Phase 7 dispatches a translator that
|
|
346
|
-
writes a sidecar beside the report; `render-views` overlays it so only the
|
|
347
|
-
human HTML is localized.
|
|
348
|
-
|
|
349
|
-
Usage:
|
|
350
|
-
okstra report-translate extract <path-to-final-report.data.json>
|
|
351
|
-
okstra report-translate source --run-manifest <path>
|
|
352
|
-
okstra report-translate write --run-manifest <path> --source-digest <sha256> --translations <markdown>
|
|
353
|
-
okstra report-translate check-data --run-manifest <path>
|
|
354
|
-
Write <stem>.translation-source.json — every pointer the report holds a
|
|
355
|
-
translatable string at, paired with its English text. The translator
|
|
356
|
-
fills in the values, so a sidecar cannot cite a path the report lacks.
|
|
357
|
-
|
|
358
|
-
okstra report-translate check <path-to-final-report.i18n.<lang>.json>
|
|
359
|
-
Resolve every pointer in a filled sidecar against the report it names
|
|
360
|
-
and report what is still English. Exits non-zero on a pointer that
|
|
361
|
-
resolves nowhere — the translator's own gate before it returns.
|
|
362
|
-
"""
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
def _parser() -> argparse.ArgumentParser:
|
|
366
|
-
parser = argparse.ArgumentParser(
|
|
367
|
-
epilog=_CLI_EPILOG,
|
|
368
|
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
369
|
-
prog="okstra report-translate",
|
|
370
|
-
description="Build and verify the final-report translation sidecar.",
|
|
371
|
-
)
|
|
372
|
-
sub = parser.add_subparsers(dest="op", required=True)
|
|
373
|
-
|
|
374
|
-
extract_cmd = sub.add_parser("extract", help="write the translator's work list")
|
|
375
|
-
extract_cmd.add_argument("data", help="path to final-report-<type>-<seq>.data.json")
|
|
376
|
-
extract_cmd.set_defaults(func=cmd_extract)
|
|
377
|
-
|
|
378
|
-
source_view = sub.add_parser("source", help="render a fixed translation work list")
|
|
379
|
-
source_view.add_argument("--run-manifest", required=True)
|
|
380
|
-
source_view.set_defaults(func=cmd_source)
|
|
381
|
-
|
|
382
|
-
write_cmd = sub.add_parser("write", help="publish translations from T-NNN blocks")
|
|
383
|
-
write_cmd.add_argument("--run-manifest", required=True)
|
|
384
|
-
write_cmd.add_argument("--source-digest", required=True)
|
|
385
|
-
write_cmd.add_argument("--translations", required=True)
|
|
386
|
-
write_cmd.set_defaults(func=cmd_write)
|
|
387
|
-
|
|
388
|
-
check_data = sub.add_parser("check-data", help="verify the derived sidecar")
|
|
389
|
-
check_data.add_argument("--run-manifest", required=True)
|
|
390
|
-
check_data.set_defaults(func=cmd_check_data)
|
|
391
|
-
|
|
392
|
-
check_cmd = sub.add_parser("check", help="verify a filled sidecar against its report")
|
|
393
|
-
check_cmd.add_argument("sidecar", help="path to final-report-<type>-<seq>.i18n.<lang>.json")
|
|
394
|
-
check_cmd.set_defaults(func=cmd_check)
|
|
395
|
-
return parser
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
def main(argv: list[str] | None = None) -> int:
|
|
399
|
-
args = _parser().parse_args(argv)
|
|
400
|
-
return int(args.func(args))
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if __name__ == "__main__":
|
|
404
|
-
raise SystemExit(main())
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schemaVersion": "1.0",
|
|
3
|
-
"id": "translator",
|
|
4
|
-
"roleId": "translator",
|
|
5
|
-
"responsibilities": [
|
|
6
|
-
"Translate only the designated sidecar into the requested language as a faithful technical translator, never as an editor of the underlying decision."
|
|
7
|
-
],
|
|
8
|
-
"requiredConduct": [
|
|
9
|
-
"Read the complete designated source, preserve identifiers, code, paths, commands, data shapes, headings, links, status tokens, normative strength, and uncertainty; use consistent project terminology; and verify that no source section or material qualification was omitted."
|
|
10
|
-
],
|
|
11
|
-
"decisionPrinciples": [
|
|
12
|
-
"Translate meaning rather than word order, producing natural target-language prose that carries the same precision, tone, uncertainty, and operational force as the source. Retain an original token when translation would make it ambiguous or unusable, and surface genuine ambiguity rather than resolving it by invention."
|
|
13
|
-
],
|
|
14
|
-
"authorityAndBoundaries": [
|
|
15
|
-
"Write only the assigned translation sidecar. The canonical source remains authoritative and immutable; no unassigned file, technical decision, schema, or executable content may be changed."
|
|
16
|
-
],
|
|
17
|
-
"evidenceStandards": [
|
|
18
|
-
"The translated structure must map completely to the source structure. Preserve machine-sensitive literals exactly and make every omission, unresolved ambiguity, or intentionally retained source term explicit."
|
|
19
|
-
],
|
|
20
|
-
"collaborationContract": [
|
|
21
|
-
"Return source ambiguity to the lead or designated owner without editing the source. Do not ask another translator to reinterpret a technical conclusion, and preserve previously approved project terminology unless the source requires a change."
|
|
22
|
-
],
|
|
23
|
-
"completionCriteria": [
|
|
24
|
-
"Every source section has a meaning-equivalent target section, technical literals and links remain usable, terminology is consistent, natural-language quality has been reviewed, and no new analysis or conclusion has entered the sidecar."
|
|
25
|
-
],
|
|
26
|
-
"prohibitions": [
|
|
27
|
-
"Do not edit the source of truth, translate unassigned files, add analysis, omit inconvenient qualifications, weaken or strengthen normative language, change a technical conclusion, or localize code and identifiers that must remain exact."
|
|
28
|
-
],
|
|
29
|
-
"blockedStateReporting": [
|
|
30
|
-
"Name the ambiguous or untranslatable source passage, explain the competing interpretations and affected output, preserve the passage unchanged where safe, and wait for the responsible owner to resolve it."
|
|
31
|
-
]
|
|
32
|
-
}
|