okstra 0.218.0 → 0.219.1
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 +0 -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/commands/pr/pr.d.mts +6 -0
- package/dist/commands/pr/pr.mjs +58 -0
- package/dist/commands/pr/pr.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 +3 -4
- package/docs/project-structure-overview.md +4 -5
- 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 +1 -3
- 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/dispatch_core.py +3 -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 +2 -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 +1 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-executor.md +1 -0
- package/runtime/python/okstra_ctl/phases/implementation/spec.md +4 -0
- 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 +11 -11
- 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_integrate.py +37 -2
- package/runtime/python/okstra_ctl/stage_rebase.py +189 -0
- 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/skills/_fragments/wizard-render-bundle.md +1 -1
- package/runtime/skills/okstra-code-review/SKILL.md +1 -1
- package/runtime/skills/okstra-discussion/SKILL.md +1 -1
- package/runtime/skills/okstra-pr-gen/SKILL.md +9 -0
- 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/pr/drafting-rules.md +4 -0
- package/runtime/templates/report-writer-prompt-preamble.md +1 -1
- package/runtime/templates/reports/html/i18n/en.json +0 -1
- package/runtime/templates/reports/html/i18n/ko.json +0 -1
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-run.py +1 -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
|
@@ -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
|
-
}
|