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.
- package/dist/cli-registry.mjs +16 -2
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.d.mts +4 -2
- package/dist/commands/execute/render-bundle.mjs +46 -5
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/dist/commands/execute/run.mjs +11 -3
- package/dist/commands/execute/run.mjs.map +1 -1
- package/dist/commands/inspect/model-io.d.mts +1 -0
- package/dist/commands/inspect/model-io.mjs +25 -0
- package/dist/commands/inspect/model-io.mjs.map +1 -0
- package/dist/commands/inspect/stage-map.mjs +29 -8
- package/dist/commands/inspect/stage-map.mjs.map +1 -1
- package/dist/commands/inspect/task-list.mjs +52 -6
- package/dist/commands/inspect/task-list.mjs.map +1 -1
- package/dist/commands/inspect/user-response.mjs +14 -4
- package/dist/commands/inspect/user-response.mjs.map +1 -1
- package/dist/commands/lifecycle/check-project.d.mts +1 -0
- package/dist/commands/lifecycle/check-project.mjs +69 -50
- package/dist/commands/lifecycle/check-project.mjs.map +1 -1
- package/dist/commands/lifecycle/contract-check.d.mts +1 -0
- package/dist/commands/lifecycle/contract-check.mjs +18 -0
- package/dist/commands/lifecycle/contract-check.mjs.map +1 -0
- package/dist/commands/lifecycle/preflight.mjs +154 -51
- package/dist/commands/lifecycle/preflight.mjs.map +1 -1
- package/dist/commands/pr/pr.d.mts +1 -0
- package/dist/commands/pr/pr.mjs +19 -1
- package/dist/commands/pr/pr.mjs.map +1 -1
- package/dist/commands/report/agent-activity.mjs +2 -2
- package/dist/commands/report/translate.mjs +3 -0
- package/dist/commands/report/translate.mjs.map +1 -1
- package/dist/lib/host-registry-client.mjs +13 -9
- package/dist/lib/host-registry-client.mjs.map +1 -1
- package/docs/architecture.md +11 -0
- package/docs/cli.md +26 -13
- package/docs/container.md +6 -4
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-brief-gen.md +5 -3
- package/docs/for-ai/skills/okstra-code-review.md +4 -4
- package/docs/for-ai/skills/okstra-container-build.md +20 -17
- package/docs/for-ai/skills/okstra-inspect.md +20 -23
- package/docs/for-ai/skills/okstra-manager.md +19 -18
- package/docs/for-ai/skills/okstra-memory.md +2 -2
- package/docs/for-ai/skills/okstra-pr-gen.md +3 -3
- package/docs/for-ai/skills/okstra-rollup.md +14 -13
- package/docs/for-ai/skills/okstra-run.md +7 -3
- package/docs/for-ai/skills/okstra-schedule-gen.md +15 -18
- package/docs/for-ai/skills/okstra-setup.md +7 -7
- package/docs/for-ai/skills/okstra-usage.md +5 -4
- package/docs/for-ai/skills/okstra-user-response.md +50 -32
- package/docs/project-structure-overview.md +29 -26
- package/docs/task-process/implementation.md +7 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +4 -4
- package/runtime/agents/workers/report-writer-worker.md +3 -3
- package/runtime/agents/workers/translator-worker.md +5 -13
- package/runtime/bin/okstra-error-log.py +51 -11
- package/runtime/bin/okstra-report-translate.py +210 -23
- package/runtime/prompts/host-orchestration/implementation.md +1 -1
- package/runtime/prompts/launch.template.md +3 -7
- package/runtime/prompts/lead/context-loader.md +41 -141
- package/runtime/prompts/lead/convergence.md +8 -6
- package/runtime/prompts/lead/okstra-lead-contract.md +25 -35
- package/runtime/prompts/lead/plan-body-verification.md +187 -14
- package/runtime/prompts/lead/report-writer.md +20 -1
- package/runtime/prompts/lead/team-contract.md +8 -53
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +3 -2
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +8 -2
- package/runtime/prompts/profiles/release-handoff.md +0 -1
- package/runtime/prompts/wizard/prompts.ko.json +0 -1
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +69 -17
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +13 -4
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +6 -1
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +50 -5
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +66 -5
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +70 -2
- package/runtime/python/okstra_ctl/agent_activity.py +118 -35
- package/runtime/python/okstra_ctl/agent_invocation.py +19 -6
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +65 -18
- package/runtime/python/okstra_ctl/analysis_inputs.py +5 -4
- package/runtime/python/okstra_ctl/analysis_packet.py +81 -1
- package/runtime/python/okstra_ctl/approval_decisions.py +3 -2
- package/runtime/python/okstra_ctl/attempt_evidence.py +2 -2
- package/runtime/python/okstra_ctl/backfill.py +13 -10
- package/runtime/python/okstra_ctl/batch.py +2 -4
- package/runtime/python/okstra_ctl/build_tools.py +6 -3
- package/runtime/python/okstra_ctl/claim_reproduction.py +101 -0
- package/runtime/python/okstra_ctl/clarification_items.py +27 -13
- package/runtime/python/okstra_ctl/cmux.py +130 -52
- package/runtime/python/okstra_ctl/code_review_target.py +34 -8
- package/runtime/python/okstra_ctl/consumers.py +5 -4
- package/runtime/python/okstra_ctl/container.py +103 -8
- package/runtime/python/okstra_ctl/context_cost.py +2 -1
- package/runtime/python/okstra_ctl/contract_graph.py +497 -0
- package/runtime/python/okstra_ctl/contract_graph_cli.py +62 -0
- package/runtime/python/okstra_ctl/convergence.py +338 -17
- package/runtime/python/okstra_ctl/convergence_engine.py +10 -18
- package/runtime/python/okstra_ctl/convergence_provenance.py +58 -8
- package/runtime/python/okstra_ctl/convergence_store.py +55 -34
- package/runtime/python/okstra_ctl/design_prep.py +7 -4
- package/runtime/python/okstra_ctl/dispatch_core.py +35 -65
- package/runtime/python/okstra_ctl/dispatch_state.py +134 -59
- package/runtime/python/okstra_ctl/doctor.py +6 -3
- package/runtime/python/okstra_ctl/domain/worker_presentation.py +70 -9
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +16 -30
- package/runtime/python/okstra_ctl/error_log_write.py +35 -30
- package/runtime/python/okstra_ctl/error_report.py +26 -1
- package/runtime/python/okstra_ctl/error_zip.py +27 -5
- package/runtime/python/okstra_ctl/execution_identity.py +3 -2
- package/runtime/python/okstra_ctl/execution_manifest.py +7 -4
- package/runtime/python/okstra_ctl/final_report_schema.py +2 -2
- package/runtime/python/okstra_ctl/fix_cycles.py +2 -2
- package/runtime/python/okstra_ctl/fixed_text.py +39 -0
- package/runtime/python/okstra_ctl/git_reconcile.py +41 -9
- package/runtime/python/okstra_ctl/handoff.py +5 -4
- package/runtime/python/okstra_ctl/i18n.py +4 -2
- package/runtime/python/okstra_ctl/implementation_direction.py +22 -14
- package/runtime/python/okstra_ctl/implementation_outcome.py +4 -7
- package/runtime/python/okstra_ctl/incremental_carry.py +2 -1
- package/runtime/python/okstra_ctl/incremental_scope.py +8 -3
- package/runtime/python/okstra_ctl/index.py +8 -11
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +79 -7
- package/runtime/python/okstra_ctl/invocation.py +3 -6
- package/runtime/python/okstra_ctl/json_boundary.py +366 -0
- package/runtime/python/okstra_ctl/json_registry.py +10 -12
- package/runtime/python/okstra_ctl/jsonl.py +19 -2
- package/runtime/python/okstra_ctl/lead_events.py +33 -1
- package/runtime/python/okstra_ctl/listing.py +3 -3
- package/runtime/python/okstra_ctl/log_report.py +24 -2
- package/runtime/python/okstra_ctl/manager_cli.py +92 -7
- package/runtime/python/okstra_ctl/manager_store.py +12 -10
- package/runtime/python/okstra_ctl/material.py +5 -1
- package/runtime/python/okstra_ctl/migrate.py +29 -25
- package/runtime/python/okstra_ctl/model_cli.py +3 -15
- package/runtime/python/okstra_ctl/model_io_cli.py +1051 -0
- package/runtime/python/okstra_ctl/mutation_probe.py +13 -4
- package/runtime/python/okstra_ctl/pane_reclaim.py +3 -2
- package/runtime/python/okstra_ctl/paths.py +9 -0
- package/runtime/python/okstra_ctl/plan_items.py +115 -4
- package/runtime/python/okstra_ctl/plan_items_cli.py +513 -18
- package/runtime/python/okstra_ctl/pr_template.py +3 -2
- package/runtime/python/okstra_ctl/project_meta.py +5 -7
- package/runtime/python/okstra_ctl/recap.py +5 -4
- package/runtime/python/okstra_ctl/reconcile.py +21 -27
- package/runtime/python/okstra_ctl/registry/host_discovery.py +3 -2
- package/runtime/python/okstra_ctl/registry/provider_registry.py +3 -2
- package/runtime/python/okstra_ctl/render.py +26 -15
- package/runtime/python/okstra_ctl/render_final_report.py +3 -2
- package/runtime/python/okstra_ctl/report_assembly.py +172 -17
- package/runtime/python/okstra_ctl/report_finalize.py +7 -10
- package/runtime/python/okstra_ctl/report_html/render.py +3 -2
- package/runtime/python/okstra_ctl/report_language.py +3 -2
- package/runtime/python/okstra_ctl/report_markdown.py +13 -1
- package/runtime/python/okstra_ctl/report_narrative.py +40 -8
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +518 -0
- package/runtime/python/okstra_ctl/report_views.py +3 -2
- package/runtime/python/okstra_ctl/rollup.py +65 -4
- package/runtime/python/okstra_ctl/run.py +159 -56
- package/runtime/python/okstra_ctl/run_audit.py +3 -2
- package/runtime/python/okstra_ctl/run_context.py +6 -9
- package/runtime/python/okstra_ctl/run_index_row.py +2 -8
- package/runtime/python/okstra_ctl/schedule_semantics.py +5 -2
- package/runtime/python/okstra_ctl/schema_excerpt.py +4 -2
- package/runtime/python/okstra_ctl/session_transcript.py +27 -1
- package/runtime/python/okstra_ctl/set_work_status.py +64 -38
- package/runtime/python/okstra_ctl/stage_fix_carry.py +4 -2
- package/runtime/python/okstra_ctl/stage_map.py +26 -6
- package/runtime/python/okstra_ctl/stage_targets.py +3 -4
- package/runtime/python/okstra_ctl/team.py +2 -1
- package/runtime/python/okstra_ctl/team_reconcile.py +11 -2
- package/runtime/python/okstra_ctl/time_report.py +51 -4
- package/runtime/python/okstra_ctl/usage_identity.py +2 -1
- package/runtime/python/okstra_ctl/usage_report.py +58 -4
- package/runtime/python/okstra_ctl/user_response.py +1431 -66
- package/runtime/python/okstra_ctl/wizard.py +11 -44
- package/runtime/python/okstra_ctl/work_categories.py +3 -2
- package/runtime/python/okstra_ctl/worker_prompt_body.py +18 -7
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -2
- package/runtime/python/okstra_ctl/worker_runner.py +14 -12
- package/runtime/python/okstra_ctl/workflow.py +2 -1
- package/runtime/python/okstra_ctl/worktree.py +3 -2
- package/runtime/python/okstra_ctl/wrapper_status.py +4 -2
- package/runtime/python/okstra_ctl/write_policy.py +4 -2
- package/runtime/python/okstra_token_usage/antigravity.py +39 -12
- package/runtime/python/okstra_token_usage/collect.py +90 -38
- package/runtime/python/okstra_token_usage/grok.py +127 -0
- package/runtime/schemas/final-report-v2.0.schema.json +9 -0
- package/runtime/schemas/final-report-v3.0.schema.json +9 -0
- package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +140 -0
- package/runtime/skills/okstra-brief-gen/SKILL.md +9 -7
- package/runtime/skills/okstra-code-review/SKILL.md +21 -11
- package/runtime/skills/okstra-container-build/SKILL.md +18 -18
- package/runtime/skills/okstra-inspect/SKILL.md +12 -11
- package/runtime/skills/okstra-inspect/facets/error-zip.md +8 -8
- package/runtime/skills/okstra-inspect/facets/errors.md +2 -2
- package/runtime/skills/okstra-inspect/facets/history.md +9 -14
- package/runtime/skills/okstra-inspect/facets/logs.md +2 -2
- package/runtime/skills/okstra-inspect/facets/recap.md +5 -5
- package/runtime/skills/okstra-inspect/facets/report.md +6 -10
- package/runtime/skills/okstra-inspect/facets/status.md +9 -8
- package/runtime/skills/okstra-inspect/facets/time.md +3 -3
- package/runtime/skills/okstra-manager/SKILL.md +16 -14
- package/runtime/skills/okstra-memory/SKILL.md +3 -3
- package/runtime/skills/okstra-pr-gen/SKILL.md +5 -4
- package/runtime/skills/okstra-rollup/SKILL.md +6 -16
- package/runtime/skills/okstra-run/SKILL.md +7 -7
- package/runtime/skills/okstra-schedule-gen/SKILL.md +21 -17
- package/runtime/skills/okstra-setup/SKILL.md +21 -13
- package/runtime/skills/okstra-setup/references/project-config.md +2 -2
- package/runtime/skills/okstra-usage/SKILL.md +10 -10
- package/runtime/skills/okstra-user-response/SKILL.md +78 -107
- package/runtime/templates/report-writer-prompt-preamble.md +17 -1
- package/runtime/templates/reports/schedule.template.md +4 -4
- package/runtime/templates/worker-error-contract.md +17 -29
- package/runtime/validators/validate-run.py +379 -63
- 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 =
|
|
393
|
-
except
|
|
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
|
-
|
|
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 =
|
|
608
|
-
except
|
|
609
|
-
raise FinalizeError(f"run manifest not
|
|
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 =
|
|
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 =
|
|
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 =
|
|
54
|
-
except
|
|
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) ==
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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:
|