okstra 0.219.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/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 +2 -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 +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 +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 +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 +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_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-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 +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
|
@@ -169,8 +169,8 @@ matching section:
|
|
|
169
169
|
- **D. PR body template** (release-handoff) — opt-in pre-registration; also
|
|
170
170
|
offered on the first release-handoff run.
|
|
171
171
|
- **E. final report language** (`reportLanguage`) — a language tag (`en`,
|
|
172
|
-
`ko`, `fr`, …); default `en`. The
|
|
173
|
-
|
|
172
|
+
`ko`, `fr`, …); default `en`. The report writer writes the report prose
|
|
173
|
+
directly in it.
|
|
174
174
|
- **F. declared architecture style** (`architecture.style`) — `hexagonal` /
|
|
175
175
|
`layered` / `none`; hand-added, default `none`. Declaring one makes that
|
|
176
176
|
architecture's placement rules binding in planning + verification.
|
|
@@ -196,8 +196,8 @@ per-run override prompt during the next release-handoff run.
|
|
|
196
196
|
|
|
197
197
|
## E. Final report language (`reportLanguage`)
|
|
198
198
|
|
|
199
|
-
The value is a language tag (`en`, `ko`, `fr`, `pt-BR`, …). The
|
|
200
|
-
|
|
199
|
+
The value is a language tag (`en`, `ko`, `fr`, `pt-BR`, …). The report writer
|
|
200
|
+
writes the report prose directly in that language; no translation step runs.
|
|
201
201
|
If you skip, no `reportLanguage` field is written and the runtime uses `en`.
|
|
202
202
|
|
|
203
203
|
AskUserQuestion (fixed options):
|
|
@@ -30,7 +30,7 @@ The narrative is not free-form Markdown. A narrative file is the line `# OKSTRA
|
|
|
30
30
|
|
|
31
31
|
Blank lines are ignored. **Every other line is rejected**, including Markdown headings (`#`, `##`, `###`), a pipe table written at column 0, code fences, bare paragraphs, JSON, YAML, and JSON Pointer. A heading or table that belongs in the reader's report is written as the text of a `> ` value, never as a narrative line of its own.
|
|
32
32
|
|
|
33
|
-
Follow the task-specific schema order
|
|
33
|
+
Follow the task-specific schema order. Write every `> ` prose value directly in the language named by `**Report Language:**`; do not draft in English first, because no translation step follows. Field names, `Verdict Token` and other enum or status values, finding and requirement ids, paths, symbols, commands, and model names stay exactly as the schema and sources spell them.
|
|
34
34
|
|
|
35
35
|
### Top-level fields you may author
|
|
36
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
|
|
|
@@ -230,7 +230,6 @@ from okstra_ctl.workflow import ( # noqa: E402
|
|
|
230
230
|
)
|
|
231
231
|
from okstra_ctl.final_report_paths import ( # noqa: E402
|
|
232
232
|
final_report_data_path as _data_path_for,
|
|
233
|
-
translation_sidecar_path,
|
|
234
233
|
)
|
|
235
234
|
from okstra_token_usage.report import _match_worker_index # noqa: E402
|
|
236
235
|
from okstra_ctl.phases.requirements_discovery.validation import (
|
|
@@ -3328,7 +3327,7 @@ _CITATION_LIST_KEYS = ("evidenceRefs", "supportingEvidence", "falsifyingEvidence
|
|
|
3328
3327
|
|
|
3329
3328
|
def _prose_pointers(node: object, key: str = "", pointer: str = "") -> list[tuple[str, str]]:
|
|
3330
3329
|
"""(JSON pointer, value) for every string under a reader-facing prose key."""
|
|
3331
|
-
from okstra_ctl.
|
|
3330
|
+
from okstra_ctl.report_prose_keys import PROSE_KEYS
|
|
3332
3331
|
|
|
3333
3332
|
out: list[tuple[str, str]] = []
|
|
3334
3333
|
if isinstance(node, dict):
|
|
@@ -4685,29 +4684,6 @@ def _validate_improvement_discovery(
|
|
|
4685
4684
|
failures.append(f"improvement-discovery: {err}")
|
|
4686
4685
|
|
|
4687
4686
|
|
|
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
4687
|
def _validate_session_conformance(
|
|
4712
4688
|
team_state: dict,
|
|
4713
4689
|
team_state_path: Path,
|
|
@@ -6003,9 +5979,6 @@ def main() -> int:
|
|
|
6003
5979
|
),
|
|
6004
5980
|
)
|
|
6005
5981
|
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
5982
|
validate_report(
|
|
6010
5983
|
report_path,
|
|
6011
5984
|
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
|
-
}
|