okstra 0.180.0 → 0.183.2

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.
Files changed (223) hide show
  1. package/dist/cli-registry.mjs +16 -2
  2. package/dist/cli-registry.mjs.map +1 -1
  3. package/dist/commands/execute/render-bundle.d.mts +4 -2
  4. package/dist/commands/execute/render-bundle.mjs +46 -5
  5. package/dist/commands/execute/render-bundle.mjs.map +1 -1
  6. package/dist/commands/execute/run.mjs +11 -3
  7. package/dist/commands/execute/run.mjs.map +1 -1
  8. package/dist/commands/inspect/model-io.d.mts +1 -0
  9. package/dist/commands/inspect/model-io.mjs +25 -0
  10. package/dist/commands/inspect/model-io.mjs.map +1 -0
  11. package/dist/commands/inspect/stage-map.mjs +29 -8
  12. package/dist/commands/inspect/stage-map.mjs.map +1 -1
  13. package/dist/commands/inspect/task-list.mjs +52 -6
  14. package/dist/commands/inspect/task-list.mjs.map +1 -1
  15. package/dist/commands/inspect/user-response.mjs +14 -4
  16. package/dist/commands/inspect/user-response.mjs.map +1 -1
  17. package/dist/commands/lifecycle/check-project.d.mts +1 -0
  18. package/dist/commands/lifecycle/check-project.mjs +69 -50
  19. package/dist/commands/lifecycle/check-project.mjs.map +1 -1
  20. package/dist/commands/lifecycle/contract-check.d.mts +1 -0
  21. package/dist/commands/lifecycle/contract-check.mjs +18 -0
  22. package/dist/commands/lifecycle/contract-check.mjs.map +1 -0
  23. package/dist/commands/lifecycle/preflight.mjs +154 -51
  24. package/dist/commands/lifecycle/preflight.mjs.map +1 -1
  25. package/dist/commands/pr/pr.d.mts +1 -0
  26. package/dist/commands/pr/pr.mjs +19 -1
  27. package/dist/commands/pr/pr.mjs.map +1 -1
  28. package/dist/commands/report/agent-activity.mjs +2 -2
  29. package/dist/commands/report/translate.mjs +3 -0
  30. package/dist/commands/report/translate.mjs.map +1 -1
  31. package/dist/lib/host-registry-client.mjs +13 -9
  32. package/dist/lib/host-registry-client.mjs.map +1 -1
  33. package/docs/architecture.md +11 -0
  34. package/docs/cli.md +26 -13
  35. package/docs/container.md +6 -4
  36. package/docs/contributor-change-matrix.md +1 -1
  37. package/docs/for-ai/README.md +2 -2
  38. package/docs/for-ai/skills/okstra-brief-gen.md +5 -3
  39. package/docs/for-ai/skills/okstra-code-review.md +4 -4
  40. package/docs/for-ai/skills/okstra-container-build.md +20 -17
  41. package/docs/for-ai/skills/okstra-inspect.md +20 -23
  42. package/docs/for-ai/skills/okstra-manager.md +19 -18
  43. package/docs/for-ai/skills/okstra-memory.md +2 -2
  44. package/docs/for-ai/skills/okstra-pr-gen.md +3 -3
  45. package/docs/for-ai/skills/okstra-rollup.md +14 -13
  46. package/docs/for-ai/skills/okstra-run.md +7 -3
  47. package/docs/for-ai/skills/okstra-schedule-gen.md +15 -18
  48. package/docs/for-ai/skills/okstra-setup.md +7 -7
  49. package/docs/for-ai/skills/okstra-usage.md +5 -4
  50. package/docs/for-ai/skills/okstra-user-response.md +50 -32
  51. package/docs/project-structure-overview.md +29 -26
  52. package/docs/task-process/implementation.md +7 -4
  53. package/package.json +1 -1
  54. package/runtime/BUILD.json +2 -2
  55. package/runtime/agents/workers/claude-worker.md +4 -4
  56. package/runtime/agents/workers/report-writer-worker.md +3 -3
  57. package/runtime/agents/workers/translator-worker.md +5 -13
  58. package/runtime/bin/okstra-error-log.py +51 -11
  59. package/runtime/bin/okstra-report-translate.py +210 -23
  60. package/runtime/prompts/host-orchestration/implementation.md +1 -1
  61. package/runtime/prompts/launch.template.md +3 -7
  62. package/runtime/prompts/lead/context-loader.md +41 -141
  63. package/runtime/prompts/lead/convergence.md +8 -6
  64. package/runtime/prompts/lead/okstra-lead-contract.md +25 -35
  65. package/runtime/prompts/lead/plan-body-verification.md +187 -14
  66. package/runtime/prompts/lead/report-writer.md +20 -1
  67. package/runtime/prompts/lead/team-contract.md +8 -53
  68. package/runtime/prompts/profiles/_coding-conventions-preflight.md +3 -2
  69. package/runtime/prompts/profiles/_common-contract.md +1 -1
  70. package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
  71. package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
  72. package/runtime/prompts/profiles/final-verification.md +1 -1
  73. package/runtime/prompts/profiles/implementation-planning.md +8 -2
  74. package/runtime/prompts/profiles/release-handoff.md +0 -1
  75. package/runtime/prompts/wizard/prompts.ko.json +0 -1
  76. package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +69 -17
  77. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +13 -4
  78. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +6 -1
  79. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
  80. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +50 -5
  81. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
  82. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +66 -5
  83. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +70 -2
  84. package/runtime/python/okstra_ctl/agent_activity.py +118 -35
  85. package/runtime/python/okstra_ctl/agent_invocation.py +19 -6
  86. package/runtime/python/okstra_ctl/agent_prompt_cli.py +65 -18
  87. package/runtime/python/okstra_ctl/analysis_inputs.py +5 -4
  88. package/runtime/python/okstra_ctl/analysis_packet.py +81 -1
  89. package/runtime/python/okstra_ctl/approval_decisions.py +3 -2
  90. package/runtime/python/okstra_ctl/attempt_evidence.py +2 -2
  91. package/runtime/python/okstra_ctl/backfill.py +13 -10
  92. package/runtime/python/okstra_ctl/batch.py +2 -4
  93. package/runtime/python/okstra_ctl/build_tools.py +6 -3
  94. package/runtime/python/okstra_ctl/claim_reproduction.py +101 -0
  95. package/runtime/python/okstra_ctl/clarification_items.py +27 -13
  96. package/runtime/python/okstra_ctl/cmux.py +130 -52
  97. package/runtime/python/okstra_ctl/code_review_target.py +34 -8
  98. package/runtime/python/okstra_ctl/consumers.py +5 -4
  99. package/runtime/python/okstra_ctl/container.py +103 -8
  100. package/runtime/python/okstra_ctl/context_cost.py +2 -1
  101. package/runtime/python/okstra_ctl/contract_graph.py +497 -0
  102. package/runtime/python/okstra_ctl/contract_graph_cli.py +62 -0
  103. package/runtime/python/okstra_ctl/convergence.py +338 -17
  104. package/runtime/python/okstra_ctl/convergence_engine.py +10 -18
  105. package/runtime/python/okstra_ctl/convergence_provenance.py +58 -8
  106. package/runtime/python/okstra_ctl/convergence_store.py +55 -34
  107. package/runtime/python/okstra_ctl/design_prep.py +7 -4
  108. package/runtime/python/okstra_ctl/dispatch_core.py +35 -65
  109. package/runtime/python/okstra_ctl/dispatch_state.py +134 -59
  110. package/runtime/python/okstra_ctl/doctor.py +6 -3
  111. package/runtime/python/okstra_ctl/domain/worker_presentation.py +70 -9
  112. package/runtime/python/okstra_ctl/entrypoints/hosts.py +16 -30
  113. package/runtime/python/okstra_ctl/error_log_write.py +35 -30
  114. package/runtime/python/okstra_ctl/error_report.py +26 -1
  115. package/runtime/python/okstra_ctl/error_zip.py +27 -5
  116. package/runtime/python/okstra_ctl/execution_identity.py +3 -2
  117. package/runtime/python/okstra_ctl/execution_manifest.py +7 -4
  118. package/runtime/python/okstra_ctl/final_report_schema.py +2 -2
  119. package/runtime/python/okstra_ctl/fix_cycles.py +2 -2
  120. package/runtime/python/okstra_ctl/fixed_text.py +39 -0
  121. package/runtime/python/okstra_ctl/git_reconcile.py +41 -9
  122. package/runtime/python/okstra_ctl/handoff.py +5 -4
  123. package/runtime/python/okstra_ctl/i18n.py +4 -2
  124. package/runtime/python/okstra_ctl/implementation_direction.py +22 -14
  125. package/runtime/python/okstra_ctl/implementation_outcome.py +4 -7
  126. package/runtime/python/okstra_ctl/incremental_carry.py +2 -1
  127. package/runtime/python/okstra_ctl/incremental_scope.py +8 -3
  128. package/runtime/python/okstra_ctl/index.py +8 -11
  129. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +79 -7
  130. package/runtime/python/okstra_ctl/invocation.py +3 -6
  131. package/runtime/python/okstra_ctl/json_boundary.py +366 -0
  132. package/runtime/python/okstra_ctl/json_registry.py +10 -12
  133. package/runtime/python/okstra_ctl/jsonl.py +19 -2
  134. package/runtime/python/okstra_ctl/lead_events.py +33 -1
  135. package/runtime/python/okstra_ctl/listing.py +3 -3
  136. package/runtime/python/okstra_ctl/log_report.py +24 -2
  137. package/runtime/python/okstra_ctl/manager_cli.py +92 -7
  138. package/runtime/python/okstra_ctl/manager_store.py +12 -10
  139. package/runtime/python/okstra_ctl/material.py +5 -1
  140. package/runtime/python/okstra_ctl/migrate.py +29 -25
  141. package/runtime/python/okstra_ctl/model_cli.py +3 -15
  142. package/runtime/python/okstra_ctl/model_io_cli.py +1051 -0
  143. package/runtime/python/okstra_ctl/mutation_probe.py +13 -4
  144. package/runtime/python/okstra_ctl/pane_reclaim.py +3 -2
  145. package/runtime/python/okstra_ctl/paths.py +9 -0
  146. package/runtime/python/okstra_ctl/plan_items.py +115 -4
  147. package/runtime/python/okstra_ctl/plan_items_cli.py +513 -18
  148. package/runtime/python/okstra_ctl/pr_template.py +3 -2
  149. package/runtime/python/okstra_ctl/project_meta.py +5 -7
  150. package/runtime/python/okstra_ctl/recap.py +5 -4
  151. package/runtime/python/okstra_ctl/reconcile.py +21 -27
  152. package/runtime/python/okstra_ctl/registry/host_discovery.py +3 -2
  153. package/runtime/python/okstra_ctl/registry/provider_registry.py +3 -2
  154. package/runtime/python/okstra_ctl/render.py +26 -15
  155. package/runtime/python/okstra_ctl/render_final_report.py +3 -2
  156. package/runtime/python/okstra_ctl/report_assembly.py +172 -17
  157. package/runtime/python/okstra_ctl/report_finalize.py +7 -10
  158. package/runtime/python/okstra_ctl/report_html/render.py +3 -2
  159. package/runtime/python/okstra_ctl/report_language.py +3 -2
  160. package/runtime/python/okstra_ctl/report_markdown.py +13 -1
  161. package/runtime/python/okstra_ctl/report_narrative.py +40 -8
  162. package/runtime/python/okstra_ctl/report_synthesis_packet.py +518 -0
  163. package/runtime/python/okstra_ctl/report_views.py +3 -2
  164. package/runtime/python/okstra_ctl/rollup.py +65 -4
  165. package/runtime/python/okstra_ctl/run.py +159 -56
  166. package/runtime/python/okstra_ctl/run_audit.py +3 -2
  167. package/runtime/python/okstra_ctl/run_context.py +6 -9
  168. package/runtime/python/okstra_ctl/run_index_row.py +2 -8
  169. package/runtime/python/okstra_ctl/schedule_semantics.py +5 -2
  170. package/runtime/python/okstra_ctl/schema_excerpt.py +4 -2
  171. package/runtime/python/okstra_ctl/session_transcript.py +27 -1
  172. package/runtime/python/okstra_ctl/set_work_status.py +64 -38
  173. package/runtime/python/okstra_ctl/stage_fix_carry.py +4 -2
  174. package/runtime/python/okstra_ctl/stage_map.py +26 -6
  175. package/runtime/python/okstra_ctl/stage_targets.py +3 -4
  176. package/runtime/python/okstra_ctl/team.py +2 -1
  177. package/runtime/python/okstra_ctl/team_reconcile.py +11 -2
  178. package/runtime/python/okstra_ctl/time_report.py +51 -4
  179. package/runtime/python/okstra_ctl/usage_identity.py +2 -1
  180. package/runtime/python/okstra_ctl/usage_report.py +58 -4
  181. package/runtime/python/okstra_ctl/user_response.py +1431 -66
  182. package/runtime/python/okstra_ctl/wizard.py +11 -44
  183. package/runtime/python/okstra_ctl/work_categories.py +3 -2
  184. package/runtime/python/okstra_ctl/worker_prompt_body.py +18 -7
  185. package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -2
  186. package/runtime/python/okstra_ctl/worker_runner.py +14 -12
  187. package/runtime/python/okstra_ctl/workflow.py +2 -1
  188. package/runtime/python/okstra_ctl/worktree.py +3 -2
  189. package/runtime/python/okstra_ctl/wrapper_status.py +4 -2
  190. package/runtime/python/okstra_ctl/write_policy.py +4 -2
  191. package/runtime/python/okstra_token_usage/antigravity.py +39 -12
  192. package/runtime/python/okstra_token_usage/collect.py +90 -38
  193. package/runtime/python/okstra_token_usage/grok.py +127 -0
  194. package/runtime/schemas/final-report-v2.0.schema.json +9 -0
  195. package/runtime/schemas/final-report-v3.0.schema.json +9 -0
  196. package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +140 -0
  197. package/runtime/skills/okstra-brief-gen/SKILL.md +9 -7
  198. package/runtime/skills/okstra-code-review/SKILL.md +21 -11
  199. package/runtime/skills/okstra-container-build/SKILL.md +18 -18
  200. package/runtime/skills/okstra-inspect/SKILL.md +12 -11
  201. package/runtime/skills/okstra-inspect/facets/error-zip.md +8 -8
  202. package/runtime/skills/okstra-inspect/facets/errors.md +2 -2
  203. package/runtime/skills/okstra-inspect/facets/history.md +9 -14
  204. package/runtime/skills/okstra-inspect/facets/logs.md +2 -2
  205. package/runtime/skills/okstra-inspect/facets/recap.md +5 -5
  206. package/runtime/skills/okstra-inspect/facets/report.md +6 -10
  207. package/runtime/skills/okstra-inspect/facets/status.md +9 -8
  208. package/runtime/skills/okstra-inspect/facets/time.md +3 -3
  209. package/runtime/skills/okstra-manager/SKILL.md +16 -14
  210. package/runtime/skills/okstra-memory/SKILL.md +3 -3
  211. package/runtime/skills/okstra-pr-gen/SKILL.md +5 -4
  212. package/runtime/skills/okstra-rollup/SKILL.md +6 -16
  213. package/runtime/skills/okstra-run/SKILL.md +7 -7
  214. package/runtime/skills/okstra-schedule-gen/SKILL.md +21 -17
  215. package/runtime/skills/okstra-setup/SKILL.md +21 -13
  216. package/runtime/skills/okstra-setup/references/project-config.md +2 -2
  217. package/runtime/skills/okstra-usage/SKILL.md +10 -10
  218. package/runtime/skills/okstra-user-response/SKILL.md +78 -107
  219. package/runtime/templates/report-writer-prompt-preamble.md +17 -1
  220. package/runtime/templates/reports/schedule.template.md +4 -4
  221. package/runtime/templates/worker-error-contract.md +17 -29
  222. package/runtime/validators/validate-run.py +379 -63
  223. package/runtime/validators/validate_session_conformance.py +43 -7
@@ -45,6 +45,7 @@ from .dispatch_state import DispatchError, link_agent_dispatch_result
45
45
  from .final_report_paths import final_report_data_path, final_report_markdown_path
46
46
  from .paths import task_dir, task_manifest_file
47
47
  from .release_gate import release_handoff_allowed
48
+ from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
48
49
  from .stage_integrate import IntegrateError
49
50
  from .stage_targets import (
50
51
  StageTargetError,
@@ -389,8 +390,8 @@ def _teardown_stage_worktrees(
389
390
  if ctx.task_type != "final-verification":
390
391
  return _done({"skipped": "not a final-verification run"})
391
392
  try:
392
- data = json.loads(Path(ctx.data_path).read_text(encoding="utf-8"))
393
- except (OSError, json.JSONDecodeError) as exc:
393
+ data = load_owned_object(Path(ctx.data_path), artifact="final report record")
394
+ except JsonBoundaryError as exc:
394
395
  return subprocess.CompletedProcess(
395
396
  command, 1, "", f"cannot read final-report data.json: {exc}")
396
397
  if data.get("verificationScope") != "whole-task":
@@ -563,9 +564,7 @@ def write_execution_roles(ctx: FinalizeContext) -> None:
563
564
  updated = apply_execution_roles(dict(data), dict(manifest))
564
565
  if updated == data:
565
566
  return
566
- tmp = ctx.data_path.with_suffix(ctx.data_path.suffix + ".tmp")
567
- tmp.write_text(json.dumps(updated, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
568
- tmp.replace(ctx.data_path)
567
+ write_owned_object_atomic(ctx.data_path, updated, artifact="final report record")
569
568
 
570
569
 
571
570
  def _link_lead_result_for_validation(ctx: FinalizeContext) -> None:
@@ -604,11 +603,9 @@ def _link_lead_result_for_validation(ctx: FinalizeContext) -> None:
604
603
 
605
604
  def _load_manifest(path: Path) -> Mapping[str, Any]:
606
605
  try:
607
- payload = json.loads(path.read_text(encoding="utf-8"))
608
- except FileNotFoundError as exc:
609
- raise FinalizeError(f"run manifest not found: {path}") from exc
610
- except json.JSONDecodeError as exc:
611
- raise FinalizeError(f"run manifest is not valid JSON: {path} ({exc})") from exc
606
+ payload = load_owned_object(path, artifact="run manifest")
607
+ except JsonBoundaryError as exc:
608
+ raise FinalizeError(f"run manifest is not valid: {path} ({exc})") from exc
612
609
  if not isinstance(payload, Mapping):
613
610
  raise FinalizeError(f"run manifest is not a JSON object: {path}")
614
611
  return payload
@@ -15,6 +15,7 @@ from ..i18n import HTML_DICTIONARY_REL, load_dictionary, make_jinja_global
15
15
  from ..report_translation import overlay
16
16
  from ..report_view_artifacts import user_responses_dir_for_report
17
17
  from ..usage_cells import format_duration_ms
18
+ from ..json_boundary import load_owned_object
18
19
  from .common import anchor_index
19
20
  from .filters import (
20
21
  code_evidence,
@@ -87,7 +88,7 @@ def _localize(data: dict, data_path: Path) -> tuple[dict, str]:
87
88
  "rendering the English source\n"
88
89
  )
89
90
  return data, lang
90
- payload = json.loads(sidecar_file.read_text(encoding="utf-8"))
91
+ payload = load_owned_object(sidecar_file, artifact="translation sidecar")
91
92
  strings = payload.get("strings")
92
93
  if not isinstance(strings, dict):
93
94
  raise HtmlRenderError(f"translation sidecar has no 'strings' object: {sidecar_file}")
@@ -120,7 +121,7 @@ def render_v2_html_view(
120
121
  no reader ever compared, which made a derived artifact a precondition for
121
122
  another derived artifact.
122
123
  """
123
- data = json.loads(data_path.read_text(encoding="utf-8"))
124
+ data = load_owned_object(data_path, artifact="final report record")
124
125
  errors = validate(data, load_schema_for_data(data))
125
126
  if errors:
126
127
  raise HtmlRenderError("invalid v2 final-report data: " + "; ".join(errors[:5]))
@@ -12,6 +12,7 @@ from pathlib import Path
12
12
  from typing import Any, Mapping
13
13
 
14
14
  from .worker_prompt_body import instruction_path
15
+ from .json_boundary import JsonBoundaryError, load_owned_object
15
16
 
16
17
  REPORT_LANGUAGE_VALUES = {"en", "ko", "auto"}
17
18
 
@@ -50,8 +51,8 @@ def _read_report_language(path: Path, label: str) -> str:
50
51
  if not path.is_file():
51
52
  return ""
52
53
  try:
53
- payload = json.loads(path.read_text(encoding="utf-8"))
54
- except (OSError, json.JSONDecodeError) as exc:
54
+ payload = load_owned_object(path, artifact=f"{label} configuration")
55
+ except JsonBoundaryError as exc:
55
56
  raise ReportLanguageError(f"{label} config is unreadable: {path}") from exc
56
57
  if not isinstance(payload, Mapping):
57
58
  raise ReportLanguageError(f"{label} config must be a JSON object: {path}")
@@ -95,6 +95,17 @@ def humanise(key: str) -> str:
95
95
  )
96
96
 
97
97
 
98
+ def label_key(label: str) -> str:
99
+ """라벨 대조용 정규화 표기.
100
+
101
+ `humanise` 는 `_ACRONYMS` 표에 있는 낱말만 대문자로 올린다(`ticketId` ->
102
+ `Ticket ID`). 그 표는 저작자에게 전달되지 않으므로 `Ticket Id` 라고 쓰면
103
+ 글자 하나 때문에 리포트 전체가 되돌아온다. 대소문자와 낱말 구분 문자를
104
+ 지운 표기로 맞춰, 약어 표를 모르고도 쓸 수 있게 한다.
105
+ """
106
+ return re.sub(r"[^a-z0-9]", "", str(label).lower())
107
+
108
+
98
109
  class SchemaIndex:
99
110
  """Property-order lookup over the final-report schema.
100
111
 
@@ -161,6 +172,7 @@ class SchemaIndex:
161
172
 
162
173
  def keys_for_label(self, label: str) -> list[str]:
163
174
  """전체 스키마에서 사람이 읽는 표기가 일치하는 고유 키 후보."""
175
+ wanted = label_key(label)
164
176
  found: set[str] = set()
165
177
  pending: list[Any] = [self._schema]
166
178
  seen: set[int] = set()
@@ -173,7 +185,7 @@ class SchemaIndex:
173
185
  continue
174
186
  seen.add(id(node))
175
187
  for key in (node.get("properties") or {}):
176
- if humanise(key) == label:
188
+ if label_key(humanise(key)) == wanted:
177
189
  found.add(key)
178
190
  pending.extend(node.values())
179
191
  return sorted(found)
@@ -8,7 +8,8 @@ from pathlib import Path
8
8
  from typing import Any, Mapping
9
9
 
10
10
  from .final_report_schema import validate
11
- from .report_markdown import SchemaIndex, humanise
11
+ from .report_markdown import SchemaIndex, humanise, label_key
12
+ from .json_boundary import load_owned_object
12
13
 
13
14
 
14
15
  TITLE = "# OKSTRA Report Narrative"
@@ -42,7 +43,7 @@ def _narrative_schema_path() -> Path:
42
43
 
43
44
 
44
45
  def _narrative_schema() -> dict[str, Any]:
45
- return json.loads(_narrative_schema_path().read_text(encoding="utf-8"))
46
+ return load_owned_object(_narrative_schema_path(), artifact="report narrative schema")
46
47
 
47
48
 
48
49
  def _allowed_top_level() -> frozenset[str]:
@@ -191,7 +192,13 @@ def _parse_tree(markdown: str) -> _Node:
191
192
  level = _line_level(value.group("indent"), number)
192
193
  _append_value(stack, value.group("value"), level, number)
193
194
  else:
194
- raise NarrativeContractError(f"line {number}: unsupported Markdown syntax")
195
+ raise NarrativeContractError(
196
+ f"line {number}: unsupported Markdown syntax `{line.strip()}` — "
197
+ "a narrative line is only `- **Field Name**`, `- Item <N>`, or "
198
+ "`> value`. Headings, tables at column 0, code fences, bare "
199
+ "paragraphs, JSON and YAML are rejected; put such text inside a "
200
+ "`> ` value line instead"
201
+ )
195
202
  return root
196
203
 
197
204
 
@@ -217,17 +224,38 @@ def _append_value(stack: list[_Node], value: str, level: int, number: int) -> No
217
224
  stack[-1].values.append(value)
218
225
 
219
226
 
227
+ def _allowed_labels(node: Any, index: SchemaIndex, path: str) -> list[str]:
228
+ """이 위치에서 저작자가 쓸 수 있는 사람이 읽는 필드 표기."""
229
+ keys = index.key_order(node)
230
+ if not path:
231
+ keys = [key for key in keys if key in _allowed_top_level()]
232
+ # 파서가 뒤에서 거부할 필드를 여기서 권하면 안내가 다음 거부를 만든다.
233
+ keys = [key for key in keys if f"{path}.{key}".lstrip(".") not in _NESTED_FORBIDDEN]
234
+ return sorted(humanise(key) for key in keys)
235
+
236
+
220
237
  def _field_key(label: str, node: Any, index: SchemaIndex, path: str) -> str:
221
238
  candidates: dict[str, list[str]] = {}
222
239
  for key in index.key_order(node):
223
- candidates.setdefault(humanise(key), []).append(key)
224
- matches = candidates.get(label, [])
240
+ candidates.setdefault(label_key(humanise(key)), []).append(key)
241
+ matches = candidates.get(label_key(label), [])
225
242
  if not matches:
226
243
  matches = index.keys_for_label(label)
227
244
  display_path = f"{path}.{label}" if path else label
228
- if len(matches) != 1:
245
+ # 거부만 알리면 다음 시도도 추측이 된다. 이 위치에서 허용되는 표기를
246
+ # 함께 실어, 재작성이 목록 대조로 끝나게 한다.
247
+ if not matches:
248
+ allowed = _allowed_labels(node, index, path)
249
+ listing = ", ".join(f"`{item}`" for item in allowed) or "(none)"
250
+ raise NarrativeContractError(
251
+ f"owner=report-writer field `{display_path}` is not an allowed unique "
252
+ f"field; allowed at this position: {listing}"
253
+ )
254
+ if len(matches) > 1:
255
+ collisions = ", ".join(f"`{key}`" for key in matches)
229
256
  raise NarrativeContractError(
230
- f"owner=report-writer field `{display_path}` is not an allowed unique field"
257
+ f"owner=report-writer field `{display_path}` is not an allowed unique "
258
+ f"field; the label matches more than one schema key: {collisions}"
231
259
  )
232
260
  return matches[0]
233
261
 
@@ -310,8 +338,12 @@ def parse_narrative(markdown: str, schema: Mapping[str, Any]) -> dict[str, Any]:
310
338
  unknown = sorted(set(result) - _allowed_top_level())
311
339
  if unknown:
312
340
  labels = [humanise(key) for key in unknown]
341
+ owned = ", ".join(
342
+ f"`{humanise(key)}`" for key in sorted(_allowed_top_level())
343
+ )
313
344
  raise NarrativeContractError(
314
- f"owner=report-writer cannot author fields: {labels}"
345
+ f"owner=report-writer cannot author fields: {labels}; "
346
+ f"writer-owned top-level fields: {owned}"
315
347
  )
316
348
  errors = validate(result, _narrative_schema())
317
349
  if errors: