okstra 0.154.0 → 0.154.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/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/python/okstra_ctl/clarification_items.py +57 -14
- package/runtime/python/okstra_ctl/user_response.py +5 -1
- package/runtime/templates/reports/html/i18n/en.json +3 -1
- package/runtime/templates/reports/html/i18n/ko.json +3 -1
- package/runtime/templates/reports/html/macros/forms.html +2 -2
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -395,11 +395,39 @@ def scan_clarification_blockers(
|
|
|
395
395
|
report_path: Path, blocking_values: frozenset[str]
|
|
396
396
|
) -> ClarificationScan:
|
|
397
397
|
"""Shared fail-closed clarification walk for both gates above — schema-v2
|
|
398
|
-
reads its rows from the data sibling, schema-v1 from the §1 table
|
|
398
|
+
reads its rows from the data sibling, schema-v1 from the §1 table, and the
|
|
399
|
+
user's `user-responses/` sidecar outranks whatever the report says about
|
|
400
|
+
those rows."""
|
|
399
401
|
v2_scan = _scan_v2_blockers(report_path, blocking_values)
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
402
|
+
scan = (
|
|
403
|
+
v2_scan if v2_scan is not None
|
|
404
|
+
else scan_section_1_blockers(_read_report_text(report_path), blocking_values)
|
|
405
|
+
)
|
|
406
|
+
return _resolve_blockers_answered_by_user(report_path, scan)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _resolve_blockers_answered_by_user(
|
|
410
|
+
report_path: Path, scan: ClarificationScan
|
|
411
|
+
) -> ClarificationScan:
|
|
412
|
+
"""사용자가 사이드카로 답한 행을 blocker 에서 뺀 스캔.
|
|
413
|
+
|
|
414
|
+
답의 정본은 사용자의 `user-responses/` 사이드카다. 리포트의 `Status` 는 그
|
|
415
|
+
run 이 스스로 적어둔 값이고, 답이 사이드카로만 들어오는 경로(HTML 뷰의
|
|
416
|
+
`Export user response`, `okstra user-response write`)에서는 갱신되지
|
|
417
|
+
않는다 — 게이트가 리포트만 보면 사용자가 답을 다 채운 뒤에도 같은 항목이
|
|
418
|
+
영원히 미해결로 남아 다음 phase 를 막는다.
|
|
419
|
+
|
|
420
|
+
fail-closed 는 그대로다: 행 자체를 못 읽은 스캔(`unreadable_reason`)은
|
|
421
|
+
어떤 id 가 blocker 인지 모르는 상태이므로 사이드카로 덮지 않는다.
|
|
422
|
+
"""
|
|
423
|
+
if scan.unreadable_reason is not None or not scan.blockers:
|
|
424
|
+
return scan
|
|
425
|
+
answers = sidecar_answers(report_path)
|
|
426
|
+
if not answers:
|
|
427
|
+
return scan
|
|
428
|
+
return ClarificationScan(
|
|
429
|
+
[b for b in scan.blockers if b.row_id not in answers], None
|
|
430
|
+
)
|
|
403
431
|
|
|
404
432
|
|
|
405
433
|
def scan_section_1_blockers(
|
|
@@ -587,11 +615,20 @@ def _sidecars_for_attachment(source: Path) -> list[Path]:
|
|
|
587
615
|
return sorted([*ordinary, *selected])
|
|
588
616
|
|
|
589
617
|
|
|
590
|
-
def
|
|
618
|
+
def sidecar_answers(source: Path) -> dict[str, str]:
|
|
591
619
|
"""`user-responses/` 사이드카들의 답변을 `{clarification-id: value}` 로 모은다.
|
|
592
620
|
|
|
593
|
-
|
|
594
|
-
|
|
621
|
+
사용자가 답한 항목이 무엇인지 아는 단일 참조점 — carry-in 병합도, 승인
|
|
622
|
+
게이트도, 스킬의 열린 항목 목록도 전부 이 한 곳을 본다.
|
|
623
|
+
|
|
624
|
+
`disposition` 이 `answer` 인 항목만 답으로 센다. `reframe` 은 "다음 run 에서
|
|
625
|
+
다시 물어달라" 이지 답이 아니므로(`skills/okstra-user-response/SKILL.md` 의
|
|
626
|
+
"A reframe is not an answer") 답 집합에서 빠져야 게이트가 그 항목을 계속
|
|
627
|
+
미해결로 잡는다.
|
|
628
|
+
|
|
629
|
+
같은 id 가 여러 사이드카에 나오면 이름순 마지막(최신 seq)이 이긴다 — 최신이
|
|
630
|
+
reframe 이거나 값이 비면 앞선 답을 지운다. 그래야 답을 물렀을 때 그 항목이
|
|
631
|
+
미해결로 돌아온다. `user_response` 를 지연 import 해 순환 참조를 피한다.
|
|
595
632
|
"""
|
|
596
633
|
from okstra_ctl.user_response import parse_user_response_entries
|
|
597
634
|
|
|
@@ -600,8 +637,10 @@ def _sidecar_answers(source: Path) -> dict[str, str]:
|
|
|
600
637
|
for entry in parse_user_response_entries(
|
|
601
638
|
sidecar.read_text(encoding="utf-8")
|
|
602
639
|
):
|
|
603
|
-
if entry.value:
|
|
640
|
+
if entry.value and entry.disposition == "answer":
|
|
604
641
|
answers[entry.response_id] = entry.value
|
|
642
|
+
else:
|
|
643
|
+
answers.pop(entry.response_id, None)
|
|
605
644
|
return answers
|
|
606
645
|
|
|
607
646
|
|
|
@@ -644,7 +683,7 @@ def clarification_response_with_sidecars(source: Path) -> str:
|
|
|
644
683
|
"""
|
|
645
684
|
text = source.read_text(encoding="utf-8")
|
|
646
685
|
section = attached_user_responses_section(source)
|
|
647
|
-
answers =
|
|
686
|
+
answers = sidecar_answers(source)
|
|
648
687
|
body = _clarification_carry_body(source, text, answers)
|
|
649
688
|
if not section:
|
|
650
689
|
return body
|
|
@@ -750,9 +789,13 @@ def _locate_user_input_column(lines: list[str]) -> tuple[int, int]:
|
|
|
750
789
|
|
|
751
790
|
|
|
752
791
|
def _reconcile_row(line: str, ui_col: int, answers: dict[str, str]) -> str:
|
|
753
|
-
"""답이 있고 open/answered
|
|
754
|
-
resolved 로 바꾼 줄을, 그 외에는 원본 줄을 그대로 돌려준다.
|
|
755
|
-
|
|
792
|
+
"""답이 있고 open/answered 인 행이면 `User input` 칸을 그 답으로 채우고 Status 를
|
|
793
|
+
resolved 로 바꾼 줄을, 그 외에는 원본 줄을 그대로 돌려준다.
|
|
794
|
+
|
|
795
|
+
칸에 이미 값이 있어도 사용자의 사이드카 답이 이긴다. 그 칸을 채우는 것은
|
|
796
|
+
run 자신(직전 렌더가 옮겨 적은 값)이고, 사용자가 나중에 답을 바꾸면 둘이
|
|
797
|
+
갈라진다 — 사용자가 쓴 쪽을 정본으로 삼지 않으면 run 이 자기가 적어둔 값으로
|
|
798
|
+
계속 되돌아간다.
|
|
756
799
|
|
|
757
800
|
판정은 앵커/백틱을 벗긴 셀(`_split_pipe_row`)로 — 그래야 `_meta_id` 가 스크롤
|
|
758
801
|
앵커의 소문자 slug 대신 진짜 대문자 ID 를 읽는다. 재조립은 원본 셀
|
|
@@ -764,7 +807,7 @@ def _reconcile_row(line: str, ui_col: int, answers: dict[str, str]) -> str:
|
|
|
764
807
|
if item.status not in UNRESOLVED_STATUSES:
|
|
765
808
|
return line
|
|
766
809
|
raw = split_pipe_row(line)
|
|
767
|
-
if not
|
|
810
|
+
if not 0 <= ui_col < len(raw):
|
|
768
811
|
return line
|
|
769
812
|
raw[ui_col] = answers[item.row_id]
|
|
770
813
|
raw[0] = _STATUS_RESOLVE_RE.sub(r"\1resolved", raw[0])
|
|
@@ -772,7 +815,7 @@ def _reconcile_row(line: str, ui_col: int, answers: dict[str, str]) -> str:
|
|
|
772
815
|
|
|
773
816
|
|
|
774
817
|
def _reconcile_user_input(section: str, answers: dict[str, str]) -> str:
|
|
775
|
-
"""§1 표에서 사이드카 답이 있는 미해결 행의
|
|
818
|
+
"""§1 표에서 사이드카 답이 있는 미해결 행의 `User input` 칸을 답으로 채우고
|
|
776
819
|
Status 를 resolved 로 바꾼 §1 본문을 돌려준다.
|
|
777
820
|
|
|
778
821
|
답의 정본 위치를 §1 표 안으로 옮긴다 — 표만 읽는 승인 게이트·프롬프트
|
|
@@ -27,6 +27,7 @@ from okstra_ctl.clarification_items import (
|
|
|
27
27
|
read_clarification_rows,
|
|
28
28
|
scan_open_user_input,
|
|
29
29
|
section_1_present_but_unparsed,
|
|
30
|
+
sidecar_answers,
|
|
30
31
|
_section_1_slice,
|
|
31
32
|
)
|
|
32
33
|
|
|
@@ -545,10 +546,13 @@ def resolve_refs(report_text: str, refs: list[str]) -> list[dict]:
|
|
|
545
546
|
|
|
546
547
|
def show_open_rows(report_path: Path) -> dict:
|
|
547
548
|
text = report_path.read_text(encoding="utf-8")
|
|
549
|
+
# 사이드카에 답이 있는 행은 사용자가 이미 답한 것이다. 리포트의 `Status` 는
|
|
550
|
+
# 그 답을 반영하지 않으므로, 이걸 빼지 않으면 스킬이 같은 질문을 다시 묻는다.
|
|
551
|
+
answered = sidecar_answers(report_path)
|
|
548
552
|
rows = []
|
|
549
553
|
for r in read_clarification_rows(report_path):
|
|
550
554
|
it = r["item"]
|
|
551
|
-
if it.status not in ("open", "answered"):
|
|
555
|
+
if it.status not in ("open", "answered") or it.row_id in answered:
|
|
552
556
|
continue
|
|
553
557
|
statement, expected = r["statement"], r["expected_form"]
|
|
554
558
|
refs = sorted(set(_SECTION_REF_RE.findall(statement + " " + expected)))
|
|
@@ -104,7 +104,9 @@
|
|
|
104
104
|
"export-my-answers": "Export my answers",
|
|
105
105
|
"at-the-foot-of-the-page": "at the foot of the page.",
|
|
106
106
|
"answer-as": "Answer as",
|
|
107
|
-
"questions-waiting-on-you": "Questions waiting on you"
|
|
107
|
+
"questions-waiting-on-you": "Questions waiting on you",
|
|
108
|
+
"count-questions-waiting-on-you": "{count} questions waiting on you",
|
|
109
|
+
"your-answer-to-id": "Your answer to {id}"
|
|
108
110
|
},
|
|
109
111
|
"visualizations": {
|
|
110
112
|
"component": "Component",
|
|
@@ -104,7 +104,9 @@
|
|
|
104
104
|
"export-my-answers": "내 답변 내보내기",
|
|
105
105
|
"at-the-foot-of-the-page": "버튼을 누르세요.",
|
|
106
106
|
"answer-as": "답변 형식",
|
|
107
|
-
"questions-waiting-on-you": "답변을 기다리는 질문"
|
|
107
|
+
"questions-waiting-on-you": "답변을 기다리는 질문",
|
|
108
|
+
"count-questions-waiting-on-you": "답변을 기다리는 질문 {count}건",
|
|
109
|
+
"your-answer-to-id": "{id}에 대한 답변"
|
|
108
110
|
},
|
|
109
111
|
"visualizations": {
|
|
110
112
|
"component": "구성 요소",
|
|
@@ -33,14 +33,14 @@
|
|
|
33
33
|
{% macro clarification_responses(items) -%}
|
|
34
34
|
{% if items %}
|
|
35
35
|
<section class="clarification-responses" aria-label="{{ t('macros.forms.questions-waiting-on-you') }}">
|
|
36
|
-
<h2>{{ items | length }}
|
|
36
|
+
<h2>{{ t('macros.forms.count-questions-waiting-on-you') | replace('{count}', items | length) }}</h2>
|
|
37
37
|
<p class="clarification-lede">{{ t('macros.forms.the-code-alone-could-not-settle-these-fill-i') }} <strong>{{ t('macros.forms.export-my-answers') }}</strong> {{ t('macros.forms.at-the-foot-of-the-page') }}</p>
|
|
38
38
|
{% for row in items %}
|
|
39
39
|
<article class="clarification-item" id="id-{{ row.id }}" data-response-id="{{ row.id }}" data-kind="{{ row.kind }}">
|
|
40
40
|
<p class="eyebrow">{{ row.id }} · {{ row.kind }}</p>
|
|
41
41
|
{{ row.statement | paragraphs }}
|
|
42
42
|
<dl class="clarification-expected"><dt>{{ t('macros.forms.answer-as') }}</dt><dd>{{ row.expectedForm | inline_code }}</dd></dl>
|
|
43
|
-
<label for="response-{{ row.id }}">
|
|
43
|
+
<label for="response-{{ row.id }}">{{ t('macros.forms.your-answer-to-id') | replace('{id}', row.id) }}</label>
|
|
44
44
|
<textarea id="response-{{ row.id }}" data-response-id="{{ row.id }}" rows="4">{{ row.userInput | default('') }}</textarea>
|
|
45
45
|
</article>
|
|
46
46
|
{% endfor %}
|