python-hwpx-automation 6.0.3__py3-none-any.whl
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.
- hwpx_automation/__init__.py +61 -0
- hwpx_automation/__init__.pyi +27 -0
- hwpx_automation/__main__.py +8 -0
- hwpx_automation/agent_document.py +392 -0
- hwpx_automation/api.py +136 -0
- hwpx_automation/blind_eval.py +407 -0
- hwpx_automation/capabilities.py +110 -0
- hwpx_automation/compat.py +48 -0
- hwpx_automation/configuration.py +60 -0
- hwpx_automation/core/__init__.py +2 -0
- hwpx_automation/core/content.py +762 -0
- hwpx_automation/core/context.py +111 -0
- hwpx_automation/core/diff.py +53 -0
- hwpx_automation/core/document.py +37 -0
- hwpx_automation/core/formatting.py +513 -0
- hwpx_automation/core/handles.py +24 -0
- hwpx_automation/core/locations.py +205 -0
- hwpx_automation/core/locator.py +162 -0
- hwpx_automation/core/plan.py +680 -0
- hwpx_automation/core/resources.py +42 -0
- hwpx_automation/core/search.py +296 -0
- hwpx_automation/core/transactions.py +434 -0
- hwpx_automation/core/txn.py +48 -0
- hwpx_automation/document_state.py +95 -0
- hwpx_automation/errors.py +174 -0
- hwpx_automation/execution_lock.py +15 -0
- hwpx_automation/fastmcp_adapter.py +672 -0
- hwpx_automation/form_fill.py +1177 -0
- hwpx_automation/form_output_models.py +223 -0
- hwpx_automation/handlers/__init__.py +2 -0
- hwpx_automation/handlers/_shared.py +377 -0
- hwpx_automation/handlers/agent_document.py +257 -0
- hwpx_automation/handlers/authoring.py +750 -0
- hwpx_automation/handlers/content_edit.py +1078 -0
- hwpx_automation/handlers/form_fill.py +607 -0
- hwpx_automation/handlers/layout_style.py +660 -0
- hwpx_automation/handlers/quality_render.py +566 -0
- hwpx_automation/handlers/read_export.py +1295 -0
- hwpx_automation/handlers/specialized.py +624 -0
- hwpx_automation/handlers/tracked_changes.py +589 -0
- hwpx_automation/handlers/workflow.py +105 -0
- hwpx_automation/hwp_converter.py +227 -0
- hwpx_automation/hwp_support.py +94 -0
- hwpx_automation/hwpx_ops.py +1439 -0
- hwpx_automation/identity.json +263 -0
- hwpx_automation/identity.py +18 -0
- hwpx_automation/ingest_adapters.py +85 -0
- hwpx_automation/markdown_plan.py +216 -0
- hwpx_automation/mcp_cli.py +29 -0
- hwpx_automation/metadata/tools_meta.py +40 -0
- hwpx_automation/mixed_form.py +3007 -0
- hwpx_automation/mutation_models.py +401 -0
- hwpx_automation/network_policy.py +232 -0
- hwpx_automation/office/__init__.py +14 -0
- hwpx_automation/office/agent/__init__.py +125 -0
- hwpx_automation/office/agent/_batch_verification.py +383 -0
- hwpx_automation/office/agent/blueprint/__init__.py +58 -0
- hwpx_automation/office/agent/blueprint/bundle.py +282 -0
- hwpx_automation/office/agent/blueprint/catalog.py +136 -0
- hwpx_automation/office/agent/blueprint/dump.py +520 -0
- hwpx_automation/office/agent/blueprint/mapping.py +312 -0
- hwpx_automation/office/agent/blueprint/model.py +722 -0
- hwpx_automation/office/agent/blueprint/native.py +621 -0
- hwpx_automation/office/agent/blueprint/replay.py +622 -0
- hwpx_automation/office/agent/catalog.py +252 -0
- hwpx_automation/office/agent/cli.py +647 -0
- hwpx_automation/office/agent/commands.py +1383 -0
- hwpx_automation/office/agent/document.py +801 -0
- hwpx_automation/office/agent/form_plan.py +1760 -0
- hwpx_automation/office/agent/model.py +808 -0
- hwpx_automation/office/agent/path.py +155 -0
- hwpx_automation/office/agent/query.py +230 -0
- hwpx_automation/office/agent/story.py +207 -0
- hwpx_automation/office/authoring/__init__.py +3542 -0
- hwpx_automation/office/authoring/advanced_generators.py +154 -0
- hwpx_automation/office/authoring/builder/__init__.py +52 -0
- hwpx_automation/office/authoring/builder/core.py +996 -0
- hwpx_automation/office/authoring/builder/report.py +195 -0
- hwpx_automation/office/authoring/design/__init__.py +30 -0
- hwpx_automation/office/authoring/design/_support.py +144 -0
- hwpx_automation/office/authoring/design/composer.py +282 -0
- hwpx_automation/office/authoring/design/harvest.py +305 -0
- hwpx_automation/office/authoring/design/plan.py +69 -0
- hwpx_automation/office/authoring/design/profile.py +88 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/application_form/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/profile.json +24 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/report/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/validator.py +107 -0
- hwpx_automation/office/authoring/presets/__init__.py +22 -0
- hwpx_automation/office/authoring/presets/proposal.py +538 -0
- hwpx_automation/office/authoring/report_parser.py +141 -0
- hwpx_automation/office/authoring/style_profile.py +437 -0
- hwpx_automation/office/authoring/template_analyzer.py +657 -0
- hwpx_automation/office/compliance/__init__.py +38 -0
- hwpx_automation/office/compliance/official_lint.py +478 -0
- hwpx_automation/office/compliance/pii.py +388 -0
- hwpx_automation/office/document_ops/__init__.py +13 -0
- hwpx_automation/office/document_ops/comparison.py +62 -0
- hwpx_automation/office/document_ops/mail_merge.py +73 -0
- hwpx_automation/office/document_ops/redline.py +35 -0
- hwpx_automation/office/evalplan/__init__.py +36 -0
- hwpx_automation/office/evalplan/runtime.py +2762 -0
- hwpx_automation/office/exam/__init__.py +44 -0
- hwpx_automation/office/exam/compose.py +282 -0
- hwpx_automation/office/exam/ir.py +44 -0
- hwpx_automation/office/exam/measure.py +163 -0
- hwpx_automation/office/exam/parser.py +150 -0
- hwpx_automation/office/exam/profile.py +123 -0
- hwpx_automation/office/form_fill/__init__.py +66 -0
- hwpx_automation/office/form_fill/classification.py +108 -0
- hwpx_automation/office/form_fill/fill_residue.py +242 -0
- hwpx_automation/office/form_fill/fit/__init__.py +36 -0
- hwpx_automation/office/form_fill/fit/apply.py +24 -0
- hwpx_automation/office/form_fill/fit/engine.py +24 -0
- hwpx_automation/office/form_fill/fit/measure.py +50 -0
- hwpx_automation/office/form_fill/fit/policy.py +28 -0
- hwpx_automation/office/form_fill/fit/report.py +28 -0
- hwpx_automation/office/form_fill/fit/seal.py +457 -0
- hwpx_automation/office/form_fill/fit/wordbox.py +1343 -0
- hwpx_automation/office/form_fill/guidance.py +704 -0
- hwpx_automation/office/form_fill/quality.py +961 -0
- hwpx_automation/office/form_fill/split_run.py +333 -0
- hwpx_automation/office/form_fill/template_formfit.py +656 -0
- hwpx_automation/office/house_style/__init__.py +196 -0
- hwpx_automation/office/house_style/composition.py +68 -0
- hwpx_automation/office/house_style/data/bank.json +625 -0
- hwpx_automation/office/house_style/data/genres.json +43 -0
- hwpx_automation/office/quality/__init__.py +14 -0
- hwpx_automation/office/quality/page_guard.py +277 -0
- hwpx_automation/office/rendering/__init__.py +145 -0
- hwpx_automation/office/rendering/_hancom_open_rate.ps1 +374 -0
- hwpx_automation/office/rendering/_refresh_hwpx_mac.applescript +162 -0
- hwpx_automation/office/rendering/_render_hwpx.ps1 +72 -0
- hwpx_automation/office/rendering/_render_hwpx_mac.applescript +249 -0
- hwpx_automation/office/rendering/block_splits.py +76 -0
- hwpx_automation/office/rendering/detectors.py +151 -0
- hwpx_automation/office/rendering/diff.py +153 -0
- hwpx_automation/office/rendering/fixture_corpus.py +215 -0
- hwpx_automation/office/rendering/oracle.py +909 -0
- hwpx_automation/office/rendering/page_qa.py +245 -0
- hwpx_automation/office/rendering/qa_contracts.py +293 -0
- hwpx_automation/office/rendering/qa_metrics.py +241 -0
- hwpx_automation/office/rendering/worker.py +290 -0
- hwpx_automation/office/utilities/__init__.py +12 -0
- hwpx_automation/office/utilities/table_compute.py +477 -0
- hwpx_automation/ops_services/__init__.py +1 -0
- hwpx_automation/ops_services/_border_fill.py +283 -0
- hwpx_automation/ops_services/composition.py +55 -0
- hwpx_automation/ops_services/content_layout.py +322 -0
- hwpx_automation/ops_services/context.py +213 -0
- hwpx_automation/ops_services/form_fields.py +557 -0
- hwpx_automation/ops_services/media.py +178 -0
- hwpx_automation/ops_services/memo_style.py +477 -0
- hwpx_automation/ops_services/package_validation.py +166 -0
- hwpx_automation/ops_services/planning.py +201 -0
- hwpx_automation/ops_services/preview_export.py +585 -0
- hwpx_automation/ops_services/read_query.py +601 -0
- hwpx_automation/ops_services/save_policy.py +604 -0
- hwpx_automation/ops_services/tables.py +539 -0
- hwpx_automation/ops_services/transactions.py +616 -0
- hwpx_automation/preview_output_models.py +69 -0
- hwpx_automation/public-modules.json +206 -0
- hwpx_automation/py.typed +1 -0
- hwpx_automation/quality.py +351 -0
- hwpx_automation/quality_generation.py +725 -0
- hwpx_automation/runtime.py +321 -0
- hwpx_automation/runtime_services.py +100 -0
- hwpx_automation/server.py +259 -0
- hwpx_automation/storage.py +747 -0
- hwpx_automation/tool_bindings.py +170 -0
- hwpx_automation/tool_contract.py +982 -0
- hwpx_automation/upstream.py +755 -0
- hwpx_automation/utils/__init__.py +2 -0
- hwpx_automation/utils/helpers.py +29 -0
- hwpx_automation/visual_qa.py +667 -0
- hwpx_automation/workflow/__init__.py +55 -0
- hwpx_automation/workflow/adapters.py +482 -0
- hwpx_automation/workflow/dispatcher.py +213 -0
- hwpx_automation/workflow/models.py +243 -0
- hwpx_automation/workflow/policy.py +198 -0
- hwpx_automation/workflow/render_contracts.py +173 -0
- hwpx_automation/workflow/render_metrics.py +196 -0
- hwpx_automation/workflow/render_queue.py +482 -0
- hwpx_automation/workflow/render_security.py +172 -0
- hwpx_automation/workflow/render_transport.py +369 -0
- hwpx_automation/workflow/rendering.py +206 -0
- hwpx_automation/workflow/service.py +758 -0
- hwpx_automation/workflow/state_machine.py +65 -0
- hwpx_automation/workflow/store.py +747 -0
- hwpx_automation/workspace.py +1694 -0
- python_hwpx_automation-6.0.3.dist-info/METADATA +279 -0
- python_hwpx_automation-6.0.3.dist-info/RECORD +217 -0
- python_hwpx_automation-6.0.3.dist-info/WHEEL +5 -0
- python_hwpx_automation-6.0.3.dist-info/entry_points.txt +3 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/LICENSE +178 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/NOTICE +14 -0
- python_hwpx_automation-6.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Callable, Sequence
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from hwpx.quality.rendering import RenderBackend
|
|
10
|
+
|
|
11
|
+
from ..office.form_fill import (
|
|
12
|
+
inspect_fill_residue as inspect_form_fill_residue,
|
|
13
|
+
)
|
|
14
|
+
from ..office.form_fill import (
|
|
15
|
+
scan_form_guidance as scan_canonical_form_guidance,
|
|
16
|
+
)
|
|
17
|
+
from ..office.form_fill import (
|
|
18
|
+
score_form_fill as score_canonical_form_fill,
|
|
19
|
+
)
|
|
20
|
+
from ..office.rendering import resolve_hancom_backend
|
|
21
|
+
from ..storage import LocalDocumentStorage
|
|
22
|
+
from ..workspace import (
|
|
23
|
+
WorkspaceMissingParentGuard,
|
|
24
|
+
WorkspaceOutputGuard,
|
|
25
|
+
)
|
|
26
|
+
from .context import DocumentContext
|
|
27
|
+
from .save_policy import SavePolicy
|
|
28
|
+
from .transactions import TransactionService
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger("hwpx_automation.hwpx_ops")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FormFieldService:
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
context: DocumentContext,
|
|
37
|
+
save: SavePolicy,
|
|
38
|
+
transactions: TransactionService,
|
|
39
|
+
*,
|
|
40
|
+
render_backend_factory: Callable[[], RenderBackend] | None = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
self._context = context
|
|
43
|
+
self._save = save
|
|
44
|
+
self._transactions = transactions
|
|
45
|
+
self._render_backend_factory = render_backend_factory
|
|
46
|
+
|
|
47
|
+
def _resolve_render_backend(self) -> RenderBackend:
|
|
48
|
+
"""Return the application-owned renderer for every core fill verdict.
|
|
49
|
+
|
|
50
|
+
Core intentionally does not discover Hancom. Keeping the factory at
|
|
51
|
+
this service boundary makes all three form-fill paths use the same
|
|
52
|
+
fail-closed injection seam, while tests can supply an observable
|
|
53
|
+
backend without replacing core's ``verify_fill`` judgement.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
factory = self._render_backend_factory or resolve_hancom_backend
|
|
57
|
+
return factory()
|
|
58
|
+
|
|
59
|
+
def list_form_fields(
|
|
60
|
+
self,
|
|
61
|
+
path: str,
|
|
62
|
+
) -> dict[str, Any]:
|
|
63
|
+
document, _resolved = self._context._open_document(path)
|
|
64
|
+
fields = document.list_form_fields()
|
|
65
|
+
return {
|
|
66
|
+
"fieldCount": len(fields),
|
|
67
|
+
"fields": fields,
|
|
68
|
+
"fallback": "table-label" if not fields else None,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
def fill_form_field(
|
|
72
|
+
self,
|
|
73
|
+
path: str,
|
|
74
|
+
*,
|
|
75
|
+
value: str,
|
|
76
|
+
field_index: int | None = None,
|
|
77
|
+
field_id: str | None = None,
|
|
78
|
+
name: str | None = None,
|
|
79
|
+
dry_run: bool = False,
|
|
80
|
+
) -> dict[str, Any]:
|
|
81
|
+
document, resolved = self._context._open_document(path)
|
|
82
|
+
result = document.fill_form_field(
|
|
83
|
+
value,
|
|
84
|
+
field_index=field_index,
|
|
85
|
+
field_id=field_id,
|
|
86
|
+
name=name,
|
|
87
|
+
)
|
|
88
|
+
result.update({"ok": True, "filename": path})
|
|
89
|
+
return self._transactions._with_transaction_verification(
|
|
90
|
+
result, document, resolved, dry_run=dry_run
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def apply_table_ops(
|
|
94
|
+
self,
|
|
95
|
+
path: str,
|
|
96
|
+
ops: Sequence[dict[str, Any]],
|
|
97
|
+
*,
|
|
98
|
+
output: str | None = None,
|
|
99
|
+
render_check: str = "off",
|
|
100
|
+
dry_run: bool = False,
|
|
101
|
+
output_guard: (
|
|
102
|
+
WorkspaceOutputGuard | WorkspaceMissingParentGuard | None
|
|
103
|
+
) = None,
|
|
104
|
+
publication_sink: Callable[[WorkspaceOutputGuard], None] | None = None,
|
|
105
|
+
) -> dict[str, Any]:
|
|
106
|
+
"""Byte-preserving structural form-fill: apply cell fills + table structure
|
|
107
|
+
ops (delete_column/row/table, insert_row_by_clone, insert_block_by_clone)
|
|
108
|
+
preserving every untouched byte. Cells/tables may be addressed by
|
|
109
|
+
tableAnchor/cellAnchor (unique-or-skip) as well as index. Optional
|
|
110
|
+
real-Hancom render gate.
|
|
111
|
+
|
|
112
|
+
dryRun=true: identical pipeline (resolution/validation/fail-closed all
|
|
113
|
+
real) but writes NOTHING — returns transcript (per-op resolution + before/
|
|
114
|
+
after dims) and applied old→new texts as approval evidence for the user
|
|
115
|
+
consult loop. renderCheck still works on the would-be bytes."""
|
|
116
|
+
try:
|
|
117
|
+
from hwpx.table_patch import apply_table_ops as _apply
|
|
118
|
+
except Exception as exc: # pragma: no cover - dependency compatibility
|
|
119
|
+
raise self._context._new_error(
|
|
120
|
+
"TABLE_OPS_UNAVAILABLE",
|
|
121
|
+
"installed python-hwpx does not provide hwpx.table_patch.apply_table_ops",
|
|
122
|
+
) from exc
|
|
123
|
+
|
|
124
|
+
source_path = self._context._resolve_path(path)
|
|
125
|
+
output_precondition = None
|
|
126
|
+
if isinstance(self._context.storage, LocalDocumentStorage):
|
|
127
|
+
output_precondition = (
|
|
128
|
+
output_guard
|
|
129
|
+
or self._context.storage.capture_output_precondition(
|
|
130
|
+
output if output else source_path
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
target_path = output_precondition.path
|
|
134
|
+
else:
|
|
135
|
+
target_path = (
|
|
136
|
+
self._context._resolve_path(output, must_exist=False)
|
|
137
|
+
if output and dry_run
|
|
138
|
+
else self._context._resolve_output_path(output)
|
|
139
|
+
if output
|
|
140
|
+
else source_path
|
|
141
|
+
)
|
|
142
|
+
result = _apply(source_path, list(ops), dry_run=dry_run)
|
|
143
|
+
payload = result.to_dict()
|
|
144
|
+
if dry_run:
|
|
145
|
+
payload["dryRun"] = True
|
|
146
|
+
payload["outputPath"] = None
|
|
147
|
+
else:
|
|
148
|
+
payload["outputPath"] = str(target_path)
|
|
149
|
+
|
|
150
|
+
if render_check and render_check != "off":
|
|
151
|
+
try:
|
|
152
|
+
from hwpx.table_patch import verify_fill
|
|
153
|
+
|
|
154
|
+
report = verify_fill(
|
|
155
|
+
source_path,
|
|
156
|
+
result.data,
|
|
157
|
+
oracle=self._resolve_render_backend(),
|
|
158
|
+
require=(render_check == "required"),
|
|
159
|
+
)
|
|
160
|
+
payload["renderVerdict"] = {
|
|
161
|
+
"renderChecked": report.render_checked,
|
|
162
|
+
"ok": report.ok,
|
|
163
|
+
"overflowDetected": report.overflow_detected,
|
|
164
|
+
"overlapDetected": report.overlap_detected,
|
|
165
|
+
"pageCountChanged": report.page_count_changed,
|
|
166
|
+
"warnings": list(report.warnings),
|
|
167
|
+
"errors": list(report.errors),
|
|
168
|
+
}
|
|
169
|
+
if report.render_checked and not report.ok:
|
|
170
|
+
payload["ok"] = False
|
|
171
|
+
if render_check == "required":
|
|
172
|
+
raise self._context._new_error(
|
|
173
|
+
"RENDER_CHECK_REQUIRED_FAILED",
|
|
174
|
+
"required render detected overflow, overlap, or layout regression",
|
|
175
|
+
)
|
|
176
|
+
except Exception as exc:
|
|
177
|
+
if render_check == "required":
|
|
178
|
+
raise self._context._new_error(
|
|
179
|
+
"RENDER_CHECK_REQUIRED_FAILED", str(exc)
|
|
180
|
+
) from exc
|
|
181
|
+
payload["renderVerdict"] = {"renderChecked": False, "note": str(exc)}
|
|
182
|
+
# A required render is a pre-publication gate: only replace the
|
|
183
|
+
# destination after the candidate has passed it.
|
|
184
|
+
if (
|
|
185
|
+
payload.get("ok") is not False
|
|
186
|
+
and not dry_run
|
|
187
|
+
and (
|
|
188
|
+
not result.byte_identical
|
|
189
|
+
or target_path.resolve(strict=False)
|
|
190
|
+
!= source_path.resolve(strict=False)
|
|
191
|
+
)
|
|
192
|
+
):
|
|
193
|
+
payload = self._save._write_patched(
|
|
194
|
+
target_path,
|
|
195
|
+
result.data,
|
|
196
|
+
payload,
|
|
197
|
+
output_guard=output_guard,
|
|
198
|
+
output_precondition=output_precondition,
|
|
199
|
+
publication_sink=publication_sink,
|
|
200
|
+
)
|
|
201
|
+
return payload
|
|
202
|
+
|
|
203
|
+
def verify_form_fill(
|
|
204
|
+
self,
|
|
205
|
+
path: str,
|
|
206
|
+
before_path: str,
|
|
207
|
+
*,
|
|
208
|
+
require: bool = False,
|
|
209
|
+
) -> dict[str, Any]:
|
|
210
|
+
"""Render before/after in REAL Hancom and judge overflow/overlap/layout.
|
|
211
|
+
Honest degrade (renderChecked=false) with no oracle unless require=true."""
|
|
212
|
+
try:
|
|
213
|
+
from hwpx.table_patch import verify_fill
|
|
214
|
+
except Exception as exc: # pragma: no cover - dependency compatibility
|
|
215
|
+
raise self._context._new_error(
|
|
216
|
+
"VERIFY_UNAVAILABLE",
|
|
217
|
+
"installed python-hwpx does not provide hwpx.table_patch.verify_fill",
|
|
218
|
+
) from exc
|
|
219
|
+
after = self._context._resolve_path(path)
|
|
220
|
+
before = self._context._resolve_path(before_path)
|
|
221
|
+
report = verify_fill(
|
|
222
|
+
before,
|
|
223
|
+
after,
|
|
224
|
+
oracle=self._resolve_render_backend(),
|
|
225
|
+
require=require,
|
|
226
|
+
)
|
|
227
|
+
return {
|
|
228
|
+
"renderChecked": report.render_checked,
|
|
229
|
+
"ok": report.ok,
|
|
230
|
+
"overflowDetected": report.overflow_detected,
|
|
231
|
+
"overlapDetected": report.overlap_detected,
|
|
232
|
+
"pageCountChanged": report.page_count_changed,
|
|
233
|
+
"warnings": list(report.warnings),
|
|
234
|
+
"errors": list(report.errors),
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
def score_form_fill(
|
|
238
|
+
self,
|
|
239
|
+
path: str,
|
|
240
|
+
gold_path: str,
|
|
241
|
+
blank_path: str,
|
|
242
|
+
*,
|
|
243
|
+
run_render: bool = True,
|
|
244
|
+
expected_pages: int | None = None,
|
|
245
|
+
) -> dict[str, Any]:
|
|
246
|
+
"""Score a filled evaluation-plan form against a gold reference on 5 axes
|
|
247
|
+
(A render cleanliness / B byte fidelity vs blank / C structure vs gold /
|
|
248
|
+
D content completeness / E compliance) -> weighted 0-100 + per-axis gap
|
|
249
|
+
report. The fitness function of the form-fill quality loop.
|
|
250
|
+
|
|
251
|
+
``path`` = produced fill, ``goldPath`` = accepted reference of the same
|
|
252
|
+
form family, ``blankPath`` = the empty province form. A requires a real
|
|
253
|
+
Hancom render (renderCheck); with no oracle A is ``unverified`` (never a
|
|
254
|
+
silent pass). Set runRender=false for a fast structural-only pass."""
|
|
255
|
+
produced = self._context._resolve_path(path)
|
|
256
|
+
gold = self._context._resolve_path(gold_path)
|
|
257
|
+
blank = self._context._resolve_path(blank_path)
|
|
258
|
+
card = score_canonical_form_fill(
|
|
259
|
+
produced,
|
|
260
|
+
gold,
|
|
261
|
+
blank,
|
|
262
|
+
run_render=run_render,
|
|
263
|
+
expected_pages=expected_pages,
|
|
264
|
+
)
|
|
265
|
+
return card.to_dict()
|
|
266
|
+
|
|
267
|
+
def apply_body_ops(
|
|
268
|
+
self,
|
|
269
|
+
path: str,
|
|
270
|
+
ops: Sequence[dict[str, Any]],
|
|
271
|
+
*,
|
|
272
|
+
output: str | None = None,
|
|
273
|
+
dry_run: bool = False,
|
|
274
|
+
output_guard: (
|
|
275
|
+
WorkspaceOutputGuard | WorkspaceMissingParentGuard | None
|
|
276
|
+
) = None,
|
|
277
|
+
publication_sink: Callable[[WorkspaceOutputGuard], None] | None = None,
|
|
278
|
+
) -> dict[str, Any]:
|
|
279
|
+
"""Byte-preserving BODY(표 밖 직속 문단) ops — Stage 2 결정표의 본문 어휘.
|
|
280
|
+
|
|
281
|
+
ops: replace_text{find,replace,count=1: <hp:t> 텍스트 안에서만, 개수 불일치
|
|
282
|
+
refuse} · delete_paragraph{index: 표 품은 문단 refuse} ·
|
|
283
|
+
insert_paragraph_by_clone{ref_index,count,texts?: 참조 문단 서식 verbatim
|
|
284
|
+
상속} · reorder_paragraphs{start,end,order}. index는 op 실행 시점 기준.
|
|
285
|
+
dryRun=true면 아무것도 쓰지 않고 transcript만(승인 근거)."""
|
|
286
|
+
try:
|
|
287
|
+
from hwpx.body_patch import apply_body_ops as _apply
|
|
288
|
+
except Exception as exc: # pragma: no cover - dependency compatibility
|
|
289
|
+
raise self._context._new_error(
|
|
290
|
+
"BODY_OPS_UNAVAILABLE",
|
|
291
|
+
"installed python-hwpx does not provide hwpx.body_patch.apply_body_ops",
|
|
292
|
+
) from exc
|
|
293
|
+
source_path = self._context._resolve_path(path)
|
|
294
|
+
output_precondition = None
|
|
295
|
+
if isinstance(self._context.storage, LocalDocumentStorage):
|
|
296
|
+
output_precondition = (
|
|
297
|
+
output_guard
|
|
298
|
+
or self._context.storage.capture_output_precondition(
|
|
299
|
+
output if output else source_path
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
target_path = output_precondition.path
|
|
303
|
+
else:
|
|
304
|
+
target_path = (
|
|
305
|
+
self._context._resolve_path(output, must_exist=False)
|
|
306
|
+
if output and dry_run
|
|
307
|
+
else self._context._resolve_output_path(output)
|
|
308
|
+
if output
|
|
309
|
+
else source_path
|
|
310
|
+
)
|
|
311
|
+
result = _apply(source_path, list(ops), dry_run=dry_run)
|
|
312
|
+
payload = result.to_dict()
|
|
313
|
+
if dry_run:
|
|
314
|
+
payload["dryRun"] = True
|
|
315
|
+
payload["outputPath"] = None
|
|
316
|
+
else:
|
|
317
|
+
payload["outputPath"] = str(target_path)
|
|
318
|
+
if not result.byte_identical or target_path.resolve(
|
|
319
|
+
strict=False
|
|
320
|
+
) != source_path.resolve(strict=False):
|
|
321
|
+
payload = self._save._write_patched(
|
|
322
|
+
target_path,
|
|
323
|
+
result.data,
|
|
324
|
+
payload,
|
|
325
|
+
output_guard=output_guard,
|
|
326
|
+
output_precondition=output_precondition,
|
|
327
|
+
publication_sink=publication_sink,
|
|
328
|
+
)
|
|
329
|
+
return payload
|
|
330
|
+
|
|
331
|
+
def inspect_fill_residue(
|
|
332
|
+
self,
|
|
333
|
+
path: str,
|
|
334
|
+
*,
|
|
335
|
+
blank_path: str | None = None,
|
|
336
|
+
) -> dict[str, Any]:
|
|
337
|
+
"""채움본 잔존물 zero-체크(비변형) — 제출 전 기계 게이트(Stage 3).
|
|
338
|
+
|
|
339
|
+
blank를 주면 범례 기반 신호까지: 삭제색 잔존·미수정 샘플(수정색이 blank와
|
|
340
|
+
동일=prose 샘플 미교체) = ERROR. placeholder ◯◯◯/□□□=ERROR, **=각주와
|
|
341
|
+
중의적이라 needs_review, 고아 마커=needs_review. ok=true는 필요조건일 뿐 —
|
|
342
|
+
제출 확언은 렌더 PDF를 사람이 전 페이지 본 뒤에만."""
|
|
343
|
+
produced = self._context._resolve_path(path)
|
|
344
|
+
blank = self._context._resolve_path(blank_path) if blank_path else None
|
|
345
|
+
return inspect_form_fill_residue(produced, blank=blank).to_dict()
|
|
346
|
+
|
|
347
|
+
def scan_form_guidance(self, path: str, *, max_items: int = 60) -> dict[str, Any]:
|
|
348
|
+
"""Recon an unfamiliar form (NON-MUTATING) — universal form-fill Stage 1.
|
|
349
|
+
|
|
350
|
+
Walks every run INCLUDING table-cell interiors and table captions, parses
|
|
351
|
+
the form's own colour legend (e.g. "검정 유지/파랑 수정/빨강 삭제"), and
|
|
352
|
+
reports candidates with table_patch-compatible addresses: delete candidates
|
|
353
|
+
(legend-delete colour + guidance keywords), modify targets, placeholder
|
|
354
|
+
tokens (◯◯◯/**/□□□), conditional-choice blocks, empty cells with neighbour
|
|
355
|
+
label + charPr format context, and an honest question list. Candidates are
|
|
356
|
+
proposals — destructive ops still require user approval."""
|
|
357
|
+
resolved = self._context._resolve_path(path)
|
|
358
|
+
report = scan_canonical_form_guidance(resolved)
|
|
359
|
+
limit = max(1, int(max_items))
|
|
360
|
+
|
|
361
|
+
def _cand(c) -> dict[str, Any]:
|
|
362
|
+
cell = None
|
|
363
|
+
if c.cell is not None:
|
|
364
|
+
cell = {
|
|
365
|
+
"tableIndex": c.cell.table_index,
|
|
366
|
+
"row": c.cell.row,
|
|
367
|
+
"col": c.cell.col,
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
"location": c.location,
|
|
371
|
+
"signals": c.signals,
|
|
372
|
+
"confidence": c.confidence,
|
|
373
|
+
"textPreview": c.text_preview,
|
|
374
|
+
"cell": cell,
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
def _cap(items) -> list[dict[str, Any]]:
|
|
378
|
+
return [_cand(c) for c in items[:limit]]
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
"legend": [
|
|
382
|
+
{
|
|
383
|
+
"colorWord": b.color_word,
|
|
384
|
+
"family": b.family,
|
|
385
|
+
"exactHex": b.exact_hex,
|
|
386
|
+
"action": b.action,
|
|
387
|
+
"sourceText": b.source_text,
|
|
388
|
+
}
|
|
389
|
+
for b in report.legend
|
|
390
|
+
],
|
|
391
|
+
"colorInventory": report.color_inventory,
|
|
392
|
+
"deleteCandidates": _cap(report.delete_candidates),
|
|
393
|
+
"deleteCandidatesTotal": len(report.delete_candidates),
|
|
394
|
+
"modifyCandidatesByTable": report.modify_candidates_by_table,
|
|
395
|
+
"emptyCellCandidates": _cap(report.empty_cell_candidates),
|
|
396
|
+
"emptyCellTotal": len(report.empty_cell_candidates),
|
|
397
|
+
"placeholderCandidates": _cap(report.placeholder_candidates),
|
|
398
|
+
"conditionalChoices": _cap(report.conditional_choices),
|
|
399
|
+
"questions": report.questions,
|
|
400
|
+
"stats": report.stats,
|
|
401
|
+
"limitations": report.limitations,
|
|
402
|
+
"markdownReport": report.to_markdown(),
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
_EVALPLAN_PHASES = ("structural", "all", "clean")
|
|
406
|
+
|
|
407
|
+
def apply_evalplan_fill(
|
|
408
|
+
self,
|
|
409
|
+
path: str,
|
|
410
|
+
review_md: str,
|
|
411
|
+
*,
|
|
412
|
+
phase: str = "all",
|
|
413
|
+
output: str | None = None,
|
|
414
|
+
render_check: str = "off",
|
|
415
|
+
score_gold_path: str | None = None,
|
|
416
|
+
expected_pages: int | None = None,
|
|
417
|
+
output_guard: (
|
|
418
|
+
WorkspaceOutputGuard | WorkspaceMissingParentGuard | None
|
|
419
|
+
) = None,
|
|
420
|
+
publication_sink: Callable[[WorkspaceOutputGuard], None] | None = None,
|
|
421
|
+
) -> dict[str, Any]:
|
|
422
|
+
"""Whole-form 평가계획 fill: {blank province form + review markdown} ->
|
|
423
|
+
byte-preserving gold-quality 채움본 in ONE call. Runs the structure-driven
|
|
424
|
+
recipe (delete red/optional tables + 정기시험 column, fill 운영계획/성취기준/
|
|
425
|
+
성취수준/성취율/반영비율/rubrics incl. 채점기준 ladder/prose sections),
|
|
426
|
+
preserving the blank's formatting (never regenerates). Works on BOTH the
|
|
427
|
+
2015-개정 and 2022-개정 form families (auto-detected from the blank + review).
|
|
428
|
+
|
|
429
|
+
``path`` = blank form, ``reviewMd`` = the structured review markdown
|
|
430
|
+
(Ⅰ 운영계획 + [1]~[11]). ``phase`` selects how far the recipe runs:
|
|
431
|
+
``"structural"`` = confident deletions only, ``"all"`` (default; the prior
|
|
432
|
+
behaviour) = structural + per-region content fills, ``"clean"`` = ``"all"``
|
|
433
|
+
plus core's deterministic post-fill cleanup (title/teacher/정의적 fills,
|
|
434
|
+
instruction-scaffolding + orphan prune, red-strip, blue->black recolor,
|
|
435
|
+
caption strip) that yields a submittable 채움본 with no bespoke driver; the
|
|
436
|
+
cleanup report is surfaced under ``contentReport.finalize``. Returns the
|
|
437
|
+
produced path + per-region contentReport with rubricNeedsReview (honest-defer
|
|
438
|
+
count, never silent). Set renderCheck='required' to gate on a real Hancom
|
|
439
|
+
render; pass scoreGoldPath (an accepted form of the same family) to also
|
|
440
|
+
return the 5-axis scorecard."""
|
|
441
|
+
if phase not in self._EVALPLAN_PHASES:
|
|
442
|
+
raise self._context._new_error(
|
|
443
|
+
"EVALPLAN_PHASE_INVALID",
|
|
444
|
+
f"phase must be one of {self._EVALPLAN_PHASES}, got {phase!r}",
|
|
445
|
+
)
|
|
446
|
+
try:
|
|
447
|
+
from hwpx_automation.office.evalplan import (
|
|
448
|
+
expected_skeleton,
|
|
449
|
+
fill_evalplan,
|
|
450
|
+
parse_review_file,
|
|
451
|
+
)
|
|
452
|
+
except Exception as exc: # pragma: no cover - broken package guard
|
|
453
|
+
raise self._context._new_error(
|
|
454
|
+
"EVALPLAN_FILL_UNAVAILABLE",
|
|
455
|
+
"installed python-hwpx-automation does not provide the evalplan runtime",
|
|
456
|
+
) from exc
|
|
457
|
+
|
|
458
|
+
blank = self._context._resolve_path(path)
|
|
459
|
+
md = self._context._resolve_path(review_md)
|
|
460
|
+
output_precondition = None
|
|
461
|
+
if isinstance(self._context.storage, LocalDocumentStorage):
|
|
462
|
+
output_precondition = (
|
|
463
|
+
output_guard
|
|
464
|
+
or self._context.storage.capture_output_precondition(
|
|
465
|
+
output if output else blank
|
|
466
|
+
)
|
|
467
|
+
)
|
|
468
|
+
target_path = output_precondition.path
|
|
469
|
+
else:
|
|
470
|
+
target_path = (
|
|
471
|
+
self._context._resolve_output_path(output) if output else blank
|
|
472
|
+
)
|
|
473
|
+
content = parse_review_file(md)
|
|
474
|
+
res = fill_evalplan(blank, content, phase=phase)
|
|
475
|
+
data = res["_data"]
|
|
476
|
+
|
|
477
|
+
report = res.get("content_report", {})
|
|
478
|
+
rubric_nr = [
|
|
479
|
+
s
|
|
480
|
+
for s in report.get("rubrics", {}).get("skipped", [])
|
|
481
|
+
if "NEEDS_REVIEW" in s
|
|
482
|
+
]
|
|
483
|
+
payload: dict[str, Any] = {
|
|
484
|
+
"ok": bool(res.get("ok")),
|
|
485
|
+
"outputPath": str(target_path),
|
|
486
|
+
"byteIdentical": bool(res.get("byteIdentical")),
|
|
487
|
+
"transcript": res.get("transcript", []),
|
|
488
|
+
"expectedSkeleton": res.get("expected_skeleton"),
|
|
489
|
+
"contentReport": report,
|
|
490
|
+
"rubricNeedsReview": len(rubric_nr),
|
|
491
|
+
"needsReviewNotes": rubric_nr,
|
|
492
|
+
"changedParts": res.get("changedParts", []),
|
|
493
|
+
"skipped": res.get("skipped", []),
|
|
494
|
+
}
|
|
495
|
+
if render_check and render_check != "off":
|
|
496
|
+
try:
|
|
497
|
+
from hwpx.table_patch import verify_fill
|
|
498
|
+
|
|
499
|
+
verdict = verify_fill(
|
|
500
|
+
blank,
|
|
501
|
+
data,
|
|
502
|
+
oracle=self._resolve_render_backend(),
|
|
503
|
+
require=(render_check == "required"),
|
|
504
|
+
)
|
|
505
|
+
payload["renderVerdict"] = {
|
|
506
|
+
"renderChecked": verdict.render_checked,
|
|
507
|
+
"ok": verdict.ok,
|
|
508
|
+
"overflowDetected": verdict.overflow_detected,
|
|
509
|
+
"overlapDetected": verdict.overlap_detected,
|
|
510
|
+
"pageCountChanged": verdict.page_count_changed,
|
|
511
|
+
"warnings": list(verdict.warnings),
|
|
512
|
+
"errors": list(verdict.errors),
|
|
513
|
+
}
|
|
514
|
+
if verdict.render_checked and not verdict.ok:
|
|
515
|
+
payload["ok"] = False
|
|
516
|
+
if render_check == "required":
|
|
517
|
+
raise self._context._new_error(
|
|
518
|
+
"RENDER_CHECK_REQUIRED_FAILED",
|
|
519
|
+
"required evalplan render detected overflow, overlap, or layout regression",
|
|
520
|
+
)
|
|
521
|
+
except Exception as exc:
|
|
522
|
+
if render_check == "required":
|
|
523
|
+
raise self._context._new_error(
|
|
524
|
+
"RENDER_CHECK_REQUIRED_FAILED", str(exc)
|
|
525
|
+
) from exc
|
|
526
|
+
payload["renderVerdict"] = {"renderChecked": False, "note": str(exc)}
|
|
527
|
+
|
|
528
|
+
# Do not publish a partial/failed domain result. Required rendering is
|
|
529
|
+
# evaluated against candidate bytes before the atomic destination swap.
|
|
530
|
+
if payload["ok"] and (
|
|
531
|
+
not res.get("byteIdentical", True)
|
|
532
|
+
or target_path.resolve(strict=False) != blank.resolve(strict=False)
|
|
533
|
+
):
|
|
534
|
+
payload = self._save._write_patched(
|
|
535
|
+
target_path,
|
|
536
|
+
data,
|
|
537
|
+
payload,
|
|
538
|
+
output_guard=output_guard,
|
|
539
|
+
output_precondition=output_precondition,
|
|
540
|
+
publication_sink=publication_sink,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
if score_gold_path:
|
|
544
|
+
try:
|
|
545
|
+
card = score_canonical_form_fill(
|
|
546
|
+
target_path,
|
|
547
|
+
self._context._resolve_path(score_gold_path),
|
|
548
|
+
blank,
|
|
549
|
+
content=md,
|
|
550
|
+
expected_skeleton=expected_skeleton(content, blank),
|
|
551
|
+
run_render=bool(render_check and render_check != "off"),
|
|
552
|
+
expected_pages=expected_pages,
|
|
553
|
+
)
|
|
554
|
+
payload["scorecard"] = card.to_dict()
|
|
555
|
+
except Exception as exc: # pragma: no cover - scoring optional
|
|
556
|
+
payload["scorecard"] = {"error": str(exc)}
|
|
557
|
+
return payload
|