okstra 0.189.1 → 0.189.3
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/docs/architecture.md +2 -0
- package/docs/cli.md +3 -3
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/duties/direction-selection-worker.md +2 -2
- package/runtime/prompts/lead/report-writer.md +1 -1
- package/runtime/prompts/profiles/_clarification-recommendation.md +1 -1
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
- package/runtime/prompts/profiles/implementation-option-selection.md +2 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +5 -1
- package/runtime/python/okstra_ctl/approval_decisions.py +85 -0
- package/runtime/python/okstra_ctl/implementation_options.py +35 -0
- package/runtime/python/okstra_ctl/plan_items_cli.py +12 -0
- package/runtime/python/okstra_ctl/render.py +13 -0
- package/runtime/python/okstra_ctl/report_html/common.py +60 -4
- package/runtime/python/okstra_ctl/report_html/context_links.py +137 -0
- package/runtime/python/okstra_ctl/report_html/filters.py +22 -20
- package/runtime/python/okstra_ctl/report_html/render.py +18 -5
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_option_selection.py +7 -0
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +5 -0
- package/runtime/python/okstra_ctl/report_translation.py +8 -0
- package/runtime/python/okstra_ctl/scope_provenance.py +28 -6
- package/runtime/python/okstra_ctl/worker_audit_check.py +23 -8
- package/runtime/templates/reports/html/assets/base.css +10 -0
- package/runtime/templates/reports/html/base.template.html +10 -0
- package/runtime/templates/reports/html/i18n/en.json +71 -2
- package/runtime/templates/reports/html/i18n/ko.json +71 -2
- package/runtime/templates/reports/html/macros/forms.html +1 -0
- package/runtime/templates/reports/html/tasks/implementation-option-selection.template.html +55 -11
- package/runtime/validators/validate-run.py +107 -0
|
@@ -30,36 +30,38 @@ def _anchors_pattern(keys: tuple[str, ...]) -> re.Pattern[str] | None:
|
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def _link_ids(escaped: str,
|
|
34
|
-
"""
|
|
33
|
+
def _link_ids(escaped: str, links: dict) -> str:
|
|
34
|
+
"""Link the row ids inside text that is already escaped.
|
|
35
35
|
|
|
36
36
|
Running after the escape keeps this anchor the only markup in the result.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
``links`` maps an id to the href that lands on it: ``#id-…`` for a row of
|
|
38
|
+
this page, or a relative page path plus fragment for a row the carry-in
|
|
39
|
+
report defines. Only ids with a link target become links — an id with no
|
|
40
|
+
row would point at a missing anchor, and a ticket number like
|
|
41
|
+
``DEV-10339`` has the same shape as a row id without being one.
|
|
40
42
|
"""
|
|
41
|
-
if not
|
|
43
|
+
if not links:
|
|
42
44
|
return escaped
|
|
43
|
-
pattern = _anchors_pattern(tuple(sorted(
|
|
45
|
+
pattern = _anchors_pattern(tuple(sorted(links)))
|
|
44
46
|
if pattern is None:
|
|
45
47
|
return escaped
|
|
46
48
|
|
|
47
49
|
def swap(match: re.Match[str]) -> str:
|
|
48
50
|
token = match.group(0)
|
|
49
|
-
|
|
50
|
-
return f'<a href="
|
|
51
|
+
href = links.get(token)
|
|
52
|
+
return f'<a href="{escape(href)}">{token}</a>' if href else token
|
|
51
53
|
|
|
52
54
|
return pattern.sub(swap, escaped)
|
|
53
55
|
|
|
54
56
|
|
|
55
|
-
def inline_code(value: object,
|
|
57
|
+
def inline_code(value: object, links: dict | None = None) -> Markup:
|
|
56
58
|
"""Convert paired backticks to ``<code>``, escaping everything else.
|
|
57
59
|
|
|
58
60
|
Workers author report prose with Markdown conventions, so an identifier
|
|
59
|
-
arrives wrapped in backticks. ``
|
|
61
|
+
arrives wrapped in backticks. ``links`` links the row ids cited in the
|
|
60
62
|
prose, inside backticks and out.
|
|
61
63
|
"""
|
|
62
|
-
index =
|
|
64
|
+
index = links or {}
|
|
63
65
|
text = "" if value is None else str(value)
|
|
64
66
|
out: list[str] = []
|
|
65
67
|
cursor = 0
|
|
@@ -84,7 +86,7 @@ def _grouped_paragraphs(text: str) -> list[str]:
|
|
|
84
86
|
]
|
|
85
87
|
|
|
86
88
|
|
|
87
|
-
def paragraphs(value: object,
|
|
89
|
+
def paragraphs(value: object, links: dict | None = None) -> Markup:
|
|
88
90
|
"""Split prose into ``<p>`` blocks, converting inline code inside each.
|
|
89
91
|
|
|
90
92
|
``userNarrative`` prose arrives as one unbroken run of sentences, so the
|
|
@@ -93,27 +95,27 @@ def paragraphs(value: object, anchors: dict | None = None) -> Markup:
|
|
|
93
95
|
"""
|
|
94
96
|
text = "" if value is None else str(value)
|
|
95
97
|
return Markup(
|
|
96
|
-
"".join(f"<p>{inline_code(block,
|
|
98
|
+
"".join(f"<p>{inline_code(block, links)}</p>" for block in _grouped_paragraphs(text))
|
|
97
99
|
)
|
|
98
100
|
|
|
99
101
|
|
|
100
|
-
def evidence_refs(refs: object,
|
|
101
|
-
"""Render a citation list,
|
|
102
|
+
def evidence_refs(refs: object, links: object) -> Markup:
|
|
103
|
+
"""Render a citation list, linking the entries that have a target.
|
|
102
104
|
|
|
103
105
|
The same index the prose links against decides here too — a citation and a
|
|
104
106
|
mid-sentence mention of the same id must land in the same place. No schema
|
|
105
107
|
behind this filter constrains an entry to an id: most of it is prose citing
|
|
106
108
|
a ``path:line``, which takes ``inline_code`` like every other prose cell.
|
|
107
109
|
"""
|
|
108
|
-
index =
|
|
110
|
+
index = links if isinstance(links, dict) else {}
|
|
109
111
|
items = refs if isinstance(refs, (list, tuple)) else []
|
|
110
112
|
out: list[str] = []
|
|
111
113
|
for ref in items:
|
|
112
|
-
|
|
113
|
-
if
|
|
114
|
+
href = index.get(str(ref))
|
|
115
|
+
if href is None:
|
|
114
116
|
out.append(str(inline_code(ref, index)))
|
|
115
117
|
continue
|
|
116
|
-
out.append(f'<a href="
|
|
118
|
+
out.append(f'<a href="{escape(href)}">{escape(str(ref))}</a>')
|
|
117
119
|
return Markup(", ".join(out))
|
|
118
120
|
|
|
119
121
|
|
|
@@ -17,6 +17,7 @@ from ..report_view_artifacts import user_responses_dir_for_report
|
|
|
17
17
|
from ..usage_cells import format_duration_ms
|
|
18
18
|
from ..json_boundary import load_owned_object
|
|
19
19
|
from .common import anchor_index
|
|
20
|
+
from .context_links import brief_end_states, carry_in_links
|
|
20
21
|
from .filters import (
|
|
21
22
|
code_evidence,
|
|
22
23
|
enum_label,
|
|
@@ -130,17 +131,26 @@ def render_v2_html_view(
|
|
|
130
131
|
env = Environment(loader=FileSystemLoader(str(root)), autoescape=select_autoescape(("html",)), undefined=StrictUndefined)
|
|
131
132
|
env.policies["json.dumps_kwargs"] = {"sort_keys": True, "ensure_ascii": False}
|
|
132
133
|
# Binding the index here is what lets a template cite an id without
|
|
133
|
-
# threading the index through every macro and call site.
|
|
134
|
-
|
|
134
|
+
# threading the index through every macro and call site. The record's
|
|
135
|
+
# own rows come first; the brief's end-state rows and the carry-in
|
|
136
|
+
# report's clarifications only fill ids this page has no row for.
|
|
137
|
+
links = {
|
|
138
|
+
row_id: f"#{name}"
|
|
139
|
+
for row_id, name in anchor_index(data, view.omitted_fields).items()
|
|
140
|
+
}
|
|
141
|
+
brief_rows = brief_end_states(data_path)
|
|
142
|
+
for row in brief_rows:
|
|
143
|
+
links.setdefault(row["id"], f"#id-{row['id']}")
|
|
144
|
+
links.update(carry_in_links(data, data_path, exclude=links))
|
|
135
145
|
chrome = load_dictionary(lang, HTML_DICTIONARY_REL)
|
|
136
146
|
translate = make_jinja_global(chrome)
|
|
137
147
|
env.globals["t"] = translate
|
|
138
148
|
env.filters["code_evidence"] = code_evidence
|
|
139
149
|
env.filters["enum_label"] = lambda value, vocabulary: enum_label(value, vocabulary, chrome)
|
|
140
150
|
env.filters["enum_legend"] = lambda vocabulary: enum_legend(vocabulary, chrome)
|
|
141
|
-
env.filters["evidence_refs"] = lambda refs: evidence_refs(refs,
|
|
142
|
-
env.filters["inline_code"] = lambda value: inline_code(value,
|
|
143
|
-
env.filters["paragraphs"] = lambda value: paragraphs(value,
|
|
151
|
+
env.filters["evidence_refs"] = lambda refs: evidence_refs(refs, links)
|
|
152
|
+
env.filters["inline_code"] = lambda value: inline_code(value, links)
|
|
153
|
+
env.filters["paragraphs"] = lambda value: paragraphs(value, links)
|
|
144
154
|
response_js = (root / "report.js").read_text(encoding="utf-8")
|
|
145
155
|
base_js = (root / "html/assets/base.js").read_text(encoding="utf-8")
|
|
146
156
|
source_data = final_report_data_path(Path(run_meta.source_report)).as_posix()
|
|
@@ -153,6 +163,9 @@ def render_v2_html_view(
|
|
|
153
163
|
"sourceData": source_data,
|
|
154
164
|
"dataSha256": _sha256(data_path),
|
|
155
165
|
"clarificationItems": data.get("clarificationItems", []),
|
|
166
|
+
# 브리프의 최종 상태 행. 모든 phase 가 EB/PB/EO 아이디를 인용하지만
|
|
167
|
+
# 문장은 브리프에만 있으므로 여기서 한 번 그려 인용에 착지점을 준다.
|
|
168
|
+
"briefEndStates": brief_rows,
|
|
156
169
|
# 합의·이견 근거는 태스크 본문과 같이 기본 화면에 올린다. 근거 대장
|
|
157
170
|
# 감사 모드에만 두면 판정이 사용자에게 안 보인다.
|
|
158
171
|
"crossVerification": data.get("crossVerification") or {},
|
package/runtime/python/okstra_ctl/report_html/view_models/implementation_option_selection.py
CHANGED
|
@@ -14,6 +14,13 @@ def build_implementation_option_selection_view(data: dict) -> HumanReportView:
|
|
|
14
14
|
selection if selection["mode"] == "candidate-comparison" else None
|
|
15
15
|
),
|
|
16
16
|
"narrative": selection["userNarrative"],
|
|
17
|
+
# 기준별 가중치. 옵션 카드의 점수 표가 기준 이름 옆에 가중치를 같이
|
|
18
|
+
# 보여야 가중 점수가 어떻게 나왔는지 읽힌다.
|
|
19
|
+
"criteriaWeights": {
|
|
20
|
+
criterion["criterion"]: criterion["weight"]
|
|
21
|
+
for criterion in selection.get("evaluationCriteria") or []
|
|
22
|
+
if isinstance(criterion, dict)
|
|
23
|
+
},
|
|
17
24
|
"recommendedOption": next(
|
|
18
25
|
(
|
|
19
26
|
option
|
|
@@ -198,6 +198,11 @@ class ReportSynthesisPacket:
|
|
|
198
198
|
"supplied one feasibility vote, at least "
|
|
199
199
|
f"{MIN_FEASIBLE_VOTES} votes are `feasible`, and both `safetyBlockers` "
|
|
200
200
|
"and `unresolvedFeasibilityFacts` are empty.",
|
|
201
|
+
"Each `feasibilityVotes` row states that analyser's own verdict, "
|
|
202
|
+
"rationale, and strongest counterevidence as its result gives them. "
|
|
203
|
+
"Two non-`uncertain` votes with identical rationale and "
|
|
204
|
+
"counterevidence are rejected as one worker's text copied under "
|
|
205
|
+
"another's name.",
|
|
201
206
|
f"Rank at most {MAX_RANKED_OPTIONS} valid options by descending "
|
|
202
207
|
"`requirement-fit`, `correctness-risk`, `architecture-fit`, "
|
|
203
208
|
"`weightedScore`, then id. With valid options set routing to "
|
|
@@ -30,6 +30,7 @@ PROSE_KEYS = frozenset({
|
|
|
30
30
|
"approach",
|
|
31
31
|
"approvalDisposition",
|
|
32
32
|
"approvalEvidence",
|
|
33
|
+
"architectureBoundaries",
|
|
33
34
|
"behavior",
|
|
34
35
|
"blastRadius",
|
|
35
36
|
"blockReason",
|
|
@@ -41,13 +42,16 @@ PROSE_KEYS = frozenset({
|
|
|
41
42
|
"change",
|
|
42
43
|
"check",
|
|
43
44
|
"claim",
|
|
45
|
+
"commitment",
|
|
44
46
|
"condition",
|
|
45
47
|
"confirmingSignal",
|
|
46
48
|
"conformanceExemption",
|
|
47
49
|
"consequences",
|
|
48
50
|
"constraint",
|
|
49
51
|
"context",
|
|
52
|
+
"coreMechanism",
|
|
50
53
|
"coreReason",
|
|
54
|
+
"counterevidence",
|
|
51
55
|
"decision",
|
|
52
56
|
"declinedFixRecommendations",
|
|
53
57
|
"description",
|
|
@@ -66,12 +70,15 @@ PROSE_KEYS = frozenset({
|
|
|
66
70
|
"exitContractSummary",
|
|
67
71
|
"expected",
|
|
68
72
|
"expectedBehaviorAfter",
|
|
73
|
+
"expectedChangeAreas",
|
|
69
74
|
"expectedForm",
|
|
70
75
|
"expectedOutcome",
|
|
71
76
|
"expectedResult",
|
|
72
77
|
"expectedVerification",
|
|
78
|
+
"fact",
|
|
73
79
|
"failureHandling",
|
|
74
80
|
"finalConclusion",
|
|
81
|
+
"goal",
|
|
75
82
|
"headline",
|
|
76
83
|
"howToStart",
|
|
77
84
|
"hypothesis",
|
|
@@ -134,6 +141,7 @@ PROSE_KEYS = frozenset({
|
|
|
134
141
|
"verification",
|
|
135
142
|
"verificationMethod",
|
|
136
143
|
"verificationSignal",
|
|
144
|
+
"whyItMatters",
|
|
137
145
|
"workingAssumption",
|
|
138
146
|
})
|
|
139
147
|
|
|
@@ -33,7 +33,9 @@ _END_STATE_ID_RE = re.compile(r"^(?:EB|PB|EO)-\d{3}$")
|
|
|
33
33
|
_END_STATE_HEADINGS = frozenset(
|
|
34
34
|
{"Expected Behavior", "Preserved Behavior", "Expected Outcome"}
|
|
35
35
|
)
|
|
36
|
-
_END_STATE_BULLET_RE = re.compile(
|
|
36
|
+
_END_STATE_BULLET_RE = re.compile(
|
|
37
|
+
r"^-\s+(?P<id>(?:EB|PB|EO)-\d{3})\b\s*(?P<statement>.*)$"
|
|
38
|
+
)
|
|
37
39
|
# Only `## ` closes a section, mirroring validate-brief.py's section_body
|
|
38
40
|
# lookahead. `_BRIEF_HEADING_RE` above matches `#` through `######` and is for
|
|
39
41
|
# heading citations, which are checked against a different reader.
|
|
@@ -89,8 +91,21 @@ def brief_headings(brief_path: Path) -> set[str]:
|
|
|
89
91
|
return headings
|
|
90
92
|
|
|
91
93
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class BriefEndState:
|
|
96
|
+
"""One `- EB-001 <statement>` bullet of the brief's end-state sections."""
|
|
97
|
+
|
|
98
|
+
id: str
|
|
99
|
+
# The normalized `## ` heading the bullet sits under — one of
|
|
100
|
+
# `_END_STATE_HEADINGS`.
|
|
101
|
+
section: str
|
|
102
|
+
# The rest of the bullet line, verbatim. The brief is the only place this
|
|
103
|
+
# sentence exists; every phase cites the id and none of them repeats it.
|
|
104
|
+
statement: str
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def brief_end_state_rows(brief_path: Path) -> tuple[BriefEndState, ...]:
|
|
108
|
+
"""End-state rows the brief declares, preserving section and item order.
|
|
94
109
|
|
|
95
110
|
The reading rules mirror validators/validate-brief.py exactly, because that
|
|
96
111
|
file checks the very same lines for their format and the two readers must
|
|
@@ -116,7 +131,7 @@ def brief_end_state_id_sequence(brief_path: Path) -> tuple[str, ...]:
|
|
|
116
131
|
except OSError:
|
|
117
132
|
return ()
|
|
118
133
|
|
|
119
|
-
|
|
134
|
+
rows: list[BriefEndState] = []
|
|
120
135
|
section = ""
|
|
121
136
|
for line in text.splitlines():
|
|
122
137
|
if m := _SECTION_HEADING_RE.match(line):
|
|
@@ -126,8 +141,15 @@ def brief_end_state_id_sequence(brief_path: Path) -> tuple[str, ...]:
|
|
|
126
141
|
if not section:
|
|
127
142
|
continue
|
|
128
143
|
if m := _END_STATE_BULLET_RE.match(line.strip()):
|
|
129
|
-
|
|
130
|
-
|
|
144
|
+
rows.append(
|
|
145
|
+
BriefEndState(m.group("id"), section, m.group("statement").strip())
|
|
146
|
+
)
|
|
147
|
+
return tuple(rows)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def brief_end_state_id_sequence(brief_path: Path) -> tuple[str, ...]:
|
|
151
|
+
"""End-state ids the brief declares, preserving section and item order."""
|
|
152
|
+
return tuple(row.id for row in brief_end_state_rows(brief_path))
|
|
131
153
|
|
|
132
154
|
|
|
133
155
|
def brief_end_state_ids(brief_path: Path) -> set[str]:
|
|
@@ -13,8 +13,16 @@ import sys
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
|
|
15
15
|
from okstra_ctl.worker_audit_ledger import (
|
|
16
|
-
check_worker_results_audit,
|
|
17
16
|
worker_result_files,
|
|
17
|
+
worker_results_audit_findings,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Phase 7 은 같은 규칙을 두 등급으로 판정한다. 여기서 그 등급을 함께 내지 않으면
|
|
21
|
+
# exit 2 하나로 "run 이 실패한다" 고 읽힌다 — 2026-09-05 실측(dev-10626 stage-1)에서
|
|
22
|
+
# 리드가 권고 등급의 미매칭 인용을 차단 실패로 읽고 결과를 거절·재배치했다.
|
|
23
|
+
RUN_IMPACT = (
|
|
24
|
+
"blocking rows fail the run at Phase 7 (validate-run); advisory rows do not "
|
|
25
|
+
"fail the run at Phase 7 and can only be fixed now, while the worker is alive"
|
|
18
26
|
)
|
|
19
27
|
|
|
20
28
|
|
|
@@ -25,16 +33,19 @@ _CLI_EPILOG = r"""Usage:
|
|
|
25
33
|
--run-dir the run directory; worker-results/ and prompts/ hang off it
|
|
26
34
|
--worker check only this worker (default: every worker in the run)
|
|
27
35
|
|
|
28
|
-
Emits one JSON object — `{ok,
|
|
29
|
-
|
|
36
|
+
Emits one JSON object — `{ok, inspected, inspectedFiles[], failures[],
|
|
37
|
+
blocking[], advisory[], runImpact}` — and exits 2 when failures[] is
|
|
38
|
+
non-empty, 0 otherwise. `failures` is `blocking` followed by `advisory`.
|
|
30
39
|
|
|
31
40
|
Runs the Phase 7 audit-sidecar rules now, while the worker session is still
|
|
32
41
|
alive: that every result file carries no `## 0. Reading Confirmation` heading,
|
|
33
|
-
that its audit sidecar exists, and that every backticked `path:line`
|
|
34
|
-
has a matching Evidence read row in that sidecar.
|
|
42
|
+
that its audit sidecar exists (blocking), and that every backticked `path:line`
|
|
43
|
+
citation has a matching Evidence read row in that sidecar (advisory).
|
|
35
44
|
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
Exit 2 means "fix it now", not "the run fails": only `blocking` rows fail the
|
|
46
|
+
run at Phase 7, and `advisory` rows are the ones nothing can repair once the
|
|
47
|
+
worker session is gone. Do not reject or re-dispatch a result over an advisory
|
|
48
|
+
row alone.
|
|
38
49
|
"""
|
|
39
50
|
|
|
40
51
|
|
|
@@ -58,9 +69,10 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
58
69
|
|
|
59
70
|
def main(argv: list[str] | None = None) -> int:
|
|
60
71
|
args = _parser().parse_args(argv)
|
|
61
|
-
|
|
72
|
+
blocking, advisory = worker_results_audit_findings(
|
|
62
73
|
args.run_dir, args.task_type, args.seq, worker=args.worker
|
|
63
74
|
)
|
|
75
|
+
failures = blocking + advisory
|
|
64
76
|
# A selector that narrows to nothing also produces no failures, so `ok`
|
|
65
77
|
# alone cannot tell a real pass from a check that judged zero files —
|
|
66
78
|
# a mistyped `--worker` used to read as a clean bill of health. The count
|
|
@@ -77,6 +89,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
77
89
|
"inspected": len(inspected),
|
|
78
90
|
"inspectedFiles": inspected,
|
|
79
91
|
"failures": failures,
|
|
92
|
+
"blocking": blocking,
|
|
93
|
+
"advisory": advisory,
|
|
94
|
+
"runImpact": RUN_IMPACT,
|
|
80
95
|
},
|
|
81
96
|
ensure_ascii=False,
|
|
82
97
|
indent=2,
|
|
@@ -31,6 +31,16 @@ nav.report-index a { color: inherit; }
|
|
|
31
31
|
.tone-high, .tone-important { border-left-color: #d94b4b; }
|
|
32
32
|
.tone-medium { border-left-color: #d79b25; }
|
|
33
33
|
.evidence-refs { color: GrayText; font-size: .85rem; }
|
|
34
|
+
.section-lede { max-width: 78ch; color: GrayText; margin-top: 0; }
|
|
35
|
+
/* A direction card reads as four labelled blocks — what, how, why, expected
|
|
36
|
+
outcome — so each block carries its own heading and the lists inside stay
|
|
37
|
+
tight enough that the card does not read as one long table. */
|
|
38
|
+
.option-detail { margin-top: 1rem; padding-top: .8rem; border-top: 1px solid color-mix(in srgb, CanvasText 12%, transparent); }
|
|
39
|
+
.option-detail h4 { margin: 0 0 .4rem; font-size: 1rem; }
|
|
40
|
+
.option-detail p { margin: .3rem 0; max-width: 78ch; }
|
|
41
|
+
.option-detail ul { margin: .2rem 0 .6rem; padding-left: 1.3rem; }
|
|
42
|
+
.option-detail table { margin-top: .5rem; }
|
|
43
|
+
.direction-goal { margin: -.4rem 0 .7rem 1.6rem; color: GrayText; font-size: .9rem; max-width: 78ch; }
|
|
34
44
|
figure { margin: 1rem 0 0; }
|
|
35
45
|
figcaption { display: flex; flex-wrap: wrap; justify-content: space-between; gap: .5rem; margin-bottom: .7rem; }
|
|
36
46
|
.visualization { overflow-x: auto; border-radius: 12px; background: color-mix(in srgb, CanvasText 4%, Canvas); }
|
|
@@ -44,6 +44,16 @@
|
|
|
44
44
|
</header>
|
|
45
45
|
<main id="main-content" data-report-role="human-main">
|
|
46
46
|
{% block human_content %}{% endblock %}
|
|
47
|
+
{% if briefEndStates %}
|
|
48
|
+
<section data-report-section="brief-end-states">
|
|
49
|
+
<h2>{{ t('base.brief-end-states') }}</h2>
|
|
50
|
+
<p class="section-lede">{{ t('base.brief-end-states-intro') }}</p>
|
|
51
|
+
<table>
|
|
52
|
+
<thead><tr><th scope="col">{{ t('macros.layout.id') }}</th><th scope="col">{{ t('base.brief-statement') }}</th></tr></thead>
|
|
53
|
+
<tbody>{% for row in briefEndStates %}<tr id="id-{{ row.id }}">{{ row_key(pairs=[(t('macros.layout.id'), row.id), (t('base.brief-section'), row.section | enum_label('briefSection'))]) }}<td>{{ row.statement | inline_code }}</td></tr>{% endfor %}</tbody>
|
|
54
|
+
</table>
|
|
55
|
+
</section>
|
|
56
|
+
{% endif %}
|
|
47
57
|
{% if crossVerification.get("consensus") or crossVerification.get("differences") %}
|
|
48
58
|
<section data-report-section="cross-check">
|
|
49
59
|
<h2>{{ t('base.cross-check') }}</h2>
|
|
@@ -89,6 +89,45 @@
|
|
|
89
89
|
"missing-information": "Missing information",
|
|
90
90
|
"follow-up": "Follow-up",
|
|
91
91
|
"plan-item": "Plan item"
|
|
92
|
+
},
|
|
93
|
+
"optionCriterion": {
|
|
94
|
+
"requirement-fit": "Requirement fit",
|
|
95
|
+
"architecture-fit": "Architecture fit",
|
|
96
|
+
"change-locality": "Change locality",
|
|
97
|
+
"implementation-complexity": "Implementation complexity",
|
|
98
|
+
"correctness-risk": "Correctness risk",
|
|
99
|
+
"reversibility": "Reversibility",
|
|
100
|
+
"verification-cost": "Verification cost",
|
|
101
|
+
"rollout-cost": "Rollout cost"
|
|
102
|
+
},
|
|
103
|
+
"feasibilityVerdict": {
|
|
104
|
+
"feasible": "Feasible",
|
|
105
|
+
"not-feasible": "Not feasible",
|
|
106
|
+
"uncertain": "Uncertain"
|
|
107
|
+
},
|
|
108
|
+
"commitmentKind": {
|
|
109
|
+
"user-behavior": "User behavior",
|
|
110
|
+
"code-change": "Code change",
|
|
111
|
+
"migration": "Migration",
|
|
112
|
+
"operational-change": "Operational change",
|
|
113
|
+
"preserved-behavior": "Preserved behavior",
|
|
114
|
+
"derived-work": "Derived work"
|
|
115
|
+
},
|
|
116
|
+
"coverageVerdict": {
|
|
117
|
+
"exact": "exact",
|
|
118
|
+
"under": "under",
|
|
119
|
+
"over": "over",
|
|
120
|
+
"contradicted": "contradicted"
|
|
121
|
+
},
|
|
122
|
+
"safetyCategory": {
|
|
123
|
+
"data-loss": "Data loss",
|
|
124
|
+
"security-violation": "Security violation",
|
|
125
|
+
"irreversible-migration": "Irreversible migration"
|
|
126
|
+
},
|
|
127
|
+
"briefSection": {
|
|
128
|
+
"Expected Behavior": "Expected behavior",
|
|
129
|
+
"Preserved Behavior": "Preserved behavior",
|
|
130
|
+
"Expected Outcome": "Expected outcome"
|
|
92
131
|
}
|
|
93
132
|
},
|
|
94
133
|
"enumHint": {
|
|
@@ -122,7 +161,11 @@
|
|
|
122
161
|
"dismiss": "Dismiss",
|
|
123
162
|
"export-downloads": "Export downloads",
|
|
124
163
|
"drop-that-file-into": ". Drop that file into",
|
|
125
|
-
"and-the-next-run-picks-your-answers-up-on-it": "and the next run picks your answers up on its own."
|
|
164
|
+
"and-the-next-run-picks-your-answers-up-on-it": "and the next run picks your answers up on its own.",
|
|
165
|
+
"brief-end-states": "End states the brief pinned",
|
|
166
|
+
"brief-end-states-intro": "The requirement ids this report cites (EB, PB, EO) name these brief items, quoted verbatim from the task brief.",
|
|
167
|
+
"brief-section": "Section",
|
|
168
|
+
"brief-statement": "Statement"
|
|
126
169
|
},
|
|
127
170
|
"runUsage": {
|
|
128
171
|
"heading": "What this run cost",
|
|
@@ -342,7 +385,33 @@
|
|
|
342
385
|
"option-id": "Option ID",
|
|
343
386
|
"direction": "Direction",
|
|
344
387
|
"confirmation-evidence": "Confirmation evidence",
|
|
345
|
-
"citation": "Citation"
|
|
388
|
+
"citation": "Citation",
|
|
389
|
+
"upstream-decisions": "Upstream decisions:",
|
|
390
|
+
"evaluation-criteria": "Evaluation criteria",
|
|
391
|
+
"evaluation-criteria-intro": "Every candidate is scored 1 to 5 on each criterion. The weighted score is the weight-averaged result, so a criterion with weight 5 counts five times as much as one with weight 1.",
|
|
392
|
+
"criterion": "Criterion",
|
|
393
|
+
"weight": "Weight",
|
|
394
|
+
"score": "Score",
|
|
395
|
+
"why-it-matters": "Why it matters",
|
|
396
|
+
"what-changes": "What this direction changes",
|
|
397
|
+
"goal": "Goal:",
|
|
398
|
+
"goal-column": "Goal",
|
|
399
|
+
"change-areas": "Expected change areas",
|
|
400
|
+
"commitment": "Commitment",
|
|
401
|
+
"commitment-body": "What the direction commits to",
|
|
402
|
+
"requirements": "Requirements",
|
|
403
|
+
"how-it-works": "How it is done",
|
|
404
|
+
"boundaries": "Architecture boundaries it keeps",
|
|
405
|
+
"why-this-direction": "Why this direction",
|
|
406
|
+
"feasibility-votes": "Feasibility by worker",
|
|
407
|
+
"worker": "Worker",
|
|
408
|
+
"verdict": "Verdict",
|
|
409
|
+
"counterevidence": "Strongest counterevidence",
|
|
410
|
+
"safety-blockers": "Safety blockers",
|
|
411
|
+
"unresolved-facts": "Unresolved feasibility facts",
|
|
412
|
+
"expected-outcome": "Expected outcome",
|
|
413
|
+
"coverage-line": "Covers {covered} of {total} requirements ({verdict}).",
|
|
414
|
+
"planning-invariants": "Must hold in planning"
|
|
346
415
|
},
|
|
347
416
|
"implementation-planning": {
|
|
348
417
|
"what-the-plan-is-for": "What the plan is for",
|
|
@@ -89,6 +89,45 @@
|
|
|
89
89
|
"missing-information": "빠진 정보",
|
|
90
90
|
"follow-up": "후속 작업",
|
|
91
91
|
"plan-item": "계획 항목"
|
|
92
|
+
},
|
|
93
|
+
"optionCriterion": {
|
|
94
|
+
"requirement-fit": "요구사항 적합",
|
|
95
|
+
"architecture-fit": "아키텍처 적합",
|
|
96
|
+
"change-locality": "변경 국소성",
|
|
97
|
+
"implementation-complexity": "구현 복잡도",
|
|
98
|
+
"correctness-risk": "정확성 위험",
|
|
99
|
+
"reversibility": "되돌리기 쉬움",
|
|
100
|
+
"verification-cost": "검증 비용",
|
|
101
|
+
"rollout-cost": "배포 비용"
|
|
102
|
+
},
|
|
103
|
+
"feasibilityVerdict": {
|
|
104
|
+
"feasible": "실행 가능",
|
|
105
|
+
"not-feasible": "실행 불가",
|
|
106
|
+
"uncertain": "불확실"
|
|
107
|
+
},
|
|
108
|
+
"commitmentKind": {
|
|
109
|
+
"user-behavior": "사용자 동작",
|
|
110
|
+
"code-change": "코드 변경",
|
|
111
|
+
"migration": "마이그레이션",
|
|
112
|
+
"operational-change": "운영 변경",
|
|
113
|
+
"preserved-behavior": "보존 동작",
|
|
114
|
+
"derived-work": "파생 작업"
|
|
115
|
+
},
|
|
116
|
+
"coverageVerdict": {
|
|
117
|
+
"exact": "정확히 일치",
|
|
118
|
+
"under": "미달",
|
|
119
|
+
"over": "초과",
|
|
120
|
+
"contradicted": "모순"
|
|
121
|
+
},
|
|
122
|
+
"safetyCategory": {
|
|
123
|
+
"data-loss": "데이터 손실",
|
|
124
|
+
"security-violation": "보안 위반",
|
|
125
|
+
"irreversible-migration": "되돌릴 수 없는 마이그레이션"
|
|
126
|
+
},
|
|
127
|
+
"briefSection": {
|
|
128
|
+
"Expected Behavior": "기대 동작",
|
|
129
|
+
"Preserved Behavior": "보존 동작",
|
|
130
|
+
"Expected Outcome": "기대 산출"
|
|
92
131
|
}
|
|
93
132
|
},
|
|
94
133
|
"enumHint": {
|
|
@@ -122,7 +161,11 @@
|
|
|
122
161
|
"dismiss": "닫기",
|
|
123
162
|
"export-downloads": "내보내면 다음 파일이 받아집니다",
|
|
124
163
|
"drop-that-file-into": ". 그 파일을 여기에 두면",
|
|
125
|
-
"and-the-next-run-picks-your-answers-up-on-it": "다음 run 이 알아서 답변을 읽어 갑니다."
|
|
164
|
+
"and-the-next-run-picks-your-answers-up-on-it": "다음 run 이 알아서 답변을 읽어 갑니다.",
|
|
165
|
+
"brief-end-states": "브리프가 고정한 최종 상태",
|
|
166
|
+
"brief-end-states-intro": "이 리포트가 인용하는 요구사항 아이디(EB·PB·EO)는 브리프의 다음 항목입니다. 문장은 태스크 브리프 원문 그대로입니다.",
|
|
167
|
+
"brief-section": "절",
|
|
168
|
+
"brief-statement": "내용"
|
|
126
169
|
},
|
|
127
170
|
"runUsage": {
|
|
128
171
|
"heading": "이번 실행에 든 비용",
|
|
@@ -342,7 +385,33 @@
|
|
|
342
385
|
"option-id": "옵션 아이디",
|
|
343
386
|
"direction": "방향",
|
|
344
387
|
"confirmation-evidence": "확인 근거",
|
|
345
|
-
"citation": "인용"
|
|
388
|
+
"citation": "인용",
|
|
389
|
+
"upstream-decisions": "선행 결정:",
|
|
390
|
+
"evaluation-criteria": "평가 기준",
|
|
391
|
+
"evaluation-criteria-intro": "모든 후보를 기준마다 1~5점으로 매깁니다. 가중 점수는 가중치를 반영한 평균이라, 가중치 5인 기준은 가중치 1인 기준의 다섯 배로 반영됩니다.",
|
|
392
|
+
"criterion": "기준",
|
|
393
|
+
"weight": "가중치",
|
|
394
|
+
"score": "점수",
|
|
395
|
+
"why-it-matters": "왜 중요한가",
|
|
396
|
+
"what-changes": "무엇을 바꾸는가",
|
|
397
|
+
"goal": "목표:",
|
|
398
|
+
"goal-column": "목표",
|
|
399
|
+
"change-areas": "예상 변경 영역",
|
|
400
|
+
"commitment": "약속",
|
|
401
|
+
"commitment-body": "이 방향이 약속하는 것",
|
|
402
|
+
"requirements": "요구사항",
|
|
403
|
+
"how-it-works": "어떻게 구현하는가",
|
|
404
|
+
"boundaries": "지키는 아키텍처 경계",
|
|
405
|
+
"why-this-direction": "왜 이 방향인가",
|
|
406
|
+
"feasibility-votes": "작업자별 실행 가능성",
|
|
407
|
+
"worker": "작업자",
|
|
408
|
+
"verdict": "판정",
|
|
409
|
+
"counterevidence": "가장 강한 반대 근거",
|
|
410
|
+
"safety-blockers": "안전 차단 요인",
|
|
411
|
+
"unresolved-facts": "미확정 실행 가능성 사실",
|
|
412
|
+
"expected-outcome": "예상 결과",
|
|
413
|
+
"coverage-line": "요구사항 {total}개 중 {covered}개를 충족합니다({verdict}).",
|
|
414
|
+
"planning-invariants": "계획에서 지켜야 할 조건"
|
|
346
415
|
},
|
|
347
416
|
"implementation-planning": {
|
|
348
417
|
"what-the-plan-is-for": "이 계획이 하려는 것",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
<fieldset><legend>{{ t('macros.forms.direction') }}</legend>
|
|
48
48
|
{% for option in state.rankedOptions %}
|
|
49
49
|
<input id="direction-selection-{{ option.id }}" type="radio" name="direction-selection-option" value="{{ option.id }}" data-option-name="{{ option.name }}" required><label for="direction-selection-{{ option.id }}">{{ option.id | inline_code }} · {{ option.name | inline_code }}{% if option.id == state.recommendedOptionId %} ({{ t('macros.forms.recommended') }}){% endif %}</label>
|
|
50
|
+
<p class="direction-goal">{{ option.goal | inline_code }}</p>
|
|
50
51
|
{% endfor %}
|
|
51
52
|
</fieldset>
|
|
52
53
|
<input id="direction-selection-confirmed" type="checkbox" required><label for="direction-selection-confirmed">{{ t('macros.forms.confirmed') }}</label>
|