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,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
|
+
]
|