okstra 0.142.0 → 0.143.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -102,6 +102,10 @@ from .worktree import (
102
102
  WorktreeProvision,
103
103
  provision_task_worktree,
104
104
  )
105
+ from .brief_frontmatter import (
106
+ has_reporter_confirmation_contract,
107
+ read_brief_frontmatter,
108
+ )
105
109
 
106
110
  # Frontmatter approval-flag matcher.
107
111
  #
@@ -854,6 +858,7 @@ class _ResolvedAssets:
854
858
  final_report_template: Path
855
859
  lead_contract: Path
856
860
  run_validator: Path
861
+ brief_validator: Path
857
862
 
858
863
 
859
864
  def _resolve_runtime_assets(workspace_root: Path, inp: PrepareInputs) -> _ResolvedAssets:
@@ -877,7 +882,14 @@ def _resolve_runtime_assets(workspace_root: Path, inp: PrepareInputs) -> _Resolv
877
882
  )
878
883
  lead_contract = workspace_root / "prompts" / "lead" / "okstra-lead-contract.md"
879
884
  run_validator = workspace_root / "validators" / "validate-run.py"
880
- for required in (task_index_template, final_report_template, run_validator, lead_contract):
885
+ brief_validator = workspace_root / "validators" / "validate-brief.py"
886
+ for required in (
887
+ task_index_template,
888
+ final_report_template,
889
+ run_validator,
890
+ brief_validator,
891
+ lead_contract,
892
+ ):
881
893
  if not required.is_file():
882
894
  raise PrepareError(
883
895
  f"required okstra template or lead contract missing: {required}.{_INSTALL_HINT}"
@@ -889,12 +901,49 @@ def _resolve_runtime_assets(workspace_root: Path, inp: PrepareInputs) -> _Resolv
889
901
  final_report_template=final_report_template,
890
902
  lead_contract=lead_contract,
891
903
  run_validator=run_validator,
904
+ brief_validator=brief_validator,
892
905
  )
893
906
 
894
907
 
908
+ def _validate_task_brief_preflight(
909
+ project_root: Path,
910
+ brief_path: Path,
911
+ validator_path: Path,
912
+ ) -> None:
913
+ """Validate canonical briefs before any prepare-time side effects."""
914
+ frontmatter = read_brief_frontmatter(brief_path)
915
+ if not has_reporter_confirmation_contract(frontmatter):
916
+ return
917
+
918
+ proc = _subprocess.run(
919
+ [
920
+ sys.executable,
921
+ str(validator_path),
922
+ str(brief_path),
923
+ "--briefs-root",
924
+ str(project_root / ".okstra" / "briefs"),
925
+ ],
926
+ capture_output=True,
927
+ text=True,
928
+ check=False,
929
+ )
930
+ if proc.returncode != 0:
931
+ detail = " ".join(
932
+ line.strip()
933
+ for output in (proc.stdout, proc.stderr)
934
+ for line in output.splitlines()
935
+ if line.strip()
936
+ )
937
+ raise PrepareError(f"task brief failed validation: {detail}")
938
+ if frontmatter["reporter-confirmations"] == "pending":
939
+ raise PrepareError(
940
+ "task brief reporter-confirmations is pending; rerun okstra-brief-gen "
941
+ "Step 6.5 before starting error-analysis"
942
+ )
943
+
944
+
895
945
  def _validate_prepare_inputs(project_root: Path, inp: PrepareInputs) -> list:
896
- """project_root/brief 존재와 task-type 별 입력 의미(plan 승인·stage·clarification)
897
- 를 검증하고, implementation 일 때 stage map 을 파싱해 돌려준다 (그 외엔 빈 리스트)."""
946
+ """Validate pure prepare inputs and return a final-verification stage map."""
898
947
  if not project_root.is_dir():
899
948
  raise PrepareError(f"project root not found: {project_root}")
900
949
  if inp.stages and inp.task_type != "release-handoff":
@@ -911,41 +960,14 @@ def _validate_prepare_inputs(project_root: Path, inp: PrepareInputs) -> list:
911
960
  raise PrepareError(f"task brief not found: {inp.brief_path}")
912
961
  ctx_stage_map: list = []
913
962
  # implementation 과 final-verification 은 둘 다 승인된 plan 의 Stage Map 을
914
- # 입력으로 받는다(전자는 실행 scope, 후자는 검증 scope). plan-presence +
915
- # stage-map 파싱은 공유하고, frontmatter 승인/option 주입/구조 검증 같은
916
- # implementation 전용 단계만 따로 게이트한다.
963
+ # 입력으로 받는다(전자는 실행 scope, 후자는 검증 scope).
917
964
  if inp.task_type in ("implementation", "final-verification"):
918
965
  if not inp.approved_plan_path:
919
966
  raise PrepareError(
920
967
  f"task-type {inp.task_type} requires "
921
968
  "--approved-plan <path-to-final-report.md>"
922
969
  )
923
- if inp.task_type == "implementation":
924
- # --approve / --implementation-option 은 공유 approved-plan 파일에
925
- # read-modify-write 한다. 같은 task-key 의 동시 stage run 둘이 둘 다
926
- # 이 플래그를 주면 후발 write 가 선발의 frontmatter 변경을 덮어쓴다
927
- # (lost update). 두 mutation 을 per-task-key 락으로 직렬화한다.
928
- if inp.approve_plan_ack or inp.implementation_option:
929
- with worktree_provision_mutex(
930
- okstra_home(), inp.project_id,
931
- slugify(inp.task_group), slugify(inp.task_id),
932
- ):
933
- if inp.approve_plan_ack:
934
- # 사용자가 직접 `--approve` 를 입력한 행위 자체를 승인 의사로
935
- # 모델링한다. frontmatter approved 를 true 로 toggle 한 뒤
936
- # 동일한 검증 경로(`_validate_approved_plan`)를 통과시킨다.
937
- _apply_cli_approval(inp.approved_plan_path)
938
- if inp.implementation_option:
939
- # 유저가 고른 Option Candidate 이름을 approved-plan
940
- # frontmatter 의 `implementation-option:` 라인에 기록한다.
941
- # 빈 값이면 implementation 이 plan 의 `Recommended Option`
942
- # 으로 폴백하므로 호출하지 않는다.
943
- _apply_cli_implementation_option(
944
- inp.approved_plan_path, inp.implementation_option
945
- )
946
- _validate_approved_plan(inp.approved_plan_path)
947
- _validate_stage_structure(inp.approved_plan_path)
948
- else:
970
+ if inp.task_type == "final-verification":
949
971
  # final-verification 에서 --approve / --implementation-option 은
950
972
  # 의미가 없다 (승인은 implementation 진입 시 이미 끝났다).
951
973
  if inp.approve_plan_ack:
@@ -958,7 +980,7 @@ def _validate_prepare_inputs(project_root: Path, inp: PrepareInputs) -> list:
958
980
  "--implementation-option is only meaningful with --task-type "
959
981
  "implementation and --approved-plan <path>"
960
982
  )
961
- ctx_stage_map = _parse_stage_map_into_ctx(inp.approved_plan_path)
983
+ ctx_stage_map = _parse_stage_map_into_ctx(inp.approved_plan_path)
962
984
  else:
963
985
  if inp.approve_plan_ack:
964
986
  # implementation 외 task-type 에서 `--approve` 는 의미가 없다. 사용자에게
@@ -988,6 +1010,24 @@ def _validate_prepare_inputs(project_root: Path, inp: PrepareInputs) -> list:
988
1010
  return ctx_stage_map
989
1011
 
990
1012
 
1013
+ def _prepare_implementation_approved_plan(inp: PrepareInputs) -> list:
1014
+ """Apply approved-plan inputs only after canonical brief preflight succeeds."""
1015
+ if inp.approve_plan_ack or inp.implementation_option:
1016
+ with worktree_provision_mutex(
1017
+ okstra_home(), inp.project_id,
1018
+ slugify(inp.task_group), slugify(inp.task_id),
1019
+ ):
1020
+ if inp.approve_plan_ack:
1021
+ _apply_cli_approval(inp.approved_plan_path)
1022
+ if inp.implementation_option:
1023
+ _apply_cli_implementation_option(
1024
+ inp.approved_plan_path, inp.implementation_option
1025
+ )
1026
+ _validate_approved_plan(inp.approved_plan_path)
1027
+ _validate_stage_structure(inp.approved_plan_path)
1028
+ return _parse_stage_map_into_ctx(inp.approved_plan_path)
1029
+
1030
+
991
1031
  def _collect_handoff_source_report_rows(
992
1032
  rows: list, nums: list,
993
1033
  ) -> list:
@@ -2072,6 +2112,14 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
2072
2112
  task_index_template = assets.task_index_template
2073
2113
  final_report_template = assets.final_report_template
2074
2114
  ctx_stage_map = _validate_prepare_inputs(project_root, inp)
2115
+ if inp.task_type != "release-handoff":
2116
+ _validate_task_brief_preflight(
2117
+ project_root,
2118
+ inp.brief_path,
2119
+ assets.brief_validator,
2120
+ )
2121
+ if inp.task_type == "implementation":
2122
+ ctx_stage_map = _prepare_implementation_approved_plan(inp)
2075
2123
 
2076
2124
  # release-handoff: 검증 보고서 인용 input 문서를 생성해 brief 자리에 채운다.
2077
2125
  # 이후의 모든 brief 소비 경로(material/instruction-set 복사)는 그대로 동작한다.
@@ -1,8 +1,9 @@
1
1
  """Build a task-type-scoped excerpt of the final-report schema.
2
2
 
3
3
  The full schema (``schemas/final-report-v1.0.schema.json``) carries the
4
- deliverable property blocks for ALL task-types (``implementationPlanning``,
5
- ``releaseHandoff``, ``implementation``, ``finalVerification``) plus a
4
+ deliverable property blocks for ALL task-types (``errorAnalysis``,
5
+ ``implementationPlanning``, ``releaseHandoff``, ``implementation``,
6
+ ``finalVerification``) plus a
6
7
  ``$defs`` library (~38% of the file) shared across them. A single run only
7
8
  authors ONE task-type's data.json, so the report-writer worker only needs
8
9
  the common structure + its own task-type's block + the ``$defs`` those
@@ -24,10 +25,11 @@ import json
24
25
  import re
25
26
 
26
27
  # task-type → the per-type deliverable property key it owns. task-types
27
- # absent from this map (requirements-discovery, error-analysis,
28
+ # absent from this map (requirements-discovery,
28
29
  # improvement-discovery) have no per-type block; their excerpt keeps only
29
30
  # the common properties.
30
31
  _TASK_TYPE_PROPERTY = {
32
+ "error-analysis": "errorAnalysis",
31
33
  "implementation-planning": "implementationPlanning",
32
34
  "release-handoff": "releaseHandoff",
33
35
  "implementation": "implementation",
@@ -30,6 +30,7 @@ from pathlib import Path
30
30
  from typing import Any, Callable, Optional
31
31
 
32
32
  from okstra_ctl.ids import slugify_task_segment
33
+ from okstra_ctl.brief_frontmatter import read_brief_frontmatter
33
34
  from okstra_ctl.models import (
34
35
  PROVIDER_MAPPINGS,
35
36
  UnknownModelError,
@@ -155,49 +156,6 @@ _RECENT_PREFIX = "__recent:"
155
156
  _REPORT_PREFIX = "__report:"
156
157
  _BRIEF_PREFIX = "__brief:"
157
158
 
158
- # Lines of `key: value` we pull from a brief markdown frontmatter. The
159
- # parser is intentionally lightweight (no yaml dep) and tolerant — a
160
- # malformed brief returns an empty dict.
161
- _BRIEF_FRONTMATTER_LINE_RE = re.compile(r"^([a-zA-Z0-9_\-]+)\s*:\s*(.*)$")
162
-
163
-
164
- def _parse_brief_frontmatter(path: Path) -> dict[str, str]:
165
- """Read the YAML-style frontmatter at the top of a brief markdown file
166
- and return a flat ``{key: value}`` map.
167
-
168
- Returns ``{}`` if the file is unreadable, has no frontmatter, or the
169
- frontmatter is malformed. Comments (``# ...``) and quoted values are
170
- stripped. Placeholder values like ``<task-group>`` are kept verbatim;
171
- callers decide whether to treat them as a real suggestion.
172
- """
173
- try:
174
- text = path.read_text(encoding="utf-8")
175
- except OSError:
176
- return {}
177
- if not text.startswith("---"):
178
- return {}
179
- lines = text.splitlines()
180
- if not lines or lines[0].strip() != "---":
181
- return {}
182
- out: dict[str, str] = {}
183
- for line in lines[1:]:
184
- if line.strip() == "---":
185
- break
186
- # strip trailing inline comment
187
- comment_idx = line.find("#")
188
- if comment_idx >= 0:
189
- line = line[:comment_idx]
190
- m = _BRIEF_FRONTMATTER_LINE_RE.match(line.strip())
191
- if not m:
192
- continue
193
- key, val = m.group(1), m.group(2).strip()
194
- # strip matching quotes
195
- if (len(val) >= 2 and val[0] == val[-1] and val[0] in ("'", '"')):
196
- val = val[1:-1]
197
- out[key] = val
198
- return out
199
-
200
-
201
159
  def _looks_like_template_placeholder(value: str) -> bool:
202
160
  """Treat ``<task-group>``, ``<...>``, empty strings, and ``self`` as
203
161
  non-suggestions. Anything else (a real slug-like value) is honored."""
@@ -223,7 +181,7 @@ def _brief_suggestions(path: Path) -> tuple[str, str]:
223
181
  A brief without frontmatter, or with placeholder values, yields two
224
182
  empty strings — callers fall back to plain-text input.
225
183
  """
226
- fm = _parse_brief_frontmatter(path)
184
+ fm = read_brief_frontmatter(path)
227
185
  tg_raw = fm.get("task-group", "")
228
186
  bid_raw = fm.get("brief-id", "")
229
187
  tg = "" if _looks_like_template_placeholder(tg_raw) else tg_raw
@@ -384,6 +384,8 @@
384
384
  "items": { "$ref": "#/$defs/RiskRow" }
385
385
  },
386
386
 
387
+ "errorAnalysis": { "$ref": "#/$defs/ErrorAnalysis" },
388
+
387
389
  "implementationPlanning": {
388
390
  "type": "object",
389
391
  "description": "RENDER_IF taskType == implementation-planning. §5.5 deliverables.",
@@ -814,6 +816,16 @@
814
816
  },
815
817
 
816
818
  "allOf": [
819
+ {
820
+ "description": "error-analysis task-type requires a structured diagnosis block.",
821
+ "if": {
822
+ "properties": { "header": { "properties": { "taskType": { "const": "error-analysis" } } } },
823
+ "required": ["header"]
824
+ },
825
+ "then": {
826
+ "required": ["errorAnalysis"]
827
+ }
828
+ },
817
829
  {
818
830
  "description": "implementation-planning task-type requires §5.5 block.",
819
831
  "if": {
@@ -952,6 +964,7 @@
952
964
  "Direction": {
953
965
  "enum": [
954
966
  "continue-investigation",
967
+ "begin-planning",
955
968
  "begin-implementation",
956
969
  "approve",
957
970
  "reject",
@@ -959,6 +972,92 @@
959
972
  ]
960
973
  },
961
974
 
975
+ "ErrorAnalysis": {
976
+ "type": "object",
977
+ "additionalProperties": false,
978
+ "required": [
979
+ "symptomVerbatim",
980
+ "observableFailure",
981
+ "reproduction",
982
+ "causeCandidates",
983
+ "nextDiagnostic",
984
+ "routing"
985
+ ],
986
+ "properties": {
987
+ "symptomVerbatim": { "type": "string" },
988
+ "observableFailure": { "type": "string" },
989
+ "reproduction": {
990
+ "type": "object",
991
+ "additionalProperties": false,
992
+ "required": ["status", "evidence", "blockedReason"],
993
+ "properties": {
994
+ "status": {
995
+ "enum": ["reproduced", "not-reproduced", "blocked-before-repro"]
996
+ },
997
+ "evidence": {
998
+ "type": "array",
999
+ "minItems": 1,
1000
+ "items": { "type": "string" }
1001
+ },
1002
+ "blockedReason": { "type": "string" }
1003
+ }
1004
+ },
1005
+ "causeCandidates": {
1006
+ "type": "array",
1007
+ "items": {
1008
+ "type": "object",
1009
+ "additionalProperties": false,
1010
+ "required": [
1011
+ "id",
1012
+ "statement",
1013
+ "supportingEvidence",
1014
+ "falsifyingEvidenceChecked",
1015
+ "confidence",
1016
+ "disproveWith"
1017
+ ],
1018
+ "properties": {
1019
+ "id": { "type": "string", "pattern": "^EA-\\d{3,}$" },
1020
+ "statement": { "type": "string" },
1021
+ "supportingEvidence": {
1022
+ "type": "array",
1023
+ "minItems": 1,
1024
+ "items": { "type": "string" }
1025
+ },
1026
+ "falsifyingEvidenceChecked": {
1027
+ "type": "array",
1028
+ "minItems": 1,
1029
+ "items": { "type": "string" }
1030
+ },
1031
+ "confidence": { "enum": ["low", "medium", "high"] },
1032
+ "disproveWith": { "type": "string", "minLength": 1 }
1033
+ }
1034
+ }
1035
+ },
1036
+ "nextDiagnostic": {
1037
+ "type": "object",
1038
+ "additionalProperties": false,
1039
+ "required": ["action", "confirmingSignal", "rejectingSignal"],
1040
+ "properties": {
1041
+ "action": { "type": "string", "minLength": 1 },
1042
+ "confirmingSignal": { "type": "string", "minLength": 1 },
1043
+ "rejectingSignal": { "type": "string", "minLength": 1 }
1044
+ }
1045
+ },
1046
+ "routing": {
1047
+ "type": "object",
1048
+ "additionalProperties": false,
1049
+ "required": ["nextTaskType", "leadingCauseId", "rationale"],
1050
+ "properties": {
1051
+ "nextTaskType": {
1052
+ "enum": ["error-analysis", "implementation-planning"]
1053
+ },
1054
+ "leadingCauseId": { "type": "string" },
1055
+ "rationale": { "type": "string", "minLength": 1 }
1056
+ }
1057
+ }
1058
+ }
1059
+ },
1060
+
962
1061
  "TicketId": {
963
1062
  "type": "string",
964
1063
  "minLength": 1,
@@ -171,6 +171,32 @@ Carried-forward plan items retain their prior verdicts verbatim; each such item
171
171
 
172
172
  {% endif %}
173
173
 
174
+ {% if header.taskType == 'error-analysis' %}
175
+ ### 2.4 Error Analysis Result{% if t("errorAnalysis.heading") != "Error Analysis Result" %} ({{ t("errorAnalysis.heading") }}){% endif %}
176
+
177
+ - **{{ t("errorAnalysis.symptomVerbatim") }}:** {{ errorAnalysis.symptomVerbatim }}
178
+ - **{{ t("errorAnalysis.observableFailure") }}:** {{ errorAnalysis.observableFailure }}
179
+ - **{{ t("errorAnalysis.reproductionStatus") }}:** `{{ errorAnalysis.reproduction.status }}`
180
+ - **{{ t("errorAnalysis.reproductionEvidence") }}:** {{ errorAnalysis.reproduction.evidence | join(", ") }}
181
+ {% if errorAnalysis.reproduction.blockedReason %}- **{{ t("errorAnalysis.blockedReason") }}:** {{ errorAnalysis.reproduction.blockedReason }}
182
+ {% endif %}
183
+
184
+ {% if errorAnalysis.causeCandidates | length == 0 -%}
185
+ {{ t("emptyState.errorAnalysisCauseCandidates") }}
186
+ {%- else %}
187
+ | ID | {{ t("errorAnalysis.candidate") }} | {{ t("errorAnalysis.supportingEvidence") }} | {{ t("errorAnalysis.falsifyingEvidence") }} | {{ t("errorAnalysis.confidence") }} | {{ t("errorAnalysis.disproveWith") }} |
188
+ |---|---|---|---|---|---|
189
+ {% for row in errorAnalysis.causeCandidates -%}
190
+ | {{ row.id | mdcell }} | {{ row.statement | mdcell }} | {{ row.supportingEvidence | join(", ") | mdcell }} | {{ row.falsifyingEvidenceChecked | join(", ") | mdcell }} | `{{ row.confidence | mdcell }}` | {{ row.disproveWith | mdcell }} |
191
+ {% endfor %}
192
+ {%- endif %}
193
+
194
+ - **{{ t("errorAnalysis.nextDiagnostic") }}:** {{ errorAnalysis.nextDiagnostic.action }}
195
+ - **{{ t("errorAnalysis.confirmingSignal") }}:** {{ errorAnalysis.nextDiagnostic.confirmingSignal }}
196
+ - **{{ t("errorAnalysis.rejectingSignal") }}:** {{ errorAnalysis.nextDiagnostic.rejectingSignal }}
197
+ - **{{ t("errorAnalysis.route") }}:** `{{ errorAnalysis.routing.nextTaskType }}`{% if errorAnalysis.routing.leadingCauseId %} — `{{ errorAnalysis.routing.leadingCauseId }}`{% endif %} — {{ errorAnalysis.routing.rationale }}
198
+ {% endif %}
199
+
174
200
  ## 3. Recommended Next Steps
175
201
 
176
202
  {% if recommendedNextSteps | length == 0 -%}
@@ -19,7 +19,8 @@
19
19
  "lingeringRisks": "- No tracked lingering risks.",
20
20
  "noClarification": "- No additional information requested. The Section 7 verdict stands as-is.",
21
21
  "noFollowUp": "- No follow-up tasks. The next phase for this run is in §3 (Recommended Next Steps).",
22
- "endStateCoverage": "No end-state coverage recorded for this phase."
22
+ "endStateCoverage": "No end-state coverage recorded for this phase.",
23
+ "errorAnalysisCauseCandidates": "No evidence-backed cause candidate yet."
23
24
  },
24
25
  "columns": {
25
26
  "recordMeta": "Record",
@@ -97,8 +98,25 @@
97
98
  "columnSections": "Sections",
98
99
  "columnRelatedIds": "Related item IDs"
99
100
  },
101
+ "errorAnalysis": {
102
+ "heading": "Error Analysis Result",
103
+ "symptomVerbatim": "Symptom Verbatim",
104
+ "observableFailure": "Observable Failure",
105
+ "reproductionStatus": "Reproduction Status",
106
+ "reproductionEvidence": "Reproduction Evidence",
107
+ "blockedReason": "Blocked Reason",
108
+ "candidate": "Cause Candidate",
109
+ "supportingEvidence": "Supporting Evidence",
110
+ "falsifyingEvidence": "Falsifying Evidence",
111
+ "confidence": "Confidence",
112
+ "disproveWith": "Disprove With",
113
+ "nextDiagnostic": "Next Diagnostic",
114
+ "confirmingSignal": "Confirming Signal",
115
+ "rejectingSignal": "Rejecting Signal",
116
+ "route": "Route"
117
+ },
100
118
  "finalVerdict": {
101
- "intro": "This run's final conclusion and next action. **`Direction`** is the recommended next action — one of `continue-investigation`, `begin-implementation`, `approve`, `reject`, or `hold` — and is present for every task-type. **`Verdict Token`** is meaningful only for the `final-verification` task-type, where it is one of `accepted`, `conditional-accept`, or `blocked` and serves as the `release-handoff` entry gate. For every other task-type, `Verdict Token` is always `not-applicable`."
119
+ "intro": "This run's final conclusion and next action. **`Direction`** is the recommended next action — one of `continue-investigation`, `begin-planning`, `begin-implementation`, `approve`, `reject`, or `hold` — and is present for every task-type. `begin-planning` means the diagnosis is ready to enter `implementation-planning`. **`Verdict Token`** is meaningful only for the `final-verification` task-type, where it is one of `accepted`, `conditional-accept`, or `blocked` and serves as the `release-handoff` entry gate. For every other task-type, `Verdict Token` is always `not-applicable`."
102
120
  },
103
121
  "evidence": {
104
122
  "sourceItemsColumnNote": "The `Source items` column is described in §6.1."
@@ -19,7 +19,8 @@
19
19
  "lingeringRisks": "- 추적 대상 잔존 위험 없음.",
20
20
  "noClarification": "- 추가 정보 요청 없음. Section 7 의 최종 판단이 그대로 유효합니다.",
21
21
  "noFollowUp": "- 후속 작업 없음. 본 run 의 다음 phase 는 §3 (Recommended Next Steps) 참고.",
22
- "endStateCoverage": "이번 phase 에 기록된 종료 상태 처리가 없습니다."
22
+ "endStateCoverage": "이번 phase 에 기록된 종료 상태 처리가 없습니다.",
23
+ "errorAnalysisCauseCandidates": "근거가 뒷받침하는 원인 후보가 아직 없습니다."
23
24
  },
24
25
  "columns": {
25
26
  "recordMeta": "항목",
@@ -97,8 +98,25 @@
97
98
  "columnSections": "등장 섹션",
98
99
  "columnRelatedIds": "관련 항목 IDs"
99
100
  },
101
+ "errorAnalysis": {
102
+ "heading": "오류 분석 결과",
103
+ "symptomVerbatim": "증상 원문",
104
+ "observableFailure": "관측된 실패",
105
+ "reproductionStatus": "재현 상태",
106
+ "reproductionEvidence": "재현 근거",
107
+ "blockedReason": "차단 사유",
108
+ "candidate": "원인 후보",
109
+ "supportingEvidence": "지지 근거",
110
+ "falsifyingEvidence": "반증 확인",
111
+ "confidence": "신뢰도",
112
+ "disproveWith": "반증 방법",
113
+ "nextDiagnostic": "다음 진단",
114
+ "confirmingSignal": "확인 신호",
115
+ "rejectingSignal": "기각 신호",
116
+ "route": "라우팅"
117
+ },
100
118
  "finalVerdict": {
101
- "intro": "이 run 의 최종 결론과 다음 행동입니다. **`Direction`** 은 권장 다음 행동으로 `continue-investigation`(조사 계속) · `begin-implementation`(구현 시작) · `approve`(승인) · `reject`(반려) · `hold`(보류) 중 하나이며, 모든 task-type 에 존재합니다. **`Verdict Token`** 은 `final-verification` task-type 에서만 의미를 가집니다 — `accepted` · `conditional-accept` · `blocked` 중 하나로 `release-handoff` 진입 게이트로 쓰입니다. 그 외 task-type 에서 `Verdict Token` 은 항상 `not-applicable`(해당 없음) 입니다."
119
+ "intro": "이 run 의 최종 결론과 다음 행동입니다. **`Direction`** 은 권장 다음 행동으로 `continue-investigation`(조사 계속) · `begin-planning`(계획 시작) · `begin-implementation`(구현 시작) · `approve`(승인) · `reject`(반려) · `hold`(보류) 중 하나이며, 모든 task-type 에 존재합니다. `begin-planning`은 진단이 `implementation-planning`에 진입할 준비가 됐음을 뜻합니다. **`Verdict Token`** 은 `final-verification` task-type 에서만 의미를 가집니다 — `accepted` · `conditional-accept` · `blocked` 중 하나로 `release-handoff` 진입 게이트로 쓰입니다. 그 외 task-type 에서 `Verdict Token` 은 항상 `not-applicable`(해당 없음) 입니다."
102
120
  },
103
121
  "evidence": {
104
122
  "sourceItemsColumnNote": "`Source items` 열 설명은 §6.1 과 동일합니다."
@@ -841,6 +841,10 @@ def validate_brief(path: Path, briefs_root: Path) -> list[str]:
841
841
 
842
842
 
843
843
  def find_briefs(root: Path) -> Iterable[Path]:
844
+ if root.is_file():
845
+ if root.suffix == ".md":
846
+ yield root
847
+ return
844
848
  yield from root.rglob("*.md")
845
849
 
846
850
 
@@ -849,7 +853,7 @@ def main(argv: list[str] | None = None) -> int:
849
853
  parser.add_argument(
850
854
  "briefs_dir",
851
855
  type=Path,
852
- help="Directory containing brief markdown files (recursed).",
856
+ help="Brief markdown file or directory containing brief files.",
853
857
  )
854
858
  parser.add_argument(
855
859
  "--briefs-root",