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.
Files changed (217) hide show
  1. hwpx_automation/__init__.py +61 -0
  2. hwpx_automation/__init__.pyi +27 -0
  3. hwpx_automation/__main__.py +8 -0
  4. hwpx_automation/agent_document.py +392 -0
  5. hwpx_automation/api.py +136 -0
  6. hwpx_automation/blind_eval.py +407 -0
  7. hwpx_automation/capabilities.py +110 -0
  8. hwpx_automation/compat.py +48 -0
  9. hwpx_automation/configuration.py +60 -0
  10. hwpx_automation/core/__init__.py +2 -0
  11. hwpx_automation/core/content.py +762 -0
  12. hwpx_automation/core/context.py +111 -0
  13. hwpx_automation/core/diff.py +53 -0
  14. hwpx_automation/core/document.py +37 -0
  15. hwpx_automation/core/formatting.py +513 -0
  16. hwpx_automation/core/handles.py +24 -0
  17. hwpx_automation/core/locations.py +205 -0
  18. hwpx_automation/core/locator.py +162 -0
  19. hwpx_automation/core/plan.py +680 -0
  20. hwpx_automation/core/resources.py +42 -0
  21. hwpx_automation/core/search.py +296 -0
  22. hwpx_automation/core/transactions.py +434 -0
  23. hwpx_automation/core/txn.py +48 -0
  24. hwpx_automation/document_state.py +95 -0
  25. hwpx_automation/errors.py +174 -0
  26. hwpx_automation/execution_lock.py +15 -0
  27. hwpx_automation/fastmcp_adapter.py +672 -0
  28. hwpx_automation/form_fill.py +1177 -0
  29. hwpx_automation/form_output_models.py +223 -0
  30. hwpx_automation/handlers/__init__.py +2 -0
  31. hwpx_automation/handlers/_shared.py +377 -0
  32. hwpx_automation/handlers/agent_document.py +257 -0
  33. hwpx_automation/handlers/authoring.py +750 -0
  34. hwpx_automation/handlers/content_edit.py +1078 -0
  35. hwpx_automation/handlers/form_fill.py +607 -0
  36. hwpx_automation/handlers/layout_style.py +660 -0
  37. hwpx_automation/handlers/quality_render.py +566 -0
  38. hwpx_automation/handlers/read_export.py +1295 -0
  39. hwpx_automation/handlers/specialized.py +624 -0
  40. hwpx_automation/handlers/tracked_changes.py +589 -0
  41. hwpx_automation/handlers/workflow.py +105 -0
  42. hwpx_automation/hwp_converter.py +227 -0
  43. hwpx_automation/hwp_support.py +94 -0
  44. hwpx_automation/hwpx_ops.py +1439 -0
  45. hwpx_automation/identity.json +263 -0
  46. hwpx_automation/identity.py +18 -0
  47. hwpx_automation/ingest_adapters.py +85 -0
  48. hwpx_automation/markdown_plan.py +216 -0
  49. hwpx_automation/mcp_cli.py +29 -0
  50. hwpx_automation/metadata/tools_meta.py +40 -0
  51. hwpx_automation/mixed_form.py +3007 -0
  52. hwpx_automation/mutation_models.py +401 -0
  53. hwpx_automation/network_policy.py +232 -0
  54. hwpx_automation/office/__init__.py +14 -0
  55. hwpx_automation/office/agent/__init__.py +125 -0
  56. hwpx_automation/office/agent/_batch_verification.py +383 -0
  57. hwpx_automation/office/agent/blueprint/__init__.py +58 -0
  58. hwpx_automation/office/agent/blueprint/bundle.py +282 -0
  59. hwpx_automation/office/agent/blueprint/catalog.py +136 -0
  60. hwpx_automation/office/agent/blueprint/dump.py +520 -0
  61. hwpx_automation/office/agent/blueprint/mapping.py +312 -0
  62. hwpx_automation/office/agent/blueprint/model.py +722 -0
  63. hwpx_automation/office/agent/blueprint/native.py +621 -0
  64. hwpx_automation/office/agent/blueprint/replay.py +622 -0
  65. hwpx_automation/office/agent/catalog.py +252 -0
  66. hwpx_automation/office/agent/cli.py +647 -0
  67. hwpx_automation/office/agent/commands.py +1383 -0
  68. hwpx_automation/office/agent/document.py +801 -0
  69. hwpx_automation/office/agent/form_plan.py +1760 -0
  70. hwpx_automation/office/agent/model.py +808 -0
  71. hwpx_automation/office/agent/path.py +155 -0
  72. hwpx_automation/office/agent/query.py +230 -0
  73. hwpx_automation/office/agent/story.py +207 -0
  74. hwpx_automation/office/authoring/__init__.py +3542 -0
  75. hwpx_automation/office/authoring/advanced_generators.py +154 -0
  76. hwpx_automation/office/authoring/builder/__init__.py +52 -0
  77. hwpx_automation/office/authoring/builder/core.py +996 -0
  78. hwpx_automation/office/authoring/builder/report.py +195 -0
  79. hwpx_automation/office/authoring/design/__init__.py +30 -0
  80. hwpx_automation/office/authoring/design/_support.py +144 -0
  81. hwpx_automation/office/authoring/design/composer.py +282 -0
  82. hwpx_automation/office/authoring/design/harvest.py +305 -0
  83. hwpx_automation/office/authoring/design/plan.py +69 -0
  84. hwpx_automation/office/authoring/design/profile.py +88 -0
  85. hwpx_automation/office/authoring/design/profiles/application_form/fragments/body.xml +1 -0
  86. hwpx_automation/office/authoring/design/profiles/application_form/fragments/heading.xml +1 -0
  87. hwpx_automation/office/authoring/design/profiles/application_form/fragments/info_table.xml +1 -0
  88. hwpx_automation/office/authoring/design/profiles/application_form/fragments/title.xml +1 -0
  89. hwpx_automation/office/authoring/design/profiles/application_form/profile.json +25 -0
  90. hwpx_automation/office/authoring/design/profiles/application_form/template.hwpx +0 -0
  91. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/body.xml +1 -0
  92. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/heading.xml +1 -0
  93. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/title.xml +1 -0
  94. hwpx_automation/office/authoring/design/profiles/home_notice/profile.json +24 -0
  95. hwpx_automation/office/authoring/design/profiles/home_notice/template.hwpx +0 -0
  96. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/body.xml +1 -0
  97. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/heading.xml +1 -0
  98. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/info_table.xml +1 -0
  99. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/title.xml +1 -0
  100. hwpx_automation/office/authoring/design/profiles/official_notice/profile.json +25 -0
  101. hwpx_automation/office/authoring/design/profiles/official_notice/template.hwpx +0 -0
  102. hwpx_automation/office/authoring/design/profiles/report/fragments/body.xml +1 -0
  103. hwpx_automation/office/authoring/design/profiles/report/fragments/heading.xml +1 -0
  104. hwpx_automation/office/authoring/design/profiles/report/fragments/info_table.xml +1 -0
  105. hwpx_automation/office/authoring/design/profiles/report/fragments/title.xml +1 -0
  106. hwpx_automation/office/authoring/design/profiles/report/profile.json +25 -0
  107. hwpx_automation/office/authoring/design/profiles/report/template.hwpx +0 -0
  108. hwpx_automation/office/authoring/design/validator.py +107 -0
  109. hwpx_automation/office/authoring/presets/__init__.py +22 -0
  110. hwpx_automation/office/authoring/presets/proposal.py +538 -0
  111. hwpx_automation/office/authoring/report_parser.py +141 -0
  112. hwpx_automation/office/authoring/style_profile.py +437 -0
  113. hwpx_automation/office/authoring/template_analyzer.py +657 -0
  114. hwpx_automation/office/compliance/__init__.py +38 -0
  115. hwpx_automation/office/compliance/official_lint.py +478 -0
  116. hwpx_automation/office/compliance/pii.py +388 -0
  117. hwpx_automation/office/document_ops/__init__.py +13 -0
  118. hwpx_automation/office/document_ops/comparison.py +62 -0
  119. hwpx_automation/office/document_ops/mail_merge.py +73 -0
  120. hwpx_automation/office/document_ops/redline.py +35 -0
  121. hwpx_automation/office/evalplan/__init__.py +36 -0
  122. hwpx_automation/office/evalplan/runtime.py +2762 -0
  123. hwpx_automation/office/exam/__init__.py +44 -0
  124. hwpx_automation/office/exam/compose.py +282 -0
  125. hwpx_automation/office/exam/ir.py +44 -0
  126. hwpx_automation/office/exam/measure.py +163 -0
  127. hwpx_automation/office/exam/parser.py +150 -0
  128. hwpx_automation/office/exam/profile.py +123 -0
  129. hwpx_automation/office/form_fill/__init__.py +66 -0
  130. hwpx_automation/office/form_fill/classification.py +108 -0
  131. hwpx_automation/office/form_fill/fill_residue.py +242 -0
  132. hwpx_automation/office/form_fill/fit/__init__.py +36 -0
  133. hwpx_automation/office/form_fill/fit/apply.py +24 -0
  134. hwpx_automation/office/form_fill/fit/engine.py +24 -0
  135. hwpx_automation/office/form_fill/fit/measure.py +50 -0
  136. hwpx_automation/office/form_fill/fit/policy.py +28 -0
  137. hwpx_automation/office/form_fill/fit/report.py +28 -0
  138. hwpx_automation/office/form_fill/fit/seal.py +457 -0
  139. hwpx_automation/office/form_fill/fit/wordbox.py +1343 -0
  140. hwpx_automation/office/form_fill/guidance.py +704 -0
  141. hwpx_automation/office/form_fill/quality.py +961 -0
  142. hwpx_automation/office/form_fill/split_run.py +333 -0
  143. hwpx_automation/office/form_fill/template_formfit.py +656 -0
  144. hwpx_automation/office/house_style/__init__.py +196 -0
  145. hwpx_automation/office/house_style/composition.py +68 -0
  146. hwpx_automation/office/house_style/data/bank.json +625 -0
  147. hwpx_automation/office/house_style/data/genres.json +43 -0
  148. hwpx_automation/office/quality/__init__.py +14 -0
  149. hwpx_automation/office/quality/page_guard.py +277 -0
  150. hwpx_automation/office/rendering/__init__.py +145 -0
  151. hwpx_automation/office/rendering/_hancom_open_rate.ps1 +374 -0
  152. hwpx_automation/office/rendering/_refresh_hwpx_mac.applescript +162 -0
  153. hwpx_automation/office/rendering/_render_hwpx.ps1 +72 -0
  154. hwpx_automation/office/rendering/_render_hwpx_mac.applescript +249 -0
  155. hwpx_automation/office/rendering/block_splits.py +76 -0
  156. hwpx_automation/office/rendering/detectors.py +151 -0
  157. hwpx_automation/office/rendering/diff.py +153 -0
  158. hwpx_automation/office/rendering/fixture_corpus.py +215 -0
  159. hwpx_automation/office/rendering/oracle.py +909 -0
  160. hwpx_automation/office/rendering/page_qa.py +245 -0
  161. hwpx_automation/office/rendering/qa_contracts.py +293 -0
  162. hwpx_automation/office/rendering/qa_metrics.py +241 -0
  163. hwpx_automation/office/rendering/worker.py +290 -0
  164. hwpx_automation/office/utilities/__init__.py +12 -0
  165. hwpx_automation/office/utilities/table_compute.py +477 -0
  166. hwpx_automation/ops_services/__init__.py +1 -0
  167. hwpx_automation/ops_services/_border_fill.py +283 -0
  168. hwpx_automation/ops_services/composition.py +55 -0
  169. hwpx_automation/ops_services/content_layout.py +322 -0
  170. hwpx_automation/ops_services/context.py +213 -0
  171. hwpx_automation/ops_services/form_fields.py +557 -0
  172. hwpx_automation/ops_services/media.py +178 -0
  173. hwpx_automation/ops_services/memo_style.py +477 -0
  174. hwpx_automation/ops_services/package_validation.py +166 -0
  175. hwpx_automation/ops_services/planning.py +201 -0
  176. hwpx_automation/ops_services/preview_export.py +585 -0
  177. hwpx_automation/ops_services/read_query.py +601 -0
  178. hwpx_automation/ops_services/save_policy.py +604 -0
  179. hwpx_automation/ops_services/tables.py +539 -0
  180. hwpx_automation/ops_services/transactions.py +616 -0
  181. hwpx_automation/preview_output_models.py +69 -0
  182. hwpx_automation/public-modules.json +206 -0
  183. hwpx_automation/py.typed +1 -0
  184. hwpx_automation/quality.py +351 -0
  185. hwpx_automation/quality_generation.py +725 -0
  186. hwpx_automation/runtime.py +321 -0
  187. hwpx_automation/runtime_services.py +100 -0
  188. hwpx_automation/server.py +259 -0
  189. hwpx_automation/storage.py +747 -0
  190. hwpx_automation/tool_bindings.py +170 -0
  191. hwpx_automation/tool_contract.py +982 -0
  192. hwpx_automation/upstream.py +755 -0
  193. hwpx_automation/utils/__init__.py +2 -0
  194. hwpx_automation/utils/helpers.py +29 -0
  195. hwpx_automation/visual_qa.py +667 -0
  196. hwpx_automation/workflow/__init__.py +55 -0
  197. hwpx_automation/workflow/adapters.py +482 -0
  198. hwpx_automation/workflow/dispatcher.py +213 -0
  199. hwpx_automation/workflow/models.py +243 -0
  200. hwpx_automation/workflow/policy.py +198 -0
  201. hwpx_automation/workflow/render_contracts.py +173 -0
  202. hwpx_automation/workflow/render_metrics.py +196 -0
  203. hwpx_automation/workflow/render_queue.py +482 -0
  204. hwpx_automation/workflow/render_security.py +172 -0
  205. hwpx_automation/workflow/render_transport.py +369 -0
  206. hwpx_automation/workflow/rendering.py +206 -0
  207. hwpx_automation/workflow/service.py +758 -0
  208. hwpx_automation/workflow/state_machine.py +65 -0
  209. hwpx_automation/workflow/store.py +747 -0
  210. hwpx_automation/workspace.py +1694 -0
  211. python_hwpx_automation-6.0.3.dist-info/METADATA +279 -0
  212. python_hwpx_automation-6.0.3.dist-info/RECORD +217 -0
  213. python_hwpx_automation-6.0.3.dist-info/WHEEL +5 -0
  214. python_hwpx_automation-6.0.3.dist-info/entry_points.txt +3 -0
  215. python_hwpx_automation-6.0.3.dist-info/licenses/LICENSE +178 -0
  216. python_hwpx_automation-6.0.3.dist-info/licenses/NOTICE +14 -0
  217. python_hwpx_automation-6.0.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,407 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Fixture-only blind benchmark receipts and provenance-safe exports.
3
+
4
+ The benchmark coordinator is intentionally provider-neutral. It validates
5
+ pre-recorded workflow receipts and fixture judgments; it does not contain a
6
+ benchmark-only document creation path. Fixture receipts are non-promotable
7
+ and can never substantiate human, real-agent, or real-Hancom claims.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import json
14
+ import shutil
15
+ from datetime import datetime, timezone
16
+ from pathlib import Path
17
+ from typing import Any, Mapping
18
+
19
+
20
+ FIXTURE_MANIFEST_SCHEMA = "hwpx.blind-benchmark.fixture-manifest/v1"
21
+ FIXTURE_RESULT_SCHEMA = "hwpx.blind-benchmark.fixture-result/v1"
22
+ FIXTURE_RECEIPT_SCHEMA = "hwpx.blind-benchmark.fixture-receipt/v1"
23
+ PUBLIC_EXPORT_SCHEMA = "hwpx.blind-benchmark.fixture-export/v1"
24
+ FIXTURE_STATUS = "fixture_evidence_only_real_environment_unverified"
25
+ _SHA256_PREFIX = "sha256:"
26
+
27
+
28
+ def _canonical_hash(value: Any) -> str:
29
+ raw = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()
30
+ return _SHA256_PREFIX + hashlib.sha256(raw).hexdigest()
31
+
32
+
33
+ def _file_hash(path: Path) -> str:
34
+ return _SHA256_PREFIX + hashlib.sha256(path.read_bytes()).hexdigest()
35
+
36
+
37
+ def _require_hash(value: Any, field: str) -> str:
38
+ text = str(value or "")
39
+ if not text.startswith(_SHA256_PREFIX) or len(text) != 71:
40
+ raise ValueError(f"{field} must be a sha256 digest")
41
+ try:
42
+ int(text.removeprefix(_SHA256_PREFIX), 16)
43
+ except ValueError as exc:
44
+ raise ValueError(f"{field} must be a sha256 digest") from exc
45
+ return text
46
+
47
+
48
+ def _require_list(value: Any, field: str) -> list[Any]:
49
+ if not isinstance(value, list):
50
+ raise ValueError(f"{field} must be an array")
51
+ return value
52
+
53
+
54
+ def _require_mapping(value: Any, field: str) -> Mapping[str, Any]:
55
+ if not isinstance(value, Mapping):
56
+ raise ValueError(f"{field} must be an object")
57
+ return value
58
+
59
+
60
+ def _unique_records(records: list[Any], key: str, field: str) -> dict[str, Mapping[str, Any]]:
61
+ result: dict[str, Mapping[str, Any]] = {}
62
+ for item in records:
63
+ record = _require_mapping(item, field)
64
+ identity = str(record.get(key, "")).strip()
65
+ if not identity:
66
+ raise ValueError(f"{field}.{key} is required")
67
+ if identity in result:
68
+ raise ValueError(f"duplicate {field}.{key}: {identity}")
69
+ result[identity] = record
70
+ return result
71
+
72
+
73
+ def load_fixture_manifest(path: str | Path) -> tuple[Path, dict[str, Any]]:
74
+ manifest_path = Path(path).expanduser().resolve()
75
+ payload = json.loads(manifest_path.read_text(encoding="utf-8"))
76
+ if not isinstance(payload, dict) or payload.get("schema") != FIXTURE_MANIFEST_SCHEMA:
77
+ raise ValueError("unsupported blind benchmark fixture manifest schema")
78
+ if payload.get("assurance") != "fixture":
79
+ raise ValueError("blind benchmark fixture assurance must be fixture")
80
+ if payload.get("frozen") is not True:
81
+ raise ValueError("blind benchmark fixture manifest must be frozen")
82
+ return manifest_path, payload
83
+
84
+
85
+ def _validate_workflow_receipt(receipt: Any, run_id: str, client_id: str) -> list[str]:
86
+ failures: list[str] = []
87
+ data = _require_mapping(receipt, f"runs[{run_id}].workflowReceipt")
88
+ if not str(data.get("schemaVersion", "")).startswith("hwpx.workflow."):
89
+ failures.append(f"{run_id}:unsupported_workflow_receipt")
90
+ if not data.get("workflowId"):
91
+ failures.append(f"{run_id}:missing_workflow_provenance")
92
+ if not data.get("toolSpecHash"):
93
+ failures.append(f"{run_id}:missing_tool_spec_hash")
94
+ versions = data.get("versions")
95
+ automation_version = (
96
+ versions.get("automation") or versions.get("mcp")
97
+ if isinstance(versions, Mapping)
98
+ else None
99
+ )
100
+ if (
101
+ not isinstance(versions, Mapping)
102
+ or not automation_version
103
+ or not versions.get("pythonHwpx")
104
+ ):
105
+ failures.append(f"{run_id}:missing_stack_versions")
106
+ if data.get("terminal") is not True:
107
+ failures.append(f"{run_id}:workflow_not_terminal")
108
+ if data.get("state") not in {"completed", "needs_review", "failed", "cancelled"}:
109
+ failures.append(f"{run_id}:invalid_terminal_state")
110
+ provenance = data.get("benchmarkProvenance")
111
+ if not isinstance(provenance, Mapping) or provenance.get("clientId") != client_id:
112
+ failures.append(f"{run_id}:client_provenance_mismatch")
113
+ return failures
114
+
115
+
116
+ def _validate_anonymization(run: Mapping[str, Any], run_id: str) -> tuple[str | None, list[str]]:
117
+ failures: list[str] = []
118
+ evidence = run.get("anonymizationEvidence")
119
+ if not isinstance(evidence, Mapping):
120
+ return None, [f"{run_id}:missing_anonymization_evidence"]
121
+ opaque_id = str(evidence.get("opaqueArtifactId", "")).strip()
122
+ if not opaque_id or not opaque_id.startswith("artifact-"):
123
+ failures.append(f"{run_id}:invalid_opaque_artifact_id")
124
+ if evidence.get("metadataScanComplete") is not True:
125
+ failures.append(f"{run_id}:metadata_scan_incomplete")
126
+ if evidence.get("revealingMetadataKeys") != []:
127
+ failures.append(f"{run_id}:revealing_metadata_present")
128
+ if not evidence.get("scannerId") or not evidence.get("scannerVersion"):
129
+ failures.append(f"{run_id}:missing_anonymization_scanner_provenance")
130
+ try:
131
+ _require_hash(evidence.get("evidenceHash"), "anonymizationEvidence.evidenceHash")
132
+ except ValueError:
133
+ failures.append(f"{run_id}:invalid_anonymization_evidence_hash")
134
+ private_tokens = {
135
+ str(run.get("clientId", "")).lower(),
136
+ str(_require_mapping(run.get("workflowReceipt"), "workflowReceipt").get("workflowId", "")).lower(),
137
+ }
138
+ if opaque_id and any(token and token in opaque_id.lower() for token in private_tokens):
139
+ failures.append(f"{run_id}:opaque_id_reveals_provenance")
140
+ return opaque_id or None, failures
141
+
142
+
143
+ def run_fixture_benchmark(
144
+ manifest_path: str | Path,
145
+ *,
146
+ output_dir: str | Path | None = None,
147
+ strict: bool = True,
148
+ ) -> dict[str, Any]:
149
+ """Validate a frozen fixture benchmark and emit a non-promotable receipt."""
150
+
151
+ source, manifest = load_fixture_manifest(manifest_path)
152
+ benchmark_id = str(manifest.get("benchmarkId", "")).strip()
153
+ if not benchmark_id:
154
+ raise ValueError("benchmarkId is required")
155
+ work_orders = _unique_records(_require_list(manifest.get("workOrders"), "workOrders"), "workOrderId", "workOrders")
156
+ clients = _unique_records(_require_list(manifest.get("clients"), "clients"), "clientId", "clients")
157
+ runs = _unique_records(_require_list(manifest.get("runs"), "runs"), "runId", "runs")
158
+ judgments = _require_list(manifest.get("judgments"), "judgments")
159
+ failures: list[str] = []
160
+
161
+ for work_order_id, work_order in work_orders.items():
162
+ if not work_order.get("family"):
163
+ failures.append(f"{work_order_id}:missing_work_family")
164
+ try:
165
+ declared_brief_hash = _require_hash(work_order.get("taskBriefHash"), "taskBriefHash")
166
+ except ValueError:
167
+ failures.append(f"{work_order_id}:invalid_task_brief_hash")
168
+ else:
169
+ if declared_brief_hash != _canonical_hash(work_order.get("taskBrief")):
170
+ failures.append(f"{work_order_id}:task_brief_hash_mismatch")
171
+
172
+ if strict and len(clients) < 3:
173
+ failures.append("fewer_than_three_fixture_client_profiles")
174
+ adapter_families = {str(item.get("adapterFamily", "")).strip() for item in clients.values()}
175
+ if "" in adapter_families:
176
+ failures.append("missing_client_adapter_family")
177
+ if strict and len(adapter_families) < 3:
178
+ failures.append("fixture_client_profiles_not_materially_distinct")
179
+
180
+ expected_pairs = {(work_order_id, client_id) for work_order_id in work_orders for client_id in clients}
181
+ observed_pairs: set[tuple[str, str]] = set()
182
+ opaque_ids: list[str] = []
183
+ normalized_runs: list[dict[str, Any]] = []
184
+ for run_id, run in runs.items():
185
+ work_order_id = str(run.get("workOrderId", ""))
186
+ client_id = str(run.get("clientId", ""))
187
+ if work_order_id not in work_orders:
188
+ failures.append(f"{run_id}:unknown_work_order")
189
+ if client_id not in clients:
190
+ failures.append(f"{run_id}:unknown_client")
191
+ pair = (work_order_id, client_id)
192
+ if pair in observed_pairs:
193
+ failures.append(f"{run_id}:duplicate_work_order_client_pair")
194
+ observed_pairs.add(pair)
195
+ failures.extend(_validate_workflow_receipt(run.get("workflowReceipt"), run_id, client_id))
196
+ artifact = _require_mapping(run.get("artifact"), f"runs[{run_id}].artifact")
197
+ artifact_path = (source.parent / str(artifact.get("path", ""))).resolve()
198
+ try:
199
+ expected_hash = _require_hash(artifact.get("contentHash"), "artifact.contentHash")
200
+ except ValueError:
201
+ expected_hash = ""
202
+ failures.append(f"{run_id}:invalid_artifact_hash")
203
+ if not artifact_path.is_file():
204
+ failures.append(f"{run_id}:artifact_missing")
205
+ elif expected_hash and _file_hash(artifact_path) != expected_hash:
206
+ failures.append(f"{run_id}:artifact_hash_mismatch")
207
+ opaque_id, anonymization_failures = _validate_anonymization(run, run_id)
208
+ failures.extend(anonymization_failures)
209
+ if opaque_id:
210
+ opaque_ids.append(opaque_id)
211
+ normalized_runs.append(
212
+ {
213
+ "runId": run_id,
214
+ "workOrderId": work_order_id,
215
+ "clientId": client_id,
216
+ "artifactPath": str(artifact_path),
217
+ "artifactContentHash": expected_hash,
218
+ "opaqueArtifactId": opaque_id,
219
+ "workflowReceipt": run.get("workflowReceipt"),
220
+ "anonymizationEvidence": run.get("anonymizationEvidence"),
221
+ }
222
+ )
223
+ missing_pairs = sorted(expected_pairs - observed_pairs)
224
+ if missing_pairs:
225
+ failures.append("incomplete_work_order_client_coverage")
226
+ if len(set(opaque_ids)) != len(opaque_ids):
227
+ failures.append("duplicate_opaque_artifact_id")
228
+
229
+ judgments_by_artifact: dict[str, list[Mapping[str, Any]]] = {item: [] for item in opaque_ids}
230
+ for raw in judgments:
231
+ judgment = _require_mapping(raw, "judgments")
232
+ artifact_id = str(judgment.get("opaqueArtifactId", ""))
233
+ if artifact_id not in judgments_by_artifact:
234
+ failures.append("judgment_for_unknown_artifact")
235
+ continue
236
+ if judgment.get("reviewerType") != "fixture_agent_judge" or judgment.get("humanLabels") is not False:
237
+ failures.append(f"{artifact_id}:judgment_provenance_misrepresented")
238
+ if not judgment.get("reviewerId"):
239
+ failures.append(f"{artifact_id}:missing_reviewer_id")
240
+ if judgment.get("provenanceVisible") is not False:
241
+ failures.append(f"{artifact_id}:judge_was_not_blinded")
242
+ judgments_by_artifact[artifact_id].append(judgment)
243
+ for artifact_id, artifact_judgments in judgments_by_artifact.items():
244
+ reviewers = {str(item.get("reviewerId", "")) for item in artifact_judgments}
245
+ if len(reviewers) < 2:
246
+ failures.append(f"{artifact_id}:fewer_than_two_fixture_judges")
247
+
248
+ coverage_complete = not missing_pairs and len(observed_pairs) == len(expected_pairs)
249
+ provenance_complete = not any("provenance" in reason or "stack_versions" in reason or "tool_spec" in reason for reason in failures)
250
+ anonymization_complete = not any("anonym" in reason or "metadata" in reason or "opaque" in reason for reason in failures)
251
+ judgment_coverage_complete = not any("judge" in reason or "judgment" in reason or "reviewer" in reason for reason in failures)
252
+ accepted = sum(
253
+ 1
254
+ for values in judgments_by_artifact.values()
255
+ if values and all(item.get("acceptedWithoutManualHwpxEdit") is True for item in values)
256
+ )
257
+ receipt = {
258
+ "schemaVersion": FIXTURE_RECEIPT_SCHEMA,
259
+ "receiptKind": "deterministic_fixture_benchmark",
260
+ "benchmarkId": benchmark_id,
261
+ "manifestHash": _canonical_hash(manifest),
262
+ "evaluatedAt": datetime.now(timezone.utc).isoformat(),
263
+ "workOrderCount": len(work_orders),
264
+ "fixtureClientProfileCount": len(clients),
265
+ "runCount": len(runs),
266
+ "coverageComplete": coverage_complete,
267
+ "provenanceComplete": provenance_complete,
268
+ "anonymizationComplete": anonymization_complete,
269
+ "judgmentCoverageComplete": judgment_coverage_complete,
270
+ "humanControls": False,
271
+ "humanJudges": False,
272
+ "realAgentClients": False,
273
+ "realHancomVerified": False,
274
+ "renderChecked": False,
275
+ "replacementClaimAllowed": False,
276
+ "verificationStatus": FIXTURE_STATUS,
277
+ }
278
+ result = {
279
+ "schema": FIXTURE_RESULT_SCHEMA,
280
+ "ok": not failures,
281
+ "strict": strict,
282
+ "receipt": receipt,
283
+ "metrics": {
284
+ "fixtureArtifactsAcceptedWithoutManualEdit": accepted,
285
+ "fixtureArtifactCount": len(opaque_ids),
286
+ "fixtureAcceptanceRate": accepted / len(opaque_ids) if opaque_ids else None,
287
+ "replacementThresholdEvaluated": False,
288
+ },
289
+ "failReasons": sorted(set(failures)),
290
+ "privateRuns": normalized_runs,
291
+ "workOrders": list(work_orders.values()),
292
+ "judgments": judgments,
293
+ }
294
+ if output_dir is not None:
295
+ target_dir = Path(output_dir).expanduser().resolve()
296
+ target_dir.mkdir(parents=True, exist_ok=True)
297
+ target = target_dir / "fixture-benchmark-result.json"
298
+ target.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
299
+ result["resultManifestPath"] = str(target)
300
+ return result
301
+
302
+
303
+ def export_fixture_benchmark(
304
+ result_manifest_path: str | Path,
305
+ *,
306
+ output_dir: str | Path,
307
+ strict: bool = True,
308
+ ) -> dict[str, Any]:
309
+ """Export an opaque fixture judge bundle without private run provenance."""
310
+
311
+ source = Path(result_manifest_path).expanduser().resolve()
312
+ result = json.loads(source.read_text(encoding="utf-8"))
313
+ if not isinstance(result, dict) or result.get("schema") != FIXTURE_RESULT_SCHEMA:
314
+ raise ValueError("unsupported blind benchmark fixture result schema")
315
+ receipt = _require_mapping(result.get("receipt"), "receipt")
316
+ required_true = ("coverageComplete", "provenanceComplete", "anonymizationComplete", "judgmentCoverageComplete")
317
+ failures = list(result.get("failReasons", []))
318
+ if result.get("ok") is not True or any(receipt.get(field) is not True for field in required_true):
319
+ failures.append("fixture_result_not_exportable")
320
+ if any(receipt.get(field) is not False for field in ("humanControls", "humanJudges", "realAgentClients", "realHancomVerified", "renderChecked")):
321
+ failures.append("fixture_receipt_contains_promoted_claim")
322
+ if failures and strict:
323
+ raise ValueError("fixture benchmark export failed closed: " + ", ".join(sorted(set(failures))))
324
+
325
+ destination = Path(output_dir).expanduser().resolve()
326
+ artifacts_dir = destination / "artifacts"
327
+ artifacts_dir.mkdir(parents=True, exist_ok=True)
328
+ public_artifacts: list[dict[str, Any]] = []
329
+ for raw in _require_list(result.get("privateRuns"), "privateRuns"):
330
+ run = _require_mapping(raw, "privateRuns")
331
+ opaque_id = str(run.get("opaqueArtifactId", ""))
332
+ source_artifact = Path(str(run.get("artifactPath", ""))).resolve()
333
+ expected_hash = _require_hash(run.get("artifactContentHash"), "artifactContentHash")
334
+ if not opaque_id.startswith("artifact-") or not source_artifact.is_file() or _file_hash(source_artifact) != expected_hash:
335
+ raise ValueError("artifact provenance changed after benchmark validation")
336
+ suffix = source_artifact.suffix.lower()
337
+ if suffix not in {".hwpx", ".pdf", ".png", ".json"}:
338
+ raise ValueError("fixture benchmark artifact type is not allow-listed")
339
+ target = artifacts_dir / f"{opaque_id}{suffix}"
340
+ shutil.copy2(source_artifact, target)
341
+ public_artifacts.append(
342
+ {"opaqueArtifactId": opaque_id, "path": f"artifacts/{target.name}", "contentHash": expected_hash}
343
+ )
344
+
345
+ work_orders = []
346
+ for raw in _require_list(result.get("workOrders"), "workOrders"):
347
+ item = _require_mapping(raw, "workOrders")
348
+ work_orders.append(
349
+ {
350
+ "workOrderId": item.get("workOrderId"),
351
+ "family": item.get("family"),
352
+ "taskBrief": item.get("taskBrief"),
353
+ "taskBriefHash": _require_hash(item.get("taskBriefHash"), "taskBriefHash"),
354
+ }
355
+ )
356
+ public_judgments = []
357
+ allowed_judgment_fields = {
358
+ "opaqueArtifactId", "reviewerId", "reviewerType", "humanLabels", "provenanceVisible",
359
+ "acceptedWithoutManualHwpxEdit", "rubricScores", "manualEditMinutes", "abstentionCorrect",
360
+ "criticalFailure", "adjudicationId",
361
+ }
362
+ forbidden_judgment_fields = {"clientId", "workflowId", "artifactPath", "sourcePath", "filename", "provenance"}
363
+ for raw in _require_list(result.get("judgments"), "judgments"):
364
+ judgment = _require_mapping(raw, "judgments")
365
+ if forbidden_judgment_fields.intersection(judgment):
366
+ raise ValueError("judgment contains private provenance")
367
+ public_judgments.append({key: value for key, value in judgment.items() if key in allowed_judgment_fields})
368
+ public = {
369
+ "schema": PUBLIC_EXPORT_SCHEMA,
370
+ "assurance": "fixture",
371
+ "benchmarkId": receipt.get("benchmarkId"),
372
+ "sourceResultHash": _canonical_hash(result),
373
+ "receipt": dict(receipt),
374
+ "workOrders": work_orders,
375
+ "artifacts": public_artifacts,
376
+ "judgments": public_judgments,
377
+ "privateProvenanceIncluded": False,
378
+ }
379
+ manifest_target = destination / "blind-fixture-benchmark.json"
380
+ manifest_target.write_text(json.dumps(public, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
381
+ return {
382
+ "ok": not failures,
383
+ "schemaVersion": PUBLIC_EXPORT_SCHEMA,
384
+ "outputPath": str(manifest_target),
385
+ "artifactCount": len(public_artifacts),
386
+ "privateProvenanceIncluded": False,
387
+ "humanControls": False,
388
+ "humanJudges": False,
389
+ "realAgentClients": False,
390
+ "realHancomVerified": False,
391
+ "renderChecked": False,
392
+ "replacementClaimAllowed": False,
393
+ "verificationStatus": FIXTURE_STATUS,
394
+ "failReasons": sorted(set(failures)),
395
+ }
396
+
397
+
398
+ __all__ = [
399
+ "FIXTURE_MANIFEST_SCHEMA",
400
+ "FIXTURE_RECEIPT_SCHEMA",
401
+ "FIXTURE_RESULT_SCHEMA",
402
+ "FIXTURE_STATUS",
403
+ "PUBLIC_EXPORT_SCHEMA",
404
+ "export_fixture_benchmark",
405
+ "load_fixture_manifest",
406
+ "run_fixture_benchmark",
407
+ ]
@@ -0,0 +1,110 @@
1
+ """Task-oriented capability report derived from the FastMCP ToolSpec contract."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from .tool_contract import (
8
+ DOMAIN_SPECS,
9
+ TOOL_SPECS,
10
+ bound_tool_registry,
11
+ contract_hash,
12
+ expected_tool_names,
13
+ )
14
+
15
+
16
+ # Backward-compatible data shape for callers that imported DOMAINS directly.
17
+ DOMAINS: list[dict[str, Any]] = [
18
+ {
19
+ "key": domain.key,
20
+ "title": domain.title,
21
+ "intent": domain.intent,
22
+ "whenToUse": domain.when_to_use,
23
+ "tools": list(domain.tools),
24
+ }
25
+ for domain in DOMAIN_SPECS
26
+ ]
27
+
28
+
29
+ def build_capability_report(
30
+ domain: str | None = None,
31
+ *,
32
+ advanced: bool = False,
33
+ ) -> dict[str, Any]:
34
+ """Return only tools callable on the active FastMCP profile."""
35
+
36
+ active = expected_tool_names(advanced=advanced)
37
+ specs = {spec.name: spec for spec in TOOL_SPECS}
38
+ bound = bound_tool_registry().by_name()
39
+ selected = DOMAIN_SPECS if domain is None else tuple(item for item in DOMAIN_SPECS if item.key == domain)
40
+ out_domains: list[dict[str, Any]] = []
41
+ for item in selected:
42
+ tools = [name for name in item.tools if name in active]
43
+ if not tools and domain is None:
44
+ continue
45
+ out_domains.append(
46
+ {
47
+ "key": item.key,
48
+ "title": item.title,
49
+ "intent": item.intent,
50
+ "whenToUse": item.when_to_use,
51
+ "toolCount": len(tools),
52
+ "tools": tools,
53
+ "toolDetails": [
54
+ {
55
+ "name": name,
56
+ "lifecycle": specs[name].classification.value,
57
+ "profile": specs[name].profile.value,
58
+ "availability": bound[name].availability.value,
59
+ "mutates": specs[name].mutates,
60
+ "tags": list(specs[name].tags),
61
+ "replacementTools": list(specs[name].replacement_tools),
62
+ }
63
+ for name in tools
64
+ ],
65
+ }
66
+ )
67
+ return {
68
+ "domains": out_domains,
69
+ "domainCount": len(out_domains),
70
+ "profile": "advanced" if advanced else "default",
71
+ "contractHash": contract_hash(),
72
+ "note": (
73
+ "실제 FastMCP 등록 표면의 작업군 지도. isolated legacy testkit와 internal fixture QA는 "
74
+ "호출 가능 도구 수에 포함하지 않는다. 양식 채움은 analyze_form_fill → apply_form_fill → "
75
+ "verify_form_fill canonical 경로를 따른다."
76
+ ),
77
+ }
78
+
79
+
80
+ def coverage_against(
81
+ tool_names: set[str],
82
+ *,
83
+ advanced: bool = False,
84
+ registry_validation: dict[str, Any] | None = None,
85
+ ) -> dict[str, Any]:
86
+ """Compare a live registry to the exact active ToolSpec set."""
87
+
88
+ expected = expected_tool_names(advanced=advanced)
89
+ missing = sorted(expected - tool_names)
90
+ unexpected = sorted(tool_names - expected)
91
+ registry_ok = True if registry_validation is None else bool(registry_validation.get("ok"))
92
+ return {
93
+ "missingExpected": missing,
94
+ "unexpectedRegistered": unexpected,
95
+ # Compatibility keys retained for older health consumers/tests.
96
+ "unmapped": unexpected,
97
+ "mappedNotRegistered": missing,
98
+ "registryValidation": registry_validation,
99
+ "ok": not missing and not unexpected and registry_ok,
100
+ }
101
+
102
+
103
+ def tool_domain(name: str) -> str | None:
104
+ for spec in TOOL_SPECS:
105
+ if spec.name == name:
106
+ return spec.domain
107
+ return None
108
+
109
+
110
+ __all__ = ["DOMAINS", "build_capability_report", "coverage_against", "tool_domain"]
@@ -0,0 +1,48 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """외부 라이브러리 호환성 패치."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import builtins
7
+ from typing import Any
8
+ from xml.etree import ElementTree as _ET
9
+
10
+
11
+ _SUBELEMENT_PATCHED = False
12
+
13
+
14
+ def _patch_sub_element_for_lxml_parent() -> None:
15
+ global _SUBELEMENT_PATCHED
16
+ if _SUBELEMENT_PATCHED:
17
+ return
18
+
19
+ original_sub_element = _ET.SubElement
20
+
21
+ def _safe_sub_element(parent: Any, tag: str, attrib: Any = None, **extra: Any):
22
+ attrs = {} if attrib is None else dict(attrib)
23
+ attrs.update(extra)
24
+ try:
25
+ return original_sub_element(parent, tag, attrs)
26
+ except TypeError as exc:
27
+ message = str(exc)
28
+ if "lxml.etree._Element" not in message or not hasattr(parent, "makeelement"):
29
+ raise
30
+ child = parent.makeelement(tag, attrs)
31
+ parent.append(child)
32
+ return child
33
+
34
+ _ET.SubElement = _safe_sub_element # type: ignore[assignment]
35
+ _SUBELEMENT_PATCHED = True
36
+
37
+
38
+ def patch_python_hwpx() -> None:
39
+ """python-hwpx의 ET 누락/혼합 파서 호환 이슈를 보정합니다.
40
+
41
+ python-hwpx 2.9.1부터 id 생성기는 업스트림에서 signed int32 범위로 정리됐기 때문에
42
+ 해당 사용자별 shim은 제거됐습니다. ``document.py`` 내에 남은 stdlib ``ET.SubElement``
43
+ 호출 경로에는 여전히 본 shim을 적용해 혼합 파서로 인한 ``TypeError``를 피합니다.
44
+ """
45
+
46
+ if not hasattr(builtins, "ET"):
47
+ builtins.ET = _ET
48
+ _patch_sub_element_for_lxml_parent()
@@ -0,0 +1,60 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Canonical automation configuration with bounded 6.x MCP fallbacks."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import os
7
+
8
+ CANONICAL_ENV_PREFIX = "HWPX_AUTOMATION_"
9
+ LEGACY_ENV_PREFIX = "HWPX_MCP_"
10
+
11
+
12
+ def canonical_env_name(suffix: str) -> str:
13
+ return f"{CANONICAL_ENV_PREFIX}{suffix}"
14
+
15
+
16
+ def legacy_env_name(suffix: str) -> str:
17
+ return f"{LEGACY_ENV_PREFIX}{suffix}"
18
+
19
+
20
+ def env_value(suffix: str, default: str | None = None) -> str | None:
21
+ """Read canonical configuration first, then the supported 6.x legacy key."""
22
+
23
+ canonical = os.environ.get(canonical_env_name(suffix))
24
+ if canonical is not None:
25
+ return canonical
26
+ legacy = os.environ.get(legacy_env_name(suffix))
27
+ if legacy is not None:
28
+ return legacy
29
+ return default
30
+
31
+
32
+ def env_int(suffix: str, default: int) -> int:
33
+ raw = env_value(suffix)
34
+ if raw is None:
35
+ return default
36
+ try:
37
+ return int(raw)
38
+ except ValueError:
39
+ return default
40
+
41
+
42
+ def env_float(suffix: str, default: float) -> float:
43
+ raw = env_value(suffix)
44
+ if raw is None:
45
+ return default
46
+ try:
47
+ return float(raw)
48
+ except ValueError:
49
+ return default
50
+
51
+
52
+ __all__ = [
53
+ "CANONICAL_ENV_PREFIX",
54
+ "LEGACY_ENV_PREFIX",
55
+ "canonical_env_name",
56
+ "env_float",
57
+ "env_int",
58
+ "env_value",
59
+ "legacy_env_name",
60
+ ]
@@ -0,0 +1,2 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Stateless HWPX 코어 연산."""